<?php
/**
* Code related to the cache control headers settings.
*
* PHP version 5
*
* @category Library
* @package Sucuri
* @subpackage SucuriScanner
* @author Daniel Cid <
[email protected]>
* @copyright 2010-2018 Sucuri Inc.
* @license https://www.gnu.org/licenses/gpl-2.0.txt GPL2
* @link https://wordpress.org/plugins/sucuri-scanner
*/
if (!defined('SUCURISCAN_INIT') || SUCURISCAN_INIT !== true) {
if (!headers_sent()) {
/* Report invalid access if possible. */
header('HTTP/1.1 403 Forbidden');
}
exit(1);
}
/**
* Returns the HTML to configure the header's cache options.
*
* WordPress by default does not come with cache control headers,
* used by WAFs and CDNs and that are useful to both improve performance
* and reduce bandwidth and other resources demand on the hosting server.
*
* @param bool $nonce True if the CSRF protection worked, false otherwise.
* @return string HTML for the email alert recipients.
*/
function sucuriscan_settings_cache_options($nonce)
{
if (!SucuriScanInterface::checkNonce()) {
SucuriScanInterface::error(__('Invalid nonce.', 'sucuri-scanner'));
return '';
}
$isWooCommerceActive = in_array(
'woocommerce/woocommerce.php',
apply_filters('active_plugins', get_option('active_plugins'))
);
$params = array(
'CacheOptions.Options' => '',
'CacheOptions.Modes' => '',
);
$availableSettings = array(
__('disabled', 'sucuri-scanner'),
__('static', 'sucuri-scanner'),
__('occasional', 'sucuri-scanner'),
__('frequent', 'sucuri-scanner'),
__('busy', 'sucuri-scanner'),
__('custom', 'sucuri-scanner'),
);
$headersCacheControlOptions = SucuriScanOption::getOption(':headers_cache_control_options');
foreach ($availableSettings as $mode) {
$params['CacheOptions.Modes'] .= sprintf('<option value="%s">%s</option>', $mode, ucfirst($mode));
}
if (SucuriScanInterface::checkNonce() && SucuriScanRequest::post(':update_cache_options')) {
$headerCacheControlMode = sanitize_text_field(SucuriScanRequest::post(':cache_options_mode'));
$newOptions = array();
foreach ($headersCacheControlOptions as $pageType => $options) {
$newOptions[$pageType] = array();
foreach ($options as $optionName => $defaultValue) {
$postKey = 'sucuriscan_' . $pageType . '_' . $optionName;
$postValue = sanitize_text_field(SucuriScanRequest::post($postKey));
if (isset($_POST[$postKey])) {
if ($postValue === 'unavailable' || $postValue === '') {
$newOptions[$pageType][$optionName] = 'unavailable';
} else {
$newOptions[$pageType][$optionName] = intval($postValue);
}
} else {
$newOptions[$pageType][$optionName] = $defaultValue;
}
}
}
if (in_array($headerCacheControlMode, $availableSettings)) {
SucuriScanOption::updateOption(':headers_cache_control', $headerCacheControlMode);
SucuriScanOption::updateOption(':headers_cache_control_options', $newOptions);
if ($headerCacheControlMode === 'disabled') {
SucuriScanInterface::info(__('Cache-Control header was deactivated.', 'sucuri-scanner'));
} else {
SucuriScanInterface::info(__('Cache-Control header was activated.', 'sucuri-scanner'));
}
} else {
SucuriScanInterface::error(__('Invalid cache control mode selected.', 'sucuri-scanner'));
}
}
$latestHeadersCacheControlOptions = SucuriScanOption::getOption(':headers_cache_control_options');
foreach ($latestHeadersCacheControlOptions as $option) {
if (!$isWooCommerceActive && in_array(
$option['id'],
array('woocommerce_products', 'woocommerce_categories')
)) {
continue;
}
$params['CacheOptions.Options'] .= SucuriScanTemplate::getSnippet(
'settings-headers-cache-option',
array(
'id' => $option['id'],
'name' => $option['title'],
'maxAge' => $option['max_age'],
'sMaxAge' => $option['s_maxage'],
'staleIfError' => $option['stale_if_error'],
'staleWhileRevalidate' => $option['stale_while_revalidate'],
'paginationFactor' => $option['pagination_factor'],
'paginationFactorVisibility' => $option['pagination_factor'] !== 'unavailable' ? 'visible' : 'hidden',
'oldAgeMultiplier' => $option['old_age_multiplier'],
'oldAgeMultiplierVisibility' => $option['old_age_multiplier'] !== 'unavailable' ? 'visible' : 'hidden',
)
);
}
$headersCacheControlMode = SucuriScanOption::getOption(':headers_cache_control');
$isCacheControlHeaderDisabled = $headersCacheControlMode === 'disabled';
$params['CacheOptions.NoItemsVisibility'] = 'hidden';
$params['CacheOptions.CacheControl'] = $isCacheControlHeaderDisabled ? 0 : 1;
$params['CacheOptions.Status'] = $isCacheControlHeaderDisabled ? __('Disabled', 'sucuri-scanner') : __(
'Enabled',
'sucuri-scanner'
);
$params['CacheOptions.Modes'] = str_replace(
'option value="' . $headersCacheControlMode . '"',
'option value="' . $headersCacheControlMode . '" selected',
$params['CacheOptions.Modes']
);
return SucuriScanTemplate::getSection('settings-headers-cache', $params);
}
/**
* Returns the HTML to configure the CSP security options.
*
* @param string $directive Name of the directive.
* @param object $option Associative array with info of the directive.
*
* @return string HTML for the security CSP header.
*/
function sucuriscan_get_csp_directive_html($directive, $option)
{
$type = isset($option['type']) ? $option['type'] : 'text';
$description = isset($option['description']) ? $option['description'] : '';
$directiveOptions = isset($option['options']) ? $option['options'] : array();
$isDirectiveEnforced = isset($option['enforced']) && (bool)$option['enforced'];
$value = isset($option['value']) ? $option['value'] : '';
$enforcedChecked = $isDirectiveEnforced ? 'checked' : '';
if ($type === 'multi_checkbox') {
$options = '';
$currentValues = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY);
foreach ($directiveOptions as $token => $optionObj) {
$checked = in_array($token, $currentValues) ? ' checked' : '';
$options .= sprintf(
'<div>
<input type="checkbox" name="sucuriscan_csp_%s[]" value="%s"%s>
<label>%s</label>
</div>',
sanitize_text_field($directive),
sanitize_text_field($token),
$checked,
sanitize_text_field($optionObj['title'])
);
}
} else {
// text input for normal directives
$options = sprintf(
'<input type="text" name="sucuriscan_csp_%s" value="%s" />',
sanitize_text_field($directive),
esc_attr($value)
);
}
return SucuriScanTemplate::getSnippet(
'settings-headers-csp-directive',
array(
'id' => sanitize_text_field($option['id']),
'directive' => sanitize_text_field($directive),
'displayName' => sanitize_text_field($option['title']),
'description' => esc_html($description),
'EnforcedChecked' => $enforcedChecked,
'options' => $options,
)
);
}
/**
* Maps the posted CSP directive values to the new options array.
*
* @param array $headersCSPControlOptions Existing CSP options from the store.
*
* @return array Updated CSP options array with enforced and value fields updated.
*/
function sucuriscan_map_csp_options($headersCSPControlOptions)
{
$newOptions = array();
foreach ($headersCSPControlOptions as $directive => $option) {
$type = isset($option['type']) ? $option['type'] : 'text';
$postKey = 'sucuriscan_csp_' . $directive;
$enforcedKey = 'sucuriscan_enforced_' . $directive;
// Determine if enforced is checked
$enforced = isset($_POST[$enforcedKey]) && $_POST[$enforcedKey] == '1';
// Handle text directives
if ($type === 'text') {
// If directive value is set in $_POST, sanitize and store it
if (isset($_POST[$postKey])) {
$postValue = wp_unslash($_POST[$postKey]);
$postValue = SucuriScanCSPHeaders::sanitize_csp_directive(sanitize_text_field($postValue));
$newOptions[$directive] = array(
'id' => esc_attr($option['id']),
'title' => esc_html($option['title']),
'type' => $type,
'description' => isset($option['description']) ? esc_html($option['description']) : '',
'options' => isset($option['options']) ? $option['options'] : array(),
'enforced' => $enforced,
'value' => $postValue,
);
continue;
}
// If not set in $_POST, keep original but update enforced
$option['enforced'] = $enforced;
$newOptions[$directive] = $option;
continue;
}
// Handle multi_checkbox directives
if ($type === 'multi_checkbox') {
$selectedValues = array();
if (isset($_POST[$postKey]) && is_array($_POST[$postKey])) {
foreach ($_POST[$postKey] as $val) {
$token = SucuriScanCSPHeaders::sanitize_csp_directive(sanitize_text_field($val));
if (!empty($token)) {
$selectedValues[] = $token;
}
}
}
$finalValue = empty($selectedValues) ? '' : implode(' ', $selectedValues);
$newOptions[$directive] = array(
'id' => esc_attr($option['id']),
'title' => esc_html($option['title']),
'type' => $type,
'description' => isset($option['description']) ? esc_html($option['description']) : '',
'options' => isset($option['options']) ? $option['options'] : array(),
'enforced' => $enforced,
'value' => $finalValue,
);
continue;
}
}
return $newOptions;
}
/**
* Returns the HTML to configure the header's CSP options.
*
* @param bool $nonce True if the CSRF protection worked, false otherwise.
*
* @return string HTML for the CSP settings.
*/
function sucuriscan_settings_csp_options($nonce)
{
if (!SucuriScanInterface::checkNonce()) {
SucuriScanInterface::error(__('Invalid nonce.', 'sucuri-scanner'));
return '';
}
$params = array(
'CSPOptions.Options' => '',
'CSPOptions.Modes' => '',
'CSPOptions.Status' => '',
'CSPOptions.CSPControl' => '',
);
$headersCSPControlOptions = SucuriScanOption::getOption(':headers_csp_options');
$availableModes = array(
'disabled' => __('Disabled', 'sucuri-scanner'),
'report-only' => __('Report Only', 'sucuri-scanner'),
);
// Process form submission
if (SucuriScanRequest::post(':update_csp_options')) {
$headerCSPMode = sanitize_text_field(SucuriScanRequest::post(':csp_options_mode'));
// Validate selected CSP mode
if (!array_key_exists($headerCSPMode, $availableModes)) {
SucuriScanInterface::error(__('Invalid CSP mode selected.', 'sucuri-scanner'));
} else {
$newOptions = sucuriscan_map_csp_options($headersCSPControlOptions);
// Save new options if valid
SucuriScanOption::updateOption(':headers_csp', $headerCSPMode);
SucuriScanOption::updateOption(':headers_csp_options', $newOptions);
SucuriScanInterface::info(__('Content Security Policy settings were updated.', 'sucuri-scanner'));
}
}
// Get the latest CSP options after update
$headersCSPControl = SucuriScanOption::getOption(':headers_csp');
$headersCSPControlOptions = SucuriScanOption::getOption(':headers_csp_options');
$isCSPControlHeaderDisabled = ($headersCSPControl === 'disabled');
$params['CSPOptions.CSPControl'] = $isCSPControlHeaderDisabled ? 0 : 1;
foreach ($headersCSPControlOptions as $directive => $option) {
$params['CSPOptions.Options'] .= sucuriscan_get_csp_directive_html($directive, $option);
}
// Render CSP mode dropdown
foreach ($availableModes as $modeValue => $modeLabel) {
$selected = ($headersCSPControl === $modeValue) ? ' selected' : '';
$params['CSPOptions.Modes'] .= sprintf(
'<option value="%s"%s>%s</option>',
esc_attr($modeValue),
$selected,
esc_html($modeLabel)
);
}
// Set CSP status
$params['CSPOptions.Status'] = $isCSPControlHeaderDisabled ? __('Disabled', 'sucuri-scanner') : __(
'Report Only',
'sucuri-scanner'
);
return SucuriScanTemplate::getSection('settings-headers-csp', $params);
}