PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.15
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.15
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 2.5.23 All 138 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.15, at includes/class-metasync-otto-cache-manager.php

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