PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.7
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.7
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.6.7, at otto/class-metasync-otto-persistence-settings.php

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