admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('fastless_cache_admin'),
'siteUrl' => home_url('/'),
'labels' => array(
'cacheCleared' => __('Cache cleared.', 'fastless-cache'),
'settingsSaved' => __('Settings saved.', 'fastless-cache'),
'imported' => __('Settings imported.', 'fastless-cache'),
'error' => __('Something went wrong.', 'fastless-cache'),
),
'preload' => array(
'start' => __('Preload cache', 'fastless-cache'),
'starting' => __('Starting preload…', 'fastless-cache'),
'running' => __('Preloading…', 'fastless-cache'),
'stop' => __('Stop', 'fastless-cache'),
'stopping' => __('Stopping…', 'fastless-cache'),
'confirmStop' => __('Stop the preload? Progress so far is kept and you can start again later.', 'fastless-cache'),
'resumed' => __('Preload resumed.', 'fastless-cache'),
'connectionLost' => __('Connection lost - the preload is paused. Reload this page to resume it.', 'fastless-cache'),
),
));
}
}
/**
* Render the admin page.
*
* @return void
*/
public function render_page()
{
$nav = Fastless_Cache_Tabs::nav();
$tab = isset($_GET['tab']) ? sanitize_key($_GET['tab']) : 'dashboard'; // phpcs:ignore WordPress.Security.NonceVerification -- read-only display parameter, sanitized with sanitize_key() and restricted to the manage_options screen.
if (! array_key_exists($tab, $nav)) {
$tab = 'dashboard';
}
// Populate the dynamic tools fields before rendering that tab.
if ('tools' === $tab) {
$this->render_tools_content();
}
$version = defined('FASTLESS_CACHE_VERSION') ? FASTLESS_CACHE_VERSION : '';
?>
render_notices(); ?>
'success', 'message' => __('Settings saved.', 'fastless-cache'));
}
if (isset($_GET['fastless_cache_cleared'])) { // phpcs:ignore WordPress.Security.NonceVerification -- isset-only check of post-redirect-get notice flags; no form processing occurs here.
$toasts[] = array('type' => 'success', 'message' => __('Cache cleared.', 'fastless-cache'));
}
if (isset($_GET['fastless_reset'])) { // phpcs:ignore WordPress.Security.NonceVerification -- isset-only check of post-redirect-get notice flags; no form processing occurs here.
$toasts[] = array('type' => 'success', 'message' => __('Settings reset.', 'fastless-cache'));
}
if (isset($_GET['fastless_imported'])) { // phpcs:ignore WordPress.Security.NonceVerification -- isset-only check of post-redirect-get notice flags; no form processing occurs here.
$toasts[] = array('type' => 'success', 'message' => __('Settings imported.', 'fastless-cache'));
}
if (isset($_GET['fastless_error'])) { // phpcs:ignore WordPress.Security.NonceVerification -- isset-only check of post-redirect-get notice flags; no form processing occurs here.
$toasts[] = array('type' => 'error', 'message' => sanitize_text_field(wp_unslash($_GET['fastless_error']))); // phpcs:ignore WordPress.Security.NonceVerification -- isset-only check of post-redirect-get notice flags; no form processing occurs here.
}
if (empty($toasts)) {
return;
}
?>
>
render_export_import());
}
/**
* The export / import block.
*
* The export is not on screen until asked for: the JSON rides along in an
* inert application/json block (hex-escaped so it can never break out of
* the script element) and the button copies it into the textarea, ready to
* select and copy.
*
* @return string
*/
private function render_export_import()
{
$export = wp_json_encode(Fastless_Cache_Options::all(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_HEX_APOS);
ob_start();
?>
save($sections);
if (is_wp_error($result)) {
wp_safe_redirect(add_query_arg('fastless_error', urlencode($result->get_error_message()), $this->admin_url()));
exit;
}
// update_option() skips its hooks when the new value equals the old
// one, so a re-save of unchanged values would leave a stale settings
// file behind (e.g. a footprint value that diverged on an earlier
// install). Rewrite the file from the stored settings every time.
if (class_exists('Fastless_Cache')) {
Fastless_Cache::on_update_backend('fastless_cache', Fastless_Cache::get_settings(false));
}
if (Fastless_Cache_Options::enabled('clearing', 'clear_site_cache_on_saved_settings')) {
Fastless_Cache::clear_site_cache();
}
wp_safe_redirect(add_query_arg('fastless_saved', 1, $this->admin_url()));
exit;
}
/**
* Handle a clear-cache request.
*
* @return void
*/
public function handle_clear()
{
$this->verify_action_nonce();
Fastless_Cache::clear_site_cache();
wp_safe_redirect(add_query_arg('fastless_cache_cleared', 1, $this->admin_url()));
exit;
}
/**
* Handle a settings reset.
*
* @return void
*/
public function handle_reset()
{
$this->verify_action_nonce();
Fastless_Cache_Options::reset();
Fastless_Cache::update_backend();
wp_safe_redirect(add_query_arg('fastless_reset', 1, $this->admin_url()));
exit;
}
/**
* Handle an export request: download the settings as a JSON file.
*
* @return void
*/
public function handle_export()
{
if (! current_user_can('manage_options')) {
wp_die(esc_html__('You do not have permission to do that.', 'fastless-cache'));
}
check_admin_referer('fastless_cache_admin', 'fastless_cache_nonce');
$data = wp_json_encode(Fastless_Cache_Options::all());
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename="fastless-cache-settings.json"');
echo $data; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit;
}
/**
* Handle an import request.
*
* @return void
*/
public function handle_import()
{
if (! current_user_can('manage_options')) {
wp_die(esc_html__('You do not have permission to do that.', 'fastless-cache'));
}
check_admin_referer('fastless_cache_admin', 'fastless_cache_nonce');
$raw = isset($_POST['fastless_cache_import']) ? wp_unslash($_POST['fastless_cache_import']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- raw JSON input validated via json_decode() and schema-driven sanitization in sanitize_import_values().
// Validate JSON: check for parse errors, ensure it's an array, allowlist keys.
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($data)) {
wp_safe_redirect(add_query_arg('fastless_error', urlencode(__('Invalid import data.', 'fastless-cache')), $this->admin_url()));
exit;
}
// Allowlist expected top-level sections (schema-driven).
$allowed_sections = array('general', 'clearing', 'exclusions', 'query_strings', 'cookies', 'tools');
$filtered_data = array();
foreach ($data as $section => $values) {
if (! in_array($section, $allowed_sections, true) || ! is_array($values)) {
continue;
}
$filtered_data[$section] = $this->sanitize_import_values($values);
}
if (empty($filtered_data)) {
wp_safe_redirect(add_query_arg('fastless_error', urlencode(__('No valid settings found in import data.', 'fastless-cache')), $this->admin_url()));
exit;
}
$result = Fastless_Cache_Options::instance()->save($filtered_data);
if (is_wp_error($result)) {
wp_safe_redirect(add_query_arg('fastless_error', urlencode($result->get_error_message()), $this->admin_url()));
exit;
}
// Same stale-file reasoning as handle_save(): force the rewrite even
// when the imported values match what is already stored.
if (class_exists('Fastless_Cache')) {
Fastless_Cache::on_update_backend('fastless_cache', Fastless_Cache::get_settings(false));
}
wp_safe_redirect(add_query_arg('fastless_imported', 1, $this->admin_url()));
exit;
}
/**
* Recursively sanitize import values by type.
*
* @param array $values Raw values from JSON import.
* @return array Sanitized values.
*/
private function sanitize_import_values($values)
{
$sanitized = array();
foreach ($values as $key => $value) {
if (is_array($value)) {
$sanitized[$key] = $this->sanitize_import_values($value);
} elseif (is_bool($value)) {
$sanitized[$key] = (bool) $value;
} elseif (is_int($value)) {
$sanitized[$key] = (int) $value;
} elseif (is_float($value)) {
$sanitized[$key] = (float) $value;
} elseif (is_string($value)) {
// Length limit for safety.
$sanitized[$key] = substr(sanitize_text_field($value), 0, 4000);
} else {
// Skip null, objects, resources.
continue;
}
}
return $sanitized;
}
/**
* Verify the capability and nonce shared by the action handlers.
*
* @return void
*/
private function verify_action_nonce()
{
if (! current_user_can('manage_options')) {
wp_die(esc_html__('You do not have permission to do that.', 'fastless-cache'));
}
check_admin_referer('fastless_cache_admin', 'fastless_cache_nonce');
}
/**
* Build a URL to the panel with extra query args.
*
* @param array $args Query args.
* @return string
*/
private function admin_url($args = array())
{
return add_query_arg(array_merge(array('page' => self::SLUG), $args), admin_url('admin.php'));
}
}