diff --git a/README.md b/README.md index f1069cd..ac759de 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,88 @@ -# Image Cache Extension +# Image Cache Extension for FreshRSS -This FreshRSS extension allows you to cache feeds’ pictures in your own facility. +This extension allows you to cache images from the feeds in FreshRSS. It helps speed up feed loading times and reduce bandwidth usage by caching images on a server. -To use it, upload this entire directory to the FreshRSS `./extensions` directory on your server and enable it on the extension panel in FreshRSS. +## Installation -There is a Cloudflare worker implementation of the cache utilizing its Cache API. Check this [repo](https://github.com/Victrid/image-cache-worker) (It's can be run on free tier). +1. **Download and Setup**: + - Download the zip file from the [Releases page](https://github.com/Victrid/freshrss-image-cache-plugin/releases). + - Extract the folder and place it in the `extensions` directory of your FreshRSS installation, ensuring that the `metadata.json` file is in the root of the `imagecache` folder. + - Enable the extension through the FreshRSS extension panel. -## Configuration settings + After installation, your directory structure should resemble: -- `cache_url` (default: `https://example.com/pic?url=`): The URL of the image used to load when the user reads the feed article. + ``` + extensions/ + |-- imagecache/ + | |-- metadata.json + | |-- imagecache.php + | |-- ... + |-- some-other-extension/ + |-- ... + ``` -- `post_url` (default: `https://example.com/prepare`): Address used to inform the caching service when FreshRSS fetches a new article. +## Usage - The plugin will send a JSON POST request to this address in this format: +**Note**: This extension does not cache images directly within the FreshRSS instance. Instead, it works with an external cache service to store images. - ```json - { - "url": "https://http.cat/418.jpg", - "access_token": "YOUR_ACCESS_TOKEN" - } - ``` +### How It Works -- `access_token` (default: `""`): See the JSON request above. +The diagram below illustrates the extension's operation: -- `url_encode` (default: `1`): whether to URL-encode (RFC 3986) the proxied URL. +![ImageCache Workflow](imagecache.svg) -## Important Note +When proactive caching is enabled, FreshRSS sends a request to your cache service to store the image if a new feed entry includes image URLs. This modifies the image URL so users access the cached version instead of the original source. -Your cache implementation should not rely on the `post`-method, in other words, the `cache_url` should support cache-miss situations. +### Setting Up a Cache Server -## See Also +You have two options for setting up your cache server: -[ImageProxy](https://github.com/FreshRSS/Extensions/tree/master/xExtension-ImageProxy) plugin: Don’t need a cache, just proxy? Use ImageProxy plugin instead. +1. **Self-Hosted Server**: + - Use the example provided in [piccache.php.example](piccache.php.example). Rename it to `piccache.php` and place it in your `/path/to/FreshRSS/p` directory. + - Update the configuration in `piccache.php` as follows: -This extension is based on ImageProxy plugin, and is licensed under GPLv3. + ```php + define("CACHE_PLACE_PATH", "/path/to/cache/folder"); + define("ACCESS_TOKEN", "SoMe_oBsCuRe_aCcEsS_ToKeN"); + ``` + + - (For Docker users) You can build a custom image with the following Dockerfile: + + ```dockerfile + FROM freshrss/freshrss:latest + + COPY piccache.php /var/www/FreshRSS/p/piccache.php + ``` + + - Configure FreshRSS to use the caching service: + + ```yaml + Cache URL: "http://192.168.1.123:4567/piccache.php?url=" + Enable proactive cache: checked + Proactive Cache URL: "http://192.168.1.123:4567/piccache.php" + Access Token: "SoMe_oBsCuRe_aCcEsS_ToKeN" + ``` + + This script is basic and does not handle cleaning up old caches or implementing crawler-detection avoidance. If you need a reliable cache server, consider the cloudflare worker solution below. + +2. **Cloudflare Worker**: + - If you have limited bandwidth or experience high latency, consider using a [Cloudflare Worker](https://github.com/Victrid/image-cache-worker). This solution caches images on Cloudflare's CDN, which can be set up on their free tier without a custom domain. + +## Additional Information + +When proactive cache is enabled, the plugin sends a JSON POST request to the cache URL in the following format: + +```json +{ + "url": "https://http.cat/418.jpg", + "access_token": "YOUR_ACCESS_TOKEN" +} +``` + +## Alternatives + +Consider the [ImageProxy](https://github.com/FreshRSS/Extensions/tree/master/xExtension-ImageProxy) plugin if you need a simpler solution for proxying images without caching. + +## License + +This extension is inspired by the ImageProxy plugin and is available under the GPLv3 license. diff --git a/configure.phtml b/configure.phtml index 9f175af..159c05f 100644 --- a/configure.phtml +++ b/configure.phtml @@ -1,35 +1,40 @@ -
- -

User fetch settings

+ + + +

- +
- +
-
- +

+

+
+
- image_cache_url_encode ? 'checked' : ''); ?>> + image_cache_post_enabled ? 'checked' : '' ?>>
-

FreshRSS notification settings

- +
- +
- +
- +
- - + +
diff --git a/extension.php b/extension.php index 533d52a..537440b 100644 --- a/extension.php +++ b/extension.php @@ -1,142 +1,167 @@ registerHook('entry_before_display', - array($this, 'content_modification_hook')); - $this->registerHook('entry_before_insert', - array($this, 'image_upload_hook')); - // Defaults - $save = false; - FreshRSS_Context::$user_conf->image_cache_url=html_entity_decode(FreshRSS_Context::$user_conf->image_cache_url); - if (is_null(FreshRSS_Context::$user_conf->image_cache_url)) { - FreshRSS_Context::$user_conf->image_cache_url = self::CACHE_URL; - $save = true; - } - if (is_null(FreshRSS_Context::$user_conf->image_cache_post_url)) { - FreshRSS_Context::$user_conf->image_cache_post_url = self::CACHE_POST_URL; - $save = true; - } - if (is_null(FreshRSS_Context::$user_conf->image_cache_post_url)) { - FreshRSS_Context::$user_conf->image_cache_access_token = self::CACHE_ACCESS_TOKEN; - $save = true; - } - if (is_null(FreshRSS_Context::$user_conf->image_cache_url_encode)) { - FreshRSS_Context::$user_conf->image_cache_url_encode = self::URL_ENCODE; - $save = true; - } - if ($save) { - FreshRSS_Context::$user_conf->save(); - } - } +final class ImageCacheExtension extends Minz_Extension +{ + // Defaults + private const CACHE_URL = 'https://wsrv.nl/?url='; + private const CACHE_POST_URL = 'https://example.com/prepare'; + private const CACHE_ACCESS_TOKEN = ''; + private const URL_ENCODE = '1'; + private const CACHE_POST_ENABLED = ''; - public function handleConfigureAction() { - $this->registerTranslates(); + #[\Override] + public function init(): void + { + if (!FreshRSS_Context::hasSystemConf()) { + throw new FreshRSS_Context_Exception('System configuration not initialised!'); + } + $this->registerHook('entry_before_display', [self::class, 'content_modification_hook']); + $this->registerHook('entry_before_insert', [self::class, 'image_upload_hook']); + // Defaults + $save = false; + if (is_null(FreshRSS_Context::userConf()->image_cache_url)) { + FreshRSS_Context::userConf()->image_cache_url = self::CACHE_URL; + $save = true; + } + if (is_null(FreshRSS_Context::userConf()->image_cache_post_url)) { + FreshRSS_Context::userConf()->image_cache_post_url = self::CACHE_POST_URL; + $save = true; + } + if (is_null(FreshRSS_Context::userConf()->image_cache_post_url)) { + FreshRSS_Context::userConf()->image_cache_access_token = self::CACHE_ACCESS_TOKEN; + $save = true; + } + if (is_null(FreshRSS_Context::userConf()->image_cache_post_enabled)) { + FreshRSS_Context::userConf()->image_cache_post_enabled = self::CACHE_POST_ENABLED; + $save = true; + } + if ($save) { + FreshRSS_Context::userConf()->save(); + } + } + + #[\Override] + public function handleConfigureAction(): void + { + $this->registerTranslates(); + + if (Minz_Request::isPost()) { + FreshRSS_Context::userConf()->image_cache_url = Minz_Request::paramString('image_cache_url'); + FreshRSS_Context::userConf()->image_cache_post_url = Minz_Request::paramString('image_cache_post_url'); + FreshRSS_Context::userConf()->image_cache_access_token = Minz_Request::paramString('image_cache_access_token'); + FreshRSS_Context::userConf()->image_cache_post_enabled = Minz_Request::paramString('image_cache_post_enabled'); + FreshRSS_Context::userConf()->save(); + } + } - if (Minz_Request::isPost()) { - FreshRSS_Context::$user_conf->image_cache_url = Minz_Request::param('image_cache_url', self::CACHE_URL); - FreshRSS_Context::$user_conf->image_cache_post_url = Minz_Request::param('image_cache_post_url', self::CACHE_POST_URL); - FreshRSS_Context::$user_conf->image_cache_access_token = Minz_Request::param('image_cache_access_token', self::CACHE_ACCESS_TOKEN); - FreshRSS_Context::$user_conf->image_cache_url_encode = Minz_Request::param('image_cache_url_encode', ''); - FreshRSS_Context::$user_conf->save(); - } - } - - public static function posturl($url,$data){ - $data = json_encode($data); + public static function curlPostRequest(string $url, array $data): mixed + { + $data = json_encode($data); $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "POST"); curl_setopt($curl, CURLOPT_POSTFIELDS, $data); - curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_HEADER, true); curl_setopt($curl, CURLOPT_HTTPHEADER, array( - "Content-Type: application/json;charset='utf-8'", - 'Content-Length: ' . strlen($data), - "Accept: application/json") - ); + "Content-Type: application/json;charset='utf-8'", + 'Content-Length: ' . strlen($data), + "Accept: application/json") + ); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($curl, CURLOPT_TIMEOUT, 10); $output = curl_exec($curl); curl_close($curl); - return json_decode($output, true); + return json_decode($output, true); } - - public static function post_request($url, $post_url) { - return self::posturl($post_url, array("access_token" => FreshRSS_Context::$user_conf->image_cache_access_token, "url" => $url)); + + public static function send_proactive_cache_request(string $url): mixed + { + if (FreshRSS_Context::userConf()->image_cache_post_enabled) { + $post_url = FreshRSS_Context::userConf()->image_cache_post_url; + return self::curlPostRequest($post_url, array("access_token" => FreshRSS_Context::userConf()->image_cache_access_token, "url" => $url)); + } + return false; } - public static function getCacheImageUri($url) { + public static function getCacheImageUri(string $url): string + { $url = rawurlencode($url); - return FreshRSS_Context::$user_conf->image_cache_url . $url; - } - - - # Used for srcset - public static function getSrcSetUris($matches) { - return str_replace($matches[1], self::getCacheImageUri($matches[1]), $matches[0]); - } - - public static function uploadSrcSetUris($matches) { - return str_replace($matches[1], self::post_request($matches[1], FreshRSS_Context::$user_conf->image_cache_post_url), $matches[0]); - } - - public static function uploadUris($content) { - if (empty($content)) { - return $content; - } - $doc = new DOMDocument(); - libxml_use_internal_errors(true); // prevent tag soup errors from showing - $doc->loadHTML(mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8')); - $imgs = $doc->getElementsByTagName('img'); - foreach ($imgs as $img) { - if ($img->hasAttribute('src')) { - self::post_request($img->getAttribute('src'), FreshRSS_Context::$user_conf->image_cache_post_url); - } - if ($img->hasAttribute('srcset')) { - $newSrcSet = preg_replace_callback('/(?:([^\s,]+)(\s*(?:\s+\d+[wx])(?:,\s*)?))/', 'self::uploadSrcSetUris', $img->getAttribute('srcset')); - } - } - } + return FreshRSS_Context::userConf()->image_cache_url . $url; + } - public static function swapUris($content) { - if (empty($content)) { - return $content; - } - $doc = new DOMDocument(); - libxml_use_internal_errors(true); // prevent tag soup errors from showing - $doc->loadHTML(mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8')); - $imgs = $doc->getElementsByTagName('img'); - foreach ($imgs as $img) { - if ($img->hasAttribute('src')) { - $newSrc = self::getCacheImageUri($img->getAttribute('src')); - $img->setAttribute('src', $newSrc); - } - if ($img->hasAttribute('srcset')) { - $newSrcSet = preg_replace_callback('/(?:([^\s,]+)(\s*(?:\s+\d+[wx])(?:,\s*)?))/', 'self::getSrcSetUris', $img->getAttribute('srcset')); - $img->setAttribute('srcset', $newSrcSet); - } - } - return $doc->saveHTML(); - } + public static function small($string) +{ + return substr($string, 0, 20); +} - public static function content_modification_hook($entry) { - $entry->_content( - self::swapUris($entry->content()) - ); + public static function cache_images(string $content): string + { + if (empty($content)) { + return $content; + } + $doc = new DOMDocument(); + libxml_use_internal_errors(true); // prevent tag soup errors from showing + $encoding = mb_detect_encoding($content); + $doc->loadHTML(''.$content); + $imgs = $doc->getElementsByTagName('img'); + foreach ($imgs as $img) { + if ($img->hasAttribute('src')) { + self::send_proactive_cache_request($img->getAttribute('src')); + } + if ($img->hasAttribute('srcset')) { + preg_replace_callback('/(?:([^\s,]+)(\s*(?:\s+\d+[wx])(?:,\s*)?))/', + function (array $matches): string { + self::send_proactive_cache_request($matches[1]); + return ''; + }, + $img->getAttribute('srcset')); + } + } + return $content; + } + + public static function swapUris(string $content): string + { + if (empty($content)) { + return $content; + } + $doc = new DOMDocument(); + libxml_use_internal_errors(true); // prevent tag soup errors from showing + $encoding = mb_detect_encoding($content); + $doc->loadHTML(''.$content); + $imgs = $doc->getElementsByTagName('img'); + foreach ($imgs as $img) { + if ($img->hasAttribute('src')) { + $newSrc = self::getCacheImageUri($img->getAttribute('src')); + $img->setAttribute('src', $newSrc); + } + if ($img->hasAttribute('srcset')) { + $newSrcSet = preg_replace_callback('/(?:([^\s,]+)(\s*(?:\s+\d+[wx])(?:,\s*)?))/', + function (array $matches): string { + return str_replace($matches[1], self::getCacheImageUri($matches[1]), $matches[0]); + } + , $img->getAttribute('srcset')); + $img->setAttribute('srcset', $newSrcSet); + } + } + return $doc->saveHTML(); + } - return $entry; - } - - public static function image_upload_hook($entry) { - self::uploadUris($entry->content()); + public static function content_modification_hook($entry) + { + $entry->_content( + self::swapUris($entry->content()) + ); - return $entry; - } + return $entry; + } + + public static function image_upload_hook($entry) + { + self::cache_images($entry->content()); + return $entry; + } } diff --git a/i18n/en/ext.php b/i18n/en/ext.php index 92adc66..a772ecc 100644 --- a/i18n/en/ext.php +++ b/i18n/en/ext.php @@ -2,9 +2,13 @@ return array( 'imagecache' => array( - 'cache_url' => 'Cache URL (for user to fetch)', - 'url_encode' => 'Encode the URL', - 'post_url' => 'Post URL (for freshRSS to inform)', - 'access_token' => 'Access Token (for freshRSS to inform)', + 'cache_url' => 'Cache URL', + 'post_url' => 'Proactive Cache URL', + 'access_token' => 'Access Token', + 'fetch_settings' => 'Fetch settings', + 'proactive_cache' => 'FreshRSS Proactive Cache Settings', + 'proactive_cache_enabled' => 'Enable proactive cache', + 'proactive_cache_desc' => 'The proactive cache allows FreshRSS to notify cache server to download pictures ' . + 'when new article is fetched. This will improve picture loading speed.', ), -); +); diff --git a/i18n/zh-CN/ext.php b/i18n/zh-CN/ext.php index 0592bdf..aaac8f6 100644 --- a/i18n/zh-CN/ext.php +++ b/i18n/zh-CN/ext.php @@ -2,9 +2,12 @@ return array( 'imagecache' => array( - 'cache_url' => '缓存URL(用于用户获取)', - 'url_encode' => '对URL进行转义', - 'post_url' => '通知URL(用于建立缓存)', - 'access_token' => '访问token(用于建立缓存)', + 'cache_url' => '缓存URL', + 'post_url' => '主动缓存URL', + 'access_token' => '主动缓存访问令牌', + 'fetch_settings' => '用户请求设置', + 'proactive_cache' => '主动缓存设置', + 'proactive_cache_enabled' => '启用主动缓存', + 'proactive_cache_desc' => '主动缓存允许FreshRSS在获取到新文章时就通知缓存服务器下载图片。这样可以加快看到图片的速度。', ), ); diff --git a/imagecache.svg b/imagecache.svg new file mode 100644 index 0000000..c110aa8 --- /dev/null +++ b/imagecache.svg @@ -0,0 +1,3 @@ + + +
contact cache server
FreshRSS
image
Cache Server
image
Original Server
Read Article
GET image
Client
New Article
Please Cache
GET image
Read Article
contact cache server
GET image
GET image
image
image
Proactive
Cache
No Proactive Cache,
First time access
(Like ImageProxy)
Cached
Read Article
contact original server
GET image
image
Original
\ No newline at end of file diff --git a/metadata.json b/metadata.json index b0b0102..00e0028 100644 --- a/metadata.json +++ b/metadata.json @@ -2,7 +2,7 @@ "name": "Image Cache", "author": "Victrid", "description": "Cache feed images on your own facility or Cloudflare cache.", - "version": 0.3, + "version": "0.4.0", "entrypoint": "ImageCache", "type": "user" } diff --git a/piccache.php.example b/piccache.php.example new file mode 100644 index 0000000..f9fa9db --- /dev/null +++ b/piccache.php.example @@ -0,0 +1,85 @@ +