Writing extensions for FreshRSS

About FreshRSS

FreshRSS is an RSS / Atom feed aggregator written in PHP dating back to October 2012. The official site is located at freshrss.org and the official repository is hosted on GitHub: github.com/FreshRSS/FreshRSS.

The problem

FreshRSS is limited in its technical possibilities by various factors:

While the first limitation can, in theory, be lifted by the participation of new contributors to the project, it depends on the willingness of contributors to take an interest in the source code of the entire project. In order to remove the other two limitations, most of the time it will be necessary to create a “fork”.

Another solution consists of an extension system. By allowing users to write their own extension without taking an interest in the core of the basic software, we allow for:

  1. Reducing the amount of source code a new contributor has to take in
  2. Unofficial integration of novelties
  3. No forking or main developer approval required.

Note: it is quite conceivable that the functionalities of an extension can later be officially integrated into the FreshRSS code. Extensions make it easy to propose a proof of concept.

Minz Framework

see Minz documentation

Write an extension for FreshRSS

Here we are! We’ve talked about the most useful features of Minz and how to run FreshRSS correctly and it’s about time to address the extensions themselves.

An extension allows you to easily add functionality to FreshRSS without having to touch the core of the project directly.

Make it work in Docker

When working on an extension, it’s easier to see it working directly in its environment. With Docker, you can leverage the use of the volume option when starting the container. Hopefully, you can use it without Docker-related knowledge by using the Makefile rule:

make start extensions="/full/path/to/extension/1 /full/path/to/extension/2"

Basic files and folders

The first thing to note is that all extensions must be located in the extensions directory, at the base of the FreshRSS tree. An extension is a directory containing a set of mandatory (and optional) files and subdirectories. The convention requires that the main directory name be preceded by an “x” to indicate that it is not an extension included by default in FreshRSS.

The main directory of an extension must contain at least two mandatory files:

Please note that there is a not a required link between the directory name of the extension and the name of the class inside extension.php, but you should follow our best practice: If you want to write a HelloWorld extension, the directory name should be xExtension-HelloWorld and the base class name HelloWorldExtension.

In the file freshrss/extensions/xExtension-HelloWorld/extension.php you need the structure:

final class HelloWorldExtension extends Minz_Extension {
	#[\Override]
	public function init(): void {
		parent::init();

		// your code here
	}
}

There is an example HelloWorld extension that you can download from our GitHub repo.

You may also need additional files or subdirectories depending on your needs:

In addition, it is good to have a LICENSE file indicating the license under which your extension is distributed and a README file giving a detailed description of it.

The metadata.json file

The metadata.json file defines your extension through a number of important elements. It must contain a valid JSON array containing the following entries:

Only the name and entrypoint fields are required.

Choosing between system and user

A user extension can be enabled by some users and not by others (typically for user preferences).

A system extension in comparison is enabled for every account.

Writing your own extension.php

This file is the core of your extension. It must define some key elements to be loaded by the extension system:

  1. The class name must be the entrypoint value defined in the metadata.json file suffixed by Extension (if your entrypoint value is HelloWorld, your class name will be HelloWorldExtension).
  2. The class must extend the Minz_Extension abstract class which defines the core methods and properties of a FreshRSS extension.
  3. The class must define the init method. This method is called only if the extension is loaded. Its purpose is to initialize the extension and its behavior during every page load.

The Minz_Extension abstract class defines a set of methods that can be overridden to fit your needs:

If your extension code is scattered in different classes, you need to load their source before using them. Of course you could include the files manually, but it’s more efficient to load them automatically. To do so, you just need to define the autoload method which will include them when needed. This method will be registered automatically when the extension is enabled.

The Minz_Extension abstract class defines another set of methods that should not be overridden:

Note that if you modify the later set of methods, you might break the extension system. Thus making FreshRSS unusable. So it’s highly recommended to let those unmodified.

The “hooks” system

You can register at the FreshRSS event system in an extensions init() method, to manipulate data when some of the core functions are executed.

final class HelloWorldExtension extends Minz_Extension
{
	#[\Override]
	public function init(): void {
		parent::init();

		$this->registerHook('entry_before_display', [$this, 'renderEntry']);
		$this->registerHook('check_url_before_add', [self::class, 'checkUrl']);
	}

	public function renderEntry(FreshRSS_Entry $entry): FreshRSS_Entry {
		$message = $this->getUserConfigurationValue('message');
		$entry->_content("<h1>{$message}</h1>" . $entry->content());
		return $entry;
	}

	public static function checkUrlBeforeAdd(string $url): string {
		if (str_starts_with($url, 'https://')) {
			return $url;
		}
		return null;
	}
}

The following events are available:

ℹ️ Note: the simplepie_* hooks are only fired for feeds using SimplePie via pull, i.e. normal RSS/Atom feeds. This excludes WebSub (push), and the various HTML or JSON Web scraping methods.

Injecting CDN content

When using the init method, it is possible to inject scripts from CDN using the Minz_View::appendScript directive. FreshRSS will include the script in the page but will not load it since it will be blocked by the default content security policy (CSP). To amend the existing CSP, you need to define the extension CSP policies:

// in the extension.php file
protected array $csp_policies = [
	'default-src' => 'example.org',
];

This will only amend the extension CSP to FreshRSS CSP.

Writing your own configure.phtml

When you want to support user configurations for your extension or simply display some information, you have to create the configure.phtml file.

TODO