| 1 |
<?php |
| 2 |
// If this file is called directly, abort. |
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Edge Cache / CDN Settings for MetaSync Plugin |
| 9 |
* |
| 10 |
* Manages CDN credentials and cache-tag header configuration. |
| 11 |
* Extracted from the main admin class to reduce file size. |
| 12 |
* |
| 13 |
* @package Metasync |
| 14 |
* @subpackage Metasync/admin |
| 15 |
*/ |
| 16 |
class Metasync_Edge_Cache_Settings { |
| 17 |
|
| 18 |
/** |
| 19 |
* Option key for storing edge cache settings. |
| 20 |
*/ |
| 21 |
const OPTION_KEY = 'metasync_edge_cache_options'; |
| 22 |
|
| 23 |
/** |
| 24 |
* Nonce action for edge cache settings. |
| 25 |
*/ |
| 26 |
const NONCE_ACTION = 'metasync_edge_cache_nonce'; |
| 27 |
|
| 28 |
/** |
| 29 |
* Nonce field name. |
| 30 |
*/ |
| 31 |
const NONCE_FIELD = 'edge_cache_nonce'; |
| 32 |
|
| 33 |
/** |
| 34 |
* Default settings. |
| 35 |
* |
| 36 |
* @return array |
| 37 |
*/ |
| 38 |
public static function get_defaults() { |
| 39 |
return array( |
| 40 |
'cache_tags_enabled' => true, |
| 41 |
'cloudflare_enabled' => false, |
| 42 |
'cloudflare_zone_id' => '', |
| 43 |
'cloudflare_api_token' => '', |
| 44 |
'sucuri_enabled' => false, |
| 45 |
'sucuri_api_key' => '', |
| 46 |
'sucuri_api_secret' => '', |
| 47 |
'fastly_enabled' => false, |
| 48 |
'fastly_service_id' => '', |
| 49 |
'fastly_api_token' => '', |
| 50 |
'akamai_enabled' => false, |
| 51 |
'akamai_client_token' => '', |
| 52 |
'akamai_access_token' => '', |
| 53 |
'akamai_client_secret' => '', |
| 54 |
'akamai_host' => '', |
| 55 |
'sevalla_enabled' => false, |
| 56 |
'sevalla_api_key' => '', |
| 57 |
'sevalla_application_id' => '', |
| 58 |
'cloudways_enabled' => false, |
| 59 |
'flywheel_enabled' => false, |
| 60 |
); |
| 61 |
} |
| 62 |
|
| 63 |
/** |
| 64 |
* Get all settings merged with defaults. |
| 65 |
* |
| 66 |
* @return array |
| 67 |
*/ |
| 68 |
public static function get_settings() { |
| 69 |
return wp_parse_args( |
| 70 |
get_option(self::OPTION_KEY, array()), |
| 71 |
self::get_defaults() |
| 72 |
); |
| 73 |
} |
| 74 |
|
| 75 |
/** |
| 76 |
* All setting keys that hold boolean toggles. |
| 77 |
* |
| 78 |
* @return array |
| 79 |
*/ |
| 80 |
private static function get_bool_keys() { |
| 81 |
return array( |
| 82 |
'cache_tags_enabled', |
| 83 |
'cloudflare_enabled', |
| 84 |
'sucuri_enabled', |
| 85 |
'fastly_enabled', |
| 86 |
'akamai_enabled', |
| 87 |
'sevalla_enabled', |
| 88 |
'cloudways_enabled', |
| 89 |
'flywheel_enabled', |
| 90 |
); |
| 91 |
} |
| 92 |
|
| 93 |
/** |
| 94 |
* Sanitize and save settings from $_POST. |
| 95 |
* |
| 96 |
* @param array $post POST data (defaults to $_POST). |
| 97 |
* @return bool True if saved. |
| 98 |
*/ |
| 99 |
public static function save_from_post($post = null) { |
| 100 |
if ($post === null) { |
| 101 |
$post = $_POST; |
| 102 |
} |
| 103 |
|
| 104 |
// Only save if at least one edge-cache field is present in the payload. |
| 105 |
if (!isset($post['cache_tags_enabled'])) { |
| 106 |
return false; |
| 107 |
} |
| 108 |
|
| 109 |
$bool_keys = self::get_bool_keys(); |
| 110 |
$defaults = self::get_defaults(); |
| 111 |
$settings = array(); |
| 112 |
|
| 113 |
foreach ($defaults as $key => $default) { |
| 114 |
if (in_array($key, $bool_keys, true)) { |
| 115 |
$settings[$key] = !empty($post[$key]) && $post[$key] === '1'; |
| 116 |
} else { |
| 117 |
$settings[$key] = sanitize_text_field($post[$key] ?? ''); |
| 118 |
} |
| 119 |
} |
| 120 |
|
| 121 |
update_option(self::OPTION_KEY, $settings); |
| 122 |
return true; |
| 123 |
} |
| 124 |
|
| 125 |
/** |
| 126 |
* AJAX handler: save edge cache settings (standalone endpoint). |
| 127 |
*/ |
| 128 |
public static function ajax_save() { |
| 129 |
if (!isset($_POST[self::NONCE_FIELD]) || !wp_verify_nonce($_POST[self::NONCE_FIELD], self::NONCE_ACTION)) { |
| 130 |
wp_send_json_error(array('message' => 'Security check failed')); |
| 131 |
return; |
| 132 |
} |
| 133 |
|
| 134 |
if (!Metasync::current_user_has_plugin_access()) { |
| 135 |
wp_send_json_error(array('message' => 'Insufficient permissions')); |
| 136 |
return; |
| 137 |
} |
| 138 |
|
| 139 |
self::save_from_post(); |
| 140 |
wp_send_json_success(array('message' => 'Edge cache settings saved')); |
| 141 |
} |
| 142 |
|
| 143 |
/** |
| 144 |
* Render the Edge Cache / CDN settings section. |
| 145 |
*/ |
| 146 |
public static function render() { |
| 147 |
$settings = self::get_settings(); |
| 148 |
$show_sevalla = !class_exists('KinstaCache'); |
| 149 |
$show_cloudways = !empty($_SERVER['HTTP_X_VARNISH']) || !empty(get_option('metasync_cloudways_detected')); |
| 150 |
$show_flywheel = defined('FLYWHEEL_CONFIG_DIR'); |
| 151 |
?> |
| 152 |
<div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;"> |
| 153 |
<p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;"> |
| 154 |
Configure CDN credentials for cache-tag purging. When enabled, cache-tag headers are added to <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?>-processed pages so CDNs can purge by post ID. |
| 155 |
</p> |
| 156 |
|
| 157 |
<?php wp_nonce_field(self::NONCE_ACTION, self::NONCE_FIELD); ?> |
| 158 |
|
| 159 |
<!-- Cache-Tag Headers Master Toggle --> |
| 160 |
<div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;"> |
| 161 |
<h3 style="color: var(--dashboard-text-primary); margin: 0 0 14px 0; font-size: 16px; font-weight: 600;">Cache-Tag Headers</h3> |
| 162 |
<label style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px; cursor: pointer;"> |
| 163 |
<input type="hidden" name="cache_tags_enabled" value="0" /> |
| 164 |
<input type="checkbox" name="cache_tags_enabled" value="1" <?php checked($settings['cache_tags_enabled']); ?> style="width: 16px; height: 16px; cursor: pointer;" /> |
| 165 |
<span style="color: var(--dashboard-text-primary); font-weight: 500;">Enable Cache-Tag Response Headers</span> |
| 166 |
<span style="color: var(--dashboard-text-secondary); font-size: 12px;">— adds purge-by-tag support for CDNs</span> |
| 167 |
</label> |
| 168 |
<div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 8px; padding: 16px;"> |
| 169 |
<p style="color: var(--dashboard-text-secondary); margin: 0; font-size: 13px;"> |
| 170 |
<strong style="color: var(--dashboard-info, #3b82f6);">How it works:</strong><br> |
| 171 |
Adds <code style="background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 3px;">Cache-Tag</code>, |
| 172 |
<code style="background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 3px;">Surrogate-Key</code>, and |
| 173 |
<code style="background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 3px;">Edge-Cache-Tag</code> |
| 174 |
headers to singular pages. These allow Cloudflare, Fastly, Akamai, and other CDNs to purge individual posts by tag instead of flushing the entire cache. |
| 175 |
</p> |
| 176 |
</div> |
| 177 |
</div> |
| 178 |
|
| 179 |
<!-- CDN Providers --> |
| 180 |
<div style="background: rgba(255,255,255,0.02); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px;"> |
| 181 |
<h5 style="margin: 0 0 14px 0; color: var(--dashboard-text-primary);">CDN Provider Credentials</h5> |
| 182 |
|
| 183 |
<?php |
| 184 |
self::render_provider('cloudflare', 'Cloudflare', 'purges via Cache-Tag API', $settings, array( |
| 185 |
array('name' => 'cloudflare_zone_id', 'label' => 'Zone ID', 'type' => 'text', 'placeholder' => 'e.g. 023e105f4ecef8ad9ca31a8372d0c353', 'hint' => 'Found in Cloudflare dashboard → Overview → Zone ID'), |
| 186 |
array('name' => 'cloudflare_api_token', 'label' => 'API Token', 'type' => 'password', 'hint' => 'Requires <strong>Cache Purge</strong> permission on the zone'), |
| 187 |
)); |
| 188 |
|
| 189 |
self::render_provider('sucuri', 'Sucuri WAF', 'purges Sucuri firewall cache', $settings, array( |
| 190 |
array('name' => 'sucuri_api_key', 'label' => 'API Key', 'type' => 'password', 'hint' => 'Found in Sucuri dashboard → Settings → API'), |
| 191 |
array('name' => 'sucuri_api_secret', 'label' => 'API Secret', 'type' => 'password'), |
| 192 |
)); |
| 193 |
|
| 194 |
self::render_provider('fastly', 'Fastly', 'purges via Surrogate-Key', $settings, array( |
| 195 |
array('name' => 'fastly_service_id', 'label' => 'Service ID', 'type' => 'text', 'hint' => 'Found in Fastly dashboard → Service details'), |
| 196 |
array('name' => 'fastly_api_token', 'label' => 'API Token', 'type' => 'password', 'hint' => 'Requires <strong>Purge all</strong> or <strong>Purge by key</strong> scope'), |
| 197 |
)); |
| 198 |
|
| 199 |
$has_more_after_akamai = $show_sevalla || $show_cloudways || $show_flywheel; |
| 200 |
self::render_provider('akamai', 'Akamai', 'purges via Edge-Cache-Tag', $settings, array( |
| 201 |
array('name' => 'akamai_client_token', 'label' => 'Client Token', 'type' => 'password'), |
| 202 |
array('name' => 'akamai_access_token', 'label' => 'Access Token', 'type' => 'password'), |
| 203 |
array('name' => 'akamai_client_secret', 'label' => 'Client Secret', 'type' => 'password'), |
| 204 |
array('name' => 'akamai_host', 'label' => 'Host', 'type' => 'text', 'placeholder' => 'e.g. akab-xxxxx.purge.akamaiapis.net', 'hint' => 'Found in Akamai Control Center → Identity & Access → API credentials'), |
| 205 |
), !$has_more_after_akamai); |
| 206 |
|
| 207 |
if ($show_sevalla) { |
| 208 |
$has_more_after_sevalla = $show_cloudways || $show_flywheel; |
| 209 |
self::render_provider('sevalla', 'Kinsta / Sevalla', 'purges edge cache via v3 API (v2 fallback)', $settings, array( |
| 210 |
array('name' => 'sevalla_api_key', 'label' => 'Sevalla API Key', 'type' => 'password', 'hint' => 'Found in Sevalla dashboard → Company → API Keys'), |
| 211 |
array('name' => 'sevalla_application_id', 'label' => 'Application ID', 'type' => 'text', 'placeholder' => 'e.g. fb5e5168-4281-4bec-94c5-0d1584e9e657', 'hint' => 'UUID format — found in the site URL: <code style="background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 3px;">my.sevalla.com/applications/<strong>{UUID}</strong></code>'), |
| 212 |
), !$has_more_after_sevalla, 'Uses v3 edge cache purge API with v2 fallback. If KinstaCache mu-plugin is available, native purge is used instead.'); |
| 213 |
} |
| 214 |
|
| 215 |
if ($show_cloudways) { |
| 216 |
self::render_provider('cloudways', 'Cloudways', 'purges Varnish cache per URL', $settings, array(), !$show_flywheel, sprintf('Detected Cloudways Varnish. Enable to purge Varnish on each %s update.', Metasync::get_whitelabel_otto_name())); |
| 217 |
} |
| 218 |
|
| 219 |
if ($show_flywheel) { |
| 220 |
self::render_provider('flywheel', 'Flywheel', sprintf('full cache flush on %s updates', Metasync::get_whitelabel_otto_name()), $settings, array(), true, sprintf('Detected Flywheel hosting. Enable to clear cache on each %s update.', Metasync::get_whitelabel_otto_name())); |
| 221 |
} |
| 222 |
?> |
| 223 |
</div> |
| 224 |
</div> |
| 225 |
|
| 226 |
<script> |
| 227 |
jQuery(document).ready(function($) { |
| 228 |
// Toggle credential fields visibility when provider checkbox changes |
| 229 |
var providers = ['cloudflare', 'sucuri', 'fastly', 'akamai', 'sevalla', 'cloudways', 'flywheel']; |
| 230 |
$.each(providers, function(_, name) { |
| 231 |
var $checkbox = $('#metasync-ec-' + name); |
| 232 |
var $fields = $('.metasync-ec-' + name + '-fields'); |
| 233 |
if ($checkbox.length && $fields.length) { |
| 234 |
$checkbox.on('change', function() { |
| 235 |
if ($(this).is(':checked')) { |
| 236 |
$fields.slideDown(200); |
| 237 |
} else { |
| 238 |
$fields.slideUp(200); |
| 239 |
} |
| 240 |
}); |
| 241 |
} |
| 242 |
}); |
| 243 |
}); |
| 244 |
</script> |
| 245 |
<?php |
| 246 |
} |
| 247 |
|
| 248 |
/** |
| 249 |
* Render a single CDN provider block. |
| 250 |
* |
| 251 |
* @param string $key Provider key (e.g. 'cloudflare'). |
| 252 |
* @param string $title Display title. |
| 253 |
* @param string $description Short dash-prefixed description. |
| 254 |
* @param array $settings Current settings. |
| 255 |
* @param array $fields Field definitions. |
| 256 |
* @param bool $is_last Whether this is the last provider (no bottom border). |
| 257 |
* @param string $notice Optional warning notice text. |
| 258 |
*/ |
| 259 |
private static function render_provider($key, $title, $description, $settings, $fields, $is_last = false, $notice = '') { |
| 260 |
$enabled_key = $key . '_enabled'; |
| 261 |
$is_enabled = !empty($settings[$enabled_key]); |
| 262 |
$border = $is_last ? '' : 'border-bottom: 1px solid var(--dashboard-border); padding-bottom: 20px; margin-bottom: 20px;'; |
| 263 |
?> |
| 264 |
<div class="metasync-edge-provider" style="<?php echo $border; ?>"> |
| 265 |
<label style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px; cursor: pointer;"> |
| 266 |
<input type="hidden" name="<?php echo esc_attr($enabled_key); ?>" value="0" /> |
| 267 |
<input type="checkbox" |
| 268 |
id="metasync-ec-<?php echo esc_attr($key); ?>" |
| 269 |
name="<?php echo esc_attr($enabled_key); ?>" |
| 270 |
value="1" |
| 271 |
<?php checked($is_enabled); ?> |
| 272 |
style="width: 16px; height: 16px; cursor: pointer;" /> |
| 273 |
<span style="color: var(--dashboard-text-primary); font-weight: 500;"><?php echo esc_html($title); ?></span> |
| 274 |
<span style="color: var(--dashboard-text-secondary); font-size: 12px;">— <?php echo esc_html($description); ?></span> |
| 275 |
</label> |
| 276 |
<div class="metasync-ec-<?php echo esc_attr($key); ?>-fields" style="margin-left: 26px; <?php echo $is_enabled ? '' : 'display: none;'; ?>"> |
| 277 |
<?php if (!empty($notice)) : ?> |
| 278 |
<div style="background: rgba(255, 193, 7, 0.1); border-left: 4px solid #ffc107; padding: 12px; border-radius: 4px; margin-bottom: 16px;"> |
| 279 |
<span style="color: var(--dashboard-text-secondary); font-size: 12px;"><?php echo esc_html($notice); ?></span> |
| 280 |
</div> |
| 281 |
<?php endif; ?> |
| 282 |
|
| 283 |
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px;"> |
| 284 |
<?php foreach ($fields as $field) : ?> |
| 285 |
<div> |
| 286 |
<label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;"> |
| 287 |
<?php echo esc_html($field['label']); ?> |
| 288 |
</label> |
| 289 |
<input type="<?php echo esc_attr($field['type']); ?>" |
| 290 |
name="<?php echo esc_attr($field['name']); ?>" |
| 291 |
value="<?php echo esc_attr($settings[$field['name']]); ?>" |
| 292 |
<?php if (!empty($field['placeholder'])) : ?>placeholder="<?php echo esc_attr($field['placeholder']); ?>"<?php endif; ?> |
| 293 |
style="width: 100%; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary); box-sizing: border-box;" /> |
| 294 |
<?php if (!empty($field['hint'])) : ?> |
| 295 |
<p style="color: var(--dashboard-text-secondary); font-size: 12px; margin: 4px 0 0 0;"> |
| 296 |
<?php echo wp_kses($field['hint'], array('strong' => array(), 'code' => array('style' => array()))); ?> |
| 297 |
</p> |
| 298 |
<?php endif; ?> |
| 299 |
</div> |
| 300 |
<?php endforeach; ?> |
| 301 |
</div> |
| 302 |
</div> |
| 303 |
</div> |
| 304 |
<?php |
| 305 |
} |
| 306 |
} |
| 307 |
|