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.html

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

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