summaryrefslogtreecommitdiff
path: root/app/Model
diff options
context:
space:
mode:
Diffstat (limited to 'app/Model')
-rw-r--r--app/Model/Acl.php289
-rw-r--r--app/Model/Action.php350
-rw-r--r--app/Model/ActionParameter.php162
-rw-r--r--app/Model/Authentication.php202
-rw-r--r--app/Model/Board.php315
-rw-r--r--app/Model/Category.php73
-rw-r--r--app/Model/Column.php209
-rw-r--r--app/Model/Comment.php70
-rw-r--r--app/Model/Config.php125
-rw-r--r--app/Model/Currency.php53
-rw-r--r--app/Model/CustomFilter.php62
-rw-r--r--app/Model/File.php300
-rw-r--r--app/Model/Group.php117
-rw-r--r--app/Model/GroupMember.php111
-rw-r--r--app/Model/LastLogin.php42
-rw-r--r--app/Model/Link.php45
-rw-r--r--app/Model/Metadata.php13
-rw-r--r--app/Model/Notification.php58
-rw-r--r--app/Model/PasswordReset.php93
-rw-r--r--app/Model/Project.php142
-rw-r--r--app/Model/ProjectActivity.php14
-rw-r--r--app/Model/ProjectAnalytic.php182
-rw-r--r--app/Model/ProjectDailyColumnStats.php280
-rw-r--r--app/Model/ProjectDailyStats.php45
-rw-r--r--app/Model/ProjectDuplication.php140
-rw-r--r--app/Model/ProjectFile.php40
-rw-r--r--app/Model/ProjectGroupRole.php190
-rw-r--r--app/Model/ProjectGroupRoleFilter.php89
-rw-r--r--app/Model/ProjectPermission.php448
-rw-r--r--app/Model/ProjectUserRole.php276
-rw-r--r--app/Model/ProjectUserRoleFilter.php88
-rw-r--r--app/Model/RememberMeSession.php151
-rw-r--r--app/Model/Setting.php6
-rw-r--r--app/Model/Subtask.php192
-rw-r--r--app/Model/SubtaskTimeTracking.php10
-rw-r--r--app/Model/Swimlane.php321
-rw-r--r--app/Model/Task.php25
-rw-r--r--app/Model/TaskAnalytic.php2
-rw-r--r--app/Model/TaskCreation.php21
-rw-r--r--app/Model/TaskDuplication.php8
-rw-r--r--app/Model/TaskExport.php6
-rw-r--r--app/Model/TaskExternalLink.php99
-rw-r--r--app/Model/TaskFile.php54
-rw-r--r--app/Model/TaskFilter.php43
-rw-r--r--app/Model/TaskFinder.php41
-rw-r--r--app/Model/TaskImport.php2
-rw-r--r--app/Model/TaskLink.php64
-rw-r--r--app/Model/TaskModification.php17
-rw-r--r--app/Model/TaskPermission.php4
-rw-r--r--app/Model/TaskPosition.php17
-rw-r--r--app/Model/TaskStatus.php32
-rw-r--r--app/Model/TaskValidator.php246
-rw-r--r--app/Model/Transition.php8
-rw-r--r--app/Model/User.php321
-rw-r--r--app/Model/UserFilter.php80
-rw-r--r--app/Model/UserImport.php18
-rw-r--r--app/Model/UserLocking.php103
-rw-r--r--app/Model/UserMention.php61
-rw-r--r--app/Model/UserNotification.php50
-rw-r--r--app/Model/UserSession.php177
60 files changed, 3065 insertions, 3737 deletions
diff --git a/app/Model/Acl.php b/app/Model/Acl.php
deleted file mode 100644
index 62f850cb..00000000
--- a/app/Model/Acl.php
+++ /dev/null
@@ -1,289 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Access List
- *
- * @package model
- * @author Frederic Guillot
- */
-class Acl extends Base
-{
- /**
- * Controllers and actions allowed from outside
- *
- * @access private
- * @var array
- */
- private $public_acl = array(
- 'auth' => array('login', 'check', 'captcha'),
- 'task' => array('readonly'),
- 'board' => array('readonly'),
- 'webhook' => '*',
- 'ical' => '*',
- 'feed' => '*',
- 'oauth' => array('google', 'github', 'gitlab'),
- );
-
- /**
- * Controllers and actions for project members
- *
- * @access private
- * @var array
- */
- private $project_member_acl = array(
- 'board' => '*',
- 'comment' => '*',
- 'file' => '*',
- 'project' => array('show'),
- 'listing' => '*',
- 'activity' => '*',
- 'subtask' => '*',
- 'task' => '*',
- 'taskduplication' => '*',
- 'taskcreation' => '*',
- 'taskmodification' => '*',
- 'taskstatus' => '*',
- 'tasklink' => '*',
- 'timer' => '*',
- 'customfilter' => '*',
- 'calendar' => array('show', 'project'),
- );
-
- /**
- * Controllers and actions for project managers
- *
- * @access private
- * @var array
- */
- private $project_manager_acl = array(
- 'action' => '*',
- 'analytic' => '*',
- 'category' => '*',
- 'column' => '*',
- 'export' => '*',
- 'taskimport' => '*',
- 'project' => array('edit', 'update', 'share', 'integrations', 'notifications', 'users', 'alloweverybody', 'allow', 'setowner', 'revoke', 'duplicate', 'disable', 'enable'),
- 'swimlane' => '*',
- 'gantt' => array('project', 'savetaskdate', 'task', 'savetask'),
- );
-
- /**
- * Controllers and actions for project admins
- *
- * @access private
- * @var array
- */
- private $project_admin_acl = array(
- 'project' => array('remove'),
- 'projectuser' => '*',
- 'gantt' => array('projects', 'saveprojectdate'),
- );
-
- /**
- * Controllers and actions for admins
- *
- * @access private
- * @var array
- */
- private $admin_acl = array(
- 'user' => array('index', 'create', 'save', 'remove', 'authentication'),
- 'userimport' => '*',
- 'config' => '*',
- 'link' => '*',
- 'currency' => '*',
- '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
- *
- * @access public
- * @param array $acl Acl list
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function matchAcl(array $acl, $controller, $action)
- {
- $controller = strtolower($controller);
- $action = strtolower($action);
- return isset($acl[$controller]) && $this->hasAction($action, $acl[$controller]);
- }
-
- /**
- * Return true if the specified action is inside the list of actions
- *
- * @access public
- * @param string $action Action name
- * @param mixed $action Actions list
- * @return bool
- */
- public function hasAction($action, $actions)
- {
- if (is_array($actions)) {
- return in_array($action, $actions);
- }
-
- return $actions === '*';
- }
-
- /**
- * Return true if the given action is public
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isPublicAction($controller, $action)
- {
- return $this->matchAcl($this->public_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for admins
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isAdminAction($controller, $action)
- {
- return $this->matchAcl($this->admin_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for project managers
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isProjectManagerAction($controller, $action)
- {
- return $this->matchAcl($this->project_manager_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for application managers
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isProjectAdminAction($controller, $action)
- {
- return $this->matchAcl($this->project_admin_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for project members
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isProjectMemberAction($controller, $action)
- {
- return $this->matchAcl($this->project_member_acl, $controller, $action);
- }
-
- /**
- * Return true if the visitor is allowed to access to the given page
- * We suppose the user already authenticated
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @param integer $project_id Project id
- * @return bool
- */
- public function isAllowed($controller, $action, $project_id = 0)
- {
- // If you are admin you have access to everything
- if ($this->userSession->isAdmin()) {
- return true;
- }
-
- // If you access to an admin action, your are not allowed
- if ($this->isAdminAction($controller, $action)) {
- return false;
- }
-
- // Check project admin permissions
- if ($this->isProjectAdminAction($controller, $action)) {
- return $this->handleProjectAdminPermissions($project_id);
- }
-
- // Check project manager permissions
- if ($this->isProjectManagerAction($controller, $action)) {
- return $this->handleProjectManagerPermissions($project_id);
- }
-
- // Check project member permissions
- if ($this->isProjectMemberAction($controller, $action)) {
- return $project_id > 0 && $this->projectPermission->isMember($project_id, $this->userSession->getId());
- }
-
- // Other applications actions are allowed
- return true;
- }
-
- /**
- * Handle permission for project manager
- *
- * @access public
- * @param integer $project_id
- * @return boolean
- */
- public function handleProjectManagerPermissions($project_id)
- {
- if ($project_id > 0) {
- if ($this->userSession->isProjectAdmin()) {
- return $this->projectPermission->isMember($project_id, $this->userSession->getId());
- }
-
- return $this->projectPermission->isManager($project_id, $this->userSession->getId());
- }
-
- return false;
- }
-
- /**
- * Handle permission for project admins
- *
- * @access public
- * @param integer $project_id
- * @return boolean
- */
- public function handleProjectAdminPermissions($project_id)
- {
- if (! $this->userSession->isProjectAdmin()) {
- return false;
- }
-
- if ($project_id > 0) {
- return $this->projectPermission->isMember($project_id, $this->userSession->getId());
- }
-
- return true;
- }
-}
diff --git a/app/Model/Action.php b/app/Model/Action.php
index ba74218f..4da2fb8f 100644
--- a/app/Model/Action.php
+++ b/app/Model/Action.php
@@ -2,14 +2,8 @@
namespace Kanboard\Model;
-use Kanboard\Integration\GitlabWebhook;
-use Kanboard\Integration\GithubWebhook;
-use Kanboard\Integration\BitbucketWebhook;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
/**
- * Action model
+ * Action Model
*
* @package model
* @author Frederic Guillot
@@ -24,152 +18,38 @@ class Action extends Base
const TABLE = 'actions';
/**
- * SQL table name for action parameters
- *
- * @var string
- */
- const TABLE_PARAMS = 'action_has_params';
-
- /**
- * Extended actions
- *
- * @access private
- * @var array
- */
- private $actions = array();
-
- /**
- * Extend the list of default actions
- *
- * @access public
- * @param string $className
- * @param string $description
- * @return Action
- */
- public function extendActions($className, $description)
- {
- $this->actions[$className] = $description;
- return $this;
- }
-
- /**
- * Return the name and description of available actions
- *
- * @access public
- * @return array
- */
- public function getAvailableActions()
- {
- $values = array(
- 'TaskClose' => t('Close a task'),
- 'TaskOpen' => t('Open a task'),
- 'TaskAssignSpecificUser' => t('Assign the task to a specific user'),
- 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'),
- 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'),
- 'TaskMoveAnotherProject' => t('Move the task to another project'),
- 'TaskMoveColumnAssigned' => t('Move the task to another column when assigned to a user'),
- 'TaskMoveColumnUnAssigned' => t('Move the task to another column when assignee is cleared'),
- 'TaskAssignColorColumn' => t('Assign a color when the task is moved to a specific column'),
- 'TaskAssignColorUser' => t('Assign a color to a specific user'),
- 'TaskAssignColorCategory' => t('Assign automatically a color based on a category'),
- 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'),
- 'CommentCreation' => t('Create a comment from an external provider'),
- 'TaskCreation' => t('Create a task from an external provider'),
- 'TaskLogMoveAnotherColumn' => t('Add a comment log when moving the task between columns'),
- 'TaskAssignUser' => t('Change the assignee based on an external username'),
- 'TaskAssignCategoryLabel' => t('Change the category based on an external label'),
- 'TaskUpdateStartDate' => t('Automatically update the start date'),
- 'TaskMoveColumnCategoryChange' => t('Move the task to another column when the category is changed'),
- 'TaskEmail' => t('Send a task by email to someone'),
- 'TaskAssignColorLink' => t('Change task color when using a specific task link'),
- );
-
- $values = array_merge($values, $this->actions);
-
- asort($values);
-
- return $values;
- }
-
- /**
- * Return the name and description of available actions
- *
- * @access public
- * @return array
- */
- public function getAvailableEvents()
- {
- $values = array(
- TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'),
- Task::EVENT_MOVE_COLUMN => t('Move a task to another column'),
- Task::EVENT_UPDATE => t('Task modification'),
- Task::EVENT_CREATE => t('Task creation'),
- Task::EVENT_OPEN => t('Reopen a task'),
- Task::EVENT_CLOSE => t('Closing a task'),
- Task::EVENT_CREATE_UPDATE => t('Task creation or modification'),
- Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'),
- GithubWebhook::EVENT_COMMIT => t('Github commit received'),
- GithubWebhook::EVENT_ISSUE_OPENED => t('Github issue opened'),
- GithubWebhook::EVENT_ISSUE_CLOSED => t('Github issue closed'),
- GithubWebhook::EVENT_ISSUE_REOPENED => t('Github issue reopened'),
- GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Github issue assignee change'),
- GithubWebhook::EVENT_ISSUE_LABEL_CHANGE => t('Github issue label change'),
- GithubWebhook::EVENT_ISSUE_COMMENT => t('Github issue comment created'),
- GitlabWebhook::EVENT_COMMIT => t('Gitlab commit received'),
- GitlabWebhook::EVENT_ISSUE_OPENED => t('Gitlab issue opened'),
- GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'),
- GitlabWebhook::EVENT_ISSUE_COMMENT => t('Gitlab issue comment created'),
- BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'),
- BitbucketWebhook::EVENT_ISSUE_OPENED => t('Bitbucket issue opened'),
- BitbucketWebhook::EVENT_ISSUE_CLOSED => t('Bitbucket issue closed'),
- BitbucketWebhook::EVENT_ISSUE_REOPENED => t('Bitbucket issue reopened'),
- BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Bitbucket issue assignee change'),
- BitbucketWebhook::EVENT_ISSUE_COMMENT => t('Bitbucket issue comment created'),
- );
-
- asort($values);
-
- return $values;
- }
-
- /**
- * Return the name and description of compatible actions
+ * Return actions and parameters for a given user
*
* @access public
- * @param string $action_name Action name
+ * @param integer $user_id
* @return array
*/
- public function getCompatibleEvents($action_name)
+ public function getAllByUser($user_id)
{
- $action = $this->load($action_name, 0, '');
- $compatible_events = $action->getCompatibleEvents();
- $events = array();
+ $project_ids = $this->projectPermission->getActiveProjectIds($user_id);
+ $actions = array();
- foreach ($this->getAvailableEvents() as $event_name => $event_description) {
- if (in_array($event_name, $compatible_events)) {
- $events[$event_name] = $event_description;
- }
+ if (! empty($project_ids)) {
+ $actions = $this->db->table(self::TABLE)->in('project_id', $project_ids)->findAll();
+ $params = $this->actionParameter->getAllByActions(array_column($actions, 'id'));
+ $this->attachParamsToActions($actions, $params);
}
- return $events;
+ return $actions;
}
/**
* Return actions and parameters for a given project
*
* @access public
- * @param $project_id
+ * @param integer $project_id
* @return array
*/
public function getAllByProject($project_id)
{
$actions = $this->db->table(self::TABLE)->eq('project_id', $project_id)->findAll();
-
- foreach ($actions as &$action) {
- $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll();
- }
-
- return $actions;
+ $params = $this->actionParameter->getAllByActions(array_column($actions, 'id'));
+ return $this->attachParamsToActions($actions, $params);
}
/**
@@ -181,63 +61,51 @@ class Action extends Base
public function getAll()
{
$actions = $this->db->table(self::TABLE)->findAll();
- $params = $this->db->table(self::TABLE_PARAMS)->findAll();
-
- foreach ($actions as &$action) {
- $action['params'] = array();
-
- foreach ($params as $param) {
- if ($param['action_id'] === $action['id']) {
- $action['params'][] = $param;
- }
- }
- }
-
- return $actions;
+ $params = $this->actionParameter->getAll();
+ return $this->attachParamsToActions($actions, $params);
}
/**
- * Get all required action parameters for all registered actions
+ * Fetch an action
*
* @access public
- * @return array All required parameters for all actions
+ * @param integer $action_id
+ * @return array
*/
- public function getAllActionParameters()
+ public function getById($action_id)
{
- $params = array();
+ $action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne();
- foreach ($this->getAll() as $action) {
- $action = $this->load($action['action_name'], $action['project_id'], $action['event_name']);
- $params += $action->getActionRequiredParameters();
+ if (! empty($action)) {
+ $action['params'] = $this->actionParameter->getAllByAction($action_id);
}
- return $params;
+ return $action;
}
/**
- * Fetch an action
+ * Attach parameters to actions
*
- * @access public
- * @param integer $action_id Action id
- * @return array Action data
+ * @access private
+ * @param array &$actions
+ * @param array &$params
+ * @return array
*/
- public function getById($action_id)
+ private function attachParamsToActions(array &$actions, array &$params)
{
- $action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne();
-
- if (! empty($action)) {
- $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action_id)->findAll();
+ foreach ($actions as &$action) {
+ $action['params'] = isset($params[$action['id']]) ? $params[$action['id']] : array();
}
- return $action;
+ return $actions;
}
/**
* Remove an action
*
* @access public
- * @param integer $action_id Action id
- * @return bool Success or not
+ * @param integer $action_id
+ * @return bool
*/
public function remove($action_id)
{
@@ -261,24 +129,16 @@ class Action extends Base
'action_name' => $values['action_name'],
);
- if (! $this->db->table(self::TABLE)->save($action)) {
+ if (! $this->db->table(self::TABLE)->insert($action)) {
$this->db->cancelTransaction();
return false;
}
$action_id = $this->db->getLastId();
- foreach ($values['params'] as $param_name => $param_value) {
- $action_param = array(
- 'action_id' => $action_id,
- 'name' => $param_name,
- 'value' => $param_value,
- );
-
- if (! $this->db->table(self::TABLE_PARAMS)->save($action_param)) {
- $this->db->cancelTransaction();
- return false;
- }
+ if (! $this->actionParameter->create($action_id, $values)) {
+ $this->db->cancelTransaction();
+ return false;
}
$this->db->closeTransaction();
@@ -287,42 +147,6 @@ class Action extends Base
}
/**
- * Load all actions and attach events
- *
- * @access public
- */
- public function attachEvents()
- {
- $actions = $this->getAll();
-
- foreach ($actions as $action) {
- $listener = $this->load($action['action_name'], $action['project_id'], $action['event_name']);
-
- foreach ($action['params'] as $param) {
- $listener->setParam($param['name'], $param['value']);
- }
-
- $this->container['dispatcher']->addListener($action['event_name'], array($listener, 'execute'));
- }
- }
-
- /**
- * Load an action
- *
- * @access public
- * @param string $name Action class name
- * @param integer $project_id Project id
- * @param string $event Event name
- * @return \Action\Base
- */
- public function load($name, $project_id, $event)
- {
- $className = $name{0}
- !== '\\' ? '\Kanboard\Action\\'.$name : $name;
- return new $className($this->container, $project_id, $event);
- }
-
- /**
* Copy actions from a project to another one (skip actions that cannot resolve parameters)
*
* @author Antonio Rabelo
@@ -344,15 +168,14 @@ class Action extends Base
);
if (! $this->db->table(self::TABLE)->insert($values)) {
- $this->container['logger']->debug('Action::duplicate => unable to create '.$action['action_name']);
$this->db->cancelTransaction();
continue;
}
$action_id = $this->db->getLastId();
- if (! $this->duplicateParameters($dst_project_id, $action_id, $action['params'])) {
- $this->container['logger']->debug('Action::duplicate => unable to copy parameters for '.$action['action_name']);
+ if (! $this->actionParameter->duplicateParameters($dst_project_id, $action_id, $action['params'])) {
+ $this->logger->error('Action::duplicate => skip action '.$action['action_name'].' '.$action['id']);
$this->db->cancelTransaction();
continue;
}
@@ -362,95 +185,4 @@ class Action extends Base
return true;
}
-
- /**
- * Duplicate action parameters
- *
- * @access public
- * @param integer $project_id
- * @param integer $action_id
- * @param array $params
- * @return boolean
- */
- public function duplicateParameters($project_id, $action_id, array $params)
- {
- foreach ($params as $param) {
- $value = $this->resolveParameters($param, $project_id);
-
- if ($value === false) {
- $this->container['logger']->debug('Action::duplicateParameters => unable to resolve '.$param['name'].'='.$param['value']);
- return false;
- }
-
- $values = array(
- 'action_id' => $action_id,
- 'name' => $param['name'],
- 'value' => $value,
- );
-
- if (! $this->db->table(self::TABLE_PARAMS)->insert($values)) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Resolve action parameter values according to another project
- *
- * @author Antonio Rabelo
- * @access public
- * @param array $param Action parameter
- * @param integer $project_id Project to find the corresponding values
- * @return mixed
- */
- public function resolveParameters(array $param, $project_id)
- {
- switch ($param['name']) {
- case 'project_id':
- return $project_id;
- case 'category_id':
- return $this->category->getIdByName($project_id, $this->category->getNameById($param['value'])) ?: false;
- case 'src_column_id':
- case 'dest_column_id':
- case 'dst_column_id':
- case 'column_id':
- $column = $this->board->getColumn($param['value']);
-
- if (empty($column)) {
- return false;
- }
-
- return $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false;
- case 'user_id':
- case 'owner_id':
- return $this->projectPermission->isMember($project_id, $param['value']) ? $param['value'] : false;
- default:
- return $param['value'];
- }
- }
-
- /**
- * Validate action creation
- *
- * @access public
- * @param array $values Required parameters to save an action
- * @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('The project id is required')),
- new Validators\Integer('project_id', t('This value must be an integer')),
- new Validators\Required('event_name', t('This value is required')),
- new Validators\Required('action_name', t('This value is required')),
- new Validators\Required('params', t('This value is required')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/ActionParameter.php b/app/Model/ActionParameter.php
new file mode 100644
index 00000000..53edcbc8
--- /dev/null
+++ b/app/Model/ActionParameter.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Action Parameter Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ActionParameter extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'action_has_params';
+
+ /**
+ * Get all action params
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ $params = $this->db->table(self::TABLE)->findAll();
+ return $this->toDictionary($params);
+ }
+
+ /**
+ * Get all params for a list of actions
+ *
+ * @access public
+ * @param array $action_ids
+ * @return array
+ */
+ public function getAllByActions(array $action_ids)
+ {
+ $params = $this->db->table(self::TABLE)->in('action_id', $action_ids)->findAll();
+ return $this->toDictionary($params);
+ }
+
+ /**
+ * Build params dictionary
+ *
+ * @access private
+ * @param array $params
+ * @return array
+ */
+ private function toDictionary(array $params)
+ {
+ $result = array();
+
+ foreach ($params as $param) {
+ $result[$param['action_id']][$param['name']] = $param['value'];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get all action params for a given action
+ *
+ * @access public
+ * @param integer $action_id
+ * @return array
+ */
+ public function getAllByAction($action_id)
+ {
+ return $this->db->hashtable(self::TABLE)->eq('action_id', $action_id)->getAll('name', 'value');
+ }
+
+ /**
+ * Insert new parameters for an action
+ *
+ * @access public
+ * @param integer $action_id
+ * @param array $values
+ * @return boolean
+ */
+ public function create($action_id, array $values)
+ {
+ foreach ($values['params'] as $name => $value) {
+ $param = array(
+ 'action_id' => $action_id,
+ 'name' => $name,
+ 'value' => $value,
+ );
+
+ if (! $this->db->table(self::TABLE)->save($param)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Duplicate action parameters
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $action_id
+ * @param array $params
+ * @return boolean
+ */
+ public function duplicateParameters($project_id, $action_id, array $params)
+ {
+ foreach ($params as $name => $value) {
+ $value = $this->resolveParameter($project_id, $name, $value);
+
+ if ($value === false) {
+ $this->logger->error('ActionParameter::duplicateParameters => unable to resolve '.$name.'='.$value);
+ return false;
+ }
+
+ $values = array(
+ 'action_id' => $action_id,
+ 'name' => $name,
+ 'value' => $value,
+ );
+
+ if (! $this->db->table(self::TABLE)->insert($values)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Resolve action parameter values according to another project
+ *
+ * @access private
+ * @param integer $project_id
+ * @param string $name
+ * @param string $value
+ * @return mixed
+ */
+ private function resolveParameter($project_id, $name, $value)
+ {
+ switch ($name) {
+ case 'project_id':
+ return $value != $project_id ? $value : false;
+ case 'category_id':
+ return $this->category->getIdByName($project_id, $this->category->getNameById($value)) ?: false;
+ case 'src_column_id':
+ case 'dest_column_id':
+ case 'dst_column_id':
+ case 'column_id':
+ $column = $this->column->getById($value);
+ return empty($column) ? false : $this->column->getColumnIdByTitle($project_id, $column['title']) ?: false;
+ case 'user_id':
+ case 'owner_id':
+ return $this->projectPermission->isAssignable($project_id, $value) ? $value : false;
+ default:
+ return $value;
+ }
+ }
+}
diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php
deleted file mode 100644
index 580c1e14..00000000
--- a/app/Model/Authentication.php
+++ /dev/null
@@ -1,202 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-use Kanboard\Core\Request;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-use Gregwar\Captcha\CaptchaBuilder;
-
-/**
- * Authentication model
- *
- * @package model
- * @author Frederic Guillot
- */
-class Authentication extends Base
-{
- /**
- * Load automatically an authentication backend
- *
- * @access public
- * @param string $name Backend class name
- * @return mixed
- */
- public function backend($name)
- {
- if (! isset($this->container[$name])) {
- $class = '\Kanboard\Auth\\'.ucfirst($name);
- $this->container[$name] = new $class($this->container);
- }
-
- return $this->container[$name];
- }
-
- /**
- * Check if the current user is authenticated
- *
- * @access public
- * @return bool
- */
- public function isAuthenticated()
- {
- // If the user is already logged it's ok
- if ($this->userSession->isLogged()) {
-
- // Check if the user session match an existing user
- $userNotFound = ! $this->user->exists($this->userSession->getId());
- $reverseProxyWrongUser = REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->getUsername() !== $_SESSION['user']['username'];
-
- if ($userNotFound || $reverseProxyWrongUser) {
- $this->backend('rememberMe')->destroy($this->userSession->getId());
- $this->session->close();
- return false;
- }
-
- return true;
- }
-
- // We try first with the RememberMe cookie
- if (REMEMBER_ME_AUTH && $this->backend('rememberMe')->authenticate()) {
- return true;
- }
-
- // Then with the ReverseProxy authentication
- if (REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->authenticate()) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Authenticate a user by different methods
- *
- * @access public
- * @param string $username Username
- * @param string $password Password
- * @return boolean
- */
- public function authenticate($username, $password)
- {
- if ($this->user->isLocked($username)) {
- $this->container['logger']->error('Account locked: '.$username);
- return false;
- } elseif ($this->backend('database')->authenticate($username, $password)) {
- $this->user->resetFailedLogin($username);
- return true;
- } elseif (LDAP_AUTH && $this->backend('ldap')->authenticate($username, $password)) {
- $this->user->resetFailedLogin($username);
- return true;
- }
-
- $this->handleFailedLogin($username);
- return false;
- }
-
- /**
- * Return true if the captcha must be shown
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function hasCaptcha($username)
- {
- return $this->user->getFailedLogin($username) >= BRUTEFORCE_CAPTCHA;
- }
-
- /**
- * Handle failed login
- *
- * @access public
- * @param string $username
- */
- public function handleFailedLogin($username)
- {
- $this->user->incrementFailedLogin($username);
-
- if ($this->user->getFailedLogin($username) >= BRUTEFORCE_LOCKDOWN) {
- $this->container['logger']->critical('Locking account: '.$username);
- $this->user->lock($username, BRUTEFORCE_LOCKDOWN_DURATION);
- }
- }
-
- /**
- * Validate user login form
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateForm(array $values)
- {
- list($result, $errors) = $this->validateFormCredentials($values);
-
- if ($result) {
- if ($this->validateFormCaptcha($values) && $this->authenticate($values['username'], $values['password'])) {
- $this->createRememberMeSession($values);
- } else {
- $result = false;
- $errors['login'] = t('Bad username or password');
- }
- }
-
- return array($result, $errors);
- }
-
- /**
- * Validate credentials syntax
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateFormCredentials(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('username', t('The username is required')),
- new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
- new Validators\Required('password', t('The password is required')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors(),
- );
- }
-
- /**
- * Validate captcha
- *
- * @access public
- * @param array $values Form values
- * @return boolean
- */
- public function validateFormCaptcha(array $values)
- {
- if ($this->hasCaptcha($values['username'])) {
- $builder = new CaptchaBuilder;
- $builder->setPhrase($this->session['captcha']);
- return $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : '');
- }
-
- return true;
- }
-
- /**
- * Create remember me session if necessary
- *
- * @access private
- * @param array $values Form values
- */
- private function createRememberMeSession(array $values)
- {
- if (REMEMBER_ME_AUTH && ! empty($values['remember_me'])) {
- $credentials = $this->backend('rememberMe')
- ->create($this->userSession->getId(), Request::getIpAddress(), Request::getUserAgent());
-
- $this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
- }
- }
-}
diff --git a/app/Model/Board.php b/app/Model/Board.php
index 79a1a92d..c10be19f 100644
--- a/app/Model/Board.php
+++ b/app/Model/Board.php
@@ -2,10 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-use PicoDb\Database;
-
/**
* Board model
*
@@ -15,13 +11,6 @@ use PicoDb\Database;
class Board extends Base
{
/**
- * SQL table name
- *
- * @var string
- */
- const TABLE = 'columns';
-
- /**
* Get Kanboard default columns
*
* @access public
@@ -75,7 +64,7 @@ class Board extends Base
'description' => $column['description'],
);
- if (! $this->db->table(self::TABLE)->save($values)) {
+ if (! $this->db->table(Column::TABLE)->save($values)) {
return false;
}
}
@@ -93,7 +82,7 @@ class Board extends Base
*/
public function duplicate($project_from, $project_to)
{
- $columns = $this->db->table(Board::TABLE)
+ $columns = $this->db->table(Column::TABLE)
->columns('title', 'task_limit', 'description')
->eq('project_id', $project_from)
->asc('position')
@@ -103,134 +92,6 @@ class Board extends Base
}
/**
- * Add a new column to the board
- *
- * @access public
- * @param integer $project_id Project id
- * @param string $title Column title
- * @param integer $task_limit Task limit
- * @param string $description Column description
- * @return boolean|integer
- */
- public function addColumn($project_id, $title, $task_limit = 0, $description = '')
- {
- $values = array(
- 'project_id' => $project_id,
- 'title' => $title,
- 'task_limit' => intval($task_limit),
- 'position' => $this->getLastColumnPosition($project_id) + 1,
- 'description' => $description,
- );
-
- return $this->persist(self::TABLE, $values);
- }
-
- /**
- * Update a column
- *
- * @access public
- * @param integer $column_id Column id
- * @param string $title Column title
- * @param integer $task_limit Task limit
- * @param string $description Optional description
- * @return boolean
- */
- public function updateColumn($column_id, $title, $task_limit = 0, $description = '')
- {
- return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array(
- 'title' => $title,
- 'task_limit' => intval($task_limit),
- 'description' => $description,
- ));
- }
-
- /**
- * Get columns with consecutive positions
- *
- * If you remove a column, the positions are not anymore consecutives
- *
- * @access public
- * @param integer $project_id
- * @return array
- */
- public function getNormalizedColumnPositions($project_id)
- {
- $columns = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'position');
- $position = 1;
-
- foreach ($columns as $column_id => $column_position) {
- $columns[$column_id] = $position++;
- }
-
- return $columns;
- }
-
- /**
- * Save the new positions for a set of columns
- *
- * @access public
- * @param array $columns Hashmap of column_id/column_position
- * @return boolean
- */
- public function saveColumnPositions(array $columns)
- {
- return $this->db->transaction(function (Database $db) use ($columns) {
-
- foreach ($columns as $column_id => $position) {
- if (! $db->table(Board::TABLE)->eq('id', $column_id)->update(array('position' => $position))) {
- return false;
- }
- }
- });
- }
-
- /**
- * Move a column down, increment the column position value
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $column_id Column id
- * @return boolean
- */
- public function moveDown($project_id, $column_id)
- {
- $columns = $this->getNormalizedColumnPositions($project_id);
- $positions = array_flip($columns);
-
- if (isset($columns[$column_id]) && $columns[$column_id] < count($columns)) {
- $position = ++$columns[$column_id];
- $columns[$positions[$position]]--;
-
- return $this->saveColumnPositions($columns);
- }
-
- return false;
- }
-
- /**
- * Move a column up, decrement the column position value
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $column_id Column id
- * @return boolean
- */
- public function moveUp($project_id, $column_id)
- {
- $columns = $this->getNormalizedColumnPositions($project_id);
- $positions = array_flip($columns);
-
- if (isset($columns[$column_id]) && $columns[$column_id] > 1) {
- $position = --$columns[$column_id];
- $columns[$positions[$position]]++;
-
- return $this->saveColumnPositions($columns);
- }
-
- return false;
- }
-
- /**
* Get all tasks sorted by columns and swimlanes
*
* @access public
@@ -241,7 +102,7 @@ class Board extends Base
public function getBoard($project_id, $callback = null)
{
$swimlanes = $this->swimlane->getSwimlanes($project_id);
- $columns = $this->getColumns($project_id);
+ $columns = $this->column->getAll($project_id);
$nb_columns = count($columns);
for ($i = 0, $ilen = count($swimlanes); $i < $ilen; $i++) {
@@ -309,174 +170,4 @@ class Board extends Base
return $prepend ? array(-1 => t('All columns')) + $listing : $listing;
}
-
- /**
- * Get the first column id for a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @return integer
- */
- public function getFirstColumn($project_id)
- {
- return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id');
- }
-
- /**
- * Get the last column id for a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @return integer
- */
- public function getLastColumn($project_id)
- {
- return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('position')->findOneColumn('id');
- }
-
- /**
- * Get the list of columns sorted by position [ column_id => title ]
- *
- * @access public
- * @param integer $project_id Project id
- * @param boolean $prepend Prepend a default value
- * @return array
- */
- public function getColumnsList($project_id, $prepend = false)
- {
- $listing = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'title');
- return $prepend ? array(-1 => t('All columns')) + $listing : $listing;
- }
-
- /**
- * Get all columns sorted by position for a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getColumns($project_id)
- {
- return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll();
- }
-
- /**
- * Get the number of columns for a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @return integer
- */
- public function countColumns($project_id)
- {
- return $this->db->table(self::TABLE)->eq('project_id', $project_id)->count();
- }
-
- /**
- * Get a column by the id
- *
- * @access public
- * @param integer $column_id Column id
- * @return array
- */
- public function getColumn($column_id)
- {
- return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne();
- }
-
- /**
- * Get a column id by the name
- *
- * @access public
- * @param integer $project_id
- * @param string $title
- * @return integer
- */
- public function getColumnIdByTitle($project_id, $title)
- {
- return (int) $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('title', $title)->findOneColumn('id');
- }
-
- /**
- * Get a column title by the id
- *
- * @access public
- * @param integer $column_id
- * @return integer
- */
- public function getColumnTitleById($column_id)
- {
- return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('title');
- }
-
- /**
- * Get the position of the last column for a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @return integer
- */
- public function getLastColumnPosition($project_id)
- {
- return (int) $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->desc('position')
- ->findOneColumn('position');
- }
-
- /**
- * Remove a column and all tasks associated to this column
- *
- * @access public
- * @param integer $column_id Column id
- * @return boolean
- */
- public function removeColumn($column_id)
- {
- return $this->db->table(self::TABLE)->eq('id', $column_id)->remove();
- }
-
- /**
- * Validate column modification
- *
- * @access public
- * @param array $values Required parameters to update a column
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Integer('task_limit', t('This value must be an integer')),
- new Validators\Required('title', t('The title is required')),
- new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate column creation
- *
- * @access public
- * @param array $values Required parameters to save an action
- * @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('The project id is required')),
- new Validators\Integer('project_id', t('This value must be an integer')),
- new Validators\Required('title', t('The title is required')),
- new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/Category.php b/app/Model/Category.php
index bf40c60a..883fc282 100644
--- a/app/Model/Category.php
+++ b/app/Model/Category.php
@@ -2,9 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
/**
* Category model
*
@@ -196,11 +193,12 @@ class Category extends Base
*/
public function duplicate($src_project_id, $dst_project_id)
{
- $categories = $this->db->table(self::TABLE)
- ->columns('name')
- ->eq('project_id', $src_project_id)
- ->asc('name')
- ->findAll();
+ $categories = $this->db
+ ->table(self::TABLE)
+ ->columns('name')
+ ->eq('project_id', $src_project_id)
+ ->asc('name')
+ ->findAll();
foreach ($categories as $category) {
$category['project_id'] = $dst_project_id;
@@ -212,63 +210,4 @@ class Category extends Base
return true;
}
-
- /**
- * Validate category 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)
- {
- $rules = array(
- new Validators\Required('project_id', t('The project id is required')),
- new Validators\Required('name', t('The name is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate category modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- new Validators\Required('name', t('The name is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('The id must be an integer')),
- new Validators\Integer('project_id', t('The project id must be an integer')),
- new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50)
- );
- }
}
diff --git a/app/Model/Column.php b/app/Model/Column.php
new file mode 100644
index 00000000..ccdcb049
--- /dev/null
+++ b/app/Model/Column.php
@@ -0,0 +1,209 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Column Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Column extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'columns';
+
+ /**
+ * Get a column by the id
+ *
+ * @access public
+ * @param integer $column_id Column id
+ * @return array
+ */
+ public function getById($column_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne();
+ }
+
+ /**
+ * Get the first column id for a given project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return integer
+ */
+ public function getFirstColumnId($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id');
+ }
+
+ /**
+ * Get the last column id for a given project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return integer
+ */
+ public function getLastColumnId($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('position')->findOneColumn('id');
+ }
+
+ /**
+ * Get the position of the last column for a given project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return integer
+ */
+ public function getLastColumnPosition($project_id)
+ {
+ return (int) $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->desc('position')
+ ->findOneColumn('position');
+ }
+
+ /**
+ * Get a column id by the name
+ *
+ * @access public
+ * @param integer $project_id
+ * @param string $title
+ * @return integer
+ */
+ public function getColumnIdByTitle($project_id, $title)
+ {
+ return (int) $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('title', $title)->findOneColumn('id');
+ }
+
+ /**
+ * Get a column title by the id
+ *
+ * @access public
+ * @param integer $column_id
+ * @return integer
+ */
+ public function getColumnTitleById($column_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('title');
+ }
+
+ /**
+ * Get all columns sorted by position for a given project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function getAll($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll();
+ }
+
+ /**
+ * Get the list of columns sorted by position [ column_id => title ]
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param boolean $prepend Prepend a default value
+ * @return array
+ */
+ public function getList($project_id, $prepend = false)
+ {
+ $listing = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'title');
+ return $prepend ? array(-1 => t('All columns')) + $listing : $listing;
+ }
+
+ /**
+ * Add a new column to the board
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $title Column title
+ * @param integer $task_limit Task limit
+ * @param string $description Column description
+ * @return boolean|integer
+ */
+ public function create($project_id, $title, $task_limit = 0, $description = '')
+ {
+ $values = array(
+ 'project_id' => $project_id,
+ 'title' => $title,
+ 'task_limit' => intval($task_limit),
+ 'position' => $this->getLastColumnPosition($project_id) + 1,
+ 'description' => $description,
+ );
+
+ return $this->persist(self::TABLE, $values);
+ }
+
+ /**
+ * Update a column
+ *
+ * @access public
+ * @param integer $column_id Column id
+ * @param string $title Column title
+ * @param integer $task_limit Task limit
+ * @param string $description Optional description
+ * @return boolean
+ */
+ public function update($column_id, $title, $task_limit = 0, $description = '')
+ {
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array(
+ 'title' => $title,
+ 'task_limit' => intval($task_limit),
+ 'description' => $description,
+ ));
+ }
+
+ /**
+ * Remove a column and all tasks associated to this column
+ *
+ * @access public
+ * @param integer $column_id Column id
+ * @return boolean
+ */
+ public function remove($column_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->remove();
+ }
+
+ /**
+ * Change column position
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $column_id
+ * @param integer $position
+ * @return boolean
+ */
+ public function changePosition($project_id, $column_id, $position)
+ {
+ if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('project_id', $project_id)->count()) {
+ return false;
+ }
+
+ $column_ids = $this->db->table(self::TABLE)->eq('project_id', $project_id)->neq('id', $column_id)->asc('position')->findAllByColumn('id');
+ $offset = 1;
+ $results = array();
+
+ foreach ($column_ids as $current_column_id) {
+ if ($offset == $position) {
+ $offset++;
+ }
+
+ $results[] = $this->db->table(self::TABLE)->eq('id', $current_column_id)->update(array('position' => $offset));
+ $offset++;
+ }
+
+ $results[] = $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position));
+
+ return !in_array(false, $results, true);
+ }
+}
diff --git a/app/Model/Comment.php b/app/Model/Comment.php
index c7125a25..6eb4a1e5 100644
--- a/app/Model/Comment.php
+++ b/app/Model/Comment.php
@@ -3,8 +3,6 @@
namespace Kanboard\Model;
use Kanboard\Event\CommentEvent;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
/**
* Comment model
@@ -26,8 +24,9 @@ class Comment extends Base
*
* @var string
*/
- const EVENT_UPDATE = 'comment.update';
- const EVENT_CREATE = 'comment.create';
+ const EVENT_UPDATE = 'comment.update';
+ const EVENT_CREATE = 'comment.create';
+ const EVENT_USER_MENTION = 'comment.user.mention';
/**
* Get all comments for a given task
@@ -74,6 +73,7 @@ class Comment extends Base
self::TABLE.'.user_id',
self::TABLE.'.date_creation',
self::TABLE.'.comment',
+ self::TABLE.'.reference',
User::TABLE.'.username',
User::TABLE.'.name'
)
@@ -110,7 +110,9 @@ class Comment extends Base
$comment_id = $this->persist(self::TABLE, $values);
if ($comment_id) {
- $this->container['dispatcher']->dispatch(self::EVENT_CREATE, new CommentEvent(array('id' => $comment_id) + $values));
+ $event = new CommentEvent(array('id' => $comment_id) + $values);
+ $this->dispatcher->dispatch(self::EVENT_CREATE, $event);
+ $this->userMention->fireEvents($values['comment'], self::EVENT_USER_MENTION, $event);
}
return $comment_id;
@@ -148,62 +150,4 @@ class Comment extends Base
{
return $this->db->table(self::TABLE)->eq('id', $comment_id)->remove();
}
-
- /**
- * Validate comment creation
- *
- * @access public
- * @param array $values Required parameters to save an action
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCreation(array $values)
- {
- $rules = array(
- new Validators\Required('task_id', t('This value is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate comment modification
- *
- * @access public
- * @param array $values Required parameters to save an action
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('This value is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('This value must be an integer')),
- new Validators\Integer('task_id', t('This value must be an integer')),
- new Validators\Integer('user_id', t('This value must be an integer')),
- new Validators\Required('comment', t('Comment is required'))
- );
- }
}
diff --git a/app/Model/Config.php b/app/Model/Config.php
index cf634f80..6f009175 100644
--- a/app/Model/Config.php
+++ b/app/Model/Config.php
@@ -3,8 +3,7 @@
namespace Kanboard\Model;
use Kanboard\Core\Translator;
-use Kanboard\Core\Security;
-use Kanboard\Core\Session;
+use Kanboard\Core\Security\Token;
/**
* Config model
@@ -15,30 +14,6 @@ use Kanboard\Core\Session;
class Config extends Setting
{
/**
- * Get available currencies
- *
- * @access public
- * @return array
- */
- public function getCurrencies()
- {
- return array(
- 'USD' => t('USD - US Dollar'),
- 'EUR' => t('EUR - Euro'),
- 'GBP' => t('GBP - British Pound'),
- 'CHF' => t('CHF - Swiss Francs'),
- 'CAD' => t('CAD - Canadian Dollar'),
- 'AUD' => t('AUD - Australian Dollar'),
- 'NZD' => t('NZD - New Zealand Dollar'),
- 'INR' => t('INR - Indian Rupee'),
- 'JPY' => t('JPY - Japanese Yen'),
- 'RSD' => t('RSD - Serbian dinar'),
- 'SEK' => t('SEK - Swedish Krona'),
- 'NOK' => t('NOK - Norwegian Krone'),
- );
- }
-
- /**
* Get available timezones
*
* @access public
@@ -58,6 +33,31 @@ class Config extends Setting
}
/**
+ * Get current timezone
+ *
+ * @access public
+ * @return string
+ */
+ public function getCurrentTimezone()
+ {
+ if ($this->userSession->isLogged() && ! empty($this->sessionStorage->user['timezone'])) {
+ return $this->sessionStorage->user['timezone'];
+ }
+
+ return $this->get('application_timezone', 'UTC');
+ }
+
+ /**
+ * Set timezone
+ *
+ * @access public
+ */
+ public function setupTimezone()
+ {
+ date_default_timezone_set($this->getCurrentTimezone());
+ }
+
+ /**
* Get available languages
*
* @access public
@@ -69,14 +69,17 @@ class Config extends Setting
// Sorted by value
$languages = array(
'id_ID' => 'Bahasa Indonesia',
+ 'bs_BA' => 'Bosanski',
'cs_CZ' => 'Čeština',
'da_DK' => 'Dansk',
'de_DE' => 'Deutsch',
'en_US' => 'English',
'es_ES' => 'Español',
'fr_FR' => 'Français',
+ 'el_GR' => 'Grec',
'it_IT' => 'Italiano',
'hu_HU' => 'Magyar',
+ 'my_MY' => 'Melayu',
'nl_NL' => 'Nederlands',
'nb_NO' => 'Norsk',
'pl_PL' => 'Polski',
@@ -108,7 +111,7 @@ class Config extends Setting
public function getJsLanguageCode()
{
$languages = array(
- 'cs_CZ' => 'cz',
+ 'cs_CZ' => 'cs',
'da_DK' => 'da',
'de_DE' => 'de',
'en_US' => 'en',
@@ -129,7 +132,8 @@ class Config extends Setting
'zh_CN' => 'zh-cn',
'ja_JP' => 'ja',
'th_TH' => 'th',
- 'id_ID' => 'id'
+ 'id_ID' => 'id',
+ 'el_GR' => 'el',
);
$lang = $this->getCurrentLanguage();
@@ -145,51 +149,14 @@ class Config extends Setting
*/
public function getCurrentLanguage()
{
- if ($this->userSession->isLogged() && ! empty($this->session['user']['language'])) {
- return $this->session['user']['language'];
+ if ($this->userSession->isLogged() && ! empty($this->sessionStorage->user['language'])) {
+ return $this->sessionStorage->user['language'];
}
return $this->get('application_language', 'en_US');
}
/**
- * Get a config variable from the session or the database
- *
- * @access public
- * @param string $name Parameter name
- * @param string $default_value Default value of the parameter
- * @return string
- */
- public function get($name, $default_value = '')
- {
- if (! Session::isOpen()) {
- return $this->getOption($name, $default_value);
- }
-
- // Cache config in session
- if (! isset($this->session['config'][$name])) {
- $this->session['config'] = $this->getAll();
- }
-
- if (! empty($this->session['config'][$name])) {
- return $this->session['config'][$name];
- }
-
- return $default_value;
- }
-
- /**
- * Reload settings in the session and the translations
- *
- * @access public
- */
- public function reload()
- {
- $this->session['config'] = $this->getAll();
- $this->setupTranslations();
- }
-
- /**
* Load translations
*
* @access public
@@ -200,28 +167,27 @@ class Config extends Setting
}
/**
- * Get current timezone
+ * Get a config variable from the session or the database
*
* @access public
+ * @param string $name Parameter name
+ * @param string $default_value Default value of the parameter
* @return string
*/
- public function getCurrentTimezone()
+ public function get($name, $default_value = '')
{
- if ($this->userSession->isLogged() && ! empty($this->session['user']['timezone'])) {
- return $this->session['user']['timezone'];
- }
-
- return $this->get('application_timezone', 'UTC');
+ $options = $this->memoryCache->proxy($this, 'getAll');
+ return isset($options[$name]) && $options[$name] !== '' ? $options[$name] : $default_value;
}
/**
- * Set timezone
+ * Reload settings in the session and the translations
*
* @access public
*/
- public function setupTimezone()
+ public function reload()
{
- date_default_timezone_set($this->getCurrentTimezone());
+ $this->setupTranslations();
}
/**
@@ -232,7 +198,7 @@ class Config extends Setting
*/
public function optimizeDatabase()
{
- return $this->db->getconnection()->exec("VACUUM");
+ return $this->db->getconnection()->exec('VACUUM');
}
/**
@@ -262,10 +228,11 @@ class Config extends Setting
*
* @access public
* @param string $option Parameter name
+ * @return boolean
*/
public function regenerateToken($option)
{
- $this->save(array($option => Security::generateToken()));
+ return $this->save(array($option => Token::getToken()));
}
/**
diff --git a/app/Model/Currency.php b/app/Model/Currency.php
index c1156610..abcce2f0 100644
--- a/app/Model/Currency.php
+++ b/app/Model/Currency.php
@@ -2,9 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
/**
* Currency
*
@@ -21,6 +18,32 @@ class Currency extends Base
const TABLE = 'currencies';
/**
+ * Get available application currencies
+ *
+ * @access public
+ * @return array
+ */
+ public function getCurrencies()
+ {
+ return array(
+ 'USD' => t('USD - US Dollar'),
+ 'EUR' => t('EUR - Euro'),
+ 'GBP' => t('GBP - British Pound'),
+ 'CHF' => t('CHF - Swiss Francs'),
+ 'CAD' => t('CAD - Canadian Dollar'),
+ 'AUD' => t('AUD - Australian Dollar'),
+ 'NZD' => t('NZD - New Zealand Dollar'),
+ 'INR' => t('INR - Indian Rupee'),
+ 'JPY' => t('JPY - Japanese Yen'),
+ 'RSD' => t('RSD - Serbian dinar'),
+ 'SEK' => t('SEK - Swedish Krona'),
+ 'NOK' => t('NOK - Norwegian Krone'),
+ 'BAM' => t('BAM - Konvertible Mark'),
+ 'RUB' => t('RUB - Russian Ruble'),
+ );
+ }
+
+ /**
* Get all currency rates
*
* @access public
@@ -45,7 +68,7 @@ class Currency extends Base
$reference = $this->config->get('application_currency', 'USD');
if ($reference !== $currency) {
- $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : array();
+ $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : $rates;
$rate = isset($rates[$currency]) ? $rates[$currency] : 1;
return $rate * $price;
@@ -68,7 +91,7 @@ class Currency extends Base
return $this->update($currency, $rate);
}
- return $this->persist(self::TABLE, compact('currency', 'rate'));
+ return $this->db->table(self::TABLE)->insert(array('currency' => $currency, 'rate' => $rate));
}
/**
@@ -83,24 +106,4 @@ class Currency extends Base
{
return $this->db->table(self::TABLE)->eq('currency', $currency)->update(array('rate' => $rate));
}
-
- /**
- * Validate
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validate(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('currency', t('Field required')),
- new Validators\Required('rate', t('Field required')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/CustomFilter.php b/app/Model/CustomFilter.php
index 6550b4a7..3a6a1a3a 100644
--- a/app/Model/CustomFilter.php
+++ b/app/Model/CustomFilter.php
@@ -2,9 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
/**
* Custom Filter model
*
@@ -102,63 +99,4 @@ class CustomFilter extends Base
{
return $this->db->table(self::TABLE)->eq('id', $filter_id)->remove();
}
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Required('project_id', t('Field required')),
- new Validators\Required('user_id', t('Field required')),
- new Validators\Required('name', t('Field required')),
- new Validators\Required('filter', t('Field required')),
- new Validators\Integer('user_id', t('This value must be an integer')),
- new Validators\Integer('project_id', t('This value must be an integer')),
- new Validators\MaxLength('name', t('The maximum length is %d characters', 100), 100),
- new Validators\MaxLength('filter', t('The maximum length is %d characters', 100), 100)
- );
- }
-
- /**
- * Validate filter 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, $this->commonValidationRules());
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate filter modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('Field required')),
- new Validators\Integer('id', t('This value must be an integer')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/File.php b/app/Model/File.php
index daade517..03ea691d 100644
--- a/app/Model/File.php
+++ b/app/Model/File.php
@@ -2,31 +2,44 @@
namespace Kanboard\Model;
+use Exception;
use Kanboard\Event\FileEvent;
use Kanboard\Core\Tool;
use Kanboard\Core\ObjectStorage\ObjectStorageException;
/**
- * File model
+ * Base File Model
*
* @package model
* @author Frederic Guillot
*/
-class File extends Base
+abstract class File extends Base
{
/**
- * SQL table name
- *
- * @var string
- */
- const TABLE = 'files';
-
- /**
- * Events
+ * Get PicoDb query to get all files
*
- * @var string
+ * @access protected
+ * @return \PicoDb\Table
*/
- const EVENT_CREATE = 'file.create';
+ protected function getQuery()
+ {
+ return $this->db
+ ->table(static::TABLE)
+ ->columns(
+ static::TABLE.'.id',
+ static::TABLE.'.name',
+ static::TABLE.'.path',
+ static::TABLE.'.is_image',
+ static::TABLE.'.'.static::FOREIGN_KEY,
+ static::TABLE.'.date',
+ static::TABLE.'.user_id',
+ static::TABLE.'.size',
+ User::TABLE.'.username',
+ User::TABLE.'.name as user_name'
+ )
+ ->join(User::TABLE, 'id', 'user_id')
+ ->asc(static::TABLE.'.name');
+ }
/**
* Get a file by the id
@@ -37,146 +50,120 @@ class File extends Base
*/
public function getById($file_id)
{
- return $this->db->table(self::TABLE)->eq('id', $file_id)->findOne();
+ return $this->db->table(static::TABLE)->eq('id', $file_id)->findOne();
}
/**
- * Remove a file
+ * Get all files
*
* @access public
- * @param integer $file_id File id
- * @return bool
+ * @param integer $id
+ * @return array
*/
- public function remove($file_id)
+ public function getAll($id)
{
- try {
- $file = $this->getbyId($file_id);
- $this->objectStorage->remove($file['path']);
-
- if ($file['is_image'] == 1) {
- $this->objectStorage->remove($this->getThumbnailPath($file['path']));
- }
-
- return $this->db->table(self::TABLE)->eq('id', $file['id'])->remove();
- } catch (ObjectStorageException $e) {
- $this->logger->error($e->getMessage());
- return false;
- }
+ return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->findAll();
}
/**
- * Remove all files for a given task
+ * Get all images
*
* @access public
- * @param integer $task_id Task id
- * @return bool
+ * @param integer $id
+ * @return array
*/
- public function removeAll($task_id)
+ public function getAllImages($id)
{
- $file_ids = $this->db->table(self::TABLE)->eq('task_id', $task_id)->asc('id')->findAllByColumn('id');
- $results = array();
-
- foreach ($file_ids as $file_id) {
- $results[] = $this->remove($file_id);
- }
+ return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->eq('is_image', 1)->findAll();
+ }
- return ! in_array(false, $results, true);
+ /**
+ * Get all files without images
+ *
+ * @access public
+ * @param integer $id
+ * @return array
+ */
+ public function getAllDocuments($id)
+ {
+ return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->eq('is_image', 0)->findAll();
}
/**
* Create a file entry in the database
*
* @access public
- * @param integer $task_id Task id
+ * @param integer $id Foreign key
* @param string $name Filename
* @param string $path Path on the disk
* @param integer $size File size
* @return bool|integer
*/
- public function create($task_id, $name, $path, $size)
+ public function create($id, $name, $path, $size)
{
- $result = $this->db->table(self::TABLE)->save(array(
- 'task_id' => $task_id,
+ $values = array(
+ static::FOREIGN_KEY => $id,
'name' => substr($name, 0, 255),
'path' => $path,
'is_image' => $this->isImage($name) ? 1 : 0,
'size' => $size,
'user_id' => $this->userSession->getId() ?: 0,
'date' => time(),
- ));
+ );
- if ($result) {
- $this->container['dispatcher']->dispatch(
- self::EVENT_CREATE,
- new FileEvent(array('task_id' => $task_id, 'name' => $name))
- );
+ $result = $this->db->table(static::TABLE)->insert($values);
- return (int) $this->db->getLastId();
+ if ($result) {
+ $file_id = (int) $this->db->getLastId();
+ $event = new FileEvent($values + array('file_id' => $file_id));
+ $this->dispatcher->dispatch(static::EVENT_CREATE, $event);
+ return $file_id;
}
return false;
}
/**
- * Get PicoDb query to get all files
+ * Remove all files
*
* @access public
- * @return \PicoDb\Table
+ * @param integer $id
+ * @return bool
*/
- public function getQuery()
+ public function removeAll($id)
{
- return $this->db
- ->table(self::TABLE)
- ->columns(
- self::TABLE.'.id',
- self::TABLE.'.name',
- self::TABLE.'.path',
- self::TABLE.'.is_image',
- self::TABLE.'.task_id',
- self::TABLE.'.date',
- self::TABLE.'.user_id',
- self::TABLE.'.size',
- User::TABLE.'.username',
- User::TABLE.'.name as user_name'
- )
- ->join(User::TABLE, 'id', 'user_id')
- ->asc(self::TABLE.'.name');
- }
+ $file_ids = $this->db->table(static::TABLE)->eq(static::FOREIGN_KEY, $id)->asc('id')->findAllByColumn('id');
+ $results = array();
- /**
- * Get all files for a given task
- *
- * @access public
- * @param integer $task_id Task id
- * @return array
- */
- public function getAll($task_id)
- {
- return $this->getQuery()->eq('task_id', $task_id)->findAll();
- }
+ foreach ($file_ids as $file_id) {
+ $results[] = $this->remove($file_id);
+ }
- /**
- * Get all images for a given task
- *
- * @access public
- * @param integer $task_id Task id
- * @return array
- */
- public function getAllImages($task_id)
- {
- return $this->getQuery()->eq('task_id', $task_id)->eq('is_image', 1)->findAll();
+ return ! in_array(false, $results, true);
}
/**
- * Get all files without images for a given task
+ * Remove a file
*
* @access public
- * @param integer $task_id Task id
- * @return array
+ * @param integer $file_id File id
+ * @return bool
*/
- public function getAllDocuments($task_id)
+ public function remove($file_id)
{
- return $this->getQuery()->eq('task_id', $task_id)->eq('is_image', 0)->findAll();
+ try {
+ $file = $this->getById($file_id);
+ $this->objectStorage->remove($file['path']);
+
+ if ($file['is_image'] == 1) {
+ $this->objectStorage->remove($this->getThumbnailPath($file['path']));
+ }
+
+ return $this->db->table(static::TABLE)->eq('id', $file['id'])->remove();
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
}
/**
@@ -202,125 +189,96 @@ class File extends Base
}
/**
- * Return the image mimetype based on the file extension
+ * Generate the path for a thumbnails
*
* @access public
- * @param $filename
+ * @param string $key Storage key
* @return string
*/
- public function getImageMimeType($filename)
+ public function getThumbnailPath($key)
{
- $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
-
- switch ($extension) {
- case 'jpeg':
- case 'jpg':
- return 'image/jpeg';
- case 'png':
- return 'image/png';
- case 'gif':
- return 'image/gif';
- default:
- return 'image/jpeg';
- }
+ return 'thumbnails'.DIRECTORY_SEPARATOR.$key;
}
/**
* Generate the path for a new filename
*
* @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
+ * @param integer $id Foreign key
* @param string $filename Filename
* @return string
*/
- public function generatePath($project_id, $task_id, $filename)
- {
- return $project_id.DIRECTORY_SEPARATOR.$task_id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time());
- }
-
- /**
- * Generate the path for a thumbnails
- *
- * @access public
- * @param string $key Storage key
- * @return string
- */
- public function getThumbnailPath($key)
+ public function generatePath($id, $filename)
{
- return 'thumbnails'.DIRECTORY_SEPARATOR.$key;
+ return static::PATH_PREFIX.DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time());
}
/**
- * Handle file upload
+ * Upload multiple files
*
* @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
- * @param string $form_name File form name
+ * @param integer $id
+ * @param array $files
* @return bool
*/
- public function uploadFiles($project_id, $task_id, $form_name)
+ public function uploadFiles($id, array $files)
{
try {
- if (empty($_FILES[$form_name])) {
+ if (empty($files)) {
return false;
}
- foreach ($_FILES[$form_name]['error'] as $key => $error) {
- if ($error == UPLOAD_ERR_OK && $_FILES[$form_name]['size'][$key] > 0) {
- $original_filename = $_FILES[$form_name]['name'][$key];
- $uploaded_filename = $_FILES[$form_name]['tmp_name'][$key];
- $destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
-
- if ($this->isImage($original_filename)) {
- $this->generateThumbnailFromFile($uploaded_filename, $destination_filename);
- }
-
- $this->objectStorage->moveUploadedFile($uploaded_filename, $destination_filename);
-
- $this->create(
- $task_id,
- $original_filename,
- $destination_filename,
- $_FILES[$form_name]['size'][$key]
- );
- }
+ foreach (array_keys($files['error']) as $key) {
+ $file = array(
+ 'name' => $files['name'][$key],
+ 'tmp_name' => $files['tmp_name'][$key],
+ 'size' => $files['size'][$key],
+ 'error' => $files['error'][$key],
+ );
+
+ $this->uploadFile($id, $file);
}
return true;
- } catch (ObjectStorageException $e) {
+ } catch (Exception $e) {
$this->logger->error($e->getMessage());
return false;
}
}
/**
- * Handle screenshot upload
+ * Upload a file
*
* @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
- * @param string $blob Base64 encoded image
- * @return bool|integer
+ * @param integer $id
+ * @param array $file
*/
- public function uploadScreenshot($project_id, $task_id, $blob)
+ public function uploadFile($id, array $file)
{
- $original_filename = e('Screenshot taken %s', dt('%B %e, %Y at %k:%M %p', time())).'.png';
- return $this->uploadContent($project_id, $task_id, $original_filename, $blob);
+ if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) {
+ $destination_filename = $this->generatePath($id, $file['name']);
+
+ if ($this->isImage($file['name'])) {
+ $this->generateThumbnailFromFile($file['tmp_name'], $destination_filename);
+ }
+
+ $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename);
+ $this->create($id, $file['name'], $destination_filename, $file['size']);
+ } else {
+ throw new Exception('File not uploaded: '.var_export($file['error'], true));
+ }
}
/**
* Handle file upload (base64 encoded content)
*
* @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
- * @param string $original_filename Filename
- * @param string $blob Base64 encoded file
+ * @param integer $id
+ * @param string $original_filename
+ * @param string $blob
* @return bool|integer
*/
- public function uploadContent($project_id, $task_id, $original_filename, $blob)
+ public function uploadContent($id, $original_filename, $blob)
{
try {
$data = base64_decode($blob);
@@ -329,7 +287,7 @@ class File extends Base
return false;
}
- $destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
+ $destination_filename = $this->generatePath($id, $original_filename);
$this->objectStorage->put($destination_filename, $data);
if ($this->isImage($original_filename)) {
@@ -337,7 +295,7 @@ class File extends Base
}
return $this->create(
- $task_id,
+ $id,
$original_filename,
$destination_filename,
strlen($data)
diff --git a/app/Model/Group.php b/app/Model/Group.php
new file mode 100644
index 00000000..67899503
--- /dev/null
+++ b/app/Model/Group.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Group Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Group extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'groups';
+
+ /**
+ * Get query to fetch all groups
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getQuery()
+ {
+ return $this->db->table(self::TABLE);
+ }
+
+ /**
+ * Get a specific group by id
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getById($group_id)
+ {
+ return $this->getQuery()->eq('id', $group_id)->findOne();
+ }
+
+ /**
+ * Get a specific group by external id
+ *
+ * @access public
+ * @param integer $external_id
+ * @return array
+ */
+ public function getByExternalId($external_id)
+ {
+ return $this->getQuery()->eq('external_id', $external_id)->findOne();
+ }
+
+ /**
+ * Get all groups
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ return $this->getQuery()->asc('name')->findAll();
+ }
+
+ /**
+ * Search groups by name
+ *
+ * @access public
+ * @param string $input
+ * @return array
+ */
+ public function search($input)
+ {
+ return $this->db->table(self::TABLE)->ilike('name', '%'.$input.'%')->asc('name')->findAll();
+ }
+
+ /**
+ * Remove a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function remove($group_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $group_id)->remove();
+ }
+
+ /**
+ * Create a new group
+ *
+ * @access public
+ * @param string $name
+ * @param string $external_id
+ * @return integer|boolean
+ */
+ public function create($name, $external_id = '')
+ {
+ return $this->persist(self::TABLE, array(
+ 'name' => $name,
+ 'external_id' => $external_id,
+ ));
+ }
+
+ /**
+ * Update existing group
+ *
+ * @access public
+ * @param array $values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
+ }
+}
diff --git a/app/Model/GroupMember.php b/app/Model/GroupMember.php
new file mode 100644
index 00000000..7ed5f733
--- /dev/null
+++ b/app/Model/GroupMember.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Group Member Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class GroupMember extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'group_has_users';
+
+ /**
+ * Get query to fetch all users
+ *
+ * @access public
+ * @param integer $group_id
+ * @return \PicoDb\Table
+ */
+ public function getQuery($group_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->join(User::TABLE, 'id', 'user_id')
+ ->eq('group_id', $group_id);
+ }
+
+ /**
+ * Get all users
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getMembers($group_id)
+ {
+ return $this->getQuery($group_id)->findAll();
+ }
+
+ /**
+ * Get all not members
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getNotMembers($group_id)
+ {
+ $subquery = $this->db->table(self::TABLE)
+ ->columns('user_id')
+ ->eq('group_id', $group_id);
+
+ return $this->db->table(User::TABLE)
+ ->notInSubquery('id', $subquery)
+ ->findAll();
+ }
+
+ /**
+ * Add user to a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function addUser($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'group_id' => $group_id,
+ 'user_id' => $user_id,
+ ));
+ }
+
+ /**
+ * Remove user from a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function removeUser($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('user_id', $user_id)
+ ->remove();
+ }
+
+ /**
+ * Check if a user is member
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function isMember($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('user_id', $user_id)
+ ->exists();
+ }
+}
diff --git a/app/Model/LastLogin.php b/app/Model/LastLogin.php
index 0f148ead..feb5f5a3 100644
--- a/app/Model/LastLogin.php
+++ b/app/Model/LastLogin.php
@@ -36,29 +36,39 @@ class LastLogin extends Base
*/
public function create($auth_type, $user_id, $ip, $user_agent)
{
- // Cleanup old sessions if necessary
+ $this->cleanup($user_id);
+
+ return $this->db
+ ->table(self::TABLE)
+ ->insert(array(
+ 'auth_type' => $auth_type,
+ 'user_id' => $user_id,
+ 'ip' => $ip,
+ 'user_agent' => substr($user_agent, 0, 255),
+ 'date_creation' => time(),
+ ));
+ }
+
+ /**
+ * Cleanup login history
+ *
+ * @access public
+ * @param integer $user_id
+ */
+ public function cleanup($user_id)
+ {
$connections = $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
- ->desc('date_creation')
+ ->desc('id')
->findAllByColumn('id');
if (count($connections) >= self::NB_LOGINS) {
$this->db->table(self::TABLE)
- ->eq('user_id', $user_id)
- ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1))
- ->remove();
+ ->eq('user_id', $user_id)
+ ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1))
+ ->remove();
}
-
- return $this->db
- ->table(self::TABLE)
- ->insert(array(
- 'auth_type' => $auth_type,
- 'user_id' => $user_id,
- 'ip' => $ip,
- 'user_agent' => $user_agent,
- 'date_creation' => time(),
- ));
}
/**
@@ -73,7 +83,7 @@ class LastLogin extends Base
return $this->db
->table(self::TABLE)
->eq('user_id', $user_id)
- ->desc('date_creation')
+ ->desc('id')
->columns('id', 'auth_type', 'ip', 'user_agent', 'date_creation')
->findAll();
}
diff --git a/app/Model/Link.php b/app/Model/Link.php
index 00b6dfc5..7b81a237 100644
--- a/app/Model/Link.php
+++ b/app/Model/Link.php
@@ -3,8 +3,6 @@
namespace Kanboard\Model;
use PDO;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
/**
* Link model
@@ -176,47 +174,4 @@ class Link extends Base
$this->db->table(self::TABLE)->eq('opposite_id', $link_id)->update(array('opposite_id' => 0));
return $this->db->table(self::TABLE)->eq('id', $link_id)->remove();
}
-
- /**
- * 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('label', t('Field required')),
- new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE),
- new Validators\NotEquals('label', 'opposite_label', t('The labels must be different')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('id', t('Field required')),
- new Validators\Required('opposite_id', t('Field required')),
- new Validators\Required('label', t('Field required')),
- new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/Metadata.php b/app/Model/Metadata.php
index 83c8f499..690b2265 100644
--- a/app/Model/Metadata.php
+++ b/app/Model/Metadata.php
@@ -95,4 +95,17 @@ abstract class Metadata extends Base
return ! in_array(false, $results, true);
}
+
+ /**
+ * Remove a metadata
+ *
+ * @access public
+ * @param integer $entity_id
+ * @param string $name
+ * @return bool
+ */
+ public function remove($entity_id, $name)
+ {
+ return $this->db->table(static::TABLE)->eq($this->getEntityKey(), $entity_id)->eq('name', $name)->remove();
+ }
}
diff --git a/app/Model/Notification.php b/app/Model/Notification.php
index f1122993..c252aa31 100644
--- a/app/Model/Notification.php
+++ b/app/Model/Notification.php
@@ -72,8 +72,12 @@ class Notification extends Base
return e('%s updated a comment on the task #%d', $event_author, $event_data['task']['id']);
case Comment::EVENT_CREATE:
return e('%s commented on the task #%d', $event_author, $event_data['task']['id']);
- case File::EVENT_CREATE:
+ case TaskFile::EVENT_CREATE:
return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']);
+ case Task::EVENT_USER_MENTION:
+ return e('%s mentioned you in the task #%d', $event_author, $event_data['task']['id']);
+ case Comment::EVENT_USER_MENTION:
+ return e('%s mentioned you in a comment on the task #%d', $event_author, $event_data['task']['id']);
default:
return e('Notification');
}
@@ -90,53 +94,41 @@ class Notification extends Base
public function getTitleWithoutAuthor($event_name, array $event_data)
{
switch ($event_name) {
- case File::EVENT_CREATE:
- $title = e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']);
- break;
+ case TaskFile::EVENT_CREATE:
+ return e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']);
case Comment::EVENT_CREATE:
- $title = e('New comment on task #%d', $event_data['comment']['task_id']);
- break;
+ return e('New comment on task #%d', $event_data['comment']['task_id']);
case Comment::EVENT_UPDATE:
- $title = e('Comment updated on task #%d', $event_data['comment']['task_id']);
- break;
+ return e('Comment updated on task #%d', $event_data['comment']['task_id']);
case Subtask::EVENT_CREATE:
- $title = e('New subtask on task #%d', $event_data['subtask']['task_id']);
- break;
+ return e('New subtask on task #%d', $event_data['subtask']['task_id']);
case Subtask::EVENT_UPDATE:
- $title = e('Subtask updated on task #%d', $event_data['subtask']['task_id']);
- break;
+ return e('Subtask updated on task #%d', $event_data['subtask']['task_id']);
case Task::EVENT_CREATE:
- $title = e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']);
- break;
+ return e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']);
case Task::EVENT_UPDATE:
- $title = e('Task updated #%d', $event_data['task']['id']);
- break;
+ return e('Task updated #%d', $event_data['task']['id']);
case Task::EVENT_CLOSE:
- $title = e('Task #%d closed', $event_data['task']['id']);
- break;
+ return e('Task #%d closed', $event_data['task']['id']);
case Task::EVENT_OPEN:
- $title = e('Task #%d opened', $event_data['task']['id']);
- break;
+ return e('Task #%d opened', $event_data['task']['id']);
case Task::EVENT_MOVE_COLUMN:
- $title = e('Column changed for task #%d', $event_data['task']['id']);
- break;
+ return e('Column changed for task #%d', $event_data['task']['id']);
case Task::EVENT_MOVE_POSITION:
- $title = e('New position for task #%d', $event_data['task']['id']);
- break;
+ return e('New position for task #%d', $event_data['task']['id']);
case Task::EVENT_MOVE_SWIMLANE:
- $title = e('Swimlane changed for task #%d', $event_data['task']['id']);
- break;
+ return e('Swimlane changed for task #%d', $event_data['task']['id']);
case Task::EVENT_ASSIGNEE_CHANGE:
- $title = e('Assignee changed on task #%d', $event_data['task']['id']);
- break;
+ return e('Assignee changed on task #%d', $event_data['task']['id']);
case Task::EVENT_OVERDUE:
$nb = count($event_data['tasks']);
- $title = $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']);
- break;
+ return $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']);
+ case Task::EVENT_USER_MENTION:
+ return e('You were mentioned in the task #%d', $event_data['task']['id']);
+ case Comment::EVENT_USER_MENTION:
+ return e('You were mentioned in a comment on the task #%d', $event_data['task']['id']);
default:
- $title = e('Notification');
+ return e('Notification');
}
-
- return $title;
}
}
diff --git a/app/Model/PasswordReset.php b/app/Model/PasswordReset.php
new file mode 100644
index 00000000..c2d7dde9
--- /dev/null
+++ b/app/Model/PasswordReset.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Password Reset Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class PasswordReset extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'password_reset';
+
+ /**
+ * Token duration (30 minutes)
+ *
+ * @var string
+ */
+ const DURATION = 1800;
+
+ /**
+ * Get all tokens
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getAll($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_creation')->limit(100)->findAll();
+ }
+
+ /**
+ * Generate a new reset token for a user
+ *
+ * @access public
+ * @param string $username
+ * @param integer $expiration
+ * @return boolean|string
+ */
+ public function create($username, $expiration = 0)
+ {
+ $user_id = $this->db->table(User::TABLE)->eq('username', $username)->neq('email', '')->notNull('email')->findOneColumn('id');
+
+ if (! $user_id) {
+ return false;
+ }
+
+ $token = $this->token->getToken();
+
+ $result = $this->db->table(self::TABLE)->insert(array(
+ 'token' => $token,
+ 'user_id' => $user_id,
+ 'date_expiration' => $expiration ?: time() + self::DURATION,
+ 'date_creation' => time(),
+ 'ip' => $this->request->getIpAddress(),
+ 'user_agent' => $this->request->getUserAgent(),
+ 'is_active' => 1,
+ ));
+
+ return $result ? $token : false;
+ }
+
+ /**
+ * Get user id from the token
+ *
+ * @access public
+ * @param string $token
+ * @return integer
+ */
+ public function getUserIdByToken($token)
+ {
+ return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_active', 1)->gte('date_expiration', time())->findOneColumn('user_id');
+ }
+
+ /**
+ * Disable all tokens for a user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function disable($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->update(array('is_active' => 0));
+ }
+}
diff --git a/app/Model/Project.php b/app/Model/Project.php
index b767af26..a79e46a1 100644
--- a/app/Model/Project.php
+++ b/app/Model/Project.php
@@ -2,9 +2,8 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-use Kanboard\Core\Security;
+use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
/**
* Project model
@@ -48,6 +47,22 @@ class Project extends Base
}
/**
+ * Get a project by id with owner name
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function getByIdWithOwner($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(self::TABLE.'.*', User::TABLE.'.username AS owner_username', User::TABLE.'.name AS owner_name')
+ ->eq(self::TABLE.'.id', $project_id)
+ ->join(User::TABLE, 'id', 'owner_id')
+ ->findOne();
+ }
+
+ /**
* Get a project by the name
*
* @access public
@@ -226,7 +241,7 @@ class Project extends Base
{
$stats = array();
$stats['nb_active_tasks'] = 0;
- $columns = $this->board->getColumns($project_id);
+ $columns = $this->column->getAll($project_id);
$column_stats = $this->board->getColumnStats($project_id);
foreach ($columns as &$column) {
@@ -250,7 +265,7 @@ class Project extends Base
*/
public function getColumnStats(array &$project)
{
- $project['columns'] = $this->board->getColumns($project['id']);
+ $project['columns'] = $this->column->getAll($project['id']);
$stats = $this->board->getColumnStats($project['id']);
foreach ($project['columns'] as &$column) {
@@ -277,23 +292,6 @@ class Project extends Base
}
/**
- * Fetch more information for each project
- *
- * @access public
- * @param array $projects
- * @return array
- */
- public function applyProjectDetails(array $projects)
- {
- foreach ($projects as &$project) {
- $this->getColumnStats($project);
- $project = array_merge($project, $this->projectPermission->getProjectUsers($project['id']));
- }
-
- return $projects;
- }
-
- /**
* Get project summary for a list of project
*
* @access public
@@ -308,30 +306,13 @@ class Project extends Base
return $this->db
->table(Project::TABLE)
- ->in('id', $project_ids)
+ ->columns(self::TABLE.'.*', User::TABLE.'.username AS owner_username', User::TABLE.'.name AS owner_name')
+ ->join(User::TABLE, 'id', 'owner_id')
+ ->in(self::TABLE.'.id', $project_ids)
->callback(array($this, 'applyColumnStats'));
}
/**
- * Get project details (users + columns) for a list of project
- *
- * @access public
- * @param array $project_ids List of project id
- * @return \PicoDb\Table
- */
- public function getQueryProjectDetails(array $project_ids)
- {
- if (empty($project_ids)) {
- return $this->db->table(Project::TABLE)->limit(0);
- }
-
- return $this->db
- ->table(Project::TABLE)
- ->in('id', $project_ids)
- ->callback(array($this, 'applyProjectDetails'));
- }
-
- /**
* Create a project
*
* @access public
@@ -347,11 +328,14 @@ class Project extends Base
$values['token'] = '';
$values['last_modified'] = time();
$values['is_private'] = empty($values['is_private']) ? 0 : 1;
+ $values['owner_id'] = $user_id;
if (! empty($values['identifier'])) {
$values['identifier'] = strtoupper($values['identifier']);
}
+ $this->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end'));
+
if (! $this->db->table(self::TABLE)->save($values)) {
$this->db->cancelTransaction();
return false;
@@ -365,7 +349,7 @@ class Project extends Base
}
if ($add_user && $user_id) {
- $this->projectPermission->addManager($project_id, $user_id);
+ $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MANAGER);
}
$this->category->createDefaultCategories($project_id);
@@ -418,6 +402,8 @@ class Project extends Base
$values['identifier'] = strtoupper($values['identifier']);
}
+ $this->convertIntegerFields($values, array('priority_default', 'priority_start', 'priority_end'));
+
return $this->exists($values['id']) &&
$this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
}
@@ -491,7 +477,7 @@ class Project extends Base
$this->db
->table(self::TABLE)
->eq('id', $project_id)
- ->save(array('is_public' => 1, 'token' => Security::generateToken()));
+ ->save(array('is_public' => 1, 'token' => Token::getToken()));
}
/**
@@ -509,72 +495,4 @@ class Project extends Base
->eq('id', $project_id)
->save(array('is_public' => 0, 'token' => ''));
}
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('This value must be an integer')),
- new Validators\Integer('is_active', t('This value must be an integer')),
- new Validators\Required('name', t('The project name is required')),
- new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50),
- new Validators\MaxLength('identifier', t('The maximum length is %d characters', 50), 50),
- new Validators\MaxLength('start_date', t('The maximum length is %d characters', 10), 10),
- new Validators\MaxLength('end_date', t('The maximum length is %d characters', 10), 10),
- new Validators\AlphaNumeric('identifier', t('This value must be alphanumeric')) ,
- new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE),
- new Validators\Unique('identifier', t('The identifier must be unique'), $this->db->getConnection(), self::TABLE),
- );
- }
-
- /**
- * Validate project 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)
- {
- if (! empty($values['identifier'])) {
- $values['identifier'] = strtoupper($values['identifier']);
- }
-
- $v = new Validator($values, $this->commonValidationRules());
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate project modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- if (! empty($values['identifier'])) {
- $values['identifier'] = strtoupper($values['identifier']);
- }
-
- $rules = array(
- new Validators\Required('id', t('This value is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php
index 309bab9a..74df26a1 100644
--- a/app/Model/ProjectActivity.php
+++ b/app/Model/ProjectActivity.php
@@ -168,15 +168,11 @@ class ProjectActivity extends Base
*/
public function cleanup($max)
{
- if ($this->db->table(self::TABLE)->count() > $max) {
- $this->db->execute('
- DELETE FROM '.self::TABLE.'
- WHERE id <= (
- SELECT id FROM (
- SELECT id FROM '.self::TABLE.' ORDER BY id DESC LIMIT 1 OFFSET '.$max.'
- ) foo
- )'
- );
+ $total = $this->db->table(self::TABLE)->count();
+
+ if ($total > $max) {
+ $ids = $this->db->table(self::TABLE)->asc('id')->limit($total - $max)->findAllByColumn('id');
+ $this->db->table(self::TABLE)->in('id', $ids)->remove();
}
}
diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php
deleted file mode 100644
index 92364c0c..00000000
--- a/app/Model/ProjectAnalytic.php
+++ /dev/null
@@ -1,182 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Project analytic model
- *
- * @package model
- * @author Frederic Guillot
- */
-class ProjectAnalytic extends Base
-{
- /**
- * Get tasks repartition
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getTaskRepartition($project_id)
- {
- $metrics = array();
- $total = 0;
- $columns = $this->board->getColumns($project_id);
-
- foreach ($columns as $column) {
- $nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']);
- $total += $nb_tasks;
-
- $metrics[] = array(
- 'column_title' => $column['title'],
- 'nb_tasks' => $nb_tasks,
- );
- }
-
- if ($total === 0) {
- return array();
- }
-
- foreach ($metrics as &$metric) {
- $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
- }
-
- return $metrics;
- }
-
- /**
- * Get users repartition
- *
- * @access public
- * @param integer $project_id
- * @return array
- */
- public function getUserRepartition($project_id)
- {
- $metrics = array();
- $total = 0;
- $tasks = $this->taskFinder->getAll($project_id);
- $users = $this->projectPermission->getMemberList($project_id);
-
- foreach ($tasks as $task) {
- $user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0];
- $total++;
-
- if (! isset($metrics[$user])) {
- $metrics[$user] = array(
- 'nb_tasks' => 0,
- 'percentage' => 0,
- 'user' => $user,
- );
- }
-
- $metrics[$user]['nb_tasks']++;
- }
-
- if ($total === 0) {
- return array();
- }
-
- foreach ($metrics as &$metric) {
- $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
- }
-
- ksort($metrics);
-
- return array_values($metrics);
- }
-
- /**
- * Get the average lead and cycle time
- *
- * @access public
- * @param integer $project_id
- * @return array
- */
- public function getAverageLeadAndCycleTime($project_id)
- {
- $stats = array(
- 'count' => 0,
- 'total_lead_time' => 0,
- 'total_cycle_time' => 0,
- 'avg_lead_time' => 0,
- 'avg_cycle_time' => 0,
- );
-
- $tasks = $this->db
- ->table(Task::TABLE)
- ->columns('date_completed', 'date_creation', 'date_started')
- ->eq('project_id', $project_id)
- ->desc('id')
- ->limit(1000)
- ->findAll();
-
- foreach ($tasks as &$task) {
- $stats['count']++;
- $stats['total_lead_time'] += ($task['date_completed'] ?: time()) - $task['date_creation'];
- $stats['total_cycle_time'] += empty($task['date_started']) ? 0 : ($task['date_completed'] ?: time()) - $task['date_started'];
- }
-
- $stats['avg_lead_time'] = $stats['count'] > 0 ? (int) ($stats['total_lead_time'] / $stats['count']) : 0;
- $stats['avg_cycle_time'] = $stats['count'] > 0 ? (int) ($stats['total_cycle_time'] / $stats['count']) : 0;
-
- return $stats;
- }
-
- /**
- * Get the average time spent into each column
- *
- * @access public
- * @param integer $project_id
- * @return array
- */
- public function getAverageTimeSpentByColumn($project_id)
- {
- $stats = array();
- $columns = $this->board->getColumnsList($project_id);
-
- // Get the time spent of the last move for each tasks
- $tasks = $this->db
- ->table(Task::TABLE)
- ->columns('id', 'date_completed', 'date_moved', 'column_id')
- ->eq('project_id', $project_id)
- ->desc('id')
- ->limit(1000)
- ->findAll();
-
- // Init values
- foreach ($columns as $column_id => $column_title) {
- $stats[$column_id] = array(
- 'count' => 0,
- 'time_spent' => 0,
- 'average' => 0,
- 'title' => $column_title,
- );
- }
-
- // Get time spent foreach task/column and take into account the last move
- foreach ($tasks as &$task) {
- $sums = $this->transition->getTimeSpentByTask($task['id']);
-
- if (! isset($sums[$task['column_id']])) {
- $sums[$task['column_id']] = 0;
- }
-
- $sums[$task['column_id']] += ($task['date_completed'] ?: time()) - $task['date_moved'];
-
- foreach ($sums as $column_id => $time_spent) {
- if (isset($stats[$column_id])) {
- $stats[$column_id]['count']++;
- $stats[$column_id]['time_spent'] += $time_spent;
- }
- }
- }
-
- // Calculate average for each column
- foreach ($columns as $column_id => $column_title) {
- $stats[$column_id]['average'] = $stats[$column_id]['count'] > 0 ? (int) ($stats[$column_id]['time_spent'] / $stats[$column_id]['count']) : 0;
- }
-
- return $stats;
- }
-}
diff --git a/app/Model/ProjectDailyColumnStats.php b/app/Model/ProjectDailyColumnStats.php
index 8ed6137f..2bcc4d55 100644
--- a/app/Model/ProjectDailyColumnStats.php
+++ b/app/Model/ProjectDailyColumnStats.php
@@ -2,8 +2,6 @@
namespace Kanboard\Model;
-use PicoDb\Database;
-
/**
* Project Daily Column Stats
*
@@ -20,7 +18,7 @@ class ProjectDailyColumnStats extends Base
const TABLE = 'project_daily_column_stats';
/**
- * Update daily totals for the project and foreach column
+ * Update daily totals for the project and for each column
*
* "total" is the number open of tasks in the column
* "score" is the sum of tasks score in the column
@@ -32,42 +30,22 @@ class ProjectDailyColumnStats extends Base
*/
public function updateTotals($project_id, $date)
{
- $status = $this->config->get('cfd_include_closed_tasks') == 1 ? array(Task::STATUS_OPEN, Task::STATUS_CLOSED) : array(Task::STATUS_OPEN);
-
- return $this->db->transaction(function (Database $db) use ($project_id, $date, $status) {
-
- $column_ids = $db->table(Board::TABLE)->eq('project_id', $project_id)->findAllByColumn('id');
-
- foreach ($column_ids as $column_id) {
-
- // This call will fail if the record already exists
- // (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
- $db->table(ProjectDailyColumnStats::TABLE)->insert(array(
- 'day' => $date,
- 'project_id' => $project_id,
- 'column_id' => $column_id,
- 'total' => 0,
- 'score' => 0,
- ));
-
- $db->table(ProjectDailyColumnStats::TABLE)
- ->eq('project_id', $project_id)
- ->eq('column_id', $column_id)
- ->eq('day', $date)
- ->update(array(
- 'score' => $db->table(Task::TABLE)
- ->eq('project_id', $project_id)
- ->eq('column_id', $column_id)
- ->eq('is_active', Task::STATUS_OPEN)
- ->sum('score'),
- 'total' => $db->table(Task::TABLE)
- ->eq('project_id', $project_id)
- ->eq('column_id', $column_id)
- ->in('is_active', $status)
- ->count()
- ));
- }
- });
+ $this->db->startTransaction();
+ $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('day', $date)->remove();
+
+ foreach ($this->getStatsByColumns($project_id) as $column_id => $column) {
+ $this->db->table(self::TABLE)->insert(array(
+ 'day' => $date,
+ 'project_id' => $project_id,
+ 'column_id' => $column_id,
+ 'total' => $column['total'],
+ 'score' => $column['score'],
+ ));
+ }
+
+ $this->db->closeTransaction();
+
+ return true;
}
/**
@@ -81,43 +59,38 @@ class ProjectDailyColumnStats extends Base
*/
public function countDays($project_id, $from, $to)
{
- $rq = $this->db->execute(
- 'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ? AND project_id=?',
- array($from, $to, $project_id)
- );
-
- return $rq !== false ? $rq->fetchColumn(0) : 0;
+ return $this->db->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->gte('day', $from)
+ ->lte('day', $to)
+ ->findOneColumn('COUNT(DISTINCT day)');
}
/**
- * Get raw metrics for the project within a data range
+ * Get aggregated metrics for the project within a data range
+ *
+ * [
+ * ['Date', 'Column1', 'Column2'],
+ * ['2014-11-16', 2, 5],
+ * ['2014-11-17', 20, 15],
+ * ]
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
+ * @param string $field Column to aggregate
* @return array
*/
- public function getRawMetrics($project_id, $from, $to)
+ public function getAggregatedMetrics($project_id, $from, $to, $field = 'total')
{
- return $this->db->table(ProjectDailyColumnStats::TABLE)
- ->columns(
- ProjectDailyColumnStats::TABLE.'.column_id',
- ProjectDailyColumnStats::TABLE.'.day',
- ProjectDailyColumnStats::TABLE.'.total',
- ProjectDailyColumnStats::TABLE.'.score',
- Board::TABLE.'.title AS column_title'
- )
- ->join(Board::TABLE, 'id', 'column_id')
- ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id)
- ->gte('day', $from)
- ->lte('day', $to)
- ->asc(ProjectDailyColumnStats::TABLE.'.day')
- ->findAll();
+ $columns = $this->column->getList($project_id);
+ $metrics = $this->getMetrics($project_id, $from, $to);
+ return $this->buildAggregate($metrics, $columns, $field);
}
/**
- * Get raw metrics for the project within a data range grouped by day
+ * Fetch metrics
*
* @access public
* @param integer $project_id Project id
@@ -125,72 +98,155 @@ class ProjectDailyColumnStats extends Base
* @param string $to End date
* @return array
*/
- public function getRawMetricsByDay($project_id, $from, $to)
+ public function getMetrics($project_id, $from, $to)
{
- return $this->db->table(ProjectDailyColumnStats::TABLE)
- ->columns(
- ProjectDailyColumnStats::TABLE.'.day',
- 'SUM('.ProjectDailyColumnStats::TABLE.'.total) AS total',
- 'SUM('.ProjectDailyColumnStats::TABLE.'.score) AS score'
- )
- ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id)
- ->gte('day', $from)
- ->lte('day', $to)
- ->asc(ProjectDailyColumnStats::TABLE.'.day')
- ->groupBy(ProjectDailyColumnStats::TABLE.'.day')
- ->findAll();
+ return $this->db->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->gte('day', $from)
+ ->lte('day', $to)
+ ->asc(self::TABLE.'.day')
+ ->findAll();
}
/**
- * Get aggregated metrics for the project within a data range
- *
- * [
- * ['Date', 'Column1', 'Column2'],
- * ['2014-11-16', 2, 5],
- * ['2014-11-17', 20, 15],
- * ]
+ * Build aggregate
*
- * @access public
- * @param integer $project_id Project id
- * @param string $from Start date (ISO format YYYY-MM-DD)
- * @param string $to End date
- * @param string $column Column to aggregate
+ * @access private
+ * @param array $metrics
+ * @param array $columns
+ * @param string $field
* @return array
*/
- public function getAggregatedMetrics($project_id, $from, $to, $column = 'total')
+ private function buildAggregate(array &$metrics, array &$columns, $field)
{
- $columns = $this->board->getColumnsList($project_id);
$column_ids = array_keys($columns);
- $metrics = array(array_merge(array(e('Date')), array_values($columns)));
- $aggregates = array();
-
- // Fetch metrics for the project
- $records = $this->db->table(ProjectDailyColumnStats::TABLE)
- ->eq('project_id', $project_id)
- ->gte('day', $from)
- ->lte('day', $to)
- ->findAll();
-
- // Aggregate by day
- foreach ($records as $record) {
- if (! isset($aggregates[$record['day']])) {
- $aggregates[$record['day']] = array($record['day']);
- }
+ $days = array_unique(array_column($metrics, 'day'));
+ $rows = array(array_merge(array(e('Date')), array_values($columns)));
- $aggregates[$record['day']][$record['column_id']] = $record[$column];
+ foreach ($days as $day) {
+ $rows[] = $this->buildRowAggregate($metrics, $column_ids, $day, $field);
}
- // Aggregate by row
- foreach ($aggregates as $aggregate) {
- $row = array($aggregate[0]);
+ return $rows;
+ }
- foreach ($column_ids as $column_id) {
- $row[] = (int) $aggregate[$column_id];
+ /**
+ * Build one row of the aggregate
+ *
+ * @access private
+ * @param array $metrics
+ * @param array $column_ids
+ * @param string $day
+ * @param string $field
+ * @return array
+ */
+ private function buildRowAggregate(array &$metrics, array &$column_ids, $day, $field)
+ {
+ $row = array($day);
+
+ foreach ($column_ids as $column_id) {
+ $row[] = $this->findValueInMetrics($metrics, $day, $column_id, $field);
+ }
+
+ return $row;
+ }
+
+ /**
+ * Find the value in the metrics
+ *
+ * @access private
+ * @param array $metrics
+ * @param string $day
+ * @param string $column_id
+ * @param string $field
+ * @return integer
+ */
+ private function findValueInMetrics(array &$metrics, $day, $column_id, $field)
+ {
+ foreach ($metrics as $metric) {
+ if ($metric['day'] === $day && $metric['column_id'] == $column_id) {
+ return $metric[$field];
}
+ }
+
+ return 0;
+ }
- $metrics[] = $row;
+ /**
+ * Get number of tasks and score by columns
+ *
+ * @access private
+ * @param integer $project_id
+ * @return array
+ */
+ private function getStatsByColumns($project_id)
+ {
+ $totals = $this->getTotalByColumns($project_id);
+ $scores = $this->getScoreByColumns($project_id);
+ $columns = array();
+
+ foreach ($totals as $column_id => $total) {
+ $columns[$column_id] = array('total' => $total, 'score' => 0);
+ }
+
+ foreach ($scores as $column_id => $score) {
+ $columns[$column_id]['score'] = (int) $score;
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Get number of tasks and score by columns
+ *
+ * @access private
+ * @param integer $project_id
+ * @return array
+ */
+ private function getScoreByColumns($project_id)
+ {
+ $stats = $this->db->table(Task::TABLE)
+ ->columns('column_id', 'SUM(score) AS score')
+ ->eq('project_id', $project_id)
+ ->eq('is_active', Task::STATUS_OPEN)
+ ->notNull('score')
+ ->groupBy('column_id')
+ ->findAll();
+
+ return array_column($stats, 'score', 'column_id');
+ }
+
+ /**
+ * Get number of tasks and score by columns
+ *
+ * @access private
+ * @param integer $project_id
+ * @return array
+ */
+ private function getTotalByColumns($project_id)
+ {
+ $stats = $this->db->table(Task::TABLE)
+ ->columns('column_id', 'COUNT(*) AS total')
+ ->eq('project_id', $project_id)
+ ->in('is_active', $this->getTaskStatusConfig())
+ ->groupBy('column_id')
+ ->findAll();
+
+ return array_column($stats, 'total', 'column_id');
+ }
+
+ /**
+ * Get task status to use for total calculation
+ *
+ * @access private
+ * @return array
+ */
+ private function getTaskStatusConfig()
+ {
+ if ($this->config->get('cfd_include_closed_tasks') == 1) {
+ return array(Task::STATUS_OPEN, Task::STATUS_CLOSED);
}
- return $metrics;
+ return array(Task::STATUS_OPEN);
}
}
diff --git a/app/Model/ProjectDailyStats.php b/app/Model/ProjectDailyStats.php
index 46ca0a4b..957ad51d 100644
--- a/app/Model/ProjectDailyStats.php
+++ b/app/Model/ProjectDailyStats.php
@@ -2,8 +2,6 @@
namespace Kanboard\Model;
-use PicoDb\Database;
-
/**
* Project Daily Stats
*
@@ -29,27 +27,22 @@ class ProjectDailyStats extends Base
*/
public function updateTotals($project_id, $date)
{
- $lead_cycle_time = $this->projectAnalytic->getAverageLeadAndCycleTime($project_id);
+ $this->db->startTransaction();
+
+ $lead_cycle_time = $this->averageLeadCycleTimeAnalytic->build($project_id);
+
+ $this->db->table(self::TABLE)->eq('day', $date)->eq('project_id', $project_id)->remove();
- return $this->db->transaction(function (Database $db) use ($project_id, $date, $lead_cycle_time) {
+ $this->db->table(self::TABLE)->insert(array(
+ 'day' => $date,
+ 'project_id' => $project_id,
+ 'avg_lead_time' => $lead_cycle_time['avg_lead_time'],
+ 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'],
+ ));
- // This call will fail if the record already exists
- // (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
- $db->table(ProjectDailyStats::TABLE)->insert(array(
- 'day' => $date,
- 'project_id' => $project_id,
- 'avg_lead_time' => 0,
- 'avg_cycle_time' => 0,
- ));
+ $this->db->closeTransaction();
- $db->table(ProjectDailyStats::TABLE)
- ->eq('project_id', $project_id)
- ->eq('day', $date)
- ->update(array(
- 'avg_lead_time' => $lead_cycle_time['avg_lead_time'],
- 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'],
- ));
- });
+ return true;
}
/**
@@ -64,11 +57,11 @@ class ProjectDailyStats extends Base
public function getRawMetrics($project_id, $from, $to)
{
return $this->db->table(self::TABLE)
- ->columns('day', 'avg_lead_time', 'avg_cycle_time')
- ->eq(self::TABLE.'.project_id', $project_id)
- ->gte('day', $from)
- ->lte('day', $to)
- ->asc(self::TABLE.'.day')
- ->findAll();
+ ->columns('day', 'avg_lead_time', 'avg_cycle_time')
+ ->eq('project_id', $project_id)
+ ->gte('day', $from)
+ ->lte('day', $to)
+ ->asc('day')
+ ->findAll();
}
}
diff --git a/app/Model/ProjectDuplication.php b/app/Model/ProjectDuplication.php
index f0c66834..9c5f80ad 100644
--- a/app/Model/ProjectDuplication.php
+++ b/app/Model/ProjectDuplication.php
@@ -2,6 +2,8 @@
namespace Kanboard\Model;
+use Kanboard\Core\Security\Role;
+
/**
* Project Duplication
*
@@ -12,6 +14,28 @@ namespace Kanboard\Model;
class ProjectDuplication extends Base
{
/**
+ * Get list of optional models to duplicate
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getOptionalSelection()
+ {
+ return array('category', 'projectPermission', 'action', 'swimlane', 'task');
+ }
+
+ /**
+ * Get list of all possible models to duplicate
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getPossibleSelection()
+ {
+ return array('board', 'category', 'projectPermission', 'action', 'swimlane', 'task');
+ }
+
+ /**
* Get a valid project name for the duplication
*
* @access public
@@ -31,78 +55,106 @@ class ProjectDuplication extends Base
}
/**
- * Create a project from another one
- *
- * @param integer $project_id Project Id
- * @return integer Cloned Project Id
- */
- public function copy($project_id)
- {
- $project = $this->project->getById($project_id);
-
- $values = array(
- 'name' => $this->getClonedProjectName($project['name']),
- 'is_active' => true,
- 'last_modified' => 0,
- 'token' => '',
- 'is_public' => 0,
- 'is_private' => empty($project['is_private']) ? 0 : 1,
- );
-
- if (! $this->db->table(Project::TABLE)->save($values)) {
- return 0;
- }
-
- return $this->db->getLastId();
- }
-
- /**
* Clone a project with all settings
*
- * @param integer $project_id Project Id
- * @param array $part_selection Selection of optional project parts to duplicate. Possible options: 'swimlane', 'action', 'category', 'task'
- * @return integer Cloned Project Id
+ * @param integer $src_project_id Project Id
+ * @param array $selection Selection of optional project parts to duplicate
+ * @param integer $owner_id Owner of the project
+ * @param string $name Name of the project
+ * @param boolean $private Force the project to be private
+ * @return integer Cloned Project Id
*/
- public function duplicate($project_id, $part_selection = array('category', 'action'))
+ public function duplicate($src_project_id, $selection = array('projectPermission', 'category', 'action'), $owner_id = 0, $name = null, $private = null)
{
$this->db->startTransaction();
// Get the cloned project Id
- $clone_project_id = $this->copy($project_id);
+ $dst_project_id = $this->copy($src_project_id, $owner_id, $name, $private);
- if (! $clone_project_id) {
+ if ($dst_project_id === false) {
$this->db->cancelTransaction();
return false;
}
// Clone Columns, Categories, Permissions and Actions
- $optional_parts = array('swimlane', 'action', 'category');
- foreach (array('board', 'category', 'projectPermission', 'action', 'swimlane') as $model) {
+ foreach ($this->getPossibleSelection() as $model) {
// Skip if optional part has not been selected
- if (in_array($model, $optional_parts) && ! in_array($model, $part_selection)) {
+ if (in_array($model, $this->getOptionalSelection()) && ! in_array($model, $selection)) {
continue;
}
- if (! $this->$model->duplicate($project_id, $clone_project_id)) {
+ // Skip permissions for private projects
+ if ($private && $model === 'projectPermission') {
+ continue;
+ }
+
+ if (! $this->$model->duplicate($src_project_id, $dst_project_id)) {
$this->db->cancelTransaction();
return false;
}
}
+ if (! $this->makeOwnerManager($dst_project_id, $owner_id)) {
+ $this->db->cancelTransaction();
+ return false;
+ }
+
$this->db->closeTransaction();
- // Clone Tasks if in $part_selection
- if (in_array('task', $part_selection)) {
- $tasks = $this->taskFinder->getAll($project_id);
+ return (int) $dst_project_id;
+ }
+
+ /**
+ * Create a project from another one
+ *
+ * @access private
+ * @param integer $src_project_id
+ * @param integer $owner_id
+ * @param string $name
+ * @param boolean $private
+ * @return integer
+ */
+ private function copy($src_project_id, $owner_id = 0, $name = null, $private = null)
+ {
+ $project = $this->project->getById($src_project_id);
+ $is_private = empty($project['is_private']) ? 0 : 1;
+
+ $values = array(
+ 'name' => $name ?: $this->getClonedProjectName($project['name']),
+ 'is_active' => 1,
+ 'last_modified' => time(),
+ 'token' => '',
+ 'is_public' => 0,
+ 'is_private' => $private ? 1 : $is_private,
+ 'owner_id' => $owner_id,
+ );
+
+ if (! $this->db->table(Project::TABLE)->save($values)) {
+ return false;
+ }
+
+ return $this->db->getLastId();
+ }
+
+ /**
+ * Make sure that the creator of the duplicated project is alsp owner
+ *
+ * @access private
+ * @param integer $dst_project_id
+ * @param integer $owner_id
+ * @return boolean
+ */
+ private function makeOwnerManager($dst_project_id, $owner_id)
+ {
+ if ($owner_id > 0) {
+ $this->projectUserRole->removeUser($dst_project_id, $owner_id);
- foreach ($tasks as $task) {
- if (! $this->taskDuplication->duplicateToProject($task['id'], $clone_project_id)) {
- return false;
- }
+ if (! $this->projectUserRole->addUser($dst_project_id, $owner_id, Role::PROJECT_MANAGER)) {
+ return false;
}
}
- return (int) $clone_project_id;
+ return true;
}
}
diff --git a/app/Model/ProjectFile.php b/app/Model/ProjectFile.php
new file mode 100644
index 00000000..aa9bf15b
--- /dev/null
+++ b/app/Model/ProjectFile.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Project File Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectFile extends File
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_has_files';
+
+ /**
+ * SQL foreign key
+ *
+ * @var string
+ */
+ const FOREIGN_KEY = 'project_id';
+
+ /**
+ * Path prefix
+ *
+ * @var string
+ */
+ const PATH_PREFIX = 'projects';
+
+ /**
+ * Events
+ *
+ * @var string
+ */
+ const EVENT_CREATE = 'project.file.create';
+}
diff --git a/app/Model/ProjectGroupRole.php b/app/Model/ProjectGroupRole.php
new file mode 100644
index 00000000..750ba7fb
--- /dev/null
+++ b/app/Model/ProjectGroupRole.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Security\Role;
+
+/**
+ * Project Group Role
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectGroupRole extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_has_groups';
+
+ /**
+ * Get the list of project visible by the given user according to groups
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $status
+ * @return array
+ */
+ public function getProjectsByUser($user_id, $status = array(Project::ACTIVE, Project::INACTIVE))
+ {
+ return $this->db
+ ->hashtable(Project::TABLE)
+ ->join(self::TABLE, 'project_id', 'id')
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE)
+ ->eq(GroupMember::TABLE.'.user_id', $user_id)
+ ->in(Project::TABLE.'.is_active', $status)
+ ->getAll(Project::TABLE.'.id', Project::TABLE.'.name');
+ }
+
+ /**
+ * For a given project get the role of the specified user
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return string
+ */
+ public function getUserRole($project_id, $user_id)
+ {
+ $roles = $this->db->table(self::TABLE)
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE)
+ ->eq(GroupMember::TABLE.'.user_id', $user_id)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->findAllByColumn('role');
+
+ return $this->projectAccessMap->getHighestRole($roles);
+ }
+
+ /**
+ * Get all groups associated directly to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getGroups($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(Group::TABLE.'.id', Group::TABLE.'.name', self::TABLE.'.role')
+ ->join(Group::TABLE, 'id', 'group_id')
+ ->eq('project_id', $project_id)
+ ->asc('name')
+ ->findAll();
+ }
+
+ /**
+ * From groups get all users associated to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getUsers($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.role')
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->asc(User::TABLE.'.username')
+ ->findAll();
+ }
+
+ /**
+ * From groups get all users assignable to tasks
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAssignableUsers($project_id)
+ {
+ return $this->db->table(User::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
+ ->join(GroupMember::TABLE, 'user_id', 'id', User::TABLE)
+ ->join(self::TABLE, 'group_id', 'group_id', GroupMember::TABLE)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->eq(User::TABLE.'.is_active', 1)
+ ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER))
+ ->asc(User::TABLE.'.username')
+ ->findAll();
+ }
+
+ /**
+ * Add a group to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $group_id
+ * @param string $role
+ * @return boolean
+ */
+ public function addGroup($project_id, $group_id, $role)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'group_id' => $group_id,
+ 'project_id' => $project_id,
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Remove a group from the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $group_id
+ * @return boolean
+ */
+ public function removeGroup($project_id, $group_id)
+ {
+ return $this->db->table(self::TABLE)->eq('group_id', $group_id)->eq('project_id', $project_id)->remove();
+ }
+
+ /**
+ * Change a group role for the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $group_id
+ * @param string $role
+ * @return boolean
+ */
+ public function changeGroupRole($project_id, $group_id, $role)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('project_id', $project_id)
+ ->update(array(
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Copy group access from a project to another one
+ *
+ * @param integer $project_src_id Project Template
+ * @return integer $project_dst_id Project that receives the copy
+ * @return boolean
+ */
+ public function duplicate($project_src_id, $project_dst_id)
+ {
+ $rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll();
+
+ foreach ($rows as $row) {
+ $result = $this->db->table(self::TABLE)->save(array(
+ 'project_id' => $project_dst_id,
+ 'group_id' => $row['group_id'],
+ 'role' => $row['role'],
+ ));
+
+ if (! $result) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/app/Model/ProjectGroupRoleFilter.php b/app/Model/ProjectGroupRoleFilter.php
new file mode 100644
index 00000000..989d3073
--- /dev/null
+++ b/app/Model/ProjectGroupRoleFilter.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Project Group Role Filter
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectGroupRoleFilter extends Base
+{
+ /**
+ * Query
+ *
+ * @access protected
+ * @var \PicoDb\Table
+ */
+ protected $query;
+
+ /**
+ * Initialize filter
+ *
+ * @access public
+ * @return UserFilter
+ */
+ public function create()
+ {
+ $this->query = $this->db->table(ProjectGroupRole::TABLE);
+ return $this;
+ }
+
+ /**
+ * Get all results of the filter
+ *
+ * @access public
+ * @param string $column
+ * @return array
+ */
+ public function findAll($column = '')
+ {
+ if ($column !== '') {
+ return $this->query->asc($column)->findAllByColumn($column);
+ }
+
+ return $this->query->findAll();
+ }
+
+ /**
+ * Get the PicoDb query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Filter by project id
+ *
+ * @access public
+ * @param integer $project_id
+ * @return ProjectUserRoleFilter
+ */
+ public function filterByProjectId($project_id)
+ {
+ $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $project_id);
+ return $this;
+ }
+
+ /**
+ * Filter by username
+ *
+ * @access public
+ * @param string $input
+ * @return ProjectUserRoleFilter
+ */
+ public function startWithUsername($input)
+ {
+ $this->query
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
+ ->ilike(User::TABLE.'.username', $input.'%');
+
+ return $this;
+ }
+}
diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php
index d9eef4db..db1573ae 100644
--- a/app/Model/ProjectPermission.php
+++ b/app/Model/ProjectPermission.php
@@ -2,11 +2,10 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
+use Kanboard\Core\Security\Role;
/**
- * Project permission model
+ * Project Permission
*
* @package model
* @author Frederic Guillot
@@ -14,117 +13,14 @@ use SimpleValidator\Validators;
class ProjectPermission extends Base
{
/**
- * SQL table name for permissions
- *
- * @var string
- */
- const TABLE = 'project_has_users';
-
- /**
- * Get a list of people that can be assigned for tasks
- *
- * @access public
- * @param integer $project_id Project id
- * @param bool $prepend_unassigned Prepend the 'Unassigned' value
- * @param bool $prepend_everybody Prepend the 'Everbody' value
- * @param bool $allow_single_user If there is only one user return only this user
- * @return array
- */
- public function getMemberList($project_id, $prepend_unassigned = true, $prepend_everybody = false, $allow_single_user = false)
- {
- $allowed_users = $this->getMembers($project_id);
-
- if ($allow_single_user && count($allowed_users) === 1) {
- return $allowed_users;
- }
-
- if ($prepend_unassigned) {
- $allowed_users = array(t('Unassigned')) + $allowed_users;
- }
-
- if ($prepend_everybody) {
- $allowed_users = array(User::EVERYBODY_ID => t('Everybody')) + $allowed_users;
- }
-
- return $allowed_users;
- }
-
- /**
- * Get a list of members and managers with a single SQL query
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getProjectUsers($project_id)
- {
- $result = array(
- 'managers' => array(),
- 'members' => array(),
- );
-
- $users = $this->db
- ->table(self::TABLE)
- ->join(User::TABLE, 'id', 'user_id')
- ->eq('project_id', $project_id)
- ->asc('username')
- ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.is_owner')
- ->findAll();
-
- foreach ($users as $user) {
- $key = $user['is_owner'] == 1 ? 'managers' : 'members';
- $result[$key][$user['id']] = $user['name'] ?: $user['username'];
- }
-
- return $result;
- }
-
- /**
- * Get a list of allowed people for a project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getMembers($project_id)
- {
- if ($this->isEverybodyAllowed($project_id)) {
- return $this->user->getList();
- }
-
- return $this->getAssociatedUsers($project_id);
- }
-
- /**
- * Get a list of owners for a project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getManagers($project_id)
- {
- $users = $this->db
- ->table(self::TABLE)
- ->join(User::TABLE, 'id', 'user_id')
- ->eq('project_id', $project_id)
- ->eq('is_owner', 1)
- ->asc('username')
- ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
- ->findAll();
-
- return $this->user->prepareList($users);
- }
-
- /**
* Get query for project users overview
*
* @access public
* @param array $project_ids
- * @param integer $is_owner
+ * @param string $role
* @return \PicoDb\Table
*/
- public function getQueryByRole(array $project_ids, $is_owner = 0)
+ public function getQueryByRole(array $project_ids, $role)
{
if (empty($project_ids)) {
$project_ids = array(-1);
@@ -132,10 +28,10 @@ class ProjectPermission extends Base
return $this
->db
- ->table(self::TABLE)
+ ->table(ProjectUserRole::TABLE)
->join(User::TABLE, 'id', 'user_id')
->join(Project::TABLE, 'id', 'project_id')
- ->eq(self::TABLE.'.is_owner', $is_owner)
+ ->eq(ProjectUserRole::TABLE.'.role', $role)
->eq(Project::TABLE.'.is_private', 0)
->in(Project::TABLE.'.id', $project_ids)
->columns(
@@ -148,169 +44,22 @@ class ProjectPermission extends Base
}
/**
- * Get a list of people associated to the project
+ * Get all usernames (fetch users from groups)
*
* @access public
- * @param integer $project_id Project id
+ * @param integer $project_id
+ * @param string $input
* @return array
*/
- public function getAssociatedUsers($project_id)
+ public function findUsernames($project_id, $input)
{
- $users = $this->db
- ->table(self::TABLE)
- ->join(User::TABLE, 'id', 'user_id')
- ->eq('project_id', $project_id)
- ->asc('username')
- ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
- ->findAll();
+ $userMembers = $this->projectUserRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username');
+ $groupMembers = $this->projectGroupRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username');
+ $members = array_unique(array_merge($userMembers, $groupMembers));
- return $this->user->prepareList($users);
- }
+ sort($members);
- /**
- * Get allowed and not allowed users for a project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getAllUsers($project_id)
- {
- $users = array(
- 'allowed' => array(),
- 'not_allowed' => array(),
- 'managers' => array(),
- );
-
- $all_users = $this->user->getList();
-
- $users['allowed'] = $this->getMembers($project_id);
- $users['managers'] = $this->getManagers($project_id);
-
- foreach ($all_users as $user_id => $username) {
- if (! isset($users['allowed'][$user_id])) {
- $users['not_allowed'][$user_id] = $username;
- }
- }
-
- return $users;
- }
-
- /**
- * Add a new project member
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function addMember($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->save(array('project_id' => $project_id, 'user_id' => $user_id));
- }
-
- /**
- * Remove a member
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function revokeMember($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->remove();
- }
-
- /**
- * Add a project manager
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function addManager($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->save(array('project_id' => $project_id, 'user_id' => $user_id, 'is_owner' => 1));
- }
-
- /**
- * Change the role of a member
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @param integer $is_owner Is user owner of the project
- * @return bool
- */
- public function changeRole($project_id, $user_id, $is_owner)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->update(array('is_owner' => (int) $is_owner));
- }
-
- /**
- * Check if a specific user is member of a project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function isMember($project_id, $user_id)
- {
- if ($this->isEverybodyAllowed($project_id)) {
- return true;
- }
-
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->exists();
- }
-
- /**
- * Check if a specific user is manager of a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function isManager($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->eq('is_owner', 1)
- ->exists();
- }
-
- /**
- * Check if a specific user is allowed to access to a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function isUserAllowed($project_id, $user_id)
- {
- return $project_id === 0 || $this->user->isAdmin($user_id) || $this->isMember($project_id, $user_id);
+ return $members;
}
/**
@@ -330,172 +79,73 @@ class ProjectPermission extends Base
}
/**
- * Return a list of allowed active projects for a given user
+ * Return true if the user is allowed to access a project
*
- * @access public
- * @param integer $user_id User id
- * @return array
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
*/
- public function getAllowedProjects($user_id)
+ public function isUserAllowed($project_id, $user_id)
{
- if ($this->user->isAdmin($user_id)) {
- return $this->project->getListByStatus(Project::ACTIVE);
+ if ($this->userSession->isAdmin()) {
+ return true;
}
- return $this->getActiveMemberProjects($user_id);
- }
-
- /**
- * Return a list of projects where the user is member
- *
- * @access public
- * @param integer $user_id User id
- * @return array
- */
- public function getMemberProjects($user_id)
- {
- return $this->db
- ->hashtable(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->join(self::TABLE, 'project_id', 'id')
- ->getAll('projects.id', 'name');
+ return in_array(
+ $this->projectUserRole->getUserRole($project_id, $user_id),
+ array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER, Role::PROJECT_VIEWER)
+ );
}
/**
- * Return a list of project ids where the user is member
+ * Return true if the user is assignable
*
* @access public
- * @param integer $user_id User id
- * @return array
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
*/
- public function getMemberProjectIds($user_id)
+ public function isAssignable($project_id, $user_id)
{
- return $this->db
- ->table(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->join(self::TABLE, 'project_id', 'id')
- ->findAllByColumn('projects.id');
+ return $this->user->isActive($user_id) &&
+ in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER));
}
/**
- * Return a list of active project ids where the user is member
+ * Return true if the user is member
*
* @access public
- * @param integer $user_id User id
- * @return array
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
*/
- public function getActiveMemberProjectIds($user_id)
+ public function isMember($project_id, $user_id)
{
- return $this->db
- ->table(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->eq(Project::TABLE.'.is_active', Project::ACTIVE)
- ->join(self::TABLE, 'project_id', 'id')
- ->findAllByColumn('projects.id');
+ return in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER, Role::PROJECT_VIEWER));
}
/**
- * Return a list of active projects where the user is member
+ * Get active project ids by user
*
* @access public
- * @param integer $user_id User id
+ * @param integer $user_id
* @return array
*/
- public function getActiveMemberProjects($user_id)
+ public function getActiveProjectIds($user_id)
{
- return $this->db
- ->hashtable(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->eq(Project::TABLE.'.is_active', Project::ACTIVE)
- ->join(self::TABLE, 'project_id', 'id')
- ->getAll('projects.id', 'name');
+ return array_keys($this->projectUserRole->getActiveProjectsByUser($user_id));
}
/**
- * Copy user access from a project to another one
+ * Copy permissions to another project
*
- * @param integer $project_src Project Template
- * @return integer $project_dst Project that receives the copy
+ * @param integer $project_src_id Project Template
+ * @param integer $project_dst_id Project that receives the copy
* @return boolean
*/
- public function duplicate($project_src, $project_dst)
- {
- $rows = $this->db
- ->table(self::TABLE)
- ->columns('project_id', 'user_id', 'is_owner')
- ->eq('project_id', $project_src)
- ->findAll();
-
- foreach ($rows as $row) {
- $result = $this->db
- ->table(self::TABLE)
- ->save(array(
- 'project_id' => $project_dst,
- 'user_id' => $row['user_id'],
- 'is_owner' => (int) $row['is_owner'], // (int) for postgres
- ));
-
- if (! $result) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Validate allow user
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateUserModification(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('project_id', t('The project id is required')),
- new Validators\Integer('project_id', t('This value must be an integer')),
- new Validators\Required('user_id', t('The user id is required')),
- new Validators\Integer('user_id', t('This value must be an integer')),
- new Validators\Integer('is_owner', t('This value must be an integer')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate allow everybody
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateProjectModification(array $values)
+ public function duplicate($project_src_id, $project_dst_id)
{
- $v = new Validator($values, array(
- new Validators\Required('id', t('The project id is required')),
- new Validators\Integer('id', t('This value must be an integer')),
- new Validators\Integer('is_everybody_allowed', t('This value must be an integer')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
+ return $this->projectUserRole->duplicate($project_src_id, $project_dst_id) &&
+ $this->projectGroupRole->duplicate($project_src_id, $project_dst_id);
}
}
diff --git a/app/Model/ProjectUserRole.php b/app/Model/ProjectUserRole.php
new file mode 100644
index 00000000..56da679c
--- /dev/null
+++ b/app/Model/ProjectUserRole.php
@@ -0,0 +1,276 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Security\Role;
+
+/**
+ * Project User Role
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectUserRole extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_has_users';
+
+ /**
+ * Get the list of active project for the given user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getActiveProjectsByUser($user_id)
+ {
+ return $this->getProjectsByUser($user_id, array(Project::ACTIVE));
+ }
+
+ /**
+ * Get the list of project visible for the given user
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $status
+ * @return array
+ */
+ public function getProjectsByUser($user_id, $status = array(Project::ACTIVE, Project::INACTIVE))
+ {
+ $userProjects = $this->db
+ ->hashtable(Project::TABLE)
+ ->beginOr()
+ ->eq(self::TABLE.'.user_id', $user_id)
+ ->eq(Project::TABLE.'.is_everybody_allowed', 1)
+ ->closeOr()
+ ->in(Project::TABLE.'.is_active', $status)
+ ->join(self::TABLE, 'project_id', 'id')
+ ->getAll(Project::TABLE.'.id', Project::TABLE.'.name');
+
+ $groupProjects = $this->projectGroupRole->getProjectsByUser($user_id, $status);
+ $projects = $userProjects + $groupProjects;
+
+ asort($projects);
+
+ return $projects;
+ }
+
+ /**
+ * For a given project get the role of the specified user
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return string
+ */
+ public function getUserRole($project_id, $user_id)
+ {
+ if ($this->projectPermission->isEverybodyAllowed($project_id)) {
+ return Role::PROJECT_MEMBER;
+ }
+
+ $role = $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->findOneColumn('role');
+
+ if (empty($role)) {
+ $role = $this->projectGroupRole->getUserRole($project_id, $user_id);
+ }
+
+ return $role;
+ }
+
+ /**
+ * Get all users associated directly to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getUsers($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.role')
+ ->join(User::TABLE, 'id', 'user_id')
+ ->eq('project_id', $project_id)
+ ->asc(User::TABLE.'.username')
+ ->asc(User::TABLE.'.name')
+ ->findAll();
+ }
+
+ /**
+ * Get all users (fetch users from groups)
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAllUsers($project_id)
+ {
+ $userMembers = $this->getUsers($project_id);
+ $groupMembers = $this->projectGroupRole->getUsers($project_id);
+ $members = array_merge($userMembers, $groupMembers);
+
+ return $this->user->prepareList($members);
+ }
+
+ /**
+ * Get users grouped by role
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function getAllUsersGroupedByRole($project_id)
+ {
+ $users = array();
+
+ $userMembers = $this->getUsers($project_id);
+ $groupMembers = $this->projectGroupRole->getUsers($project_id);
+ $members = array_merge($userMembers, $groupMembers);
+
+ foreach ($members as $user) {
+ if (! isset($users[$user['role']])) {
+ $users[$user['role']] = array();
+ }
+
+ $users[$user['role']][$user['id']] = $user['name'] ?: $user['username'];
+ }
+
+ return $users;
+ }
+
+ /**
+ * Get list of users that can be assigned to a task (only Manager and Member)
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAssignableUsers($project_id)
+ {
+ if ($this->projectPermission->isEverybodyAllowed($project_id)) {
+ return $this->user->getActiveUsersList();
+ }
+
+ $userMembers = $this->db->table(self::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
+ ->join(User::TABLE, 'id', 'user_id')
+ ->eq(User::TABLE.'.is_active', 1)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER))
+ ->findAll();
+
+ $groupMembers = $this->projectGroupRole->getAssignableUsers($project_id);
+ $members = array_merge($userMembers, $groupMembers);
+
+ return $this->user->prepareList($members);
+ }
+
+ /**
+ * Get list of users that can be assigned to a task (only Manager and Member)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param bool $unassigned Prepend the 'Unassigned' value
+ * @param bool $everybody Prepend the 'Everbody' value
+ * @param bool $singleUser If there is only one user return only this user
+ * @return array
+ */
+ public function getAssignableUsersList($project_id, $unassigned = true, $everybody = false, $singleUser = false)
+ {
+ $users = $this->getAssignableUsers($project_id);
+
+ if ($singleUser && count($users) === 1) {
+ return $users;
+ }
+
+ if ($unassigned) {
+ $users = array(t('Unassigned')) + $users;
+ }
+
+ if ($everybody) {
+ $users = array(User::EVERYBODY_ID => t('Everybody')) + $users;
+ }
+
+ return $users;
+ }
+
+ /**
+ * Add a user to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @param string $role
+ * @return boolean
+ */
+ public function addUser($project_id, $user_id, $role)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'user_id' => $user_id,
+ 'project_id' => $project_id,
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Remove a user from the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function removeUser($project_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->remove();
+ }
+
+ /**
+ * Change a user role for the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @param string $role
+ * @return boolean
+ */
+ public function changeUserRole($project_id, $user_id, $role)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('user_id', $user_id)
+ ->eq('project_id', $project_id)
+ ->update(array(
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Copy user access from a project to another one
+ *
+ * @param integer $project_src_id Project Template
+ * @return integer $project_dst_id Project that receives the copy
+ * @return boolean
+ */
+ public function duplicate($project_src_id, $project_dst_id)
+ {
+ $rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll();
+
+ foreach ($rows as $row) {
+ $result = $this->db->table(self::TABLE)->save(array(
+ 'project_id' => $project_dst_id,
+ 'user_id' => $row['user_id'],
+ 'role' => $row['role'],
+ ));
+
+ if (! $result) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/app/Model/ProjectUserRoleFilter.php b/app/Model/ProjectUserRoleFilter.php
new file mode 100644
index 00000000..64403643
--- /dev/null
+++ b/app/Model/ProjectUserRoleFilter.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Project User Role Filter
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectUserRoleFilter extends Base
+{
+ /**
+ * Query
+ *
+ * @access protected
+ * @var \PicoDb\Table
+ */
+ protected $query;
+
+ /**
+ * Initialize filter
+ *
+ * @access public
+ * @return UserFilter
+ */
+ public function create()
+ {
+ $this->query = $this->db->table(ProjectUserRole::TABLE);
+ return $this;
+ }
+
+ /**
+ * Get all results of the filter
+ *
+ * @access public
+ * @param string $column
+ * @return array
+ */
+ public function findAll($column = '')
+ {
+ if ($column !== '') {
+ return $this->query->asc($column)->findAllByColumn($column);
+ }
+
+ return $this->query->findAll();
+ }
+
+ /**
+ * Get the PicoDb query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Filter by project id
+ *
+ * @access public
+ * @param integer $project_id
+ * @return ProjectUserRoleFilter
+ */
+ public function filterByProjectId($project_id)
+ {
+ $this->query->eq(ProjectUserRole::TABLE.'.project_id', $project_id);
+ return $this;
+ }
+
+ /**
+ * Filter by username
+ *
+ * @access public
+ * @param string $input
+ * @return ProjectUserRoleFilter
+ */
+ public function startWithUsername($input)
+ {
+ $this->query
+ ->join(User::TABLE, 'id', 'user_id')
+ ->ilike(User::TABLE.'.username', $input.'%');
+
+ return $this;
+ }
+}
diff --git a/app/Model/RememberMeSession.php b/app/Model/RememberMeSession.php
new file mode 100644
index 00000000..8989a6d7
--- /dev/null
+++ b/app/Model/RememberMeSession.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Security\Token;
+
+/**
+ * Remember Me Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class RememberMeSession extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'remember_me';
+
+ /**
+ * Expiration (60 days)
+ *
+ * @var integer
+ */
+ const EXPIRATION = 5184000;
+
+ /**
+ * Get a remember me record
+ *
+ * @access public
+ * @param $token
+ * @param $sequence
+ * @return mixed
+ */
+ public function find($token, $sequence)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('token', $token)
+ ->eq('sequence', $sequence)
+ ->gt('expiration', time())
+ ->findOne();
+ }
+
+ /**
+ * Get all sessions for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getAll($user_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('user_id', $user_id)
+ ->desc('date_creation')
+ ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
+ ->findAll();
+ }
+
+ /**
+ * Create a new RememberMe session
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param string $ip IP Address
+ * @param string $user_agent User Agent
+ * @return array
+ */
+ public function create($user_id, $ip, $user_agent)
+ {
+ $token = hash('sha256', $user_id.$user_agent.$ip.Token::getToken());
+ $sequence = Token::getToken();
+ $expiration = time() + self::EXPIRATION;
+
+ $this->cleanup($user_id);
+
+ $this
+ ->db
+ ->table(self::TABLE)
+ ->insert(array(
+ 'user_id' => $user_id,
+ 'ip' => $ip,
+ 'user_agent' => $user_agent,
+ 'token' => $token,
+ 'sequence' => $sequence,
+ 'expiration' => $expiration,
+ 'date_creation' => time(),
+ ));
+
+ return array(
+ 'token' => $token,
+ 'sequence' => $sequence,
+ 'expiration' => $expiration,
+ );
+ }
+
+ /**
+ * Remove a session record
+ *
+ * @access public
+ * @param integer $session_id Session id
+ * @return mixed
+ */
+ public function remove($session_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $session_id)
+ ->remove();
+ }
+
+ /**
+ * Remove old sessions for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return bool
+ */
+ public function cleanup($user_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('user_id', $user_id)
+ ->lt('expiration', time())
+ ->remove();
+ }
+
+ /**
+ * Return a new sequence token and update the database
+ *
+ * @access public
+ * @param string $token Session token
+ * @return string
+ */
+ public function updateSequence($token)
+ {
+ $sequence = Token::getToken();
+
+ $this
+ ->db
+ ->table(self::TABLE)
+ ->eq('token', $token)
+ ->update(array('sequence' => $sequence));
+
+ return $sequence;
+ }
+}
diff --git a/app/Model/Setting.php b/app/Model/Setting.php
index 3507d424..44e6c065 100644
--- a/app/Model/Setting.php
+++ b/app/Model/Setting.php
@@ -47,10 +47,12 @@ abstract class Setting extends Base
*/
public function getOption($name, $default = '')
{
- return $this->db
+ $value = $this->db
->table(self::TABLE)
->eq('option', $name)
- ->findOneColumn('value') ?: $default;
+ ->findOneColumn('value');
+
+ return $value === null || $value === false || $value === '' ? $default : $value;
}
/**
diff --git a/app/Model/Subtask.php b/app/Model/Subtask.php
index 664e41e1..b5898fcf 100644
--- a/app/Model/Subtask.php
+++ b/app/Model/Subtask.php
@@ -4,11 +4,9 @@ namespace Kanboard\Model;
use PicoDb\Database;
use Kanboard\Event\SubtaskEvent;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
/**
- * Subtask model
+ * Subtask Model
*
* @package model
* @author Frederic Guillot
@@ -265,89 +263,36 @@ class Subtask extends Base
}
/**
- * Get subtasks with consecutive positions
- *
- * If you remove a subtask, the positions are not anymore consecutives
- *
- * @access public
- * @param integer $task_id
- * @return array
- */
- public function getNormalizedPositions($task_id)
- {
- $subtasks = $this->db->hashtable(self::TABLE)->eq('task_id', $task_id)->asc('position')->getAll('id', 'position');
- $position = 1;
-
- foreach ($subtasks as $subtask_id => $subtask_position) {
- $subtasks[$subtask_id] = $position++;
- }
-
- return $subtasks;
- }
-
- /**
- * Save the new positions for a set of subtasks
- *
- * @access public
- * @param array $subtasks Hashmap of column_id/column_position
- * @return boolean
- */
- public function savePositions(array $subtasks)
- {
- return $this->db->transaction(function (Database $db) use ($subtasks) {
-
- foreach ($subtasks as $subtask_id => $position) {
- if (! $db->table(Subtask::TABLE)->eq('id', $subtask_id)->update(array('position' => $position))) {
- return false;
- }
- }
- });
- }
-
- /**
- * Move a subtask down, increment the position value
+ * Save subtask position
*
* @access public
* @param integer $task_id
* @param integer $subtask_id
+ * @param integer $position
* @return boolean
*/
- public function moveDown($task_id, $subtask_id)
+ public function changePosition($task_id, $subtask_id, $position)
{
- $subtasks = $this->getNormalizedPositions($task_id);
- $positions = array_flip($subtasks);
-
- if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] < count($subtasks)) {
- $position = ++$subtasks[$subtask_id];
- $subtasks[$positions[$position]]--;
-
- return $this->savePositions($subtasks);
+ if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('task_id', $task_id)->count()) {
+ return false;
}
- return false;
- }
-
- /**
- * Move a subtask up, decrement the position value
- *
- * @access public
- * @param integer $task_id
- * @param integer $subtask_id
- * @return boolean
- */
- public function moveUp($task_id, $subtask_id)
- {
- $subtasks = $this->getNormalizedPositions($task_id);
- $positions = array_flip($subtasks);
+ $subtask_ids = $this->db->table(self::TABLE)->eq('task_id', $task_id)->neq('id', $subtask_id)->asc('position')->findAllByColumn('id');
+ $offset = 1;
+ $results = array();
- if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] > 1) {
- $position = --$subtasks[$subtask_id];
- $subtasks[$positions[$position]]++;
+ foreach ($subtask_ids as $current_subtask_id) {
+ if ($offset == $position) {
+ $offset++;
+ }
- return $this->savePositions($subtasks);
+ $results[] = $this->db->table(self::TABLE)->eq('id', $current_subtask_id)->update(array('position' => $offset));
+ $offset++;
}
- return false;
+ $results[] = $this->db->table(self::TABLE)->eq('id', $subtask_id)->update(array('position' => $position));
+
+ return !in_array(false, $results, true);
}
/**
@@ -355,15 +300,16 @@ class Subtask extends Base
*
* @access public
* @param integer $subtask_id
- * @return bool
+ * @return boolean|integer
*/
public function toggleStatus($subtask_id)
{
$subtask = $this->getById($subtask_id);
+ $status = ($subtask['status'] + 1) % 3;
$values = array(
'id' => $subtask['id'],
- 'status' => ($subtask['status'] + 1) % 3,
+ 'status' => $status,
'task_id' => $subtask['task_id'],
);
@@ -371,7 +317,7 @@ class Subtask extends Base
$values['user_id'] = $this->userSession->getId();
}
- return $this->update($values);
+ return $this->update($values) ? $status : false;
}
/**
@@ -437,10 +383,10 @@ class Subtask extends Base
return $this->db->transaction(function (Database $db) use ($src_task_id, $dst_task_id) {
$subtasks = $db->table(Subtask::TABLE)
- ->columns('title', 'time_estimated', 'position')
- ->eq('task_id', $src_task_id)
- ->asc('position')
- ->findAll();
+ ->columns('title', 'time_estimated', 'position')
+ ->eq('task_id', $src_task_id)
+ ->asc('position')
+ ->findAll();
foreach ($subtasks as &$subtask) {
$subtask['task_id'] = $dst_task_id;
@@ -451,90 +397,4 @@ class Subtask extends Base
}
});
}
-
- /**
- * 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)
- {
- $rules = array(
- new Validators\Required('task_id', t('The task id is required')),
- new Validators\Required('title', t('The title is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The subtask id is required')),
- new Validators\Required('task_id', t('The task id is required')),
- new Validators\Required('title', t('The title is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate API modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateApiModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The subtask id is required')),
- new Validators\Required('task_id', t('The task id is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('The subtask id must be an integer')),
- new Validators\Integer('task_id', t('The task id must be an integer')),
- new Validators\MaxLength('title', t('The maximum length is %d characters', 255), 255),
- new Validators\Integer('user_id', t('The user id must be an integer')),
- new Validators\Integer('status', t('The status must be an integer')),
- new Validators\Numeric('time_estimated', t('The time must be a numeric value')),
- new Validators\Numeric('time_spent', t('The time must be a numeric value')),
- );
- }
}
diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php
index a741dbb5..b766b542 100644
--- a/app/Model/SubtaskTimeTracking.php
+++ b/app/Model/SubtaskTimeTracking.php
@@ -302,11 +302,11 @@ class SubtaskTimeTracking extends Base
{
$hook = 'model:subtask-time-tracking:calculate:time-spent';
$start_time = $this->db
- ->table(self::TABLE)
- ->eq('subtask_id', $subtask_id)
- ->eq('user_id', $user_id)
- ->eq('end', 0)
- ->findOneColumn('start');
+ ->table(self::TABLE)
+ ->eq('subtask_id', $subtask_id)
+ ->eq('user_id', $user_id)
+ ->eq('end', 0)
+ ->findOneColumn('start');
if (empty($start_time)) {
return 0;
diff --git a/app/Model/Swimlane.php b/app/Model/Swimlane.php
index df44985a..721f20d3 100644
--- a/app/Model/Swimlane.php
+++ b/app/Model/Swimlane.php
@@ -2,9 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
/**
* Swimlanes
*
@@ -99,10 +96,11 @@ class Swimlane extends Base
*/
public function getDefault($project_id)
{
- $result = $this->db->table(Project::TABLE)
- ->eq('id', $project_id)
- ->columns('id', 'default_swimlane', 'show_default_swimlane')
- ->findOne();
+ $result = $this->db
+ ->table(Project::TABLE)
+ ->eq('id', $project_id)
+ ->columns('id', 'default_swimlane', 'show_default_swimlane')
+ ->findOne();
if ($result['default_swimlane'] === 'Default swimlane') {
$result['default_swimlane'] = t($result['default_swimlane']);
@@ -120,10 +118,11 @@ class Swimlane extends Base
*/
public function getAll($project_id)
{
- return $this->db->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->orderBy('position', 'asc')
- ->findAll();
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->orderBy('position', 'asc')
+ ->findAll();
}
/**
@@ -136,9 +135,10 @@ class Swimlane extends Base
*/
public function getAllByStatus($project_id, $status = self::ACTIVE)
{
- $query = $this->db->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', $status);
+ $query = $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('is_active', $status);
if ($status == self::ACTIVE) {
$query->asc('position');
@@ -158,17 +158,19 @@ class Swimlane extends Base
*/
public function getSwimlanes($project_id)
{
- $swimlanes = $this->db->table(self::TABLE)
- ->columns('id', 'name', 'description')
- ->eq('project_id', $project_id)
- ->eq('is_active', self::ACTIVE)
- ->orderBy('position', 'asc')
- ->findAll();
-
- $default_swimlane = $this->db->table(Project::TABLE)
- ->eq('id', $project_id)
- ->eq('show_default_swimlane', 1)
- ->findOneColumn('default_swimlane');
+ $swimlanes = $this->db
+ ->table(self::TABLE)
+ ->columns('id', 'name', 'description')
+ ->eq('project_id', $project_id)
+ ->eq('is_active', self::ACTIVE)
+ ->orderBy('position', 'asc')
+ ->findAll();
+
+ $default_swimlane = $this->db
+ ->table(Project::TABLE)
+ ->eq('id', $project_id)
+ ->eq('show_default_swimlane', 1)
+ ->findOneColumn('default_swimlane');
if ($default_swimlane) {
if ($default_swimlane === 'Default swimlane') {
@@ -203,11 +205,12 @@ class Swimlane extends Base
$swimlanes[0] = $default === 'Default swimlane' ? t($default) : $default;
}
- return $swimlanes + $this->db->hashtable(self::TABLE)
- ->eq('project_id', $project_id)
- ->in('is_active', $only_active ? array(self::ACTIVE) : array(self::ACTIVE, self::INACTIVE))
- ->orderBy('position', 'asc')
- ->getAll('id', 'name');
+ return $swimlanes + $this->db
+ ->hashtable(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->in('is_active', $only_active ? array(self::ACTIVE) : array(self::ACTIVE, self::INACTIVE))
+ ->orderBy('position', 'asc')
+ ->getAll('id', 'name');
}
/**
@@ -235,9 +238,10 @@ class Swimlane extends Base
*/
public function update(array $values)
{
- return $this->db->table(self::TABLE)
- ->eq('id', $values['id'])
- ->update($values);
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $values['id'])
+ ->update($values);
}
/**
@@ -250,12 +254,46 @@ class Swimlane extends Base
public function updateDefault(array $values)
{
return $this->db
- ->table(Project::TABLE)
- ->eq('id', $values['id'])
- ->update(array(
- 'default_swimlane' => $values['default_swimlane'],
- 'show_default_swimlane' => $values['show_default_swimlane'],
- ));
+ ->table(Project::TABLE)
+ ->eq('id', $values['id'])
+ ->update(array(
+ 'default_swimlane' => $values['default_swimlane'],
+ 'show_default_swimlane' => $values['show_default_swimlane'],
+ ));
+ }
+
+ /**
+ * Enable the default swimlane
+ *
+ * @access public
+ * @param integer $project_id
+ * @return bool
+ */
+ public function enableDefault($project_id)
+ {
+ return $this->db
+ ->table(Project::TABLE)
+ ->eq('id', $project_id)
+ ->update(array(
+ 'show_default_swimlane' => 1,
+ ));
+ }
+
+ /**
+ * Disable the default swimlane
+ *
+ * @access public
+ * @param integer $project_id
+ * @return bool
+ */
+ public function disableDefault($project_id)
+ {
+ return $this->db
+ ->table(Project::TABLE)
+ ->eq('id', $project_id)
+ ->update(array(
+ 'show_default_swimlane' => 0,
+ ));
}
/**
@@ -267,10 +305,11 @@ class Swimlane extends Base
*/
public function getLastPosition($project_id)
{
- return $this->db->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', 1)
- ->count() + 1;
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('is_active', 1)
+ ->count() + 1;
}
/**
@@ -284,12 +323,12 @@ class Swimlane extends Base
public function disable($project_id, $swimlane_id)
{
$result = $this->db
- ->table(self::TABLE)
- ->eq('id', $swimlane_id)
- ->update(array(
- 'is_active' => self::INACTIVE,
- 'position' => 0,
- ));
+ ->table(self::TABLE)
+ ->eq('id', $swimlane_id)
+ ->update(array(
+ 'is_active' => self::INACTIVE,
+ 'position' => 0,
+ ));
if ($result) {
// Re-order positions
@@ -310,12 +349,12 @@ class Swimlane extends Base
public function enable($project_id, $swimlane_id)
{
return $this->db
- ->table(self::TABLE)
- ->eq('id', $swimlane_id)
- ->update(array(
- 'is_active' => self::ACTIVE,
- 'position' => $this->getLastPosition($project_id),
- ));
+ ->table(self::TABLE)
+ ->eq('id', $swimlane_id)
+ ->update(array(
+ 'is_active' => self::ACTIVE,
+ 'position' => $this->getLastPosition($project_id),
+ ));
}
/**
@@ -356,11 +395,13 @@ class Swimlane extends Base
public function updatePositions($project_id)
{
$position = 0;
- $swimlanes = $this->db->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', 1)
- ->asc('position')
- ->findAllByColumn('id');
+ $swimlanes = $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('is_active', 1)
+ ->asc('position')
+ ->asc('id')
+ ->findAllByColumn('id');
if (! $swimlanes) {
return false;
@@ -368,77 +409,50 @@ class Swimlane extends Base
foreach ($swimlanes as $swimlane_id) {
$this->db->table(self::TABLE)
- ->eq('id', $swimlane_id)
- ->update(array('position' => ++$position));
+ ->eq('id', $swimlane_id)
+ ->update(array('position' => ++$position));
}
return true;
}
/**
- * Move a swimlane down, increment the position value
+ * Change swimlane position
*
* @access public
- * @param integer $project_id Project id
- * @param integer $swimlane_id Swimlane id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $position
* @return boolean
*/
- public function moveDown($project_id, $swimlane_id)
+ public function changePosition($project_id, $swimlane_id, $position)
{
- $swimlanes = $this->db->hashtable(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', self::ACTIVE)
- ->asc('position')
- ->getAll('id', 'position');
-
- $positions = array_flip($swimlanes);
-
- if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] < count($swimlanes)) {
- $position = ++$swimlanes[$swimlane_id];
- $swimlanes[$positions[$position]]--;
-
- $this->db->startTransaction();
- $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position));
- $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]]));
- $this->db->closeTransaction();
-
- return true;
+ if ($position < 1 || $position > $this->db->table(self::TABLE)->eq('project_id', $project_id)->count()) {
+ return false;
}
- return false;
- }
+ $swimlane_ids = $this->db->table(self::TABLE)
+ ->eq('is_active', 1)
+ ->eq('project_id', $project_id)
+ ->neq('id', $swimlane_id)
+ ->asc('position')
+ ->findAllByColumn('id');
- /**
- * Move a swimlane up, decrement the position value
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $swimlane_id Swimlane id
- * @return boolean
- */
- public function moveUp($project_id, $swimlane_id)
- {
- $swimlanes = $this->db->hashtable(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('is_active', self::ACTIVE)
- ->asc('position')
- ->getAll('id', 'position');
-
- $positions = array_flip($swimlanes);
+ $offset = 1;
+ $results = array();
- if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] > 1) {
- $position = --$swimlanes[$swimlane_id];
- $swimlanes[$positions[$position]]++;
-
- $this->db->startTransaction();
- $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position));
- $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]]));
- $this->db->closeTransaction();
+ foreach ($swimlane_ids as $current_swimlane_id) {
+ if ($offset == $position) {
+ $offset++;
+ }
- return true;
+ $results[] = $this->db->table(self::TABLE)->eq('id', $current_swimlane_id)->update(array('position' => $offset));
+ $offset++;
}
- return false;
+ $results[] = $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position));
+
+ return !in_array(false, $results, true);
}
/**
@@ -470,85 +484,4 @@ class Swimlane extends Base
return true;
}
-
- /**
- * 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)
- {
- $rules = array(
- new Validators\Required('project_id', t('The project id is required')),
- new Validators\Required('name', t('The name is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- new Validators\Required('name', t('The name is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate default swimlane modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateDefaultModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- new Validators\Required('default_swimlane', t('The name is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('The id must be an integer')),
- new Validators\Integer('project_id', t('The project id must be an integer')),
- new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50)
- );
- }
}
diff --git a/app/Model/Task.php b/app/Model/Task.php
index f1cd094f..f8b41b9f 100644
--- a/app/Model/Task.php
+++ b/app/Model/Task.php
@@ -41,6 +41,8 @@ class Task extends Base
const EVENT_CREATE_UPDATE = 'task.create_update';
const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change';
const EVENT_OVERDUE = 'task.overdue';
+ const EVENT_USER_MENTION = 'task.user.mention';
+ const EVENT_DAILY_CRONJOB = 'task.cronjob.daily';
/**
* Recurrence: status
@@ -90,7 +92,7 @@ class Task extends Base
return false;
}
- $this->file->removeAll($task_id);
+ $this->taskFile->removeAll($task_id);
return $this->db->table(self::TABLE)->eq('id', $task_id)->remove();
}
@@ -197,4 +199,25 @@ class Task extends Base
return round(($position * 100) / count($columns), 1);
}
+
+ /**
+ * Helper method to duplicate all tasks to another project
+ *
+ * @access public
+ * @param integer $src_project_id
+ * @param integer $dst_project_id
+ * @return boolean
+ */
+ public function duplicate($src_project_id, $dst_project_id)
+ {
+ $task_ids = $this->taskFinder->getAllIds($src_project_id, array(Task::STATUS_OPEN, Task::STATUS_CLOSED));
+
+ foreach ($task_ids as $task_id) {
+ if (! $this->taskDuplication->duplicateToProject($task_id, $dst_project_id)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
diff --git a/app/Model/TaskAnalytic.php b/app/Model/TaskAnalytic.php
index bdfec3cb..cff56744 100644
--- a/app/Model/TaskAnalytic.php
+++ b/app/Model/TaskAnalytic.php
@@ -48,7 +48,7 @@ class TaskAnalytic extends Base
public function getTimeSpentByColumn(array $task)
{
$result = array();
- $columns = $this->board->getColumnsList($task['project_id']);
+ $columns = $this->column->getList($task['project_id']);
$sums = $this->transition->getTimeSpentByTask($task['id']);
foreach ($columns as $column_id => $column_title) {
diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php
index 5ef1a04b..576eb18c 100644
--- a/app/Model/TaskCreation.php
+++ b/app/Model/TaskCreation.php
@@ -49,13 +49,14 @@ class TaskCreation extends Base
*/
public function prepare(array &$values)
{
- $this->dateParser->convert($values, array('date_due'));
- $this->dateParser->convert($values, array('date_started'), true);
+ $values = $this->dateParser->convert($values, array('date_due'));
+ $values = $this->dateParser->convert($values, array('date_started'), true);
+
$this->removeFields($values, array('another_task'));
$this->resetFields($values, array('date_started', 'creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated'));
if (empty($values['column_id'])) {
- $values['column_id'] = $this->board->getFirstColumn($values['project_id']);
+ $values['column_id'] = $this->column->getFirstColumnId($values['project_id']);
}
if (empty($values['color_id'])) {
@@ -86,8 +87,16 @@ class TaskCreation extends Base
*/
private function fireEvents($task_id, array $values)
{
- $values['task_id'] = $task_id;
- $this->container['dispatcher']->dispatch(Task::EVENT_CREATE_UPDATE, new TaskEvent($values));
- $this->container['dispatcher']->dispatch(Task::EVENT_CREATE, new TaskEvent($values));
+ $event = new TaskEvent(array('task_id' => $task_id) + $values);
+
+ $this->logger->debug('Event fired: '.Task::EVENT_CREATE_UPDATE);
+ $this->logger->debug('Event fired: '.Task::EVENT_CREATE);
+
+ $this->dispatcher->dispatch(Task::EVENT_CREATE_UPDATE, $event);
+ $this->dispatcher->dispatch(Task::EVENT_CREATE, $event);
+
+ if (! empty($values['description'])) {
+ $this->userMention->fireEvents($values['description'], Task::EVENT_USER_MENTION, $event);
+ }
}
}
diff --git a/app/Model/TaskDuplication.php b/app/Model/TaskDuplication.php
index e81fb232..b081aac1 100644
--- a/app/Model/TaskDuplication.php
+++ b/app/Model/TaskDuplication.php
@@ -64,7 +64,7 @@ class TaskDuplication extends Base
if ($values['recurrence_status'] == Task::RECURRING_STATUS_PENDING) {
$values['recurrence_parent'] = $task_id;
- $values['column_id'] = $this->board->getFirstColumn($values['project_id']);
+ $values['column_id'] = $this->column->getFirstColumnId($values['project_id']);
$this->calculateRecurringTaskDueDate($values);
$recurring_task_id = $this->save($task_id, $values);
@@ -181,12 +181,12 @@ class TaskDuplication extends Base
// Check if the column exists for the destination project
if ($values['column_id'] > 0) {
- $values['column_id'] = $this->board->getColumnIdByTitle(
+ $values['column_id'] = $this->column->getColumnIdByTitle(
$values['project_id'],
- $this->board->getColumnTitleById($values['column_id'])
+ $this->column->getColumnTitleById($values['column_id'])
);
- $values['column_id'] = $values['column_id'] ?: $this->board->getFirstColumn($values['project_id']);
+ $values['column_id'] = $values['column_id'] ?: $this->column->getFirstColumnId($values['project_id']);
}
return $values;
diff --git a/app/Model/TaskExport.php b/app/Model/TaskExport.php
index d38a5384..ed179a4f 100644
--- a/app/Model/TaskExport.php
+++ b/app/Model/TaskExport.php
@@ -58,6 +58,7 @@ class TaskExport extends Base
tasks.date_due,
creators.username AS creator_username,
users.username AS assignee_username,
+ users.name AS assignee_name,
tasks.score,
tasks.title,
tasks.date_creation,
@@ -105,7 +106,7 @@ class TaskExport extends Base
$task['score'] = $task['score'] ?: 0;
$task['swimlane_id'] = isset($swimlanes[$task['swimlane_id']]) ? $swimlanes[$task['swimlane_id']] : '?';
- $this->dateParser->format($task, array('date_due', 'date_modification', 'date_creation', 'date_started', 'date_completed'), 'Y-m-d');
+ $task = $this->dateParser->format($task, array('date_due', 'date_modification', 'date_creation', 'date_started', 'date_completed'), 'Y-m-d');
return $task;
}
@@ -129,7 +130,8 @@ class TaskExport extends Base
e('Color'),
e('Due date'),
e('Creator'),
- e('Assignee'),
+ e('Assignee Username'),
+ e('Assignee Name'),
e('Complexity'),
e('Title'),
e('Creation date'),
diff --git a/app/Model/TaskExternalLink.php b/app/Model/TaskExternalLink.php
new file mode 100644
index 00000000..f2c756b4
--- /dev/null
+++ b/app/Model/TaskExternalLink.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Task External Link Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskExternalLink extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'task_has_external_links';
+
+ /**
+ * Get all links
+ *
+ * @access public
+ * @param integer $task_id
+ * @return array
+ */
+ public function getAll($task_id)
+ {
+ $types = $this->externalLinkManager->getTypes();
+
+ $links = $this->db->table(self::TABLE)
+ ->columns(self::TABLE.'.*', User::TABLE.'.name AS creator_name', User::TABLE.'.username AS creator_username')
+ ->eq('task_id', $task_id)
+ ->asc('title')
+ ->join(User::TABLE, 'id', 'creator_id')
+ ->findAll();
+
+ foreach ($links as &$link) {
+ $link['dependency_label'] = $this->externalLinkManager->getDependencyLabel($link['link_type'], $link['dependency']);
+ $link['type'] = isset($types[$link['link_type']]) ? $types[$link['link_type']] : t('Unknown');
+ }
+
+ return $links;
+ }
+
+ /**
+ * Get link
+ *
+ * @access public
+ * @param integer $link_id
+ * @return array
+ */
+ public function getById($link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $link_id)->findOne();
+ }
+
+ /**
+ * Add a new link in the database
+ *
+ * @access public
+ * @param array $values Form values
+ * @return boolean|integer
+ */
+ public function create(array $values)
+ {
+ unset($values['id']);
+ $values['creator_id'] = $this->userSession->getId();
+ $values['date_creation'] = time();
+ $values['date_modification'] = $values['date_creation'];
+
+ return $this->persist(self::TABLE, $values);
+ }
+
+ /**
+ * Modify external link
+ *
+ * @access public
+ * @param array $values Form values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ $values['date_modification'] = time();
+ return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ * @param integer $link_id
+ * @return boolean
+ */
+ public function remove($link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $link_id)->remove();
+ }
+}
diff --git a/app/Model/TaskFile.php b/app/Model/TaskFile.php
new file mode 100644
index 00000000..45a3b97f
--- /dev/null
+++ b/app/Model/TaskFile.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Task File Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskFile extends File
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'task_has_files';
+
+ /**
+ * SQL foreign key
+ *
+ * @var string
+ */
+ const FOREIGN_KEY = 'task_id';
+
+ /**
+ * Path prefix
+ *
+ * @var string
+ */
+ const PATH_PREFIX = 'tasks';
+
+ /**
+ * Events
+ *
+ * @var string
+ */
+ const EVENT_CREATE = 'task.file.create';
+
+ /**
+ * Handle screenshot upload
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param string $blob Base64 encoded image
+ * @return bool|integer
+ */
+ public function uploadScreenshot($task_id, $blob)
+ {
+ $original_filename = e('Screenshot taken %s', $this->helper->dt->datetime(time())).'.png';
+ return $this->uploadContent($task_id, $original_filename, $blob);
+ }
+}
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
index 137a7a8e..1883298d 100644
--- a/app/Model/TaskFilter.php
+++ b/app/Model/TaskFilter.php
@@ -30,6 +30,7 @@ class TaskFilter extends Base
'T_COLUMN' => 'filterByColumnName',
'T_REFERENCE' => 'filterByReference',
'T_SWIMLANE' => 'filterBySwimlaneName',
+ 'T_LINK' => 'filterByLinkName',
);
/**
@@ -108,6 +109,22 @@ class TaskFilter extends Base
}
/**
+ * Create a new link query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function createLinkQuery()
+ {
+ return $this->db->table(TaskLink::TABLE)
+ ->columns(
+ TaskLink::TABLE.'.task_id',
+ Link::TABLE.'.label'
+ )
+ ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE);
+ }
+
+ /**
* Clone the filter
*
* @access public
@@ -452,7 +469,7 @@ class TaskFilter extends Base
$this->query->beginOr();
foreach ($values as $project) {
- $this->query->ilike(Board::TABLE.'.title', $project);
+ $this->query->ilike(Column::TABLE.'.title', $project);
}
$this->query->closeOr();
@@ -507,6 +524,30 @@ class TaskFilter extends Base
}
/**
+ * Filter by link
+ *
+ * @access public
+ * @param array $values List of links
+ * @return TaskFilter
+ */
+ public function filterByLinkName(array $values)
+ {
+ $this->query->beginOr();
+
+ $link_query = $this->createLinkQuery()->in(Link::TABLE.'.label', $values);
+ $matching_task_ids = $link_query->findAllByColumn('task_id');
+ if (empty($matching_task_ids)) {
+ $this->query->eq(Task::TABLE.'.id', 0);
+ } else {
+ $this->query->in(Task::TABLE.'.id', $matching_task_ids);
+ }
+
+ $this->query->closeOr();
+
+ return $this;
+ }
+
+ /**
* Filter by due date
*
* @access public
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index 9514fe4a..0492a9bf 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -38,14 +38,14 @@ class TaskFinder extends Base
Task::TABLE.'.time_spent',
Task::TABLE.'.time_estimated',
Project::TABLE.'.name AS project_name',
- Board::TABLE.'.title AS column_name',
+ Column::TABLE.'.title AS column_name',
User::TABLE.'.username AS assignee_username',
User::TABLE.'.name AS assignee_name'
)
->eq(Task::TABLE.'.is_active', $is_active)
->in(Project::TABLE.'.id', $project_ids)
->join(Project::TABLE, 'id', 'project_id')
- ->join(Board::TABLE, 'id', 'column_id', Task::TABLE)
+ ->join(Column::TABLE, 'id', 'column_id', Task::TABLE)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE);
}
@@ -88,11 +88,12 @@ class TaskFinder extends Base
return $this->db
->table(Task::TABLE)
->columns(
- '(SELECT count(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
- '(SELECT count(*) FROM '.File::TABLE.' WHERE task_id=tasks.id) AS nb_files',
- '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks',
- '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
- '(SELECT count(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links',
+ '(SELECT COUNT(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
+ '(SELECT COUNT(*) FROM '.TaskFile::TABLE.' WHERE task_id=tasks.id) AS nb_files',
+ '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks',
+ '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
+ '(SELECT COUNT(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links',
+ '(SELECT COUNT(*) FROM '.TaskExternalLink::TABLE.' WHERE '.TaskExternalLink::TABLE.'.task_id = tasks.id) AS nb_external_links',
'(SELECT DISTINCT 1 FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id AND '.TaskLink::TABLE.'.link_id = 9) AS is_milestone',
'tasks.id',
'tasks.reference',
@@ -113,6 +114,7 @@ class TaskFinder extends Base
'tasks.is_active',
'tasks.score',
'tasks.category_id',
+ 'tasks.priority',
'tasks.date_moved',
'tasks.recurrence_status',
'tasks.recurrence_trigger',
@@ -122,19 +124,20 @@ class TaskFinder extends Base
'tasks.recurrence_parent',
'tasks.recurrence_child',
'tasks.time_estimated',
+ 'tasks.time_spent',
User::TABLE.'.username AS assignee_username',
User::TABLE.'.name AS assignee_name',
Category::TABLE.'.name AS category_name',
Category::TABLE.'.description AS category_description',
- Board::TABLE.'.title AS column_name',
- Board::TABLE.'.position AS column_position',
+ Column::TABLE.'.title AS column_name',
+ Column::TABLE.'.position AS column_position',
Swimlane::TABLE.'.name AS swimlane_name',
Project::TABLE.'.default_swimlane',
Project::TABLE.'.name AS project_name'
)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE)
->join(Category::TABLE, 'id', 'category_id', Task::TABLE)
- ->join(Board::TABLE, 'id', 'column_id', Task::TABLE)
+ ->join(Column::TABLE, 'id', 'column_id', Task::TABLE)
->join(Swimlane::TABLE, 'id', 'swimlane_id', Task::TABLE)
->join(Project::TABLE, 'id', 'project_id', Task::TABLE);
}
@@ -177,6 +180,23 @@ class TaskFinder extends Base
}
/**
+ * Get all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id
+ * @param array $status
+ * @return array
+ */
+ public function getAllIds($project_id, array $status = array(Task::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(Task::TABLE)
+ ->eq(Task::TABLE.'.project_id', $project_id)
+ ->in(Task::TABLE.'.is_active', $status)
+ ->findAllByColumn('id');
+ }
+
+ /**
* Get overdue tasks query
*
* @access public
@@ -307,6 +327,7 @@ class TaskFinder extends Base
tasks.is_active,
tasks.score,
tasks.category_id,
+ tasks.priority,
tasks.swimlane_id,
tasks.date_moved,
tasks.recurrence_status,
diff --git a/app/Model/TaskImport.php b/app/Model/TaskImport.php
index e8dd1946..ccab0152 100644
--- a/app/Model/TaskImport.php
+++ b/app/Model/TaskImport.php
@@ -111,7 +111,7 @@ class TaskImport extends Base
}
if (! empty($row['column'])) {
- $values['column_id'] = $this->board->getColumnIdByTitle($this->projectId, $row['column']);
+ $values['column_id'] = $this->column->getColumnIdByTitle($this->projectId, $row['column']);
}
if (! empty($row['category'])) {
diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php
index 1ac59203..a57bf3b0 100644
--- a/app/Model/TaskLink.php
+++ b/app/Model/TaskLink.php
@@ -2,8 +2,6 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
use Kanboard\Event\TaskLinkEvent;
/**
@@ -77,22 +75,23 @@ class TaskLink extends Base
Task::TABLE.'.title',
Task::TABLE.'.is_active',
Task::TABLE.'.project_id',
+ Task::TABLE.'.column_id',
Task::TABLE.'.time_spent AS task_time_spent',
Task::TABLE.'.time_estimated AS task_time_estimated',
Task::TABLE.'.owner_id AS task_assignee_id',
User::TABLE.'.username AS task_assignee_username',
User::TABLE.'.name AS task_assignee_name',
- Board::TABLE.'.title AS column_title',
+ Column::TABLE.'.title AS column_title',
Project::TABLE.'.name AS project_name'
)
->eq(self::TABLE.'.task_id', $task_id)
->join(Link::TABLE, 'id', 'link_id')
->join(Task::TABLE, 'id', 'opposite_task_id')
- ->join(Board::TABLE, 'id', 'column_id', Task::TABLE)
+ ->join(Column::TABLE, 'id', 'column_id', Task::TABLE)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE)
->join(Project::TABLE, 'id', 'project_id', Task::TABLE)
->asc(Link::TABLE.'.id')
- ->desc(Board::TABLE.'.position')
+ ->desc(Column::TABLE.'.position')
->desc(Task::TABLE.'.is_active')
->asc(Task::TABLE.'.position')
->asc(Task::TABLE.'.id')
@@ -261,59 +260,4 @@ class TaskLink extends Base
return true;
}
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Required('task_id', t('Field required')),
- new Validators\Required('opposite_task_id', t('Field required')),
- new Validators\Required('link_id', t('Field required')),
- new Validators\NotEquals('opposite_task_id', 'task_id', t('A task cannot be linked to itself')),
- new Validators\Exists('opposite_task_id', t('This linked task id doesn\'t exists'), $this->db->getConnection(), Task::TABLE, 'id')
- );
- }
-
- /**
- * 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, $this->commonValidationRules());
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('Field required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
}
diff --git a/app/Model/TaskModification.php b/app/Model/TaskModification.php
index 781646b8..8e59b3fe 100644
--- a/app/Model/TaskModification.php
+++ b/app/Model/TaskModification.php
@@ -17,16 +17,17 @@ class TaskModification extends Base
*
* @access public
* @param array $values
+ * @param boolean $fire_events
* @return boolean
*/
- public function update(array $values)
+ public function update(array $values, $fire_events = true)
{
$original_task = $this->taskFinder->getById($values['id']);
$this->prepare($values);
$result = $this->db->table(Task::TABLE)->eq('id', $original_task['id'])->update($values);
- if ($result) {
+ if ($fire_events && $result) {
$this->fireEvents($original_task, $values);
}
@@ -51,13 +52,14 @@ class TaskModification extends Base
if ($this->isFieldModified('owner_id', $event_data['changes'])) {
$events[] = Task::EVENT_ASSIGNEE_CHANGE;
- } else {
+ } elseif (! empty($event_data['changes'])) {
$events[] = Task::EVENT_CREATE_UPDATE;
$events[] = Task::EVENT_UPDATE;
}
foreach ($events as $event) {
- $this->container['dispatcher']->dispatch($event, new TaskEvent($event_data));
+ $this->logger->debug('Event fired: '.$event);
+ $this->dispatcher->dispatch($event, new TaskEvent($event_data));
}
}
@@ -82,11 +84,12 @@ class TaskModification extends Base
*/
public function prepare(array &$values)
{
- $this->dateParser->convert($values, array('date_due'));
- $this->dateParser->convert($values, array('date_started'), true);
+ $values = $this->dateParser->convert($values, array('date_due'));
+ $values = $this->dateParser->convert($values, array('date_started'), true);
+
$this->removeFields($values, array('another_task', 'id'));
$this->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent'));
- $this->convertIntegerFields($values, array('is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate'));
+ $this->convertIntegerFields($values, array('priority', 'is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate'));
$values['date_modification'] = time();
}
diff --git a/app/Model/TaskPermission.php b/app/Model/TaskPermission.php
index 4bbe6d1d..fac2153e 100644
--- a/app/Model/TaskPermission.php
+++ b/app/Model/TaskPermission.php
@@ -2,6 +2,8 @@
namespace Kanboard\Model;
+use Kanboard\Core\Security\Role;
+
/**
* Task permission model
*
@@ -20,7 +22,7 @@ class TaskPermission extends Base
*/
public function canRemoveTask(array $task)
{
- if ($this->userSession->isAdmin() || $this->projectPermission->isManager($task['project_id'], $this->userSession->getId())) {
+ if ($this->userSession->isAdmin() || $this->projectUserRole->getUserRole($task['project_id'], $this->userSession->getId()) === Role::PROJECT_MANAGER) {
return true;
} elseif (isset($task['creator_id']) && $task['creator_id'] == $this->userSession->getId()) {
return true;
diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php
index da363cb3..4c9928d7 100644
--- a/app/Model/TaskPosition.php
+++ b/app/Model/TaskPosition.php
@@ -32,7 +32,6 @@ class TaskPosition extends Base
$task = $this->taskFinder->getById($task_id);
- // Ignore closed tasks
if ($task['is_active'] == Task::STATUS_CLOSED) {
return true;
}
@@ -167,7 +166,12 @@ class TaskPosition extends Base
return false;
}
- return true;
+ $now = time();
+
+ return $this->db->table(Task::TABLE)->eq('id', $task_id)->update(array(
+ 'date_moved' => $now,
+ 'date_modification' => $now,
+ ));
}
/**
@@ -221,11 +225,14 @@ class TaskPosition extends Base
);
if ($task['swimlane_id'] != $new_swimlane_id) {
- $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data));
+ $this->logger->debug('Event fired: '.Task::EVENT_MOVE_SWIMLANE);
+ $this->dispatcher->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data));
} elseif ($task['column_id'] != $new_column_id) {
- $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data));
+ $this->logger->debug('Event fired: '.Task::EVENT_MOVE_COLUMN);
+ $this->dispatcher->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data));
} elseif ($task['position'] != $new_position) {
- $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data));
+ $this->logger->debug('Event fired: '.Task::EVENT_MOVE_POSITION);
+ $this->dispatcher->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data));
}
}
}
diff --git a/app/Model/TaskStatus.php b/app/Model/TaskStatus.php
index a5199ed9..2b902815 100644
--- a/app/Model/TaskStatus.php
+++ b/app/Model/TaskStatus.php
@@ -62,6 +62,32 @@ class TaskStatus extends Base
}
/**
+ * Close multiple tasks
+ *
+ * @access public
+ * @param array $task_ids
+ */
+ public function closeMultipleTasks(array $task_ids)
+ {
+ foreach ($task_ids as $task_id) {
+ $this->close($task_id);
+ }
+ }
+
+ /**
+ * Close all tasks within a column/swimlane
+ *
+ * @access public
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ */
+ public function closeTasksBySwimlaneAndColumn($swimlane_id, $column_id)
+ {
+ $task_ids = $this->db->table(Task::TABLE)->eq('swimlane_id', $swimlane_id)->eq('column_id', $column_id)->findAllByColumn('id');
+ $this->closeMultipleTasks($task_ids);
+ }
+
+ /**
* Common method to change the status of task
*
* @access private
@@ -87,10 +113,8 @@ class TaskStatus extends Base
));
if ($result) {
- $this->container['dispatcher']->dispatch(
- $event,
- new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id))
- );
+ $this->logger->debug('Event fired: '.$event);
+ $this->dispatcher->dispatch($event, new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id)));
}
return $result;
diff --git a/app/Model/TaskValidator.php b/app/Model/TaskValidator.php
deleted file mode 100644
index 683cb0b1..00000000
--- a/app/Model/TaskValidator.php
+++ /dev/null
@@ -1,246 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
-/**
- * Task validator model
- *
- * @package model
- * @author Frederic Guillot
- */
-class TaskValidator extends Base
-{
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\Integer('id', t('This value must be an integer')),
- new Validators\Integer('project_id', t('This value must be an integer')),
- new Validators\Integer('column_id', t('This value must be an integer')),
- new Validators\Integer('owner_id', t('This value must be an integer')),
- new Validators\Integer('creator_id', t('This value must be an integer')),
- new Validators\Integer('score', t('This value must be an integer')),
- new Validators\Integer('category_id', t('This value must be an integer')),
- new Validators\Integer('swimlane_id', t('This value must be an integer')),
- new Validators\Integer('recurrence_child', t('This value must be an integer')),
- new Validators\Integer('recurrence_parent', t('This value must be an integer')),
- new Validators\Integer('recurrence_factor', t('This value must be an integer')),
- new Validators\Integer('recurrence_timeframe', t('This value must be an integer')),
- new Validators\Integer('recurrence_basedate', t('This value must be an integer')),
- new Validators\Integer('recurrence_trigger', t('This value must be an integer')),
- new Validators\Integer('recurrence_status', t('This value must be an integer')),
- new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200),
- new Validators\MaxLength('reference', t('The maximum length is %d characters', 50), 50),
- new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats()),
- new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getAllFormats()),
- new Validators\Numeric('time_spent', t('This value must be numeric')),
- new Validators\Numeric('time_estimated', t('This value must be numeric')),
- );
- }
-
- /**
- * Validate task 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)
- {
- $rules = array(
- new Validators\Required('project_id', t('The project is required')),
- new Validators\Required('title', t('The title is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate description creation
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateDescriptionCreation(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate edit recurrence
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateEditRecurrence(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
-
- /**
- * Validate task modification (form)
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- new Validators\Required('title', t('The title is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate task modification (Api)
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateApiModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate assignee change
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateAssigneeModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- new Validators\Required('project_id', t('The project is required')),
- new Validators\Required('owner_id', t('This value is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate category change
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateCategoryModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- new Validators\Required('project_id', t('The project is required')),
- new Validators\Required('category_id', t('This value is required')),
-
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate project modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateProjectModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- new Validators\Required('project_id', t('The project is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate time tracking modification (form)
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateTimeModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The id is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-}
diff --git a/app/Model/Transition.php b/app/Model/Transition.php
index b1f8f678..aa76d58f 100644
--- a/app/Model/Transition.php
+++ b/app/Model/Transition.php
@@ -76,8 +76,8 @@ class Transition extends Base
->eq('task_id', $task_id)
->desc('date')
->join(User::TABLE, 'id', 'user_id')
- ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src')
- ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst')
+ ->join(Column::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src')
+ ->join(Column::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst')
->findAll();
}
@@ -118,8 +118,8 @@ class Transition extends Base
->desc('date')
->join(Task::TABLE, 'id', 'task_id')
->join(User::TABLE, 'id', 'user_id')
- ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src')
- ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst')
+ ->join(Column::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src')
+ ->join(Column::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst')
->findAll();
}
diff --git a/app/Model/User.php b/app/Model/User.php
index 6e7e94e0..2d87d35b 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -3,10 +3,8 @@
namespace Kanboard\Model;
use PicoDb\Database;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-use Kanboard\Core\Session;
-use Kanboard\Core\Security;
+use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
/**
* User model
@@ -43,6 +41,18 @@ class User extends Base
}
/**
+ * Return true if the user is active
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return boolean
+ */
+ public function isActive($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $user_id)->eq('is_active', 1)->exists();
+ }
+
+ /**
* Get query to fetch all users
*
* @access public
@@ -50,21 +60,7 @@ class User extends Base
*/
public function getQuery()
{
- return $this->db
- ->table(self::TABLE)
- ->columns(
- 'id',
- 'username',
- 'name',
- 'email',
- 'is_admin',
- 'is_project_admin',
- 'is_ldap_user',
- 'notifications_enabled',
- 'google_id',
- 'github_id',
- 'twofactor_activated'
- );
+ return $this->db->table(self::TABLE);
}
/**
@@ -91,7 +87,7 @@ class User extends Base
$this->db
->table(User::TABLE)
->eq('id', $user_id)
- ->eq('is_admin', 1)
+ ->eq('role', Role::APP_ADMIN)
->exists();
}
@@ -111,48 +107,17 @@ class User extends Base
* Get a specific user by the Google id
*
* @access public
- * @param string $google_id Google unique id
+ * @param string $column
+ * @param string $id
* @return array|boolean
*/
- public function getByGoogleId($google_id)
+ public function getByExternalId($column, $id)
{
- if (empty($google_id)) {
+ if (empty($id)) {
return false;
}
- return $this->db->table(self::TABLE)->eq('google_id', $google_id)->findOne();
- }
-
- /**
- * Get a specific user by the Github id
- *
- * @access public
- * @param string $github_id Github user id
- * @return array|boolean
- */
- public function getByGithubId($github_id)
- {
- if (empty($github_id)) {
- return false;
- }
-
- return $this->db->table(self::TABLE)->eq('github_id', $github_id)->findOne();
- }
-
- /**
- * Get a specific user by the Gitlab id
- *
- * @access public
- * @param string $gitlab_id Gitlab user id
- * @return array|boolean
- */
- public function getByGitlabId($gitlab_id)
- {
- if (empty($gitlab_id)) {
- return false;
- }
-
- return $this->db->table(self::TABLE)->eq('gitlab_id', $gitlab_id)->findOne();
+ return $this->db->table(self::TABLE)->eq($column, $id)->findOne();
}
/**
@@ -172,7 +137,7 @@ class User extends Base
*
* @access public
* @param string $username Username
- * @return array
+ * @return integer
*/
public function getIdByUsername($username)
{
@@ -240,9 +205,9 @@ class User extends Base
* @param boolean $prepend Prepend "All users"
* @return array
*/
- public function getList($prepend = false)
+ public function getActiveUsersList($prepend = false)
{
- $users = $this->db->table(self::TABLE)->columns('id', 'username', 'name')->findAll();
+ $users = $this->db->table(self::TABLE)->eq('is_active', 1)->columns('id', 'username', 'name')->findAll();
$listing = $this->prepareList($users);
if ($prepend) {
@@ -289,7 +254,7 @@ class User extends Base
}
$this->removeFields($values, array('confirmation', 'current_password'));
- $this->resetFields($values, array('is_admin', 'is_ldap_user', 'is_project_admin', 'disable_login_form'));
+ $this->resetFields($values, array('is_ldap_user', 'disable_login_form'));
$this->convertNullFields($values, array('gitlab_id'));
$this->convertIntegerFields($values, array('gitlab_id'));
}
@@ -312,7 +277,7 @@ class User extends Base
*
* @access public
* @param array $values Form values
- * @return array
+ * @return boolean
*/
public function update(array $values)
{
@@ -320,14 +285,38 @@ class User extends Base
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
// If the user is connected refresh his session
- if (Session::isOpen() && $this->userSession->getId() == $values['id']) {
- $this->userSession->refresh();
+ if ($this->userSession->getId() == $values['id']) {
+ $this->userSession->initialize($this->getById($this->userSession->getId()));
}
return $result;
}
/**
+ * Disable a specific user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function disable($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $user_id)->update(array('is_active' => 0));
+ }
+
+ /**
+ * Enable a specific user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function enable($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $user_id)->update(array('is_active' => 1));
+ }
+
+ /**
* Remove a specific user
*
* @access public
@@ -355,10 +344,10 @@ class User extends Base
// All private projects are removed
$project_ids = $db->table(Project::TABLE)
- ->eq('is_private', 1)
- ->eq(ProjectPermission::TABLE.'.user_id', $user_id)
- ->join(ProjectPermission::TABLE, 'project_id', 'id')
- ->findAllByColumn(Project::TABLE.'.id');
+ ->eq('is_private', 1)
+ ->eq(ProjectUserRole::TABLE.'.user_id', $user_id)
+ ->join(ProjectUserRole::TABLE, 'project_id', 'id')
+ ->findAllByColumn(Project::TABLE.'.id');
if (! empty($project_ids)) {
$db->table(Project::TABLE)->in('id', $project_ids)->remove();
@@ -383,7 +372,7 @@ class User extends Base
return $this->db
->table(self::TABLE)
->eq('id', $user_id)
- ->save(array('token' => Security::generateToken()));
+ ->save(array('token' => Token::getToken()));
}
/**
@@ -400,200 +389,4 @@ class User extends Base
->eq('id', $user_id)
->save(array('token' => ''));
}
-
- /**
- * Get the number of failed login for the user
- *
- * @access public
- * @param string $username
- * @return integer
- */
- public function getFailedLogin($username)
- {
- return (int) $this->db->table(self::TABLE)->eq('username', $username)->findOneColumn('nb_failed_login');
- }
-
- /**
- * Reset to 0 the counter of failed login
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function resetFailedLogin($username)
- {
- return $this->db->table(self::TABLE)->eq('username', $username)->update(array('nb_failed_login' => 0, 'lock_expiration_date' => 0));
- }
-
- /**
- * Increment failed login counter
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function incrementFailedLogin($username)
- {
- return $this->db->execute('UPDATE '.self::TABLE.' SET nb_failed_login=nb_failed_login+1 WHERE username=?', array($username)) !== false;
- }
-
- /**
- * Check if the account is locked
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function isLocked($username)
- {
- return $this->db->table(self::TABLE)
- ->eq('username', $username)
- ->neq('lock_expiration_date', 0)
- ->gte('lock_expiration_date', time())
- ->exists();
- }
-
- /**
- * Lock the account for the specified duration
- *
- * @access public
- * @param string $username Username
- * @param integer $duration Duration in minutes
- * @return boolean
- */
- public function lock($username, $duration = 15)
- {
- return $this->db->table(self::TABLE)->eq('username', $username)->update(array('lock_expiration_date' => time() + $duration * 60));
- }
-
- /**
- * Common validation rules
- *
- * @access private
- * @return array
- */
- private function commonValidationRules()
- {
- return array(
- new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
- new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
- new Validators\Email('email', t('Email address invalid')),
- new Validators\Integer('is_admin', t('This value must be an integer')),
- new Validators\Integer('is_project_admin', t('This value must be an integer')),
- new Validators\Integer('is_ldap_user', t('This value must be an integer')),
- );
- }
-
- /**
- * Common password validation rules
- *
- * @access private
- * @return array
- */
- private function commonPasswordValidationRules()
- {
- return array(
- new Validators\Required('password', t('The password is required')),
- new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
- new Validators\Required('confirmation', t('The confirmation is required')),
- new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
- );
- }
-
- /**
- * Validate user 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)
- {
- $rules = array(
- new Validators\Required('username', t('The username is required')),
- );
-
- if (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1) {
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
- } else {
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules()));
- }
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate user modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The user id is required')),
- new Validators\Required('username', t('The username is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate user API modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateApiModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The user id is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate password modification
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validatePasswordModification(array $values)
- {
- $rules = array(
- new Validators\Required('id', t('The user id is required')),
- new Validators\Required('current_password', t('The current password is required')),
- );
-
- $v = new Validator($values, array_merge($rules, $this->commonPasswordValidationRules()));
-
- if ($v->execute()) {
-
- // Check password
- if ($this->authentication->authenticate($this->session['user']['username'], $values['current_password'])) {
- return array(true, array());
- } else {
- return array(false, array('current_password' => array(t('Wrong password'))));
- }
- }
-
- return array(false, $v->getErrors());
- }
}
diff --git a/app/Model/UserFilter.php b/app/Model/UserFilter.php
new file mode 100644
index 00000000..ff546e96
--- /dev/null
+++ b/app/Model/UserFilter.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * User Filter
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class UserFilter extends Base
+{
+ /**
+ * Search query
+ *
+ * @access private
+ * @var string
+ */
+ private $input;
+
+ /**
+ * Query
+ *
+ * @access protected
+ * @var \PicoDb\Table
+ */
+ protected $query;
+
+ /**
+ * Initialize filter
+ *
+ * @access public
+ * @param string $input
+ * @return UserFilter
+ */
+ public function create($input)
+ {
+ $this->query = $this->db->table(User::TABLE);
+ $this->input = $input;
+ return $this;
+ }
+
+ /**
+ * Filter users by name or username
+ *
+ * @access public
+ * @return UserFilter
+ */
+ public function filterByUsernameOrByName()
+ {
+ $this->query->beginOr()
+ ->ilike('username', '%'.$this->input.'%')
+ ->ilike('name', '%'.$this->input.'%')
+ ->closeOr();
+
+ return $this;
+ }
+
+ /**
+ * Get all results of the filter
+ *
+ * @access public
+ * @return array
+ */
+ public function findAll()
+ {
+ return $this->query->findAll();
+ }
+
+ /**
+ * Get the PicoDb query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+}
diff --git a/app/Model/UserImport.php b/app/Model/UserImport.php
index 3c9e7a57..0ec4e802 100644
--- a/app/Model/UserImport.php
+++ b/app/Model/UserImport.php
@@ -4,6 +4,7 @@ namespace Kanboard\Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
+use Kanboard\Core\Security\Role;
use Kanboard\Core\Csv;
/**
@@ -36,7 +37,7 @@ class UserImport extends Base
'email' => 'Email',
'name' => 'Full Name',
'is_admin' => 'Administrator',
- 'is_project_admin' => 'Project Administrator',
+ 'is_manager' => 'Manager',
'is_ldap_user' => 'Remote User',
);
}
@@ -75,10 +76,21 @@ class UserImport extends Base
{
$row['username'] = strtolower($row['username']);
- foreach (array('is_admin', 'is_project_admin', 'is_ldap_user') as $field) {
+ foreach (array('is_admin', 'is_manager', 'is_ldap_user') as $field) {
$row[$field] = Csv::getBooleanValue($row[$field]);
}
+ if ($row['is_admin'] == 1) {
+ $row['role'] = Role::APP_ADMIN;
+ } elseif ($row['is_manager'] == 1) {
+ $row['role'] = Role::APP_MANAGER;
+ } else {
+ $row['role'] = Role::APP_USER;
+ }
+
+ unset($row['is_admin']);
+ unset($row['is_manager']);
+
$this->removeEmptyFields($row, array('password', 'email', 'name'));
return $row;
@@ -98,8 +110,6 @@ class UserImport extends Base
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), User::TABLE, 'id'),
new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
new Validators\Email('email', t('Email address invalid')),
- new Validators\Integer('is_admin', t('This value must be an integer')),
- new Validators\Integer('is_project_admin', t('This value must be an integer')),
new Validators\Integer('is_ldap_user', t('This value must be an integer')),
));
diff --git a/app/Model/UserLocking.php b/app/Model/UserLocking.php
new file mode 100644
index 00000000..67e4c244
--- /dev/null
+++ b/app/Model/UserLocking.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * User Locking Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class UserLocking extends Base
+{
+ /**
+ * Get the number of failed login for the user
+ *
+ * @access public
+ * @param string $username
+ * @return integer
+ */
+ public function getFailedLogin($username)
+ {
+ return (int) $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->findOneColumn('nb_failed_login');
+ }
+
+ /**
+ * Reset to 0 the counter of failed login
+ *
+ * @access public
+ * @param string $username
+ * @return boolean
+ */
+ public function resetFailedLogin($username)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->update(array(
+ 'nb_failed_login' => 0,
+ 'lock_expiration_date' => 0,
+ ));
+ }
+
+ /**
+ * Increment failed login counter
+ *
+ * @access public
+ * @param string $username
+ * @return boolean
+ */
+ public function incrementFailedLogin($username)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->increment('nb_failed_login', 1);
+ }
+
+ /**
+ * Check if the account is locked
+ *
+ * @access public
+ * @param string $username
+ * @return boolean
+ */
+ public function isLocked($username)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->neq('lock_expiration_date', 0)
+ ->gte('lock_expiration_date', time())
+ ->exists();
+ }
+
+ /**
+ * Lock the account for the specified duration
+ *
+ * @access public
+ * @param string $username Username
+ * @param integer $duration Duration in minutes
+ * @return boolean
+ */
+ public function lock($username, $duration = 15)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->update(array(
+ 'lock_expiration_date' => time() + $duration * 60
+ ));
+ }
+
+ /**
+ * Return true if the captcha must be shown
+ *
+ * @access public
+ * @param string $username
+ * @param integer $tries
+ * @return boolean
+ */
+ public function hasCaptcha($username, $tries = BRUTEFORCE_CAPTCHA)
+ {
+ return $this->getFailedLogin($username) >= $tries;
+ }
+}
diff --git a/app/Model/UserMention.php b/app/Model/UserMention.php
new file mode 100644
index 00000000..97a4e419
--- /dev/null
+++ b/app/Model/UserMention.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Event\GenericEvent;
+
+/**
+ * User Mention
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class UserMention extends Base
+{
+ /**
+ * Get list of mentioned users
+ *
+ * @access public
+ * @param string $content
+ * @return array
+ */
+ public function getMentionedUsers($content)
+ {
+ $users = array();
+
+ if (preg_match_all('/@([^\s]+)/', $content, $matches)) {
+ $users = $this->db->table(User::TABLE)
+ ->columns('id', 'username', 'name', 'email', 'language')
+ ->eq('notifications_enabled', 1)
+ ->neq('id', $this->userSession->getId())
+ ->in('username', array_unique($matches[1]))
+ ->findAll();
+ }
+
+ return $users;
+ }
+
+ /**
+ * Fire events for user mentions
+ *
+ * @access public
+ * @param string $content
+ * @param string $eventName
+ * @param GenericEvent $event
+ */
+ public function fireEvents($content, $eventName, GenericEvent $event)
+ {
+ if (empty($event['project_id'])) {
+ $event['project_id'] = $this->taskFinder->getProjectId($event['task_id']);
+ }
+
+ $users = $this->getMentionedUsers($content);
+
+ foreach ($users as $user) {
+ if ($this->projectPermission->isMember($event['project_id'], $user['id'])) {
+ $event['mention'] = $user;
+ $this->dispatcher->dispatch($eventName, $event);
+ }
+ }
+ }
+}
diff --git a/app/Model/UserNotification.php b/app/Model/UserNotification.php
index 3d98ebe9..e8a967ac 100644
--- a/app/Model/UserNotification.php
+++ b/app/Model/UserNotification.php
@@ -21,18 +21,12 @@ class UserNotification extends Base
*/
public function sendNotifications($event_name, array $event_data)
{
- $logged_user_id = $this->userSession->isLogged() ? $this->userSession->getId() : 0;
- $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $logged_user_id);
-
- if (! empty($users)) {
- foreach ($users as $user) {
- if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) {
- $this->sendUserNotification($user, $event_name, $event_data);
- }
- }
+ $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $this->userSession->getId());
- // Restore locales
- $this->config->setupTranslations();
+ foreach ($users as $user) {
+ if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) {
+ $this->sendUserNotification($user, $event_name, $event_data);
+ }
}
}
@@ -58,6 +52,9 @@ class UserNotification extends Base
foreach ($this->userNotificationType->getSelectedTypes($user['id']) as $type) {
$this->userNotificationType->getType($type)->notifyUser($user, $event_name, $event_data);
}
+
+ // Restore locales
+ $this->config->setupTranslations();
}
/**
@@ -74,7 +71,17 @@ class UserNotification extends Base
return $this->getEverybodyWithNotificationEnabled($exclude_user_id);
}
- return $this->getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id);
+ $users = array();
+ $members = $this->getProjectUserMembersWithNotificationEnabled($project_id, $exclude_user_id);
+ $groups = $this->getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id);
+
+ foreach (array_merge($members, $groups) as $user) {
+ if (! isset($users[$user['id']])) {
+ $users[$user['id']] = $user;
+ }
+ }
+
+ return array_values($users);
}
/**
@@ -145,17 +152,17 @@ class UserNotification extends Base
}
/**
- * Get a list of project members with notification enabled
+ * Get a list of group members with notification enabled
*
* @access private
* @param integer $project_id Project id
* @param integer $exclude_user_id User id to exclude
* @return array
*/
- private function getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id)
+ private function getProjectUserMembersWithNotificationEnabled($project_id, $exclude_user_id)
{
return $this->db
- ->table(ProjectPermission::TABLE)
+ ->table(ProjectUserRole::TABLE)
->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter')
->join(User::TABLE, 'id', 'user_id')
->eq('project_id', $project_id)
@@ -164,6 +171,19 @@ class UserNotification extends Base
->findAll();
}
+ private function getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id)
+ {
+ return $this->db
+ ->table(ProjectGroupRole::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter')
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
+ ->eq(ProjectGroupRole::TABLE.'.project_id', $project_id)
+ ->eq(User::TABLE.'.notifications_enabled', '1')
+ ->neq(User::TABLE.'.id', $exclude_user_id)
+ ->findAll();
+ }
+
/**
* Get a list of project members with notification enabled
*
diff --git a/app/Model/UserSession.php b/app/Model/UserSession.php
deleted file mode 100644
index 1778114e..00000000
--- a/app/Model/UserSession.php
+++ /dev/null
@@ -1,177 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * User Session
- *
- * @package model
- * @author Frederic Guillot
- */
-class UserSession extends Base
-{
- /**
- * Update user session information
- *
- * @access public
- * @param array $user User data
- */
- public function refresh(array $user = array())
- {
- if (empty($user)) {
- $user = $this->user->getById($this->userSession->getId());
- }
-
- if (isset($user['password'])) {
- unset($user['password']);
- }
-
- if (isset($user['twofactor_secret'])) {
- unset($user['twofactor_secret']);
- }
-
- $user['id'] = (int) $user['id'];
- $user['is_admin'] = (bool) $user['is_admin'];
- $user['is_project_admin'] = (bool) $user['is_project_admin'];
- $user['is_ldap_user'] = (bool) $user['is_ldap_user'];
- $user['twofactor_activated'] = (bool) $user['twofactor_activated'];
-
- $this->session['user'] = $user;
- }
-
- /**
- * Return true if the user has validated the 2FA key
- *
- * @access public
- * @return bool
- */
- public function check2FA()
- {
- return isset($this->session['2fa_validated']) && $this->session['2fa_validated'] === true;
- }
-
- /**
- * Return true if the user has 2FA enabled
- *
- * @access public
- * @return bool
- */
- public function has2FA()
- {
- return isset($this->session['user']['twofactor_activated']) && $this->session['user']['twofactor_activated'] === true;
- }
-
- /**
- * Return true if the logged user is admin
- *
- * @access public
- * @return bool
- */
- public function isAdmin()
- {
- return isset($this->session['user']['is_admin']) && $this->session['user']['is_admin'] === true;
- }
-
- /**
- * Return true if the logged user is project admin
- *
- * @access public
- * @return bool
- */
- public function isProjectAdmin()
- {
- return isset($this->session['user']['is_project_admin']) && $this->session['user']['is_project_admin'] === true;
- }
-
- /**
- * Get the connected user id
- *
- * @access public
- * @return integer
- */
- public function getId()
- {
- return isset($this->session['user']['id']) ? (int) $this->session['user']['id'] : 0;
- }
-
- /**
- * Check is the user is connected
- *
- * @access public
- * @return bool
- */
- public function isLogged()
- {
- return ! empty($this->session['user']);
- }
-
- /**
- * Get project filters from the session
- *
- * @access public
- * @param integer $project_id
- * @return string
- */
- public function getFilters($project_id)
- {
- return ! empty($_SESSION['filters'][$project_id]) ? $_SESSION['filters'][$project_id] : 'status:open';
- }
-
- /**
- * Save project filters in the session
- *
- * @access public
- * @param integer $project_id
- * @param string $filters
- */
- public function setFilters($project_id, $filters)
- {
- $_SESSION['filters'][$project_id] = $filters;
- }
-
- /**
- * Is board collapsed or expanded
- *
- * @access public
- * @param integer $project_id
- * @return boolean
- */
- public function isBoardCollapsed($project_id)
- {
- return ! empty($_SESSION['board_collapsed'][$project_id]) ? $_SESSION['board_collapsed'][$project_id] : false;
- }
-
- /**
- * Set board display mode
- *
- * @access public
- * @param integer $project_id
- * @param boolean $collapsed
- */
- public function setBoardDisplayMode($project_id, $collapsed)
- {
- $_SESSION['board_collapsed'][$project_id] = $collapsed;
- }
-
- /**
- * Set comments sorting
- *
- * @access public
- * @param string $order
- */
- public function setCommentSorting($order)
- {
- $this->session['comment_sorting'] = $order;
- }
-
- /**
- * Get comments sorting direction
- *
- * @access public
- * @return string
- */
- public function getCommentSorting()
- {
- return $this->session['comment_sorting'] ?: 'ASC';
- }
-}