]> 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 3ea55728cc4c9b6af22d88da6f92e533b270069c..7fc0cb047db70d1ca33d8f72529b57ff598b579e 100644 (file)
@@ -4,6 +4,7 @@ namespace Shaarli\Plugin;
 
 use Shaarli\Config\ConfigManager;
 use Shaarli\Plugin\Exception\PluginFileNotFoundException;
+use Shaarli\Plugin\Exception\PluginInvalidRouteException;
 
 /**
  * Class PluginManager
@@ -26,6 +27,14 @@ class PluginManager
      */
     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.
      */
@@ -86,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]));
             }
         }
     }
@@ -166,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;
     }
 
@@ -237,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.
      *
@@ -246,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;
+    }
 }