* Copy CustomCSS and CustomJS Original: FreshRSS/Extensions@9f21984 * Rename CustomCSS -> UserCSS * Rename CustomJS -> UserJS * Change metadata The name is used for the directory where the configuration is stored and should not contain spaces. Since the name was changed, I reset the version number and changed to semantic versioning. * Change data directory Changed the location of the configuration file to the user data directory, because it is not `static`. That way, the user's configurations are gathered in the user directory, which makes it easier to backup them. * Edit documentations Remove procedures to install the extension because it is no longer necessary. * Fix wrong variables in the configuration page Remove permission error indication because the storage location is now in the user data directory managed by the application. * Remove the `xExtension-` prefix for core extensions * Set version to 1.0.0 for UserCSS, UserJS * Refactoring * Remove unused variables * Remove version 0.0.1 in Changelog Version 0.0.1 will not be merged, so only version 1.0.0 will remain. * public getFileUrl * Revert more protected * Use entrypoint for extension user path instead of name * Add space to extension name * Add `#[\Override]` * Add explains of User CSS and User JS to docs * Remove README of User CSS and User JS * Add migration code for extension user path --------- Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
12 KiB
Écriture d’extensions pour FreshRSS
Présentation de FreshRSS
- FreshRSS est un agrégateur de flux RSS / Atom écrit en PHP depuis octobre
- 2012. Le site officiel est situé à l’adresse
- freshrss.org et son dépot Git est hébergé par GitHub
- github.com/FreshRSS/FreshRSS.
Problème à résoudre
FreshRSS est limité dans ses possibilités techniques par différents facteurs :
- La disponibilité des développeurs principaux ;
- La volonté d’intégrer certains changements ;
- Le niveau de « hack » nécessaire pour intégrer des fonctionnalités à la marge.
Si la première limitation peut, en théorie, être levée par la participation de nouveaux contributeurs au projet, elle est en réalité conditionnée par la volonté des contributeurs à s’intéresser au code source du projet en entier. Afin de lever les deux autres limitations quant à elles, il faudra la plupart du temps passer par un « à-coté » souvent synonyme de « fork ».
Une autre solution consiste à passer par un système d’extensions. En permettant à des utilisateurs d’écrire leur propre extension sans avoir à s’intéresser au cœur même du logiciel de base, on permet :
- De réduire la quantité de code source à assimiler pour un nouveau contributeur ;
- De permettre d’intégrer des nouveautés de façon non-officielles ;
- De se passer des développeurs principaux pour d’éventuelles améliorations sans passer par la case « fork ».
Note : il est tout à fait imaginable que les fonctionnalités d’une extension puissent par la suite être intégrées dans le code initial de FreshRSS de façon officielle. Cela permet de proposer un « proof of concept » assez facilement.
Minz Framework
Écrire une extension pour FreshRSS
Nous y voilà ! Nous avons abordé les fonctionnalités les plus utiles de Minz et qui permettent de faire tourner FreshRSS correctement et il est plus que temps d’aborder les extensions en elles-même.
Une extension permet donc d’ajouter des fonctionnalités facilement à FreshRSS sans avoir à toucher au cœur du projet directement.
Travailler dans Docker
Quand on travaille sur une extension, c’est toujours plus facile de la travailler directement dans son environnement. Avec Docker, on peut exploiter l’option volume quand on démarre le conteneur. Heureusement, on peut l’utiliser sans avoir de connaissances particulières de Docker en utilisant la règle du Makefile :
make start extensions="/chemin/complet/de/l/extension/1 /chemin/complet/de/l/extension/2"
Les fichiers et répertoires de base
La première chose à noter est que toutes les extensions doivent se
situer dans le répertoire extensions, à la base de l’arborescence de
FreshRSS. Une extension est un répertoire contenant un ensemble de fichiers
et sous-répertoires obligatoires ou facultatifs. La convention veut que l’on
précède le nom du répertoire principal par un « x » pour indiquer qu’il ne
s’agit pas d’une extension incluse par défaut dans FreshRSS.
Le répertoire principal d’une extension doit comporter au moins deux fichiers obligatoire :
- Un fichier
metadata.jsonqui contient une description de l’extension. Ce fichier est écrit en JSON ; - Un fichier
extension.phpcontenant le point d’entrée de l’extension.
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() {
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:
configure.phtmlest le fichier contenant le formulaire pour paramétrer votre extension- A
static/directory containing CSS and JavaScript files that you will need for your extension (note that if you need to write a lot of CSS it may be more interesting to write a complete theme) - A
Controllersdirectory containing additional controllers - An
i18ndirectory containing additional translations layoutandviewsdirectories to define new views or to overwrite the current views
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:
name: le nom de votre extension ;author: votre nom, éventuellement votre adresse mail mais il n’y a pas de format spécifique à adopter ;description: une description de votre extension ;version: le numéro de version actuel de l’extension ;entrypoint: indique le point d’entrée de votre extension. Il doit correspondre au nom de la classe contenue dans le fichierextension.phpsans le suffixeExtension(donc si le point d’entrée estHelloWorld, votre classe s’appelleraHelloWorldExtension) ;type: définit le type de votre extension. Il existe deux types :systemetuser. Nous étudierons cette différence juste après.
Seuls les champs name et entrypoint sont requis.
Choisir entre extension « system » ou « 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 entry point of your extension. It must contain a specific
class to function. As mentioned above, the name of the class must be your
entrypoint suffixed by Extension (HelloWorldExtension for example).
In addition, this class must be inherited from the Minz_Extension class to
benefit from extensions-specific methods.
Your class will benefit from four methods to redefine:
-
install()is called when a user clicks the button to activate your extension. It allows, for example, to update the database of a user in order to make it compatible with the extension. It returnstrueif everything went well or, if not, a string explaining the problem. -
uninstall()is called when a user clicks the button to disable your extension. This will allow you to undo the database changes you potentially made ininstall (). It returnstrueif everything went well or, if not, a string explaining the problem. -
init()is called for every page load if the extension is enabled. It will therefore initialize the behavior of the extension. This is the most important method. -
handleConfigureAction()is called when a user loads the extension management panel. Specifically, it is called when the?c=extension&a=configured&e=name-of-your-extensionURL is loaded. You should also write here the behavior you want when validating the form in yourconfigure.phtmlfile.In addition, you will have a number of methods directly inherited from
Minz_Extensionthat you should not redefine: -
The "getters" first: most are explicit enough not to detail them here -
getName(),getEntrypoint(),getPath()(allows you to retrieve the path to your extension),getAuthor(),getDescription(),getVersion(),getType(). -
getFileUrl($filename, $type)will return the URL to a file in thestaticdirectory. The first parameter is the name of the file (withoutstatic /), the second is the type of file to be used (cssorjs). -
registerController($base_name)will tell Minz to take into account the given controller in the routing system. The controller must be located in yourControllersdirectory, the name of the file must be<base_name>Controller.phpand the name of theFreshExtension_<base_name>_Controllerclass.
À FAIRE
registerViews()registerTranslates()registerHook($hook_name, $hook_function)
Le système « hooks »
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:
check_url_before_add(function($url) -> Url | null): will be executed every time a URL is added. The URL itself will be passed as parameter. This way a website known to have feeds which doesn’t advertise it in the header can still be automatically supported.entry_auto_read(function(FreshRSS_Entry $entry, string $why): void): Appelé lorsqu’une entrée est automatiquement marquée comme lue. Le paramètre why supporte les règles {filter,upon_reception,same_title_in_feed}.entry_auto_unread(function(FreshRSS_Entry $entry, string $why): void): Appelé lorsqu’une entrée est automatiquement marquée comme non-lue. Le paramètre why supporte les règles {updated_article}.entry_before_display(function($entry) -> Entry | null): will be executed every time an entry is rendered. The entry itself (instance of FreshRSS_Entry) will be passed as parameter.entry_before_insert(function($entry) -> Entry | null): will be executed when a feed is refreshed and new entries will be imported into the database. The new entry (instance of FreshRSS_Entry) will be passed as parameter.feed_before_actualize(function($feed) -> Feed | null): will be executed when a feed is updated. The feed (instance of FreshRSS_Feed) will be passed as parameter.feed_before_insert(function($feed) -> Feed | null): will be executed when a new feed is imported into the database. The new feed (instance of FreshRSS_Feed) will be passed as parameter.freshrss_init(function() -> none): will be executed at the end of the initialization of FreshRSS, useful to initialize components or to do additional access checksmenu_admin_entry(function() -> string): add an entry at the end of the "Administration" menu, the returned string must be valid HTML (e.g.<li class="item active"><a href="url">New entry</a></li>)menu_configuration_entry(function() -> string): add an entry at the end of the "Configuration" menu, the returned string must be valid HTML (e.g.<li class="item active"><a href="url">New entry</a></li>)menu_other_entry(function() -> string): add an entry at the end of the header dropdown menu (i.e. after the "About" entry), the returned string must be valid HTML (e.g.<li class="item active"><a href="url">New entry</a></li>)nav_reading_modes(function($reading_modes) -> array | null): TODO add documentationpost_update(function(none) -> none): TODO add documentationsimplepie_before_init(function($simplePie, $feed) -> none): TODO add documentation
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.
À FAIRE