Files
librenms-librenms/app/Plugins/PluginManager.php
Kevin Zink 98ed6bb9dc New plugin system based on Laravel Package Development (#12998)
* use Blade view and Eloquent models for plugins

* move views

* fix style

* fix style

* revert mistake

* Update Plugin.php

delete test property "name"

* rename plugin function to settings

* last but not least - rename in Test.php

* Rename Test to Example

* fix typo

* fix style

* fix style

* fix style

* fix style - I hate tabs...

* Extract view calls

* fix method calls and style

* Move Models the the abstract class

* fix style

* Convert to traits

* Change the Example description

* Fix style

* Fix style

* Fix style

* Convert plugin function to Model static methods and delete .inc.php

* fix style

* fix style

* Use scope

* final methods blows up legacy code

* Config > \LibreNMS\Config

* convert the static string to a static method

* Correct placement in the page

* fix tabs

* fix style

* Rename from tait to hook

to make it easier to understand and be complient

* rename file

* Typo

* Started to change the docu

* change to a more usefully Device_Overview example

* and activate of course

* PluginManager

* fix .gitignore

* only php files in the root folder

* corrected .gitignore with all files :)

* Rename the Hooks and ExampleClass for better readability

* Fix style

* Fix style

* Exception handling (especially if DB is not present)

* Fix style and update schema

* fix indentation

* actually correct indent

* fix migration collation check include utf8mb4_bin

* stop phpstan whining

* A view lines documentation

* add typeHints

* Allow return null on handle

* lint

* fix return types

* fix logic of column collation check

* Fix MenuEntryHook

* switch to longtext instead of json type for now :D

* try phpstan on PHP 7.3

* set phpstan target version to 7.3

* all the typehints

* optional

* more

* Use namespace to prevent view collisions
disambiguate plugin and hook
no magic guessing of names in PluginManager, bad assumptions
remove unused plugins from the DB

* cleanup plugin menu

* cleanup on shutdown and ignore but log query error on cleanup

* instanceof must be called against an instance

* Allow multiple hooks per plugin

* Port plugin ui code to Laravel

* page instead of settings for v1 plugins

* actually working settings pages
a little url cleanup plugin/admin -> plugin/settings

* fix style

* Add page hook

* PHPstan

* Try to fix Illuminate\Http\RedirectResponse

* typehint

* Rewrite the doc

* Fix style

Co-authored-by: PipoCanaja <38363551+PipoCanaja@users.noreply.github.com>
Co-authored-by: Tony Murray <murraytony@gmail.com>
2021-10-19 06:53:28 -05:00

257 lines
7.7 KiB
PHP

<?php
/*
* PluginManager.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2021 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace App\Plugins;
use App\Exceptions\PluginException;
use App\Models\Plugin;
use Exception;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Log;
class PluginManager
{
/** @var Collection */
private $hooks;
/** @var Collection */
private $plugins;
/** @var array */
private $validPlugins = [];
public function __construct()
{
$this->hooks = new Collection;
}
/**
* Publish plugin hook, this is the main way to hook into different parts of LibreNMS.
* plugin_name should be unique. For internal (user) plugins in the app/Plugins directory, the directory name will be used.
* Hook type will be the full class name of the hook from app/Plugins/Hooks.
*
* @param string $pluginName
* @param string $hookType
* @param string $implementationClass
* @return bool
*/
public function publishHook(string $pluginName, string $hookType, string $implementationClass): bool
{
try {
$instance = new $implementationClass;
$this->validPlugins[$pluginName] = 1;
if ($instance instanceof $hookType && $this->pluginEnabled($pluginName)) {
if (! $this->hooks->has($hookType)) {
$this->hooks->put($hookType, new Collection);
}
$this->hooks->get($hookType)->push([
'plugin_name' => $pluginName,
'instance' => $instance,
]);
return true;
}
} catch (Exception $e) {
Log::error("Error when loading hook $implementationClass of type $hookType for $pluginName: " . $e->getMessage());
}
return false;
}
/**
* Check if there are any valid hooks
*
* @param string $hookType
* @param array $args
* @param string|null $plugin only for this plugin if set
* @return bool
*/
public function hasHooks(string $hookType, array $args = [], ?string $plugin = null): bool
{
return $this->hooksFor($hookType, $args, $plugin)->isNotEmpty();
}
/**
* Coll all hooks for the given hook type.
* args will be available for injection into the handle method to pass data through
* settings is automatically injected
*
* @param string $hookType
* @param array $args
* @param string|null $plugin only for this plugin if set
* @return \Illuminate\Support\Collection
*/
public function call(string $hookType, array $args = [], ?string $plugin = null): Collection
{
try {
return $this->hooksFor($hookType, $args, $plugin)
->map(function ($hook) use ($args) {
return app()->call([$hook['instance'], 'handle'], $this->fillArgs($args, $hook['plugin_name']));
});
} catch (Exception $e) {
Log::error("Error calling hook $hookType: " . $e->getMessage());
return new Collection;
}
}
/**
* Get the settings stored in the database for a plugin.
* One plugin shares the settings across all hooks
*
* @param string $pluginName
* @return array
*/
public function getSettings(string $pluginName): array
{
return (array) $this->getPlugin($pluginName)->settings;
}
/**
* Save settings array to the database for the given plugin
*
* @param string $pluginName
* @param array $settings
* @return bool
*/
public function setSettings(string $pluginName, array $settings): bool
{
$plugin = $this->getPlugin($pluginName);
$plugin->settings = $settings;
return $plugin->save();
}
/**
* Check if plugin exists.
* Does not create a DB entry if it does not exist.
*
* @param string $pluginName
* @return bool
*/
public function pluginExists(string $pluginName): bool
{
return $this->getPlugins()->has($pluginName);
}
/**
* Check if plugin of the given name is enabled.
* Creates DB entry if one does not exist yet.
*
* @param string $pluginName
* @return bool
*/
public function pluginEnabled(string $pluginName): bool
{
return $this->getPlugin($pluginName)->plugin_active;
}
/**
* Remove plugins that do not have any registered hooks.
*/
public function cleanupPlugins(): void
{
try {
$valid = array_keys($this->validPlugins);
Plugin::versionTwo()->whereNotIn('plugin_name', $valid)->get()->each->delete();
} catch (QueryException $qe) {
Log::error('Failed to clean up plugins: ' . $qe->getMessage());
}
}
protected function getPlugin(string $name): ?Plugin
{
$plugin = $this->getPlugins()->get($name);
if (! $plugin) {
try {
$plugin = Plugin::create([
'plugin_name' => $name,
'plugin_active' => 1,
'version' => 2,
]);
$this->getPlugins()->put($name, $plugin);
} catch (QueryException $e) {
// DB not migrated/connected
}
}
return $plugin;
}
protected function getPlugins(): Collection
{
if ($this->plugins === null) {
try {
$this->plugins = Plugin::versionTwo()->get()->keyBy('plugin_name');
} catch (QueryException $e) {
// DB not migrated/connected
$this->plugins = new Collection;
}
}
return $this->plugins;
}
/**
* @param string $hookType
* @param array $args
* @param string|null $onlyPlugin
* @return \Illuminate\Support\Collection
*/
protected function hooksFor(string $hookType, array $args, ?string $onlyPlugin): Collection
{
if (! $this->hooks->has($hookType)) {
return new Collection;
}
return $this->hooks->get($hookType)
->when($onlyPlugin, function (Collection $hooks, $only) {
return $hooks->where('plugin_name', $only);
})
->filter(function ($hook) use ($args) {
return app()->call([$hook['instance'], 'authorize'], $this->fillArgs($args, $hook['plugin_name']));
});
}
protected function fillArgs(array $args, string $pluginName): array
{
if (isset($args['settings'])) {
throw new PluginException('You cannot inject "settings", this is a reserved name');
}
if (isset($args['pluginName'])) {
throw new PluginException('You cannot inject "pluginName", this is a reserved name');
}
return array_merge($args, [
'pluginName' => $pluginName,
'settings' => $this->getSettings($pluginName),
]);
}
}