aboutsummaryrefslogtreecommitdiff
path: root/app/Models/Feed.php
diff options
context:
space:
mode:
Diffstat (limited to 'app/Models/Feed.php')
-rw-r--r--app/Models/Feed.php187
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);
}