First draft for plugins system

This commit is contained in:
Frederic Guillot 2015-09-13 14:07:56 -04:00
parent c405f99fc8
commit a6a00a0040
31 changed files with 626 additions and 237 deletions

View File

@ -5,6 +5,7 @@ New features:
* Add LDAP group sync
* Add swimlane description
* New plugin system (alpha)
Improvements:

31
app/Core/PluginBase.php Normal file
View File

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

144
app/Core/PluginLoader.php Normal file
View File

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

View File

@ -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
);
}
$class = empty($plugin) ? '\Controller\\'.ucfirst($controller) : '\Plugin\\'.ucfirst($plugin).'\Controller\\'.ucfirst($controller);
/**
* 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;
}
return false;
$instance = new $class($this->container);
$instance->beforeAction($controller, $action);
$instance->$action();
}
}

View File

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

View File

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

View File

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

49
app/Helper/Hook.php Normal file
View File

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

View File

@ -94,6 +94,18 @@ class Acl extends Base
'twofactor' => array('disable'),
);
/**
* 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
*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

33
app/Template/header.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'])) ?>

View File

@ -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();

117
app/routes.php Normal file
View File

@ -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');

View File

@ -19,6 +19,10 @@
"gregwar/captcha": "1.*"
},
"autoload" : {
"classmap" : ["app/"],
"psr-4" : {
"Plugin\\": "plugins/"
},
"psr-0" : {
"" : "app/"
},

2
plugins/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!/.gitignore

View File

@ -0,0 +1,23 @@
<?php
require_once __DIR__.'/../Base.php';
use Core\PluginLoader;
class PluginLoaderTest extends Base
{
public function testGetSchemaVersion()
{
$p = new PluginLoader($this->container);
$this->assertEquals(0, $p->getSchemaVersion('not_found'));
$this->assertTrue($p->setSchemaVersion('plugin1', 1));
$this->assertEquals(1, $p->getSchemaVersion('plugin1'));
$this->assertTrue($p->setSchemaVersion('plugin2', 33));
$this->assertEquals(33, $p->getSchemaVersion('plugin2'));
$this->assertTrue($p->setSchemaVersion('plugin1', 2));
$this->assertEquals(2, $p->getSchemaVersion('plugin1'));
}
}

View File

@ -0,0 +1,28 @@
<?php
require_once __DIR__.'/../Base.php';
use Core\Template;
class TemplateTest extends Base
{
public function testGetTemplateFile()
{
$t = new Template($this->container);
$this->assertStringEndsWith('app/Core/../Template/a/b.php', $t->getTemplateFile('a/b'));
}
public function testGetPluginTemplateFile()
{
$t = new Template($this->container);
$this->assertStringEndsWith('app/Core/../../plugins/Myplugin/Template/a/b.php', $t->getTemplateFile('myplugin:a/b'));
}
public function testGetOverridedTemplateFile()
{
$t = new Template($this->container);
$t->setTemplateOverride('a/b', 'myplugin:c');
$this->assertStringEndsWith('app/Core/../../plugins/Myplugin/Template/c.php', $t->getTemplateFile('a/b'));
$this->assertStringEndsWith('app/Core/../Template/d.php', $t->getTemplateFile('d'));
}
}

View File

@ -290,4 +290,16 @@ class AclTest extends Base
$this->assertFalse($acl->isAllowed('task', 'remove', 1));
$this->assertTrue($acl->isAllowed('app', 'index', 1));
}
public function testExtend()
{
$acl = new Acl($this->container);
$this->assertFalse($acl->isProjectManagerAction('plop', 'show'));
$acl->extend('project_manager_acl', array('plop' => '*'));
$this->assertTrue($acl->isProjectManagerAction('plop', 'show'));
$this->assertTrue($acl->isProjectManagerAction('swimlane', 'index'));
}
}

View File

@ -26,7 +26,7 @@ class ProjectTest extends Base
$this->assertNotFalse($p->create(array('name' => 'UnitTest '.$locale)), 'Unable to create project with '.$locale.':'.$language);
}
Translator::load('en_US');
Translator::unload();
}
public function testCreation()