diff --git a/app/Plugins/ExamplePlugin/DeviceOverview.php b/app/Plugins/ExamplePlugin/DeviceOverview.php index 28c3f11179..92d8f2dd6d 100644 --- a/app/Plugins/ExamplePlugin/DeviceOverview.php +++ b/app/Plugins/ExamplePlugin/DeviceOverview.php @@ -29,4 +29,31 @@ use App\Plugins\Hooks\DeviceOverviewHook; class DeviceOverview extends DeviceOverviewHook { + // point to the view for your plugin's settings + // this is the default name so you can create the blade file as in this plugin + // by ommitting the variable, or point to another one + +// public string $view = 'resources.views.device-overview'; + + public function authorize(\App\Models\User $user, \App\Models\Device $device): bool + { + // In this example, we check if the user has a custom role/permission and if it is member of any device groups +// return $user->can('view-extra-port-info') && $device->has('groups'); + + return true; + } + + // override the data function to add additional data to be accessed in the view + // title is a required attribute and will be shown above your returned html from your blade file + // inside the blade, all variables will be named based on the key in the returned array + public function data(\App\Models\Device $device): array + { + // here we pass a title string, url to notes, and the device to the blade view for display + + return [ + 'title' => 'Example Plugin: Device Notes', + 'device' => $device, + 'url' => url('device/' . $device->device_id . '/notes'), + ]; + } } diff --git a/app/Plugins/ExamplePlugin/Menu.php b/app/Plugins/ExamplePlugin/Menu.php index b45ab75be4..7a2101f7ad 100644 --- a/app/Plugins/ExamplePlugin/Menu.php +++ b/app/Plugins/ExamplePlugin/Menu.php @@ -4,6 +4,33 @@ namespace App\Plugins\ExamplePlugin; use App\Plugins\Hooks\MenuEntryHook; +// this will create a menu entry in the plugin menu +// it should generally just be a class Menu extends MenuEntryHook { + // point to the view for your plugin's settings + // this is the default name so you can create the blade file as in this plugin + // by ommitting the variable, or point to another one + +// public string $view = 'resources.views.menu'; + + // this will determine if the menu entry should be shown to the user + public function authorize(\App\Models\User $user, array $settings = []): bool + { + // menu entry shown if users has the global-read role and there is a setting that has > one entries in it +// return $user->can('global-read') && isset($settings['some_data']) && count($settings['some_data']) > 0; + + return true; // allow every logged in user + } + + // override the data function to add additional data to be accessed in the view + // inside the blade, all variables will be named based on the key in the returned array + public function data(array $settings = []): array + { + // inject settings and count how many we have so we can display it in the menu + + return [ + 'count' => count($settings), + ]; + } } diff --git a/app/Plugins/ExamplePlugin/Page.php b/app/Plugins/ExamplePlugin/Page.php index c356e145d3..57b06fb956 100644 --- a/app/Plugins/ExamplePlugin/Page.php +++ b/app/Plugins/ExamplePlugin/Page.php @@ -27,6 +27,40 @@ namespace App\Plugins\ExamplePlugin; use App\Plugins\Hooks\PageHook; +// this page will be shown when the user clicks on the plugin from the plugins menu. +// This allows you to output a full screen of whatever you want to the user class Page extends PageHook { + // point to the view for your plugin's settings + // this is the default name so you can create the blade file as in this plugin + // by ommitting the variable, or point to another one + +// public string $view = 'resources.views.page'; + + // The authorize method will determine if the user has access to this page. + // if you want all users to be able to access this page simple return true + public function authorize(\App\Models\User $user): bool + { + // you can check user's roles like this: +// return $user->can('admin'); + + // or use whatever you like +// return \Carbon\Carbon::now()->dayOfWeek == Carbon::THURSDAY; // only allowed access on Thursdays! + + return true; // allow every logged in user to access + } + + // override the data function to add additional data to be accessed in the view + // default just passes the stored data through + // inside the blade, all variables will be named based on the key in the returned array + public function data(): array + { + // run any calculations here + $username = auth()->user()->username; + + return [ + 'something' => 'this is a variable and can be accessed with {{ $something }}', + 'hello' => 'Hello: ' . $username, + ]; + } } diff --git a/app/Plugins/ExamplePlugin/PortTab.php b/app/Plugins/ExamplePlugin/PortTab.php index ed47d71b4d..aa91728f60 100644 --- a/app/Plugins/ExamplePlugin/PortTab.php +++ b/app/Plugins/ExamplePlugin/PortTab.php @@ -4,6 +4,28 @@ namespace App\Plugins\ExamplePlugin; use App\Plugins\Hooks\PortTabHook; +// this will insert a tab into every port view class PortTab extends PortTabHook { + // point to the view for your plugin's port plugin + // this is the default name so you can create the blade file as in this plugin + // by ommitting the variable, or point to another one + +// public string $view = 'resources.views.port-tab'; + + // override the data function to add additional data to be accessed in the view + // title is a required attribute and will be shown above your returned html from your blade file + // inside the blade, all variables will be named based on the key in the returned array + public function data(\App\Models\Port $port): array + { + // run any calculations here + $total_delta = $port->ifOutOctets_delta + $port->ifInOctets_delta; // nonsense calculation :) + + return [ + 'title' => 'Example Plugin', + 'port' => $port, + 'something' => 'this is a variable and can be accessed with {{ $something }}', + 'total' => $total_delta, + ]; + } } diff --git a/app/Plugins/ExamplePlugin/Settings.php b/app/Plugins/ExamplePlugin/Settings.php index 0ad995e75a..04b6ffbfe8 100644 --- a/app/Plugins/ExamplePlugin/Settings.php +++ b/app/Plugins/ExamplePlugin/Settings.php @@ -27,6 +27,29 @@ namespace App\Plugins\ExamplePlugin; use App\Plugins\Hooks\SettingsHook; +// In the plugins admin page, there will be a settings button if you implement this hook +// To save settings in your settings page, you should have a form that returns all variables +// you want to save in the database. class Settings extends SettingsHook { + // point to the view for your plugin's settings + // this is the default name so you can create the blade file as in this plugin + // by ommitting the variable, or point to another one + +// public string $view = 'resources.views.settings'; + + // override the data function to add additional data to be accessed in the view + // default just passes the stored data through + // inside the blade, all variables will be named based on the key in the returned array + public function data(array $settings = []): array + { + // run any calculations here + $total = array_sum([1, 2, 3, 4]); + + return [ + 'settings' => $settings, // this is an array of all the settings stored in the database + 'something' => 'this is a variable and can be accessed with {{ $something }}', + 'total' => $total, + ]; + } } diff --git a/app/Plugins/ExamplePlugin/resources/views/device-overview.blade.php b/app/Plugins/ExamplePlugin/resources/views/device-overview.blade.php index af279c6344..977bec1d4e 100644 --- a/app/Plugins/ExamplePlugin/resources/views/device-overview.blade.php +++ b/app/Plugins/ExamplePlugin/resources/views/device-overview.blade.php @@ -2,11 +2,13 @@
- {{ $title }} [EDIT] + {{ $title }} [EDIT]
+ {{-- This is a comment. Below we output the markdown output unescaped because we want the raw html + to be output to the page. Be careful with unescaped output as it can lead to security issues. --}} {!! Str::markdown($device->notes ?? '') !!}
diff --git a/app/Plugins/ExamplePlugin/resources/views/menu.blade.php b/app/Plugins/ExamplePlugin/resources/views/menu.blade.php index 57d47b922c..c52c5daeb3 100644 --- a/app/Plugins/ExamplePlugin/resources/views/menu.blade.php +++ b/app/Plugins/ExamplePlugin/resources/views/menu.blade.php @@ -1 +1 @@ - Example Menu + Example Menu @if($count)({{ $count }})@endif diff --git a/app/Plugins/Hooks/DeviceOverviewHook.php b/app/Plugins/Hooks/DeviceOverviewHook.php index 941519eeb6..44a255fa64 100644 --- a/app/Plugins/Hooks/DeviceOverviewHook.php +++ b/app/Plugins/Hooks/DeviceOverviewHook.php @@ -27,14 +27,14 @@ namespace App\Plugins\Hooks; use App\Models\Device; use App\Models\User; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Str; abstract class DeviceOverviewHook { - /** @var string */ - public $view = 'resources.views.device-overview'; + public string $view = 'resources.views.device-overview'; - public function authorize(User $user, Device $device, array $settings): bool + public function authorize(User $user, Device $device): bool { return true; } @@ -47,8 +47,11 @@ abstract class DeviceOverviewHook ]; } - final public function handle(string $pluginName, Device $device): \Illuminate\Contracts\View\View + final public function handle(string $pluginName, array $settings, Device $device, Application $app): \Illuminate\Contracts\View\View { - return view(Str::start($this->view, "$pluginName::"), $this->data($device)); + return view(Str::start($this->view, "$pluginName::"), $app->call([$this, 'data'], [ + 'device' => $device, + 'settings' => $settings, + ])); } } diff --git a/app/Plugins/Hooks/MenuEntryHook.php b/app/Plugins/Hooks/MenuEntryHook.php index 3390ce09db..fd164526c9 100644 --- a/app/Plugins/Hooks/MenuEntryHook.php +++ b/app/Plugins/Hooks/MenuEntryHook.php @@ -26,14 +26,14 @@ namespace App\Plugins\Hooks; use App\Models\User; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Str; abstract class MenuEntryHook { - /** @var string */ - public $view = 'resources.views.menu'; + public string $view = 'resources.views.menu'; - public function authorize(User $user, array $settings): bool + public function authorize(User $user): bool { return true; } @@ -43,8 +43,10 @@ abstract class MenuEntryHook return []; } - final public function handle(string $pluginName): array + final public function handle(string $pluginName, array $settings, Application $app): array { - return [Str::start($this->view, "$pluginName::"), $this->data()]; + return [Str::start($this->view, "$pluginName::"), $app->call([$this, 'data'], [ + 'settings' => $settings, + ])]; } } diff --git a/app/Plugins/Hooks/PageHook.php b/app/Plugins/Hooks/PageHook.php index d0152cf47e..a3e8f99753 100644 --- a/app/Plugins/Hooks/PageHook.php +++ b/app/Plugins/Hooks/PageHook.php @@ -26,12 +26,12 @@ namespace App\Plugins\Hooks; use App\Models\User; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Str; abstract class PageHook { - /** @var string */ - public $view = 'resources.views.page'; + public string $view = 'resources.views.page'; public function authorize(User $user): bool { @@ -40,14 +40,15 @@ abstract class PageHook public function data(): array { - return [ - ]; + return []; } - final public function handle(string $pluginName): array + final public function handle(string $pluginName, array $settings, Application $app): array { return array_merge([ 'settings_view' => Str::start($this->view, "$pluginName::"), - ], $this->data()); + ], $app->call([$this, 'data'], [ + 'settings' => $settings, + ])); } } diff --git a/app/Plugins/Hooks/PortTabHook.php b/app/Plugins/Hooks/PortTabHook.php index 34109c22ec..af7b6541d0 100644 --- a/app/Plugins/Hooks/PortTabHook.php +++ b/app/Plugins/Hooks/PortTabHook.php @@ -28,6 +28,7 @@ namespace App\Plugins\Hooks; use App\Models\Port; use App\Models\User; use App\Plugins\Hook; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Str; abstract class PortTabHook implements Hook @@ -35,7 +36,7 @@ abstract class PortTabHook implements Hook /** @var string */ public $view = 'resources.views.port-tab'; - public function authorize(User $user, Port $port, array $settings): bool + public function authorize(User $user, Port $port): bool { return true; } @@ -48,8 +49,11 @@ abstract class PortTabHook implements Hook ]; } - final public function handle(string $pluginName, Port $port): \Illuminate\Contracts\View\View + final public function handle(string $pluginName, Port $port, array $settings, Application $app): \Illuminate\Contracts\View\View { - return view(Str::start($this->view, "$pluginName::"), $this->data($port)); + return view(Str::start($this->view, "$pluginName::"), $app->call([$this, 'data'], [ + 'port' => $port, + 'settings' => $settings, + ])); } } diff --git a/app/Plugins/Hooks/SettingsHook.php b/app/Plugins/Hooks/SettingsHook.php index 2f0fe9573e..aa0c0c8196 100644 --- a/app/Plugins/Hooks/SettingsHook.php +++ b/app/Plugins/Hooks/SettingsHook.php @@ -26,14 +26,14 @@ namespace App\Plugins\Hooks; use App\Models\User; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Str; abstract class SettingsHook { - /** @var string */ - public $view = 'resources.views.settings'; + public string $view = 'resources.views.settings'; - public function authorize(User $user, array $settings): bool + public function authorize(User $user): bool { return true; } @@ -45,10 +45,12 @@ abstract class SettingsHook ]; } - final public function handle(string $pluginName, array $settings): array + final public function handle(string $pluginName, array $settings, Application $app): array { return array_merge([ 'settings_view' => Str::start($this->view, "$pluginName::"), - ], $this->data($settings)); + ], $this->data($app->call([$this, 'data'], [ + 'settings' => $settings, + ]))); } } diff --git a/app/Plugins/PluginManager.php b/app/Plugins/PluginManager.php index 6969cea122..a9280df125 100644 --- a/app/Plugins/PluginManager.php +++ b/app/Plugins/PluginManager.php @@ -114,7 +114,11 @@ class PluginManager return app()->call([$hook['instance'], 'handle'], $this->fillArgs($args, $hook['plugin_name'])); } catch (Exception|\Error $e) { $name = $hook['plugin_name']; - Log::error("Error calling hook $hookType for $name: " . $e->getMessage()); + Log::error("Error calling hook $hookType for $name: " . $e->getMessage() . PHP_EOL . $e->getTraceAsString()); + + if (\LibreNMS\Config::get('plugins.show_errors')) { + throw $e; + } Notifications::create("Plugin $name disabled", "$name caused an error and was disabled, please check with the plugin creator to fix the error. The error can be found in logs/librenms.log", 'plugins', 2); Plugin::where('plugin_name', $name)->update(['plugin_active' => 0]); @@ -174,7 +178,7 @@ class PluginManager */ public function pluginEnabled(string $pluginName): bool { - return (bool) optional($this->getPlugin($pluginName))->plugin_active; + return (bool) $this->getPlugin($pluginName)?->plugin_active; } /** diff --git a/doc/Extensions/Plugin-System.md b/doc/Extensions/Plugin-System.md index ec4b8586e0..b861be0a82 100644 --- a/doc/Extensions/Plugin-System.md +++ b/doc/Extensions/Plugin-System.md @@ -14,6 +14,8 @@ LibreNMS. An example plugin is included in the LibreNMS distribution. Plugins in version 2 need to be installed into app/Plugins +>Note: Plugins are disabled when the have an error, to show errors instead set plugins.show_errors + The structure of a plugin is follows: ``` @@ -78,20 +80,63 @@ class in 'app/Plugins/PluginName' and overload the hook methods. - settings.blade.php :: If you need your own settings and variables, you can have a look in the ExamplePlugin. +### PHP Hooks customization -If you want to change the behavior, you can customize the hooks methods. Just as an example, you could imagine that the device-overview.blade.php should only be displayed when the device is in maintanence mode. Of course the method is more for a permission concept but it gives you the idea. +PHP code should run inside your hooks method and not your blade view. +The built in hooks support authorize and data methods. -``` -abstract class DeviceOverviewHook +These methods are called with [Dependency Injection](https://laravel.com/docs/container#method-invocation-and-injection) +Hooks with relevant database models will include them in these calls. +Additionally, the settings argument may be included to inject the plugin settings into the method. + +#### Data + +You can overrid the data method to supply data to your view. You should also do any processing here. +You can do things like access the database or configuration settings and more. + +In the data method we are injecting settings here to count how many we have for display in the menu entry blade view. +Note that you must specify a default value (`= []` here) for any arguments that don't exist on the parent method. + +```php +class Menu extends MenuEntryHook { - ... - public function authorize(User $user, Device $device, array $settings): bool + public function data(array $settings = []): array { - return $device->isUnderMaintenance(); + return [ + 'count' => count($settings), + ]; } - ... +} ``` +#### Authorize + +By default hooks are always shown, but you may control when the user is authorized to view the hook content. + +As an example, you could imagine that the device-overview.blade.php should only be displayed when the +device is in maintanence mode and the current user has the admin role. + +```php +class DeviceOverview extends DeviceOverviewHook +{ + public function authorize(User $user, Device $device): bool + { + return $user->can('admin') && $device->isUnderMaintenance(); + } +} +``` + + +### Full plugin + +You may create a full plugin that can publish multiple routes, views, database migrations and more. +Create a package according to the Laravel documentation you may call any of the supported hooks to tie into LibreNMS. + +https://laravel.com/docs/packages + +> This is untested, please come to discord and share any expriences and update this documentation! + + ## Version 1 Plugin System structure (legacy verion) Plugins need to be installed into html/plugins diff --git a/misc/config_definitions.json b/misc/config_definitions.json index 4947909205..fa8e7b48ea 100644 --- a/misc/config_definitions.json +++ b/misc/config_definitions.json @@ -4712,6 +4712,10 @@ "plugin_dir": { "type": "directory" }, + "plugins.show_errors": { + "type": "boolean", + "default": false + }, "poller_modules.unix-agent": { "order": 420, "group": "poller", diff --git a/resources/views/plugins/settings.blade.php b/resources/views/plugins/settings.blade.php index 0b7a83b0b1..b2f895a568 100644 --- a/resources/views/plugins/settings.blade.php +++ b/resources/views/plugins/settings.blade.php @@ -2,4 +2,6 @@ @section('title', $title) -@include($settings_view, $settings) +@section('content') + @include($settings_view, $settings) +@endsection