]> git.immae.eu Git - github/shaarli/Shaarli.git/blobdiff - application/plugin/PluginManager.php
Plugin system: allow plugins to provide custom routes
[github/shaarli/Shaarli.git] / application / plugin / PluginManager.php
index 1b2197c9d8d0aa1af56d44842ed18c3fa4f619de..7fc0cb047db70d1ca33d8f72529b57ff598b579e 100644 (file)
@@ -1,8 +1,10 @@
 <?php
+
 namespace Shaarli\Plugin;
 
 use Shaarli\Config\ConfigManager;
 use Shaarli\Plugin\Exception\PluginFileNotFoundException;
+use Shaarli\Plugin\Exception\PluginInvalidRouteException;
 
 /**
  * Class PluginManager
@@ -23,7 +25,15 @@ class PluginManager
      *
      * @var array $loadedPlugins
      */
-    private $loadedPlugins = array();
+    private $loadedPlugins = [];
+
+    /** @var array List of registered routes. Contains keys:
+     *               - `method`: HTTP method, GET/POST/PUT/PATCH/DELETE
+     *               - `route` (path): without prefix, e.g. `/up/{variable}`
+     *                 It will be later prefixed by `/plugin/<plugin name>/`.
+     *               - `callable` string, function name or FQN class's method, e.g. `demo_plugin_custom_controller`.
+     */
+    protected $registeredRoutes = [];
 
     /**
      * @var ConfigManager Configuration Manager instance.
@@ -57,7 +67,7 @@ class PluginManager
     public function __construct(&$conf)
     {
         $this->conf = $conf;
-        $this->errors = array();
+        $this->errors = [];
     }
 
     /**
@@ -85,6 +95,9 @@ class PluginManager
                 $this->loadPlugin($dirs[$index], $plugin);
             } catch (PluginFileNotFoundException $e) {
                 error_log($e->getMessage());
+            } catch (\Throwable $e) {
+                $error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
+                $this->errors = array_unique(array_merge($this->errors, [$error]));
             }
         }
     }
@@ -98,12 +111,13 @@ class PluginManager
      *
      * @return void
      */
-    public function executeHooks($hook, &$data, $params = array())
+    public function executeHooks($hook, &$data, $params = [])
     {
         $metadataParameters = [
             'target' => '_PAGE_',
             'loggedin' => '_LOGGEDIN_',
             'basePath' => '_BASE_PATH_',
+            'rootPath' => '_ROOT_PATH_',
             'bookmarkService' => '_BOOKMARK_SERVICE_',
         ];
 
@@ -164,6 +178,22 @@ class PluginManager
             }
         }
 
+        $registerRouteFunction = $pluginName . '_register_routes';
+        $routes = null;
+        if (function_exists($registerRouteFunction)) {
+            $routes = call_user_func($registerRouteFunction);
+        }
+
+        if ($routes !== null) {
+            foreach ($routes as $route) {
+                if (static::validateRouteRegistration($route)) {
+                    $this->registeredRoutes[$pluginName][] = $route;
+                } else {
+                    throw new PluginInvalidRouteException($pluginName);
+                }
+            }
+        }
+
         $this->loadedPlugins[] = $pluginName;
     }
 
@@ -195,7 +225,7 @@ class PluginManager
      */
     public function getPluginsMeta()
     {
-        $metaData = array();
+        $metaData = [];
         $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
 
         // Browse all plugin directories.
@@ -216,9 +246,9 @@ class PluginManager
             if (isset($metaData[$plugin]['parameters'])) {
                 $params = explode(';', $metaData[$plugin]['parameters']);
             } else {
-                $params = array();
+                $params = [];
             }
-            $metaData[$plugin]['parameters'] = array();
+            $metaData[$plugin]['parameters'] = [];
             foreach ($params as $param) {
                 if (empty($param)) {
                     continue;
@@ -235,6 +265,14 @@ class PluginManager
         return $metaData;
     }
 
+    /**
+     * @return array List of registered custom routes by plugins.
+     */
+    public function getRegisteredRoutes(): array
+    {
+        return $this->registeredRoutes;
+    }
+
     /**
      * Return the list of encountered errors.
      *
@@ -244,4 +282,32 @@ class PluginManager
     {
         return $this->errors;
     }
+
+    /**
+     * Checks whether provided input is valid to register a new route.
+     * It must contain keys `method`, `route`, `callable` (all strings).
+     *
+     * @param string[] $input
+     *
+     * @return bool
+     */
+    protected static function validateRouteRegistration(array $input): bool
+    {
+        if (
+            !array_key_exists('method', $input)
+            || !in_array(strtoupper($input['method']), ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
+        ) {
+            return false;
+        }
+
+        if (!array_key_exists('route', $input) || !preg_match('#^[a-z\d/\.\-_]+$#', $input['route'])) {
+            return false;
+        }
+
+        if (!array_key_exists('callable', $input)) {
+            return false;
+        }
+
+        return true;
+    }
 }