Skip to content

Commit

Permalink
Refined logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Alkarex committed Sep 15, 2024
1 parent 0cd7af4 commit 0c7c435
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 24 deletions.
2 changes: 2 additions & 0 deletions src/Cache/BaseDataCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ public function set_data(string $key, array $value, ?int $ttl = null): bool
{
if ($ttl === null) {
$ttl = 3600;
} elseif ($ttl <= 0) {
return $this->delete_data($key); // FreshRSS
}

// place internal cache expiration time
Expand Down
27 changes: 15 additions & 12 deletions src/HTTP/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,57 @@

/**
* HTTP util functions
*
* FreshRSS
* @internal
*/
final class Utils
{
/**
* Negotiate the cache expiration time based on the HTTP response headers.
* Return the cache duration time in number of seconds since the Unix Epoch, accounting for:
* - `Cache-Control: max-age` minus `Age`, extendable by `$simplepie_cache_duration`
* - `Cache-Control: must-revalidate` will prevent `$simplepie_cache_duration` from extending past the `max-age`
* - `Cache-Control: max-age` minus `Age`, bounded by `$cache_duration_min` and `$cache_duration_max`
* - `Cache-Control: must-revalidate` will prevent `$cache_duration_min` from extending past the `max-age`
* - `Cache-Control: no-cache` will return the current time
* - `Cache-Control: no-store` will return `0`
* - `Expires` but only if `Cache-Control: max-age` is absent
* - `Expires` like `Cache-Control: max-age` but only if it is absent
*
* @param int $simplepie_cache_duration Cache duration in seconds desired from SimplePie
* @param array<string,mixed> $http_headers HTTP headers of the response
* @return int
* @param int $cache_duration Desired cache duration in seconds, potentially overriden by HTTP response headers
* @param int $cache_duration_min Minimal cache duration (in seconds), overriding HTTP response headers `Cache-Control: max-age` and `Expires`
* @param int $cache_duration_max Maximal cache duration (in seconds), overriding HTTP response headers `Cache-Control: max-age` and `Expires`
* @return int The negociated cache expiration time in seconds since the Unix Epoch
*
* FreshRSS
*/
public static function negociate_cache_expiration_time(int $simplepie_cache_duration, array $http_headers): int
public static function negociate_cache_expiration_time(array $http_headers, int $cache_duration, int $cache_duration_min, int $cache_duration_max): int
{
$cache_control = $http_headers['cache-control'] ?? '';
if ($cache_control !== '') {
if (preg_match('/\bno-store\b/', $cache_control)) {
return 0;
}
if (preg_match('/\bno-cache\b/', $cache_control)) {
return time() + 1; // +1 to account for inequalities
return time();
}
if (preg_match('/\bmust-revalidate\b/', $cache_control)) {
$simplepie_cache_duration = 0;
$cache_duration_min = 0;
}
if (preg_match('/\bmax-age=(\d+)\b/', $cache_control, $matches)) {
$max_age = (int) $matches[1];
$age = $http_headers['age'] ?? '';
if (is_numeric($age)) {
$max_age -= (int) $age;
}
return time() + $max_age + $simplepie_cache_duration + 1;
return time() + min(max($max_age, $cache_duration_min), $cache_duration_max);
}
}
$expires = $http_headers['expires'] ?? '';
if ($expires !== '') {
$expire_date = \SimplePie\Misc::parse_date($expires);
if ($expire_date !== false) {
return $expire_date + $simplepie_cache_duration + 1;
return min(max($expire_date, time() + $cache_duration_min), time() + $cache_duration_max);
}
}
return $simplepie_cache_duration + time();
return time() + min(max($cache_duration, $cache_duration_min), $cache_duration_max);
}
}
63 changes: 51 additions & 12 deletions src/SimplePie.php
Original file line number Diff line number Diff line change
Expand Up @@ -509,12 +509,30 @@ class SimplePie
public $force_cache_fallback = false;

/**
* @var int Cache duration (in seconds)
* @var int Cache duration (in seconds), but may be overriden by HTTP response headers (FreshRSS)
* @see SimplePie::set_cache_duration()
* @access private
*/
public $cache_duration = 3600;

/**
* @var int Minimal cache duration (in seconds), overriding HTTP response headers `Cache-Control: max-age` and `Expires`.
* But no effect on `Cache-Control: no-store` and `Cache-Control: no-cache`
* @see SimplePie::set_cache_duration()
* @access private
* FreshRSS
*/
public $cache_duration_min = 60;

/**
* @var int Maximal cache duration (in seconds), overriding HTTP response headers `Cache-Control: max-age` and `Expires`.
* But no effect on `Cache-Control: no-store` and `Cache-Control: no-cache`
* @see SimplePie::set_cache_duration()
* @access private
* FreshRSS
*/
public $cache_duration_max = 86400;

/**
* @var int Auto-discovery cache duration (in seconds)
* @see SimplePie::set_autodiscovery_cache_duration()
Expand Down Expand Up @@ -989,12 +1007,27 @@ public function force_cache_fallback(bool $enable = false)
* Set the length of time (in seconds) that the contents of a feed will be
* cached
*
* @param int $seconds The feed content cache duration
* FreshRSS: Note that the cache is (partially) HTTP compliant,
* so minimum and maximum parameters have no effect on `Cache-Control: no-store` and `Cache-Control: no-cache`
*
* @param int $seconds The feed content cache duration, which may be overriden by HTTP response headers)
* @param int $min The minimun cache duration (default: 60s), overriding HTTP response headers `Cache-Control: max-age` and `Expires`
* @param int $max The maximum cache duration (default: 24h), overriding HTTP response headers `Cache-Control: max-age` and `Expires`
* @return void
*/
public function set_cache_duration(int $seconds = 3600)
public function set_cache_duration(int $seconds = 3600, ?int $min = null, ?int $max = null)
{
$this->cache_duration = $seconds;
if (is_int($min)) { // FreshRSS
$this->cache_duration_min = $min;
} elseif ($this->cache_duration_min > $seconds) {
$this->cache_duration_min = $seconds;
}
if (is_int($max)) { // FreshRSS
$this->cache_duration_max = $max;
} elseif ($this->cache_duration_max < $seconds) {
$this->cache_duration_max = $seconds;
}
}

/**
Expand Down Expand Up @@ -1851,7 +1884,7 @@ public function init()
$this->data['hash'] = $this->data['hash'] ?? $this->clean_hash($this->raw_data); // FreshRSS

// Cache the file if caching is enabled
$this->data['cache_expiration_time'] = \SimplePie\HTTP\Utils::negociate_cache_expiration_time($this->cache_duration, $this->data['headers'] ?? []);
$this->data['cache_expiration_time'] = \SimplePie\HTTP\Utils::negociate_cache_expiration_time($this->data['headers'] ?? [], $this->cache_duration, $this->cache_duration_min, $this->cache_duration_max);

if ($cache && !$cache->set_data($this->get_cache_filename($this->feed_url), $this->data, $this->cache_duration)) {
trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
Expand Down Expand Up @@ -1972,8 +2005,10 @@ protected function fetch_data(&$cache)
$this->status_code = 0;

if ($this->force_cache_fallback) {
$this->data['cache_expiration_time'] = \SimplePie\HTTP\Utils::negociate_cache_expiration_time($this->cache_duration, $this->data['headers'] ?? []); // FreshRSS
$cache->set_data($cacheKey, $this->data, $this->cache_duration);
$this->data['cache_expiration_time'] = \SimplePie\HTTP\Utils::negociate_cache_expiration_time($this->data['headers'] ?? [], $this->cache_duration, $this->cache_duration_min, $this->cache_duration_max); // FreshRSS
if (!$cache->set_data($cacheKey, $this->data, $this->data['cache_expiration_time'] <= 0 ? 0 : $this->cache_duration_max)) { // FreshRSS
trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
}

return true;
}
Expand All @@ -1987,12 +2022,14 @@ protected function fetch_data(&$cache)
$this->raw_data = false;
if (isset($file)) { // FreshRSS
// Update cache metadata
$this->data['cache_expiration_time'] = \SimplePie\HTTP\Utils::negociate_cache_expiration_time($this->cache_duration, $this->data['headers'] ?? []);
$this->data['cache_expiration_time'] = \SimplePie\HTTP\Utils::negociate_cache_expiration_time($this->data['headers'] ?? [], $this->cache_duration, $this->cache_duration_min, $this->cache_duration_max);
$this->data['headers'] = array_map(function (array $values): string {
return implode(',', $values);
}, $file->get_headers());
}
$cache->set_data($cacheKey, $this->data, $this->cache_duration);
if (!$cache->set_data($cacheKey, $this->data, ($this->data['cache_expiration_time'] ?? 1) <= 0 ? 0 : $this->cache_duration_max)) { // FreshRSS
trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
}

return true;
}
Expand All @@ -2001,11 +2038,13 @@ protected function fetch_data(&$cache)
$hash = $this->clean_hash($file->get_body_content());
if (($this->data['hash'] ?? null) === $hash) {
// Update cache metadata
$this->data['cache_expiration_time'] = \SimplePie\HTTP\Utils::negociate_cache_expiration_time($this->cache_duration, $this->data['headers'] ?? []);
$this->data['cache_expiration_time'] = \SimplePie\HTTP\Utils::negociate_cache_expiration_time($this->data['headers'] ?? [], $this->cache_duration, $this->cache_duration_min, $this->cache_duration_max);
$this->data['headers'] = array_map(function (array $values): string {
return implode(',', $values);
}, $file->get_headers());
$cache->set_data($cacheKey, $this->data, $this->cache_duration);
if (!$cache->set_data($cacheKey, $this->data, $this->data['cache_expiration_time'] <= 0 ? 0 : $this->cache_duration_max)) { // FreshRSS
trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
}

return true; // Content unchanged even though server did not send a 304
} else {
Expand Down Expand Up @@ -2138,12 +2177,12 @@ protected function fetch_data(&$cache)
'url' => $this->feed_url,
'feed_url' => $file->get_final_requested_uri(),
'build' => Misc::get_build(),
'cache_expiration_time' => \SimplePie\HTTP\Utils::negociate_cache_expiration_time($this->cache_duration, $this->data['headers'] ?? []), // FreshRSS
'cache_expiration_time' => \SimplePie\HTTP\Utils::negociate_cache_expiration_time($this->data['headers'] ?? [], $this->cache_duration, $this->cache_duration_min, $this->cache_duration_max), // FreshRSS
'cache_version' => self::CACHE_VERSION, // FreshRSS
'hash' => empty($hash) ? $this->clean_hash($file->get_body_content()) : $hash, // FreshRSS
];

if (!$cache->set_data($cacheKey, $this->data, $this->cache_duration)) {
if (!$cache->set_data($cacheKey, $this->data, $this->data['cache_expiration_time'] <= 0 ? 0 : $this->cache_duration_max)) { // FreshRSS
trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
}
}
Expand Down

0 comments on commit 0c7c435

Please sign in to comment.