aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2025-06-03 00:16:17 +0200
committerGravatar GitHub <noreply@github.com> 2025-06-03 00:16:17 +0200
commitcc35094bb261cb3185def89d745317fa756560ee (patch)
tree0d94bbd1dfe1013101dd96b8dbfa975d275ddf7b
parent430d4e898e60ec1892d7191cf6e039aaf693822f (diff)
Add API endpoint for extensions (#7576)
* Add API endpoint for extensions Useful for https://github.com/FreshRSS/FreshRSS/issues/7572 * Support PATH_INFO Now also support being invoked like `/api/misc.php/Extension%20Name/` * More documentation
-rw-r--r--docs/en/admins/10_ServerConfig.md2
-rw-r--r--docs/en/developers/03_Backend/05_Extensions.md2
-rw-r--r--docs/fr/developers/03_Backend/05_Extensions.md2
-rw-r--r--docs/fr/users/01_Installation.md2
-rw-r--r--lib/Minz/ExtensionManager.php18
-rw-r--r--p/api/index.php14
-rw-r--r--p/api/misc.php68
7 files changed, 100 insertions, 8 deletions
diff --git a/docs/en/admins/10_ServerConfig.md b/docs/en/admins/10_ServerConfig.md
index b7f57e61a..07ea147b6 100644
--- a/docs/en/admins/10_ServerConfig.md
+++ b/docs/en/admins/10_ServerConfig.md
@@ -98,7 +98,7 @@ server {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
# By default, the variable PATH_INFO is not set under PHP-FPM
- # But FreshRSS API greader.php need it. If you have a “Bad Request” error, double check this var!
+ # But FreshRSS APIs greader.php and misc.php need it. If you have a “Bad Request” error, double check this var!
# NOTE: the separate $path_info variable is required. For more details, see:
# https://trac.nginx.org/nginx/ticket/321
set $path_info $fastcgi_path_info;
diff --git a/docs/en/developers/03_Backend/05_Extensions.md b/docs/en/developers/03_Backend/05_Extensions.md
index aeb693cba..db9b03d60 100644
--- a/docs/en/developers/03_Backend/05_Extensions.md
+++ b/docs/en/developers/03_Backend/05_Extensions.md
@@ -164,6 +164,8 @@ final class HelloWorldExtension extends Minz_Extension
The following events are available:
+* `api_misc` (`function(): void`): to allow extensions to have own API endpoint
+ on `/api/misc.php/Extension%20Name/` or `/api/misc.php?ext=Extension%20Name`.
* `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`): Triggered when an entry is automatically marked as read. The *why* parameter supports the rules {`filter`, `upon_reception`, `same_title_in_feed`}.
* `entry_auto_unread` (`function(FreshRSS_Entry $entry, string $why): void`): Triggered when an entry is automatically marked as unread. The *why* parameter supports the rules {`updated_article`}.
diff --git a/docs/fr/developers/03_Backend/05_Extensions.md b/docs/fr/developers/03_Backend/05_Extensions.md
index 728d5c843..6996cee58 100644
--- a/docs/fr/developers/03_Backend/05_Extensions.md
+++ b/docs/fr/developers/03_Backend/05_Extensions.md
@@ -218,6 +218,8 @@ final class HelloWorldExtension extends Minz_Extension
The following events are available:
+* `api_misc` (`function(): void`) : permet aux extensions d’avoir leur propre point d’accès API
+ sur `/api/misc.php/Nom%20Extension/` ou `/api/misc.php?ext=Nom%20Extension`.
* `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
diff --git a/docs/fr/users/01_Installation.md b/docs/fr/users/01_Installation.md
index 85b75fea5..8236776fd 100644
--- a/docs/fr/users/01_Installation.md
+++ b/docs/fr/users/01_Installation.md
@@ -118,7 +118,7 @@ server {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
# Par défaut la variable PATH_INFO n’est pas définie sous PHP-FPM
- # or l’API FreshRSS greader.php en a besoin. Si vous avez un “Bad Request”, vérifiez bien cette dernière !
+ # mais les APIs FreshRSS greader.php et misc.php en ont besoin. Si vous avez un “Bad Request”, vérifiez bien cette dernière !
# REMARQUE : l’utilisation de la variable $path_info est requis. Pour plus de détails, voir :
# https://trac.nginx.org/nginx/ticket/321
set $path_info $fastcgi_path_info;
diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php
index fea1f5879..b1d499392 100644
--- a/lib/Minz/ExtensionManager.php
+++ b/lib/Minz/ExtensionManager.php
@@ -22,6 +22,10 @@ final class Minz_ExtensionManager {
* @var array<string,array{'list':array<callable>,'signature':'NoneToNone'|'NoneToString'|'OneToOne'|'PassArguments'}>
*/
private static array $hook_list = [
+ 'api_misc' => [ // function(): void
+ 'list' => [],
+ 'signature' => 'NoneToNone',
+ ],
'check_url_before_add' => [ // function($url) -> Url | null
'list' => [],
'signature' => 'OneToOne',
@@ -429,4 +433,18 @@ final class Minz_ExtensionManager {
call_user_func($function);
}
}
+
+ /**
+ * Call a hook which takes no argument and returns nothing.
+ * Same as callHookVoid but only calls the first extension.
+ *
+ * @param string $hook_name is the hook to call.
+ */
+ public static function callHookUnique(string $hook_name): bool {
+ foreach (self::$hook_list[$hook_name]['list'] ?? [] as $function) {
+ call_user_func($function);
+ return true;
+ }
+ return false;
+ }
}
diff --git a/p/api/index.php b/p/api/index.php
index 66c9e465c..fd9828080 100644
--- a/p/api/index.php
+++ b/p/api/index.php
@@ -28,9 +28,7 @@ echo json_encode([
<h2>Google Reader compatible API</h2>
<dl>
<dt>Your API address:</dt>
-<dd><?php
-echo Minz_Url::display('/api/greader.php', 'html', true);
-?></dd>
+<dd><?= Minz_Url::display('/api/greader.php', 'html', true) ?></dd>
<dt>Google Reader API configuration test:</dt>
<dd id="greaderOutput">?</dd>
</dl>
@@ -38,12 +36,16 @@ echo Minz_Url::display('/api/greader.php', 'html', true);
<h2>Fever compatible API</h2>
<dl>
<dt>Your API address:</dt>
-<dd><?php
-echo Minz_Url::display('/api/fever.php', 'html', true);
-?></dd>
+<dd><?= Minz_Url::display('/api/fever.php', 'html', true) ?></dd>
<dt>Fever API configuration test:</dt>
<dd id="feverOutput">?</dd>
</dl>
+<h2>API for extensions</h2>
+<dl>
+<dt>Your API address:</dt>
+<dd><?= Minz_Url::display('/api/misc.php/Extension%20name/', 'html', true) ?></dd>
+</dl>
+
</body>
</html>
diff --git a/p/api/misc.php b/p/api/misc.php
new file mode 100644
index 000000000..7724caa7a
--- /dev/null
+++ b/p/api/misc.php
@@ -0,0 +1,68 @@
+<?php
+declare(strict_types=1);
+/**
+ * API entry point for FreshRSS extensions on
+ * `/api/misc.php/Extension%20name/` or `/api/misc.php?ext=Extension%20name`
+ */
+
+require(__DIR__ . '/../../constants.php');
+require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
+
+function badRequest(): never {
+ header('HTTP/1.1 400 Bad Request');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Bad Request!');
+}
+
+function serviceUnavailable(): never {
+ header('HTTP/1.1 503 Service Unavailable');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Service Unavailable!');
+}
+
+$extensionName = is_string($_GET['ext'] ?? null) ? $_GET['ext'] : '';
+
+if ($extensionName === '') {
+ $pathInfo = '';
+ if (empty($_SERVER['PATH_INFO']) || !is_string($_SERVER['PATH_INFO'] ?? null)) {
+ if (!empty($_SERVER['ORIG_PATH_INFO']) && is_string($_SERVER['ORIG_PATH_INFO'])) {
+ // Compatibility https://php.net/reserved.variables.server
+ $pathInfo = $_SERVER['ORIG_PATH_INFO'];
+ }
+ } else {
+ $pathInfo = $_SERVER['PATH_INFO'];
+ }
+ $pathInfo = rawurldecode($pathInfo);
+ $pathInfo = preg_replace('%^(/api)?(/misc\.php)?%', '', $pathInfo); //Discard common errors
+ if ($pathInfo !== '' && is_string($pathInfo)) {
+ $pathInfos = explode('/', $pathInfo, limit: 3);
+ if (count($pathInfos) > 1) {
+ $extensionName = $pathInfos[1];
+ }
+ }
+}
+
+if ($extensionName === '') {
+ badRequest();
+}
+
+Minz_Session::init('FreshRSS', volatile: true);
+
+FreshRSS_Context::initSystem();
+if (
+ !FreshRSS_Context::hasSystemConf() ||
+ !FreshRSS_Context::systemConf()->api_enabled ||
+ empty(FreshRSS_Context::systemConf()->extensions_enabled[$extensionName])
+) {
+ serviceUnavailable();
+}
+
+// Only enable the extension that is being called
+FreshRSS_Context::systemConf()->extensions_enabled = [$extensionName => true];
+Minz_ExtensionManager::init();
+
+Minz_Translate::init();
+
+if (!Minz_ExtensionManager::callHookUnique('api_misc')) {
+ serviceUnavailable();
+}