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>
This commit is contained in:
Kevin Zink 2021-10-19 13:53:28 +02:00 committed by GitHub
parent 2d5d7e14ed
commit 98ed6bb9dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1440 additions and 215 deletions

2
.gitignore vendored
View File

@ -19,8 +19,6 @@ Thumbs.db
config.php
html/css/custom/*
html/images/custom/*
html/plugins/*
!html/plugins/Test/
includes/custom/*
junk
nbproject

View File

@ -214,4 +214,40 @@ class Plugins
return count(self::$plugins);
}
public static function scanNew()
{
$countInstalled = 0;
if (file_exists(\LibreNMS\Config::get('plugin_dir'))) {
$plugin_files = array_diff(scandir(\LibreNMS\Config::get('plugin_dir')), ['..', '.']);
$plugin_files = array_diff($plugin_files, Plugin::versionOne()->pluck('plugin_name')->toArray());
foreach ($plugin_files as $name) {
if (is_dir(\LibreNMS\Config::get('plugin_dir') . '/' . $name)
&& is_file(\LibreNMS\Config::get('plugin_dir') . '/' . $name . '/' . $name . '.php')) {
Plugin::create(['plugin_name' => $name, 'plugin_active' => false, 'version' => 1]);
$countInstalled++;
}
}
}
return $countInstalled;
}
public static function scanRemoved()
{
$countRemoved = 0;
if (file_exists(\LibreNMS\Config::get('plugin_dir'))) {
$plugin_files = array_diff(scandir(\LibreNMS\Config::get('plugin_dir')), ['.', '..', '.gitignore']);
$plugins = Plugin::versionOne()->whereNotIn('plugin_name', $plugin_files)->select('plugin_id')->get();
foreach ($plugins as $plugin) {
if ($plugin->delete()) {
$countRemoved++;
}
}
}
return $countRemoved;
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* PluginDoesNotImplementHook.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\Exceptions;
use Throwable;
class PluginDoesNotImplementHookException extends PluginException
{
public function __construct(string $plugin, int $code = 0, Throwable $previous = null)
{
parent::__construct("Plugin ($plugin) does not implement hook.", $code, $previous);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* PluginException.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\Exceptions;
class PluginException extends \Exception
{
}

View File

@ -0,0 +1,36 @@
<?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\Facades;
use Illuminate\Support\Facades\Facade;
class PluginManager extends Facade
{
protected static function getFacadeAccessor()
{
return \App\Plugins\PluginManager::class;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use App\Models\Plugin;
use App\Plugins\PluginManager;
class PluginAdminController extends Controller
{
public function __invoke(PluginManager $manager): \Illuminate\Contracts\View\View
{
// legacy v1 plugins
\LibreNMS\Plugins::scanNew();
\LibreNMS\Plugins::scanRemoved();
// v2 cleanup
$manager->cleanupPlugins();
$plugins = Plugin::get();
return view('plugins.admin', [
'plugins' => $plugins,
]);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers;
use App\Models\Plugin;
use Illuminate\Http\Request;
class PluginLegacyController extends Controller
{
public function redirect(Request $request, ?string $pluginName = null): \Illuminate\Http\RedirectResponse
{
if ($request->get('view') == 'admin') {
return redirect()->route('plugin.admin')->setStatusCode(301);
}
if ($resolved_plugin_name = $request->get('p', $pluginName)) {
return redirect()->route('plugin.legacy', ['plugin' => $resolved_plugin_name])->setStatusCode(301);
}
return redirect()->route('plugin.admin');
}
public function __invoke(?Plugin $plugin): \Illuminate\Contracts\View\View
{
if (! empty($plugin)) {
$plugin_path = \LibreNMS\Config::get('plugin_dir') . '/' . $plugin->plugin_name . '/' . $plugin->plugin_name . '.inc.php';
if (is_file($plugin_path)) {
$init_modules = ['web', 'auth'];
require base_path('/includes/init.php');
chdir(base_path('html'));
ob_start();
include $plugin_path;
$output = ob_get_contents();
ob_end_clean();
chdir(base_path());
}
}
return view('plugins.legacy', [
'title' => $plugin->plugin_name ?? trans('plugins.errors.not_exist'),
'content' => $output ?? 'This plugin is either disabled or not available.',
]);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Models\Plugin;
use App\Plugins\Hooks\PageHook;
use App\Plugins\PluginManager;
use Illuminate\Http\Request;
class PluginPageController extends Controller
{
public function __invoke(PluginManager $manager, Plugin $plugin): \Illuminate\Contracts\View\View
{
if (! $manager->pluginEnabled($plugin->plugin_name)) {
abort(404, trans('plugins.errors.disabled', ['plugin' => $plugin->plugin_name]));
}
$data = array_merge([
// fallbacks to prevent exceptions
'title' => trans('plugins.settings_page', ['plugin' => $plugin->plugin_name]),
'plugin_name' => $plugin->plugin_name,
'plugin_id' => Plugin::where('plugin_name', $plugin->plugin_name)->value('plugin_id'),
'settings_view' => 'plugins.missing',
'settings' => [],
],
(array) $manager->call(PageHook::class, [], $plugin->plugin_name)->first()
);
return view('plugins.settings', $data);
}
public function update(Request $request, Plugin $plugin): \Illuminate\Http\RedirectResponse
{
$validated = $this->validate($request, [
'plugin_active' => 'in:0,1',
'settings' => 'array',
]);
$plugin->fill($validated)->save();
return redirect()->back();
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Models\Plugin;
use App\Plugins\Hooks\SettingsHook;
use App\Plugins\PluginManager;
use Illuminate\Http\Request;
class PluginSettingsController extends Controller
{
public function __invoke(PluginManager $manager, Plugin $plugin): \Illuminate\Contracts\View\View
{
if (! $manager->pluginEnabled($plugin->plugin_name)) {
abort(404, trans('plugins.errors.disabled', ['plugin' => $plugin->plugin_name]));
}
$data = array_merge([
// fallbacks to prevent exceptions
'title' => trans('plugins.settings_page', ['plugin' => $plugin->plugin_name]),
'plugin_name' => $plugin->plugin_name,
'plugin_id' => Plugin::where('plugin_name', $plugin->plugin_name)->value('plugin_id'),
'settings_view' => 'plugins.missing',
'settings' => [],
],
(array) $manager->call(SettingsHook::class, [], $plugin->plugin_name)->first()
);
return view('plugins.settings', $data);
}
public function update(Request $request, Plugin $plugin): \Illuminate\Http\RedirectResponse
{
$validated = $this->validate($request, [
'plugin_active' => 'in:0,1',
'settings' => 'array',
]);
$plugin->fill($validated)->save();
return redirect()->back();
}
}

View File

@ -37,10 +37,13 @@ use App\Models\User;
use App\Models\UserPref;
use App\Models\Vminfo;
use App\Models\WirelessSensor;
use App\Plugins\Hooks\MenuEntryHook;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use LibreNMS\Config;
use LibreNMS\Plugins;
use LibreNMS\Util\ObjectCache;
use PluginManager;
class MenuComposer
{
@ -250,6 +253,12 @@ class MenuComposer
// Search bar
$vars['typeahead_limit'] = Config::get('webui.global_search_result_limit');
// Plugins
$vars['has_v1_plugins'] = Plugins::count() != 0;
$vars['v1_plugin_menu'] = Plugins::call('menu');
$vars['has_v2_plugins'] = PluginManager::hasHooks(MenuEntryHook::class);
$vars['menu_hooks'] = PluginManager::call(MenuEntryHook::class);
$vars['browser_push'] = $user->hasBrowserPushTransport();
$view->with($vars);

View File

@ -31,6 +31,8 @@ class Plugin extends BaseModel
{
public $timestamps = false;
protected $primaryKey = 'plugin_id';
protected $fillable = ['plugin_name', 'plugin_active', 'version', 'settings'];
protected $casts = ['plugin_active' => 'bool', 'settings' => 'array'];
// ---- Query scopes ----
@ -42,4 +44,14 @@ class Plugin extends BaseModel
{
return $query->where('plugin_active', 1);
}
public function scopeVersionOne($query)
{
return $query->where('version', 1);
}
public function scopeVersionTwo($query)
{
return $query->where('version', 2);
}
}

9
app/Plugins/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
# Ignore everything in this directory
*
# Except these
!/.gitignore
!/*.php
!/Hooks/
!/Hooks/**
!/ExamplePlugin/
!/ExamplePlugin/**

View File

@ -0,0 +1,32 @@
<?php
/*
* ExampleSettingsPlugin.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\ExamplePlugin;
use App\Plugins\Hooks\DeviceOverviewHook;
class DeviceOverview extends DeviceOverviewHook
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Plugins\ExamplePlugin;
use App\Plugins\Hooks\MenuEntryHook;
class Menu extends MenuEntryHook
{
}

View File

@ -0,0 +1,32 @@
<?php
/*
* ExampleSettingsPlugin.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\ExamplePlugin;
use App\Plugins\Hooks\PageHook;
class Page extends PageHook
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Plugins\ExamplePlugin;
use App\Plugins\Hooks\PortTabHook;
class PortTab extends PortTabHook
{
}

View File

@ -0,0 +1,32 @@
<?php
/*
* ExampleSettingsPlugin.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\ExamplePlugin;
use App\Plugins\Hooks\SettingsHook;
class Settings extends SettingsHook
{
}

View File

@ -0,0 +1,16 @@
<div class="row">
<div class="col-md-12">
<div class="panel panel-default panel-condensed">
<div class="panel-heading">
<strong>{{ $title }}</strong> <a href="{{ url('device/' . $device->device_id . '/notes') }}">[EDIT]</a>
</div>
<div class="panel-body">
<div class="row">
<div class="col-sm-12">
{!! Str::markdown($device->notes) !!}
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1 @@
<a href="{{ url('plugin/ExamplePlugin') }}"><i class="fa fa-coffee fa-fw fa-lg" aria-hidden="true"></i> Example Menu</a>

View File

@ -0,0 +1,8 @@
<div class="panel panel-default">
<div class="panel-body ">
<div class="pull-left" style="margin-top: 5px;">
<span style="font-size: 20px;">{{ $title }}</a></span><br>
Description
</div>
</div>
</div>

View File

@ -0,0 +1 @@
This is a port plugin tab plugin for port {{ $port->getLabel() }}

View File

@ -0,0 +1,94 @@
<div style="margin: 15px;">
<h4>{{ $plugin_name }} Settings:</h4>
<!-- Example of free-form settings, real plugins should use specific fields -->
<!-- All input fields should be in the settings array (settings[]) -->
<form method="post" style="margin: 15px">
@csrf
<table id="settings-table">
<tr>
<th>Name</th>
<th>Value</th>
</tr>
@forelse($settings as $name => $value)
<tr id="settings-row-{{ $name }}">
<td>
{{ $name }}
</td>
<td>
<input id="value-{{ $value }}" type="text" name="settings[{{ $name }}]" value="{{ $value }}">
<button type="button" onclick="deleteSetting('{{ $name }}')" class="delete-button"><i class="fa fa-trash"></i></button>
</td>
</tr>
@empty
<tr>
<td>No settings yet</td>
</tr>
@endforelse
</table>
<div style="margin: 15px 0;">
<input id="new-setting-name" style="display: inline-block;" type="text" placeholder="Name">
<input id="new-setting-value" style="display: inline-block;" type="text" placeholder="Value">
<button type="button" onclick="newSetting()">Add Setting</button>
</div>
<div>
<button type="submit">Save</button>
</div>
</form>
</div>
<script>
function newSetting() {
var name = document.getElementById('new-setting-name').value;
var value = document.getElementById('new-setting-value').value;
var existing = document.getElementById('value-' + name);
if (existing) {
existing.value = value;
} else {
// insert setting
var newValue = document.createElement('input');
newValue.id = 'value-' + name;
newValue.type = 'text';
newValue.name = 'settings[' + name + ']';
newValue.value = value;
var deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'delete-button';
deleteButton.onclick = () => deleteSetting(name);
var deleteIcon = document.createElement('i');
deleteIcon.className = 'fa fa-trash';
deleteButton.appendChild(deleteIcon);
var row = document.createElement('tr');
row.id = 'settings-row-' + name;
var col1 = document.createElement('td');
var col2 = document.createElement('td');
col1.innerText = name;
col2.appendChild(newValue);
col2.appendChild(document.createTextNode(' '));
col2.appendChild(deleteButton);
row.appendChild(col1);
row.appendChild(col2);
document.getElementById('settings-table').appendChild(row);
}
document.getElementById('new-setting-name').value = '';
document.getElementById('new-setting-value').value = '';
}
function deleteSetting(name) {
document.getElementById('settings-row-' + name).remove();
}
</script>
<style>
#settings-table td, #settings-table th {
padding: .2em;
}
.delete-button {
padding: 3px 5px;
}
</style>

16
app/Plugins/Hook.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Plugins;
interface Hook
{
/**
* Will be called by the plugin manager to check if the user is authorized. Will be called with Dependency Injection.
*/
// public function authorize(): bool;
/**
* Will be called by the plugin manager to execute this plugin at the correct time. Will be called with Dependency Injection.
*/
// public function handle();
}

View File

@ -0,0 +1,54 @@
<?php
/*
* DeviceHook.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\Hooks;
use App\Models\Device;
use App\Models\User;
use Illuminate\Support\Str;
abstract class DeviceOverviewHook
{
/** @var string */
public $view = 'resources.views.device-overview';
public function authorize(User $user, Device $device, array $settings): bool
{
return true;
}
public function data(Device $device): array
{
return [
'title' => __CLASS__,
'device' => $device,
];
}
final public function handle(string $pluginName, Device $device): \Illuminate\Contracts\View\View
{
return view(Str::start($this->view, "$pluginName::"), $this->data($device));
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* PluginMenuEntry.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\Hooks;
use App\Models\User;
use Illuminate\Support\Str;
abstract class MenuEntryHook
{
/** @var string */
public $view = 'resources.views.menu';
public function authorize(User $user, array $settings): bool
{
return true;
}
public function data(): array
{
return [];
}
final public function handle(string $pluginName): array
{
return [Str::start($this->view, "$pluginName::"), $this->data()];
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* SettingsHook.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\Hooks;
use App\Models\User;
use Illuminate\Support\Str;
abstract class PageHook
{
/** @var string */
public $view = 'resources.views.page';
public function authorize(User $user): bool
{
return true;
}
public function data(): array
{
return [
];
}
final public function handle(string $pluginName): array
{
return array_merge([
'settings_view' => Str::start($this->view, "$pluginName::"),
], $this->data());
}
}

View File

@ -0,0 +1,55 @@
<?php
/*
* PortPluginTab.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\Hooks;
use App\Models\Port;
use App\Models\User;
use App\Plugins\Hook;
use Illuminate\Support\Str;
abstract class PortTabHook implements Hook
{
/** @var string */
public $view = 'resources.views.port-tab';
public function authorize(User $user, Port $port, array $settings): bool
{
return true;
}
public function data(Port $port): array
{
return [
'title' => __CLASS__,
'port' => $port,
];
}
final public function handle(string $pluginName, Port $port): \Illuminate\Contracts\View\View
{
return view(Str::start($this->view, "$pluginName::"), $this->data($port));
}
}

View File

@ -0,0 +1,54 @@
<?php
/*
* SettingsHook.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\Hooks;
use App\Models\User;
use Illuminate\Support\Str;
abstract class SettingsHook
{
/** @var string */
public $view = 'resources.views.settings';
public function authorize(User $user, array $settings): bool
{
return true;
}
public function data(array $settings): array
{
return [
'settings' => $settings,
];
}
final public function handle(string $pluginName, array $settings): array
{
return array_merge([
'settings_view' => Str::start($this->view, "$pluginName::"),
], $this->data($settings));
}
}

View File

@ -0,0 +1,256 @@
<?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),
]);
}
}

View File

@ -0,0 +1,99 @@
<?php
/*
* PluginProvider.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\Providers;
use App\Exceptions\PluginDoesNotImplementHookException;
use App\Plugins\PluginManager;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
class PluginProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(PluginManager::class, function ($app) {
return new PluginManager;
});
}
public function boot(): void
{
$this->loadLocalPlugins($this->app->make(PluginManager::class));
}
/**
* Load any local plugins these plugins must implement only one hook.
*/
protected function loadLocalPlugins(PluginManager $manager): void
{
$plugin_view_location_registered = [];
foreach (glob(base_path('app/Plugins/*/*.php')) as $file) {
if (preg_match('#^(.*/([^/]+))/([^/.]+)\.php#', $file, $matches)) {
$plugin_name = $matches[2]; // containing directory name
if ($plugin_name == 'Hooks') {
continue; // don't load the hooks :D
}
$class = $this->className($plugin_name, $matches[3]);
$hook_type = $this->hookType($class);
// publish hooks in class
$hook_published = $manager->publishHook($plugin_name, $hook_type, $class);
// register view namespace
if ($hook_published && ! in_array($plugin_name, $plugin_view_location_registered)) {
$plugin_view_location_registered[] = $plugin_name; // don't register twice
$this->loadViewsFrom($matches[1], $plugin_name);
}
}
}
}
/**
* Check if a hook is extended by the given class.
*
* @param string $class
* @return string
*
* @throws \App\Exceptions\PluginDoesNotImplementHookException
*/
protected function hookType(string $class): string
{
foreach (class_parents($class) as $parent) {
if (Str::startsWith($parent, 'App\Plugins\Hooks\\')) {
return $parent;
}
}
throw new PluginDoesNotImplementHookException($class);
}
protected function className(string $dir, string $name): string
{
return 'App\Plugins\\' . $dir . '\\' . $name;
}
}

View File

@ -186,6 +186,7 @@ return [
* LibreNMS Service Providers...
*/
App\Providers\SnmptrapProvider::class,
App\Providers\PluginProvider::class,
],
/*
@ -244,6 +245,7 @@ return [
// LibreNMS
'Permissions' => \App\Facades\Permissions::class,
'PluginManager' => \App\Facades\PluginManager::class,
'DeviceCache' => \App\Facades\DeviceCache::class,
'Rrd' => \App\Facades\Rrd::class,
'SnmpQuery' => \App\Facades\FacadeAccessorSnmp::class,

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class PluginsAddVersionAndSettings extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('plugins', function (Blueprint $table) {
$table->integer('version')->default(1);
$table->longText('settings')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('plugins', function (Blueprint $table) {
$table->dropColumn(['version', 'settings']);
});
}
}

View File

@ -3,13 +3,99 @@ path: blob/master/doc/
# Developing for the Plugin System
This will most likely be deprecated in favour of adding the possible
extensions to the core code base.
With plugins you can extend LibreNMS with special functions that are
specific to your setup or are not relevant or interesting for all community members.
This documentation will hopefully give you a basis for how to write a
plugin for LibreNMS. A test plugin is included in LibreNMS distribution.
You are able to intervene in defined places in the behavior of
the website, without it coming to problems with future updates.
# Generic structure
This documentation will give you a basis for writing a plugin for
LibreNMS. An example plugin is included in the LibreNMS distribution.
# Version 2 Plugin System structure
Plugins in version 2 need to be installed into app/Plugins
The structure of a plugin is follows:
```
app/Plugins
/PluginName
/DeviceOverview.php
/Menu.php
/Page.php
/PortTab.php
/Settings.php
/resources/views
/device-overview.blade.php
/menu.blade.php
/page.blade.php
/port-tab.blade.php
/settings.blade.php
```
The above structure is checked before a plugin can be installed.
All file/folder names are case sensitive and must match the structure.
Only the blade files that are really needed need to be created. A plugin manager
will then load a hook that has a basic functionality.
If you want to customize the basic behavior of the hooks, you can create a
class in 'app/Plugins/PluginName' and overload the hook methods.
- device-overview.blade.php :: This is called in the Device
Overview page. You receive the $device as a object per default, you can do your
work here and display your results in a frame.
```
<div class="row">
<div class="col-md-12">
<div class="panel panel-default panel-condensed">
<div class="panel-heading">
<strong>{{ $title }}</strong>
</div>
<div class="panel-body">
<div class="row">
<div class="col-sm-12">
{{ $device->hostname }}
<!-- Do you stuff here -->
</div>
</div>
</div>
</div>
</div>
</div>
```
- port-tab.blade.php :: This is called in the Port page,
in the "Plugins" menu_option that will appear when your plugin gets
enabled. In this blade, you can do your work and display your
results in a frame.
- menu.blade.php :: For a menu entry
- page.blade.pho :: Here is a good place to add a own LibreNMS page without dependence with a device. A good place to create your own lists with special requirements and behavior.
- settings.blade.php :: If you need your own settings and variables, you can have a look in the ExamplePlugin.
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.
```
abstract class DeviceOverviewHook
{
...
public function authorize(User $user, Device $device, array $settings): bool
{
return $device->isUnderMaintenance();
}
...
```
# Version 1 Plugin System structure (legacy verion)
Plugins need to be installed into html/plugins

4
html/plugins/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@ -1,3 +0,0 @@
<?php
echo 'Well done, the plugin system is up and running';

View File

@ -1,27 +0,0 @@
<?php
namespace LibreNMS\Plugins;
class Test
{
public static function menu()
{
echo '<li><a href="plugin/p=Test">Test</a></li>';
}
//end menu()
public function device_overview_container($device)
{
echo '<div class="container-fluid"><div class="row"> <div class="col-md-12"> <div class="panel panel-default panel-condensed"> <div class="panel-heading"><strong>' . get_class() . ' Plugin </strong> </div>';
echo ' Example plugin in "Device - Overview" tab <br>';
echo '</div></div></div></div>';
}
public function port_container($device, $port)
{
echo '<div class="container-fluid"><div class="row"> <div class="col-md-12"> <div class="panel panel-default panel-condensed"> <div class="panel-heading"><strong>' . get_class() . ' plugin in "Port" tab</strong> </div>';
echo 'Example display in Port tab</br>';
echo '</div></div></div></div>';
}
}

View File

@ -884,51 +884,6 @@ function port_fill_missing(&$port, $device)
}
}
function scan_new_plugins()
{
$installed = 0; // Track how many plugins we install.
if (file_exists(Config::get('plugin_dir'))) {
$plugin_files = scandir(Config::get('plugin_dir'));
foreach ($plugin_files as $name) {
if (is_dir(Config::get('plugin_dir') . '/' . $name)) {
if ($name != '.' && $name != '..') {
if (is_file(Config::get('plugin_dir') . '/' . $name . '/' . $name . '.php') && is_file(Config::get('plugin_dir') . '/' . $name . '/' . $name . '.inc.php')) {
$plugin_id = dbFetchRow('SELECT `plugin_id` FROM `plugins` WHERE `plugin_name` = ?', [$name]);
if (empty($plugin_id)) {
if (dbInsert(['plugin_name' => $name, 'plugin_active' => '0'], 'plugins')) {
$installed++;
}
}
}
}
}
}
}
return $installed;
}
function scan_removed_plugins()
{
$removed = 0; // Track how many plugins will be removed from database
if (file_exists(Config::get('plugin_dir'))) {
$plugin_files = scandir(Config::get('plugin_dir'));
$installed_plugins = dbFetchColumn('SELECT `plugin_name` FROM `plugins`');
foreach ($installed_plugins as $name) {
if (in_array($name, $plugin_files)) {
continue;
}
if (dbDelete('plugins', '`plugin_name` = ?', $name)) {
$removed++;
}
}
}
return $removed;
}
function validate_device_id($id)
{
if (empty($id) || ! is_numeric($id)) {

View File

@ -1,5 +1,7 @@
<?php
use App\Plugins\Hooks\DeviceOverviewHook;
$overview = 1;
echo '
@ -18,6 +20,9 @@ require 'overview/puppet_agent.inc.php';
require 'overview/tracepath.inc.php';
echo LibreNMS\Plugins::call('device_overview_container', [$device]);
PluginManager::call(DeviceOverviewHook::class, ['device' => DeviceCache::getPrimary()])->each(function ($view) {
echo $view;
});
require 'overview/ports.inc.php';

View File

@ -1,5 +1,9 @@
<?php
use App\Plugins\Hooks\PortTabHook;
use LibreNMS\Util\Rewrite;
use LibreNMS\Util\Url;
$vars['view'] = basename($vars['view'] ?? 'graphs');
$port = dbFetchRow('SELECT * FROM `ports` WHERE `port_id` = ?', [$vars['port']]);
@ -32,7 +36,7 @@ if ($port['ifAdminStatus'] == 'up' && $port['ifOperStatus'] == 'up') {
}
$i = 1;
$inf = \LibreNMS\Util\Rewrite::normalizeIfName($ifname);
$inf = Rewrite::normalizeIfName($ifname);
$bg = '#ffffff';
@ -100,7 +104,9 @@ if (count($components) > 0) {
$menu_options['cbqos'] = 'CBQoS';
}
if (LibreNMS\Plugins::countHooks('port_container')) {
$portModel = \App\Models\Port::find($port['port_id']);
if (LibreNMS\Plugins::countHooks('port_container') || \PluginManager::hasHooks(PortTabHook::class, ['port' => $portModel])) {
// Checking if any plugin implements the port_container. If yes, allow to display the menu_option
$menu_options['plugins'] = 'Plugins';
}
@ -198,7 +204,7 @@ if (dbFetchCell("SELECT COUNT(*) FROM juniAtmVp WHERE port_id = '" . $port['port
echo "<span class='pagemenu-selected'>";
}
echo "<a href='" . \LibreNMS\Util\Url::generate(['page' => 'device', 'device' => $device['device_id'], 'tab' => 'port', 'port' => $port['port_id']]) . "/junose-atm-vp/bits/'>Bits</a>";
echo "<a href='" . Url::generate(['page' => 'device', 'device' => $device['device_id'], 'tab' => 'port', 'port' => $port['port_id']]) . "/junose-atm-vp/bits/'>Bits</a>";
if ($vars['view'] == 'junose-atm-vp' && $vars['graph'] == 'bits') {
echo '</span>';
}
@ -208,7 +214,7 @@ if (dbFetchCell("SELECT COUNT(*) FROM juniAtmVp WHERE port_id = '" . $port['port
echo "<span class='pagemenu-selected'>";
}
echo "<a href='" . \LibreNMS\Util\Url::generate(['page' => 'device', 'device' => $device['device_id'], 'tab' => 'port', 'port' => $port['port_id']]) . "/junose-atm-vp/packets/'>Packets</a>";
echo "<a href='" . Url::generate(['page' => 'device', 'device' => $device['device_id'], 'tab' => 'port', 'port' => $port['port_id']]) . "/junose-atm-vp/packets/'>Packets</a>";
if ($vars['view'] == 'junose-atm-vp' && $vars['graph'] == 'bits') {
echo '</span>';
}
@ -218,7 +224,7 @@ if (dbFetchCell("SELECT COUNT(*) FROM juniAtmVp WHERE port_id = '" . $port['port
echo "<span class='pagemenu-selected'>";
}
echo "<a href='" . \LibreNMS\Util\Url::generate(['page' => 'device', 'device' => $device['device_id'], 'tab' => 'port', 'port' => $port['port_id']]) . "/junose-atm-vp/cells/'>Cells</a>";
echo "<a href='" . Url::generate(['page' => 'device', 'device' => $device['device_id'], 'tab' => 'port', 'port' => $port['port_id']]) . "/junose-atm-vp/cells/'>Cells</a>";
if ($vars['view'] == 'junose-atm-vp' && $vars['graph'] == 'bits') {
echo '</span>';
}
@ -228,7 +234,7 @@ if (dbFetchCell("SELECT COUNT(*) FROM juniAtmVp WHERE port_id = '" . $port['port
echo "<span class='pagemenu-selected'>";
}
echo "<a href='" . \LibreNMS\Util\Url::generate(['page' => 'device', 'device' => $device['device_id'], 'tab' => 'port', 'port' => $port['port_id']]) . "/junose-atm-vp/errors/'>Errors</a>";
echo "<a href='" . Url::generate(['page' => 'device', 'device' => $device['device_id'], 'tab' => 'port', 'port' => $port['port_id']]) . "/junose-atm-vp/errors/'>Errors</a>";
if ($vars['view'] == 'junose-atm-vp' && $vars['graph'] == 'bits') {
echo '</span>';
}
@ -237,11 +243,11 @@ if (dbFetchCell("SELECT COUNT(*) FROM juniAtmVp WHERE port_id = '" . $port['port
if (Auth::user()->hasGlobalAdmin() && \LibreNMS\Config::get('enable_billing') == 1) {
$bills = dbFetchRows('SELECT `bill_id` FROM `bill_ports` WHERE `port_id`=?', [$port['port_id']]);
if (count($bills) === 1) {
echo "<span style='float: right;'><a href='" . \LibreNMS\Util\Url::generate(['page' => 'bill', 'bill_id' => $bills[0]['bill_id']]) . "'><i class='fa fa-money fa-lg icon-theme' aria-hidden='true'></i> View Bill</a></span>";
echo "<span style='float: right;'><a href='" . Url::generate(['page' => 'bill', 'bill_id' => $bills[0]['bill_id']]) . "'><i class='fa fa-money fa-lg icon-theme' aria-hidden='true'></i> View Bill</a></span>";
} elseif (count($bills) > 1) {
echo "<span style='float: right;'><a href='" . \LibreNMS\Util\Url::generate(['page' => 'bills']) . "'><i class='fa fa-money fa-lg icon-theme' aria-hidden='true'></i> View Bills</a></span>";
echo "<span style='float: right;'><a href='" . Url::generate(['page' => 'bills']) . "'><i class='fa fa-money fa-lg icon-theme' aria-hidden='true'></i> View Bills</a></span>";
} else {
echo "<span style='float: right;'><a href='" . \LibreNMS\Util\Url::generate(['page' => 'bills', 'view' => 'add', 'port' => $port['port_id']]) . "'><i class='fa fa-money fa-lg icon-theme' aria-hidden='true'></i> Create Bill</a></span>";
echo "<span style='float: right;'><a href='" . Url::generate(['page' => 'bills', 'view' => 'add', 'port' => $port['port_id']]) . "'><i class='fa fa-money fa-lg icon-theme' aria-hidden='true'></i> Create Bill</a></span>";
}
}

View File

@ -13,6 +13,8 @@
* @author PipoCanaja <pipocanaja@gmail.com>
*/
use App\Plugins\Hooks\PortTabHook;
$pagetitle[] = 'Plugins';
$no_refresh = true;
?>
@ -21,3 +23,6 @@ $no_refresh = true;
<hr>
<?php
echo \LibreNMS\Plugins::call('port_container', [$device, $port]);
PluginManager::call(PortTabHook::class, ['port' => $portModel])->each(function ($view) {
echo $view;
});

View File

@ -1,24 +0,0 @@
<?php
use LibreNMS\Config;
$link_array = ['page' => 'plugin'];
if ($vars['view'] == 'admin') {
include_once Config::get('install_dir') . '/includes/html/pages/plugin/admin.inc.php';
$pagetitle[] = 'Plugins';
} else {
$pagetitle[] = $vars['p'];
$plugin = dbFetchRow("SELECT `plugin_name` FROM `plugins` WHERE `plugin_name` = ? AND `plugin_active`='1'", [$vars['p']]);
if (! empty($plugin)) {
$plugin_path = Config::get('plugin_dir') . '/' . $plugin['plugin_name'] . '/' . $plugin['plugin_name'] . '.inc.php';
if (is_file($plugin_path)) {
chdir(Config::get('install_dir') . '/html');
include $plugin_path;
chdir(Config::get('install_dir'));
return;
}
}
print_error('This plugin is either disabled or not available.');
}

View File

@ -1,93 +0,0 @@
<?php
if (Auth::user()->hasGlobalAdmin()) {
// Scan for new plugins and add to the database
$new_plugins = scan_new_plugins();
$removed_plugins = scan_removed_plugins();
// Check if we have to toggle enabled / disable a particular module
$plugin_id = $_POST['plugin_id'];
$plugin_active = $_POST['plugin_active'];
if (is_numeric($plugin_id) && is_numeric($plugin_active)) {
if ($plugin_active == '0') {
$plugin_active = 1;
} elseif ($plugin_active == '1') {
$plugin_active = 0;
} else {
$plugin_active = 0;
}
if (dbUpdate(['plugin_active' => $plugin_active], 'plugins', '`plugin_id` = ?', [$plugin_id])) {
echo '
<script type="text/javascript">
$.ajax({
url: "",
context: document.body,
success: function(s,x){
$(this).html(s);
}
});
</script>
';
}
}//end if?>
<div class="panel panel-default panel-condensed">
<div class="panel-heading">
<strong>System plugins</strong>
</div>
<?php
if ($new_plugins > 0) {
echo '<div class="panel-body">
<div class="alert alert-warning">
We have found ' . $new_plugins . ' new plugins that need to be configured and enabled
</div>
</div>';
}
if ($removed_plugins > 0) {
echo '<div class="panel-body">
<div class="alert alert-warning">
We have found ' . $removed_plugins . ' removed plugins
</div>
</div>';
} ?>
<table class="table table-condensed">
<tr>
<th>Name</th>
<th>Action</th>
</tr>
<?php
foreach (dbFetchRows('SELECT * FROM plugins') as $plugins) {
if ($plugins['plugin_active'] == 1) {
$plugin_colour = 'bg-success';
$plugin_button = 'danger';
$plugin_label = 'Disable';
} else {
$plugin_colour = 'bg-danger';
$plugin_button = 'success';
$plugin_label = 'Enable';
}
echo '<tr class="' . $plugin_colour . '">
<td>
' . $plugins['plugin_name'] . '
</td>
<td>
<form class="form-inline" role="form" action="" method="post" id="' . $plugins['plugin_id'] . '" name=="' . $plugins['plugin_id'] . '">
' . csrf_field() . '
<input type="hidden" name="plugin_id" value="' . $plugins['plugin_id'] . '">
<input type="hidden" name="plugin_active" value="' . $plugins['plugin_active'] . '">
<button type="submit" class="btn btn-sm btn-' . $plugin_button . '">' . $plugin_label . '</button>
</form>
</td>
</tr>';
}//end foreach
?>
</table>
</div>
<?php
} else {
include 'includes/html/error-no-perm.inc.php';
}//end if

View File

@ -1349,6 +1349,8 @@ plugins:
- { Field: plugin_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
- { Field: plugin_name, Type: varchar(60), 'Null': false, Extra: '' }
- { Field: plugin_active, Type: int, 'Null': false, Extra: '' }
- { Field: version, Type: int, 'Null': false, Extra: '', Default: '1' }
- { Field: settings, Type: longtext, 'Null': true, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [plugin_id], Unique: true, Type: BTREE }
pollers:

View File

@ -0,0 +1,12 @@
<?php
return [
'settings_page' => ':plugin: Settings',
'admin_page' => 'Plugin Admin',
'admin_title' => 'System Plugins',
'errors' => [
'not_exist' => 'Plugin :plugin does not exist.',
'disabled' => 'Plugin :plugin is disabled.',
'view_missing' => 'Missing view.',
],
];

View File

@ -81,17 +81,21 @@
</a></li>
</ul>
</li>
@if(auth()->user()->isAdmin() || \LibreNMS\Plugins::count())
@if(auth()->user()->isAdmin() || $has_v1_plugins || $has_v2_plugins)
<li class="dropdown-submenu">
<a><i class="fa fa-plug fa-fw fa-lg" aria-hidden="true"></i> @lang('Plugins')</a>
<ul class="dropdown-menu">
{!! \LibreNMS\Plugins::call('menu') !!}
{!! $v1_plugin_menu !!}
@foreach($menu_hooks as [$view, $data])
<li>@include($view, $data)</li>
@endforeach
@admin
@if(\LibreNMS\Plugins::count())
<li role="presentation" class="divider"></li>
@endif
<li><a href="{{ url('plugin/view=admin') }}"> <i class="fa fa-lock fa-fw fa-lg"
aria-hidden="true"></i>@lang('Plugin Admin')
@if($has_v1_plugins || $has_v2_plugins)
<li role="presentation" class="divider"></li>
@endif
<li>
<a href="{{ route('plugin.admin') }}">
<i class="fa fa-lock fa-fw fa-lg" aria-hidden="true"></i>@lang('Plugin Admin')
</a>
</li>
@endadmin

View File

@ -0,0 +1,42 @@
@extends('layouts.librenmsv1')
@section('title', trans('plugins.admin_page'))
@section('content')
<div class="container">
<div class="panel panel-default panel-condensed col-md-6 col-md-offset-3 col-xs-12 col-sm-8 col-sm-offset-2" style="padding: 0">
<div class="panel-heading">
<strong>@lang('plugins.admin_title')</strong>
</div>
<table class="table table-condensed">
<tr>
<th>Name</th>
<th>Action</th>
</tr>
@foreach($plugins as $plugin)
<tr class="{{ $plugin->plugin_active ? 'bg-success' : 'bg-danger' }}">
<td>{{ $plugin->plugin_name }}</td>
<td>
<form class="form-inline" role="form" action="{{ route('plugin.update', ['plugin' => $plugin->plugin_name]) }}" method="post" id="{{ $plugin->plugin_id }}" name="{{ $plugin->plugin_id }}">
@csrf
@if($plugin->plugin_active)
<input type="hidden" name="plugin_active" value="0">
<button type="submit" class="btn btn-sm btn-danger" style="min-width: 66px">@lang('Disable')</button>
@else
<input type="hidden" name="plugin_active" value="1">
<button type="submit" class="btn btn-sm btn-success" style="min-width: 66px">@lang('Enable')</button>
@endif
@if($plugin->version == 1)
<a href="{{ route('plugin.legacy', $plugin->plugin_name) }}" class="btn btn-sm btn-primary" style="min-width: 72px">@lang('Page')</a>
@else
<a href="{{ route('plugin.settings', $plugin->plugin_name) }}" class="btn btn-sm btn-primary" style="min-width: 72px">@lang('Settings')</a>
@endif
</form>
</td>
</tr>
@endforeach
</table>
</div>
</div>
@endsection

View File

@ -0,0 +1,7 @@
@extends('layouts.librenmsv1')
@section('title', $title)
@section('content')
{!! $content !!}
@endsection

View File

@ -0,0 +1 @@
<h2>@lang('plugins.errors.view_missing')</h2>

View File

@ -0,0 +1,5 @@
@extends('layouts.librenmsv1')
@section('title', $title)
@include($settings_view, $settings)

View File

@ -77,6 +77,15 @@ Route::group(['middleware' => ['auth'], 'guard' => 'auth'], function () {
Route::post('alert/transports/{transport}/test', [\App\Http\Controllers\AlertTransportController::class, 'test'])->name('alert.transports.test');
});
Route::get('plugin/settings', 'PluginAdminController')->name('plugin.admin');
Route::get('plugin/settings/{plugin:plugin_name}', 'PluginSettingsController')->name('plugin.settings');
Route::post('plugin/settings/{plugin:plugin_name}', 'PluginSettingsController@update')->name('plugin.update');
Route::get('plugin', 'PluginLegacyController@redirect');
Route::redirect('plugin/view=admin', '/plugin/admin');
Route::get('plugin/p={pluginName}', 'PluginLegacyController@redirect');
Route::get('plugin/v1/{plugin:plugin_name}', 'PluginLegacyController')->name('plugin.legacy');
Route::get('plugin/{plugin:plugin_name}', 'PluginPageController')->name('plugin.page');
// old route redirects
Route::permanentRedirect('poll-log', 'poller/log');