Преглед изворни кода

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
Alexandre Alapetite пре 10 месеци
родитељ
комит
cc35094bb2

+ 1 - 1
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;

+ 2 - 0
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`}.

+ 2 - 0
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

+ 1 - 1
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;

+ 18 - 0
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;
+	}
 }

+ 8 - 6
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>

+ 68 - 0
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();
+}