summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/Controller/ColumnRestrictionController.php103
-rw-r--r--app/Controller/TaskCreationController.php12
-rw-r--r--app/Controller/TaskMovePositionController.php1
-rw-r--r--app/Core/Base.php4
-rw-r--r--app/Decorator/ColumnMoveRestrictionCacheDecorator.php3
-rw-r--r--app/Decorator/ColumnRestrictionCacheDecorator.php59
-rw-r--r--app/Decorator/ProjectRoleRestrictionCacheDecorator.php59
-rw-r--r--app/Helper/ProjectRoleHelper.php112
-rw-r--r--app/Model/ColumnRestrictionModel.php152
-rw-r--r--app/Model/ProjectPermissionModel.php9
-rw-r--r--app/Model/ProjectRoleModel.php36
-rw-r--r--app/Model/ProjectRoleRestrictionModel.php44
-rw-r--r--app/Model/ProjectUserRoleModel.php2
-rw-r--r--app/Model/TaskFinderModel.php1
-rw-r--r--app/Schema/Mysql.php20
-rw-r--r--app/Schema/Postgres.php19
-rw-r--r--app/Schema/Sqlite.php19
-rw-r--r--app/ServiceProvider/CacheProvider.php16
-rw-r--r--app/ServiceProvider/ClassProvider.php2
-rw-r--r--app/Template/board/table_column.php2
-rw-r--r--app/Template/column_move_restriction/create.php2
-rw-r--r--app/Template/column_restriction/create.php22
-rw-r--r--app/Template/column_restriction/remove.php14
-rw-r--r--app/Template/project_role/show.php34
-rw-r--r--app/Template/task/dropdown.php2
-rw-r--r--app/Template/task/sidebar.php2
-rw-r--r--app/Validator/ColumnRestrictionValidator.php40
27 files changed, 725 insertions, 66 deletions
diff --git a/app/Controller/ColumnRestrictionController.php b/app/Controller/ColumnRestrictionController.php
new file mode 100644
index 00000000..ce2a1ca8
--- /dev/null
+++ b/app/Controller/ColumnRestrictionController.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Kanboard\Core\Controller\AccessForbiddenException;
+
+/**
+ * Class ColumnMoveRestrictionController
+ *
+ * @package Kanboard\Controller
+ * @author Frederic Guillot
+ */
+class ColumnRestrictionController extends BaseController
+{
+ /**
+ * Show form to create a new column restriction
+ *
+ * @param array $values
+ * @param array $errors
+ * @throws AccessForbiddenException
+ */
+ public function create(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+ $role_id = $this->request->getIntegerParam('role_id');
+ $role = $this->projectRoleModel->getById($project['id'], $role_id);
+
+ $this->response->html($this->template->render('column_restriction/create', array(
+ 'project' => $project,
+ 'role' => $role,
+ 'rules' => $this->columnRestrictionModel->getRules(),
+ 'columns' => $this->columnModel->getList($project['id']),
+ 'values' => $values + array('project_id' => $project['id'], 'role_id' => $role['role_id']),
+ 'errors' => $errors,
+ )));
+ }
+
+ /**
+ * Save new column restriction
+ */
+ public function save()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+
+ list($valid, $errors) = $this->columnRestrictionValidator->validateCreation($values);
+
+ if ($valid) {
+ $restriction_id = $this->columnRestrictionModel->create(
+ $project['id'],
+ $values['role_id'],
+ $values['column_id'],
+ $values['rule']
+ );
+
+ if ($restriction_id !== false) {
+ $this->flash->success(t('The column restriction has been created successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to create this column restriction.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectRoleController', 'show', array('project_id' => $project['id'])));
+ } else {
+ $this->create($values, $errors);
+ }
+ }
+
+ /**
+ * Confirm suppression
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $project = $this->getProject();
+ $restriction_id = $this->request->getIntegerParam('restriction_id');
+
+ $this->response->html($this->helper->layout->project('column_restriction/remove', array(
+ 'project' => $project,
+ 'restriction' => $this->columnRestrictionModel->getById($project['id'], $restriction_id),
+ )));
+ }
+
+ /**
+ * Remove a restriction
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $project = $this->getProject();
+ $this->checkCSRFParam();
+ $restriction_id = $this->request->getIntegerParam('restriction_id');
+
+ if ($this->columnRestrictionModel->remove($restriction_id)) {
+ $this->flash->success(t('Column restriction removed successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to remove this restriction.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectRoleController', 'show', array('project_id' => $project['id'])));
+ }
+}
diff --git a/app/Controller/TaskCreationController.php b/app/Controller/TaskCreationController.php
index c68964f6..c754b029 100644
--- a/app/Controller/TaskCreationController.php
+++ b/app/Controller/TaskCreationController.php
@@ -52,12 +52,16 @@ class TaskCreationController extends BaseController
list($valid, $errors) = $this->taskValidator->validateCreation($values);
- if ($valid && ($task_id = $this->taskCreationModel->create($values))) {
- $this->flash->success(t('Task created successfully.'));
- $this->afterSave($project, $values, $task_id);
- } else {
+ if (! $valid) {
$this->flash->failure(t('Unable to create your task.'));
$this->show($values, $errors);
+ } else if (! $this->helper->projectRole->canCreateTaskInColumn($project['id'], $values['column_id'])) {
+ $this->flash->failure(t('You cannot create tasks in this column.'));
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true);
+ } else {
+ $task_id = $this->taskCreationModel->create($values);
+ $this->flash->success(t('Task created successfully.'));
+ $this->afterSave($project, $values, $task_id);
}
}
diff --git a/app/Controller/TaskMovePositionController.php b/app/Controller/TaskMovePositionController.php
index c6e8be0c..1a8eee45 100644
--- a/app/Controller/TaskMovePositionController.php
+++ b/app/Controller/TaskMovePositionController.php
@@ -2,6 +2,7 @@
namespace Kanboard\Controller;
+use Kanboard\Core\Controller\AccessForbiddenException;
use Kanboard\Formatter\BoardFormatter;
/**
diff --git a/app/Core/Base.php b/app/Core/Base.php
index d6da13f2..44dfaa39 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -57,7 +57,9 @@ use Pimple\Container;
* @property \Kanboard\Core\Paginator $paginator
* @property \Kanboard\Core\Template $template
* @property \Kanboard\Decorator\MetadataCacheDecorator $userMetadataCacheDecorator
+ * @property \Kanboard\Decorator\columnRestrictionCacheDecorator $columnRestrictionCacheDecorator
* @property \Kanboard\Decorator\ColumnMoveRestrictionCacheDecorator $columnMoveRestrictionCacheDecorator
+ * @property \Kanboard\Decorator\ProjectRoleRestrictionCacheDecorator $projectRoleRestrictionCacheDecorator
* @property \Kanboard\Model\ActionModel $actionModel
* @property \Kanboard\Model\ActionParameterModel $actionParameterModel
* @property \Kanboard\Model\AvatarFileModel $avatarFileModel
@@ -65,6 +67,7 @@ use Pimple\Container;
* @property \Kanboard\Model\CategoryModel $categoryModel
* @property \Kanboard\Model\ColorModel $colorModel
* @property \Kanboard\Model\ColumnModel $columnModel
+ * @property \Kanboard\Model\ColumnRestrictionModel $columnRestrictionModel
* @property \Kanboard\Model\ColumnMoveRestrictionModel $columnMoveRestrictionModel
* @property \Kanboard\Model\CommentModel $commentModel
* @property \Kanboard\Model\ConfigModel $configModel
@@ -136,6 +139,7 @@ use Pimple\Container;
* @property \Kanboard\Validator\AuthValidator $authValidator
* @property \Kanboard\Validator\ColumnValidator $columnValidator
* @property \Kanboard\Validator\CategoryValidator $categoryValidator
+ * @property \Kanboard\Validator\ColumnRestrictionValidator $columnRestrictionValidator
* @property \Kanboard\Validator\ColumnMoveRestrictionValidator $columnMoveRestrictionValidator
* @property \Kanboard\Validator\CommentValidator $commentValidator
* @property \Kanboard\Validator\CurrencyValidator $currencyValidator
diff --git a/app/Decorator/ColumnMoveRestrictionCacheDecorator.php b/app/Decorator/ColumnMoveRestrictionCacheDecorator.php
index 2a3e9c2a..82140d16 100644
--- a/app/Decorator/ColumnMoveRestrictionCacheDecorator.php
+++ b/app/Decorator/ColumnMoveRestrictionCacheDecorator.php
@@ -40,7 +40,8 @@ class ColumnMoveRestrictionCacheDecorator
/**
* Proxy method to get sortable columns
*
- * @param int $project_id
+ * @param int $project_id
+ * @param string $role
* @return array|mixed
*/
public function getSortableColumns($project_id, $role)
diff --git a/app/Decorator/ColumnRestrictionCacheDecorator.php b/app/Decorator/ColumnRestrictionCacheDecorator.php
new file mode 100644
index 00000000..a615030d
--- /dev/null
+++ b/app/Decorator/ColumnRestrictionCacheDecorator.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Kanboard\Decorator;
+
+use Kanboard\Core\Cache\CacheInterface;
+use Kanboard\Model\ColumnRestrictionModel;
+
+/**
+ * Class ColumnRestrictionCacheDecorator
+ *
+ * @package Kanboard\Decorator
+ * @author Frederic Guillot
+ */
+class ColumnRestrictionCacheDecorator
+{
+ protected $cachePrefix = 'column_restriction:';
+
+ /**
+ * @var CacheInterface
+ */
+ protected $cache;
+
+ /**
+ * @var ColumnRestrictionModel
+ */
+ protected $columnRestrictionModel;
+
+ /**
+ * ColumnMoveRestrictionDecorator constructor.
+ *
+ * @param CacheInterface $cache
+ * @param ColumnRestrictionModel $columnMoveRestrictionModel
+ */
+ public function __construct(CacheInterface $cache, ColumnRestrictionModel $columnMoveRestrictionModel)
+ {
+ $this->cache = $cache;
+ $this->columnRestrictionModel = $columnMoveRestrictionModel;
+ }
+
+ /**
+ * Proxy method to get sortable columns
+ *
+ * @param int $project_id
+ * @param string $role
+ * @return array|mixed
+ */
+ public function getAllByRole($project_id, $role)
+ {
+ $key = $this->cachePrefix.$project_id.$role;
+ $columnRestrictions = $this->cache->get($key);
+
+ if ($columnRestrictions === null) {
+ $columnRestrictions = $this->columnRestrictionModel->getAllByRole($project_id, $role);
+ $this->cache->set($key, $columnRestrictions);
+ }
+
+ return $columnRestrictions;
+ }
+}
diff --git a/app/Decorator/ProjectRoleRestrictionCacheDecorator.php b/app/Decorator/ProjectRoleRestrictionCacheDecorator.php
new file mode 100644
index 00000000..a6e24048
--- /dev/null
+++ b/app/Decorator/ProjectRoleRestrictionCacheDecorator.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Kanboard\Decorator;
+
+use Kanboard\Core\Cache\CacheInterface;
+use Kanboard\Model\ProjectRoleRestrictionModel;
+
+/**
+ * Class ProjectRoleRestrictionCacheDecorator
+ *
+ * @package Kanboard\Decorator
+ * @author Frederic Guillot
+ */
+class ProjectRoleRestrictionCacheDecorator
+{
+ protected $cachePrefix = 'project_restriction:';
+
+ /**
+ * @var CacheInterface
+ */
+ protected $cache;
+
+ /**
+ * @var ProjectRoleRestrictionModel
+ */
+ protected $projectRoleRestrictionModel;
+
+ /**
+ * ColumnMoveRestrictionDecorator constructor.
+ *
+ * @param CacheInterface $cache
+ * @param ProjectRoleRestrictionModel $projectRoleRestrictionModel
+ */
+ public function __construct(CacheInterface $cache, ProjectRoleRestrictionModel $projectRoleRestrictionModel)
+ {
+ $this->cache = $cache;
+ $this->projectRoleRestrictionModel = $projectRoleRestrictionModel;
+ }
+
+ /**
+ * Proxy method to get sortable columns
+ *
+ * @param int $project_id
+ * @param string $role
+ * @return array|mixed
+ */
+ public function getAllByRole($project_id, $role)
+ {
+ $key = $this->cachePrefix.$project_id.$role;
+ $projectRestrictions = $this->cache->get($key);
+
+ if ($projectRestrictions === null) {
+ $projectRestrictions = $this->projectRoleRestrictionModel->getAllByRole($project_id, $role);
+ $this->cache->set($key, $projectRestrictions);
+ }
+
+ return $projectRestrictions;
+ }
+}
diff --git a/app/Helper/ProjectRoleHelper.php b/app/Helper/ProjectRoleHelper.php
index 99fa82bc..e1808be5 100644
--- a/app/Helper/ProjectRoleHelper.php
+++ b/app/Helper/ProjectRoleHelper.php
@@ -4,6 +4,8 @@ namespace Kanboard\Helper;
use Kanboard\Core\Base;
use Kanboard\Core\Security\Role;
+use Kanboard\Model\ColumnRestrictionModel;
+use Kanboard\Model\ProjectRoleRestrictionModel;
/**
* Class ProjectRoleHelper
@@ -99,6 +101,46 @@ class ProjectRoleHelper extends Base
}
/**
+ * Return true if the user can create a task for the given column
+ *
+ * @param int $project_id
+ * @param int $column_id
+ * @return bool
+ */
+ public function canCreateTaskInColumn($project_id, $column_id)
+ {
+ $role = $this->getProjectUserRole($project_id);
+
+ if ($this->role->isCustomProjectRole($role)) {
+ if (! $this->isAllowedToCreateTask($project_id, $column_id, $role)) {
+ return false;
+ }
+ }
+
+ return $this->helper->user->hasProjectAccess('TaskCreationController', 'show', $project_id);
+ }
+
+ /**
+ * Return true if the user can create a task for the given column
+ *
+ * @param int $project_id
+ * @param int $column_id
+ * @return bool
+ */
+ public function canChangeTaskStatusInColumn($project_id, $column_id)
+ {
+ $role = $this->getProjectUserRole($project_id);
+
+ if ($this->role->isCustomProjectRole($role)) {
+ if (! $this->isAllowedToChangeTaskStatus($project_id, $column_id, $role)) {
+ return false;
+ }
+ }
+
+ return $this->helper->user->hasProjectAccess('TaskStatusController', 'close', $project_id);
+ }
+
+ /**
* Return true if the user can remove a task
*
* Regular users can't remove tasks from other people
@@ -145,13 +187,77 @@ class ProjectRoleHelper extends Base
$role = $this->getProjectUserRole($project_id);
if ($this->role->isCustomProjectRole($role)) {
- $restrictions = $this->projectRoleRestrictionModel->getAllByRole($project_id, $role);
- $result = $this->projectRoleRestrictionModel->isAllowed($restrictions, $controller, $action);
- $result = $result && $this->projectAuthorization->isAllowed($controller, $action, Role::PROJECT_MEMBER);
+ $result = $this->projectAuthorization->isAllowed($controller, $action, Role::PROJECT_MEMBER);
} else {
$result = $this->projectAuthorization->isAllowed($controller, $action, $role);
}
return $result;
}
+
+ /**
+ * Check authorization for a custom project role to change the task status
+ *
+ * @param int $project_id
+ * @param int $column_id
+ * @param string $role
+ * @return bool
+ */
+ protected function isAllowedToChangeTaskStatus($project_id, $column_id, $role)
+ {
+ $columnRestrictions = $this->columnRestrictionCacheDecorator->getAllByRole($project_id, $role);
+
+ foreach ($columnRestrictions as $restriction) {
+ if ($restriction['column_id'] == $column_id) {
+ if ($restriction['rule'] == ColumnRestrictionModel::RULE_ALLOW_TASK_OPEN_CLOSE) {
+ return true;
+ } else if ($restriction['rule'] == ColumnRestrictionModel::RULE_BLOCK_TASK_OPEN_CLOSE) {
+ return false;
+ }
+ }
+ }
+
+ $projectRestrictions = $this->projectRoleRestrictionCacheDecorator->getAllByRole($project_id, $role);
+
+ foreach ($projectRestrictions as $restriction) {
+ if ($restriction['rule'] == ProjectRoleRestrictionModel::RULE_TASK_OPEN_CLOSE) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Check authorization for a custom project role to create a task
+ *
+ * @param int $project_id
+ * @param int $column_id
+ * @param string $role
+ * @return bool
+ */
+ protected function isAllowedToCreateTask($project_id, $column_id, $role)
+ {
+ $columnRestrictions = $this->columnRestrictionCacheDecorator->getAllByRole($project_id, $role);
+
+ foreach ($columnRestrictions as $restriction) {
+ if ($restriction['column_id'] == $column_id) {
+ if ($restriction['rule'] == ColumnRestrictionModel::RULE_ALLOW_TASK_CREATION) {
+ return true;
+ } else if ($restriction['rule'] == ColumnRestrictionModel::RULE_BLOCK_TASK_CREATION) {
+ return false;
+ }
+ }
+ }
+
+ $projectRestrictions = $this->projectRoleRestrictionCacheDecorator->getAllByRole($project_id, $role);
+
+ foreach ($projectRestrictions as $restriction) {
+ if ($restriction['rule'] == ProjectRoleRestrictionModel::RULE_TASK_CREATION) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
diff --git a/app/Model/ColumnRestrictionModel.php b/app/Model/ColumnRestrictionModel.php
new file mode 100644
index 00000000..92b2ac60
--- /dev/null
+++ b/app/Model/ColumnRestrictionModel.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Base;
+
+/**
+ * Class ColumnRestrictionModel
+ *
+ * @package Kanboard\Model
+ * @author Frederic Guillot
+ */
+class ColumnRestrictionModel extends Base
+{
+ const TABLE = 'column_has_restrictions';
+
+ const RULE_ALLOW_TASK_CREATION = 'allow.task_creation';
+ const RULE_ALLOW_TASK_OPEN_CLOSE = 'allow.task_open_close';
+ const RULE_BLOCK_TASK_CREATION = 'block.task_creation';
+ const RULE_BLOCK_TASK_OPEN_CLOSE = 'block.task_open_close';
+
+ /**
+ * Get rules
+ *
+ * @return array
+ */
+ public function getRules()
+ {
+ return array(
+ self::RULE_ALLOW_TASK_CREATION => t('Task creation is permitted for this column'),
+ self::RULE_ALLOW_TASK_OPEN_CLOSE => t('Closing or opening a task is permitted for this column'),
+ self::RULE_BLOCK_TASK_CREATION => t('Task creation is blocked for this column'),
+ self::RULE_BLOCK_TASK_OPEN_CLOSE => t('Closing or opening a task is blocked for this column'),
+ );
+ }
+
+ /**
+ * Fetch one restriction
+ *
+ * @param int $project_id
+ * @param int $restriction_id
+ * @return array|null
+ */
+ public function getById($project_id, $restriction_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.restriction_id',
+ self::TABLE.'.project_id',
+ self::TABLE.'.role_id',
+ self::TABLE.'.column_id',
+ self::TABLE.'.rule',
+ 'pr.role',
+ 'c.title as column_title'
+ )
+ ->left(ColumnModel::TABLE, 'c', 'id', self::TABLE, 'column_id')
+ ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->eq(self::TABLE.'.restriction_id', $restriction_id)
+ ->findOne();
+ }
+
+ /**
+ * Get all project column restrictions
+ *
+ * @param int $project_id
+ * @return array
+ */
+ public function getAll($project_id)
+ {
+ $rules = $this->getRules();
+ $restrictions = $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.restriction_id',
+ self::TABLE.'.project_id',
+ self::TABLE.'.role_id',
+ self::TABLE.'.column_id',
+ self::TABLE.'.rule',
+ 'pr.role',
+ 'c.title as column_title'
+ )
+ ->left(ColumnModel::TABLE, 'c', 'id', self::TABLE, 'column_id')
+ ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->findAll();
+
+ foreach ($restrictions as &$restriction) {
+ $restriction['title'] = $rules[$restriction['rule']];
+ }
+
+ return $restrictions;
+ }
+
+ /**
+ * Get restrictions
+ *
+ * @param int $project_id
+ * @param string $role
+ * @return array
+ */
+ public function getAllByRole($project_id, $role)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.restriction_id',
+ self::TABLE.'.project_id',
+ self::TABLE.'.role_id',
+ self::TABLE.'.column_id',
+ self::TABLE.'.rule',
+ 'pr.role'
+ )
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->eq('pr.role', $role)
+ ->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
+ ->findAll();
+ }
+
+ /**
+ * Create a new column restriction
+ *
+ * @param int $project_id
+ * @param int $role_id
+ * @param int $column_id
+ * @param int $rule
+ * @return bool|int
+ */
+ public function create($project_id, $role_id, $column_id, $rule)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->persist(array(
+ 'project_id' => $project_id,
+ 'role_id' => $role_id,
+ 'column_id' => $column_id,
+ 'rule' => $rule,
+ ));
+ }
+
+ /**
+ * Remove a permission
+ *
+ * @param int $restriction_id
+ * @return bool
+ */
+ public function remove($restriction_id)
+ {
+ return $this->db->table(self::TABLE)->eq('restriction_id', $restriction_id)->remove();
+ }
+}
diff --git a/app/Model/ProjectPermissionModel.php b/app/Model/ProjectPermissionModel.php
index 4882343d..25b6a382 100644
--- a/app/Model/ProjectPermissionModel.php
+++ b/app/Model/ProjectPermissionModel.php
@@ -122,8 +122,13 @@ class ProjectPermissionModel extends Base
*/
public function isAssignable($project_id, $user_id)
{
- return $this->userModel->isActive($user_id) &&
- in_array($this->projectUserRoleModel->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER));
+ if ($this->userModel->isActive($user_id)) {
+ $role = $this->projectUserRoleModel->getUserRole($project_id, $user_id);
+
+ return ! empty($role) && $role !== Role::PROJECT_VIEWER;
+ }
+
+ return false;
}
/**
diff --git a/app/Model/ProjectRoleModel.php b/app/Model/ProjectRoleModel.php
index ed86d6ed..962ff44f 100644
--- a/app/Model/ProjectRoleModel.php
+++ b/app/Model/ProjectRoleModel.php
@@ -71,10 +71,14 @@ class ProjectRoleModel extends Base
{
$roles = $this->getAll($project_id);
- $column_restrictions = $this->columnMoveRestrictionModel->getAll($project_id);
+ $column_restrictions = $this->columnRestrictionModel->getAll($project_id);
$column_restrictions = array_column_index($column_restrictions, 'role_id');
array_merge_relation($roles, $column_restrictions, 'column_restrictions', 'role_id');
+ $column_move_restrictions = $this->columnMoveRestrictionModel->getAll($project_id);
+ $column_move_restrictions = array_column_index($column_move_restrictions, 'role_id');
+ array_merge_relation($roles, $column_move_restrictions, 'column_move_restrictions', 'role_id');
+
$project_restrictions = $this->projectRoleRestrictionModel->getAll($project_id);
$project_restrictions = array_column_index($project_restrictions, 'role_id');
array_merge_relation($roles, $project_restrictions, 'project_restrictions', 'role_id');
@@ -109,13 +113,41 @@ class ProjectRoleModel extends Base
*/
public function update($role_id, $project_id, $role)
{
- return $this->db
+ $this->db->startTransaction();
+
+ $previousRole = $this->getById($project_id, $role_id);
+
+ $r1 = $this->db
+ ->table(ProjectUserRoleModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('role', $previousRole['role'])
+ ->update(array(
+ 'role' => $role
+ ));
+
+ $r2 = $this->db
+ ->table(ProjectGroupRoleModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('role', $previousRole['role'])
+ ->update(array(
+ 'role' => $role
+ ));
+
+ $r3 = $this->db
->table(self::TABLE)
->eq('role_id', $role_id)
->eq('project_id', $project_id)
->update(array(
'role' => $role,
));
+
+ if ($r1 && $r2 && $r3) {
+ $this->db->closeTransaction();
+ return true;
+ }
+
+ $this->db->cancelTransaction();
+ return false;
}
/**
diff --git a/app/Model/ProjectRoleRestrictionModel.php b/app/Model/ProjectRoleRestrictionModel.php
index 7679f650..dc8abf79 100644
--- a/app/Model/ProjectRoleRestrictionModel.php
+++ b/app/Model/ProjectRoleRestrictionModel.php
@@ -17,15 +17,6 @@ class ProjectRoleRestrictionModel extends Base
const RULE_TASK_CREATION = 'task_creation';
const RULE_TASK_OPEN_CLOSE = 'task_open_close';
- protected $ruleMapping = array(
- self::RULE_TASK_CREATION => array(
- array('controller' => 'TaskCreationController', 'method' => '*'),
- ),
- self::RULE_TASK_OPEN_CLOSE => array(
- array('controller' => 'TaskStatusController', 'method' => '*'),
- )
- );
-
/**
* Get rules
*
@@ -91,7 +82,7 @@ class ProjectRoleRestrictionModel extends Base
*/
public function getAllByRole($project_id, $role)
{
- $rules = $this->db
+ return $this->db
->table(self::TABLE)
->columns(
self::TABLE.'.restriction_id',
@@ -104,12 +95,6 @@ class ProjectRoleRestrictionModel extends Base
->eq('role', $role)
->left(ProjectRoleModel::TABLE, 'pr', 'role_id', self::TABLE, 'role_id')
->findAll();
-
- foreach ($rules as &$rule) {
- $rule['acl'] = $this->ruleMapping[$rule['rule']];
- }
-
- return $rules;
}
/**
@@ -140,31 +125,4 @@ class ProjectRoleRestrictionModel extends Base
{
return $this->db->table(self::TABLE)->eq('restriction_id', $restriction_id)->remove();
}
-
- /**
- * Check if the controller/method is allowed
- *
- * @param array $restrictions
- * @param string $controller
- * @param string $method
- * @return bool
- */
- public function isAllowed(array $restrictions, $controller, $method)
- {
- $controller = strtolower($controller);
- $method = strtolower($method);
-
- foreach ($restrictions as $restriction) {
- foreach ($restriction['acl'] as $acl) {
- $acl['controller'] = strtolower($acl['controller']);
- $acl['method'] = strtolower($acl['method']);
-
- if ($acl['controller'] === $controller && ($acl['method'] === '*' || $acl['method'] === $method)) {
- return false;
- }
- }
- }
-
- return true;
- }
}
diff --git a/app/Model/ProjectUserRoleModel.php b/app/Model/ProjectUserRoleModel.php
index a0df0cfa..76094431 100644
--- a/app/Model/ProjectUserRoleModel.php
+++ b/app/Model/ProjectUserRoleModel.php
@@ -166,7 +166,7 @@ class ProjectUserRoleModel extends Base
->join(UserModel::TABLE, 'id', 'user_id')
->eq(UserModel::TABLE.'.is_active', 1)
->eq(self::TABLE.'.project_id', $project_id)
- ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER))
+ ->neq(self::TABLE.'.role', Role::PROJECT_VIEWER)
->findAll();
$groupMembers = $this->projectGroupRoleModel->getAssignableUsers($project_id);
diff --git a/app/Model/TaskFinderModel.php b/app/Model/TaskFinderModel.php
index 3c32e140..3185afb7 100644
--- a/app/Model/TaskFinderModel.php
+++ b/app/Model/TaskFinderModel.php
@@ -67,6 +67,7 @@ class TaskFinderModel extends Base
TaskModel::TABLE.'.date_due',
TaskModel::TABLE.'.date_creation',
TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.column_id',
TaskModel::TABLE.'.color_id',
TaskModel::TABLE.'.priority',
TaskModel::TABLE.'.time_spent',
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 398b963e..274ce8c8 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,25 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 114;
+const VERSION = 115;
+
+function version_115(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE column_has_restrictions (
+ restriction_id INT NOT NULL AUTO_INCREMENT,
+ project_id INT NOT NULL,
+ role_id INT NOT NULL,
+ column_id INT NOT NULL,
+ rule VARCHAR(255) NOT NULL,
+ UNIQUE(role_id, column_id, rule),
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ FOREIGN KEY(role_id) REFERENCES project_has_roles(role_id) ON DELETE CASCADE,
+ FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
+ PRIMARY KEY(restriction_id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+}
function version_114(PDO $pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index 56cd9de9..213d9869 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,24 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 93;
+const VERSION = 94;
+
+function version_94(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE column_has_restrictions (
+ restriction_id SERIAL PRIMARY KEY,
+ project_id INTEGER NOT NULL,
+ role_id INTEGER NOT NULL,
+ column_id INTEGER NOT NULL,
+ rule VARCHAR(255) NOT NULL,
+ UNIQUE(role_id, column_id, rule),
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ FOREIGN KEY(role_id) REFERENCES project_has_roles(role_id) ON DELETE CASCADE,
+ FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE
+ )
+ ");
+}
function version_93(PDO $pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index c952d58f..f86a6af0 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,24 @@ use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
use PDO;
-const VERSION = 105;
+const VERSION = 106;
+
+function version_106(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE column_has_restrictions (
+ restriction_id INTEGER PRIMARY KEY,
+ project_id INTEGER NOT NULL,
+ role_id INTEGER NOT NULL,
+ column_id INTEGER NOT NULL,
+ rule VARCHAR(255) NOT NULL,
+ UNIQUE(role_id, column_id, rule),
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ FOREIGN KEY(role_id) REFERENCES project_has_roles(role_id) ON DELETE CASCADE,
+ FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE
+ )
+ ");
+}
function version_105(PDO $pdo)
{
diff --git a/app/ServiceProvider/CacheProvider.php b/app/ServiceProvider/CacheProvider.php
index 90d63f81..e93dd502 100644
--- a/app/ServiceProvider/CacheProvider.php
+++ b/app/ServiceProvider/CacheProvider.php
@@ -5,7 +5,9 @@ namespace Kanboard\ServiceProvider;
use Kanboard\Core\Cache\FileCache;
use Kanboard\Core\Cache\MemoryCache;
use Kanboard\Decorator\ColumnMoveRestrictionCacheDecorator;
+use Kanboard\Decorator\ColumnRestrictionCacheDecorator;
use Kanboard\Decorator\MetadataCacheDecorator;
+use Kanboard\Decorator\ProjectRoleRestrictionCacheDecorator;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
@@ -54,6 +56,20 @@ class CacheProvider implements ServiceProviderInterface
);
};
+ $container['columnRestrictionCacheDecorator'] = function($c) {
+ return new ColumnRestrictionCacheDecorator(
+ $c['memoryCache'],
+ $c['columnRestrictionModel']
+ );
+ };
+
+ $container['projectRoleRestrictionCacheDecorator'] = function($c) {
+ return new ProjectRoleRestrictionCacheDecorator(
+ $c['memoryCache'],
+ $c['projectRoleRestrictionModel']
+ );
+ };
+
return $container;
}
}
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index 4841d1f0..c5bf0678 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -34,6 +34,7 @@ class ClassProvider implements ServiceProviderInterface
'CategoryModel',
'ColorModel',
'ColumnModel',
+ 'ColumnRestrictionModel',
'ColumnMoveRestrictionModel',
'CommentModel',
'ConfigModel',
@@ -101,6 +102,7 @@ class ClassProvider implements ServiceProviderInterface
'AuthValidator',
'CategoryValidator',
'ColumnMoveRestrictionValidator',
+ 'ColumnRestrictionValidator',
'ColumnValidator',
'CommentValidator',
'CurrencyValidator',
diff --git a/app/Template/board/table_column.php b/app/Template/board/table_column.php
index c0b71eab..3daa8aed 100644
--- a/app/Template/board/table_column.php
+++ b/app/Template/board/table_column.php
@@ -12,7 +12,7 @@
<!-- column in expanded mode -->
<div class="board-column-expanded">
- <?php if (! $not_editable && $this->user->hasProjectAccess('TaskCreationController', 'show', $column['project_id'])): ?>
+ <?php if (! $not_editable && $this->projectRole->canCreateTaskInColumn($column['project_id'], $column['id'])): ?>
<div class="board-add-icon">
<?= $this->url->link('+', 'TaskCreationController', 'show', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover', t('Add a new task')) ?>
</div>
diff --git a/app/Template/column_move_restriction/create.php b/app/Template/column_move_restriction/create.php
index 8d161c3e..1eb6d539 100644
--- a/app/Template/column_move_restriction/create.php
+++ b/app/Template/column_move_restriction/create.php
@@ -1,6 +1,6 @@
<section id="main">
<div class="page-header">
- <h2><?= t('New column restriction for the role "%s"', $role['role']) ?></h2>
+ <h2><?= t('New drag and drop restriction for the role "%s"', $role['role']) ?></h2>
</div>
<form class="popover-form" method="post" action="<?= $this->url->href('ColumnMoveRestrictionController', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
diff --git a/app/Template/column_restriction/create.php b/app/Template/column_restriction/create.php
new file mode 100644
index 00000000..982733b4
--- /dev/null
+++ b/app/Template/column_restriction/create.php
@@ -0,0 +1,22 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('New column restriction for the role "%s"', $role['role']) ?></h2>
+ </div>
+ <form class="popover-form" method="post" action="<?= $this->url->href('ColumnRestrictionController', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('project_id', $values) ?>
+ <?= $this->form->hidden('role_id', $values) ?>
+
+ <?= $this->form->label(t('Rule'), 'rule') ?>
+ <?= $this->form->select('rule', $rules, $values, $errors) ?>
+
+ <?= $this->form->label(t('Column'), 'column_id') ?>
+ <?= $this->form->select('column_id', $columns, $values, $errors) ?>
+
+ <div class="form-actions">
+ <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'ProjectRoleController', 'show', array(), false, 'close-popover') ?>
+ </div>
+ </form>
+</section>
diff --git a/app/Template/column_restriction/remove.php b/app/Template/column_restriction/remove.php
new file mode 100644
index 00000000..97650e2d
--- /dev/null
+++ b/app/Template/column_restriction/remove.php
@@ -0,0 +1,14 @@
+<div class="page-header">
+ <h2><?= t('Remove a column restriction') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this column restriction?') ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'ColumnRestrictionController', 'remove', array('project_id' => $project['id'], 'restriction_id' => $restriction['restriction_id']), true, 'btn btn-red') ?>
+ <?= t('or') ?> <?= $this->url->link(t('cancel'), 'ProjectRoleController', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
+ </div>
+</div>
diff --git a/app/Template/project_role/show.php b/app/Template/project_role/show.php
index 81281a3e..59200fc9 100644
--- a/app/Template/project_role/show.php
+++ b/app/Template/project_role/show.php
@@ -24,7 +24,11 @@
</li>
<li>
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
- <?= $this->url->link(t('Add a new column restriction'), 'ColumnMoveRestrictionController', 'create', array('project_id' => $project['id'], 'role_id' => $role['role_id']), false, 'popover') ?>
+ <?= $this->url->link(t('Add a new drag and drop restriction'), 'ColumnMoveRestrictionController', 'create', array('project_id' => $project['id'], 'role_id' => $role['role_id']), false, 'popover') ?>
+ </li>
+ <li>
+ <i class="fa fa-plus fa-fw" aria-hidden="true"></i>
+ <?= $this->url->link(t('Add a new column restriction'), 'ColumnRestrictionController', 'create', array('project_id' => $project['id'], 'role_id' => $role['role_id']), false, 'popover') ?>
</li>
<li>
<i class="fa fa-pencil fa-fw" aria-hidden="true"></i>
@@ -41,7 +45,7 @@
<?= t('Actions') ?>
</th>
</tr>
- <?php if (empty($role['project_restrictions']) && empty($role['column_restrictions'])): ?>
+ <?php if (empty($role['project_restrictions']) && empty($role['column_restrictions']) && empty($role['column_move_restrictions'])): ?>
<tr>
<td colspan="2"><?= t('There is no restriction for this role.') ?></td>
</tr>
@@ -49,6 +53,9 @@
<?php foreach ($role['project_restrictions'] as $restriction): ?>
<tr>
<td>
+ <i class="fa fa-ban fa-fw" aria-hidden="true"></i>
+ <strong><?= t('Project') ?></strong>
+ <i class="fa fa-arrow-right fa-fw" aria-hidden="true"></i>
<?= $this->text->e($restriction['title']) ?>
</td>
<td>
@@ -60,7 +67,28 @@
<?php foreach ($role['column_restrictions'] as $restriction): ?>
<tr>
<td>
- <?= t('Only moving task from the column "%s" to "%s" is permitted', $restriction['src_column_title'], $restriction['dst_column_title']) ?>
+ <?php if (strpos($restriction['rule'], 'block') === 0): ?>
+ <i class="fa fa-ban fa-fw" aria-hidden="true"></i>
+ <?php else: ?>
+ <i class="fa fa-check-circle-o fa-fw" aria-hidden="true"></i>
+ <?php endif ?>
+ <strong><?= $this->text->e($restriction['column_title']) ?></strong>
+ <i class="fa fa-arrow-right fa-fw" aria-hidden="true"></i>
+ <?= $this->text->e($restriction['title']) ?>
+ </td>
+ <td>
+ <i class="fa fa-trash-o fa-fw" aria-hidden="true"></i>
+ <?= $this->url->link(t('Remove'), 'ColumnRestrictionController', 'confirm', array('project_id' => $project['id'], 'restriction_id' => $restriction['restriction_id']), false, 'popover') ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ <?php foreach ($role['column_move_restrictions'] as $restriction): ?>
+ <tr>
+ <td>
+ <i class="fa fa-check-circle-o fa-fw" aria-hidden="true"></i>
+ <strong><?= $this->text->e($restriction['src_column_title']) ?> / <?= $this->text->e($restriction['dst_column_title']) ?></strong>
+ <i class="fa fa-arrow-right fa-fw" aria-hidden="true"></i>
+ <?= t('Only moving task between those columns is permitted') ?>
</td>
<td>
<i class="fa fa-trash-o fa-fw" aria-hidden="true"></i>
diff --git a/app/Template/task/dropdown.php b/app/Template/task/dropdown.php
index 127fc89c..f2423dd8 100644
--- a/app/Template/task/dropdown.php
+++ b/app/Template/task/dropdown.php
@@ -49,7 +49,7 @@
<?= $this->url->link(t('Remove'), 'TaskSuppressionController', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
</li>
<?php endif ?>
- <?php if (isset($task['is_active']) && $this->user->hasProjectAccess('TaskStatusController', 'close', $task['project_id'])): ?>
+ <?php if (isset($task['is_active']) && $this->projectRole->canChangeTaskStatusInColumn($task['project_id'], $task['column_id'])): ?>
<li>
<?php if ($task['is_active'] == 1): ?>
<i class="fa fa-times fa-fw"></i>
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index 87fe8cee..640423f4 100644
--- a/app/Template/task/sidebar.php
+++ b/app/Template/task/sidebar.php
@@ -78,7 +78,7 @@
<i class="fa fa-clone fa-fw"></i>
<?= $this->url->link(t('Move to another project'), 'TaskDuplicationController', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
</li>
- <?php if ($this->user->hasProjectAccess('TaskStatusController', 'close', $task['project_id'])): ?>
+ <?php if ($this->projectRole->canChangeTaskStatusInColumn($task['project_id'], $task['column_id'])): ?>
<?php if ($task['is_active'] == 1): ?>
<li>
<i class="fa fa-arrows fa-fw"></i>
diff --git a/app/Validator/ColumnRestrictionValidator.php b/app/Validator/ColumnRestrictionValidator.php
new file mode 100644
index 00000000..b1b2e5a0
--- /dev/null
+++ b/app/Validator/ColumnRestrictionValidator.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Class ColumnRestrictionValidator
+ *
+ * @package Kanboard\Validator
+ * @author Frederic Guillot
+ */
+class ColumnRestrictionValidator extends BaseValidator
+{
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('project_id', t('This field is required')),
+ new Validators\Integer('project_id', t('This value must be an integer')),
+ new Validators\Required('role_id', t('This field is required')),
+ new Validators\Integer('role_id', t('This value must be an integer')),
+ new Validators\Required('rule', t('This field is required')),
+ new Validators\Required('column_id', t('This field is required')),
+ new Validators\Integer('column_id', t('This value must be an integer')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}