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

469 lines 14.0 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 Authorization: Bearer header (preferred), X-API-Key header, or ?apikey= query param for backward compat
167 $api_key = '';
168
169 $auth_header = $request->get_header('authorization');
170 if (!empty($auth_header) && preg_match('/^Bearer\s+(.+)$/i', $auth_header, $matches)) {
171 $api_key = sanitize_text_field($matches[1]);
172 }
173
174 if (empty($api_key)) {
175 $header_key = $request->get_header('x-api-key');
176 if (!empty($header_key)) {
177 $api_key = sanitize_text_field($header_key);
178 }
179 }
180
181 if (empty($api_key)) {
182 $get_data = array_map('sanitize_text_field', $_GET);
183 if (!empty($get_data['apikey'])) {
184 $api_key = $get_data['apikey'];
185 // Deprecated: ?apikey= query-param auth — switch callers to Authorization: Bearer
186 }
187 }
188
189 if (empty($api_key)) {
190 return new WP_Error(
191 'rest_forbidden',
192 'API key is required. Pass Authorization: Bearer header, X-API-Key header, or ?apikey= query parameter.',
193 ['status' => 401]
194 );
195 }
196
197 $options = get_option('metasync_options', []);
198 $stored_api_key = isset($options['general']['apikey']) ? $options['general']['apikey'] : null;
199
200 if (empty($stored_api_key)) {
201 return new WP_Error(
202 'rest_forbidden',
203 'No API key configured in MetaSync settings.',
204 ['status' => 401]
205 );
206 }
207
208 if (!hash_equals($stored_api_key, $api_key)) {
209 return new WP_Error(
210 'rest_forbidden',
211 'Invalid API key.',
212 ['status' => 401]
213 );
214 }
215
216 return true;
217 }
218
219 /**
220 * Validate setting key exists
221 *
222 * @param string $key
223 * @return bool
224 */
225 public function validate_setting_key($key) {
226 return array_key_exists($key, self::$default_settings);
227 }
228
229 /**
230 * Handle GET request - Retrieve all persistence settings
231 *
232 * @param WP_REST_Request $request
233 * @return WP_REST_Response
234 */
235 public function handle_get_settings($request) {
236 $settings = self::get_settings();
237
238 return rest_ensure_response([
239 'success' => true,
240 'data' => $settings,
241 'message' => 'Persistence settings retrieved successfully',
242 ]);
243 }
244
245 /**
246 * Handle POST request - Update persistence settings
247 *
248 * @param WP_REST_Request $request
249 * @return WP_REST_Response
250 */
251 public function handle_update_settings($request) {
252 $params = $request->get_json_params();
253
254 if (empty($params)) {
255 $params = $request->get_body_params();
256 }
257
258 if (empty($params)) {
259 return rest_ensure_response([
260 'success' => false,
261 'message' => 'No settings provided',
262 ]);
263 }
264
265 # Get current settings
266 $current_settings = self::get_settings();
267 $updated_keys = [];
268
269 # Update only provided settings
270 foreach ($params as $key => $value) {
271 # Skip non-setting keys (like apikey)
272 if (!array_key_exists($key, self::$default_settings)) {
273 continue;
274 }
275
276 # Convert to boolean safely
277 $bool_value = false;
278 if (is_bool($value)) {
279 $bool_value = $value;
280 } elseif (is_string($value)) {
281 $bool_value = in_array(strtolower($value), ['true', '1', 'yes'], true);
282 } elseif (is_numeric($value)) {
283 $bool_value = (bool) $value;
284 } else {
285 # Use filter_var for other types
286 $bool_value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
287 }
288
289 $current_settings[$key] = $bool_value;
290 $updated_keys[] = $key;
291 }
292
293 # Save updated settings
294 $saved = update_option(self::OPTION_NAME, $current_settings);
295
296 # update_option returns false when value is unchanged (not an error) or on DB failure.
297 # Verify by reading back:
298 $verified = get_option(self::OPTION_NAME, []);
299 $success = empty(array_diff_key($current_settings, $verified));
300
301 return rest_ensure_response([
302 'success' => $success,
303 'data' => $current_settings,
304 'updated_keys' => $updated_keys,
305 'message' => $success
306 ? sprintf('Updated %d persistence setting(s)', count($updated_keys))
307 : 'Settings may not have been saved. Please try again.',
308 ]);
309 }
310
311 /**
312 * Handle GET request for single setting
313 *
314 * @param WP_REST_Request $request
315 * @return WP_REST_Response
316 */
317 public function handle_get_single_setting($request) {
318 $key = $request->get_param('setting_key');
319 $settings = self::get_settings();
320
321 if (!isset($settings[$key])) {
322 return new WP_REST_Response([
323 'success' => false,
324 'message' => sprintf('Unknown setting key: %s', $key),
325 ], 404);
326 }
327
328 return rest_ensure_response([
329 'success' => true,
330 'data' => [
331 'key' => $key,
332 'value' => $settings[$key],
333 ],
334 ]);
335 }
336
337 /**
338 * Handle PUT request for single setting
339 *
340 * @param WP_REST_Request $request
341 * @return WP_REST_Response
342 */
343 public function handle_update_single_setting($request) {
344 $key = $request->get_param('setting_key');
345 $value = $request->get_param('value');
346
347 # Get body params if not in URL params
348 if ($value === null) {
349 $body = $request->get_json_params();
350 $value = $body['value'] ?? null;
351 }
352
353 if ($value === null) {
354 return new WP_REST_Response([
355 'success' => false,
356 'message' => 'Value is required',
357 ], 400);
358 }
359
360 # Convert to boolean safely
361 $bool_value = false;
362 if (is_bool($value)) {
363 $bool_value = $value;
364 } elseif (is_string($value)) {
365 $bool_value = in_array(strtolower($value), ['true', '1', 'yes'], true);
366 } elseif (is_numeric($value)) {
367 $bool_value = (bool) $value;
368 } else {
369 $bool_value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
370 }
371
372 # Update the specific setting
373 $settings = self::get_settings();
374 $settings[$key] = $bool_value;
375 update_option(self::OPTION_NAME, $settings);
376
377 return rest_ensure_response([
378 'success' => true,
379 'data' => [
380 'key' => $key,
381 'value' => $bool_value,
382 ],
383 'message' => sprintf('Setting "%s" updated to %s', $key, $bool_value ? 'true' : 'false'),
384 ]);
385 }
386
387 /**
388 * Get all persistence settings
389 *
390 * @return array
391 */
392 public static function get_settings() {
393 $saved_settings = get_option(self::OPTION_NAME, []);
394
395 # Merge with defaults to ensure all keys exist
396 return array_merge(self::$default_settings, $saved_settings);
397 }
398
399 /**
400 * Get a specific persistence setting
401 *
402 * @param string $key
403 * @return bool
404 */
405 public static function get_setting($key) {
406 $settings = self::get_settings();
407 return $settings[$key] ?? false;
408 }
409
410 /**
411 * Check if a specific data type should be persisted
412 *
413 * @param string $key
414 * @return bool
415 */
416 public static function should_persist($key) {
417 return self::get_setting($key) === true;
418 }
419
420 /**
421 * Set a specific persistence setting
422 *
423 * @param string $key
424 * @param bool $value
425 * @return bool
426 */
427 public static function set_setting($key, $value) {
428 if (!array_key_exists($key, self::$default_settings)) {
429 return false;
430 }
431
432 $settings = self::get_settings();
433 $settings[$key] = (bool) $value;
434
435 return update_option(self::OPTION_NAME, $settings);
436 }
437
438 /**
439 * Reset all settings to defaults
440 *
441 * @return bool
442 */
443 public static function reset_settings() {
444 return update_option(self::OPTION_NAME, self::$default_settings);
445 }
446
447 /**
448 * Get available setting keys
449 *
450 * @return array
451 */
452 public static function get_available_keys() {
453 return array_keys(self::$default_settings);
454 }
455
456 /**
457 * Delete all persistence settings (used on plugin uninstall)
458 *
459 * @return bool
460 */
461 public static function delete_settings() {
462 return delete_option(self::OPTION_NAME);
463 }
464 }
465
466
467
468
469