mirror of
https://github.com/Mibew/mibew.git
synced 2024-11-15 16:44:11 +03:00
Implement new plugin management system
This commit is contained in:
parent
014a7fabcb
commit
80077be185
@ -289,3 +289,19 @@ visitedpagestatistics:
|
||||
acceptedinvitations: "int NOT NULL DEFAULT 0"
|
||||
rejectedinvitations: "int NOT NULL DEFAULT 0"
|
||||
ignoredinvitations: "int NOT NULL DEFAULT 0"
|
||||
|
||||
# Contains info about installed plugins
|
||||
plugin:
|
||||
fields:
|
||||
# Artificial ID
|
||||
id: "INT NOT NULL auto_increment PRIMARY KEY"
|
||||
# Plugin name in "<Vendor>:<Name>" format.
|
||||
name: "varchar(1024) NOT NULL"
|
||||
# Installed version of the plugin.
|
||||
version: "varchar(256) NOT NULL"
|
||||
# Indicates if the plugin is installed or not.
|
||||
installed: "tinyint NOT NULL DEFAULT 0"
|
||||
# Indicates if the plugin is enabled or not.
|
||||
enabled: "tinyint NOT NULL DEFAULT 0"
|
||||
unique_keys:
|
||||
name: [name]
|
||||
|
@ -31,12 +31,8 @@ plugins: []
|
||||
|
||||
## Exapmle of plugins configuration
|
||||
# plugins:
|
||||
# -
|
||||
# name: "VendorName:PluginName"
|
||||
# config:
|
||||
# weight: 100
|
||||
# some_configurable_value: value
|
||||
# -
|
||||
# name: "VendorName:AnotherPluginName"
|
||||
# config:
|
||||
# very_important_value: "$3.50"
|
||||
# "VendorName:PluginName":
|
||||
# weight: 100
|
||||
# some_configurable_value: value
|
||||
# "VendorName:AnotherPluginName":
|
||||
# very_important_value: "$3.50"
|
||||
|
@ -548,6 +548,35 @@ password_recovery_reset:
|
||||
defaults:
|
||||
_controller: Mibew\Controller\PasswordRecoveryController::resetAction
|
||||
|
||||
## Plugins
|
||||
plugin_enable:
|
||||
path: /operator/plugin/{plugin_name}/enable
|
||||
defaults:
|
||||
_controller: Mibew\Controller\PluginController::enableAction
|
||||
_access_check: Mibew\AccessControl\Check\PermissionsCheck
|
||||
_access_permissions: [CAN_ADMINISTRATE]
|
||||
|
||||
plugin_disable:
|
||||
path: /operator/plugin/{plugin_name}/disable
|
||||
defaults:
|
||||
_controller: Mibew\Controller\PluginController::disableAction
|
||||
_access_check: Mibew\AccessControl\Check\PermissionsCheck
|
||||
_access_permissions: [CAN_ADMINISTRATE]
|
||||
|
||||
plugin_uninstall:
|
||||
path: /operator/plugin/{plugin_name}/uninstall
|
||||
defaults:
|
||||
_controller: Mibew\Controller\PluginController::uninstallAction
|
||||
_access_check: Mibew\AccessControl\Check\PermissionsCheck
|
||||
_access_permissions: [CAN_ADMINISTRATE]
|
||||
|
||||
plugins:
|
||||
path: /operator/plugin
|
||||
defaults:
|
||||
_controller: Mibew\Controller\PluginController::indexAction
|
||||
_access_check: Mibew\AccessControl\Check\PermissionsCheck
|
||||
_access_permissions: [CAN_ADMINISTRATE]
|
||||
|
||||
## Settings
|
||||
settings_common:
|
||||
path: /operator/settings
|
||||
|
28
src/mibew/js/source/plugins.js
Normal file
28
src/mibew/js/source/plugins.js
Normal file
@ -0,0 +1,28 @@
|
||||
/*!
|
||||
* This file is a part of Mibew Messenger.
|
||||
*
|
||||
* Copyright 2005-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
(function(Mibew, $) {
|
||||
$(document).ready(function(){
|
||||
$('a.uninstall-link').click(function(){
|
||||
return confirm(Mibew.Localization.trans(
|
||||
'Are you sure that you want to uninstall plugin "{0}"?',
|
||||
$(this).data('plugin-name')
|
||||
));
|
||||
});
|
||||
});
|
||||
})(Mibew, jQuery);
|
185
src/mibew/libs/classes/Mibew/Controller/PluginController.php
Normal file
185
src/mibew/libs/classes/Mibew/Controller/PluginController.php
Normal file
@ -0,0 +1,185 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is a part of Mibew Messenger.
|
||||
*
|
||||
* Copyright 2005-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Mibew\Controller;
|
||||
|
||||
use Mibew\Http\Exception\NotFoundException;
|
||||
use Mibew\Plugin\PluginInfo;
|
||||
use Mibew\Plugin\PluginManager;
|
||||
use Mibew\Plugin\Utils as PluginUtils;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Contains all actions which are related with plugins management.
|
||||
*/
|
||||
class PluginController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* Generates list of all plugins in the system.
|
||||
*
|
||||
* @param Request $request Incoming request.
|
||||
* @return string Rendered page content.
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
{
|
||||
set_csrf_token();
|
||||
|
||||
$page = array(
|
||||
// Use errors list stored in the request. We need to do so to have
|
||||
// an ability to pass errors from another actions.
|
||||
'errors' => $request->attributes->get('errors', array()),
|
||||
);
|
||||
|
||||
$page['plugins'] = $this->buildPluginsList();
|
||||
$page['title'] = getlocal('Plugins');
|
||||
$page['menuid'] = 'plugins';
|
||||
$page = array_merge($page, prepare_menu($this->getOperator()));
|
||||
|
||||
$this->getAssetManager()->attachJs('js/compiled/plugins.js');
|
||||
|
||||
return $this->render('plugins', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a plugin.
|
||||
*
|
||||
* @param Request $request Incoming request.
|
||||
* @return string Rendered page content.
|
||||
* @throws NotFoundException If the plugin with specified name is not found
|
||||
* in the system.
|
||||
*/
|
||||
public function enableAction(Request $request)
|
||||
{
|
||||
csrf_check_token($request);
|
||||
|
||||
$plugin_name = $request->attributes->get('plugin_name');
|
||||
|
||||
if (!PluginUtils::pluginExists($plugin_name)) {
|
||||
throw new NotFoundException('The plugin is not found.');
|
||||
}
|
||||
|
||||
// Enable the plugin
|
||||
if (!PluginManager::getInstance()->enable($plugin_name)) {
|
||||
$error = getlocal(
|
||||
'Plugin "{0}" cannot be enabled.',
|
||||
array($plugin_name)
|
||||
);
|
||||
$request->attributes->set('errors', array($error));
|
||||
|
||||
// The plugin cannot be enabled by some reasons. Just rebuild
|
||||
// index page and show errors there.
|
||||
return $this->indexAction($request);
|
||||
}
|
||||
|
||||
return $this->redirect($this->generateUrl('plugins'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables a plugin.
|
||||
*
|
||||
* @param Request $request Incoming request.
|
||||
* @return string Rendered page content.
|
||||
* @throws NotFoundException If the plugin with specified name is not found
|
||||
* in the system.
|
||||
*/
|
||||
public function disableAction(Request $request)
|
||||
{
|
||||
csrf_check_token($request);
|
||||
|
||||
$plugin_name = $request->attributes->get('plugin_name');
|
||||
|
||||
if (!PluginUtils::pluginExists($plugin_name)) {
|
||||
throw new NotFoundException('The plugin is not found.');
|
||||
}
|
||||
|
||||
// Disable the plugin
|
||||
if (!PluginManager::getInstance()->disable($plugin_name)) {
|
||||
$error = getlocal(
|
||||
'Plugin "{0}" cannot be disabled.',
|
||||
array($plugin_name)
|
||||
);
|
||||
$request->attributes->set('errors', array($error));
|
||||
|
||||
// The plugin cannot be disabled by some reasons. Just rebuild
|
||||
// index page and show errors there.
|
||||
return $this->indexAction($request);
|
||||
}
|
||||
|
||||
return $this->redirect($this->generateUrl('plugins'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls a plugin.
|
||||
*
|
||||
* @param Request $request Incoming request.
|
||||
* @return string Rendered page content.
|
||||
* @throws NotFoundException If the plugin with specified name is not found
|
||||
* in the system.
|
||||
*/
|
||||
public function uninstallAction(Request $request)
|
||||
{
|
||||
csrf_check_token($request);
|
||||
|
||||
$plugin_name = $request->attributes->get('plugin_name');
|
||||
|
||||
if (!PluginUtils::pluginExists($plugin_name)) {
|
||||
throw new NotFoundException('The plugin is not found.');
|
||||
}
|
||||
|
||||
// Uninstall the plugin
|
||||
if (!PluginManager::getInstance()->uninstall($plugin_name)) {
|
||||
$error = getlocal(
|
||||
'Plugin "{0}" cannot be uninstalled.',
|
||||
array($plugin_name)
|
||||
);
|
||||
$request->attributes->set('errors', array($error));
|
||||
|
||||
// The plugin cannot be uninstalled by some reasons. Just rebuild
|
||||
// index page and show errors there.
|
||||
return $this->indexAction($request);
|
||||
}
|
||||
|
||||
return $this->redirect($this->generateUrl('plugins'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds plugins list that will be passed to templates engine.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function buildPluginsList()
|
||||
{
|
||||
$plugins = array();
|
||||
foreach (PluginUtils::discoverPlugins() as $plugin_name) {
|
||||
$plugin = new PluginInfo($plugin_name);
|
||||
$plugins[] = array(
|
||||
'name' => $plugin_name,
|
||||
'version' => $plugin->getInstalledVersion() ?: $plugin->getVersion(),
|
||||
'dependencies' => $plugin->getDependencies(),
|
||||
'enabled' => $plugin->isEnabled(),
|
||||
'installed' => $plugin->isInstalled(),
|
||||
'canBeEnabled' => $plugin->canBeEnabled(),
|
||||
'canBeDisabled' => $plugin->canBeDisabled(),
|
||||
'canBeUninstalled' => $plugin->canBeUninstalled(),
|
||||
);
|
||||
}
|
||||
|
||||
return $plugins;
|
||||
}
|
||||
}
|
352
src/mibew/libs/classes/Mibew/Plugin/DependencyGraph.php
Normal file
352
src/mibew/libs/classes/Mibew/Plugin/DependencyGraph.php
Normal file
@ -0,0 +1,352 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is a part of Mibew Messenger.
|
||||
*
|
||||
* Copyright 2005-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Mibew\Plugin;
|
||||
|
||||
use vierbergenlars\SemVer\version as Version;
|
||||
use vierbergenlars\SemVer\expression as VersionExpression;
|
||||
|
||||
/**
|
||||
* Represents a dependency graph.
|
||||
*
|
||||
* The main aim of the class is to validate dependencies and build loading queue
|
||||
* based on them.
|
||||
*/
|
||||
class DependencyGraph
|
||||
{
|
||||
/**
|
||||
* Indicates that a plugin was not visited by depth-first search algorithm.
|
||||
*/
|
||||
const DFS_NOT_VISITED = 'not_visited';
|
||||
|
||||
/**
|
||||
* Indicates that depth-first search algorithm is processing the plugin.
|
||||
*/
|
||||
const DFS_IN_PROGRESS = 'in_progress';
|
||||
|
||||
/**
|
||||
* Indicates that a plugin was visited by depth-first search algorithm.
|
||||
*/
|
||||
const DFS_VISITED = 'visited';
|
||||
|
||||
/**
|
||||
* List of all plugins attached to the graph.
|
||||
* @var PluginInfo[]
|
||||
*/
|
||||
protected $plugins = array();
|
||||
|
||||
/**
|
||||
* Contains plugins states related with depth-first search algorithm.
|
||||
*
|
||||
* Each key of the array is plugin name and each value is one of
|
||||
* DependencyGraph::DFS_* constants.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dfsState = array();
|
||||
|
||||
/**
|
||||
* Plugins loading queue.
|
||||
* @var PluginInfo[]
|
||||
*/
|
||||
private $loadingQueue = array();
|
||||
|
||||
/**
|
||||
* List of plugins with fully satisfied dependencies.
|
||||
* @var PluginInfo[]
|
||||
*/
|
||||
private $loadablePlugins = array();
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param PluginInfo[]|null $plugins List of plugins that should be added to
|
||||
* the graph.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct($plugins = null)
|
||||
{
|
||||
if (!is_null($plugins)) {
|
||||
if (!is_array($plugins)) {
|
||||
throw new \InvalidArgumentException('The first argument must be an array or null');
|
||||
}
|
||||
foreach ($plugins as $plugin) {
|
||||
$this->addPlugin($plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a plugin to the graph.
|
||||
*
|
||||
* Notice that this method accepts an instance of
|
||||
* {@link \Mibew\Plugin\PluginInfo} class. It's done intentionally because
|
||||
* we do not need an instance of {@link \Mibew\Plugin\PluginInterface} to
|
||||
* analize its dependencies.
|
||||
*
|
||||
* @param PluginInfo $plugin A plugin that should be added.
|
||||
*/
|
||||
public function addPlugin(PluginInfo $plugin)
|
||||
{
|
||||
$this->plugins[$plugin->getName()] = $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the plugin with specified name was attached to the graph.
|
||||
*
|
||||
* @param string $name Name of the plugin.
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasPlugin($name)
|
||||
{
|
||||
return isset($this->plugins[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the plugin from the graph.
|
||||
*
|
||||
* @param string $name Name of the plugin.
|
||||
* @throws \RuntimeException If the plugin with such name was not attached
|
||||
* to the graph.
|
||||
*/
|
||||
public function removePlugin($name)
|
||||
{
|
||||
if (!isset($this->plugins[$name])) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
'There is no "%s" plugin in dependency graph',
|
||||
$name
|
||||
));
|
||||
}
|
||||
|
||||
unset($this->plugins[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a plugin attached to the graph.
|
||||
*
|
||||
* Notice that this method accepts an instance of
|
||||
* {@link \Mibew\Plugin\PluginInfo} class. It's done intentionally because
|
||||
* we do not need an instance of {@link \Mibew\Plugin\PluginInterface} to
|
||||
* analize its dependencies.
|
||||
*
|
||||
* @param string $name Name of the plugin.
|
||||
* @return PluginInfo
|
||||
* @throws \RuntimeException If the plugin with such name was not attached
|
||||
* to the graph.
|
||||
*/
|
||||
public function getPlugin($name)
|
||||
{
|
||||
if (!isset($this->plugins[$name])) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
'There is no "%s" plugin in dependency graph',
|
||||
$name
|
||||
));
|
||||
}
|
||||
|
||||
return $this->plugins[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds plugins loading queue.
|
||||
*
|
||||
* The method filters plugins that cannot be loaded because of unsatisfied
|
||||
* dependencies and sorts the others by loading turn.
|
||||
*
|
||||
* Together with {@link \Mibew\Plugin\DependencyGraph::doSortStep()} method
|
||||
* is implements topological sorting algoritm. The only deference from the
|
||||
* topological sorting the results are in reverse order.
|
||||
*
|
||||
* @return PluginInfo[]
|
||||
*/
|
||||
public function getLoadingQueue()
|
||||
{
|
||||
$this->loadingQueue = array();
|
||||
|
||||
$this->clearDfsState();
|
||||
foreach ($this->getLoadablePlugins() as $plugin) {
|
||||
if ($this->getDfsState($plugin) != self::DFS_VISITED) {
|
||||
$this->doSortStep($plugin);
|
||||
}
|
||||
}
|
||||
$this->clearDfsState();
|
||||
|
||||
return $this->loadingQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a step of sorting algorithm.
|
||||
*
|
||||
* Actually this method represents a step of depth-first serach algorithm
|
||||
* with some additions related with sorting.
|
||||
*
|
||||
* @param PluginInfo $plugin A plugin that should be sorted.
|
||||
* @throws \LogicException If cyclic dependencies are found.
|
||||
*/
|
||||
protected function doSortStep(PluginInfo $plugin)
|
||||
{
|
||||
if ($this->getDfsState($plugin) == self::DFS_IN_PROGRESS) {
|
||||
throw new \LogicException(sprintf(
|
||||
'Cyclic dependencies found for plugin "%s"',
|
||||
$plugin->getName()
|
||||
));
|
||||
}
|
||||
|
||||
$this->setDfsState($plugin, self::DFS_IN_PROGRESS);
|
||||
foreach (array_keys($plugin->getDependencies()) as $dependency_name) {
|
||||
$dependency = $this->getPlugin($dependency_name);
|
||||
if ($this->getDfsState($dependency) != self::DFS_VISITED) {
|
||||
$this->doSortStep($dependency);
|
||||
}
|
||||
}
|
||||
$this->setDfsState($plugin, self::DFS_VISITED);
|
||||
$this->loadingQueue[] = $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of plugins that can be loaded.
|
||||
*
|
||||
* This method together with
|
||||
* {@link \Mibew\Plugin\DependencyGraph::doVerificationStep()} method
|
||||
* implements depth-first search algorithm to filter plugins with
|
||||
* unsatisfied dependencies.
|
||||
*
|
||||
* @return PluginInfo[]
|
||||
*/
|
||||
protected function getLoadablePlugins()
|
||||
{
|
||||
$this->loadablePlugins = array();
|
||||
|
||||
$this->clearDfsState();
|
||||
foreach ($this->plugins as $plugin) {
|
||||
if ($this->getDfsState($plugin) != self::DFS_VISITED) {
|
||||
$this->doVerificationStep($plugin);
|
||||
}
|
||||
}
|
||||
$this->clearDfsState();
|
||||
|
||||
return array_values($this->loadablePlugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a step of plugin's verification algorithm.
|
||||
*
|
||||
* Actually this method represents a step of depth-first search algorithm
|
||||
* with additions related with plugin's verification.
|
||||
*
|
||||
* @param PluginInfo $plugin A plugin that should be verified.
|
||||
* @throws \LogicException If cyclic dependencies are found.
|
||||
*/
|
||||
protected function doVerificationStep(PluginInfo $plugin)
|
||||
{
|
||||
if ($this->getDfsState($plugin) == self::DFS_IN_PROGRESS) {
|
||||
throw new \LogicException(sprintf(
|
||||
'Cyclic dependencies found for plugin "%s"',
|
||||
$plugin->getName()
|
||||
));
|
||||
}
|
||||
|
||||
$this->setDfsState($plugin, self::DFS_IN_PROGRESS);
|
||||
$can_be_loaded = true;
|
||||
foreach ($plugin->getDependencies() as $dependency_name => $required_version) {
|
||||
// Make sure the dependency exist
|
||||
if (!$this->hasPlugin($dependency_name)) {
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'Plugin "%s" does not exist but is listed in "%s" dependencies!',
|
||||
$dependency_name,
|
||||
$plugin->getName()
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
$can_be_loaded = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check that version of the dependency satisfied requirements
|
||||
$version_constrain = new VersionExpression($required_version);
|
||||
$dependency = $this->getPlugin($dependency_name);
|
||||
if (!$version_constrain->satisfiedBy(new Version($dependency->getInstalledVersion()))) {
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'Plugin "%s" has version incompatible with "%s" requirements!',
|
||||
$dependency_name,
|
||||
$plugin->getName()
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
$can_be_loaded = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check that dependencies of the current dependency are satisfied
|
||||
if ($this->getDfsState($dependency) != self::DFS_VISITED) {
|
||||
$this->doVerificationStep($dependency);
|
||||
}
|
||||
if (!isset($this->loadablePlugins[$dependency_name])) {
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'Not all dependencies of "%s" plugin are satisfied!',
|
||||
$plugin->getName()
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
$can_be_loaded = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->setDfsState($plugin, self::DFS_VISITED);
|
||||
|
||||
if ($can_be_loaded) {
|
||||
$this->loadablePlugins[$plugin->getName()] = $plugin;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear state related with depth-first search algorithm for all plugins.
|
||||
*/
|
||||
protected function clearDfsState()
|
||||
{
|
||||
$this->dfsState = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get state related with depth-first search algorithm for a plugin.
|
||||
*
|
||||
* @param PluginInfo $plugin A plugin for which the state should be
|
||||
* retrieved.
|
||||
* @return string One of DependencyGraph::DFS_* constant.
|
||||
*/
|
||||
protected function getDfsState(PluginInfo $plugin)
|
||||
{
|
||||
return isset($this->dfsState[$plugin->getName()])
|
||||
? $this->dfsState[$plugin->getName()]
|
||||
: self::DFS_NOT_VISITED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets state related with depth-first search algorithm for a plugin.
|
||||
*
|
||||
* @param PluginInfo $plugin A plugin for which the state should be set.
|
||||
* @param string $state One of DependencyGraph::DFS_* constants.
|
||||
*/
|
||||
protected function setDfsState(PluginInfo $plugin, $state)
|
||||
{
|
||||
$this->dfsState[$plugin->getName()] = $state;
|
||||
}
|
||||
}
|
@ -19,6 +19,9 @@
|
||||
|
||||
namespace Mibew\Plugin;
|
||||
|
||||
use vierbergenlars\SemVer\version as Version;
|
||||
use vierbergenlars\SemVer\expression as VersionExpression;
|
||||
|
||||
/**
|
||||
* Provides a handy wrapper for plugin info.
|
||||
*/
|
||||
@ -36,6 +39,12 @@ class PluginInfo
|
||||
*/
|
||||
protected $pluginClass = null;
|
||||
|
||||
/**
|
||||
* The current state of the plugin.
|
||||
* @var State|null
|
||||
*/
|
||||
protected $pluginState = null;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
@ -56,6 +65,46 @@ class PluginInfo
|
||||
$this->pluginName = $plugin_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current state of the plugin.
|
||||
*
|
||||
* @return State
|
||||
*/
|
||||
public function getState()
|
||||
{
|
||||
if (is_null($this->pluginState)) {
|
||||
$state = State::loadByName($this->pluginName);
|
||||
if (!$state) {
|
||||
// There is no appropriate state in the database. Use a new one.
|
||||
$state = new State();
|
||||
$state->pluginName = $this->pluginName;
|
||||
$state->version = false;
|
||||
$state->installed = false;
|
||||
$state->enabled = false;
|
||||
}
|
||||
$this->pluginState = $state;
|
||||
}
|
||||
|
||||
return $this->pluginState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears state of the plugin attached to the info object.
|
||||
*
|
||||
* Also the method deletes state from database but only if it's stored
|
||||
* where.
|
||||
*/
|
||||
public function clearState()
|
||||
{
|
||||
if (!is_null($this->pluginState)) {
|
||||
if ($this->pluginState->id) {
|
||||
// Remove state only if it's in the database.
|
||||
$this->pluginState->delete();
|
||||
}
|
||||
$this->pluginState = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns fully qualified plugin's class.
|
||||
*
|
||||
@ -90,6 +139,20 @@ class PluginInfo
|
||||
return call_user_func(array($this->getClass(), 'getVersion'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns installed version of the plugin.
|
||||
*
|
||||
* Notice that in can differs from
|
||||
* {@link \Mibew\Plugin\PluginInfo::getVersion()} results if the plugin's
|
||||
* files are updated without database changes.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getInstalledVersion()
|
||||
{
|
||||
return $this->getState()->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns dependencies of the plugin.
|
||||
*
|
||||
@ -133,4 +196,115 @@ class PluginInfo
|
||||
|
||||
return new $plugin_class($configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the plugin is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return $this->getState()->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the plugin is installed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInstalled()
|
||||
{
|
||||
return $this->getState()->installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the plugin can be enabled.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function canBeEnabled()
|
||||
{
|
||||
if ($this->isEnabled()) {
|
||||
// The plugin cannot be enabled twice
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure all plugin's dependencies exist, are enabled and have
|
||||
// appropriate versions
|
||||
foreach ($this->getDependencies() as $plugin_name => $required_version) {
|
||||
if (!Utils::pluginExists($plugin_name)) {
|
||||
return false;
|
||||
}
|
||||
$plugin = new PluginInfo($plugin_name);
|
||||
if (!$plugin->isInstalled() || !$plugin->isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
$version_constrain = new VersionExpression($required_version);
|
||||
if (!$version_constrain->satisfiedBy(new Version($plugin->getInstalledVersion()))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the plugin can be disabled.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function canBeDisabled()
|
||||
{
|
||||
if (!$this->isEnabled()) {
|
||||
// The plugin was not enabled thus it cannot be disabled
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure that the plugin has no enabled dependent plugins.
|
||||
foreach ($this->getDependentPlugins() as $plugin_name) {
|
||||
$plugin = new PluginInfo($plugin_name);
|
||||
if ($plugin->isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the plugin can be uninstalled.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function canBeUninstalled()
|
||||
{
|
||||
if ($this->isEnabled()) {
|
||||
// Enabled plugin cannot be uninstalled
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure that the plugin has no installed dependent plugins.
|
||||
foreach ($this->getDependentPlugins() as $plugin_name) {
|
||||
$plugin = new PluginInfo($plugin_name);
|
||||
if ($plugin->isInstalled()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates plugin info object based on a state object.
|
||||
*
|
||||
* @param State $state A state of the plugin.
|
||||
* @return PluginInfo
|
||||
*/
|
||||
public static function fromState(State $state)
|
||||
{
|
||||
$info = new self($state->pluginName);
|
||||
$info->pluginState = $state;
|
||||
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +19,6 @@
|
||||
|
||||
namespace Mibew\Plugin;
|
||||
|
||||
use vierbergenlars\SemVer\version as Version;
|
||||
use vierbergenlars\SemVer\expression as VersionExpression;
|
||||
|
||||
/**
|
||||
* Manage plugins.
|
||||
*
|
||||
@ -92,110 +89,181 @@ class PluginManager
|
||||
/**
|
||||
* Loads plugins.
|
||||
*
|
||||
* The method checks dependences and plugin avaiulability before loading and
|
||||
* The method checks dependences and plugin availability before loading and
|
||||
* invokes PluginInterface::run() after loading.
|
||||
*
|
||||
* @param array $plugins_list List of plugins' names and configurations.
|
||||
* For example:
|
||||
* <code>
|
||||
* $plugins_list = array();
|
||||
* $plugins_list[] = array(
|
||||
* 'name' => 'vendor:plugin_name', // Obligatory value
|
||||
* 'config' => array( // Pass to plugin constructor
|
||||
* 'weight' => 100,
|
||||
* 'some_configurable_value' => 'value'
|
||||
* )
|
||||
* )
|
||||
* </code>
|
||||
* @param array $configs List of plugins' configurations. Each key is a
|
||||
* plugin name and each value is a configurations array.
|
||||
*
|
||||
* @see \Mibew\Plugin\PluginInterface::run()
|
||||
*/
|
||||
public function loadPlugins($plugins_list)
|
||||
public function loadPlugins($configs)
|
||||
{
|
||||
// Load plugins one by one
|
||||
$loading_queue = array();
|
||||
// Builds Dependency graph with available plugins.
|
||||
$graph = new DependencyGraph();
|
||||
foreach (State::loadAllEnabled() as $plugin_state) {
|
||||
if (!Utils::pluginExists($plugin_state->pluginName)) {
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'Plugin "%s" exists in database base but is not found in file system!',
|
||||
$plugin_state->pluginName
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$plugin_info = PluginInfo::fromState($plugin_state);
|
||||
if ($plugin_info->getVersion() != $plugin_info->getInstalledVersion()) {
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'Versions of "%s" plugin in database and in file system are different!'
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$graph->addPlugin($plugin_info);
|
||||
}
|
||||
|
||||
$offset = 0;
|
||||
foreach ($plugins_list as $plugin) {
|
||||
if (empty($plugin['name'])) {
|
||||
trigger_error("Plugin name is undefined!", E_USER_WARNING);
|
||||
continue;
|
||||
}
|
||||
$plugin_name = $plugin['name'];
|
||||
$plugin_config = isset($plugin['config']) ? $plugin['config'] : array();
|
||||
|
||||
// Get vendor name and short name from plugin's name
|
||||
if (!Utils::isValidPluginName($plugin_name)) {
|
||||
trigger_error(
|
||||
"Wrong formated plugin name '" . $plugin_name . "'!",
|
||||
E_USER_WARNING
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build name of the plugin class
|
||||
$plugin_classname = Utils::getPluginClassName($plugin_name);
|
||||
|
||||
// Check plugin class name
|
||||
if (!class_exists($plugin_classname)) {
|
||||
trigger_error(
|
||||
"Plugin class '{$plugin_classname}' is undefined!",
|
||||
E_USER_WARNING
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Check if plugin extends abstract 'Plugin' class
|
||||
if (!in_array('Mibew\\Plugin\\PluginInterface', class_implements($plugin_classname))) {
|
||||
$error_message = "Plugin class '{$plugin_classname}' does not "
|
||||
. "implement '\\Mibew\\Plugin\\PluginInterface' interface!";
|
||||
trigger_error($error_message, E_USER_WARNING);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check plugin dependencies
|
||||
$plugin_dependencies = call_user_func(array(
|
||||
$plugin_classname,
|
||||
'getDependencies',
|
||||
));
|
||||
foreach ($plugin_dependencies as $dependency => $required_version) {
|
||||
if (empty($this->loadedPlugins[$dependency])) {
|
||||
$error_message = "Plugin '{$dependency}' was not loaded "
|
||||
. "yet, but exists in '{$plugin_name}' dependencies list!";
|
||||
trigger_error($error_message, E_USER_WARNING);
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$version_constrain = new VersionExpression($required_version);
|
||||
$dependency_version = call_user_func(array(
|
||||
$this->loadedPlugins[$dependency],
|
||||
'getVersion'
|
||||
));
|
||||
|
||||
if (!$version_constrain->satisfiedBy(new Version($dependency_version))) {
|
||||
$error_message = "Plugin '{$dependency}' has version "
|
||||
. "incompatible with '{$plugin_name}' requirements!";
|
||||
trigger_error($error_message, E_USER_WARNING);
|
||||
$running_queue = array();
|
||||
foreach ($graph->getLoadingQueue() as $plugin_info) {
|
||||
// Make sure all depedendencies are loaded
|
||||
foreach (array_keys($plugin_info->getDependencies()) as $dependency) {
|
||||
if (!isset($this->loadedPlugins[$dependency])) {
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'Plugin "%s" was not loaded yet, but exists in "%s" dependencies list!',
|
||||
$dependency,
|
||||
$plugin_info->getName()
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Add plugin to loading queue
|
||||
$plugin_instance = new $plugin_classname($plugin_config);
|
||||
if ($plugin_instance->initialized()) {
|
||||
// Store plugin instance
|
||||
$this->loadedPlugins[$plugin_name] = $plugin_instance;
|
||||
$loading_queue[$plugin_instance->getWeight() . "_" . $offset] = $plugin_instance;
|
||||
// Try to load the plugin.
|
||||
$name = $plugin_info->getName();
|
||||
$config = isset($configs[$name]) ? $configs[$name] : array();
|
||||
$instance = $plugin_info->getInstance($config);
|
||||
if ($instance->initialized()) {
|
||||
// Store the plugin and add it to running queue
|
||||
$this->loadedPlugins[$name] = $instance;
|
||||
$running_queue[$instance->getWeight() . "_" . $offset] = $instance;
|
||||
$offset++;
|
||||
} else {
|
||||
// The plugin cannot be loaded. Just skip it.
|
||||
trigger_error(
|
||||
"Plugin '{$plugin_name}' was not initialized correctly!",
|
||||
"Plugin '{$name}' was not initialized correctly!",
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort queue in order to plugins' weights and run plugins one by one
|
||||
uksort($loading_queue, 'strnatcmp');
|
||||
foreach ($loading_queue as $plugin) {
|
||||
uksort($running_queue, 'strnatcmp');
|
||||
foreach ($running_queue as $plugin) {
|
||||
$plugin->run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to enable a plugin.
|
||||
*
|
||||
* @param string $plugin_name Name of the plugin to enable.
|
||||
* @return boolean Indicates if the plugin has been enabled or not.
|
||||
*/
|
||||
public function enable($plugin_name)
|
||||
{
|
||||
$plugin = new PluginInfo($plugin_name);
|
||||
|
||||
if ($plugin->isEnabled()) {
|
||||
// The plugin is already enabled. There is nothing we can do.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$plugin->canBeEnabled()) {
|
||||
// The plugin cannot be enabled.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$plugin->isInstalled()) {
|
||||
// Try to install the plugin.
|
||||
$plugin_class = $plugin->getClass();
|
||||
if (!$plugin_class::install()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Plugin installed successfully. Update the state
|
||||
$plugin->getState()->version = $plugin->getVersion();
|
||||
$plugin->getState()->installed = true;
|
||||
}
|
||||
|
||||
$plugin->getState()->enabled = true;
|
||||
$plugin->getState()->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to disable a plugin.
|
||||
*
|
||||
* @param string $plugin_name Name of the plugin to disable.
|
||||
* @return boolean Indicates if the plugin has been disabled or not.
|
||||
*/
|
||||
public function disable($plugin_name)
|
||||
{
|
||||
$plugin = new PluginInfo($plugin_name);
|
||||
|
||||
if (!$plugin->isEnabled()) {
|
||||
// The plugin is not enabled
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$plugin->canBeDisabled()) {
|
||||
// The plugin cannot be disabled
|
||||
return false;
|
||||
}
|
||||
|
||||
$plugin->getState()->enabled = false;
|
||||
$plugin->getState()->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to uninstall a plugin.
|
||||
*
|
||||
* @param string $plugin_name Name of the plugin to uninstall.
|
||||
* @return boolean Indicates if the plugin has been uninstalled or not.
|
||||
*/
|
||||
public function uninstall($plugin_name)
|
||||
{
|
||||
$plugin = new PluginInfo($plugin_name);
|
||||
|
||||
if (!$plugin->isInstalled()) {
|
||||
// The plugin was not installed
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$plugin->canBeUninstalled()) {
|
||||
// The plugin cannot be uninstalled.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to uninstall the plugin.
|
||||
$plugin_class = $plugin->getClass();
|
||||
if (!$plugin_class::uninstall()) {
|
||||
// Something went wrong. The plugin cannot be uninstalled.
|
||||
return false;
|
||||
}
|
||||
|
||||
// The plugin state is not needed anymore.
|
||||
$plugin->clearState();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
265
src/mibew/libs/classes/Mibew/Plugin/State.php
Normal file
265
src/mibew/libs/classes/Mibew/Plugin/State.php
Normal file
@ -0,0 +1,265 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is a part of Mibew Messenger.
|
||||
*
|
||||
* Copyright 2005-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
namespace Mibew\Plugin;
|
||||
|
||||
use Mibew\Database;
|
||||
|
||||
/**
|
||||
* Represents plugin's state that is stored in database.
|
||||
*/
|
||||
class State
|
||||
{
|
||||
/**
|
||||
* ID of the plugin's record in database.
|
||||
* @var int|boolean
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Name of the plugin.
|
||||
* @var string
|
||||
*/
|
||||
public $pluginName;
|
||||
|
||||
/**
|
||||
* Version of the plugin.
|
||||
* @var string
|
||||
*/
|
||||
public $version;
|
||||
|
||||
/**
|
||||
* Indicates if the plugin is installed or not.
|
||||
* @var boolean
|
||||
*/
|
||||
public $installed;
|
||||
|
||||
/**
|
||||
* Indicates if the plugin is enabled or not.
|
||||
* @var boolean
|
||||
*/
|
||||
public $enabled;
|
||||
|
||||
/**
|
||||
* Loads state by its ID.
|
||||
*
|
||||
* @param int $id ID of the state.
|
||||
* @return State|boolean An instance of state or boolean false on failure.
|
||||
*/
|
||||
public static function load($id)
|
||||
{
|
||||
// Check $id
|
||||
if (empty($id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load plugin state
|
||||
$info = Database::getInstance()->query(
|
||||
'SELECT * FROM {plugin} WHERE id = :id',
|
||||
array(':id' => $id),
|
||||
array('return_rows' => Database::RETURN_ONE_ROW)
|
||||
);
|
||||
|
||||
// There is no such record in database
|
||||
if (!$info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create and populate object
|
||||
$state = new self();
|
||||
$state->populateFromDbFields($info);
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads state by plugin's name.
|
||||
*
|
||||
* @param string $name Name of the plugin which state should be loaded.
|
||||
* @return State|boolean An instance of state or boolean false on failure.
|
||||
*/
|
||||
public static function loadByName($name)
|
||||
{
|
||||
if (!Utils::isValidPluginName($name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load plugin state
|
||||
$info = Database::getInstance()->query(
|
||||
'SELECT * FROM {plugin} WHERE name = :name',
|
||||
array(':name' => $name),
|
||||
array('return_rows' => Database::RETURN_ONE_ROW)
|
||||
);
|
||||
|
||||
// There is no such record in database
|
||||
if (!$info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create and populate object
|
||||
$state = new self();
|
||||
$state->populateFromDbFields($info);
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all states from database.
|
||||
*
|
||||
* @return State[] List of state objects.
|
||||
* @throws \RuntimeException If the data cannot be retrieved because of a
|
||||
* failure.
|
||||
*/
|
||||
public static function loadAll()
|
||||
{
|
||||
$rows = Database::getInstance()->query(
|
||||
"SELECT * FROM {plugin}",
|
||||
null,
|
||||
array('return_rows' => Database::RETURN_ALL_ROWS)
|
||||
);
|
||||
|
||||
if ($rows === false) {
|
||||
throw new \RuntimeException('Plugins list cannot be retrieved.');
|
||||
}
|
||||
|
||||
$states = array();
|
||||
foreach ($rows as $row) {
|
||||
$state = new self();
|
||||
$state->populateFromDbFields($row);
|
||||
$states[] = $state;
|
||||
}
|
||||
|
||||
return $states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads state for all enabled plugins.
|
||||
*
|
||||
* @return State[] List of state objects.
|
||||
* @throws \RuntimeException If the data cannot be retrieved because of a
|
||||
* failure.
|
||||
*/
|
||||
public static function loadAllEnabled()
|
||||
{
|
||||
$rows = Database::getInstance()->query(
|
||||
'SELECT * FROM {plugin} WHERE enabled = :enabled AND installed = :installed',
|
||||
array(
|
||||
':enabled' => 1,
|
||||
':installed' => 1,
|
||||
),
|
||||
array('return_rows' => Database::RETURN_ALL_ROWS)
|
||||
);
|
||||
|
||||
if ($rows === false) {
|
||||
throw new \RuntimeException('Plugins list cannot be retrieved.');
|
||||
}
|
||||
|
||||
$states = array();
|
||||
foreach ($rows as $row) {
|
||||
$state = new self();
|
||||
$state->populateFromDbFields($row);
|
||||
$states[] = $state;
|
||||
}
|
||||
|
||||
return $states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param string $plugin_name Name of the plugin the state belongs to.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Set default values
|
||||
$this->id = false;
|
||||
$this->pluginName = null;
|
||||
$this->version = null;
|
||||
$this->installed = false;
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the state to the database.
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
|
||||
if (!$this->id) {
|
||||
// This state is new.
|
||||
$db->query(
|
||||
("INSERT INTO {plugin} (name, version, installed, enabled) "
|
||||
. "VALUES (:name, :version, :installed, :enabled)"),
|
||||
array(
|
||||
':name' => $this->pluginName,
|
||||
':version' => $this->version,
|
||||
':installed' => (int)$this->installed,
|
||||
':enabled' => (int)$this->enabled,
|
||||
)
|
||||
);
|
||||
$this->id = $db->insertedId();
|
||||
} else {
|
||||
// Update existing state
|
||||
$db->query(
|
||||
("UPDATE {plugin} SET name = :name, version = :version, "
|
||||
. "installed = :installed, enabled = :enabled WHERE id = :id"),
|
||||
array(
|
||||
':id' => $this->id,
|
||||
':name' => $this->pluginName,
|
||||
':version' => $this->version,
|
||||
':installed' => (int)$this->installed,
|
||||
':enabled' => (int)$this->enabled,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a state from the database.
|
||||
*
|
||||
* @throws \RuntimeException If the state is not stored in the database.
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if (!$this->id) {
|
||||
throw new \RuntimeException('You cannot delete a plugin state without ID');
|
||||
}
|
||||
|
||||
Database::getInstance()->query(
|
||||
"DELETE FROM {plugin} WHERE id = :id LIMIT 1",
|
||||
array(':id' => $this->id)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates fields of the instance with values from database row.
|
||||
*
|
||||
* @param array $db_fields Associative array of database fields for the
|
||||
* state.
|
||||
*/
|
||||
protected function populateFromDbFields($db_fields)
|
||||
{
|
||||
$this->id = $db_fields['id'];
|
||||
$this->pluginName = $db_fields['name'];
|
||||
$this->version = $db_fields['version'];
|
||||
$this->enabled = (bool)$db_fields['enabled'];
|
||||
$this->installed = (bool)$db_fields['installed'];
|
||||
}
|
||||
}
|
@ -573,6 +573,11 @@ table.list td.level1{
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
table.list .disabled-link {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* awaiting */
|
||||
|
||||
table.awaiting {
|
||||
|
@ -32,6 +32,7 @@
|
||||
<li{{#ifEqual menuid "operators"}} class="active"{{/ifEqual}}><a href="{{route "operators"}}">{{l10n "Operators"}}</a></li>
|
||||
<li{{#ifEqual menuid "groups"}} class="active"{{/ifEqual}}><a href="{{route "groups"}}">{{l10n "Groups"}}</a></li>
|
||||
<li{{#ifEqual menuid "settings"}} class="active"{{/ifEqual}}><a href="{{route "settings_common"}}">{{l10n "Settings"}}</a></li>
|
||||
<li{{#ifEqual menuid "plugins"}} class="active"{{/ifEqual}}><a href="{{route "plugins"}}">{{l10n "Plugins"}}</a></li>
|
||||
<li{{#ifEqual menuid "styles"}} class="active"{{/ifEqual}}><a href="{{route "style_preview" type="page"}}">{{l10n "Styles"}}</a></li>
|
||||
<li{{#ifEqual menuid "translation"}} class="active"{{/ifEqual}}><a href="{{route "translations"}}">{{l10n "Localize"}}</a></li>
|
||||
<li{{#ifEqual menuid "mail_templates"}} class="active"{{/ifEqual}}><a href="{{route "mail_templates"}}">{{l10n "Mail templates"}}</a></li>
|
||||
|
@ -0,0 +1,63 @@
|
||||
{{#extends "_layout"}}
|
||||
{{#override "menu"}}{{> _menu}}{{/override}}
|
||||
|
||||
{{#override "content"}}
|
||||
{{l10n "Here you can manage plugins."}} {{l10n "Notice that plugins are configured via the main config file."}}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
{{> _errors}}
|
||||
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th>{{l10n "Name"}}</th>
|
||||
<th>{{l10n "Version"}}</th>
|
||||
<th>{{l10n "Dependencies"}}</th>
|
||||
<th>{{l10n "Edit"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each plugins}}
|
||||
<tr>
|
||||
<td class="notlast">{{name}}</td>
|
||||
<td class="notlast">{{version}}</td>
|
||||
<td class="notlast">
|
||||
{{#each dependencies}}{{#unless @first}}, {{/unless}}{{@key}}({{this}}){{/each}}
|
||||
</td>
|
||||
<td>
|
||||
{{#if enabled}}
|
||||
{{#if canBeDisabled}}
|
||||
<a href="{{csrfProtectedRoute "plugin_disable" plugin_name=name}}">{{l10n "disable"}}</a>
|
||||
{{else}}
|
||||
<span class="disabled-link" title="{{l10n "Disable all the dependencies first"}}">{{l10n "disable"}}</span>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if canBeEnabled}}
|
||||
<a href="{{csrfProtectedRoute "plugin_enable" plugin_name=name}}">{{l10n "enable"}}</a>
|
||||
{{else}}
|
||||
<span class="disabled-link" title="{{l10n "Enable all the dependencies first"}}">{{l10n "enable"}}</span>
|
||||
{{/if}}
|
||||
{{#if installed}}
|
||||
{{#if canBeUninstalled}}
|
||||
<a href="{{csrfProtectedRoute "plugin_uninstall" plugin_name=name}}" class="uninstall-link" data-plugin-name="{{name}}">{{l10n "uninstall"}}</a>
|
||||
{{else}}
|
||||
<span class="disabled-link" title="{{l10n "Uninstall all the dependencies first"}}">{{l10n "uninstall"}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
{{l10n "No elements"}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/override}}
|
||||
{{/extends}}
|
Loading…
Reference in New Issue
Block a user