PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
2.7.1 2.7.2 2.7.0 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 All 141 releases
metasync / otto / class-metasync-otto-persistence-settings.php

class-metasync-otto-persistence-settings.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.0, at otto/class-metasync-otto-persistence-settings.php

474 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OTTO Persistence Settings
4 *
5 * Manages configuration for which OTTO data types should be persisted
6 * to native WordPress fields (surviving plugin uninstallation).
7 *
8 * Each persistence flag controls whether stored data is RETAINED when OTTO
9 * is disabled or paused. OTTO always writes to its own _metasync_otto_* keys
10 * during an active sync regardless of these flags.
11 *
12 * The flags DO gate one thing: copying an OTTO-generated value into a
13 * third-party SEO plugin's storage (Yoast / Rank Math / AIOSEO), for posts
14 * and terms alike. That is a permanent write into another plugin's data which
15 * outlives MetaSync, so it happens only while the matching flag is enabled.
16 * Manually entered values are never gated.
17 *
18 * @package MetaSync
19 * @since 2.6.0
20 */
21
22 if (!defined('ABSPATH')) {
23 exit;
24 }
25
26 class Metasync_Otto_Persistence_Settings {
27
28 /**
29 * Option name for storing persistence settings
30 */
31 private const OPTION_NAME = 'metasync_otto_persistence_settings';
32
33 /**
34 * REST API namespace
35 */
36 private const REST_NAMESPACE = 'metasync/v1';
37
38 /**
39 * Default persistence settings
40 * All values default to false (no persistence) until platform enables them
41 */
42 private static $default_settings = [
43 'meta_title' => false,
44 'meta_description' => false,
45 'meta_keywords' => false,
46 'og_title' => false,
47 'og_description' => false,
48 'twitter_title' => false,
49 'twitter_description' => false,
50 'canonical_url' => false,
51 'image_alt_text' => false,
52 'link_corrections' => false,
53 'heading_changes' => false,
54 'structured_data' => false, // Controls whether otto_jsonld in metasync_schema_markup survives OTTO being disabled/paused. During active syncs, otto_jsonld is always written regardless of this flag.
55 ];
56
57 /**
58 * Singleton instance
59 */
60 private static $instance = null;
61
62 /**
63 * Get singleton instance
64 */
65 public static function get_instance() {
66 if (self::$instance === null) {
67 self::$instance = new self();
68 }
69 return self::$instance;
70 }
71
72 /**
73 * Constructor
74 */
75 private function __construct() {
76 add_action('rest_api_init', [$this, 'register_rest_routes']);
77 }
78
79 /**
80 * Initialize the class (call this from plugin init)
81 */
82 public static function init() {
83 return self::get_instance();
84 }
85
86 /**
87 * Register REST API routes
88 */
89 public function register_rest_routes() {
90 # GET endpoint - Retrieve current persistence settings
91 register_rest_route(
92 self::REST_NAMESPACE,
93 '/otto_persistence_settings',
94 [
95 [
96 'methods' => 'GET',
97 'callback' => [$this, 'handle_get_settings'],
98 'permission_callback' => [$this, 'authorize_request'],
99 ],
100 [
101 'methods' => 'POST',
102 'callback' => [$this, 'handle_update_settings'],
103 'permission_callback' => [$this, 'authorize_request'],
104 'args' => $this->get_endpoint_args(),
105 ],
106 ]
107 );
108
109 # Individual setting endpoint for granular updates
110 register_rest_route(
111 self::REST_NAMESPACE,
112 '/otto_persistence_settings/(?P<setting_key>[a-z_]+)',
113 [
114 [
115 'methods' => 'GET',
116 'callback' => [$this, 'handle_get_single_setting'],
117 'permission_callback' => [$this, 'authorize_request'],
118 'args' => [
119 'setting_key' => [
120 'required' => true,
121 'type' => 'string',
122 'validate_callback' => [$this, 'validate_setting_key'],
123 ],
124 ],
125 ],
126 [
127 'methods' => 'PUT',
128 'callback' => [$this, 'handle_update_single_setting'],
129 'permission_callback' => [$this, 'authorize_request'],
130 'args' => [
131 'setting_key' => [
132 'required' => true,
133 'type' => 'string',
134 'validate_callback' => [$this, 'validate_setting_key'],
135 ],
136 'value' => [
137 'required' => true,
138 'type' => 'boolean',
139 'sanitize_callback' => 'rest_sanitize_boolean',
140 ],
141 ],
142 ],
143 ]
144 );
145 }
146
147 /**
148 * Define endpoint arguments for POST request
149 */
150 private function get_endpoint_args() {
151 $args = [];
152 foreach (array_keys(self::$default_settings) as $key) {
153 $args[$key] = [
154 'required' => false,
155 'type' => 'boolean',
156 'sanitize_callback' => 'rest_sanitize_boolean',
157 'description' => sprintf('Whether to persist %s to native WordPress fields', str_replace('_', ' ', $key)),
158 ];
159 }
160 return $args;
161 }
162
163 /**
164 * Authorize REST API request using apikey
165 * Uses same pattern as rest_authorization_middleware in Metasync_Public
166 *
167 * @param WP_REST_Request $request
168 * @return bool|WP_Error
169 */
170 public function authorize_request($request) {
171 # Accept Authorization: Bearer header (preferred), X-API-Key header, or ?apikey= query param for backward compat
172 $api_key = '';
173
174 $auth_header = $request->get_header('authorization');
175 if (!empty($auth_header) && preg_match('/^Bearer\s+(.+)$/i', $auth_header, $matches)) {
176 $api_key = sanitize_text_field($matches[1]);
177 }
178
179 if (empty($api_key)) {
180 $header_key = $request->get_header('x-api-key');
181 if (!empty($header_key)) {
182 $api_key = sanitize_text_field($header_key);
183 }
184 }
185
186 if (empty($api_key)) {
187 $get_data = array_map('sanitize_text_field', $_GET);
188 if (!empty($get_data['apikey'])) {
189 $api_key = $get_data['apikey'];
190 // Deprecated: ?apikey= query-param auth — switch callers to Authorization: Bearer
191 }
192 }
193
194 if (empty($api_key)) {
195 return new WP_Error(
196 'rest_forbidden',
197 'API key is required. Pass Authorization: Bearer header, X-API-Key header, or ?apikey= query parameter.',
198 ['status' => 401]
199 );
200 }
201
202 $options = get_option('metasync_options', []);
203 $stored_api_key = isset($options['general']['apikey']) ? $options['general']['apikey'] : null;
204
205 if (empty($stored_api_key)) {
206 return new WP_Error(
207 'rest_forbidden',
208 'No API key configured in MetaSync settings.',
209 ['status' => 401]
210 );
211 }
212
213 if (!hash_equals($stored_api_key, $api_key)) {
214 return new WP_Error(
215 'rest_forbidden',
216 'Invalid API key.',
217 ['status' => 401]
218 );
219 }
220
221 return true;
222 }
223
224 /**
225 * Validate setting key exists
226 *
227 * @param string $key
228 * @return bool
229 */
230 public function validate_setting_key($key) {
231 return array_key_exists($key, self::$default_settings);
232 }
233
234 /**
235 * Handle GET request - Retrieve all persistence settings
236 *
237 * @param WP_REST_Request $request
238 * @return WP_REST_Response
239 */
240 public function handle_get_settings($request) {
241 $settings = self::get_settings();
242
243 return rest_ensure_response([
244 'success' => true,
245 'data' => $settings,
246 'message' => 'Persistence settings retrieved successfully',
247 ]);
248 }
249
250 /**
251 * Handle POST request - Update persistence settings
252 *
253 * @param WP_REST_Request $request
254 * @return WP_REST_Response
255 */
256 public function handle_update_settings($request) {
257 $params = $request->get_json_params();
258
259 if (empty($params)) {
260 $params = $request->get_body_params();
261 }
262
263 if (empty($params)) {
264 return rest_ensure_response([
265 'success' => false,
266 'message' => 'No settings provided',
267 ]);
268 }
269
270 # Get current settings
271 $current_settings = self::get_settings();
272 $updated_keys = [];
273
274 # Update only provided settings
275 foreach ($params as $key => $value) {
276 # Skip non-setting keys (like apikey)
277 if (!array_key_exists($key, self::$default_settings)) {
278 continue;
279 }
280
281 # Convert to boolean safely
282 $bool_value = false;
283 if (is_bool($value)) {
284 $bool_value = $value;
285 } elseif (is_string($value)) {
286 $bool_value = in_array(strtolower($value), ['true', '1', 'yes'], true);
287 } elseif (is_numeric($value)) {
288 $bool_value = (bool) $value;
289 } else {
290 # Use filter_var for other types
291 $bool_value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
292 }
293
294 $current_settings[$key] = $bool_value;
295 $updated_keys[] = $key;
296 }
297
298 # Save updated settings
299 $saved = update_option(self::OPTION_NAME, $current_settings);
300
301 # update_option returns false when value is unchanged (not an error) or on DB failure.
302 # Verify by reading back:
303 $verified = get_option(self::OPTION_NAME, []);
304 $success = empty(array_diff_key($current_settings, $verified));
305
306 return rest_ensure_response([
307 'success' => $success,
308 'data' => $current_settings,
309 'updated_keys' => $updated_keys,
310 'message' => $success
311 ? sprintf('Updated %d persistence setting(s)', count($updated_keys))
312 : 'Settings may not have been saved. Please try again.',
313 ]);
314 }
315
316 /**
317 * Handle GET request for single setting
318 *
319 * @param WP_REST_Request $request
320 * @return WP_REST_Response
321 */
322 public function handle_get_single_setting($request) {
323 $key = $request->get_param('setting_key');
324 $settings = self::get_settings();
325
326 if (!isset($settings[$key])) {
327 return new WP_REST_Response([
328 'success' => false,
329 'message' => sprintf('Unknown setting key: %s', $key),
330 ], 404);
331 }
332
333 return rest_ensure_response([
334 'success' => true,
335 'data' => [
336 'key' => $key,
337 'value' => $settings[$key],
338 ],
339 ]);
340 }
341
342 /**
343 * Handle PUT request for single setting
344 *
345 * @param WP_REST_Request $request
346 * @return WP_REST_Response
347 */
348 public function handle_update_single_setting($request) {
349 $key = $request->get_param('setting_key');
350 $value = $request->get_param('value');
351
352 # Get body params if not in URL params
353 if ($value === null) {
354 $body = $request->get_json_params();
355 $value = $body['value'] ?? null;
356 }
357
358 if ($value === null) {
359 return new WP_REST_Response([
360 'success' => false,
361 'message' => 'Value is required',
362 ], 400);
363 }
364
365 # Convert to boolean safely
366 $bool_value = false;
367 if (is_bool($value)) {
368 $bool_value = $value;
369 } elseif (is_string($value)) {
370 $bool_value = in_array(strtolower($value), ['true', '1', 'yes'], true);
371 } elseif (is_numeric($value)) {
372 $bool_value = (bool) $value;
373 } else {
374 $bool_value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
375 }
376
377 # Update the specific setting
378 $settings = self::get_settings();
379 $settings[$key] = $bool_value;
380 update_option(self::OPTION_NAME, $settings);
381
382 return rest_ensure_response([
383 'success' => true,
384 'data' => [
385 'key' => $key,
386 'value' => $bool_value,
387 ],
388 'message' => sprintf('Setting "%s" updated to %s', $key, $bool_value ? 'true' : 'false'),
389 ]);
390 }
391
392 /**
393 * Get all persistence settings
394 *
395 * @return array
396 */
397 public static function get_settings() {
398 $saved_settings = get_option(self::OPTION_NAME, []);
399
400 # Merge with defaults to ensure all keys exist
401 return array_merge(self::$default_settings, $saved_settings);
402 }
403
404 /**
405 * Get a specific persistence setting
406 *
407 * @param string $key
408 * @return bool
409 */
410 public static function get_setting($key) {
411 $settings = self::get_settings();
412 return $settings[$key] ?? false;
413 }
414
415 /**
416 * Check if a specific data type should be persisted
417 *
418 * @param string $key
419 * @return bool
420 */
421 public static function should_persist($key) {
422 return self::get_setting($key) === true;
423 }
424
425 /**
426 * Set a specific persistence setting
427 *
428 * @param string $key
429 * @param bool $value
430 * @return bool
431 */
432 public static function set_setting($key, $value) {
433 if (!array_key_exists($key, self::$default_settings)) {
434 return false;
435 }
436
437 $settings = self::get_settings();
438 $settings[$key] = (bool) $value;
439
440 return update_option(self::OPTION_NAME, $settings);
441 }
442
443 /**
444 * Reset all settings to defaults
445 *
446 * @return bool
447 */
448 public static function reset_settings() {
449 return update_option(self::OPTION_NAME, self::$default_settings);
450 }
451
452 /**
453 * Get available setting keys
454 *
455 * @return array
456 */
457 public static function get_available_keys() {
458 return array_keys(self::$default_settings);
459 }
460
461 /**
462 * Delete all persistence settings (used on plugin uninstall)
463 *
464 * @return bool
465 */
466 public static function delete_settings() {
467 return delete_option(self::OPTION_NAME);
468 }
469 }
470
471
472
473
474