diff options
Diffstat (limited to 'app/Models/Feed.php')
| -rw-r--r-- | app/Models/Feed.php | 187 |
1 files changed, 172 insertions, 15 deletions
diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 3c5fed507..b91d1af75 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -92,27 +92,171 @@ class FreshRSS_Feed extends Minz_Model { public function hash(): string { if ($this->hash == '') { $salt = FreshRSS_Context::systemConf()->salt; - $params = $this->url; - $curl_params = $this->attributeArray('curl_params'); - if (is_array($curl_params)) { - // Content provided through a proxy may be completely different - $params .= is_string($curl_params[CURLOPT_PROXY] ?? null) ? $curl_params[CURLOPT_PROXY] : ''; - } + $params = $this->url . $this->proxyParam(); $this->hash = sha1($salt . $params); } return $this->hash; } - public function hashFavicon(): string { - if ($this->hashFavicon == '') { + public function resetFaviconHash(): void { + $this->hashFavicon(skipCache: true); + } + + public function proxyParam(): string { + $curl_params = $this->attributeArray('curl_params'); + if (is_array($curl_params)) { + // Content provided through a proxy may be completely different + return is_string($curl_params[CURLOPT_PROXY] ?? null) ? $curl_params[CURLOPT_PROXY] : ''; + } + return ''; + } + + /** + * Resets the custom favicon to the default one. Also deletes the favicon when allowed by extension. + * + * @param array{'url'?:string,'kind'?:int,'category'?:int,'name'?:string,'website'?:string,'description'?:string,'lastUpdate'?:int,'priority'?:int, + * 'pathEntries'?:string,'httpAuth'?:string,'error'?:int,'ttl'?:int,'attributes'?:string|array<string,mixed>} &$values &$values + * + * @param bool $updateFeed Whether `updateFeed()` should be called immediately. If false, it must be handled manually. + * + * @return void + * + * @throws FreshRSS_Feed_Exception + */ + public function resetCustomFavicon(?array &$values = null, bool $updateFeed = true) { + if (!$this->customFavicon()) { + return; + } + if (!$this->attributeBoolean('customFaviconDisallowDel')) { + FreshRSS_Feed::faviconDelete($this->hashFavicon()); + } + $this->_attribute('customFavicon', false); + $this->_attribute('customFaviconExt', null); + $this->_attribute('customFaviconDisallowDel', false); + if ($values !== null) { + $values['attributes'] = $this->attributes(); + $feedDAO = FreshRSS_Factory::createFeedDao(); + if ($updateFeed && !$feedDAO->updateFeed($this->id(), $values)) { + throw new FreshRSS_Feed_Exception(); + } + } + $this->resetFaviconHash(); + } + + /** + * Set a custom favicon for the feed. + * + * @param string $contents Contents of the favicon file. Optional if $tmpPath is set. + * @param string $tmpPath Use only when handling file uploads. (value from `tmp_name` goes here) + * + * @param array{'url'?:string,'kind'?:int,'category'?:int,'name'?:string,'website'?:string,'description'?:string,'lastUpdate'?:int,'priority'?:int, + * 'pathEntries'?:string,'httpAuth'?:string,'error'?:int,'ttl'?:int,'attributes'?:string|array<string,mixed>} &$values &$values + * + * @param bool $updateFeed Whether `updateFeed()` should be called immediately. If false, it must be handled manually. + * @param string $extName The extension name of the calling extension. + * @param bool $disallowDelete Whether the icon can be later deleted when it's being reset. Intended for use by extensions. + * @param bool $overrideCustomIcon Whether a custom favicon set by a user can be overridden. + * + * @return string|null Path where the favicon can be found. Useful for checking if the favicon already exists, before downloading it for example. + * + * @throws FreshRSS_UnsupportedImageFormat_Exception + * @throws FreshRSS_Feed_Exception + */ + public function setCustomFavicon( + ?string $contents = null, + string $tmpPath = '', + ?array &$values = null, + bool $updateFeed = true, + ?string $extName = null, + bool $disallowDelete = false, + bool $overrideCustomIcon = false + ): ?string { + if ($contents === null && $tmpPath !== '') { + $contents = file_get_contents($tmpPath); + } + + $attributesOnly = $contents === null && $tmpPath === ''; + if (!$attributesOnly && !isImgMime(is_string($contents) ? $contents : '')) { + throw new FreshRSS_UnsupportedImageFormat_Exception(); + } + + $oldHash = ''; + $oldDisallowDelete = false; + if ($this->customFavicon()) { + /* If $overrideCustomFavicon is true, custom favicons set by extensions can be overridden, + * but not ones explicitly set by the user */ + if (!$overrideCustomIcon && $this->customFaviconExt() === null) { + return null; + } + $oldHash = $this->hashFavicon(skipCache: true); + $oldDisallowDelete = $this->attributeBoolean('customFaviconDisallowDel'); + } + $this->_attribute('customFavicon', true); + $this->_attribute('customFaviconExt', $extName); + $this->_attribute('customFaviconDisallowDel', $disallowDelete); + + require_once(LIB_PATH . '/favicons.php'); + $newPath = FAVICONS_DIR . $this->hashFavicon(skipCache: true) . '.ico'; + if ($attributesOnly && !file_exists($newPath)) { + $updateFeed = false; + } + + if ($values !== null) { + $values['attributes'] = $this->attributes(); + $feedDAO = FreshRSS_Factory::createFeedDao(); + if ($updateFeed && !$feedDAO->updateFeed($this->id(), $values)) { + throw new FreshRSS_Feed_Exception(); + } + } + + if ($tmpPath !== '') { + move_uploaded_file($tmpPath, $newPath); + } elseif ($contents !== null) { + file_put_contents($newPath, $contents); + } + + if ($oldHash !== '' && !$oldDisallowDelete) { + FreshRSS_Feed::faviconDelete($oldHash); + } + + return $newPath; + } + + /** + * Checks if the feed has a custom favicon set by an extension. + * Additionally, it also checks if the extension that set the icon is still enabled + * And if not, it resets attributes related to custom favicons. + * + * @return string|null The name of the extension that set the icon. + */ + public function customFaviconExt(): ?string { + $customFaviconExt = $this->attributeString('customFaviconExt'); + if ($customFaviconExt !== null && !Minz_ExtensionManager::isExtensionEnabled($customFaviconExt)) { + $this->_attribute('customFavicon', false); + $this->_attribute('customFaviconExt', null); + $this->_attribute('customFaviconDisallowDel', false); + $customFaviconExt = null; + } + return $customFaviconExt; + } + + public function customFavicon(): bool { + $this->customFaviconExt(); + return $this->attributeBoolean('customFavicon') ?? false; + } + + public function hashFavicon(bool $skipCache = false): string { + if ($this->hashFavicon == '' || $skipCache) { $salt = FreshRSS_Context::systemConf()->salt; - $params = $this->website(fallback: true); - $curl_params = $this->attributeArray('curl_params'); - if (is_array($curl_params)) { - // Content provided through a proxy may be completely different - $params .= is_string($curl_params[CURLOPT_PROXY] ?? null) ? $curl_params[CURLOPT_PROXY] : ''; + $params = ''; + if ($this->customFavicon()) { + $current = $this->id . Minz_User::name(); + $hookParams = Minz_ExtensionManager::callHook('custom_favicon_hash', $this); + $params = $hookParams !== null ? $hookParams : $current; + } else { + $params = $this->website(fallback: true) . $this->proxyParam(); } - $this->hashFavicon = hash('crc32b', $salt . $params); + $this->hashFavicon = hash('crc32b', $salt . (is_string($params) ? $params : '')); } return $this->hashFavicon; } @@ -257,6 +401,9 @@ class FreshRSS_Feed extends Minz_Model { public function faviconPrepare(bool $force = false): void { require_once(LIB_PATH . '/favicons.php'); + if ($this->customFavicon()) { + return; + } $url = $this->website(fallback: true); $txt = FAVICONS_DIR . $this->hashFavicon() . '.txt'; if (@file_get_contents($txt) !== $url) { @@ -286,7 +433,14 @@ class FreshRSS_Feed extends Minz_Model { @unlink($path . '.txt'); } public function favicon(): string { - return Minz_Url::display('/f.php?' . $this->hashFavicon()); + $hash = $this->hashFavicon(); + $url = '/f.php?h=' . $hash; + if ($this->customFavicon() + // when the below attribute is set, icon won't be changing frequently so cache buster is not needed + && !$this->attributeBoolean('customFaviconDisallowDel')) { + $url .= '&t=' . @filemtime(DATA_PATH . '/favicons/' . $hash . '.ico'); + } + return Minz_Url::display($url); } public function _id(int $value): void { @@ -1069,6 +1223,9 @@ class FreshRSS_Feed extends Minz_Model { } private function faviconRebuild(): void { + if ($this->customFavicon()) { + return; + } FreshRSS_Feed::faviconDelete($this->hashFavicon()); $this->faviconPrepare(true); } |
