看板初始化提交

This commit is contained in:
zephyr
2026-06-01 21:23:12 -07:00
commit 54a842f4ab
2104 changed files with 241695 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
<?php
namespace Kanboard\Core\Plugin;
/**
* Plugin Base class
*
* @package Kanboard\Core\Plugin
* @author Frederic Guillot
*/
abstract class Base extends \Kanboard\Core\Base
{
/**
* Method called for each request
*
* @abstract
* @access public
*/
abstract public function initialize();
/**
* Override default CSP rules
*
* @access public
* @param array $rules
*/
public function setContentSecurityPolicy(array $rules)
{
$this->container['cspRules'] = $rules;
}
/**
* Returns all classes that needs to be stored in the DI container
*
* @access public
* @return array
*/
public function getClasses()
{
return array();
}
/**
* Returns all helper classes that needs to be stored in the DI container
*
* @access public
* @return array
*/
public function getHelpers()
{
return array();
}
/**
* Listen on internal events
*
* @access public
* @param string $event
* @param callable $callback
*/
public function on($event, $callback)
{
$container = $this->container;
$this->dispatcher->addListener($event, function () use ($container, $callback) {
call_user_func($callback, $container);
});
}
/**
* Get plugin name
*
* This method should be overridden by your Plugin class
*
* @access public
* @return string
*/
public function getPluginName()
{
return ucfirst(substr(get_called_class(), 16, -7));
}
/**
* Get plugin description
*
* This method should be overridden by your Plugin class
*
* @access public
* @return string
*/
public function getPluginDescription()
{
return '';
}
/**
* Get plugin author
*
* This method should be overridden by your Plugin class
*
* @access public
* @return string
*/
public function getPluginAuthor()
{
return '?';
}
/**
* Get plugin version
*
* This method should be overridden by your Plugin class
*
* @access public
* @return string
*/
public function getPluginVersion()
{
return '?';
}
/**
* Get plugin homepage
*
* This method should be overridden by your Plugin class
*
* @access public
* @return string
*/
public function getPluginHomepage()
{
return '';
}
/**
* Get application compatibility version
*
* Examples: >=1.0.36, 1.0.37, APP_VERSION
*
* @access public
* @return string
*/
public function getCompatibleVersion()
{
return APP_VERSION;
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Kanboard\Core\Plugin;
use Kanboard\Core\Base as BaseCore;
/**
* Class Directory
*
* @package Kanboard\Core\Plugin
* @author Frederic Guillot
*/
class Directory extends BaseCore
{
/**
* Get all plugins available
*
* @access public
* @param string $url
* @return array
*/
public function getAvailablePlugins($url = PLUGIN_API_URL)
{
$plugins = $this->httpClient->getJson($url);
$plugins = array_filter($plugins, array($this, 'isCompatible'));
$plugins = array_filter($plugins, array($this, 'isInstallable'));
return $plugins;
}
/**
* Filter plugins
*
* @param array $plugin
* @param string $appVersion
* @return bool
*/
public function isCompatible(array $plugin, $appVersion = APP_VERSION)
{
return Version::isCompatible($plugin['compatible_version'], $appVersion);
}
/**
* Filter plugins
*
* @param array $plugin
* @return bool
*/
public function isInstallable(array $plugin)
{
return $plugin['remote_install'];
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
namespace Kanboard\Core\Plugin;
/**
* Plugin Hooks Handler
*
* @package Kanboard\Core\Plugin
* @author Frederic Guillot
*/
class Hook
{
/**
* List of hooks
*
* @access private
* @var array
*/
private $hooks = array();
/**
* Bind something on a hook
*
* @access public
* @param string $hook
* @param mixed $value
*/
public function on($hook, $value)
{
if (! isset($this->hooks[$hook])) {
$this->hooks[$hook] = array();
}
$this->hooks[$hook][] = $value;
}
/**
* Get all bindings for a hook
*
* @access public
* @param string $hook
* @return array
*/
public function getListeners($hook)
{
return isset($this->hooks[$hook]) ? $this->hooks[$hook] : array();
}
/**
* Return true if the hook is used
*
* @access public
* @param string $hook
* @return boolean
*/
public function exists($hook)
{
return isset($this->hooks[$hook]);
}
/**
* Merge listener results with input array
*
* @access public
* @param string $hook
* @param array $values
* @param array $params
* @return array
*/
public function merge($hook, array &$values, array $params = array())
{
foreach ($this->getListeners($hook) as $listener) {
$result = call_user_func_array($listener, $params);
if (is_array($result) && ! empty($result)) {
$values = array_merge($values, $result);
}
}
return $values;
}
/**
* Execute only first listener
*
* @access public
* @param string $hook
* @param array $params
* @return mixed
*/
public function first($hook, array $params = array())
{
foreach ($this->getListeners($hook) as $listener) {
return call_user_func_array($listener, $params);
}
return null;
}
/**
* Hook with reference
*
* @access public
* @param string $hook
* @param mixed $param
* @return mixed
*/
public function reference($hook, &$param)
{
foreach ($this->getListeners($hook) as $listener) {
$listener($param);
}
return $param;
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
namespace Kanboard\Core\Plugin;
use ZipArchive;
use Kanboard\Core\Tool;
/**
* Class Installer
*
* @package Kanboard\Core\Plugin
* @author Frederic Guillot
*/
class Installer extends \Kanboard\Core\Base
{
/**
* Return true if Kanboard is configured to install plugins
*
* @static
* @access public
* @return bool
*/
public static function isConfigured()
{
return PLUGIN_INSTALLER && is_writable(PLUGINS_DIR) && extension_loaded('zip');
}
/**
* Install a plugin
*
* @access public
* @param string $archiveUrl
* @throws PluginInstallerException
*/
public function install($archiveUrl)
{
$zip = $this->downloadPluginArchive($archiveUrl);
if (! $zip->extractTo(PLUGINS_DIR)) {
$this->cleanupArchive($zip);
throw new PluginInstallerException(t('Unable to extract plugin archive.'));
}
$this->cleanupArchive($zip);
}
/**
* Uninstall a plugin
*
* @access public
* @param string $pluginId
* @throws PluginInstallerException
*/
public function uninstall($pluginId)
{
$pluginFolder = PLUGINS_DIR.DIRECTORY_SEPARATOR.basename($pluginId);
if (! file_exists($pluginFolder)) {
throw new PluginInstallerException(t('Plugin not found.'));
}
if (! is_writable($pluginFolder)) {
throw new PluginInstallerException(e('You don\'t have the permission to remove this plugin.'));
}
Tool::removeAllFiles($pluginFolder);
}
/**
* Update a plugin
*
* @access public
* @param string $archiveUrl
* @throws PluginInstallerException
*/
public function update($archiveUrl)
{
$zip = $this->downloadPluginArchive($archiveUrl);
$firstEntry = $zip->statIndex(0);
$this->uninstall($firstEntry['name']);
if (! $zip->extractTo(PLUGINS_DIR)) {
$this->cleanupArchive($zip);
throw new PluginInstallerException(t('Unable to extract plugin archive.'));
}
$this->cleanupArchive($zip);
}
/**
* Download archive from URL
*
* @access protected
* @param string $archiveUrl
* @return ZipArchive
* @throws PluginInstallerException
*/
protected function downloadPluginArchive($archiveUrl)
{
if (!preg_match('/^https?:\/\//', $archiveUrl) || !filter_var($archiveUrl, FILTER_VALIDATE_URL)) {
throw new PluginInstallerException(t('This URL is invalid'));
}
$archiveData = $this->httpClient->get($archiveUrl);
$archiveFile = tempnam(ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir(), 'kb_plugin');
if (empty($archiveData)) {
unlink($archiveFile);
throw new PluginInstallerException(t('Unable to download plugin archive.'));
}
if (file_put_contents($archiveFile, $archiveData) === false) {
unlink($archiveFile);
throw new PluginInstallerException(t('Unable to write temporary file for plugin.'));
}
$zip = new ZipArchive();
if ($zip->open($archiveFile) !== true) {
unlink($archiveFile);
throw new PluginInstallerException(t('Unable to open plugin archive.'));
}
if ($zip->numFiles === 0) {
unlink($archiveFile);
throw new PluginInstallerException(t('There is no file in the plugin archive.'));
}
return $zip;
}
/**
* Remove archive file
*
* @access protected
* @param ZipArchive $zip
*/
protected function cleanupArchive(ZipArchive $zip)
{
$filename = $zip->filename;
$zip->close();
unlink($filename);
}
}
+137
View File
@@ -0,0 +1,137 @@
<?php
namespace Kanboard\Core\Plugin;
use Composer\Autoload\ClassLoader;
use DirectoryIterator;
use Exception;
use LogicException;
use Kanboard\Core\Tool;
/**
* Plugin Loader
*
* @package Kanboard\Core\Plugin
* @author Frederic Guillot
*/
class Loader extends \Kanboard\Core\Base
{
/**
* Plugin instances
*
* @access protected
* @var array
*/
protected $plugins = array();
protected $incompatiblePlugins = array();
/**
* Get list of loaded plugins
*
* @access public
* @return Base[]
*/
public function getPlugins()
{
return $this->plugins;
}
/**
* Get list of not compatible plugins
*
* @access public
* @return Base[]
*/
public function getIncompatiblePlugins()
{
return $this->incompatiblePlugins;
}
/**
* Scan plugin folder and load plugins
*
* @access public
*/
public function scan()
{
if (file_exists(PLUGINS_DIR)) {
$loader = new ClassLoader();
$loader->addPsr4('Kanboard\Plugin\\', PLUGINS_DIR);
$loader->register();
$dir = new DirectoryIterator(PLUGINS_DIR);
foreach ($dir as $fileInfo) {
if ($fileInfo->isDir() && substr($fileInfo->getFilename(), 0, 1) !== '.') {
$pluginName = $fileInfo->getFilename();
$this->initializePlugin($pluginName);
}
}
}
}
/**
* Load plugin schema
*
* @access public
* @param string $pluginName
*/
public function loadSchema($pluginName)
{
if (SchemaHandler::hasSchema($pluginName)) {
$schemaHandler = new SchemaHandler($this->container);
$schemaHandler->loadSchema($pluginName);
}
}
/**
* Load plugin
*
* @access public
* @throws LogicException
* @param string $pluginName
* @return Base
*/
public function loadPlugin($pluginName)
{
$className = '\Kanboard\Plugin\\'.$pluginName.'\\Plugin';
if (! class_exists($className)) {
throw new LogicException('Unable to load this plugin class: '.$className);
}
return new $className($this->container);
}
/**
* Initialize plugin
*
* @access public
* @param string $pluginName
*/
public function initializePlugin($pluginName)
{
try {
$plugin = $this->loadPlugin($pluginName);
if (Version::isCompatible($plugin->getCompatibleVersion(), APP_VERSION)) {
$this->loadSchema($pluginName);
if (method_exists($plugin, 'onStartup')) {
$this->dispatcher->addListener('app.bootstrap', array($plugin, 'onStartup'));
}
Tool::buildDIC($this->container, $plugin->getClasses());
Tool::buildDICHelpers($this->container, $plugin->getHelpers());
$plugin->initialize();
$this->plugins[$pluginName] = $plugin;
} else {
$this->incompatiblePlugins[$pluginName] = $plugin;
$this->logger->error($pluginName.' is not compatible with this version');
}
} catch (Exception $e) {
$this->logger->critical($pluginName.': '.$e->getMessage());
}
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Kanboard\Core\Plugin;
use Exception;
/**
* Class PluginException
*
* @package Kanboard\Core\Plugin
* @author Frederic Guillot
*/
class PluginException extends Exception
{
}
@@ -0,0 +1,13 @@
<?php
namespace Kanboard\Core\Plugin;
/**
* Class PluginInstallerException
*
* @package Kanboard\Core\Plugin
* @author Frederic Guillot
*/
class PluginInstallerException extends PluginException
{
}
+122
View File
@@ -0,0 +1,122 @@
<?php
namespace Kanboard\Core\Plugin;
use PDOException;
use RuntimeException;
/**
* Class SchemaHandler
*
* @package Kanboard\Core\Plugin
* @author Frederic Guillot
*/
class SchemaHandler extends \Kanboard\Core\Base
{
/**
* Schema version table for plugins
*
* @var string
*/
const TABLE_SCHEMA = 'plugin_schema_versions';
/**
* Get schema filename
*
* @static
* @access public
* @param string $pluginName
* @return string
*/
public static function getSchemaFilename($pluginName)
{
return PLUGINS_DIR.'/'.$pluginName.'/Schema/'.ucfirst(DB_DRIVER).'.php';
}
/**
* Return true if the plugin has schema
*
* @static
* @access public
* @param string $pluginName
* @return boolean
*/
public static function hasSchema($pluginName)
{
return file_exists(self::getSchemaFilename($pluginName));
}
/**
* Load plugin schema
*
* @access public
* @param string $pluginName
*/
public function loadSchema($pluginName)
{
require_once self::getSchemaFilename($pluginName);
$this->migrateSchema($pluginName);
}
/**
* Execute plugin schema migrations
*
* @access public
* @param string $pluginName
*/
public function migrateSchema($pluginName)
{
$lastVersion = constant('\Kanboard\Plugin\\'.$pluginName.'\Schema\VERSION');
$currentVersion = $this->getSchemaVersion($pluginName);
try {
$this->db->startTransaction();
$this->db->getDriver()->disableForeignKeys();
for ($i = $currentVersion + 1; $i <= $lastVersion; $i++) {
$functionName = '\Kanboard\Plugin\\'.$pluginName.'\Schema\version_'.$i;
if (function_exists($functionName)) {
call_user_func($functionName, $this->db->getConnection());
}
}
$this->db->getDriver()->enableForeignKeys();
$this->db->closeTransaction();
$this->setSchemaVersion($pluginName, $i - 1);
} catch (PDOException $e) {
$this->db->cancelTransaction();
$this->db->getDriver()->enableForeignKeys();
throw new RuntimeException('Unable to migrate schema for the plugin: '.$pluginName.' => '.$e->getMessage());
}
}
/**
* Get current plugin schema version
*
* @access public
* @param string $plugin
* @return integer
*/
public function getSchemaVersion($plugin)
{
return (int) $this->db->table(self::TABLE_SCHEMA)->eq('plugin', strtolower($plugin))->findOneColumn('version');
}
/**
* Save last plugin schema version
*
* @access public
* @param string $plugin
* @param integer $version
* @return boolean
*/
public function setSchemaVersion($plugin, $version)
{
$dictionary = array(
strtolower($plugin) => $version
);
return $this->db->getDriver()->upsert(self::TABLE_SCHEMA, 'plugin', 'version', $dictionary);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Kanboard\Core\Plugin;
/**
* Class Version
*
* @package Kanboard\Core\Plugin
* @author Frederic Guillot
*/
class Version
{
/**
* Check plugin version compatibility with application version
*
* @param string $pluginCompatibleVersion
* @param string $appVersion
* @return bool
*/
public static function isCompatible($pluginCompatibleVersion, $appVersion = APP_VERSION)
{
if (strpos($appVersion, 'master') !== false || strpos($appVersion, 'main') !== false) {
return true;
}
$appVersion = str_replace('v', '', $appVersion);
$pluginCompatibleVersion = str_replace('v', '', $pluginCompatibleVersion);
foreach (array('>=', '>', '<=', '<') as $operator) {
if (strpos($pluginCompatibleVersion, $operator) === 0) {
$pluginVersion = substr($pluginCompatibleVersion, strlen($operator));
return version_compare($appVersion, $pluginVersion, $operator);
}
}
return $pluginCompatibleVersion === $appVersion;
}
}