PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.17
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.17
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 2.6.4 2.6.3 All 139 releases
metasync / includes / class-metasync-api-key-monitor.php

class-metasync-api-key-monitor.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.17, at includes/class-metasync-api-key-monitor.php

378 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7
8 /**
9 * API Key Change Monitor for MetaSync Plugin
10 *
11 * @link https://searchatlas.com
12 * @since 1.0.0
13 *
14 * @package Metasync
15 * @subpackage Metasync/includes
16 */
17
18 /**
19 * Centralized API Key Change Monitor
20 *
21 * This class monitors all API key changes and ensures the backend is notified
22 * via Heartbeat API whenever the Plugin Auth Token or Search Atlas API Key changes.
23 *
24 * @since 1.0.0
25 * @package Metasync
26 * @subpackage Metasync/includes
27 * @author Engineering Team <[email protected]>
28 */
29 class Metasync_API_Key_Monitor
30 {
31 /**
32 * Instance of this class
33 */
34 private static $instance = null;
35
36 /**
37 * Flag to prevent infinite loops during monitoring
38 */
39 private $monitoring_active = false;
40
41 /**
42 * Get singleton instance
43 */
44 public static function get_instance()
45 {
46 if (self::$instance === null) {
47 self::$instance = new self();
48 }
49 return self::$instance;
50 }
51
52 /**
53 * Initialize the monitor
54 */
55 public function __construct()
56 {
57 // Only initialize once
58 if (self::$instance !== null) {
59 return;
60 }
61
62 $this->init_hooks();
63 }
64
65 /**
66 * Initialize WordPress hooks
67 */
68 private function init_hooks()
69 {
70 // Monitor option changes (catches direct database updates)
71 add_action('update_option_metasync_options', array($this, 'on_option_update'), 10, 2);
72
73 // Monitor option additions (for fresh installs)
74 add_action('add_option_metasync_options', array($this, 'on_option_add'), 10, 2);
75
76 // Provide centralized heartbeat trigger method
77 add_action('metasync_api_key_changed', array($this, 'trigger_heartbeat_for_key_change'), 10, 3);
78 }
79
80 /**
81 * Handle option updates
82 */
83 public function on_option_update($old_value, $new_value)
84 {
85 $this->analyze_and_respond_to_changes($old_value, $new_value, 'option_update');
86 }
87
88 /**
89 * Handle option additions (fresh installs)
90 */
91 public function on_option_add($option_name, $new_value)
92 {
93 // For new installations, treat as change from empty state
94 $this->analyze_and_respond_to_changes(array(), $new_value, 'option_add');
95 }
96
97 /**
98 * Analyze API key changes and trigger appropriate responses
99 */
100 private function analyze_and_respond_to_changes($old_value, $new_value, $trigger_context = 'unknown')
101 {
102 // Prevent infinite loops
103 if ($this->monitoring_active) {
104 return;
105 }
106
107 try {
108 $this->monitoring_active = true;
109
110 // Normalize values
111 $old_value = is_array($old_value) ? $old_value : array();
112 $new_value = is_array($new_value) ? $new_value : array();
113
114 // Extract API keys
115 $changes = $this->detect_api_key_changes($old_value, $new_value);
116
117 if (!empty($changes['changes_detected'])) {
118 $this->log_changes($changes, $trigger_context);
119 $this->trigger_heartbeat_response($changes, $trigger_context);
120 }
121
122 } catch (Exception $e) {
123 error_log('MetaSync API Key Monitor Error: ' . $e->getMessage());
124 } finally {
125 $this->monitoring_active = false;
126 }
127 }
128
129 /**
130 * Detect API key changes between old and new values
131 */
132 private function detect_api_key_changes($old_value, $new_value)
133 {
134 // Extract values
135 $old_plugin_auth_token = $old_value['general']['apikey'] ?? '';
136 $new_plugin_auth_token = $new_value['general']['apikey'] ?? '';
137 $old_searchatlas_api_key = $old_value['general']['searchatlas_api_key'] ?? '';
138 $new_searchatlas_api_key = $new_value['general']['searchatlas_api_key'] ?? '';
139
140 // Analyze changes
141 $plugin_token_changed = $old_plugin_auth_token !== $new_plugin_auth_token;
142 $searchatlas_key_changed = $old_searchatlas_api_key !== $new_searchatlas_api_key;
143
144 return array(
145 'changes_detected' => $plugin_token_changed || $searchatlas_key_changed,
146 'plugin_auth_token' => array(
147 'changed' => $plugin_token_changed,
148 'old' => $old_plugin_auth_token,
149 'new' => $new_plugin_auth_token,
150 'action' => $this->determine_change_action($old_plugin_auth_token, $new_plugin_auth_token)
151 ),
152 'searchatlas_api_key' => array(
153 'changed' => $searchatlas_key_changed,
154 'old' => $old_searchatlas_api_key,
155 'new' => $new_searchatlas_api_key,
156 'action' => $this->determine_change_action($old_searchatlas_api_key, $new_searchatlas_api_key)
157 )
158 );
159 }
160
161 /**
162 * Determine the type of change (added, removed, changed)
163 */
164 private function determine_change_action($old_value, $new_value)
165 {
166 if (empty($old_value) && !empty($new_value)) {
167 return 'added';
168 } elseif (!empty($old_value) && empty($new_value)) {
169 return 'removed';
170 } elseif (!empty($old_value) && !empty($new_value) && $old_value !== $new_value) {
171 return 'changed';
172 }
173 return 'none';
174 }
175
176 /**
177 * Log API key changes
178 */
179 private function log_changes($changes, $context)
180 {
181 $log_messages = array();
182
183 if ($changes['plugin_auth_token']['changed']) {
184 $token_change = $changes['plugin_auth_token'];
185 $log_messages[] = sprintf('Plugin Auth Token %s: %s → %s',
186 $token_change['action'],
187 empty($token_change['old']) ? '(empty)' : substr($token_change['old'], 0, 8) . '...',
188 empty($token_change['new']) ? '(empty)' : substr($token_change['new'], 0, 8) . '...'
189 );
190 }
191
192 if ($changes['searchatlas_api_key']['changed']) {
193 $key_change = $changes['searchatlas_api_key'];
194 $log_messages[] = sprintf('Search Atlas API Key %s: %s → %s',
195 $key_change['action'],
196 empty($key_change['old']) ? '(empty)' : substr($key_change['old'], 0, 8) . '...',
197 empty($key_change['new']) ? '(empty)' : substr($key_change['new'], 0, 8) . '...'
198 );
199 }
200
201 if (!empty($log_messages)) {
202 // Use centralized logging for consistent formatting
203 $details = array(
204 'context' => $context,
205 'changes' => implode(' | ', $log_messages)
206 );
207
208 if (class_exists('Metasync')) {
209 Metasync::log_api_key_event('API key change detected', 'multiple', $details, 'info');
210 } else {
211 // Fallback to basic error_log if main class not available
212 error_log('MetaSync API Key Monitor [' . $context . ']: ' . implode(' | ', $log_messages));
213 }
214 }
215 }
216
217 /**
218 * Trigger heartbeat response based on changes
219 */
220 private function trigger_heartbeat_response($changes, $context)
221 {
222 // Determine the most appropriate heartbeat trigger message
223 $heartbeat_context = $this->build_heartbeat_context($changes, $context);
224
225 // Fire the centralized action for heartbeat triggering (always fire this for other plugins to hook into)
226 do_action('metasync_api_key_changed', $changes, $context, $heartbeat_context);
227
228 // Only trigger immediate heartbeat if Search Atlas API key is present
229 // This follows the business rule that heartbeat API should only be called when Search Atlas API key is set
230 if ($this->should_trigger_heartbeat($changes)) {
231 do_action('metasync_heartbeat_state_key_pending'); // PR3: burst mode
232 do_action('metasync_trigger_immediate_heartbeat', $heartbeat_context);
233 } else {
234 $this->log_heartbeat_skip_reason($changes, $heartbeat_context);
235 }
236 }
237
238 /**
239 * Determine if heartbeat should be triggered based on API key availability
240 * Heartbeat API should only be called when Search Atlas API Key is present
241 */
242 private function should_trigger_heartbeat($changes)
243 {
244 // Get current Search Atlas API key
245 $current_options = is_array(Metasync::get_option()) ? Metasync::get_option() : array();
246 $current_searchatlas_key = $current_options['general']['searchatlas_api_key'] ?? '';
247
248 // Always trigger heartbeat if Search Atlas API key is being changed
249 if ($changes['searchatlas_api_key']['changed']) {
250 // If the new Search Atlas API key is not empty, trigger heartbeat
251 if (!empty($changes['searchatlas_api_key']['new'])) {
252 return true;
253 }
254 // If Search Atlas API key was removed, still trigger to update backend status
255 if (!empty($changes['searchatlas_api_key']['old'])) {
256 return true;
257 }
258 }
259
260 // For Plugin Auth Token changes, only trigger if Search Atlas API key exists
261 if ($changes['plugin_auth_token']['changed']) {
262 if (!empty($current_searchatlas_key)) {
263 return true;
264 }
265 // Plugin Auth Token changed but no Search Atlas API key present
266 return false;
267 }
268
269 // No changes that require heartbeat
270 return false;
271 }
272
273 /**
274 * Log why heartbeat trigger was skipped
275 */
276 private function log_heartbeat_skip_reason($changes, $context)
277 {
278 $skip_reasons = array();
279
280 if ($changes['plugin_auth_token']['changed']) {
281 $skip_reasons[] = 'Plugin Auth Token changed but Search Atlas API key not configured';
282 }
283
284 if ($changes['searchatlas_api_key']['changed'] && empty($changes['searchatlas_api_key']['new'])) {
285 $skip_reasons[] = 'Search Atlas API key was removed';
286 }
287
288 if (!empty($skip_reasons)) {
289 error_log('MetaSync API Key Monitor: Skipping heartbeat trigger - ' . implode(' | ', $skip_reasons) . ' - Context: ' . $context);
290 }
291 }
292
293 /**
294 * Build descriptive context for heartbeat trigger
295 */
296 private function build_heartbeat_context($changes, $context)
297 {
298 $context_parts = array('API Key Monitor');
299
300 if ($changes['plugin_auth_token']['changed']) {
301 $context_parts[] = 'Plugin Auth Token ' . $changes['plugin_auth_token']['action'];
302 }
303
304 if ($changes['searchatlas_api_key']['changed']) {
305 $context_parts[] = 'Search Atlas API Key ' . $changes['searchatlas_api_key']['action'];
306 }
307
308 $context_parts[] = '(' . $context . ')';
309
310 return implode(' - ', $context_parts);
311 }
312
313 /**
314 * Handle centralized heartbeat trigger action
315 */
316 public function trigger_heartbeat_for_key_change($changes, $context, $heartbeat_context)
317 {
318 // This method can be extended to handle specific logic based on the type of change
319 // For now, it serves as a central point for other plugins/components to hook into
320
321 // Log the centralized trigger
322
323 }
324
325 /**
326 * Manual API key change notification (for programmatic triggers)
327 */
328 public static function notify_api_key_change($key_type, $old_value, $new_value, $context = 'manual')
329 {
330 $instance = self::get_instance();
331
332 // Build simulated change array
333 if ($key_type === 'plugin_auth_token') {
334 $changes = array(
335 'changes_detected' => true,
336 'plugin_auth_token' => array(
337 'changed' => true,
338 'old' => $old_value,
339 'new' => $new_value,
340 'action' => $instance->determine_change_action($old_value, $new_value)
341 ),
342 'searchatlas_api_key' => array('changed' => false)
343 );
344 } elseif ($key_type === 'searchatlas_api_key') {
345 $changes = array(
346 'changes_detected' => true,
347 'plugin_auth_token' => array('changed' => false),
348 'searchatlas_api_key' => array(
349 'changed' => true,
350 'old' => $old_value,
351 'new' => $new_value,
352 'action' => $instance->determine_change_action($old_value, $new_value)
353 )
354 );
355 } else {
356 return false;
357 }
358
359 $instance->log_changes($changes, $context);
360 $instance->trigger_heartbeat_response($changes, $context);
361
362 return true;
363 }
364
365 /**
366 * Get monitoring status for debugging
367 */
368 public function get_monitoring_status()
369 {
370 return array(
371 'class_loaded' => true,
372 'hooks_active' => has_action('update_option_metasync_options', array($this, 'on_option_update')),
373 'monitoring_active' => $this->monitoring_active,
374 'instance_initialized' => self::$instance !== null
375 );
376 }
377 }
378