summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2015-09-13 14:07:56 -0400
committerFrederic Guillot <fred@kanboard.net>2015-09-13 14:07:56 -0400
commita6a00a00400f164c4b18094999a5ed72366dd519 (patch)
tree7d2bfc3fe4a36649f9092463228f4553979aef94 /app
parentc405f99fc8b7420b3e69c633b3259756a1ceb2f2 (diff)
First draft for plugins system
Diffstat (limited to 'app')
-rw-r--r--app/Core/PluginBase.php31
-rw-r--r--app/Core/PluginLoader.php144
-rw-r--r--app/Core/Router.php46
-rw-r--r--app/Core/Template.php52
-rw-r--r--app/Core/Tool.php23
-rw-r--r--app/Core/Translator.php23
-rw-r--r--app/Helper/Hook.php49
-rw-r--r--app/Model/Acl.php12
-rw-r--r--app/Schema/Mysql.php13
-rw-r--r--app/Schema/Postgres.php12
-rw-r--r--app/Schema/Sqlite.php12
-rw-r--r--app/ServiceProvider/ClassProvider.php13
-rw-r--r--app/Template/app/sidebar.php1
-rw-r--r--app/Template/config/sidebar.php1
-rw-r--r--app/Template/export/sidebar.php1
-rw-r--r--app/Template/header.php33
-rw-r--r--app/Template/layout.php42
-rw-r--r--app/Template/project/dropdown.php34
-rw-r--r--app/Template/project/sidebar.php2
-rw-r--r--app/Template/project_user/sidebar.php2
-rw-r--r--app/Template/task/sidebar.php4
-rw-r--r--app/Template/user/sidebar.php4
-rw-r--r--app/common.php120
-rw-r--r--app/routes.php117
24 files changed, 555 insertions, 236 deletions
diff --git a/app/Core/PluginBase.php b/app/Core/PluginBase.php
new file mode 100644
index 00000000..9c3d6e32
--- /dev/null
+++ b/app/Core/PluginBase.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Core;
+
+/**
+ * Plugin Base class
+ *
+ * @package core
+ * @author Frederic Guillot
+ */
+abstract class PluginBase extends Base
+{
+ /**
+ * Method called for each request
+ *
+ * @abstract
+ * @access public
+ */
+ abstract public function initialize();
+
+ /**
+ * Returns all classes that needs to be stored in the DI container
+ *
+ * @access public
+ * @return array
+ */
+ public function getClasses()
+ {
+ return array();
+ }
+}
diff --git a/app/Core/PluginLoader.php b/app/Core/PluginLoader.php
new file mode 100644
index 00000000..6030ded4
--- /dev/null
+++ b/app/Core/PluginLoader.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace Core;
+
+use DirectoryIterator;
+use PDOException;
+
+/**
+ * Plugin Loader
+ *
+ * @package core
+ * @author Frederic Guillot
+ */
+class PluginLoader extends Base
+{
+ /**
+ * Schema version table for plugins
+ *
+ * @var string
+ */
+ const TABLE_SCHEMA = 'plugin_schema_versions';
+
+ /**
+ * Plugin folder
+ *
+ * @var string
+ */
+ const PATH = __DIR__.'/../../plugins';
+
+ /**
+ * Scan plugin folder and load plugins
+ *
+ * @access public
+ */
+ public function scan()
+ {
+ if (file_exists(self::PATH)) {
+ $dir = new DirectoryIterator(self::PATH);
+
+ foreach ($dir as $fileinfo) {
+ if (! $fileinfo->isDot() && $fileinfo->isDir()) {
+ $plugin = $fileinfo->getFilename();
+ $this->loadSchema($plugin);
+ $this->load($plugin);
+ }
+ }
+ }
+ }
+
+ /**
+ * Load plugin
+ *
+ * @access public
+ */
+ public function load($plugin)
+ {
+ $class = '\Plugin\\'.$plugin.'\\Plugin';
+ $instance = new $class($this->container);
+
+ Tool::buildDic($this->container, $instance->getClasses());
+
+ $instance->initialize();
+ }
+
+ /**
+ * Load plugin schema
+ *
+ * @access public
+ * @param string $plugin
+ */
+ public function loadSchema($plugin)
+ {
+ $filename = __DIR__.'/../../plugins/'.$plugin.'/Schema/'.ucfirst(DB_DRIVER).'.php';
+
+ if (file_exists($filename)) {
+ require($filename);
+ $this->migrateSchema($plugin);
+ }
+ }
+
+ /**
+ * Execute plugin schema migrations
+ *
+ * @access public
+ * @param string $plugin
+ */
+ public function migrateSchema($plugin)
+ {
+ $last_version = constant('\Plugin\\'.$plugin.'\Schema\VERSION');
+ $current_version = $this->getSchemaVersion($plugin);
+
+ try {
+
+ $this->db->startTransaction();
+ $this->db->getDriver()->disableForeignKeys();
+
+ for ($i = $current_version + 1; $i <= $last_version; $i++) {
+ $function_name = '\Plugin\\'.$plugin.'\Schema\version_'.$i;
+
+ if (function_exists($function_name)) {
+ call_user_func($function_name, $this->db->getConnection());
+ }
+ }
+
+ $this->db->getDriver()->enableForeignKeys();
+ $this->db->closeTransaction();
+ $this->setSchemaVersion($plugin, $i - 1);
+ }
+ catch (PDOException $e) {
+ $this->db->cancelTransaction();
+ $this->db->getDriver()->enableForeignKeys();
+ die('Unable to migrate schema for the plugin: '.$plugin.' => '.$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);
+ }
+}
diff --git a/app/Core/Router.php b/app/Core/Router.php
index 6e7576d6..36bbfd55 100644
--- a/app/Core/Router.php
+++ b/app/Core/Router.php
@@ -213,49 +213,17 @@ class Router extends Base
if (! empty($_GET['controller']) && ! empty($_GET['action'])) {
$controller = $this->sanitize($_GET['controller'], 'app');
$action = $this->sanitize($_GET['action'], 'index');
+ $plugin = ! empty($_GET['plugin']) ? $this->sanitize($_GET['plugin'], '') : '';
}
else {
- list($controller, $action) = $this->findRoute($this->getPath($uri, $query_string));
+ list($controller, $action) = $this->findRoute($this->getPath($uri, $query_string)); // TODO: add plugin for routes
+ $plugin = '';
}
- return $this->load(
- __DIR__.'/../Controller/'.ucfirst($controller).'.php',
- $controller,
- '\Controller\\'.ucfirst($controller),
- $action
- );
- }
-
- /**
- * Load a controller and execute the action
- *
- * @access private
- * @param string $filename
- * @param string $controller
- * @param string $class
- * @param string $method
- * @return bool
- */
- private function load($filename, $controller, $class, $method)
- {
- if (file_exists($filename)) {
-
- require $filename;
-
- if (! method_exists($class, $method)) {
- return false;
- }
-
- $this->action = $method;
- $this->controller = $controller;
-
- $instance = new $class($this->container);
- $instance->beforeAction($controller, $method);
- $instance->$method();
-
- return true;
- }
+ $class = empty($plugin) ? '\Controller\\'.ucfirst($controller) : '\Plugin\\'.ucfirst($plugin).'\Controller\\'.ucfirst($controller);
- return false;
+ $instance = new $class($this->container);
+ $instance->beforeAction($controller, $action);
+ $instance->$action();
}
}
diff --git a/app/Core/Template.php b/app/Core/Template.php
index ba869ee6..b75f7da1 100644
--- a/app/Core/Template.php
+++ b/app/Core/Template.php
@@ -13,11 +13,12 @@ use LogicException;
class Template extends Helper
{
/**
- * Template path
+ * List of template overrides
*
- * @var string
+ * @access private
+ * @var array
*/
- const PATH = 'app/Template/';
+ private $overrides = array();
/**
* Render a template
@@ -33,16 +34,10 @@ class Template extends Helper
*/
public function render($__template_name, array $__template_args = array())
{
- $__template_file = self::PATH.$__template_name.'.php';
-
- if (! file_exists($__template_file)) {
- throw new LogicException('Unable to load the template: "'.$__template_name.'"');
- }
-
extract($__template_args);
ob_start();
- include $__template_file;
+ include $this->getTemplateFile($__template_name);
return ob_get_clean();
}
@@ -62,4 +57,41 @@ class Template extends Helper
$template_args + array('content_for_layout' => $this->render($template_name, $template_args))
);
}
+
+ /**
+ * Define a new template override
+ *
+ * @access public
+ * @param string $original_template
+ * @param string $new_template
+ */
+ public function setTemplateOverride($original_template, $new_template)
+ {
+ $this->overrides[$original_template] = $new_template;
+ }
+
+ /**
+ * Find template filename
+ *
+ * Core template name: 'task/show'
+ * Plugin template name: 'myplugin:task/show'
+ *
+ * @access public
+ * @param string $template_name
+ * @return string
+ */
+ public function getTemplateFile($template_name)
+ {
+ $template_name = isset($this->overrides[$template_name]) ? $this->overrides[$template_name] : $template_name;
+
+ if (strpos($template_name, ':') !== false) {
+ list($plugin, $template) = explode(':', $template_name);
+ $path = __DIR__.'/../../plugins/'.ucfirst($plugin).'/Template/'.$template.'.php';
+ }
+ else {
+ $path = __DIR__.'/../Template/'.$template_name.'.php';
+ }
+
+ return $path;
+ }
}
diff --git a/app/Core/Tool.php b/app/Core/Tool.php
index 84e42ba8..7939a80e 100644
--- a/app/Core/Tool.php
+++ b/app/Core/Tool.php
@@ -2,6 +2,8 @@
namespace Core;
+use Pimple\Container;
+
/**
* Tool class
*
@@ -23,7 +25,6 @@ class Tool
$fp = fopen($filename, 'w');
if (is_resource($fp)) {
-
foreach ($rows as $fields) {
fputcsv($fp, $fields);
}
@@ -51,4 +52,24 @@ class Tool
return $identifier;
}
+
+ /**
+ * Build dependency injection container from an array
+ *
+ * @static
+ * @access public
+ * @param Container $container
+ * @param array $namespaces
+ */
+ public static function buildDIC(Container $container, array $namespaces)
+ {
+ foreach ($namespaces as $namespace => $classes) {
+ foreach ($classes as $name) {
+ $class = '\\'.$namespace.'\\'.$name;
+ $container[lcfirst($name)] = function ($c) use ($class) {
+ return new $class($c);
+ };
+ }
+ }
+ }
}
diff --git a/app/Core/Translator.php b/app/Core/Translator.php
index e3d19692..e9aa1f3f 100644
--- a/app/Core/Translator.php
+++ b/app/Core/Translator.php
@@ -15,7 +15,7 @@ class Translator
*
* @var string
*/
- const PATH = 'app/Locale/';
+ const PATH = 'app/Locale';
/**
* Locale
@@ -196,18 +196,27 @@ class Translator
* @static
* @access public
* @param string $language Locale code: fr_FR
+ * @param string $path Locale folder
*/
- public static function load($language)
+ public static function load($language, $path = self::PATH)
{
setlocale(LC_TIME, $language.'.UTF-8', $language);
- $filename = self::PATH.$language.DIRECTORY_SEPARATOR.'translations.php';
+ $filename = $path.DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.'translations.php';
if (file_exists($filename)) {
- self::$locales = require $filename;
- }
- else {
- self::$locales = array();
+ self::$locales = array_merge(self::$locales, require($filename));
}
}
+
+ /**
+ * Clear locales stored in memory
+ *
+ * @static
+ * @access public
+ */
+ public static function unload()
+ {
+ self::$locales = array();
+ }
}
diff --git a/app/Helper/Hook.php b/app/Helper/Hook.php
new file mode 100644
index 00000000..77756757
--- /dev/null
+++ b/app/Helper/Hook.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Helper;
+
+/**
+ * Template Hook helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class Hook extends \Core\Base
+{
+ private $hooks = array();
+
+ /**
+ * Render all attached hooks
+ *
+ * @access public
+ * @param string $hook
+ * @param array $variables
+ * @return string
+ */
+ public function render($hook, array $variables = array())
+ {
+ $buffer = '';
+
+ foreach ($this->hooks as $name => $template) {
+ if ($hook === $name) {
+ $buffer .= $this->template->render($template, $variables);
+ }
+ }
+
+ return $buffer;
+ }
+
+ /**
+ * Attach a template to a hook
+ *
+ * @access public
+ * @param string $hook
+ * @param string $template
+ * @return \Helper\Hook
+ */
+ public function attach($hook, $template)
+ {
+ $this->hooks[$hook] = $template;
+ return $this;
+ }
+}
diff --git a/app/Model/Acl.php b/app/Model/Acl.php
index 8c28cb1a..6042bc29 100644
--- a/app/Model/Acl.php
+++ b/app/Model/Acl.php
@@ -95,6 +95,18 @@ class Acl extends Base
);
/**
+ * Extend ACL rules
+ *
+ * @access public
+ * @param string $acl_name
+ * @param aray $rules
+ */
+ public function extend($acl_name, array $rules)
+ {
+ $this->$acl_name = array_merge($this->$acl_name, $rules);
+ }
+
+ /**
* Return true if the specified controller/action match the given acl
*
* @access public
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index efdb159b..5a12bb3c 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,18 @@ use PDO;
use Core\Security;
use Model\Link;
-const VERSION = 86;
+const VERSION = 87;
+
+function version_87($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE plugin_schema_versions (
+ plugin VARCHAR(80) NOT NULL,
+ version INT NOT NULL DEFAULT 0,
+ PRIMARY KEY(plugin)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+}
function version_86($pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index a5d28dcf..ad460cc7 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,17 @@ use PDO;
use Core\Security;
use Model\Link;
-const VERSION = 66;
+const VERSION = 67;
+
+function version_67($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE plugin_schema_versions (
+ plugin VARCHAR(80) NOT NULL PRIMARY KEY,
+ version INTEGER NOT NULL DEFAULT 0
+ )
+ ");
+}
function version_66($pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index 8efa016c..16fe0649 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,17 @@ use Core\Security;
use PDO;
use Model\Link;
-const VERSION = 82;
+const VERSION = 83;
+
+function version_83($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE plugin_schema_versions (
+ plugin TEXT NOT NULL PRIMARY KEY,
+ version INTEGER NOT NULL DEFAULT 0
+ )
+ ");
+}
function version_82($pdo)
{
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index b570f5cf..53bddc1b 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -4,6 +4,7 @@ namespace ServiceProvider;
use Core\Paginator;
use Core\OAuth2;
+use Core\Tool;
use Model\Config;
use Model\Project;
use Model\Webhook;
@@ -94,17 +95,7 @@ class ClassProvider implements ServiceProviderInterface
public function register(Container $container)
{
- foreach ($this->classes as $namespace => $classes) {
-
- foreach ($classes as $name) {
-
- $class = '\\'.$namespace.'\\'.$name;
-
- $container[lcfirst($name)] = function ($c) use ($class) {
- return new $class($c);
- };
- }
- }
+ Tool::buildDIC($container, $this->classes);
$container['paginator'] = $container->factory(function ($c) {
return new Paginator($c);
diff --git a/app/Template/app/sidebar.php b/app/Template/app/sidebar.php
index 2d966009..f4a455f8 100644
--- a/app/Template/app/sidebar.php
+++ b/app/Template/app/sidebar.php
@@ -19,6 +19,7 @@
<li <?= $this->app->getRouterAction() === 'activity' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('My activity stream'), 'app', 'activity', array('user_id' => $user['id'])) ?>
</li>
+ <?= $this->hook->render('dashboard:sidebar') ?>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
diff --git a/app/Template/config/sidebar.php b/app/Template/config/sidebar.php
index 3617979a..083da283 100644
--- a/app/Template/config/sidebar.php
+++ b/app/Template/config/sidebar.php
@@ -34,6 +34,7 @@
<li>
<?= $this->url->link(t('Documentation'), 'doc', 'show') ?>
</li>
+ <?= $this->hook->render('config:sidebar') ?>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
diff --git a/app/Template/export/sidebar.php b/app/Template/export/sidebar.php
index f204d29d..7e39a5af 100644
--- a/app/Template/export/sidebar.php
+++ b/app/Template/export/sidebar.php
@@ -13,6 +13,7 @@
<li <?= $this->app->getRouterAction() === 'summary' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Daily project summary'), 'export', 'summary', array('project_id' => $project['id'])) ?>
</li>
+ <?= $this->hook->render('export:sidebar') ?>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
diff --git a/app/Template/header.php b/app/Template/header.php
new file mode 100644
index 00000000..0bcfdbbc
--- /dev/null
+++ b/app/Template/header.php
@@ -0,0 +1,33 @@
+<header>
+ <nav>
+ <h1><?= $this->url->link('K<span>B</span>', 'app', 'index', array(), false, 'logo', t('Dashboard')).' '.$this->e($title) ?>
+ <?php if (! empty($description)): ?>
+ <span class="tooltip" title='<?= $this->e($this->text->markdown($description)) ?>'>
+ <i class="fa fa-info-circle"></i>
+ </span>
+ <?php endif ?>
+ </h1>
+ <ul>
+ <?php if (isset($board_selector) && ! empty($board_selector)): ?>
+ <li>
+ <select id="board-selector"
+ class="chosen-select select-auto-redirect"
+ tabindex="-1"
+ data-notfound="<?= t('No results match:') ?>"
+ data-placeholder="<?= t('Display another project') ?>"
+ data-redirect-regex="PROJECT_ID"
+ data-redirect-url="<?= $this->url->href('board', 'show', array('project_id' => 'PROJECT_ID')) ?>">
+ <option value=""></option>
+ <?php foreach($board_selector as $board_id => $board_name): ?>
+ <option value="<?= $board_id ?>"><?= $this->e($board_name) ?></option>
+ <?php endforeach ?>
+ </select>
+ </li>
+ <?php endif ?>
+ <li>
+ <?= $this->url->link(t('Logout'), 'auth', 'logout') ?>
+ <span class="username hide-tablet">(<?= $this->user->getProfileLink() ?>)</span>
+ </li>
+ </ul>
+ </nav>
+</header> \ No newline at end of file
diff --git a/app/Template/layout.php b/app/Template/layout.php
index 60108175..934fb62c 100644
--- a/app/Template/layout.php
+++ b/app/Template/layout.php
@@ -28,6 +28,8 @@
<link rel="apple-touch-icon" sizes="144x144" href="<?= $this->url->dir() ?>assets/img/touch-icon-ipad-retina.png">
<title><?= isset($title) ? $this->e($title) : 'Kanboard' ?></title>
+
+ <?= $this->hook->render('layout:head') ?>
</head>
<body data-status-url="<?= $this->url->href('app', 'status') ?>"
data-login-url="<?= $this->url->href('auth', 'login') ?>"
@@ -38,43 +40,17 @@
<?php if (isset($no_layout) && $no_layout): ?>
<?= $content_for_layout ?>
<?php else: ?>
- <header>
- <nav>
- <h1><?= $this->url->link('K<span>B</span>', 'app', 'index', array(), false, 'logo', t('Dashboard')).' '.$this->e($title) ?>
- <?php if (! empty($description)): ?>
- <span class="tooltip" title='<?= $this->e($this->text->markdown($description)) ?>'>
- <i class="fa fa-info-circle"></i>
- </span>
- <?php endif ?>
- </h1>
- <ul>
- <?php if (isset($board_selector) && ! empty($board_selector)): ?>
- <li>
- <select id="board-selector"
- class="chosen-select select-auto-redirect"
- tabindex="-1"
- data-notfound="<?= t('No results match:') ?>"
- data-placeholder="<?= t('Display another project') ?>"
- data-redirect-regex="PROJECT_ID"
- data-redirect-url="<?= $this->url->href('board', 'show', array('project_id' => 'PROJECT_ID')) ?>">
- <option value=""></option>
- <?php foreach($board_selector as $board_id => $board_name): ?>
- <option value="<?= $board_id ?>"><?= $this->e($board_name) ?></option>
- <?php endforeach ?>
- </select>
- </li>
- <?php endif ?>
- <li>
- <?= $this->url->link(t('Logout'), 'auth', 'logout') ?>
- <span class="username hide-tablet">(<?= $this->user->getProfileLink() ?>)</span>
- </li>
- </ul>
- </nav>
- </header>
+ <?= $this->hook->render('layout:top') ?>
+ <?= $this->render('header', array(
+ 'title' => $title,
+ 'description' => isset($description) ? $description : '',
+ 'board_selector' => $board_selector,
+ )) ?>
<section class="page">
<?= $this->app->flashMessage() ?>
<?= $content_for_layout ?>
</section>
+ <?= $this->hook->render('layout:bottom') ?>
<?php endif ?>
</body>
</html>
diff --git a/app/Template/project/dropdown.php b/app/Template/project/dropdown.php
index 0a53cc05..c9563a4f 100644
--- a/app/Template/project/dropdown.php
+++ b/app/Template/project/dropdown.php
@@ -9,21 +9,23 @@
</li>
<?php endif ?>
+<?= $this->hook->render('project:dropdown', array('project' => $project)) ?>
+
<?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
-<li>
- <i class="fa fa-line-chart fa-fw"></i>
- <?= $this->url->link(t('Analytics'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?>
-</li>
-<li>
- <i class="fa fa-pie-chart fa-fw"></i>
- <?= $this->url->link(t('Budget'), 'budget', 'index', array('project_id' => $project['id'])) ?>
-</li>
-<li>
- <i class="fa fa-download fa-fw"></i>
- <?= $this->url->link(t('Exports'), 'export', 'tasks', array('project_id' => $project['id'])) ?>
-</li>
-<li>
- <i class="fa fa-cog fa-fw"></i>
- <?= $this->url->link(t('Settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
-</li>
+ <li>
+ <i class="fa fa-line-chart fa-fw"></i>
+ <?= $this->url->link(t('Analytics'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <i class="fa fa-pie-chart fa-fw"></i>
+ <?= $this->url->link(t('Budget'), 'budget', 'index', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <i class="fa fa-download fa-fw"></i>
+ <?= $this->url->link(t('Exports'), 'export', 'tasks', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <i class="fa fa-cog fa-fw"></i>
+ <?= $this->url->link(t('Settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
+ </li>
<?php endif ?>
diff --git a/app/Template/project/sidebar.php b/app/Template/project/sidebar.php
index 7b5d976f..84bbb6b1 100644
--- a/app/Template/project/sidebar.php
+++ b/app/Template/project/sidebar.php
@@ -48,6 +48,8 @@
</li>
<?php endif ?>
<?php endif ?>
+
+ <?= $this->hook->render('project:sidebar') ?>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
diff --git a/app/Template/project_user/sidebar.php b/app/Template/project_user/sidebar.php
index 8cc3f41b..98219a87 100644
--- a/app/Template/project_user/sidebar.php
+++ b/app/Template/project_user/sidebar.php
@@ -24,5 +24,7 @@
<li <?= $this->app->getRouterAction() === 'closed' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Closed tasks'), 'projectuser', 'closed', $filter) ?>
</li>
+
+ <?= $this->hook->render('project-user:sidebar') ?>
</ul>
</div> \ No newline at end of file
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index 1f06ab8c..cf0e9f76 100644
--- a/app/Template/task/sidebar.php
+++ b/app/Template/task/sidebar.php
@@ -18,6 +18,8 @@
<?= $this->url->link(t('Time tracking'), 'task', 'timetracking', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
<?php endif ?>
+
+ <?= $this->hook->render('task:sidebar:information') ?>
</ul>
<h2><?= t('Actions') ?></h2>
<ul>
@@ -66,6 +68,8 @@
<?= $this->url->link(t('Remove'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
<?php endif ?>
+
+ <?= $this->hook->render('task:sidebar:actions') ?>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php
index cd1c85c1..77612d0f 100644
--- a/app/Template/user/sidebar.php
+++ b/app/Template/user/sidebar.php
@@ -20,6 +20,8 @@
<?= $this->url->link(t('Persistent connections'), 'user', 'sessions', array('user_id' => $user['id'])) ?>
</li>
<?php endif ?>
+
+ <?= $this->hook->render('user:sidebar:information') ?>
</ul>
<h2><?= t('Actions') ?></h2>
@@ -68,6 +70,8 @@
</li>
<?php endif ?>
+ <?= $this->hook->render('user:sidebar:actions', array('user' => $user)) ?>
+
<?php if ($this->user->isAdmin() && ! $this->user->isCurrentUser($user['id'])): ?>
<li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'remove' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Remove'), 'user', 'remove', array('user_id' => $user['id'])) ?>
diff --git a/app/common.php b/app/common.php
index 1f1c7273..ea38ab36 100644
--- a/app/common.php
+++ b/app/common.php
@@ -30,120 +30,8 @@ $container->register(new ServiceProvider\ClassProvider);
$container->register(new ServiceProvider\EventDispatcherProvider);
if (ENABLE_URL_REWRITE) {
-
- // Dashboard
- $container['router']->addRoute('dashboard', 'app', 'index');
- $container['router']->addRoute('dashboard/:user_id', 'app', 'index', array('user_id'));
- $container['router']->addRoute('dashboard/:user_id/projects', 'app', 'projects', array('user_id'));
- $container['router']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks', array('user_id'));
- $container['router']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks', array('user_id'));
- $container['router']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar', array('user_id'));
- $container['router']->addRoute('dashboard/:user_id/activity', 'app', 'activity', array('user_id'));
-
- // Search routes
- $container['router']->addRoute('search', 'search', 'index');
- $container['router']->addRoute('search/:search', 'search', 'index', array('search'));
-
- // Project routes
- $container['router']->addRoute('projects', 'project', 'index');
- $container['router']->addRoute('project/create', 'project', 'create');
- $container['router']->addRoute('project/create/:private', 'project', 'create', array('private'));
- $container['router']->addRoute('project/:project_id', 'project', 'show', array('project_id'));
- $container['router']->addRoute('p/:project_id', 'project', 'show', array('project_id'));
- $container['router']->addRoute('project/:project_id/share', 'project', 'share', array('project_id'));
- $container['router']->addRoute('project/:project_id/edit', 'project', 'edit', array('project_id'));
- $container['router']->addRoute('project/:project_id/integration', 'project', 'integration', array('project_id'));
- $container['router']->addRoute('project/:project_id/users', 'project', 'users', array('project_id'));
- $container['router']->addRoute('project/:project_id/duplicate', 'project', 'duplicate', array('project_id'));
- $container['router']->addRoute('project/:project_id/remove', 'project', 'remove', array('project_id'));
- $container['router']->addRoute('project/:project_id/disable', 'project', 'disable', array('project_id'));
- $container['router']->addRoute('project/:project_id/enable', 'project', 'enable', array('project_id'));
-
- // Action routes
- $container['router']->addRoute('project/:project_id/actions', 'action', 'index', array('project_id'));
- $container['router']->addRoute('project/:project_id/action/:action_id/confirm', 'action', 'confirm', array('project_id', 'action_id'));
-
- // Column routes
- $container['router']->addRoute('project/:project_id/columns', 'column', 'index', array('project_id'));
- $container['router']->addRoute('project/:project_id/column/:column_id/edit', 'column', 'edit', array('project_id', 'column_id'));
- $container['router']->addRoute('project/:project_id/column/:column_id/confirm', 'column', 'confirm', array('project_id', 'column_id'));
- $container['router']->addRoute('project/:project_id/column/:column_id/move/:direction', 'column', 'move', array('project_id', 'column_id', 'direction'));
-
- // Swimlane routes
- $container['router']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index', array('project_id'));
- $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/edit', 'swimlane', 'edit', array('project_id', 'swimlane_id'));
- $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/confirm', 'swimlane', 'confirm', array('project_id', 'swimlane_id'));
- $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/disable', 'swimlane', 'disable', array('project_id', 'swimlane_id'));
- $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/enable', 'swimlane', 'enable', array('project_id', 'swimlane_id'));
- $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/up', 'swimlane', 'moveup', array('project_id', 'swimlane_id'));
- $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/down', 'swimlane', 'movedown', array('project_id', 'swimlane_id'));
-
- // Category routes
- $container['router']->addRoute('project/:project_id/categories', 'category', 'index', array('project_id'));
- $container['router']->addRoute('project/:project_id/category/:category_id/edit', 'category', 'edit', array('project_id', 'category_id'));
- $container['router']->addRoute('project/:project_id/category/:category_id/confirm', 'category', 'confirm', array('project_id', 'category_id'));
-
- // Task routes
- $container['router']->addRoute('project/:project_id/task/:task_id', 'task', 'show', array('project_id', 'task_id'));
- $container['router']->addRoute('t/:task_id', 'task', 'show', array('task_id'));
- $container['router']->addRoute('public/task/:task_id/:token', 'task', 'readonly', array('task_id', 'token'));
-
- $container['router']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task', array('project_id', 'task_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot', array('project_id', 'task_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create', array('project_id', 'task_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create', array('project_id', 'task_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create', array('project_id', 'task_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions', array('project_id', 'task_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics', array('project_id', 'task_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove', array('project_id', 'task_id'));
-
- $container['router']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit', array('project_id', 'task_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description', array('project_id', 'task_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence', array('project_id', 'task_id'));
-
- $container['router']->addRoute('project/:project_id/task/:task_id/close', 'taskstatus', 'close', array('task_id', 'project_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/open', 'taskstatus', 'open', array('task_id', 'project_id'));
-
- $container['router']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate', array('task_id', 'project_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy', array('task_id', 'project_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy', array('task_id', 'project_id', 'dst_project_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move', array('task_id', 'project_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move', array('task_id', 'project_id', 'dst_project_id'));
-
- // Board routes
- $container['router']->addRoute('board/:project_id', 'board', 'show', array('project_id'));
- $container['router']->addRoute('b/:project_id', 'board', 'show', array('project_id'));
- $container['router']->addRoute('public/board/:token', 'board', 'readonly', array('token'));
-
- // Calendar routes
- $container['router']->addRoute('calendar/:project_id', 'calendar', 'show', array('project_id'));
- $container['router']->addRoute('c/:project_id', 'calendar', 'show', array('project_id'));
-
- // Listing routes
- $container['router']->addRoute('list/:project_id', 'listing', 'show', array('project_id'));
- $container['router']->addRoute('l/:project_id', 'listing', 'show', array('project_id'));
-
- // Gantt routes
- $container['router']->addRoute('gantt/:project_id', 'gantt', 'project', array('project_id'));
- $container['router']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project', array('project_id', 'sorting'));
-
- // Subtask routes
- $container['router']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create', array('project_id', 'task_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm', array('project_id', 'task_id', 'subtask_id'));
- $container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/edit', 'subtask', 'edit', array('project_id', 'task_id', 'subtask_id'));
-
- // Feed routes
- $container['router']->addRoute('feed/project/:token', 'feed', 'project', array('token'));
- $container['router']->addRoute('feed/user/:token', 'feed', 'user', array('token'));
-
- // Ical routes
- $container['router']->addRoute('ical/project/:token', 'ical', 'project', array('token'));
- $container['router']->addRoute('ical/user/:token', 'ical', 'user', array('token'));
-
- // Auth routes
- $container['router']->addRoute('oauth/google', 'oauth', 'google');
- $container['router']->addRoute('oauth/github', 'oauth', 'github');
- $container['router']->addRoute('oauth/gitlab', 'oauth', 'gitlab');
- $container['router']->addRoute('login', 'auth', 'login');
- $container['router']->addRoute('logout', 'auth', 'logout');
+ require __DIR__.'/routes.php';
}
+
+$plugin = new Core\PluginLoader($container);
+$plugin->scan();
diff --git a/app/routes.php b/app/routes.php
new file mode 100644
index 00000000..159e8f6e
--- /dev/null
+++ b/app/routes.php
@@ -0,0 +1,117 @@
+<?php
+
+// Dashboard
+$container['router']->addRoute('dashboard', 'app', 'index');
+$container['router']->addRoute('dashboard/:user_id', 'app', 'index', array('user_id'));
+$container['router']->addRoute('dashboard/:user_id/projects', 'app', 'projects', array('user_id'));
+$container['router']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks', array('user_id'));
+$container['router']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks', array('user_id'));
+$container['router']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar', array('user_id'));
+$container['router']->addRoute('dashboard/:user_id/activity', 'app', 'activity', array('user_id'));
+
+// Search routes
+$container['router']->addRoute('search', 'search', 'index');
+$container['router']->addRoute('search/:search', 'search', 'index', array('search'));
+
+// Project routes
+$container['router']->addRoute('projects', 'project', 'index');
+$container['router']->addRoute('project/create', 'project', 'create');
+$container['router']->addRoute('project/create/:private', 'project', 'create', array('private'));
+$container['router']->addRoute('project/:project_id', 'project', 'show', array('project_id'));
+$container['router']->addRoute('p/:project_id', 'project', 'show', array('project_id'));
+$container['router']->addRoute('project/:project_id/share', 'project', 'share', array('project_id'));
+$container['router']->addRoute('project/:project_id/edit', 'project', 'edit', array('project_id'));
+$container['router']->addRoute('project/:project_id/integration', 'project', 'integration', array('project_id'));
+$container['router']->addRoute('project/:project_id/users', 'project', 'users', array('project_id'));
+$container['router']->addRoute('project/:project_id/duplicate', 'project', 'duplicate', array('project_id'));
+$container['router']->addRoute('project/:project_id/remove', 'project', 'remove', array('project_id'));
+$container['router']->addRoute('project/:project_id/disable', 'project', 'disable', array('project_id'));
+$container['router']->addRoute('project/:project_id/enable', 'project', 'enable', array('project_id'));
+
+// Action routes
+$container['router']->addRoute('project/:project_id/actions', 'action', 'index', array('project_id'));
+$container['router']->addRoute('project/:project_id/action/:action_id/confirm', 'action', 'confirm', array('project_id', 'action_id'));
+
+// Column routes
+$container['router']->addRoute('project/:project_id/columns', 'column', 'index', array('project_id'));
+$container['router']->addRoute('project/:project_id/column/:column_id/edit', 'column', 'edit', array('project_id', 'column_id'));
+$container['router']->addRoute('project/:project_id/column/:column_id/confirm', 'column', 'confirm', array('project_id', 'column_id'));
+$container['router']->addRoute('project/:project_id/column/:column_id/move/:direction', 'column', 'move', array('project_id', 'column_id', 'direction'));
+
+// Swimlane routes
+$container['router']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index', array('project_id'));
+$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/edit', 'swimlane', 'edit', array('project_id', 'swimlane_id'));
+$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/confirm', 'swimlane', 'confirm', array('project_id', 'swimlane_id'));
+$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/disable', 'swimlane', 'disable', array('project_id', 'swimlane_id'));
+$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/enable', 'swimlane', 'enable', array('project_id', 'swimlane_id'));
+$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/up', 'swimlane', 'moveup', array('project_id', 'swimlane_id'));
+$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/down', 'swimlane', 'movedown', array('project_id', 'swimlane_id'));
+
+// Category routes
+$container['router']->addRoute('project/:project_id/categories', 'category', 'index', array('project_id'));
+$container['router']->addRoute('project/:project_id/category/:category_id/edit', 'category', 'edit', array('project_id', 'category_id'));
+$container['router']->addRoute('project/:project_id/category/:category_id/confirm', 'category', 'confirm', array('project_id', 'category_id'));
+
+// Task routes
+$container['router']->addRoute('project/:project_id/task/:task_id', 'task', 'show', array('project_id', 'task_id'));
+$container['router']->addRoute('t/:task_id', 'task', 'show', array('task_id'));
+$container['router']->addRoute('public/task/:task_id/:token', 'task', 'readonly', array('task_id', 'token'));
+
+$container['router']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task', array('project_id', 'task_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot', array('project_id', 'task_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create', array('project_id', 'task_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create', array('project_id', 'task_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create', array('project_id', 'task_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions', array('project_id', 'task_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics', array('project_id', 'task_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove', array('project_id', 'task_id'));
+
+$container['router']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit', array('project_id', 'task_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description', array('project_id', 'task_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence', array('project_id', 'task_id'));
+
+$container['router']->addRoute('project/:project_id/task/:task_id/close', 'taskstatus', 'close', array('task_id', 'project_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/open', 'taskstatus', 'open', array('task_id', 'project_id'));
+
+$container['router']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate', array('task_id', 'project_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy', array('task_id', 'project_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy', array('task_id', 'project_id', 'dst_project_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move', array('task_id', 'project_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move', array('task_id', 'project_id', 'dst_project_id'));
+
+// Board routes
+$container['router']->addRoute('board/:project_id', 'board', 'show', array('project_id'));
+$container['router']->addRoute('b/:project_id', 'board', 'show', array('project_id'));
+$container['router']->addRoute('public/board/:token', 'board', 'readonly', array('token'));
+
+// Calendar routes
+$container['router']->addRoute('calendar/:project_id', 'calendar', 'show', array('project_id'));
+$container['router']->addRoute('c/:project_id', 'calendar', 'show', array('project_id'));
+
+// Listing routes
+$container['router']->addRoute('list/:project_id', 'listing', 'show', array('project_id'));
+$container['router']->addRoute('l/:project_id', 'listing', 'show', array('project_id'));
+
+// Gantt routes
+$container['router']->addRoute('gantt/:project_id', 'gantt', 'project', array('project_id'));
+$container['router']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project', array('project_id', 'sorting'));
+
+// Subtask routes
+$container['router']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create', array('project_id', 'task_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm', array('project_id', 'task_id', 'subtask_id'));
+$container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/edit', 'subtask', 'edit', array('project_id', 'task_id', 'subtask_id'));
+
+// Feed routes
+$container['router']->addRoute('feed/project/:token', 'feed', 'project', array('token'));
+$container['router']->addRoute('feed/user/:token', 'feed', 'user', array('token'));
+
+// Ical routes
+$container['router']->addRoute('ical/project/:token', 'ical', 'project', array('token'));
+$container['router']->addRoute('ical/user/:token', 'ical', 'user', array('token'));
+
+// Auth routes
+$container['router']->addRoute('oauth/google', 'oauth', 'google');
+$container['router']->addRoute('oauth/github', 'oauth', 'github');
+$container['router']->addRoute('oauth/gitlab', 'oauth', 'gitlab');
+$container['router']->addRoute('login', 'auth', 'login');
+$container['router']->addRoute('logout', 'auth', 'logout');