PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / trunk
Search Atlas SEO – OTTO AI SEO Automation for WordPress vtrunk
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / media-optimization / class-media-settings.php

class-media-settings.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress trunk, at media-optimization/class-media-settings.php

184 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Metasync_Media_Settings
4 * Manages settings for the Media Optimization module.
5 * Settings are stored in the plugin's unified option system.
6 *
7 * @package Search Atlas SEO
8 * @copyright Copyright (C) 2021-2025, Search Atlas Group - support@searchatlas.com
9 * @since 2.6.0
10 */
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 class Metasync_Media_Settings {
17
18 const OPTION_KEY = 'metasync_media_optimization';
19
20 private static $defaults = [
21 // Image Conversion
22 'enable_conversion' => false,
23 'conversion_format' => 'webp', // 'webp' or 'avif'
24 'conversion_quality' => 82,
25 'conversion_strategy' => 'alongside', // 'replace' or 'alongside'
26 'convert_existing_sizes' => true,
27 'max_image_dimensions' => 2560, // Max width/height (px) before conversion; 0 disables downscaling
28 // Lazy Loading
29 'enable_lazy_loading' => false,
30 'lazy_load_iframes' => true,
31 'lcp_skip_count' => 2,
32 // Dimension Injection
33 'enable_dimension_injection' => false,
34 // Exclusions
35 'exclude_classes' => '', // Comma-separated CSS classes to exclude
36 'exclude_urls' => '', // Comma-separated URL patterns to exclude
37 ];
38
39 /**
40 * Get merged settings with defaults.
41 *
42 * AVIF is coerced back to WebP when the runtime cannot support it. A value of
43 * 'avif' can already be persisted (saved on a capable server, then the site
44 * moved or downgraded), so the coercion has to happen on read as well as on
45 * save — otherwise stored settings would keep driving AVIF conversion on a
46 * runtime that mis-measures the result. See supports_avif().
47 */
48 public static function get_settings(): array {
49 $saved = get_option(self::OPTION_KEY, []);
50 $settings = wp_parse_args($saved, self::$defaults);
51
52 if (($settings['conversion_format'] ?? '') === 'avif' && !self::supports_avif()) {
53 $settings['conversion_format'] = 'webp';
54 }
55
56 return $settings;
57 }
58
59 /**
60 * Get default settings.
61 */
62 public static function get_defaults(): array {
63 return self::$defaults;
64 }
65
66 /**
67 * Save settings with sanitization.
68 */
69 public static function save_settings(array $input): bool {
70 $sanitized = self::sanitize($input);
71 return update_option(self::OPTION_KEY, $sanitized);
72 }
73
74 /**
75 * Sanitize and validate settings input.
76 */
77 public static function sanitize(array $input): array {
78 return [
79 'enable_conversion' => !empty($input['enable_conversion']),
80 'conversion_format' => self::sanitize_conversion_format($input['conversion_format'] ?? ''),
81 'conversion_quality' => max(1, min(100, (int) ($input['conversion_quality'] ?? 82))),
82 'conversion_strategy' => in_array($input['conversion_strategy'] ?? '', ['replace', 'alongside'], true) ? $input['conversion_strategy'] : 'alongside',
83 'convert_existing_sizes' => !empty($input['convert_existing_sizes']),
84 'max_image_dimensions' => max(0, min(10000, (int) ($input['max_image_dimensions'] ?? 2560))),
85 'enable_lazy_loading' => !empty($input['enable_lazy_loading']),
86 'lazy_load_iframes' => !empty($input['lazy_load_iframes']),
87 'lcp_skip_count' => max(0, min(10, (int) ($input['lcp_skip_count'] ?? 2))),
88 'enable_dimension_injection' => !empty($input['enable_dimension_injection']),
89 'exclude_classes' => sanitize_text_field($input['exclude_classes'] ?? ''),
90 'exclude_urls' => sanitize_text_field($input['exclude_urls'] ?? ''),
91 ];
92 }
93
94 /**
95 * Resolve the target conversion format, refusing AVIF the runtime can't measure.
96 *
97 * Anything unrecognised falls back to WebP, as does AVIF on a runtime or GD/
98 * Imagick build that cannot support it. This keeps an unsupported value from
99 * ever reaching the database.
100 */
101 private static function sanitize_conversion_format(string $format): string {
102 if ($format === 'avif' && self::supports_avif()) {
103 return 'avif';
104 }
105
106 return 'webp';
107 }
108
109 /**
110 * Minimum PHP version at which AVIF output can be measured correctly.
111 *
112 * getimagesize() only learned to read real AVIF dimensions in PHP 8.2. On
113 * 8.1 it returns 0x0 for a valid AVIF file (and a truthy [0,0] for a
114 * truncated header, where 8.2 returns false). Every dimension read in this
115 * module guards with `$info && $info[0] > 0 && $info[1] > 0`, so on 8.1 the
116 * guard fails and:
117 * - the converter cannot sync post-downscale dimensions onto the
118 * attachment, leaving stale oversized width/height in metadata, and
119 * - the dimension injector then emits those stale values into the
120 * rendered HTML, which is worse than emitting none because the browser
121 * trusts them (a Core Web Vitals / CLS regression).
122 *
123 * imageavif() exists from 8.1 onward, so capability detection alone would
124 * pass and let an 8.1 site select AVIF. WebP is unaffected and is the
125 * default, so 8.1 sites are held to WebP until the runtime can measure AVIF.
126 */
127 const AVIF_MIN_PHP_VERSION_ID = 80200;
128
129 /**
130 * Check if the server supports AVIF conversion.
131 */
132 public static function supports_avif(): bool {
133 // Encoding is not enough — the result also has to be measurable.
134 if (PHP_VERSION_ID < self::AVIF_MIN_PHP_VERSION_ID) {
135 return false;
136 }
137 if (extension_loaded('imagick')) {
138 try {
139 $formats = \Imagick::queryFormats('AVIF');
140 return !empty($formats);
141 } catch (\Exception $e) {
142 return false;
143 }
144 }
145 if (function_exists('gd_info')) {
146 $info = gd_info();
147 return !empty($info['AVIF Support']);
148 }
149 return false;
150 }
151
152 /**
153 * Check if the server supports WebP conversion.
154 */
155 public static function supports_webp(): bool {
156 if (extension_loaded('imagick')) {
157 try {
158 $formats = \Imagick::queryFormats('WEBP');
159 return !empty($formats);
160 } catch (\Exception $e) {
161 return false;
162 }
163 }
164 if (function_exists('gd_info')) {
165 $info = gd_info();
166 return !empty($info['WebP Support']);
167 }
168 return false;
169 }
170
171 /**
172 * Get server capability info for display on admin page.
173 */
174 public static function get_server_capabilities(): array {
175 return [
176 'imagick' => extension_loaded('imagick'),
177 'gd' => extension_loaded('gd'),
178 'webp_support' => self::supports_webp(),
179 'avif_support' => self::supports_avif(),
180 'has_library' => extension_loaded('imagick') || extension_loaded('gd'),
181 ];
182 }
183 }
184