|
|
@@ -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);
|
|
|
}
|