_wp_optimize = WP_Optimize();
$this->_htaccess = new WP_Optimize_Htaccess();
$this->_options = $this->_wp_optimize->get_options();
$this->maybe_restore_expire_values_from_htaccess();
}
/**
* Returns singleton instance object
*
* @return WP_Optimize_Browser_Cache Returns `WP_Optimize_Browser_Cache` object
*/
public static function instance() {
static $_instance = null;
if (null === $_instance) {
$_instance = new self();
}
return $_instance;
}
/**
* Check headers for Cache-Control and Etag. And if they are exist return true.
*
* @return bool|WP_Error
**/
public function is_enabled() {
$headers = $this->get_stylesheet_headers();
if (is_wp_error($headers)) return $headers;
$cache_control = isset($headers['cache-control']) ? (is_array($headers['cache-control']) ? implode(', ', $headers['cache-control']) : $headers['cache-control']) : '';
if (array_key_exists('expires', $headers) && array_key_exists('cache-control', $headers) && preg_match('/\b(public|private)\b/i', $cache_control)) {
$is_enabled = true;
} else {
$is_enabled = false;
}
if ($this->is_browser_cache_section_exists() && false === $this->_wp_optimize->is_apache_module_loaded(array('mod_expires', 'mod_headers'))) {
$is_enabled = new WP_Error('Browser cache', __('We successfully updated your .htaccess file.', 'wp-optimize') . ' ' . __('But it seems one of Apache modules - mod_expires or mod_headers is not active.', 'wp-optimize'));
}
return $is_enabled;
}
/**
* Get expire days and hours values from options.
*
* @return array // [0] - expire days, [1] - expire hours
*/
private function get_expire_data() {
return array(
absint($this->_options->get_option('browser_cache_expire_days', 0)),
absint($this->_options->get_option('browser_cache_expire_hours', 0)),
);
}
/**
* Check if .htaccess has already updated settings (manually by user) then reset error message output
*
* @return void
*/
public function maybe_reset_error_message_output() {
$error_message = $this->_options->get_option('browser_cache_error_message', '');
if ('' === $error_message) return;
list($expire_days, $expire_hours) = $this->get_expire_data();
$expire = $this->prepare_interval($expire_days, $expire_hours);
$search = 'ExpiresByType text/css "access '.$expire.'"';
// if we already have setting in the file then we can reset error message output
if (false !== strpos($this->_htaccess->get_content(), $search)) {
$this->_options->update_option('browser_cache_error_message', '');
$this->_options->update_option('browser_cache_output', '');
}
}
/**
* Set expire days and hours values.
*
* @param int $expire_days
* @param int $expire_hours
* @return void
*/
private function set_expire_values($expire_days, $expire_hours) {
$this->_options->update_option('browser_cache_expire_days', absint($expire_days));
$this->_options->update_option('browser_cache_expire_hours', absint($expire_hours));
}
/**
* Enable browser cache - add settings into .htaccess.
*
* @return void
*/
public function enable() {
list($expire_days, $expire_hours) = $this->get_expire_data();
$this->_htaccess->update_commented_section($this->prepare_browser_cache_section($expire_days, $expire_hours), $this->_htaccess_section_comment);
$this->_htaccess->write_file();
$this->_options->update_option('enable_browser_cache', true);
}
/**
* Disable cache - remove settings from .htaccess added in enable() function.
*
* @return void
*/
public function disable() {
$this->_htaccess->remove_commented_section($this->_htaccess_section_comment);
$this->_htaccess->write_file();
$this->_options->update_option('enable_browser_cache', false);
}
/**
* Restore expiration settings from .htaccess if the options are empty.
*
* @return void
*/
public function maybe_restore_expire_values_from_htaccess() {
$expire_days = $this->_options->get_option('browser_cache_expire_days', '');
$expire_hours = $this->_options->get_option('browser_cache_expire_hours', '');
if ('' === $expire_days && '' === $expire_hours) {
$this->restore_expire_values_from_htaccess();
}
}
/**
* Check if browser cache option is set to true then add section with browser cache settings into .htaccess.
*
* @return void
*/
public function restore() {
list($expire_days, $expire_hours) = $this->get_expire_data();
$enabled = 0 !== $expire_days || 0 !== $expire_hours;
// If we don't have values in the database try to read them from .htaccess file
if (!$enabled) {
$this->restore_expire_values_from_htaccess();
list($expire_days, $expire_hours) = $this->get_expire_data();
$enabled = 0 !== $expire_days || 0 !== $expire_hours;
}
if ($enabled && $this->_htaccess->is_writable()) $this->enable();
}
/**
* Restore expire days and expire hours values form .htaccess if possible.
*
* @return void
*/
private function restore_expire_values_from_htaccess() {
$htaccess_values = $this->get_browser_cache_section_expire_values();
list($expire_days, $expire_hours) = $htaccess_values;
$enabled = 0 !== $expire_days || 0 !== $expire_hours;
if ($enabled) {
$this->set_expire_values($expire_days, $expire_hours);
}
}
/**
* Check if section with browser cache settings already exists.
*
* @return bool
*/
public function is_browser_cache_section_exists() {
return $this->_htaccess->is_commented_section_exists($this->_htaccess_section_comment);
}
/**
* Get expire days and hours values from the .htaccess file
*
* @return array|bool
*/
private function get_browser_cache_section_expire_values() {
$section_content = $this->_htaccess->get_section_content();
if (preg_match('/ExpiresByType text\/css "access ([0-9a-z\s]+)"/i', $section_content, $matches)) {
return $this->expire_string_to_days_hours($matches[1]);
}
return array(0, 0);
}
/**
* Returns days and hours values from expire string
*
* @param string $string
* @return array
*/
private function expire_string_to_days_hours($string) {
$days = 0;
$hours = 0;
preg_match_all(
'/(\d+)\s+(year|years|month|months|day|days|hour|hours)/i',
$string,
$matches,
PREG_SET_ORDER
);
foreach ($matches as $match) {
$value = (int) $match[1];
$unit = strtolower($match[2]);
switch ($unit) {
case 'year':
case 'years':
$days += $value * 365;
break;
case 'month':
case 'months':
$days += $value * 30;
break;
case 'day':
case 'days':
$days += $value;
break;
case 'hour':
case 'hours':
$hours += $value;
break;
}
}
// Convert extra hours into days
$days += intdiv($hours, 24);
$hours %= 24;
return array(absint($days), absint($hours));
}
/**
* Handle for enable_browser_cache command used in WP_Optimize_Commands.
*
* @param array $params - ['browser_cache_expire' => '1 month 15 days 2 hours' || '' - for disable cache]
* @return array
*/
public function enable_browser_cache_command_handler(array $params): array {
$expire_days = absint($params['browser_cache_expire_days']);
$expire_hours = absint($params['browser_cache_expire_hours']);
list($current_expire_days, $current_expire_hours) = $this->get_browser_cache_section_expire_values();
// clear error message output
$this->_options->update_option('browser_cache_error_message', '');
$this->_options->update_option('browser_cache_output', '');
$enable = 0 !== $expire_days || 0 !== $expire_hours;
// Store new expire values in the database
$this->set_expire_values($expire_days, $expire_hours);
/**
* If we don't need to do anything in .htaccess then return message.
*/
if ($enable === $this->_htaccess->is_commented_section_exists()
&& $expire_days === $current_expire_days
&& $expire_hours === $current_expire_hours
) {
if ($enable) {
$message = __('Browser static caching settings already exists in the .htaccess file', 'wp-optimize');
} else {
$message = '';
}
return array(
'success' => true,
'enabled' => $enable,
'message' => $message,
);
}
$section_updated = $this->update_htaccess_section($enable);
$is_enabled = $this->is_enabled();
if ($section_updated) {
if (is_wp_error($is_enabled)) {
return array(
'success' => true,
'enabled' => false,
'error_message' => $is_enabled->get_error_message(),
);
} else {
return array(
'success' => true,
'enabled' => $is_enabled,
'message' => __('We successfully updated your .htaccess file.', 'wp-optimize'),
);
}
} else {
if ($enable) {
// translators: %s is a file name
$message = sprintf(__("We can't update your %s file.", 'wp-optimize'), $this->_htaccess->get_filename()) . ' ' . __('Please try to add following lines manually:', 'wp-optimize');
} else {
// translators: %s is a file name
$message = sprintf(__("We can't update your %s file.", 'wp-optimize'), $this->_htaccess->get_filename()) . ' ' . __('Please try to remove following lines manually:', 'wp-optimize');
}
$output = htmlentities($this->get_htaccess_section_output($enable));
// store error message output
$this->_options->update_option('browser_cache_error_message', $message);
$this->_options->update_option('browser_cache_output', $output);
if (is_wp_error($is_enabled)) {
$message .= ' ' .$is_enabled->get_error_message();
$is_enabled = false;
}
return array(
'success' => false,
'enabled' => $is_enabled,
'error_message' => $message,
'output' => $output,
);
}
}
/**
* Update the .htaccess section for browser caching.
*
* @param boolean $enable
* @return bool True if section updated successfully, false otherwise
*/
private function update_htaccess_section(bool $enable) {
if (!$this->_htaccess->is_writable()) {
return false;
}
// update commented section
if ($enable) {
$this->enable();
} else {
$this->disable();
}
// read updated file.
$this->_htaccess->read_file();
// check if section added or removed successfully.
$section_exists = $this->_htaccess->is_commented_section_exists();
// return true if section updated successfully, false otherwise.
return $enable === $section_exists;
}
/**
* Get output content for enable/disable browser cache in .htaccess.
*
* @param bool $enable
* @return string
*/
private function get_htaccess_section_output($enable) {
list($expire_days, $expire_hours) = $this->get_expire_data();
if ($enable) {
$cache_section = $this->prepare_browser_cache_section($expire_days, $expire_hours);
$output = $this->_htaccess->get_section_begin_comment() . PHP_EOL .
join(PHP_EOL, $this->_htaccess->get_flat_array($cache_section)).
PHP_EOL . $this->_htaccess->get_section_end_comment();
} else {
$output = $this->_htaccess->get_section_begin_comment() . PHP_EOL .
' ... ... ... '.
PHP_EOL . $this->_htaccess->get_section_end_comment();
}
return $output;
}
/**
* Use $days and $hours values to build correct time interval as a string like '2 days 3 hours' or empty string if date is empty.
*
* @param int $days
* @param int $hours
* @return string
*/
private function prepare_interval(int $days, int $hours): string {
if (0 === $days && 0 === $hours) {
return '';
}
$parts = array();
// if hours value more than one day then fix it.
$days += floor($hours / 24);
$hours = $hours % 24;
$years = floor($days / 365);
$days = $days % 365;
$months = floor($days / 30);
$days = $days % 30;
if ($years > 0) {
$parts[] = $years . ($years > 1 ? ' years' : ' year');
}
if ($months > 0) {
$parts[] = $months . ($months > 1 ? ' months' : ' month');
}
if ($days > 0) {
$parts[] = $days . ($days > 1 ? ' days' : ' day');
}
if ($hours > 0) {
$parts[] = $hours . ($hours > 1 ? ' hours' : ' hour');
}
return join(' ', $parts);
}
/**
* Converts a human-readable time string into total seconds
*
* @param int $days
* @param int $hours
* @return string Total number of seconds
*/
private function convert_to_seconds(int $days, int $hours): string {
// Split the string into parts
$total_seconds = $days * 24 * 60 * 60 + $hours * 60 * 60;
return strval($total_seconds);
}
/**
* Build browser cache section array.
*
* @param int $expire_days
* @param int $expire_hours
* @return array
*/
public function prepare_browser_cache_section(int $expire_days, int $expire_hours): array {
$expire = $this->prepare_interval($expire_days, $expire_hours);
$max_age = $this->convert_to_seconds($expire_days, $expire_hours);
return array(
array(
'',
'SetEnvIf Request_URI "/wp-admin/" WPO_NO_CACHE',
'',
),
array(
'',
'ExpiresActive On',
'ExpiresByType text/css "access '.$expire.'"',
'ExpiresByType image/gif "access '.$expire.'"',
'ExpiresByType image/png "access '.$expire.'"',
'ExpiresByType image/jpg "access '.$expire.'"',
'ExpiresByType image/jpeg "access '.$expire.'"',
'ExpiresByType image/webp "access '.$expire.'"',
'ExpiresByType image/x-icon "access '.$expire.'"',
'ExpiresByType application/pdf "access '.$expire.'"',
'ExpiresByType application/javascript "access '.$expire.'"',
'ExpiresByType text/x-javascript "access '.$expire.'"',
'ExpiresByType application/x-shockwave-flash "access '.$expire.'"',
'ExpiresByType image/svg+xml "access '.$expire.'"',
'ExpiresByType font/woff "access '.$expire.'"',
'ExpiresByType font/woff2 "access '.$expire.'"',
'ExpiresByType font/x-woff "access '.$expire.'"',
'ExpiresByType application/font-woff "access '.$expire.'"',
'ExpiresByType application/x-font-woff "access '.$expire.'"',
'ExpiresByType font/ttf "access '.$expire.'"',
'ExpiresByType font/otf "access '.$expire.'"',
'ExpiresByType application/vnd.ms-fontobject "access '.$expire.'"',
'ExpiresByType application/json "access 0 seconds"',
'ExpiresByType application/xml "access 0 seconds"',
'',
),
'',
array(
'',
array(
'',
'Header set Cache-Control "public, max-age='.$max_age.'" env=!WPO_NO_CACHE',
'',
),
array(
'',
'Header set Cache-Control "public, max-age='.$max_age.'" env=!WPO_NO_CACHE',
'',
),
array(
'',
'Header set Cache-Control "private, max-age='.$max_age.'" env=!WPO_NO_CACHE',
'',
),
array(
'',
'Header set Cache-Control "public, max-age='.$max_age.'" env=!WPO_NO_CACHE',
'',
),
array(
'',
'Header set Cache-Control "private, must-revalidate, max-age='.$max_age.'" env=!WPO_NO_CACHE',
'',
),
array(
'',
'Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"',
'',
),
'',
),
array(
'',
'Header unset Expires env=WPO_NO_CACHE',
'',
),
'',
'#Disable ETag',
'FileETag None',
);
}
}