PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.15
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.15
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 / includes / class-metasync-oxygen-compat.php

class-metasync-oxygen-compat.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.15, at includes/class-metasync-oxygen-compat.php

268 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Oxygen Builder Compatibility – auto re-signs [oxygen] dynamic shortcodes
5 * when their HMAC signatures are invalid (e.g. after design-set import or migration).
6 *
7 * Runs once on `admin_init`. Skips entirely when Oxygen is inactive or signature
8 * validation is disabled. Uses a two-tier fingerprint so it only re-processes when
9 * the private key or template content actually changes.
10 *
11 * Uses WordPress shortcode_parse_atts() to correctly parse ALL shortcode attributes
12 * (data, format, size, taxonomy, separator, etc.) — matching Oxygen's own signing
13 * approach in ct_sign_oxy_dynamic_shortcode().
14 *
15 * @package MetaSync
16 */
17
18 if (!defined('ABSPATH')) {
19 exit;
20 }
21
22 class Metasync_Oxygen_Compat
23 {
24 const OPTION_KEY = 'metasync_oxygen_signatures_hash';
25
26 /**
27 * Regex: captures the full inner attribute string of any [oxygen …] shortcode.
28 * Works for both normal quotes and backslash-escaped quotes (\') used inside
29 * ct_options JSON in _ct_builder_shortcodes.
30 */
31 const OXY_PATTERN = '/\[oxygen\s+([^\]]+)\]/';
32
33 /**
34 * Entry point – hooked to admin_init.
35 */
36 public static function maybe_resign_shortcodes()
37 {
38 if (!self::should_run()) {
39 return;
40 }
41
42 // Tier 1: lightweight check — private key + template count + latest modification.
43 // Avoids loading all JSON blobs on every admin page load.
44 $light_fp = self::build_light_fingerprint();
45 $stored = get_option(self::OPTION_KEY, '');
46
47 if (!empty($stored) && strpos($stored, '|') !== false) {
48 list($stored_light) = explode('|', $stored, 2);
49 if ($stored_light === $light_fp) {
50 return; // Nothing changed since last run
51 }
52 }
53
54 // Tier 2: full check — load JSON, verify signatures, re-sign if needed.
55 $full_fp = self::build_full_fingerprint();
56 if (!empty($stored)) {
57 list(, $stored_full) = array_pad(explode('|', $stored, 2), 2, '');
58 if ($stored_full === $full_fp) {
59 // Templates changed (e.g. CSS edit) but signatures are still valid.
60 // Update tier-1 to reflect the new lightweight fingerprint.
61 update_option(self::OPTION_KEY, $light_fp . '|' . $full_fp, true);
62 return;
63 }
64 }
65
66 $updated = self::resign_templates();
67
68 if ($updated > 0) {
69 error_log("MetaSync Oxygen Compat: Re-signed shortcodes in {$updated} template(s).");
70 $full_fp = self::build_full_fingerprint(); // Recompute after DB writes
71 }
72
73 update_option(self::OPTION_KEY, $light_fp . '|' . $full_fp, true);
74 }
75
76 // ------------------------------------------------------------------
77 // Guards
78 // ------------------------------------------------------------------
79
80 private static function should_run()
81 {
82 if (!class_exists('OXYGEN_VSB_Signature')) {
83 return false;
84 }
85
86 $enabled = get_option('oxygen_vsb_enable_signature_validation');
87 return $enabled && $enabled !== 'false';
88 }
89
90 /**
91 * Get or create an Oxygen signature helper.
92 */
93 private static function get_signer()
94 {
95 global $oxygen_signature;
96
97 if (isset($oxygen_signature) && $oxygen_signature instanceof OXYGEN_VSB_Signature) {
98 return $oxygen_signature;
99 }
100
101 if (class_exists('OXYGEN_VSB_Signature')) {
102 return new OXYGEN_VSB_Signature();
103 }
104
105 return null;
106 }
107
108 // ------------------------------------------------------------------
109 // Fingerprints
110 // ------------------------------------------------------------------
111
112 /**
113 * Tier 1: lightweight fingerprint — private key + template count + latest modification.
114 * No JSON blobs loaded; just a single aggregate query.
115 */
116 private static function build_light_fingerprint()
117 {
118 global $wpdb;
119
120 $key = get_option('oxygen_private_key', '');
121
122 $row = $wpdb->get_row(
123 "SELECT COUNT(*) AS cnt, MAX(p.post_modified_gmt) AS latest
124 FROM {$wpdb->posts} p
125 INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_ct_builder_json'
126 WHERE p.post_type = 'ct_template'
127 AND p.post_status = 'publish'"
128 );
129
130 $cnt = $row ? $row->cnt : 0;
131 $latest = $row ? $row->latest : '';
132
133 return hash('sha256', $key . '|' . $cnt . '|' . $latest);
134 }
135
136 /**
137 * Tier 2: full fingerprint — SHA-256 of private key + all template JSON content.
138 * Only called when the lightweight check detects a change.
139 */
140 private static function build_full_fingerprint()
141 {
142 global $wpdb;
143
144 $key = get_option('oxygen_private_key', '');
145
146 $json_rows = $wpdb->get_col(
147 "SELECT pm.meta_value
148 FROM {$wpdb->postmeta} pm
149 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
150 WHERE p.post_type = 'ct_template'
151 AND p.post_status = 'publish'
152 AND pm.meta_key = '_ct_builder_json'
153 ORDER BY pm.post_id ASC"
154 );
155
156 return hash('sha256', $key . implode('|', $json_rows));
157 }
158
159 // ------------------------------------------------------------------
160 // Re-sign
161 // ------------------------------------------------------------------
162
163 private static function resign_templates()
164 {
165 global $wpdb;
166
167 $signer = self::get_signer();
168 if (!$signer) {
169 return 0;
170 }
171
172 $template_ids = $wpdb->get_col(
173 "SELECT p.ID
174 FROM {$wpdb->posts} p
175 WHERE p.post_type = 'ct_template'
176 AND p.post_status = 'publish'
177 ORDER BY p.ID ASC"
178 );
179
180 if (empty($template_ids)) {
181 return 0;
182 }
183
184 $updated = 0;
185
186 foreach ($template_ids as $tpl_id) {
187 $changed = false;
188
189 // _ct_builder_json — primary rendering source
190 $json = get_post_meta($tpl_id, '_ct_builder_json', true);
191 if (!empty($json)) {
192 $new_json = self::resign_oxygen_in_string($json, $signer);
193 if ($new_json !== $json) {
194 update_post_meta($tpl_id, '_ct_builder_json', wp_slash($new_json));
195 $changed = true;
196 }
197 }
198
199 // _ct_builder_shortcodes — secondary format
200 $sc = get_post_meta($tpl_id, '_ct_builder_shortcodes', true);
201 if (!empty($sc)) {
202 $new_sc = self::resign_oxygen_in_string($sc, $signer);
203 if ($new_sc !== $sc) {
204 update_post_meta($tpl_id, '_ct_builder_shortcodes', wp_slash($new_sc));
205 $changed = true;
206 }
207 }
208
209 if ($changed) {
210 $updated++;
211 }
212 }
213
214 return $updated;
215 }
216
217 /**
218 * Find every [oxygen …] shortcode in a string and re-sign any whose
219 * signature doesn't match the current private key. Preserves ALL attributes.
220 *
221 * Handles both normal quotes: data='title'
222 * and escaped quotes: data=\'title\'
223 *
224 * @param string $content Raw meta value.
225 * @param OXYGEN_VSB_Signature $signer Oxygen's signature helper.
226 * @return string Content with corrected signatures.
227 */
228 private static function resign_oxygen_in_string($content, $signer)
229 {
230 return preg_replace_callback(self::OXY_PATTERN, function ($match) use ($signer) {
231 $inner = $match[1];
232
233 // Detect whether this shortcode uses escaped quotes (\')
234 $uses_escaped_quotes = strpos($inner, "\\'") !== false;
235
236 // Normalize escaped quotes for shortcode_parse_atts()
237 $normalized = $uses_escaped_quotes ? str_replace("\\'", "'", $inner) : $inner;
238 $attr = shortcode_parse_atts(trim($normalized));
239
240 if (!is_array($attr) || empty($attr['ct_sign_sha256'])) {
241 return $match[0]; // Not a signed oxygen shortcode, leave as-is
242 }
243
244 $stored_sig = $attr['ct_sign_sha256'];
245
246 // Build the attributes WITHOUT the signature for verification
247 $attr_without_sig = $attr;
248 unset($attr_without_sig['ct_sign_sha256']);
249
250 $expected = $signer->generate_signature('oxygen', $attr_without_sig, null);
251
252 // If signature is already valid, skip
253 if (hash_equals($expected, $stored_sig)) {
254 return $match[0];
255 }
256
257 // Rebuild the shortcode with the correct signature + all original attributes
258 $q = $uses_escaped_quotes ? "\\'" : "'";
259 $parts = "ct_sign_sha256={$q}{$expected}{$q}";
260 foreach ($attr_without_sig as $key => $val) {
261 $parts .= " {$key}={$q}{$val}{$q}";
262 }
263
264 return "[oxygen {$parts} ]";
265 }, $content);
266 }
267 }
268