Implement new plugin management system

This commit is contained in:
Dmitriy Simushev 2014-11-27 11:42:23 +00:00
parent 014a7fabcb
commit 80077be185
12 changed files with 1278 additions and 96 deletions

View File

@ -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]

View File

@ -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"

View File

@ -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

View 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);

View 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;
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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'];
}
}

View File

@ -573,6 +573,11 @@ table.list td.level1{
padding-left: 20px;
}
table.list .disabled-link {
cursor: pointer;
text-decoration: none;
}
/* awaiting */
table.awaiting {

View File

@ -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>

View File

@ -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}}