| 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 & Bootstrapping</a></li> |
| 118 |
<li><a href="#namespace">Namespace & PSR-4 Structure</a></li> |
| 119 |
<li><a href="#optimization-process">Optimization Process — Class Hierarchy & Call Flow</a></li> |
| 120 |
<li><a href="#file-class">Optimization\File — Method Signatures</a></li> |
| 121 |
<li><a href="#api">API Client — Endpoints & Request/Response</a></li> |
| 122 |
<li><a href="#metadata">WordPress Postmeta Keys & Data Structures</a></li> |
| 123 |
<li><a href="#settings">Settings — Option Keys, Types & 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 & Locking Mechanisms</a></li> |
| 127 |
<li><a href="#picture-display">Picture\Display — Output Buffer HTML Rewrite</a></li> |
| 128 |
<li><a href="#ajax">AJAX & 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 & Parameter Types</a></li> |
| 131 |
<li><a href="#cron">Scheduled Tasks — Cron & 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 & Account Management</a></li> |
| 136 |
<li><a href="#roles">Roles & Capabilities</a></li> |
| 137 |
<li><a href="#tools">Troubleshooting Tools — InternalStateList & Reset</a></li> |
| 138 |
<li><a href="#error-handling">Error Handling Paths</a></li> |
| 139 |
<li><a href="#filesystem">Filesystem Operations & 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 & 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 <picture> 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 & 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 & 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 & 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 > <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 & 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 & 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 & 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 & 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><img></code> tags are rewritten to <code><picture></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 </html> and be >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 <picture> wrappers</span> |
| 960 |
<span class="flow-arrow">→</span> |
| 961 |
<span class="flow-step">get_images() — regex extract all <img> 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><source></code> tag mirrors whichever attribute was active.</p> |
| 979 |
|
| 980 |
<h3>Generated HTML Structure</h3> |
| 981 |
<pre><span class="c"><!-- Input --></span> |
| 982 |
<img src="/uploads/photo.jpg" srcset="/uploads/photo-300.jpg 300w" sizes="..." alt="..."> |
| 983 |
|
| 984 |
<span class="c"><!-- Output (when both AVIF and WebP exist) --></span> |
| 985 |
<picture> |
| 986 |
<source type="image/avif" srcset="/uploads/photo.jpg.avif, /uploads/photo-300.jpg.avif 300w" sizes="..."> |
| 987 |
<source type="image/webp" srcset="/uploads/photo.jpg.webp, /uploads/photo-300.jpg.webp 300w" sizes="..."> |
| 988 |
<img src="/uploads/photo.jpg" srcset="/uploads/photo-300.jpg 300w" sizes="..." alt="..."> |
| 989 |
</picture></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><picture></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><source></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><img></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><source></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 & 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 |
|