JezK
Edit File: imagify-technical-deep-dive.md
# Imagify — Engineering Deep Dive > Version 2.2.8 · PHP 7.3+ · WordPress 5.3+ · PSR-4 · League Container · ActionScheduler > > Complete process-level reference for plugin engineers. Class hierarchies, call flows, DB schemas, hook signatures, API structures, and concurrency details. --- ## Table of Contents 1. [Architecture & Bootstrapping](#1-architecture--bootstrapping) 2. [Namespace & PSR-4 Structure](#2-namespace--psr-4-structure) 3. [Optimization Process — Class Hierarchy & Call Flow](#3-optimization-process--class-hierarchy--call-flow) 4. [Optimization\File — Method Signatures](#4-optimizationfile--method-signatures) 5. [API Client — Endpoints & Request/Response](#5-api-client--endpoints--requestresponse) 6. [WordPress Postmeta Keys & Data Structures](#6-wordpress-postmeta-keys--data-structures) 7. [Settings — Option Keys, Types & Defaults](#7-settings--option-keys-types--defaults) 8. [Database Schemas](#8-database-schemas) 9. [Bulk Optimization — ActionScheduler Integration](#9-bulk-optimization--actionscheduler-integration) 10. [Concurrency & Locking Mechanisms](#10-concurrency--locking-mechanisms) 11. [Picture\Display — Output Buffer HTML Rewrite](#11-picturedisplay--output-buffer-html-rewrite) 12. [AJAX & Admin-Post — Full Security Table](#12-ajax--admin-post--full-security-table) 13. [WP-CLI Commands — Full Signatures](#13-wp-cli-commands--full-signatures) 14. [Developer Hooks — Exact Signatures & Parameter Types](#14-developer-hooks--exact-signatures--parameter-types) 15. [Scheduled Tasks — Cron & ActionScheduler](#15-scheduled-tasks--cron--actionscheduler) 16. [Multisite Handling](#16-multisite-handling) 17. [NextGEN Gallery Integration](#17-nextgen-gallery-integration) 18. [Third-Party Integrations](#18-third-party-integrations) 19. [Quota & Account Management](#19-quota--account-management) 20. [Roles & Capabilities](#20-roles--capabilities) 21. [Troubleshooting Tools — InternalStateList & Reset](#21-troubleshooting-tools--internalstatelist--reset) 22. [Error Handling Paths](#22-error-handling-paths) 23. [Filesystem Operations & Paths](#23-filesystem-operations--paths) --- ## 1. Architecture & Bootstrapping *Entry point, constants, DI container, service provider chain, and init sequence.* ### Entry Point — `imagify.php` WordPress loads `imagify.php` during the `plugins_loaded` phase. It defines all constants and registers activation/deactivation hooks before delegating to `inc/main.php`. ```php // imagify.php — constants defined at plugin load time define( 'IMAGIFY_VERSION', '2.2.8' ); define( 'IMAGIFY_SLUG', 'imagify' ); define( 'IMAGIFY_FILE', __FILE__ ); define( 'IMAGIFY_PATH', realpath( plugin_dir_path( IMAGIFY_FILE ) ) . '/' ); define( 'IMAGIFY_URL', plugin_dir_url( IMAGIFY_FILE ) ); define( 'IMAGIFY_ASSETS_IMG_URL', IMAGIFY_URL . 'assets/images/' ); define( 'IMAGIFY_MAX_BYTES', 5242880 ); // 5 MB hard limit per image define( 'IMAGIFY_INT_MAX', PHP_INT_MAX - 30 ); define( 'IMAGIFY_SITE_DOMAIN', 'https://imagify.io' ); define( 'IMAGIFY_APP_DOMAIN', 'https://app.imagify.io' ); define( 'IMAGIFY_APP_API_URL', IMAGIFY_APP_DOMAIN . '/api/' ); ``` ### Bootstrap Sequence `plugins_loaded` → `imagify_init()` → `vendor/autoload.php` → `new Plugin(Container, args)` → `Plugin::init($providers)` → `do_action('imagify_loaded')` `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: ```php // classes/Plugin.php — init sequence (abridged) public function init( array $providers ): void { // 1. Register shared services $this->container->addShared( 'event_manager', fn() => new EventManager() ); $this->container->addShared( 'filesystem', fn() => new Imagify_Filesystem() ); // 2. Include procedural files (functions/, common/, 3rd-party/) $this->include_files(); // 3. Init legacy singletons Imagify_Auto_Optimization::get_instance()->init(); Imagify_Options::get_instance()->init(); Imagify_Data::get_instance()->init(); Imagify_Folders_DB::get_instance()->init(); Imagify_Files_DB::get_instance()->init(); Imagify_Cron_Library_Size::get_instance()->init(); Imagify_Cron_Rating::get_instance()->init(); Imagify_Cron_Sync_Files::get_instance()->init(); Imagify\Auth\Basic::get_instance()->init(); Imagify\Job\MediaOptimization::get_instance()->init(); Bulk::get_instance()->init(); // 4. Admin-only classes if ( is_admin() ) { ... } // 5. Register PSR-4 service providers + subscribers foreach ( $providers as $service_provider ) { $this->container->addServiceProvider( new $service_provider() ); $this->load_subscribers( $provider_instance ); } do_action( 'imagify_loaded', $this ); } ``` ### Activation / Deactivation Hooks | Hook | Handler | What it does | |------|---------|-------------| | `register_activation_hook` | `imagify_set_activation()` | Sets transient `imagify_activation` with current user ID (TTL 30s). On network: `set_site_transient`. | | `register_deactivation_hook` | `imagify_deactivation()` | Deletes `imagify_check_api_version` and `imagify_check_licence_1` site transients; fires `imagify_deactivation` action. | | `init` (Plugin) | `Plugin::maybe_activate()` | Reads activation transient; fires `imagify_activation` action with user ID, then deletes transient. | ### Service Providers (`config/providers.php`) | Provider | Description | |----------|-------------| | `Imagify\User\ServiceProvider` | Registers `User` singleton; binds account/quota service. | | `Imagify\Admin\ServiceProvider` | AdminBar, PluginFamily, AdminSubscriber. | | `Imagify\Avif\ServiceProvider` | AVIF rewrite-rule writers for Apache/Nginx/IIS. | | `Imagify\CDN\ServiceProvider` | CDN push integration. | | `Imagify\Picture\ServiceProvider` | Registers `Picture\Display` subscriber for `<picture>` tag rewriting. | | `Imagify\Stats\ServiceProvider` | Stat counters (e.g. `OptimizedMediaWithoutNextGen`). | | `Imagify\Webp\ServiceProvider` | WebP rewrite-rule writers. | | `Imagify\ThirdParty\ServiceProvider` | GravityForms, Extendify; loads all `inc/3rd-party/` integrations. | | `Imagify\Media\ServiceProvider` | Media subscribers, upload handler. | | `Imagify\Tools\ServiceProvider` | Reset internal state tool, troubleshooting subscriber. | --- ## 2. Namespace & PSR-4 Structure *Composer autoload map, directory layout, and naming conventions.* ### PSR-4 Autoload Map (`composer.json`) | Namespace Prefix | Directory | Notes | |-----------------|-----------|-------| | `Imagify\` | `classes/` | Primary PSR-4 root for all modern classes | | `Imagify\Deprecated\Traits\` | `inc/deprecated/Traits/` | Backward compat trait shims | | `Imagify\ThirdParty\AS3CF\` | `inc/3rd-party/amazon-s3-and-cloudfront/classes/` | S3 Offload integration | | `Imagify\ThirdParty\EnableMediaReplace\` | `inc/3rd-party/enable-media-replace/classes/` | Enable Media Replace compat | | `Imagify\ThirdParty\FormidablePro\` | `inc/3rd-party/formidable-pro/classes/` | Formidable Forms compat | | `Imagify\ThirdParty\NGG\` | `inc/3rd-party/nextgen-gallery/classes/` | NextGEN Gallery integration | | `Imagify\ThirdParty\RegenerateThumbnails\` | `inc/3rd-party/regenerate-thumbnails/classes/` | Regenerate Thumbnails compat | | `Imagify\ThirdParty\WPRocket\` | `inc/3rd-party/wp-rocket/classes/` | WP Rocket compat | ### Classmap (Legacy, Non-PSR-4) `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). ### Key `classes/` Sub-namespaces | Namespace | Classes | |-----------|---------| | `Imagify\Bulk\` | `Bulk`, `BulkInterface`, `AbstractBulk`, `WP`, `CustomFolders`, `Noop` | | `Imagify\CLI\` | `AbstractCommand`, `BulkOptimizeCommand`, `RestoreCommand`, `GenerateMissingNextgenCommand` | | `Imagify\Context\` | `ContextInterface`, `AbstractContext`, `WP`, `CustomFolders`, `Noop` | | `Imagify\Media\` | `MediaInterface`, `AbstractMedia`, `WP`, `CustomFolders`, `Noop` | | `Imagify\Optimization\` | `File`, `Process\{AbstractProcess, WP, CustomFolders, Noop}`, `Data\{AbstractData, WP, CustomFolders, Noop}` | | `Imagify\Picture\` | `Display` (output buffer rewriter) | | `Imagify\Job\` | `MediaOptimization` (background queue worker) | | `Imagify\Tools\` | `InternalStateList`, `ResetInternalState`, `Subscriber` | | `Imagify\Traits\` | `InstanceGetterTrait` (lightweight singleton), `MediaRowTrait` | > **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`. --- ## 3. Optimization Process — Class Hierarchy & Call Flow *Full inheritance chain from context factory to per-file API call.* ### Class Inheritance Chain ``` ProcessInterface // classes/Optimization/Process/ProcessInterface.php └── AbstractProcess // classes/Optimization/Process/AbstractProcess.php (~2100 lines) ├── Process\WP // WP Media Library context ├── Process\CustomFolders // Custom Folders context └── Process\Noop // No-op fallback DataInterface // classes/Optimization/Data/DataInterface.php └── AbstractData ├── Data\WP // stores _imagify_data postmeta ├── Data\CustomFolders // stores in imagify_files table └── Data\Noop MediaInterface // classes/Media/MediaInterface.php └── AbstractMedia ├── Media\WP ├── Media\CustomFolders └── Media\Noop ContextInterface // classes/Context/ContextInterface.php └── AbstractContext ├── Context\WP ├── Context\CustomFolders └── Context\Noop ``` ### Context Factory Functions ```php // inc/functions/common.php imagify_get_context( string $context ): ContextInterface imagify_get_optimization_process( int $media_id, string $context ): ProcessInterface // Context values: 'wp' | 'custom-folders' | 'ngg' (when NGG active) // Filterable via: imagify_context_class_name, imagify_process_class_name ``` ### AbstractProcess — Key Method Signatures | Method | Signature | Description | |--------|-----------|-------------| | `__construct` | `(int\|WP_Post\|MediaInterface $id)` | Accepts attachment ID, WP_Post, or MediaInterface object | | `optimize` | `(?int $optimization_level, array $args = []): bool\|WP_Error` | Main entry for single-media optimization; acquires lock, iterates sizes | | `reoptimize` | `(?int $optimization_level, array $args = []): bool\|WP_Error` | Restore then re-optimize at new level | | `optimize_sizes` | `(array $sizes, ?int $level, array $args = []): bool\|WP_Error` | Push sizes to background job queue | | `optimize_size` | `(string $size, ?int $level): bool\|WP_Error` | Optimize a single named size (e.g. `'full'`, `'thumbnail'`) | | `optimize_missing_thumbnails` | `(): bool\|WP_Error` | Find and optimize sizes missing from postmeta | | `restore` | `(): bool\|WP_Error` | Restore all sizes from backup; acquires restoring lock | | `delete_backup` | `(): bool\|WP_Error` | Remove backup files for this media | | `generate_nextgen_versions` | `(): bool\|WP_Error` | Generate WebP/AVIF variants for all optimized sizes | | `delete_nextgen_files` | `(bool $keep_full = false, bool $all_next_gen = false): void` | Remove WebP/AVIF sidecar files | | `lock` | `(string $action = 'optimizing'): void` | Set transient lock for 10 minutes | | `unlock` | `(): void` | Delete lock transient | | `is_locked` | `(): string\|false` | Returns lock action string or false | | `update_size_optimization_data` | `(object $response, string $size, int $level): void` | Persist API response data for a size | ### Optimization Call Flow — Single Media `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()` ### Per-Size Data Structure Stored ```php // Stored in _imagify_data['sizes'][$size_name] (WP context) // On success: [ 'success' => true, 'original_size' => int, // bytes before optimization 'optimized_size' => int, // bytes after optimization 'percent' => float, // savings percentage (2 decimal places) ] // On error: [ 'success' => false, 'error' => string, // human-readable error message ] ``` --- ## 4. Optimization\File — Method Signatures *Low-level file operations: validation, resize, backup, API call, next-gen path generation.* Class: `Imagify\Optimization\File` — `classes/Optimization/File.php` (931 lines). Injected with `Imagify_Filesystem::get_instance()`. Does not extend anything — purely compositional. ### Constructor & Properties ```php class File { protected string $path; // absolute path to file protected ?bool $is_image; // cached result of is_image() protected ?object $file_type; // {ext, type} from wp_check_filetype() protected Imagify_Filesystem $filesystem; protected mixed $editor; // WP_Image_Editor_Imagick|WP_Image_Editor_GD|WP_Error protected array $options; // cached get_imagify_option() calls public function __construct( string $file_path ) {...} } ``` ### Public Methods | Method | Parameters → Return | Notes | |--------|-------------------|-------| | `is_valid()` | `→ bool` | Returns true if `$path` is non-empty | | `can_be_processed()` | `→ true\|WP_Error` | Checks: path not empty, filesystem no errors, file exists, is a file, file writable, parent dir writable | | `optimize(array $args)` | `→ stdClass\|WP_Error` | Calls backup(), then `upload_imagify_image()`, downloads result, moves to destination | | `resize(array $dimensions, int $max_width)` | `→ string\|WP_Error` | Resizes via WP_Image_Editor; corrects EXIF orientation (cases 2–8); returns temp path | | `create_thumbnail(array $destination)` | `→ bool\|array\|WP_Error` | Calls `$editor->multi_resize()`; moves to destination path | | `backup(?string $backup_path, ?string $backup_source)` | `→ true\|false\|WP_Error` | Copies file to backup_path; also copies `-scaled` variant if exists | | `is_exceeded()` | `→ bool` | Returns true if file size > `IMAGIFY_MAX_BYTES` (5 MB) | | `is_supported(array $allowed_mime_types)` | `→ bool` | Checks MIME type against allow-list | | `is_image()` | `→ bool` | MIME type starts with `image/` | | `is_pdf()` | `→ bool` | MIME type is `application/pdf` | | `is_webp()` | `→ bool` | Regex: `@(?!^|/|\)\.webp$@i` — rejects bare `.webp` | | `is_avif()` | `→ bool` | Same pattern for `.avif` | | `get_path()` | `→ string` | Current absolute path (may change post-conversion) | | `get_path_to_webp()` | `→ string\|false` | Appends `.webp` to path; false if not an image or already WebP | | `get_path_to_nextgen(string $format)` | `→ string\|false` | Appends `.webp` or `.avif`; false if already next-gen | | `get_mime_type()` | `→ string` | From cached `wp_check_filetype()` | | `get_extension()` | `→ string\|false` | File extension without dot | | `get_dimensions()` | `→ array{width:int, height:int}` | Returns `[0,0]` if not image | ### `optimize()` — Args Array ```php optimize( [ 'backup' => true, // false = skip backup regardless of user setting 'backup_path' => null, // string — explicit backup destination path 'backup_source' => null, // string — source to backup (WP 5.3+ original) 'optimization_level' => 0, // 0=normal/lossless, 1=aggressive, 2=ultra 'convert' => '', // 'webp' | 'avif' | '' for original format 'context' => 'wp', // sent to API for logging 'original_size' => 0, // bytes, sent to API ] ); ``` --- ## 5. API Client — Endpoints & Request/Response *HTTP transport, authentication, all endpoints, response schema, error handling.* Class: `Imagify` (legacy classmap) — `inc/classes/class-imagify.php`. Singleton via `InstanceGetterTrait`. Base URL: `IMAGIFY_APP_API_URL` = `https://app.imagify.io/api/` ### Authentication ```php // Headers set in __construct() using stored API key $this->all_headers['Accept'] = 'Accept: application/json'; $this->all_headers['Content-Type'] = 'Content-Type: application/json'; $this->all_headers['Authorization'] = 'Authorization: token ' . $this->api_key; // upload_image() sends only Authorization header (multipart/form-data via cURL) // All other endpoints send all three headers ``` ### Transport Strategy 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. ### All Endpoints | Method | Endpoint | HTTP | Body / Response | |--------|----------|------|----------------| | `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}` | | `create_user($data)` | `users/` | POST | JSON body; no auth header | | `update_user($data)` | `users/me/` | PUT | JSON body with all headers | | `get_status($data)` | `status/{$data}/` | GET | Cached in static array per type | | `get_api_version()` | `version/` | GET | 5s timeout; cached in site transient | | `get_public_info()` | `public-info` | GET | Marketing/public plan info | | `upload_image($data)` | `upload/` | POST (cURL multipart) | `$data = ['image' => $path, 'data' => json_encode($opts)]`. Response: `{image: $url, ...}` | | `fetch_image($data)` | `fetch/` | POST (JSON) | Optimize image from URL; same response shape as upload | | `get_plans_prices()` | `pricing/plan/` | GET | Plan pricing objects | | `get_all_prices()` | `pricing/all/` | GET | All pricing including packs | | `check_coupon_code($coupon)` | `coupons/{$coupon}/` | GET | Coupon validity response | | `check_discount()` | `pricing/discount/` | GET | Active discount info | ### Upload Request Body (multipart via cURL) ```php // $data array passed to upload_image() [ 'image' => '/absolute/path/to/image.jpg', // CURLFile in cURL transport 'data' => json_encode([ 'normal' => true/false, // level === 0 'aggressive' => true/false, // level === 1 'ultra' => true/false, // level === 2 'keep_exif' => true, 'original_size' => int, 'context' => string, // 'wp' | 'custom-folders' | 'ngg' 'convert' => string, // 'webp' | 'avif' — only when converting ]), ] ``` ### API Response Shape (upload/fetch) ```json // Success — stdClass { "image": "https://app.imagify.io/...temp_url...", "original_size": 123456, "new_size": 98765, "percent": 19.87 } // Error — WP_Error with code 'error {http_code}' // HTTP 401 → invalid API key // HTTP 413 → file too large // HTTP 4xx/5xx → $response->detail or $response->image error array ``` ### HTTP Response Handling ```php private function handle_response( string $response, int $http_code, string $error = '' ) { $response = json_decode( $response ); // stdClass or null if ( 200 !== $http_code && !empty( $response->code ) ) { // $response->detail → WP_Error message // $response->image → array of field errors return new WP_Error( 'error ' . $http_code, ... ); } if ( ! is_object( $response ) ) { return new WP_Error( 'not_valid_json', ... ); } return $response; } ``` > **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`. --- ## 6. WordPress Postmeta Keys & Data Structures *Every key written to `wp_postmeta` by Imagify, with types and full schemas.* ### Primary Metadata Keys (WP Media Library) All stored on the attachment post (`post_type = attachment`). Managed by `Imagify\Optimization\Data\WP`. | Meta Key | Type | Values / Schema | |----------|------|----------------| | `_imagify_data` | Serialized array | `['sizes' => [...], 'stats' => [...], 'message' => string]` | | `_imagify_status` | string | `'success'` \| `'already_optimized'` \| error string \| `''` (not optimized) | | `_imagify_optimization_level` | int (stored as string) | `0` = normal/lossless, `1` = aggressive, `2` = ultra/smart | ### `_imagify_data` Full Schema ```php // _imagify_data serialized array structure [ 'sizes' => [ 'full' => [ 'success' => true, 'original_size' => int, 'optimized_size' => int, 'percent' => float ], 'thumbnail' => [ 'success' => true, ... ], 'medium' => [ 'success' => false, 'error' => string ], // ... one entry per registered image size + any custom sizes ], 'stats' => [ 'original_size' => int, // sum across all successful sizes 'optimized_size' => int, // sum across all successful sizes 'percent' => float, // aggregate % (2 decimal places) ], 'message' => string, // optional message from API (e.g. already optimized) ] ``` > `_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']`. ### Standard WordPress Keys (also modified by Imagify) | Meta Key | Modified When | |----------|-------------| | `_wp_attachment_metadata` | After resize (adds/removes sizes array entries); after thumbnail generation (updates width/height); after WP 5.3 original file handling | | `_wp_attached_file` | Not directly modified; read to resolve absolute paths | --- ## 7. Settings — Option Keys, Types & Defaults *All values stored under a single serialized option. Class: `Imagify_Options` (`inc/classes/class-imagify-options.php`).* 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)`. | Key | Type | Default | Reset Value | Description | |-----|------|---------|-------------|-------------| | `api_key` | string | `''` | — | Imagify API key. Overridable via PHP constant `IMAGIFY_API_KEY`. | | `optimization_level` | int | `2` | `2` | 0=lossless, 1=aggressive, 2=ultra | | `lossless` | int (bool) | `0` | — | Force level 0 for all optimizations | | `auto_optimize` | int (bool) | `0` | `1` | Auto-optimize on upload | | `backup` | int (bool) | `0` | `1` | Keep backup of originals | | `resize_larger` | int (bool) | `0` | `1` (if WP 5.3+) | Resize images larger than threshold | | `resize_larger_w` | int | `0` | From `big_image_size_threshold` filter (default 2560) | Max width in pixels for resize | | `display_nextgen` | int (bool) | `0` | — | Enable next-gen format delivery | | `display_nextgen_method` | string | `'picture'` | — | `'picture'` = HTML rewrite; `'rewrite'` = server-side rules | | `display_webp` | int (bool) | `0` | — | Legacy WebP delivery toggle | | `display_webp_method` | string | `'picture'` | — | Legacy WebP method selector | | `cdn_url` | string | `''` | — | CDN base URL for URL→path resolution | | `disallowed-sizes` | array | `[]` | — | Size names excluded from optimization | | `admin_bar_menu` | int (bool) | `1` | `1` | Show Imagify in admin bar | | `partner_links` | int (bool) | `0` | `1` | Show partner links in plugin UI | | `convert_to_avif` | int (bool) | `0` | — | Generate AVIF sidecar files | | `convert_to_webp` | int (bool) | `0` | — | Generate WebP sidecar files | | `optimization_format` | string | `'webp'` | — | `'webp'` \| `'avif'` \| `'off'` | > 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. --- ## 8. Database Schemas *Complete DDL for all three custom tables: `imagify_folders`, `imagify_files`, and `ngg_imagify_data`.* ### `imagify_folders` Class: `Imagify_Folders_DB` (`inc/classes/class-imagify-folders-db.php`). Global table in multisite (`$wpdb->base_prefix`). Table version: `100`. ```sql CREATE TABLE `{prefix}imagify_folders` ( `folder_id` bigint(20) unsigned NOT NULL auto_increment, `path` varchar(191) NOT NULL default '', `active` tinyint(1) unsigned NOT NULL default 0, PRIMARY KEY (folder_id), UNIQUE KEY path (path), KEY active (active) ); ``` | Column | Type | Description | |--------|------|-------------| | `folder_id` | bigint unsigned PK | Auto-increment primary key | | `path` | varchar(191) UNIQUE | Absolute path with placeholder: `{{ROOT}}/wp-content/uploads/gallery/`. Uses `{{ROOT}}` and `{{ABSPATH}}` tokens for portability. | | `active` | tinyint(1) | `1` = selected in settings; `0` = deactivated. Indexed for fast active-folder queries. | ### `imagify_files` Class: `Imagify_Files_DB` (`inc/classes/class-imagify-files-db.php`). Global table in multisite. Table version: `102`. ```sql CREATE TABLE `{prefix}imagify_files` ( `file_id` bigint(20) unsigned NOT NULL auto_increment, `folder_id` bigint(20) unsigned NOT NULL default 0, `file_date` datetime NOT NULL default '0000-00-00 00:00:00', `path` varchar(191) NOT NULL default '', `hash` varchar(32) NOT NULL default '', -- MD5 of file `mime_type` varchar(100) NOT NULL default '', `modified` tinyint(1) unsigned NOT NULL default 0, `width` smallint(2) unsigned NOT NULL default 0, `height` smallint(2) unsigned NOT NULL default 0, `original_size` int(4) unsigned NOT NULL default 0, `optimized_size` int(4) unsigned default NULL, `percent` smallint(2) unsigned default NULL, `optimization_level` tinyint(1) unsigned default NULL, `status` varchar(20) default NULL, `error` varchar(255) default NULL, `data` longtext default NULL, -- serialized PRIMARY KEY (file_id), UNIQUE KEY path (path), KEY folder_id (folder_id), KEY optimization_level (optimization_level), KEY status (status), KEY modified (modified) ); ``` | Column | Notes | |--------|-------| | `folder_id` | FK reference to `imagify_folders.folder_id` (not enforced at DB level) | | `path` | Absolute path using same `{{ROOT}}` tokens as folders table | | `hash` | MD5 hash of file contents — used by `refresh_file()` to detect modifications | | `modified` | `1` when file has changed since last optimization (hash mismatch) | | `status` | `'success'` \| `'already_optimized'` \| `'error'` \| NULL (not yet processed) | | `data` | Serialized array — same shape as `_imagify_data` (sizes + stats) | ### `ngg_imagify_data` (NextGEN Gallery) Class: `Imagify\ThirdParty\NGG\DB` (`inc/3rd-party/nextgen-gallery/classes/DB.php`). Per-site table (`$wpdb->prefix`). Table version: `100`. ```sql CREATE TABLE `{prefix}ngg_imagify_data` ( `data_id` bigint(20) unsigned NOT NULL auto_increment, `pid` bigint(20) unsigned NOT NULL default 0, `optimization_level` varchar(1) NOT NULL default '', `status` varchar(30) NOT NULL default '', `data` longtext default NULL, PRIMARY KEY (data_id), KEY pid (pid) ); ``` `pid` is the NextGEN picture ID. `data` is serialized the same way as `_imagify_data`. ### Abstract DB Base Class All three DB classes extend `Imagify_Abstract_DB` which implements `Imagify\DB\DBInterface`. It provides: **Table Management** - `maybe_upgrade_table()` — create/upgrade on plugin init - `create_table()` — issues `dbDelta()` - `can_operate(): bool` — true when table is ready - Version stored in option: `{option_prefix}_db_version` **CRUD Methods** - `get($id)`, `get_by($col, $val)`, `get_in($col, $vals)` - `get_var($col, $where)`, `get_column_in($col, $ids)` - `insert($data)`, `update($data, $where)`, `delete($id)` - Auto-serialize array columns before insert/update via `serialize_columns()` - Auto-cast results via `cast_row()` based on column type map --- ## 9. Bulk Optimization — ActionScheduler Integration *How bulk jobs are enqueued, tracked, and completed via ActionScheduler async actions.* Class: `Imagify\Bulk\Bulk` (`classes/Bulk/Bulk.php`). Singleton. Registered hooks in `init()`. ### Bulk Run Flow `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()` ### ActionScheduler Job Enqueue ```php // Bulk::run_optimize() — one as_enqueue_async_action() per media as_enqueue_async_action( 'imagify_optimize_media', [ 'id' => (int) $media_id, 'context' => (string) $context, // 'wp' | 'custom-folders' 'level' => (int) $optimization_level, ], "imagify-{$context}-optimize-media" // group name — allows cancellation per context ); // Next-gen generation uses a separate hook as_enqueue_async_action( 'imagify_convert_next_gen', [ 'id' => $media_id, 'context' => $context ], "imagify-{$context}-convert-nextgen" ); ``` ### Progress Tracking Transients | Transient | Set When | Shape | TTL | |-----------|---------|-------|-----| | `imagify_wp_optimize_running` | Start of WP library bulk run | `['total' => int, 'remaining' => int]` | DAY_IN_SECONDS | | `imagify_custom-folders_optimize_running` | Start of custom-folders bulk run | `['total' => int, 'remaining' => int]` | DAY_IN_SECONDS | | `imagify_bulk_optimization_result` | After each successful optimization | `['total' => int, 'original_size' => int, 'optimized_size' => int]` | DAY_IN_SECONDS | | `imagify_bulk_optimization_complete` | When remaining reaches 0 | `1` | DAY_IN_SECONDS | | `imagify_missing_next_gen_total` | Start of next-gen generation run | `int` (total count) | HOUR_IN_SECONDS | | `imagify_bulk_optimization_infos` | User dismisses info popup | `1` | WEEK_IN_SECONDS | ### ActionScheduler Job Lifecycle ActionScheduler is bundled at `inc/Dependencies/ActionScheduler/action-scheduler.php`. Jobs go through: `pending` → `in-progress` → `complete` 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. ### Multisite Context Routing ```php // Bulk::get_contexts() — determines which contexts appear on bulk page if ( ! is_network_admin() ) { $types['library|wp'] = 1; // library only in site admin } if ( imagify_is_active_for_network() && is_network_admin() ) { $types['custom-folders|custom-folders'] = 1; // custom folders in network admin } elseif ( ! imagify_is_active_for_network() ) { $types['custom-folders|custom-folders'] = 1; // custom folders in site admin } ``` --- ## 10. Concurrency & Locking Mechanisms *Transient-based per-media locks that prevent duplicate concurrent optimization or restore jobs.* ### Per-Media Process Lock Defined in `AbstractProcess`. Each lock is a transient named after the context and media ID. ```php // Transient name pattern (LOCK_NAME constant) const LOCK_NAME = 'imagify_%1$s_%2$s_process_locked'; // Example: 'imagify_wp_42_process_locked' // Example: 'imagify_custom-folders_7_process_locked' // Network-aware: uses set_site_transient when context is_network_wide() public function lock( string $action = 'optimizing' ): void { $name = $this->get_lock_name(); // sprintf(LOCK_NAME, ctx, id) $callback = $media->get_context_instance()->is_network_wide() ? 'set_site_transient' : 'set_transient'; call_user_func( $callback, $name, $action, 10 * MINUTE_IN_SECONDS ); } public function is_locked(): string|false { // Returns 'optimizing' | 'restoring' | false $callback = ...'get_site_transient' or 'get_transient'...; $action = call_user_func( $callback, $name ); return $this->validate_lock_action( $action ); // normalizes 'restore' → 'restoring' } public function unlock(): void { $callback = ...'delete_site_transient' or 'delete_transient'...; call_user_func( $callback, $name ); } ``` ### Lock Actions | Value | Set By | Cleared By | |-------|--------|-----------| | `'optimizing'` | `optimize()` before iterating sizes | `optimize()` after all sizes complete | | `'restoring'` | `restore()` at start | `restore()` at end (success or error) | ### Transient Name Summary for Cleanup ``` '_transient_%imagify-auto-optimize-%' // Legacy (deprecated) '_transient_%imagify_rpc_%' // Legacy (deprecated) '_transient_imagify_%_process_locked' // Active single-site locks '_site_transient_imagify_%_process_lock%' // Active network-wide locks ``` > **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. --- ## 11. Picture\Display — Output Buffer HTML Rewrite *How `<img>` tags are rewritten to `<picture>` tags at the HTTP response level.* Class: `Imagify\Picture\Display` (`classes/Picture/Display.php`). Implements `SubscriberInterface`. ### Subscribed Events ```php public static function get_subscribed_events(): array { return [ 'template_redirect' => 'start_content_process', 'imagify_process_webp_content' => 'process_content', ]; } ``` ### Full Rewrite Pipeline `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` ### Guards in `start_content_process()` - `get_imagify_option('display_nextgen')` must be truthy - `get_imagify_option('display_nextgen_method')` must equal `'picture'` (`Display::OPTION_VALUE`) - Filter `imagify_allow_picture_tags_for_nextgen` must return true ### Lazy-Load Support 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. ### Generated HTML Structure ```html <!-- Input --> <img src="/uploads/photo.jpg" srcset="/uploads/photo-300.jpg 300w" sizes="..." alt="..."> <!-- Output (when both AVIF and WebP exist) --> <picture> <source type="image/avif" srcset="/uploads/photo.jpg.avif, /uploads/photo-300.jpg.avif 300w" sizes="..."> <source type="image/webp" srcset="/uploads/photo.jpg.webp, /uploads/photo-300.jpg.webp 300w" sizes="..."> <img src="/uploads/photo.jpg" srcset="/uploads/photo-300.jpg 300w" sizes="..." alt="..."> </picture> ``` ### URL → Path Resolution `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. ### Filters on the Rewrite Path | Filter | Signature | Purpose | |--------|-----------|---------| | `imagify_allow_picture_tags_for_nextgen` | `(bool $allow): bool` | Global on/off switch for the rewriter | | `imagify_webp_picture_images_to_display` | `(array $images, string $content): array` | Filter/add/remove images before rewriting | | `imagify_webp_picture_process_image` | `(array $data, string $img_tag): array\|false` | Per-image data manipulation (used by S3 Offload integration) | | `imagify_picture_attributes` | `(array $attributes, array $data): array` | Attributes on the `<picture>` element | | `imagify_picture_source_attributes` | `(array $attributes, array $data): array` | Attributes on each `<source>` element | | `imagify_picture_img_attributes` | `(array $attributes, array $data): array` | Attributes on the fallback `<img>` | | `imagify_additional_source_tags` | `(string $html, array $data): string` | Inject extra `<source>` elements before the generated ones | | `imagify_buffer` | `(string $buffer): string` | Final buffer after all replacements | | `imagify_cdn_source_url` | `(string $url): string` | CDN base URL for URL-to-path mapping | --- ## 12. AJAX & Admin-Post — Full Security Table *Every `wp_ajax_*` and `admin_post_*` action with its nonce name and capability requirement.* 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)`. ### `wp_ajax_*` + `admin_post_*` (both) | Action | Nonce Name | Capability | Description | |--------|-----------|-----------|-------------| | `imagify_manual_optimize` | `imagify-optimize-{id}-{ctx}` | `manual-optimize` | Optimize single attachment | | `imagify_manual_reoptimize` | `imagify-manual-reoptimize-{id}-{ctx}` | `manual-optimize` | Re-optimize at different level | | `imagify_optimize_missing_sizes` | `imagify-optimize-missing-sizes-{id}-{ctx}` | `manual-optimize` | Generate missing thumbnail sizes | | `imagify_generate_nextgen_versions` | `imagify-generate-nextgen-versions-{id}-{ctx}` | `manual-optimize` | Generate WebP/AVIF for one attachment | | `imagify_delete_nextgen_versions` | `imagify-delete-nextgen-versions-{id}-{ctx}` | `manual-restore` | Remove WebP/AVIF sidecar files | | `imagify_restore` | `imagify-restore-{id}-{ctx}` | `manual-restore` | Restore attachment from backup | | `imagify_optimize_file` | `imagify_optimize_file` | `manual-optimize` (custom-folders ctx) | Optimize custom folder file | | `imagify_reoptimize_file` | `imagify_reoptimize_file` | `manual-optimize` (custom-folders ctx) | Re-optimize custom folder file | | `imagify_restore_file` | `imagify_restore_file` | `manual-restore` (custom-folders ctx) | Restore custom folder file from backup | | `imagify_refresh_file_modified` | `imagify_refresh_file_modified` | `manual-optimize` (custom-folders ctx) | Refresh file hash/modified status | ### `wp_ajax_*` Only | Action | Nonce Name | Capability / Check | Description | |--------|-----------|-------------------|-------------| | `imagify_bulk_optimize` | `imagify-bulk-optimize` | `bulk-optimize` | Launch ActionScheduler bulk job | | `imagify_missing_nextgen_generation` | `imagify-bulk-optimize` | `bulk-optimize` per context | Generate all missing next-gen files | | `imagify_get_folder_type_data` | `imagify-bulk-optimize` | `bulk-optimize` | Stats for one folder type on bulk page | | `imagify_bulk_info_seen` | `imagify-bulk-optimize` | `bulk-optimize` | Set `imagify_bulk_optimization_infos` transient | | `imagify_bulk_get_stats` | `imagify-bulk-optimize` | `bulk-optimize` per folder type | Aggregate bulk page statistics | | `imagify_reset_internal_state` | `imagify_reset_internal_state` | `manage` (wp ctx) | Clear all locks, transients, AS jobs | | `imagify_check_backup_dir_is_writable` | `imagify_check_backup_dir_is_writable` | `manage` (wp ctx) | Test backup directory writability | | `imagify_get_files_tree` | `get-files-tree` | `manage` (custom-folders ctx) | Filesystem tree for folder picker | | `imagify_signup` | `imagify-signup` | `manage` (wp ctx) | Create Imagify account | | `imagify_check_api_key_validity` | `imagify-check-api-key` | `manage` | Validate API key against API | | `imagify_get_prices` | `imagify_get_pricing_{user_id}` | `manage` | Fetch plan prices | | `imagify_check_coupon` | `imagify_get_pricing_{user_id}` | `manage` | Validate coupon code | | `imagify_get_discount` | `imagify_get_pricing_{user_id}` | `manage` | Check active discount | | `imagify_get_images_counts` | `imagify_get_pricing_{user_id}` | `manage` | Count images per status | | `imagify_update_estimate_sizes` | `update_estimate_sizes` | `manage` | Recalculate size estimates | | `imagify_get_user_data` | `imagify_get_user_data` | `manage` | Fetch fresh account data from API | | `imagify_delete_user_data_cache` | `imagify_delete_user_data_cache` | `manage` | Purge cached user data transient | | `nopriv_imagify_rpc` | `imagify_rpc_{rpc_id}` | None (nonce only) | Internal RPC dispatch | ### `admin_post_*` Only | Action | Nonce Name | Capability | |--------|-----------|-----------| | `imagify_scan_custom_folders` | `imagify_scan_custom_folders` | `optimize` (custom-folders ctx) | | `imagify_dismiss_ad` | `imagify-dismiss-ad` | `manage` (wp ctx) | | `imagify_dismiss_notice` | `imagify-dismiss-notice` | Varies per notice | | `imagify_deactivate_plugin` | `imagify-deactivate-plugin` | Varies per notice | | `imagify_rollback` | `imagify_rollback` | `manage_options` | > 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. --- ## 13. WP-CLI Commands — Full Signatures See section 9 in [imagify-summary.md](imagify-summary.md) for usage details. ### `wp imagify bulk-optimize` - **Context:** `wp`, `custom-folders` (default: `wp`) - **Options:** `--optimization-level=<0|1|2>` - **Execution:** Asynchronous via ActionScheduler ### `wp imagify restore` - **Context:** `library`, `custom-folders` - **Execution:** Synchronous (blocking) - **Returns:** success count, error count, total ### `wp imagify generate-missing-nextgen` - **Context:** all optimized images across contexts - **Execution:** Asynchronous via ActionScheduler --- ## 14. Developer Hooks — Exact Signatures & Parameter Types See section 13 in [imagify-summary.md](imagify-summary.md) for the full table. Below are additional filter signatures from the Picture\Display system. | Filter | Full Signature | Notes | |--------|---------------|-------| | `imagify_allow_picture_tags_for_nextgen` | `apply_filters('imagify_allow_picture_tags_for_nextgen', bool $allow)` | Return `false` to disable `<picture>` rewriting globally | | `imagify_picture_attributes` | `apply_filters('imagify_picture_attributes', array $attr, array $data)` | `$data` contains `src`, `srcset`, `sizes`, `img_tag` | | `imagify_buffer` | `apply_filters('imagify_buffer', string $html)` | Full page HTML after all replacements | | `imagify_register_context` | `apply_filters('imagify_register_context', array $contexts)` | Register a custom context; key = context slug, value = class name | | `imagify_backup_directory` | `apply_filters('imagify_backup_directory', string $path, int $attachment_id)` | Override per-attachment backup directory | | `imagify_api_http_request_timeout` | `apply_filters('imagify_api_http_request_timeout', int $timeout, string $endpoint)` | Override API request timeout in seconds | | `imagify_event_recurrence` | `apply_filters('imagify_event_recurrence', string $recurrence, string $event)` | Change cron job recurrence (e.g. `'daily'`, `'hourly'`) | | `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 | --- ## 15. Scheduled Tasks — Cron & ActionScheduler See section 12 in [imagify-summary.md](imagify-summary.md) for the cron table. ### ActionScheduler Table ActionScheduler stores its job queue in `{prefix}actionscheduler_actions`. Key columns relevant to Imagify: | Column | Notes | |--------|-------| | `hook` | `'imagify_optimize_media'` or `'imagify_convert_next_gen'` | | `group` | `'imagify-wp-optimize-media'`, `'imagify-custom-folders-optimize-media'`, etc. | | `args` | JSON: `{"id": 42, "context": "wp", "level": 2}` | | `status` | `'pending'` \| `'in-progress'` \| `'complete'` \| `'failed'` \| `'canceled'` | All Imagify AS jobs are **async actions** (`as_enqueue_async_action()`), not scheduled recurring jobs. --- ## 16. Multisite Handling - Plugin can be network-activated (`imagify_is_active_for_network()` → true). - When network-activated, settings are stored in `wp_sitemeta` via `get_site_option`. - `imagify_folders` and `imagify_files` tables use `$wpdb->base_prefix` (global tables shared across sites). - `ngg_imagify_data` uses `$wpdb->prefix` (per-site). - Locks use `set_site_transient` when `$context->is_network_wide()` is true. - Bulk page shows `custom-folders` context in network admin; `library` context only in site admin. - Quota is shared across all sites on the same API key. --- ## 17. NextGEN Gallery Integration Automatically detected via `class_exists('C_Gallery_Storage')`. Activates `Imagify\Context\NGG` and `Imagify\ThirdParty\NGG\DB`. **Compatibility check:** `imagify_ngg_has_pope_storage()` verifies NGG v4.x is present before activating deep integration. **Data storage:** `{prefix}ngg_imagify_data` (per-site, see schema in section 8). **Bulk context:** registered as `'ngg'`. Appears on the bulk page when NextGEN Gallery is active. --- ## 18. Third-Party Integrations All loaded by `Imagify\ThirdParty\ServiceProvider` which scans `inc/3rd-party/` on boot. | Plugin | Integration Path | What it does | |--------|-----------------|-------------| | WooCommerce | `inc/3rd-party/woocommerce/` | Fixes `wp-post-image` class on `<picture>` tags for product image switching | | WP Rocket | `inc/3rd-party/wp-rocket/classes/` | PSR-4 compat shim | | Gravity Forms | `Imagify\ThirdParty\ServiceProvider` | Registers Gravity Forms upload folder as custom folder context | | Formidable Pro | `inc/3rd-party/formidable-pro/classes/` | Same approach as Gravity Forms | | Yoast SEO | `inc/3rd-party/` | Ensures Open Graph images are optimized | | AMP | `inc/3rd-party/` | Disables `<picture>` rewriter on AMP pages | | Enable Media Replace | `inc/3rd-party/enable-media-replace/classes/` | Re-optimizes on media replacement | | Regenerate Thumbnails | `inc/3rd-party/regenerate-thumbnails/classes/` | Re-triggers optimization after thumbnail regeneration | | Amazon S3 / CloudFront | `inc/3rd-party/amazon-s3-and-cloudfront/classes/` | S3 URL resolution for `url_to_path()` | | Cloudflare Super Page Cache | `inc/3rd-party/` | Cache purge on next-gen file generation | --- ## 19. Quota & Account Management See section 14 in [imagify-summary.md](imagify-summary.md). **`plan_id` mapping:** | plan_id | Plan | |---------|------| | `1` | Free | | `15` | Infinite (annual) | | `16` | Growth (monthly) | | `17` | Infinite (monthly) | | `18` | Growth (annual) | **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. **Transient:** `imagify_user_cache` — TTL 5 minutes. Cleared on: API key change, account signup, `imagify_delete_user_data_cache` AJAX action. --- ## 20. Roles & Capabilities | Capability | WP Capability | Defined In | |-----------|--------------|-----------| | `manage` | `manage_options` | `AbstractContext::get_capacity()` | | `optimize` | `upload_files` | `AbstractContext::get_capacity()` | | `manual-optimize` | `upload_files` | `AbstractContext::get_capacity()` | | `manual-restore` | `upload_files` | `AbstractContext::get_capacity()` | | `bulk-optimize` | `manage_options` | `AbstractContext::get_capacity()` | Filter: `option_page_capability_imagify` — override any capability mapping. --- ## 21. Troubleshooting Tools — InternalStateList & Reset ### `Imagify\Tools\InternalStateList` Single source of truth for all lockable/clearable state. Used by both `ResetInternalState` (admin tool) and `uninstall.php` (full cleanup). **Methods:** - `get_transients(): array` — list of transient keys to delete - `get_locked_transient_patterns(): array` — LIKE patterns for SQL bulk delete of locks - `get_action_scheduler_hooks(): array` — AS hook names to cancel all pending jobs ### AJAX Action: `imagify_reset_internal_state` Requires `manage` capability. Steps: 1. Delete all transients listed by `get_transients()` 2. SQL `DELETE FROM wp_options WHERE option_name LIKE '_transient_%imagify...'` for each lock pattern 3. `as_unschedule_all_actions('imagify_optimize_media')` 4. `as_unschedule_all_actions('imagify_convert_next_gen')` 5. Returns JSON `{success: true}` --- ## 22. Error Handling Paths - **`WP_Error` throughout** — all `optimize()`, `restore()`, `File::optimize()` return `WP_Error` on failure, never throw exceptions. - **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. - **File system errors** — `can_be_processed()` returns `WP_Error` with specific codes: `'file_not_exists'`, `'file_not_writable'`, `'dir_not_writable'`. - **Lock conflicts** — `optimize()` returns early (`false`) if `is_locked()` is truthy; does not return `WP_Error`. - **Quota exceeded** — returns `WP_Error` with code `'over_quota'` before hitting the API. - **Size exceeds 5 MB** — `is_exceeded()` triggers `WP_Error` with code `'file_too_big'`. --- ## 23. Filesystem Operations & Paths All filesystem operations go through `Imagify_Filesystem` (wraps `WP_Filesystem`). Never uses `file_put_contents()` or `fopen()` directly. **Key path helpers:** - `imagify_get_upload_basedir()` — returns `wp_upload_dir()['basedir']` with trailing slash - `imagify_get_upload_baseurl()` — returns `wp_upload_dir()['baseurl']` - `imagify_get_backup_dir()` — returns `{upload_basedir}backup/imagify/`; filterable via `imagify_backup_directory` - `imagify_get_filesystem()` — returns `Imagify_Filesystem::get_instance()` **Next-gen sidecar paths:** - WebP: `{original_path}.webp` (e.g. `photo.jpg.webp`) - AVIF: `{original_path}.avif` (e.g. `photo.jpg.avif`) - Note: sidecar files are never `.webp` or `.avif` as standalone extensions — the original extension is preserved as a prefix. --- *Imagify v2.2.8 · wp-media/imagify-plugin · Engineering Deep Dive*