PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.6
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.6
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 / includes / class-metasync-otto-cache-manager.php

class-metasync-otto-cache-manager.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.6, at includes/class-metasync-otto-cache-manager.php

742 lines 35.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 /**
7 * OTTO Cache Management manager.
8 *
9 * Extracted from Metasync_Admin to keep the admin class focused on UI concerns.
10 * Handles OTTO transient cache rendering, clearing, and excluded-URL AJAX operations.
11 *
12 * @package Metasync
13 * @subpackage Metasync/includes
14 */
15 class Metasync_Otto_Cache_Manager
16 {
17 /** @var self|null */
18 private static $instance = null;
19
20 /**
21 * Get singleton instance.
22 *
23 * @return self
24 */
25 public static function instance()
26 {
27 if (self::$instance === null) {
28 self::$instance = new self();
29 }
30 return self::$instance;
31 }
32
33 private function __construct() {}
34
35 /**
36 * Get the admin page slug.
37 */
38 private function get_page_slug()
39 {
40 return Metasync_Admin::$page_slug;
41 }
42
43 // ------------------------------------------------------------------
44 // Rendering
45 // ------------------------------------------------------------------
46
47 /**
48 * Render OTTO cache management section for inclusion in Advanced settings
49 */
50 public function render_otto_cache_management()
51 {
52 if (!class_exists('Metasync_Otto_Transient_Cache')) {
53 echo '<div class="notice notice-error inline"><p>';
54 echo '❌ <strong>Error:</strong> Transient Cache class not found.';
55 echo '</p></div>';
56 return;
57 }
58
59 $cache_count = Metasync_Otto_Transient_Cache::get_cache_count();
60
61 ?>
62 <!-- Cache Plugin Management -->
63 <div style="margin-bottom: 30px;">
64 <h4 style="margin-top: 0; color: var(--dashboard-text-primary);">Clear All Cache Plugins</h4>
65 <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);">Clear all cache plugins to ensure changes are visible immediately.</p>
66
67 <?php
68 if (class_exists('Metasync_Cache_Purge')) {
69 try {
70 $cache_purge = Metasync_Cache_Purge::get_instance();
71 $active_cache_plugins = $cache_purge->get_active_cache_plugins();
72
73 if (!empty($active_cache_plugins)) {
74 echo '<p style="color: var(--dashboard-text-primary);"><strong>Active Cache Plugins Detected:</strong></p>';
75 echo '<ul style="margin-bottom: 15px; color: var(--dashboard-text-primary);">';
76 foreach ($active_cache_plugins as $plugin_name) {
77 echo '<li>�
78 ' . esc_html($plugin_name) . '</li>';
79 }
80 echo '</ul>';
81 } else {
82 echo '<p style="color: var(--dashboard-text-secondary);">ℹ️ No cache plugins detected.</p>';
83 }
84 } catch (Exception $e) {
85 echo '<p style="color: var(--dashboard-error);">⚠️ Error: ' . esc_html($e->getMessage()) . '</p>';
86 }
87 } else {
88 echo '<p style="color: var(--dashboard-error);">⚠️ Cache Purge class not loaded.</p>';
89 }
90 ?>
91
92 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="margin-top: 15px;">
93 <input type="hidden" name="action" value="metasync_clear_all_cache_plugins" />
94 <?php wp_nonce_field('metasync_clear_cache_nonce', 'clear_cache_nonce'); ?>
95 <button type="submit" class="metasync-btn-primary" style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); display: inline-block; width: auto; min-width: 240px; max-width: fit-content;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(0, 0, 0, 0.15)';" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0, 0, 0, 0.1)';">
96 🔄 Clear All Cache Plugins
97 </button>
98 <p class="description" style="margin-top: 10px; color: var(--dashboard-text-secondary);">This will clear cache from WP Rocket, LiteSpeed, W3 Total Cache, and all other detected cache plugins.</p>
99 </form>
100
101 <?php
102 if (isset($_GET['cache_cleared']) && $_GET['cache_cleared'] == '1') {
103 $cleared = isset($_GET['cleared']) ? intval($_GET['cleared']) : 0;
104 $failed = isset($_GET['failed']) ? intval($_GET['failed']) : 0;
105 $plugins = isset($_GET['plugins']) ? sanitize_text_field($_GET['plugins']) : '';
106
107 if ($cleared > 0) {
108 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
109 echo '�
110 <strong>Success!</strong> Cleared cache for ' . $cleared . ' plugin(s)';
111 if ($plugins) {
112 echo ': ' . esc_html($plugins);
113 }
114 echo '</p></div>';
115 } else {
116 echo '<div class="notice notice-info inline" style="margin-top: 15px;"><p>';
117 echo 'ℹ️ No cache plugins found to clear. WordPress object cache was cleared.';
118 echo '</p></div>';
119 }
120
121 if ($failed > 0) {
122 echo '<div class="notice notice-warning inline" style="margin-top: 15px;"><p>';
123 echo '⚠️ Failed to clear ' . $failed . ' plugin(s).';
124 echo '</p></div>';
125 }
126 }
127
128 if (isset($_GET['cache_error']) && $_GET['cache_error'] == '1') {
129 $message = isset($_GET['message']) ? urldecode(sanitize_text_field($_GET['message'])) : '';
130 if (empty($message)) {
131 $message = 'An unknown error occurred while clearing cache. Please check error logs for details.';
132 }
133 echo '<div class="notice notice-error inline" style="margin-top: 15px;"><p>';
134 echo '❌ <strong>Error clearing cache:</strong> ' . esc_html($message);
135 echo '</p></div>';
136 }
137 ?>
138 </div>
139
140 <!-- Hosting Cache Integration -->
141 <?php
142 $hosting_defaults = array('wpengine_enabled' => true, 'kinsta_enabled' => true);
143 $hosting_settings = wp_parse_args(get_option('metasync_hosting_cache_options', array()), $hosting_defaults);
144 $wpe_detected = class_exists('WpeCommon');
145 $kinsta_detected = class_exists('KinstaCache');
146 ?>
147 <div style="margin-bottom: 30px;">
148 <h4 style="margin-top: 0; color: var(--dashboard-text-primary);">Hosting Cache Integration</h4>
149 <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);">
150 Use your hosting provider's native API to purge the <strong>entire site cache</strong> in one click.
151 These options are independent of cache plugins and target the server-level cache layer.
152 </p>
153
154 <!-- Detection status badges -->
155 <div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;">
156 <span style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500;
157 background: <?php echo $wpe_detected ? 'rgba(34,197,94,0.12)' : 'rgba(156,163,175,0.12)'; ?>;
158 color: <?php echo $wpe_detected ? '#22c55e' : 'var(--dashboard-text-secondary)'; ?>;
159 border: 1px solid <?php echo $wpe_detected ? 'rgba(34,197,94,0.3)' : 'rgba(156,163,175,0.3)'; ?>;">
160 <?php echo $wpe_detected ? '�
161 ' : '⬜'; ?> WP Engine <?php echo $wpe_detected ? '(detected)' : '(not detected)'; ?>
162 </span>
163 <span style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500;
164 background: <?php echo $kinsta_detected ? 'rgba(34,197,94,0.12)' : 'rgba(156,163,175,0.12)'; ?>;
165 color: <?php echo $kinsta_detected ? '#22c55e' : 'var(--dashboard-text-secondary)'; ?>;
166 border: 1px solid <?php echo $kinsta_detected ? 'rgba(34,197,94,0.3)' : 'rgba(156,163,175,0.3)'; ?>;">
167 <?php echo $kinsta_detected ? '�
168 ' : '⬜'; ?> Kinsta <?php echo $kinsta_detected ? '(detected)' : '(not detected)'; ?>
169 </span>
170 </div>
171
172 <!-- Settings toggles -->
173 <div style="background: rgba(255,255,255,0.02); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
174 <h5 style="margin: 0 0 14px 0; color: var(--dashboard-text-primary);">Enable Native Cache Purge</h5>
175
176 <label style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px; cursor: pointer;">
177 <input type="checkbox"
178 id="metasync-hc-wpengine"
179 <?php checked(true, !empty($hosting_settings['wpengine_enabled'])); ?>
180 <?php echo !$wpe_detected ? 'disabled' : ''; ?>
181 style="width: 16px; height: 16px; cursor: <?php echo $wpe_detected ? 'pointer' : 'not-allowed'; ?>;" />
182 <span style="color: var(--dashboard-text-primary); font-weight: 500;">WP Engine</span>
183 <span style="color: var(--dashboard-text-secondary); font-size: 12px;">— purges Varnish + Memcached</span>
184 </label>
185
186 <label style="display: flex; align-items: center; gap: 10px; margin-bottom: 16px; cursor: pointer;">
187 <input type="checkbox"
188 id="metasync-hc-kinsta"
189 <?php checked(true, !empty($hosting_settings['kinsta_enabled'])); ?>
190 <?php echo !$kinsta_detected ? 'disabled' : ''; ?>
191 style="width: 16px; height: 16px; cursor: <?php echo $kinsta_detected ? 'pointer' : 'not-allowed'; ?>;" />
192 <span style="color: var(--dashboard-text-primary); font-weight: 500;">Kinsta</span>
193 <span style="color: var(--dashboard-text-secondary); font-size: 12px;">— purges full-page cache (kinsta_cache_purge_full)</span>
194 </label>
195
196 <div style="display: flex; align-items: center; gap: 12px;">
197 <button type="button"
198 id="metasync-hc-save-btn"
199 class="metasync-btn-primary"
200 style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 9px 18px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease;"
201 onmouseover="this.style.transform='translateY(-1px)';"
202 onmouseout="this.style.transform='translateY(0)';">
203 Save Settings
204 </button>
205 <span id="metasync-hc-save-msg" style="display: none; font-size: 13px;"></span>
206 </div>
207
208 <input type="hidden" id="metasync-hc-nonce" value="<?php echo wp_create_nonce('metasync_hosting_cache_nonce'); ?>" />
209 </div>
210
211 <!-- Purge button -->
212 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
213 <input type="hidden" name="action" value="metasync_purge_hosting_cache" />
214 <?php wp_nonce_field('metasync_hosting_cache_purge_nonce', 'hosting_cache_purge_nonce'); ?>
215 <button type="submit"
216 class="metasync-btn-primary"
217 style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1); display: inline-block; min-width: 240px; max-width: fit-content;"
218 onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(0,0,0,0.15)';"
219 onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0,0,0,0.1)';">
220 <span class="dashicons dashicons-update" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Purge Entire Hosting Cache
221 </button>
222 <p class="description" style="margin-top: 10px; color: var(--dashboard-text-secondary);">
223 Triggers a full-site cache purge using the native WP Engine and/or Kinsta APIs (based on toggles above).
224 </p>
225 </form>
226
227 <?php
228 // Hosting cache result messages
229 if (isset($_GET['hosting_cache_cleared']) && $_GET['hosting_cache_cleared'] == '1') {
230 $hc_cleared = isset($_GET['hc_cleared']) ? sanitize_text_field(urldecode($_GET['hc_cleared'])) : '';
231 $hc_failed = isset($_GET['hc_failed']) ? sanitize_text_field(urldecode($_GET['hc_failed'])) : '';
232 $hc_not_detected = isset($_GET['hc_not_detected']) ? sanitize_text_field(urldecode($_GET['hc_not_detected'])) : '';
233
234 if ($hc_cleared) {
235 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
236 echo '�
237 <strong>Success!</strong> Purged hosting cache on: ' . esc_html($hc_cleared);
238 echo '</p></div>';
239 }
240 if ($hc_failed) {
241 echo '<div class="notice notice-error inline" style="margin-top: 10px;"><p>';
242 echo '❌ <strong>Failed</strong> to purge: ' . esc_html($hc_failed);
243 echo '</p></div>';
244 }
245 if ($hc_not_detected && !$hc_cleared && !$hc_failed) {
246 echo '<div class="notice notice-info inline" style="margin-top: 10px;"><p>';
247 echo 'ℹ️ No enabled hosting providers were detected on this server (' . esc_html($hc_not_detected) . ').';
248 echo '</p></div>';
249 }
250 }
251 ?>
252
253 <script>
254 jQuery(document).ready(function($) {
255 $('#metasync-hc-save-btn').on('click', function() {
256 var $btn = $(this);
257 var $msg = $('#metasync-hc-save-msg');
258
259 $btn.prop('disabled', true).text('Saving…');
260
261 $.ajax({
262 url: ajaxurl,
263 type: 'POST',
264 data: {
265 action: 'metasync_save_hosting_cache_settings',
266 hosting_cache_nonce: $('#metasync-hc-nonce').val(),
267 wpengine_enabled: $('#metasync-hc-wpengine').is(':checked') ? '1' : '0',
268 kinsta_enabled: $('#metasync-hc-kinsta').is(':checked') ? '1' : '0',
269 },
270 success: function(response) {
271 if (response.success) {
272 $msg.text('�
273 Saved').css('color', '#22c55e').show();
274 } else {
275 $msg.text('❌ ' + (response.data.message || 'Save failed')).css('color', '#ef4444').show();
276 }
277 },
278 error: function() {
279 $msg.text('❌ Request failed').css('color', '#ef4444').show();
280 },
281 complete: function() {
282 $btn.prop('disabled', false).text('Save Settings');
283 setTimeout(function() { $msg.fadeOut(); }, 4000);
284 }
285 });
286 });
287 });
288 </script>
289 </div>
290
291 <!-- OTTO Transient Cache -->
292 <div style="margin-bottom: 30px;">
293 <h4 style="margin-top: 0; color: var(--dashboard-text-primary);"><?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> Transient Cache</h4>
294 <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);">
295 Manage <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> suggestions cache. Clearing cache will force fresh API calls on next page load.
296 </p>
297
298 <div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 4px; margin-bottom: 20px; border: 1px solid var(--dashboard-border);">
299 <strong style="color: var(--dashboard-text-primary);">Current Cache Status:</strong>
300 <span style="color: var(--dashboard-accent);"><?php echo esc_html($cache_count); ?> cached entries</span>
301 </div>
302
303 <?php
304 if (isset($_GET['otto_cache_cleared']) && $_GET['otto_cache_cleared'] == '1') {
305 $cleared_count = isset($_GET['count']) ? intval($_GET['count']) : 0;
306 $url = isset($_GET['url']) ? urldecode(sanitize_text_field($_GET['url'])) : '';
307
308 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
309 if (!empty($url)) {
310 echo '�
311 <strong>Success!</strong> Cleared cache for URL: <code>' . esc_html($url) . '</code> (' . $cleared_count . ' entries)';
312 } else {
313 echo '�
314 <strong>Success!</strong> Cleared entire transient cache (' . $cleared_count . ' entries)';
315 }
316 echo '</p></div>';
317 }
318
319 if (isset($_GET['otto_cache_error']) && $_GET['otto_cache_error'] == '1') {
320 $message = isset($_GET['message']) ? urldecode(sanitize_text_field($_GET['message'])) : 'An unknown error occurred.';
321 echo '<div class="notice notice-error inline" style="margin-top: 15px;"><p>';
322 echo '❌ <strong>Error:</strong> ' . esc_html($message);
323 echo '</p></div>';
324 }
325 ?>
326
327 <!-- Clear Entire Cache -->
328 <div style="margin-bottom: 30px; padding: 20px; border: 1px solid var(--dashboard-border); border-radius: 4px; background: rgba(255, 255, 255, 0.02);">
329 <h5 style="margin-top: 0; color: var(--dashboard-text-primary);">Clear Entire Transient Cache</h5>
330 <p style="color: var(--dashboard-text-secondary); margin-bottom: 15px;">
331 This will clear all <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> transient cache entries (suggestions, locks, stale cache, rate limits).
332 </p>
333 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
334 onsubmit="return confirm('Are you sure you want to clear the entire transient cache? This will force fresh API calls for all URLs.');">
335 <input type="hidden" name="action" value="metasync_clear_otto_cache_all" />
336 <?php wp_nonce_field('metasync_clear_otto_cache_nonce', 'clear_otto_cache_nonce'); ?>
337 <button type="submit" class="metasync-btn-primary" style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); display: inline-block; width: auto; min-width: 240px; max-width: fit-content;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(0, 0, 0, 0.15)';" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0, 0, 0, 0.1)';">
338 🗑️ Clear Entire Cache
339 </button>
340 </form>
341 </div>
342
343 <!-- Clear Cache by URL -->
344 <div style="padding: 20px; border: 1px solid var(--dashboard-border); border-radius: 4px; background: rgba(255, 255, 255, 0.02);">
345 <h5 style="margin-top: 0; color: var(--dashboard-text-primary);">Clear Cache by URL</h5>
346 <p style="color: var(--dashboard-text-secondary); margin-bottom: 15px;">
347 Enter a specific URL to clear its cached <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> suggestions. Use the full URL including protocol (e.g., https://example.com/page/).
348 </p>
349 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
350 <input type="hidden" name="action" value="metasync_clear_otto_cache_url" />
351 <?php wp_nonce_field('metasync_clear_otto_cache_nonce', 'clear_otto_cache_nonce'); ?>
352 <table class="form-table">
353 <tr>
354 <th scope="row">
355 <label for="otto_cache_url" style="color: var(--dashboard-text-primary);">URL to Clear</label>
356 </th>
357 <td>
358 <input type="url"
359 id="otto_cache_url"
360 name="otto_cache_url"
361 value="<?php echo isset($_GET['url']) ? esc_attr(urldecode(sanitize_text_field($_GET['url']))) : ''; ?>"
362 class="regular-text"
363 placeholder="https://example.com/page/"
364 required />
365 <p class="description" style="color: var(--dashboard-text-secondary);">Enter the full URL of the page whose cache you want to clear.</p>
366 </td>
367 </tr>
368 </table>
369 <button type="submit" class="metasync-btn-primary" style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); display: inline-block; width: auto; max-width: fit-content;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(0, 0, 0, 0.15)';" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0, 0, 0, 0.1)';">
370 🗑️ Clear Cache for URL
371 </button>
372 </form>
373 </div>
374 </div>
375 <?php
376 }
377
378 // ------------------------------------------------------------------
379 // Admin-post handlers (form submissions)
380 // ------------------------------------------------------------------
381
382 /**
383 * WordPress standard handler for clearing all cache plugins (admin_post hook)
384 */
385 public function handle_clear_all_cache_plugins() {
386 if (!isset($_POST['clear_cache_nonce']) || !wp_verify_nonce($_POST['clear_cache_nonce'], 'metasync_clear_cache_nonce')) {
387 wp_die('Security check failed');
388 }
389
390 if (!Metasync::current_user_has_plugin_access()) {
391 wp_die('You do not have permission to perform this action');
392 }
393
394 $redirect_url = admin_url('admin.php?page=' . $this->get_page_slug() . '&tab=advanced');
395
396 if (class_exists('Metasync_Cache_Purge')) {
397 try {
398 $cache_purge = Metasync_Cache_Purge::get_instance();
399 $results = $cache_purge->clear_all_caches('manual_admin');
400
401 $cleared_count = count($results['cleared']);
402 $failed_count = count($results['failed']);
403
404 $redirect_url .= '&cache_cleared=1&cleared=' . $cleared_count . '&failed=' . $failed_count;
405 if (!empty($results['cleared'])) {
406 $redirect_url .= '&plugins=' . urlencode(implode(',', $results['cleared']));
407 }
408 } catch (Exception $e) {
409 error_log('MetaSync Cache Clear Error: ' . $e->getMessage());
410 $redirect_url .= '&cache_error=1&message=' . urlencode($e->getMessage());
411 }
412 } else {
413 $redirect_url .= '&cache_error=1&message=' . urlencode('Cache Purge class not available');
414 }
415
416 wp_safe_redirect($redirect_url);
417 exit;
418 }
419
420 /**
421 * WordPress standard handler for clearing OTTO cache (admin_post hook)
422 */
423 public function handle_clear_otto_cache_all() {
424 if (!isset($_POST['clear_otto_cache_nonce']) || !wp_verify_nonce($_POST['clear_otto_cache_nonce'], 'metasync_clear_otto_cache_nonce')) {
425 wp_die('Security check failed');
426 }
427
428 if (!Metasync::current_user_has_plugin_access()) {
429 wp_die('You do not have permission to perform this action');
430 }
431
432 $redirect_url = admin_url('admin.php?page=' . $this->get_page_slug() . '&tab=advanced');
433
434 if (class_exists('Metasync_Otto_Transient_Cache')) {
435 try {
436 $result = Metasync_Otto_Transient_Cache::clear_all_transients();
437 $redirect_url .= '&otto_cache_cleared=1&count=' . $result['cleared_count'];
438 } catch (Exception $e) {
439 error_log('MetaSync OTTO Cache Clear Error: ' . $e->getMessage());
440 $redirect_url .= '&otto_cache_error=1&message=' . urlencode($e->getMessage());
441 }
442 } else {
443 $redirect_url .= '&otto_cache_error=1&message=' . urlencode('Transient Cache class not found');
444 }
445
446 delete_transient('metasync_otto_js_detected');
447
448 wp_safe_redirect($redirect_url);
449 exit;
450 }
451
452 /**
453 * WordPress standard handler for clearing OTTO cache by URL (admin_post hook)
454 */
455 public function handle_clear_otto_cache_url() {
456 if (!isset($_POST['clear_otto_cache_nonce']) || !wp_verify_nonce($_POST['clear_otto_cache_nonce'], 'metasync_clear_otto_cache_nonce')) {
457 wp_die('Security check failed');
458 }
459
460 if (!Metasync::current_user_has_plugin_access()) {
461 wp_die('You do not have permission to perform this action');
462 }
463
464 $redirect_url = admin_url('admin.php?page=' . $this->get_page_slug() . '&tab=advanced');
465 $url = isset($_POST['otto_cache_url']) ? trim($_POST['otto_cache_url']) : '';
466
467 if (empty($url)) {
468 wp_safe_redirect($redirect_url . '&otto_cache_error=1&message=' . urlencode('URL is required'));
469 exit;
470 }
471
472 if (!filter_var($url, FILTER_VALIDATE_URL)) {
473 wp_safe_redirect($redirect_url . '&otto_cache_error=1&message=' . urlencode('Invalid URL format'));
474 exit;
475 }
476
477 if (class_exists('Metasync_Otto_Transient_Cache')) {
478 try {
479 $result = Metasync_Otto_Transient_Cache::clear_url_transient($url);
480
481 if ($result['success']) {
482 $redirect_url .= '&otto_cache_cleared=1&count=' . $result['cleared_count'] . '&url=' . urlencode($url);
483 } else {
484 $redirect_url .= '&otto_cache_error=1&message=' . urlencode($result['message']);
485 }
486 } catch (Exception $e) {
487 error_log('MetaSync OTTO Cache Clear Error: ' . $e->getMessage());
488 $redirect_url .= '&otto_cache_error=1&message=' . urlencode($e->getMessage());
489 }
490 } else {
491 $redirect_url .= '&otto_cache_error=1&message=' . urlencode('Transient Cache class not found');
492 }
493
494 delete_transient('metasync_otto_js_detected');
495
496 wp_safe_redirect($redirect_url);
497 exit;
498 }
499
500 // ------------------------------------------------------------------
501 // AJAX handlers – excluded URLs
502 // ------------------------------------------------------------------
503
504 /**
505 * AJAX handler to add excluded URL for OTTO
506 */
507 public function ajax_otto_add_excluded_url()
508 {
509 if (!Metasync::current_user_has_plugin_access()) {
510 wp_send_json_error(['message' => 'Insufficient permissions']);
511 return;
512 }
513
514 if (!check_ajax_referer('metasync_otto_excluded_urls', 'nonce', false)) {
515 wp_send_json_error(['message' => 'Invalid security token']);
516 return;
517 }
518
519 $url_pattern = isset($_POST['url_pattern']) ? sanitize_text_field($_POST['url_pattern']) : '';
520 $pattern_type = isset($_POST['pattern_type']) ? sanitize_text_field($_POST['pattern_type']) : 'exact';
521 $description = isset($_POST['description']) ? sanitize_textarea_field($_POST['description']) : '';
522
523 if (empty($url_pattern)) {
524 wp_send_json_error(['message' => 'URL pattern is required']);
525 return;
526 }
527
528 $valid_types = ['exact', 'contain', 'start', 'end', 'regex'];
529 if (!in_array($pattern_type, $valid_types)) {
530 wp_send_json_error(['message' => 'Invalid pattern type']);
531 return;
532 }
533
534 if ($pattern_type === 'regex') {
535 $test_pattern = $url_pattern;
536 $delimiter_chars = ['/', '#', '~', '%', '@'];
537 $has_valid_delimiters = false;
538
539 if (strlen($test_pattern) >= 2) {
540 $first_char = $test_pattern[0];
541 if (in_array($first_char, $delimiter_chars)) {
542 $last_pos = strrpos($test_pattern, $first_char);
543 if ($last_pos > 0) {
544 $has_valid_delimiters = true;
545 }
546 }
547 }
548
549 if (!$has_valid_delimiters) {
550 $test_pattern = '/' . $test_pattern . '/';
551 }
552
553 if (@preg_match($test_pattern, '') === false) {
554 wp_send_json_error(['message' => 'Invalid regular expression pattern']);
555 return;
556 }
557 }
558
559 require_once plugin_dir_path(dirname(__FILE__)) . 'otto/class-metasync-otto-excluded-urls-database.php';
560 $db = new Metasync_Otto_Excluded_URLs_Database();
561
562 $result = $db->add([
563 'url_pattern' => $url_pattern,
564 'pattern_type' => $pattern_type,
565 'description' => $description,
566 'status' => 'active',
567 ]);
568
569 if ($result === 'duplicate') {
570 wp_send_json_error([
571 'message' => 'This URL pattern already exists in the exclusion list',
572 'code' => 'duplicate'
573 ]);
574 return;
575 }
576
577 if ($result === 'reactivated') {
578 $db->clear_cache();
579
580 if ($pattern_type === 'exact') {
581 try {
582 if (class_exists('Metasync_Cache_Purge')) {
583 $cache_purge = Metasync_Cache_Purge::get_instance();
584 $cache_purge->clear_url_cache($url_pattern);
585 }
586 wp_cache_flush();
587 } catch (Exception $e) {
588 error_log('MetaSync: Failed to clear cache for reactivated URL: ' . $e->getMessage());
589 }
590 }
591
592 wp_send_json_success([
593 'message' => 'Previously inactive URL pattern has been reactivated. Cache cleared.',
594 ]);
595 return;
596 }
597
598 if ($result === true) {
599 $db->clear_cache();
600
601 if ($pattern_type === 'exact') {
602 try {
603 if (class_exists('Metasync_Cache_Purge')) {
604 $cache_purge = Metasync_Cache_Purge::get_instance();
605 $cache_purge->clear_url_cache($url_pattern);
606 }
607
608 wp_cache_flush();
609 } catch (Exception $e) {
610 error_log('MetaSync: Failed to clear cache for excluded URL: ' . $e->getMessage());
611 }
612 }
613
614 wp_send_json_success([
615 'message' => sprintf('URL excluded from %s successfully. Cache cleared.', Metasync::get_whitelabel_otto_name()),
616 ]);
617 } else {
618 wp_send_json_error(['message' => 'Failed to add excluded URL']);
619 }
620 }
621
622 /**
623 * AJAX handler to delete excluded URL for OTTO
624 */
625 public function ajax_otto_delete_excluded_url()
626 {
627 if (!Metasync::current_user_has_plugin_access()) {
628 wp_send_json_error(['message' => 'Insufficient permissions']);
629 return;
630 }
631
632 if (!check_ajax_referer('metasync_otto_excluded_urls', 'nonce', false)) {
633 wp_send_json_error(['message' => 'Invalid security token']);
634 return;
635 }
636
637 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
638
639 if ($id <= 0) {
640 wp_send_json_error(['message' => 'Invalid ID']);
641 return;
642 }
643
644 require_once plugin_dir_path(dirname(__FILE__)) . 'otto/class-metasync-otto-excluded-urls-database.php';
645 $db = new Metasync_Otto_Excluded_URLs_Database();
646
647 $result = $db->delete([$id]);
648
649 if ($result) {
650 wp_send_json_success([
651 'message' => 'Excluded URL deleted successfully',
652 ]);
653 } else {
654 wp_send_json_error(['message' => 'Failed to delete excluded URL']);
655 }
656 }
657
658 /**
659 * AJAX handler to recheck if an excluded URL is now available
660 */
661 public function ajax_otto_recheck_excluded_url()
662 {
663 if (!Metasync::current_user_has_plugin_access()) {
664 wp_send_json_error(['message' => 'Insufficient permissions']);
665 return;
666 }
667
668 if (!check_ajax_referer('metasync_otto_excluded_urls', 'nonce', false)) {
669 wp_send_json_error(['message' => 'Invalid security token']);
670 return;
671 }
672
673 $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
674 if ($id <= 0) {
675 wp_send_json_error(['message' => 'Invalid ID']);
676 return;
677 }
678
679 require_once plugin_dir_path(dirname(__FILE__)) . 'otto/class-metasync-otto-excluded-urls-database.php';
680 require_once plugin_dir_path(dirname(__FILE__)) . 'otto/otto_pixel.php';
681
682 $db = new Metasync_Otto_Excluded_URLs_Database();
683 $row = $db->get_record_by_id($id);
684
685 if (!$row || empty($row->url_pattern)) {
686 wp_send_json_error(['message' => 'Excluded URL not found']);
687 return;
688 }
689
690 $url = trim($row->url_pattern);
691 $available = metasync_otto_is_url_available($url);
692
693 wp_send_json_success([
694 'available' => $available,
695 'url' => $url,
696 ]);
697 }
698
699 /**
700 * AJAX handler to get excluded URLs with pagination
701 */
702 public function ajax_otto_get_excluded_urls()
703 {
704 if (!Metasync::current_user_has_plugin_access()) {
705 wp_send_json_error(['message' => 'Insufficient permissions']);
706 return;
707 }
708
709 if (!check_ajax_referer('metasync_otto_excluded_urls', 'nonce', false)) {
710 wp_send_json_error(['message' => 'Invalid security token']);
711 return;
712 }
713
714 $page = isset($_POST['page']) ? intval($_POST['page']) : 1;
715 $per_page = isset($_POST['per_page']) ? intval($_POST['per_page']) : 10;
716
717 if ($page < 1) {
718 $page = 1;
719 }
720 if ($per_page < 1 || $per_page > 100) {
721 $per_page = 10;
722 }
723
724 require_once plugin_dir_path(dirname(__FILE__)) . 'otto/class-metasync-otto-excluded-urls-database.php';
725 $db = new Metasync_Otto_Excluded_URLs_Database();
726
727 $records = $db->get_paginated_records($per_page, $page);
728 $total_count = $db->get_total_count();
729 $total_pages = ceil($total_count / $per_page);
730
731 wp_send_json_success([
732 'records' => $records,
733 'pagination' => [
734 'current_page' => $page,
735 'per_page' => $per_page,
736 'total_count' => $total_count,
737 'total_pages' => $total_pages,
738 ],
739 ]);
740 }
741 }
742