diff options
Diffstat (limited to 'app')
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() + ); + } +} |