diff options
author | Nala Ginrut <nalaginrut@gmail.com> | 2014-06-19 15:18:13 +0800 |
---|---|---|
committer | Nala Ginrut <nalaginrut@gmail.com> | 2014-06-19 15:18:13 +0800 |
commit | bfd1db41367f7931016931a94cf1b67396481c79 (patch) | |
tree | 2d696f2d8eca9ed2e4561c61c16584952d9f7b0b | |
parent | d0944e682d5a3491f72c5b566248b87fbaff032a (diff) | |
parent | efdc959c555872677e599d2ff12e1263d719f3f2 (diff) |
Merge remote-tracking branch 'upstream/master'
92 files changed, 1441 insertions, 240 deletions
diff --git a/README.markdown b/README.markdown index b4337988..e3c5b24c 100644 --- a/README.markdown +++ b/README.markdown @@ -21,7 +21,7 @@ Features - Multiple boards/projects - Boards customization, rename or add columns -- Tasks with different colors, Markdown support for the description +- Tasks with different colors, categories, sub-tasks, attachments, Markdown support for the description - Automatic actions - Users management with a basic privileges separation (administrator or regular user) - External authentication: Google Account and LDAP/ActiveDirectory diff --git a/app/Action/TaskAssignCategoryColor.php b/app/Action/TaskAssignCategoryColor.php new file mode 100644 index 00000000..19d7fa9c --- /dev/null +++ b/app/Action/TaskAssignCategoryColor.php @@ -0,0 +1,85 @@ +<?php + +namespace Action; + +use Model\Task; + +/** + * Set a category automatically according to the color + * + * @package action + * @author Frederic Guillot + */ +class TaskAssignCategoryColor extends Base +{ + /** + * Task model + * + * @accesss private + * @var \Model\Task + */ + private $task; + + /** + * Constructor + * + * @access public + * @param integer $project_id Project id + * @param \Model\Task $task Task model instance + */ + public function __construct($project_id, Task $task) + { + parent::__construct($project_id); + $this->task = $task; + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'color_id' => t('Color'), + 'category_id' => t('Category'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'color_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if ($data['color_id'] == $this->getParam('color_id')) { + + $this->task->update(array( + 'id' => $data['task_id'], + 'category_id' => $this->getParam('category_id'), + )); + + return true; + } + + return false; + } +} diff --git a/app/Controller/Action.php b/app/Controller/Action.php index 2aa85c14..11dc3b29 100644 --- a/app/Controller/Action.php +++ b/app/Controller/Action.php @@ -129,6 +129,7 @@ class Action extends Base */ public function remove() { + $this->checkCSRFParam(); $action = $this->action->getById($this->request->getIntegerParam('action_id')); if ($action && $this->action->remove($action['id'])) { diff --git a/app/Controller/Base.php b/app/Controller/Base.php index b21d9b8f..9b695a82 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -3,6 +3,7 @@ namespace Controller; use Core\Registry; +use Core\Security; use Core\Translator; use Model\LastLogin; @@ -23,6 +24,7 @@ use Model\LastLogin; * @property \Model\Ldap $ldap * @property \Model\Project $project * @property \Model\RememberMe $rememberMe + * @property \Model\SubTask $subTask * @property \Model\Task $task * @property \Model\User $user */ @@ -160,6 +162,28 @@ abstract class Base } /** + * Application forbidden page + * + * @access public + */ + public function forbidden() + { + $this->response->html($this->template->layout('app_forbidden', array('title' => t('Access Forbidden')))); + } + + /** + * Check if the CSRF token from the URL is correct + * + * @access protected + */ + protected function checkCSRFParam() + { + if (! Security::validateCSRFToken($this->request->getStringParam('csrf_token'))) { + $this->forbidden(); + } + } + + /** * Check if the current user have access to the given project * * @access protected @@ -170,7 +194,7 @@ abstract class Base if ($this->acl->isRegularUser()) { if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->response->redirect('?controller=project&action=forbidden'); + $this->forbidden(); } } } diff --git a/app/Controller/Board.php b/app/Controller/Board.php index 53fdeab9..67072895 100644 --- a/app/Controller/Board.php +++ b/app/Controller/Board.php @@ -4,6 +4,7 @@ namespace Controller; use Model\Project as ProjectModel; use Model\User as UserModel; +use Core\Security; /** * Board controller @@ -20,6 +21,7 @@ class Board extends Base */ public function moveUp() { + $this->checkCSRFParam(); $project_id = $this->request->getIntegerParam('project_id'); $column_id = $this->request->getIntegerParam('column_id'); @@ -35,6 +37,7 @@ class Board extends Base */ public function moveDown() { + $this->checkCSRFParam(); $project_id = $this->request->getIntegerParam('project_id'); $column_id = $this->request->getIntegerParam('column_id'); @@ -344,6 +347,7 @@ class Board extends Base */ public function remove() { + $this->checkCSRFParam(); $column = $this->board->getColumn($this->request->getIntegerParam('column_id')); if ($column && $this->board->removeColumn($column['id'])) { @@ -362,25 +366,31 @@ class Board extends Base */ public function save() { - $project_id = $this->request->getIntegerParam('project_id'); - $values = $this->request->getValues(); + if ($this->request->isAjax()) { - if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->response->text('Not Authorized', 401); - } + $project_id = $this->request->getIntegerParam('project_id'); + $values = $this->request->getValues(); - if (isset($values['positions'])) { - $this->board->saveTasksPosition($values['positions']); - } + if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { + $this->response->text('Not Authorized', 401); + } + + if (isset($values['positions'])) { + $this->board->saveTasksPosition($values['positions']); + } - $this->response->html( - $this->template->load('board_show', array( - 'current_project_id' => $project_id, - 'board' => $this->board->get($project_id), - 'categories' => $this->category->getList($project_id, false), - )), - 201 - ); + $this->response->html( + $this->template->load('board_show', array( + 'current_project_id' => $project_id, + 'board' => $this->board->get($project_id), + 'categories' => $this->category->getList($project_id, false), + )), + 201 + ); + } + else { + $this->response->status(401); + } } /** @@ -390,24 +400,30 @@ class Board extends Base */ public function check() { - $project_id = $this->request->getIntegerParam('project_id'); - $timestamp = $this->request->getIntegerParam('timestamp'); + if ($this->request->isAjax()) { - if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->response->text('Not Authorized', 401); - } + $project_id = $this->request->getIntegerParam('project_id'); + $timestamp = $this->request->getIntegerParam('timestamp'); - if ($this->project->isModifiedSince($project_id, $timestamp)) { - $this->response->html( - $this->template->load('board_show', array( - 'current_project_id' => $project_id, - 'board' => $this->board->get($project_id), - 'categories' => $this->category->getList($project_id, false), - )) - ); + if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { + $this->response->text('Not Authorized', 401); + } + + if ($this->project->isModifiedSince($project_id, $timestamp)) { + $this->response->html( + $this->template->load('board_show', array( + 'current_project_id' => $project_id, + 'board' => $this->board->get($project_id), + 'categories' => $this->category->getList($project_id, false), + )) + ); + } + else { + $this->response->status(304); + } } else { - $this->response->status(304); + $this->response->status(401); } } } diff --git a/app/Controller/Category.php b/app/Controller/Category.php index f96c1d4a..9b73f207 100644 --- a/app/Controller/Category.php +++ b/app/Controller/Category.php @@ -175,6 +175,7 @@ class Category extends Base */ public function remove() { + $this->checkCSRFParam(); $project = $this->getProject(); $category = $this->getCategory($project['id']); diff --git a/app/Controller/Comment.php b/app/Controller/Comment.php index 47eaf6b6..a0a11fc8 100644 --- a/app/Controller/Comment.php +++ b/app/Controller/Comment.php @@ -178,6 +178,7 @@ class Comment extends Base */ public function remove() { + $this->checkCSRFParam(); $task = $this->getTask(); $comment = $this->getComment(); diff --git a/app/Controller/Config.php b/app/Controller/Config.php index b4a5b8d3..daa57790 100644 --- a/app/Controller/Config.php +++ b/app/Controller/Config.php @@ -76,6 +76,7 @@ class Config extends Base */ public function downloadDb() { + $this->checkCSRFParam(); $this->response->forceDownload('db.sqlite.gz'); $this->response->binary($this->config->downloadDatabase()); } @@ -87,6 +88,7 @@ class Config extends Base */ public function optimizeDb() { + $this->checkCSRFParam(); $this->config->optimizeDatabase(); $this->session->flash(t('Database optimization done.')); $this->response->redirect('?controller=config'); @@ -99,6 +101,7 @@ class Config extends Base */ public function tokens() { + $this->checkCSRFParam(); $this->config->regenerateTokens(); $this->session->flash(t('All tokens have been regenerated.')); $this->response->redirect('?controller=config'); @@ -111,6 +114,7 @@ class Config extends Base */ public function removeRememberMeToken() { + $this->checkCSRFParam(); $this->rememberMe->remove($this->request->getIntegerParam('id')); $this->response->redirect('?controller=config&action=index#remember-me'); } diff --git a/app/Controller/File.php b/app/Controller/File.php index 1604ab13..3c8c32d1 100644 --- a/app/Controller/File.php +++ b/app/Controller/File.php @@ -24,6 +24,7 @@ class File extends Base $this->response->html($this->taskLayout('file_new', array( 'task' => $task, 'menu' => 'tasks', + 'max_size' => ini_get('upload_max_filesize'), 'title' => t('Attach a document') ))); } @@ -36,8 +37,14 @@ class File extends Base public function save() { $task = $this->getTask(); - $this->file->upload($task['project_id'], $task['id'], 'files'); - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#attachments'); + + if ($this->file->upload($task['project_id'], $task['id'], 'files') === true) { + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#attachments'); + } + else { + $this->session->flashError(t('Unable to upload the file.')); + $this->response->redirect('?controller=file&action=create&task_id='.$task['id']); + } } /** @@ -104,6 +111,7 @@ class File extends Base */ public function remove() { + $this->checkCSRFParam(); $task = $this->getTask(); $file = $this->file->getById($this->request->getIntegerParam('file_id')); diff --git a/app/Controller/Project.php b/app/Controller/Project.php index e539f364..0de67691 100644 --- a/app/Controller/Project.php +++ b/app/Controller/Project.php @@ -13,19 +13,6 @@ use Model\Task as TaskModel; class Project extends Base { /** - * Display access forbidden page - * - * @access public - */ - public function forbidden() - { - $this->response->html($this->template->layout('project_forbidden', array( - 'menu' => 'projects', - 'title' => t('Access Forbidden') - ))); - } - - /** * Task search for a given project * * @access public @@ -254,6 +241,7 @@ class Project extends Base */ public function remove() { + $this->checkCSRFParam(); $project_id = $this->request->getIntegerParam('project_id'); if ($project_id && $this->project->remove($project_id)) { @@ -272,6 +260,7 @@ class Project extends Base */ public function enable() { + $this->checkCSRFParam(); $project_id = $this->request->getIntegerParam('project_id'); if ($project_id && $this->project->enable($project_id)) { @@ -290,6 +279,7 @@ class Project extends Base */ public function disable() { + $this->checkCSRFParam(); $project_id = $this->request->getIntegerParam('project_id'); if ($project_id && $this->project->disable($project_id)) { @@ -353,6 +343,8 @@ class Project extends Base */ public function revoke() { + $this->checkCSRFParam(); + $values = array( 'project_id' => $this->request->getIntegerParam('project_id'), 'user_id' => $this->request->getIntegerParam('user_id'), diff --git a/app/Controller/Subtask.php b/app/Controller/Subtask.php new file mode 100644 index 00000000..1c217fa2 --- /dev/null +++ b/app/Controller/Subtask.php @@ -0,0 +1,186 @@ +<?php + +namespace Controller; + +/** + * SubTask controller + * + * @package controller + * @author Frederic Guillot + */ +class Subtask extends Base +{ + /** + * Get the current subtask + * + * @access private + * @return array + */ + private function getSubtask() + { + $subtask = $this->subTask->getById($this->request->getIntegerParam('subtask_id')); + + if (! $subtask) { + $this->notfound(); + } + + return $subtask; + } + + /** + * Creation form + * + * @access public + */ + public function create() + { + $task = $this->getTask(); + + $this->response->html($this->taskLayout('subtask_create', array( + 'values' => array( + 'task_id' => $task['id'], + ), + 'errors' => array(), + 'users_list' => $this->project->getUsersList($task['project_id']), + 'task' => $task, + 'menu' => 'tasks', + 'title' => t('Add a sub-task') + ))); + } + + /** + * Validation and creation + * + * @access public + */ + public function save() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->subTask->validate($values); + + if ($valid) { + + if ($this->subTask->create($values)) { + $this->session->flash(t('Sub-task added successfully.')); + } + else { + $this->session->flashError(t('Unable to create your sub-task.')); + } + + if (isset($values['another_subtask']) && $values['another_subtask'] == 1) { + $this->response->redirect('?controller=subtask&action=create&task_id='.$task['id']); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + } + + $this->response->html($this->taskLayout('subtask_create', array( + 'values' => $values, + 'errors' => $errors, + 'users_list' => $this->project->getUsersList($task['project_id']), + 'task' => $task, + 'menu' => 'tasks', + 'title' => t('Add a sub-task') + ))); + } + + /** + * Edit form + * + * @access public + */ + public function edit() + { + $task = $this->getTask(); + $subtask = $this->getSubTask(); + + $this->response->html($this->taskLayout('subtask_edit', array( + 'values' => $subtask, + 'errors' => array(), + 'users_list' => $this->project->getUsersList($task['project_id']), + 'status_list' => $this->subTask->getStatusList(), + 'subtask' => $subtask, + 'task' => $task, + 'menu' => 'tasks', + 'title' => t('Edit a sub-task') + ))); + } + + /** + * Update and validate a subtask + * + * @access public + */ + public function update() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->subTask->validate($values); + + if ($valid) { + + if ($this->subTask->update($values)) { + $this->session->flash(t('Sub-task updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your sub-task.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + } + + $this->response->html($this->taskLayout('subtask_edit', array( + 'values' => $values, + 'errors' => $errors, + 'users_list' => $this->project->getUsersList($task['project_id']), + 'status_list' => $this->subTask->getStatusList(), + 'subtask' => $subtask, + 'task' => $task, + 'menu' => 'tasks', + 'title' => t('Edit a sub-task') + ))); + } + + /** + * Confirmation dialog before removing a subtask + * + * @access public + */ + public function confirm() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); + + $this->response->html($this->taskLayout('subtask_remove', array( + 'subtask' => $subtask, + 'task' => $task, + 'menu' => 'tasks', + 'title' => t('Remove a sub-task') + ))); + } + + /** + * Remove a subtask + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $task = $this->getTask(); + $subtask = $this->getSubtask(); + + if ($this->subTask->remove($subtask['id'])) { + $this->session->flash(t('Sub-task removed successfully.')); + } + else { + $this->session->flashError(t('Unable to remove this sub-task.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + } +} diff --git a/app/Controller/Task.php b/app/Controller/Task.php index 8230eef3..d44ba268 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -62,6 +62,7 @@ class Task extends Base $this->response->html($this->taskLayout('task_show', array( 'files' => $this->file->getAll($task['id']), 'comments' => $this->comment->getAll($task['id']), + 'subtasks' => $this->subTask->getAll($task['id']), 'task' => $task, 'columns_list' => $this->board->getColumnsList($task['project_id']), 'colors_list' => $this->task->getColors(), @@ -217,6 +218,7 @@ class Task extends Base */ public function close() { + $this->checkCSRFParam(); $task = $this->getTask(); if ($this->task->close($task['id'])) { @@ -251,6 +253,7 @@ class Task extends Base */ public function open() { + $this->checkCSRFParam(); $task = $this->getTask(); if ($this->task->open($task['id'])) { @@ -285,6 +288,7 @@ class Task extends Base */ public function remove() { + $this->checkCSRFParam(); $task = $this->getTask(); if ($this->task->remove($task['id'])) { diff --git a/app/Controller/User.php b/app/Controller/User.php index e3fd8253..fca33b28 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -11,25 +11,13 @@ namespace Controller; class User extends Base { /** - * Display access forbidden page - * - * @access public - */ - public function forbidden() - { - $this->response->html($this->template->layout('user_forbidden', array( - 'menu' => 'users', - 'title' => t('Access Forbidden') - ))); - } - - /** * Logout and destroy session * * @access public */ public function logout() { + $this->checkCSRFParam(); $this->rememberMe->destroy($this->acl->getUserId()); $this->session->close(); $this->response->redirect('?controller=user&action=login'); @@ -42,7 +30,9 @@ class User extends Base */ public function login() { - if (isset($_SESSION['user'])) $this->response->redirect('?controller=app'); + if (isset($_SESSION['user'])) { + $this->response->redirect('?controller=app'); + } $this->response->html($this->template->layout('user_login', array( 'errors' => array(), @@ -236,6 +226,7 @@ class User extends Base */ public function remove() { + $this->checkCSRFParam(); $user_id = $this->request->getIntegerParam('user_id'); if ($user_id && $this->user->remove($user_id)) { @@ -298,6 +289,7 @@ class User extends Base */ public function unlinkGoogle() { + $this->checkCSRFParam(); if ($this->google->unlink($this->acl->getUserId())) { $this->session->flash(t('Your Google Account is not linked anymore to your profile.')); } diff --git a/app/Core/Event.php b/app/Core/Event.php index 2c029b49..0e6df5e8 100644 --- a/app/Core/Event.php +++ b/app/Core/Event.php @@ -67,13 +67,16 @@ class Event */ public function trigger($eventName, array $data) { - $this->lastEvent = $eventName; - $this->events[] = $eventName; + if (! $this->isEventTriggered($eventName)) { - if (isset($this->listeners[$eventName])) { - foreach ($this->listeners[$eventName] as $listener) { - if ($listener->execute($data)) { - $this->lastListener = get_class($listener); + $this->lastEvent = $eventName; + $this->events[] = $eventName; + + if (isset($this->listeners[$eventName])) { + foreach ($this->listeners[$eventName] as $listener) { + if ($listener->execute($data)) { + $this->lastListener = get_class($listener); + } } } } @@ -113,6 +116,29 @@ class Event } /** + * Check if an event have been triggered + * + * @access public + * @param string $eventName Event name + * @return bool + */ + public function isEventTriggered($eventName) + { + return in_array($eventName, $this->events); + } + + /** + * Flush the list of triggered events + * + * @access public + */ + public function clearTriggeredEvents() + { + $this->events = array(); + $this->lastEvent = ''; + } + + /** * Check if a listener bind to an event * * @access public diff --git a/app/Core/Request.php b/app/Core/Request.php index 7e9f24ac..6bc738be 100644 --- a/app/Core/Request.php +++ b/app/Core/Request.php @@ -2,6 +2,8 @@ namespace Core; +use Core\Security; + /** * Request class * @@ -58,7 +60,12 @@ class Request public function getValues() { if (! empty($_POST)) { - return $_POST; + + if (Security::validateCSRFFormToken($_POST)) { + return $_POST; + } + + return array(); } $result = json_decode($this->getBody(), true); @@ -116,6 +123,19 @@ class Request */ public function isAjax() { - return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'; + return $this->getHeader('X-Requested-With') === 'XMLHttpRequest'; + } + + /** + * Return a HTTP header value + * + * @access public + * @param string $name Header name + * @return string + */ + public function getHeader($name) + { + $name = 'HTTP_'.str_replace('-', '_', strtoupper($name)); + return isset($_SERVER[$name]) ? $_SERVER[$name] : ''; } } diff --git a/app/Core/Response.php b/app/Core/Response.php index 87d2fa4a..aee029af 100644 --- a/app/Core/Response.php +++ b/app/Core/Response.php @@ -11,6 +11,20 @@ namespace Core; class Response { /** + * Send no cache headers + * + * @access public + */ + public function nocache() + { + header('Pragma: no-cache'); + header('Expires: Sat, 26 Jul 1997 05:00:00 GMT'); + + // Use no-store due to a Chrome bug: https://code.google.com/p/chromium/issues/detail?id=28035 + header('Cache-Control: no-store, must-revalidate'); + } + + /** * Send a custom Content-Type header * * @access public @@ -66,7 +80,7 @@ class Response public function json(array $data, $status_code = 200) { $this->status($status_code); - + $this->nocache(); header('Content-Type: application/json'); echo json_encode($data); @@ -83,7 +97,7 @@ class Response public function text($data, $status_code = 200) { $this->status($status_code); - + $this->nocache(); header('Content-Type: text/plain; charset=utf-8'); echo $data; @@ -100,7 +114,7 @@ class Response public function html($data, $status_code = 200) { $this->status($status_code); - + $this->nocache(); header('Content-Type: text/html; charset=utf-8'); echo $data; @@ -117,7 +131,7 @@ class Response public function xml($data, $status_code = 200) { $this->status($status_code); - + $this->nocache(); header('Content-Type: text/xml; charset=utf-8'); echo $data; @@ -151,7 +165,7 @@ class Response public function binary($data, $status_code = 200) { $this->status($status_code); - + $this->nocache(); header('Content-Transfer-Encoding: binary'); header('Content-Type: application/octet-stream'); echo $data; diff --git a/app/Core/Security.php b/app/Core/Security.php new file mode 100644 index 00000000..0bd7c991 --- /dev/null +++ b/app/Core/Security.php @@ -0,0 +1,87 @@ +<?php + +namespace Core; + +/** + * Security class + * + * @package core + * @author Frederic Guillot + */ +class Security +{ + /** + * Generate a random token with different methods: openssl or /dev/urandom or fallback to uniqid() + * + * @static + * @access public + * @return string Random token + */ + public static function generateToken() + { + if (function_exists('openssl_random_pseudo_bytes')) { + return bin2hex(\openssl_random_pseudo_bytes(30)); + } + else if (ini_get('open_basedir') === '' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + return hash('sha256', file_get_contents('/dev/urandom', false, null, 0, 30)); + } + + return hash('sha256', uniqid(mt_rand(), true)); + } + + /** + * Generate and store a CSRF token in the current session + * + * @static + * @access public + * @return string Random token + */ + public static function getCSRFToken() + { + $nonce = self::generateToken(); + + if (empty($_SESSION['csrf_tokens'])) { + $_SESSION['csrf_tokens'] = array(); + } + + $_SESSION['csrf_tokens'][$nonce] = true; + + return $nonce; + } + + /** + * Check if the token exists for the current session (a token can be used only one time) + * + * @static + * @access public + * @param string $token CSRF token + * @return bool + */ + public static function validateCSRFToken($token) + { + if (isset($_SESSION['csrf_tokens'][$token])) { + unset($_SESSION['csrf_tokens'][$token]); + return true; + } + + return false; + } + + /** + * Check if the token used in a form is correct and then remove the value + * + * @static + * @access public + * @param array $values Form values + * @return bool + */ + public static function validateCSRFFormToken(array &$values) + { + if (! empty($values['csrf_token']) && self::validateCSRFToken($values['csrf_token'])) { + unset($values['csrf_token']); + return true; + } + + return false; + } +} diff --git a/app/Core/Session.php b/app/Core/Session.php index 6ce1bd40..af7a9123 100644 --- a/app/Core/Session.php +++ b/app/Core/Session.php @@ -15,7 +15,7 @@ class Session * * @var integer */ - const SESSION_LIFETIME = 86400; // 1 day + const SESSION_LIFETIME = 7200; // 2 hours /** * Open a session @@ -35,7 +35,7 @@ class Session self::SESSION_LIFETIME, $base_path ?: '/', null, - isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', + ! empty($_SERVER['HTTPS']), true ); @@ -66,6 +66,25 @@ class Session */ public function close() { + // Flush all sessions variables + $_SESSION = array(); + + // Destroy the session cookie + if (ini_get('session.use_cookies')) { + $params = session_get_cookie_params(); + + setcookie( + session_name(), + '', + time() - 42000, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); + } + + // Destroy session data session_destroy(); } diff --git a/app/Core/Translator.php b/app/Core/Translator.php index 015a76cb..d9386d3a 100644 --- a/app/Core/Translator.php +++ b/app/Core/Translator.php @@ -114,7 +114,15 @@ class Translator return ''; } - return strftime($this->get($format, $format), (int) $timestamp); + $format = $this->get($format, $format); + + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $format = str_replace('%e', '%d', $format); + $format = str_replace('%G', '%Y', $format); + $format = str_replace('%k', '%H', $format); + } + + return strftime($format, (int) $timestamp); } /** diff --git a/app/Locales/de_DE/translations.php b/app/Locales/de_DE/translations.php index 90958853..97858079 100644 --- a/app/Locales/de_DE/translations.php +++ b/app/Locales/de_DE/translations.php @@ -126,7 +126,7 @@ return array( 'The username must be unique' => 'Der Benutzername muss eindeutig sein', 'The username must be alphanumeric' => 'Der Benutzername muss alphanumerisch sein', 'The user id is required' => 'Die Benutzer ID wird benötigt', - 'Passwords doesn\'t matches' => 'Passwörter passen nicht zusammen', + 'Passwords don\'t match' => 'Passwörter passen nicht zusammen', 'The confirmation is required' => 'Die Bestätigung wird benötigt', 'The column is required' => 'Die Spalte wird benötigt', 'The project is required' => 'Das Projekt wird benötigt', @@ -312,7 +312,8 @@ return array( // 'Unable to remove this task.' => '', // 'Remove a task' => '', // 'Do you really want to remove this task: "%s"?' => '', - // 'Assign a color to a specific category' => '', + // 'Assign automatically a color based on a category' => '', + // 'Assign automatically a category based on a color' => '', // 'Task creation or modification' => '', // 'Category' => '', // 'Category:' => '', @@ -346,4 +347,31 @@ return array( // 'Add a comment' => '', // 'Edit a comment' => '', // 'Summary' => '', + // 'Time tracking' => '', + // 'Estimate:' => '', + // 'Spent:' => '', + // 'Do you really want to remove this sub-task?' => '', + // 'Remaining:' => '', + // 'hours' => '', + // 'spent' => '', + // 'estimated' => '', + // 'Sub-Tasks' => '', + // 'Add a sub-task' => '', + // 'Original Estimate' => '', + // 'Create another sub-task' => '', + // 'Time Spent' => '', + // 'Edit a sub-task' => '', + // 'Remove a sub-task' => '', + // 'The time must be a numeric value' => '', + // 'Todo' => '', + // 'In progress' => '', + // 'Done' => '', + // 'Sub-task removed successfully.' => '', + // 'Unable to remove this sub-task.' => '', + // 'Sub-task updated successfully.' => '', + // 'Unable to update your sub-task.' => '', + // 'Unable to create your sub-task.' => '', + // 'Sub-task added successfully.' => '', + // 'Maximum size: ' => '', + // 'Unable to upload the file.' => '', ); diff --git a/app/Locales/es_ES/translations.php b/app/Locales/es_ES/translations.php index d0d9efa8..61080f85 100644 --- a/app/Locales/es_ES/translations.php +++ b/app/Locales/es_ES/translations.php @@ -125,7 +125,7 @@ return array( 'The username must be unique' => 'El nombre de usuario debe ser único', 'The username must be alphanumeric' => 'El nombre de usuario debe ser alfanumérico', 'The user id is required' => 'El identificador del usuario es obligatorio', - 'Passwords doesn\'t matches' => 'Las contraseñas no corresponden', + 'Passwords don\'t match' => 'Las contraseñas no corresponden', 'The confirmation is required' => 'La confirmación es obligatoria', 'The column is required' => 'La columna es obligatoria', 'The project is required' => 'El proyecto es obligatorio', @@ -310,7 +310,8 @@ return array( // 'Unable to remove this task.' => '', // 'Remove a task' => '', // 'Do you really want to remove this task: "%s"?' => '', - // 'Assign a color to a specific category' => '', + // 'Assign automatically a color based on a category' => '', + // 'Assign automatically a category based on a color' => '', // 'Task creation or modification' => '', // 'Category' => '', // 'Category:' => '', @@ -344,4 +345,31 @@ return array( // 'Add a comment' => '', // 'Edit a comment' => '', // 'Summary' => '', + // 'Time tracking' => '', + // 'Estimate:' => '', + // 'Spent:' => '', + // 'Do you really want to remove this sub-task?' => '', + // 'Remaining:' => '', + // 'hours' => '', + // 'spent' => '', + // 'estimated' => '', + // 'Sub-Tasks' => '', + // 'Add a sub-task' => '', + // 'Original Estimate' => '', + // 'Create another sub-task' => '', + // 'Time Spent' => '', + // 'Edit a sub-task' => '', + // 'Remove a sub-task' => '', + // 'The time must be a numeric value' => '', + // 'Todo' => '', + // 'In progress' => '', + // 'Done' => '', + // 'Sub-task removed successfully.' => '', + // 'Unable to remove this sub-task.' => '', + // 'Sub-task updated successfully.' => '', + // 'Unable to update your sub-task.' => '', + // 'Unable to create your sub-task.' => '', + // 'Sub-task added successfully.' => '', + // 'Maximum size: ' => '', + // 'Unable to upload the file.' => '', ); diff --git a/app/Locales/fr_FR/translations.php b/app/Locales/fr_FR/translations.php index d1ed9f91..9c284892 100644 --- a/app/Locales/fr_FR/translations.php +++ b/app/Locales/fr_FR/translations.php @@ -125,7 +125,7 @@ return array( 'The username must be unique' => 'Le nom d\'utilisateur doit être unique', 'The username must be alphanumeric' => 'Le nom d\'utilisateur doit être alpha-numérique', 'The user id is required' => 'L\'id de l\'utilisateur est obligatoire', - 'Passwords doesn\'t matches' => 'Les mots de passe ne correspondent pas', + 'Passwords don\'t match' => 'Les mots de passe ne correspondent pas', 'The confirmation is required' => 'Le confirmation est requise', 'The column is required' => 'La colonne est obligatoire', 'The project is required' => 'Le projet est obligatoire', @@ -310,7 +310,8 @@ return array( 'Unable to remove this task.' => 'Impossible de supprimer cette tâche.', 'Remove a task' => 'Supprimer une tâche', 'Do you really want to remove this task: "%s"?' => 'Voulez-vous vraiment supprimer cette tâche « %s » ?', - 'Assign a color to a specific category' => 'Assigner une couleur à une catégorie spécifique', + 'Assign automatically a color based on a category' => 'Assigner automatiquement une couleur par rapport à une catégorie définie', + 'Assign automatically a category based on a color' => 'Assigner automatiquement une catégorie par rapport à une couleur définie', 'Task creation or modification' => 'Création ou modification d\'une tâche', 'Category' => 'Catégorie', 'Category:' => 'Catégorie :', @@ -344,4 +345,31 @@ return array( 'Add a comment' => 'Ajouter un commentaire', 'Edit a comment' => 'Modifier un commentaire', 'Summary' => 'Résumé', + 'Time tracking' => 'Gestion du temps', + 'Estimate:' => 'Estimation :', + 'Spent:' => 'Passé :', + 'Do you really want to remove this sub-task?' => 'Voulez-vous vraiment supprimer cette sous-tâche ?', + 'Remaining:' => 'Restant :', + 'hours' => 'heures', + 'spent' => 'passé', + 'estimated' => 'estimé', + 'Sub-Tasks' => 'Sous-Tâches', + 'Add a sub-task' => 'Ajouter une sous-tâche', + 'Original Estimate' => 'Estimation originale', + 'Create another sub-task' => 'Créer une autre sous-tâche', + 'Time Spent' => 'Temps passé', + 'Edit a sub-task' => 'Modifier une sous-tâche', + 'Remove a sub-task' => 'Supprimer une sous-tâche', + 'The time must be a numeric value' => 'Le temps doit-être une valeur numérique', + 'Todo' => 'À faire', + 'In progress' => 'En cours', + 'Done' => 'Terminé', + 'Sub-task removed successfully.' => 'Sous-tâche supprimée avec succès.', + 'Unable to remove this sub-task.' => 'Impossible de supprimer cette sous-tâche.', + 'Sub-task updated successfully.' => 'Sous-tâche mise à jour avec succès.', + 'Unable to update your sub-task.' => 'Impossible de mettre à jour votre sous-tâche.', + 'Unable to create your sub-task.' => 'Impossible de créer votre sous-tâche.', + 'Sub-task added successfully.' => 'Sous-tâche ajouté avec succès.', + 'Maximum size: ' => 'Taille maximum : ', + 'Unable to upload the file.' => 'Impossible de transférer le fichier.', ); diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php index 3490810a..0ab2db52 100644 --- a/app/Locales/pl_PL/translations.php +++ b/app/Locales/pl_PL/translations.php @@ -125,7 +125,7 @@ return array( 'The username must be unique' => 'Nazwa użytkownika musi być unikalna', 'The username must be alphanumeric' => 'Nazwa użytkownika musi być alfanumeryczna', 'The user id is required' => 'ID użytkownika jest wymagane', - 'Passwords doesn\'t matches' => 'Hasła nie pasują do siebie', + 'Passwords don\'t match' => 'Hasła nie pasują do siebie', 'The confirmation is required' => 'Wymagane jest potwierdzenie', 'The column is required' => 'Kolumna jest wymagana', 'The project is required' => 'Projekt jest wymagany', @@ -315,7 +315,8 @@ return array( // 'Unable to remove this task.' => '', // 'Remove a task' => '', // 'Do you really want to remove this task: "%s"?' => '', - // 'Assign a color to a specific category' => '', + // 'Assign automatically a color based on a category' => '', + // 'Assign automatically a category based on a color' => '', // 'Task creation or modification' => '', // 'Category' => '', // 'Category:' => '', @@ -349,4 +350,31 @@ return array( // 'Add a comment' => '', // 'Edit a comment' => '', // 'Summary' => '', + // 'Time tracking' => '', + // 'Estimate:' => '', + // 'Spent:' => '', + // 'Do you really want to remove this sub-task?' => '', + // 'Remaining:' => '', + // 'hours' => '', + // 'spent' => '', + // 'estimated' => '', + // 'Sub-Tasks' => '', + // 'Add a sub-task' => '', + // 'Original Estimate' => '', + // 'Create another sub-task' => '', + // 'Time Spent' => '', + // 'Edit a sub-task' => '', + // 'Remove a sub-task' => '', + // 'The time must be a numeric value' => '', + // 'Todo' => '', + // 'In progress' => '', + // 'Done' => '', + // 'Sub-task removed successfully.' => '', + // 'Unable to remove this sub-task.' => '', + // 'Sub-task updated successfully.' => '', + // 'Unable to update your sub-task.' => '', + // 'Unable to create your sub-task.' => '', + // 'Sub-task added successfully.' => '', + // 'Maximum size: ' => '', + // 'Unable to upload the file.' => '', ); diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php index 267006ce..58d8f7ef 100644 --- a/app/Locales/pt_BR/translations.php +++ b/app/Locales/pt_BR/translations.php @@ -125,7 +125,7 @@ return array( 'The username must be unique' => 'O nome de usuário deve ser único', 'The username must be alphanumeric' => 'O nome de usuário deve ser alfanumérico, sem espaços ou _', 'The user id is required' => 'O id de usuário é obrigatório', - 'Passwords doesn\'t matches' => 'As senhas não conferem', + 'Passwords don\'t match' => 'As senhas não conferem', 'The confirmation is required' => 'A confirmação é obrigatória', 'The column is required' => 'A coluna é obrigatória', 'The project is required' => 'O projeto é obrigatório', @@ -311,7 +311,8 @@ return array( // 'Unable to remove this task.' => '', // 'Remove a task' => '', // 'Do you really want to remove this task: "%s"?' => '', - // 'Assign a color to a specific category' => '', + // 'Assign automatically a color based on a category' => '', + // 'Assign automatically a category based on a color' => '', // 'Task creation or modification' => '', // 'Category' => '', // 'Category:' => '', @@ -345,4 +346,31 @@ return array( // 'Add a comment' => '', // 'Edit a comment' => '', // 'Summary' => '', + // 'Time tracking' => '', + // 'Estimate:' => '', + // 'Spent:' => '', + // 'Do you really want to remove this sub-task?' => '', + // 'Remaining:' => '', + // 'hours' => '', + // 'spent' => '', + // 'estimated' => '', + // 'Sub-Tasks' => '', + // 'Add a sub-task' => '', + // 'Original Estimate' => '', + // 'Create another sub-task' => '', + // 'Time Spent' => '', + // 'Edit a sub-task' => '', + // 'Remove a sub-task' => '', + // 'The time must be a numeric value' => '', + // 'Todo' => '', + // 'In progress' => '', + // 'Done' => '', + // 'Sub-task removed successfully.' => '', + // 'Unable to remove this sub-task.' => '', + // 'Sub-task updated successfully.' => '', + // 'Unable to update your sub-task.' => '', + // 'Unable to create your sub-task.' => '', + // 'Sub-task added successfully.' => '', + // 'Maximum size: ' => '', + // 'Unable to upload the file.' => '', ); diff --git a/app/Model/Acl.php b/app/Model/Acl.php index c6ed8686..035fd7c3 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -36,6 +36,7 @@ class Acl extends Base 'config' => array('index', 'removeremembermetoken'), 'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'), 'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'), + 'subtask' => array('create', 'save', 'edit', 'update', 'confirm', 'remove'), 'task' => array( 'show', 'create', diff --git a/app/Model/Action.php b/app/Model/Action.php index 7cd917e9..0e3aee71 100644 --- a/app/Model/Action.php +++ b/app/Model/Action.php @@ -42,7 +42,8 @@ class Action extends Base 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'), 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'), 'TaskAssignColorUser' => t('Assign a color to a specific user'), - 'TaskAssignColorCategory' => t('Assign a color to a specific category'), + 'TaskAssignColorCategory' => t('Assign automatically a color based on a category'), + 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'), ); } @@ -237,6 +238,9 @@ class Action extends Base case 'TaskAssignColorCategory': $className = '\Action\TaskAssignColorCategory'; return new $className($project_id, new Task($this->db, $this->event)); + case 'TaskAssignCategoryColor': + $className = '\Action\TaskAssignCategoryColor'; + return new $className($project_id, new Task($this->db, $this->event)); default: throw new LogicException('Action not found: '.$name); } diff --git a/app/Model/Base.php b/app/Model/Base.php index e95296bb..66185aeb 100644 --- a/app/Model/Base.php +++ b/app/Model/Base.php @@ -14,6 +14,7 @@ require __DIR__.'/../../vendor/SimpleValidator/Validators/AlphaNumeric.php'; require __DIR__.'/../../vendor/SimpleValidator/Validators/GreaterThan.php'; require __DIR__.'/../../vendor/SimpleValidator/Validators/Date.php'; require __DIR__.'/../../vendor/SimpleValidator/Validators/Email.php'; +require __DIR__.'/../../vendor/SimpleValidator/Validators/Numeric.php'; use Core\Event; use PicoDb\Database; @@ -54,23 +55,4 @@ abstract class Base $this->db = $db; $this->event = $event; } - - /** - * Generate a random token with different methods: openssl or /dev/urandom or fallback to uniqid() - * - * @static - * @access public - * @return string Random token - */ - public static function generateToken() - { - if (function_exists('openssl_random_pseudo_bytes')) { - return bin2hex(\openssl_random_pseudo_bytes(16)); - } - else if (ini_get('open_basedir') === '' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { - return hash('sha256', file_get_contents('/dev/urandom', false, null, 0, 30)); - } - - return hash('sha256', uniqid(mt_rand(), true)); - } } diff --git a/app/Model/Config.php b/app/Model/Config.php index 23abd8b5..469e6447 100644 --- a/app/Model/Config.php +++ b/app/Model/Config.php @@ -5,6 +5,7 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; use Core\Translator; +use Core\Security; /** * Config model @@ -29,7 +30,7 @@ class Config extends Base */ public function getTimezones() { - $timezones = \timezone_identifiers_list(); + $timezones = timezone_identifiers_list(); return array_combine(array_values($timezones), $timezones); } @@ -171,12 +172,12 @@ class Config extends Base */ public function regenerateTokens() { - $this->db->table(self::TABLE)->update(array('webhooks_token' => $this->generateToken())); + $this->db->table(self::TABLE)->update(array('webhooks_token' => Security::generateToken())); $projects = $this->db->table(Project::TABLE)->findAllByColumn('id'); foreach ($projects as $project_id) { - $this->db->table(Project::TABLE)->eq('id', $project_id)->update(array('token' => $this->generateToken())); + $this->db->table(Project::TABLE)->eq('id', $project_id)->update(array('token' => Security::generateToken())); } } } diff --git a/app/Model/File.php b/app/Model/File.php index 41ecfba1..e5aa527e 100644 --- a/app/Model/File.php +++ b/app/Model/File.php @@ -55,6 +55,22 @@ class File extends Base } /** + * Remove all files for a given task + * + * @access public + * @param integer $task_id Task id + * @return bool + */ + public function removeAll($task_id) + { + $files = $this->getAll($task_id); + + foreach ($files as $file) { + $this->remove($file['id']); + } + } + + /** * Create a file entry in the database * * @access public @@ -144,6 +160,7 @@ class File extends Base public function upload($project_id, $task_id, $form_name) { $this->setup(); + $result = array(); if (! empty($_FILES[$form_name])) { @@ -159,7 +176,7 @@ class File extends Base if (@move_uploaded_file($uploaded_filename, self::BASE_PATH.$destination_filename)) { - $this->create( + $result[] = $this->create( $task_id, $original_filename, $destination_filename, @@ -169,5 +186,7 @@ class File extends Base } } } + + return count(array_unique($result)) === 1; } } diff --git a/app/Model/Project.php b/app/Model/Project.php index 9fbb0806..e1465012 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -5,6 +5,7 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; use Event\TaskModification; +use Core\Security; /** * Project model @@ -363,7 +364,7 @@ class Project extends Base { $this->db->startTransaction(); - $values['token'] = self::generateToken(); + $values['token'] = Security::generateToken(); if (! $this->db->table(self::TABLE)->save($values)) { $this->db->cancelTransaction(); diff --git a/app/Model/RememberMe.php b/app/Model/RememberMe.php index 1494b14a..c9ef819f 100644 --- a/app/Model/RememberMe.php +++ b/app/Model/RememberMe.php @@ -2,6 +2,8 @@ namespace Model; +use Core\Security; + /** * RememberMe model * @@ -174,8 +176,8 @@ class RememberMe extends Base */ public function create($user_id, $ip, $user_agent) { - $token = hash('sha256', $user_id.$user_agent.$ip.$this->generateToken()); - $sequence = $this->generateToken(); + $token = hash('sha256', $user_id.$user_agent.$ip.Security::generateToken()); + $sequence = Security::generateToken(); $expiration = time() + self::EXPIRATION; $this->cleanup($user_id); @@ -225,7 +227,7 @@ class RememberMe extends Base */ public function update($token, $sequence) { - $new_sequence = $this->generateToken(); + $new_sequence = Security::generateToken(); $this->db ->table(self::TABLE) diff --git a/app/Model/SubTask.php b/app/Model/SubTask.php new file mode 100644 index 00000000..21ccdaac --- /dev/null +++ b/app/Model/SubTask.php @@ -0,0 +1,179 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Subtask model + * + * @package model + * @author Frederic Guillot + */ +class SubTask extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_subtasks'; + + /** + * Task "done" status + * + * @var integer + */ + const STATUS_DONE = 2; + + /** + * Task "in progress" status + * + * @var integer + */ + const STATUS_INPROGRESS = 1; + + /** + * Task "todo" status + * + * @var integer + */ + const STATUS_TODO = 0; + + /** + * Get available status + * + * @access public + * @return array + */ + public function getStatusList() + { + $status = array( + self::STATUS_TODO => t('Todo'), + self::STATUS_INPROGRESS => t('In progress'), + self::STATUS_DONE => t('Done'), + ); + + asort($status); + + return $status; + } + + /** + * Get all subtasks for a given task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getAll($task_id) + { + $status = $this->getStatusList(); + $subtasks = $this->db->table(self::TABLE) + ->eq('task_id', $task_id) + ->columns(self::TABLE.'.*', User::TABLE.'.username') + ->join(User::TABLE, 'id', 'user_id') + ->findAll(); + + foreach ($subtasks as &$subtask) { + $subtask['status_name'] = $status[$subtask['status']]; + } + + return $subtasks; + } + + /** + * Get a subtask by the id + * + * @access public + * @param integer $subtask_id Subtask id + * @return array + */ + public function getById($subtask_id) + { + return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne(); + } + + /** + * Create + * + * @access public + * @param array $values Form values + * @return bool + */ + public function create(array $values) + { + if (isset($values['another_subtask'])) { + unset($values['another_subtask']); + } + + if (isset($values['time_estimated']) && empty($values['time_estimated'])) { + $values['time_estimated'] = 0; + } + + if (isset($values['time_spent']) && empty($values['time_spent'])) { + $values['time_spent'] = 0; + } + + return $this->db->table(self::TABLE)->save($values); + } + + /** + * Update + * + * @access public + * @param array $values Form values + * @return bool + */ + public function update(array $values) + { + if (isset($values['time_estimated']) && empty($values['time_estimated'])) { + $values['time_estimated'] = 0; + } + + if (isset($values['time_spent']) && empty($values['time_spent'])) { + $values['time_spent'] = 0; + } + + return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); + } + + /** + * Remove + * + * @access public + * @param integer $subtask_id Subtask id + * @return bool + */ + public function remove($subtask_id) + { + return $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove(); + } + + /** + * Validate creation/modification + * + * @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('task_id', t('The task id is required')), + new Validators\Integer('task_id', t('The task id must be an integer')), + new Validators\Required('title', t('The title is required')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 100), 100), + 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')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/Task.php b/app/Model/Task.php index faa33ca9..70f1404c 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -359,12 +359,10 @@ class Task extends Base // Trigger events if ($result) { - $events = array(); - - if (! in_array($this->event->getLastTriggeredEvent(), array(self::EVENT_CREATE_UPDATE))) { - $events[] = self::EVENT_CREATE_UPDATE; - $events[] = self::EVENT_UPDATE; - } + $events = array( + self::EVENT_CREATE_UPDATE, + self::EVENT_UPDATE, + ); if (isset($values['column_id']) && $original_task['column_id'] != $values['column_id']) { $events[] = self::EVENT_MOVE_COLUMN; @@ -441,6 +439,9 @@ class Task extends Base */ public function remove($task_id) { + $file = new File($this->db, $this->event); + $file->removeAll($task_id); + return $this->db->table(self::TABLE)->eq('id', $task_id)->remove(); } @@ -455,6 +456,8 @@ class Task extends Base */ public function move($task_id, $column_id, $position) { + $this->event->clearTriggeredEvents(); + return $this->update(array( 'id' => $task_id, 'column_id' => $column_id, diff --git a/app/Model/User.php b/app/Model/User.php index bce717a7..6804d765 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -203,7 +203,7 @@ class User extends Base 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 doesn\'t matches')), + new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')), new Validators\Integer('default_project_id', t('This value must be an integer')), new Validators\Integer('is_admin', t('This value must be an integer')), new Validators\Email('email', t('Email address invalid')), @@ -264,7 +264,7 @@ class User extends Base 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 doesn\'t matches')), + new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')), new Validators\Integer('default_project_id', t('This value must be an integer')), new Validators\Integer('is_admin', t('This value must be an integer')), new Validators\Email('email', t('Email address invalid')), @@ -359,7 +359,6 @@ class User extends Base // LDAP authentication if (! $authenticated && LDAP_AUTH) { - require __DIR__.'/ldap.php'; $ldap = new Ldap($this->db, $this->event); $authenticated = $ldap->authenticate($username, $password); $method = LastLogin::AUTH_LDAP; diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index d3b111f9..5c7e256f 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -2,7 +2,24 @@ namespace Schema; -const VERSION = 17; +const VERSION = 18; + +function version_18($pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_subtasks ( + id INT NOT NULL AUTO_INCREMENT, + title VARCHAR(255), + status INT DEFAULT 0, + time_estimated INT DEFAULT 0, + time_spent INT DEFAULT 0, + task_id INT, + user_id INT, + PRIMARY KEY (id), + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8" + ); +} function version_17($pdo) { @@ -246,6 +263,6 @@ function version_1($pdo) $pdo->exec(" INSERT INTO config (webhooks_token) - VALUES ('".\Model\Base::generateToken()."') + VALUES ('".\Core\Security::generateToken()."') "); } diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 94ef0316..bfe81c13 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -2,7 +2,23 @@ namespace Schema; -const VERSION = 17; +const VERSION = 18; + +function version_18($pdo) +{ + $pdo->exec(" + CREATE TABLE task_has_subtasks ( + id INTEGER PRIMARY KEY, + title TEXT COLLATE NOCASE, + status INTEGER DEFAULT 0, + time_estimated INTEGER DEFAULT 0, + time_spent INTEGER DEFAULT 0, + task_id INTEGER, + user_id INTEGER, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + )" + ); +} function version_17($pdo) { @@ -193,7 +209,7 @@ function version_3($pdo) foreach ($results as &$result) { $rq = $pdo->prepare('UPDATE projects SET token=? WHERE id=?'); - $rq->execute(array(\Model\Base::generateToken(), $result['id'])); + $rq->execute(array(\Core\Security::generateToken(), $result['id'])); } } } @@ -268,6 +284,6 @@ function version_1($pdo) $pdo->exec(" INSERT INTO config (language, webhooks_token) - VALUES ('en_US', '".\Model\Base::generateToken()."') + VALUES ('en_US', '".\Core\Security::generateToken()."') "); } diff --git a/app/Templates/action_index.php b/app/Templates/action_index.php index b515ccaa..36c333a9 100644 --- a/app/Templates/action_index.php +++ b/app/Templates/action_index.php @@ -56,7 +56,7 @@ <h3><?= t('Add an action') ?></h3> <form method="post" action="?controller=action&action=params&project_id=<?= $project['id'] ?>" autocomplete="off"> - + <?= Helper\form_csrf() ?> <?= Helper\form_hidden('project_id', $values) ?> <?= Helper\form_label(t('Event'), 'event_name') ?> diff --git a/app/Templates/action_params.php b/app/Templates/action_params.php index 15a1d420..da685860 100644 --- a/app/Templates/action_params.php +++ b/app/Templates/action_params.php @@ -9,7 +9,7 @@ <h3><?= t('Define action parameters') ?></h3> <form method="post" action="?controller=action&action=create&project_id=<?= $project['id'] ?>" autocomplete="off"> - + <?= Helper\form_csrf() ?> <?= Helper\form_hidden('project_id', $values) ?> <?= Helper\form_hidden('event_name', $values) ?> <?= Helper\form_hidden('action_name', $values) ?> diff --git a/app/Templates/action_remove.php b/app/Templates/action_remove.php index b90136e8..13679eab 100644 --- a/app/Templates/action_remove.php +++ b/app/Templates/action_remove.php @@ -9,7 +9,7 @@ </p> <div class="form-actions"> - <a href="?controller=action&action=remove&action_id=<?= $action['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <a href="?controller=action&action=remove&action_id=<?= $action['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <?= t('or') ?> <a href="?controller=action&action=index&project_id=<?= $action['project_id'] ?>"><?= t('cancel') ?></a> </div> </div> diff --git a/app/Templates/user_forbidden.php b/app/Templates/app_forbidden.php index 853159ba..0c035404 100644 --- a/app/Templates/user_forbidden.php +++ b/app/Templates/app_forbidden.php @@ -4,6 +4,6 @@ </div> <p class="alert alert-error"> - <?= t('Only administrators can access to this page.') ?> + <?= t('Access Forbidden') ?> </p> </section>
\ No newline at end of file diff --git a/app/Templates/board_assign.php b/app/Templates/board_assign.php index 74448a5c..6f92b375 100644 --- a/app/Templates/board_assign.php +++ b/app/Templates/board_assign.php @@ -18,7 +18,7 @@ <section> <h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3> <form method="post" action="?controller=board&action=assignTask" autocomplete="off"> - + <?= Helper\form_csrf() ?> <?= Helper\form_hidden('id', $values) ?> <?= Helper\form_hidden('project_id', $values) ?> diff --git a/app/Templates/board_edit.php b/app/Templates/board_edit.php index 575536a8..05d9a6f6 100644 --- a/app/Templates/board_edit.php +++ b/app/Templates/board_edit.php @@ -9,7 +9,7 @@ <h3><?= t('Change columns') ?></h3> <form method="post" action="?controller=board&action=update&project_id=<?= $project['id'] ?>" autocomplete="off"> - + <?= Helper\form_csrf() ?> <?php $i = 0; ?> <table> <tr> @@ -27,12 +27,12 @@ <ul> <?php if ($column['position'] != 1): ?> <li> - <a href="?controller=board&action=moveUp&project_id=<?= $project['id'] ?>&column_id=<?= $column['id'] ?>"><?= t('Move Up') ?></a> + <a href="?controller=board&action=moveUp&project_id=<?= $project['id'] ?>&column_id=<?= $column['id'].Helper\param_csrf() ?>"><?= t('Move Up') ?></a> </li> <?php endif ?> <?php if ($column['position'] != count($columns)): ?> <li> - <a href="?controller=board&action=moveDown&project_id=<?= $project['id'] ?>&column_id=<?= $column['id'] ?>"><?= t('Move Down') ?></a> + <a href="?controller=board&action=moveDown&project_id=<?= $project['id'] ?>&column_id=<?= $column['id'].Helper\param_csrf() ?>"><?= t('Move Down') ?></a> </li> <?php endif ?> <li> @@ -52,7 +52,7 @@ <h3><?= t('Add a new column') ?></h3> <form method="post" action="?controller=board&action=add&project_id=<?= $project['id'] ?>" autocomplete="off"> - + <?= Helper\form_csrf() ?> <?= Helper\form_hidden('project_id', $values) ?> <?= Helper\form_label(t('Title'), 'title') ?> <?= Helper\form_text('title', $values, $errors, array('required')) ?> diff --git a/app/Templates/board_index.php b/app/Templates/board_index.php index 8e664219..38fb985c 100644 --- a/app/Templates/board_index.php +++ b/app/Templates/board_index.php @@ -39,4 +39,4 @@ </section> -<script type="text/javascript" src="assets/js/board.js"></script> +<?= Helper\js('assets/js/board.js') ?> diff --git a/app/Templates/board_remove.php b/app/Templates/board_remove.php index b406eb38..76c217b3 100644 --- a/app/Templates/board_remove.php +++ b/app/Templates/board_remove.php @@ -10,7 +10,7 @@ </p> <div class="form-actions"> - <a href="?controller=board&action=remove&column_id=<?= $column['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <a href="?controller=board&action=remove&column_id=<?= $column['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <?= t('or') ?> <a href="?controller=board&action=edit&project_id=<?= $column['project_id'] ?>"><?= t('cancel') ?></a> </div> </div> diff --git a/app/Templates/board_show.php b/app/Templates/board_show.php index e5cd9ceb..6a138bf9 100644 --- a/app/Templates/board_show.php +++ b/app/Templates/board_show.php @@ -1,4 +1,4 @@ -<table id="board" data-project-id="<?= $current_project_id ?>" data-time="<?= time() ?>" data-check-interval="<?= BOARD_CHECK_INTERVAL ?>"> +<table id="board" data-project-id="<?= $current_project_id ?>" data-time="<?= time() ?>" data-check-interval="<?= BOARD_CHECK_INTERVAL ?>" data-csrf-token=<?= \Core\Security::getCSRFToken() ?>> <tr> <?php $column_with = round(100 / count($board), 2); ?> <?php foreach ($board as $column): ?> diff --git a/app/Templates/category_edit.php b/app/Templates/category_edit.php index 99ba0c7c..1339f6da 100644 --- a/app/Templates/category_edit.php +++ b/app/Templates/category_edit.php @@ -8,7 +8,7 @@ <section> <form method="post" action="?controller=category&action=update&project_id=<?= $project['id'] ?>" autocomplete="off"> - + <?= Helper\form_csrf() ?> <?= Helper\form_hidden('id', $values) ?> <?= Helper\form_hidden('project_id', $values) ?> diff --git a/app/Templates/category_index.php b/app/Templates/category_index.php index db986143..7fb923ba 100644 --- a/app/Templates/category_index.php +++ b/app/Templates/category_index.php @@ -34,6 +34,7 @@ <h3><?= t('Add a new category') ?></h3> <form method="post" action="?controller=category&action=save&project_id=<?= $project['id'] ?>" autocomplete="off"> + <?= Helper\form_csrf() ?> <?= Helper\form_hidden('project_id', $values) ?> <?= Helper\form_label(t('Category Name'), 'name') ?> diff --git a/app/Templates/category_remove.php b/app/Templates/category_remove.php index cc2eb678..cfc23e07 100644 --- a/app/Templates/category_remove.php +++ b/app/Templates/category_remove.php @@ -9,7 +9,7 @@ </p> <div class="form-actions"> - <a href="?controller=category&action=remove&project_id=<?= $project['id'] ?>&category_id=<?= $category['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <a href="?controller=category&action=remove&project_id=<?= $project['id'] ?>&category_id=<?= $category['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <?= t('or') ?> <a href="?controller=category&project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a> </div> </div> diff --git a/app/Templates/comment_create.php b/app/Templates/comment_create.php index a566d9c8..f598532d 100644 --- a/app/Templates/comment_create.php +++ b/app/Templates/comment_create.php @@ -3,7 +3,7 @@ </div> <form method="post" action="?controller=comment&action=save&task_id=<?= $task['id'] ?>" autocomplete="off"> - + <?= Helper\form_csrf() ?> <?= Helper\form_hidden('task_id', $values) ?> <?= Helper\form_hidden('user_id', $values) ?> <?= Helper\form_textarea('comment', $values, $errors, array('required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/> diff --git a/app/Templates/comment_edit.php b/app/Templates/comment_edit.php index 0a17a95e..fdf3db54 100644 --- a/app/Templates/comment_edit.php +++ b/app/Templates/comment_edit.php @@ -4,6 +4,7 @@ <form method="post" action="?controller=comment&action=update&task_id=<?= $task['id'] ?>&comment_id=<?= $comment['id'] ?>" autocomplete="off"> + <?= Helper\form_csrf() ?> <?= Helper\form_hidden('id', $values) ?> <?= Helper\form_textarea('comment', $values, $errors, array('required', 'placeholder="'.t('Leave a comment').'"')) ?><br/> diff --git a/app/Templates/comment_remove.php b/app/Templates/comment_remove.php index 02a23f93..7b117781 100644 --- a/app/Templates/comment_remove.php +++ b/app/Templates/comment_remove.php @@ -1,5 +1,5 @@ <div class="page-header"> - <h2><?= t('Add a comment') ?></h2> + <h2><?= t('Remove a comment') ?></h2> </div> <div class="confirm"> @@ -10,7 +10,7 @@ <?= Helper\template('comment_show', array('comment' => $comment, 'task' => $task, 'preview' => true)) ?> <div class="form-actions"> - <a href="?controller=comment&action=remove&task_id=<?= $task['id'] ?>&comment_id=<?= $comment['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <a href="?controller=comment&action=remove&task_id=<?= $task['id'] ?>&comment_id=<?= $comment['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>#comment-<?= $comment['id'] ?>"><?= t('cancel') ?></a> </div> </div>
\ No newline at end of file diff --git a/app/Templates/config_index.php b/app/Templates/config_index.php index 6c610d2b..602e2070 100644 --- a/app/Templates/config_index.php +++ b/app/Templates/config_index.php @@ -7,6 +7,8 @@ <section> <form method="post" action="?controller=config&action=save" autocomplete="off"> + <?= Helper\form_csrf() ?> + <?= Helper\form_label(t('Language'), 'language') ?> <?= Helper\form_select('language', $languages, $values, $errors) ?><br/> @@ -39,7 +41,7 @@ </div> <section class="settings"> <ul> - <li><a href="?controller=config&action=tokens"><?= t('Reset all tokens') ?></a></li> + <li><a href="?controller=config&action=tokens<?= Helper\param_csrf() ?>"><?= t('Reset all tokens') ?></a></li> <li> <?= t('Webhooks token:') ?> <strong><?= Helper\escape($values['webhooks_token']) ?></strong> @@ -50,11 +52,11 @@ <strong><?= Helper\format_bytes($db_size) ?></strong> </li> <li> - <a href="?controller=config&action=downloadDb"><?= t('Download the database') ?></a> + <a href="?controller=config&action=downloadDb<?= Helper\param_csrf() ?>"><?= t('Download the database') ?></a> <?= t('(Gzip compressed Sqlite file)') ?> </li> <li> - <a href="?controller=config&action=optimizeDb"><?= t('Optimize the database') ?></a> + <a href="?controller=config&action=optimizeDb <?= Helper\param_csrf() ?>"><?= t('Optimize the database') ?></a> <?= t('(VACUUM command)') ?> </li> <?php endif ?> @@ -112,7 +114,7 @@ <td><?= dt('%B %e, %G at %k:%M %p', $session['expiration']) ?></td> <td><?= Helper\escape($session['ip']) ?></td> <td><?= Helper\escape($session['user_agent']) ?></td> - <td><a href="?controller=config&action=removeRememberMeToken&id=<?= $session['id'] ?>"><?= t('Remove') ?></a></td> + <td><a href="?controller=config&action=removeRememberMeToken&id=<?= $session['id'].Helper\param_csrf() ?>"><?= t('Remove') ?></a></td> </tr> <?php endforeach ?> </table> diff --git a/app/Templates/file_new.php b/app/Templates/file_new.php index 43223d0c..7f7f1d1c 100644 --- a/app/Templates/file_new.php +++ b/app/Templates/file_new.php @@ -3,7 +3,9 @@ </div> <form action="?controller=file&action=save&task_id=<?= $task['id'] ?>" method="post" enctype="multipart/form-data"> + <?= Helper\form_csrf() ?> <input type="file" name="files[]" multiple /> + <div class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? Helper\format_bytes($max_size) : $max_size ?></div> <div class="form-actions"> <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> <?= t('or') ?> diff --git a/app/Templates/file_remove.php b/app/Templates/file_remove.php index 1d26c15e..af77591c 100644 --- a/app/Templates/file_remove.php +++ b/app/Templates/file_remove.php @@ -8,7 +8,7 @@ </p> <div class="form-actions"> - <a href="?controller=file&action=remove&task_id=<?= $task['id'] ?>&file_id=<?= $file['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <a href="?controller=file&action=remove&task_id=<?= $task['id'] ?>&file_id=<?= $file['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> </div> </div>
\ No newline at end of file diff --git a/app/Templates/file_show.php b/app/Templates/file_show.php new file mode 100644 index 00000000..674861dd --- /dev/null +++ b/app/Templates/file_show.php @@ -0,0 +1,17 @@ +<div class="page-header"> + <h2><?= t('Attachments') ?></h2> +</div> + +<ul class="task-show-files"> +<?php foreach ($files as $file): ?> + <li> + <a href="?controller=file&action=download&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a> + <span class="task-show-file-actions"> + <?php if ($file['is_image']): ?> + <a href="?controller=file&action=open&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>" class="popover"><?= t('open') ?></a>, + <?php endif ?> + <a href="?controller=file&action=confirm&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>"><?= t('remove') ?></a> + </span> + </li> +<?php endforeach ?> +</ul>
\ No newline at end of file diff --git a/app/Templates/layout.php b/app/Templates/layout.php index 0bb8446d..aa430477 100644 --- a/app/Templates/layout.php +++ b/app/Templates/layout.php @@ -6,12 +6,12 @@ <meta name="viewport" content="width=device-width"> <meta name="mobile-web-app-capable" content="yes"> - <script src="assets/js/jquery-1.11.1.min.js"></script> - <script src="assets/js/jquery-ui-1.10.4.custom.min.js"></script> - <script src="assets/js/jquery.ui.touch-punch.min.js"></script> + <?= Helper\js('assets/js/jquery-1.11.1.min.js') ?> + <?= Helper\js('assets/js/jquery-ui-1.10.4.custom.min.js') ?> + <?= Helper\js('assets/js/jquery.ui.touch-punch.min.js') ?> - <link rel="stylesheet" href="assets/css/app.css" media="screen"> - <link rel="stylesheet" href="assets/css/font-awesome.min.css" media="screen"> + <?= Helper\css('assets/css/app.css') ?> + <?= Helper\css('assets/css/font-awesome.min.css') ?> <link rel="icon" type="image/png" href="assets/img/favicon.png"> <link rel="apple-touch-icon" href="assets/img/touch-icon-iphone.png"> @@ -45,7 +45,7 @@ <a href="?controller=config"><?= t('Settings') ?></a> </li> <li> - <a href="?controller=user&action=logout"><?= t('Logout') ?></a> + <a href="?controller=user&action=logout<?= Helper\param_csrf() ?>"><?= t('Logout') ?></a> (<?= Helper\escape(Helper\get_username()) ?>) </li> </ul> diff --git a/app/Templates/project_edit.php b/app/Templates/project_edit.php index 557986bf..a882fbc6 100644 --- a/app/Templates/project_edit.php +++ b/app/Templates/project_edit.php @@ -8,6 +8,7 @@ <section> <form method="post" action="?controller=project&action=update&project_id=<?= $values['id'] ?>" autocomplete="off"> + <?= Helper\form_csrf() ?> <?= Helper\form_hidden('id', $values) ?> <?= Helper\form_label(t('Name'), 'name') ?> diff --git a/app/Templates/project_forbidden.php b/app/Templates/project_forbidden.php deleted file mode 100644 index 1cba7b58..00000000 --- a/app/Templates/project_forbidden.php +++ /dev/null @@ -1,9 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('Forbidden') ?></h2> - </div> - - <p class="alert alert-error"> - <?= t('You are not allowed to access to this project.') ?> - </p> -</section>
\ No newline at end of file diff --git a/app/Templates/project_index.php b/app/Templates/project_index.php index 1a3dbd49..927924a5 100644 --- a/app/Templates/project_index.php +++ b/app/Templates/project_index.php @@ -78,9 +78,9 @@ </li> <li> <?php if ($project['is_active']): ?> - <a href="?controller=project&action=disable&project_id=<?= $project['id'] ?>"><?= t('Disable') ?></a> + <a href="?controller=project&action=disable&project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Disable') ?></a> <?php else: ?> - <a href="?controller=project&action=enable&project_id=<?= $project['id'] ?>"><?= t('Enable') ?></a> + <a href="?controller=project&action=enable&project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Enable') ?></a> <?php endif ?> </li> <li> diff --git a/app/Templates/project_new.php b/app/Templates/project_new.php index 2026d461..b4ed9990 100644 --- a/app/Templates/project_new.php +++ b/app/Templates/project_new.php @@ -8,6 +8,7 @@ <section> <form method="post" action="?controller=project&action=save" autocomplete="off"> + <?= Helper\form_csrf() ?> <?= Helper\form_label(t('Name'), 'name') ?> <?= Helper\form_text('name', $values, $errors, array('autofocus', 'required')) ?> diff --git a/app/Templates/project_remove.php b/app/Templates/project_remove.php index e9f213b5..e25efa2f 100644 --- a/app/Templates/project_remove.php +++ b/app/Templates/project_remove.php @@ -9,7 +9,7 @@ </p> <div class="form-actions"> - <a href="?controller=project&action=remove&project_id=<?= $project['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <a href="?controller=project&action=remove&project_id=<?= $project['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a> </div> </div> diff --git a/app/Templates/project_users.php b/app/Templates/project_users.php index 0448004f..8afac709 100644 --- a/app/Templates/project_users.php +++ b/app/Templates/project_users.php @@ -10,6 +10,8 @@ <?php if (! empty($users['not_allowed'])): ?> <form method="post" action="?controller=project&action=allow&project_id=<?= $project['id'] ?>" autocomplete="off"> + <?= Helper\form_csrf() ?> + <?= Helper\form_hidden('project_id', array('project_id' => $project['id'])) ?> <?= Helper\form_label(t('User'), 'user_id') ?> @@ -32,7 +34,7 @@ <?php foreach ($users['allowed'] as $user_id => $username): ?> <li> <strong><?= Helper\escape($username) ?></strong> - (<a href="?controller=project&action=revoke&project_id=<?= $project['id'] ?>&user_id=<?= $user_id ?>"><?= t('revoke') ?></a>) + (<a href="?controller=project&action=revoke&project_id=<?= $project['id'] ?>&user_id=<?= $user_id.Helper\param_csrf() ?>"><?= t('revoke') ?></a>) </li> <?php endforeach ?> </ul> diff --git a/app/Templates/subtask_create.php b/app/Templates/subtask_create.php new file mode 100644 index 00000000..f1b27ab9 --- /dev/null +++ b/app/Templates/subtask_create.php @@ -0,0 +1,27 @@ +<div class="page-header"> + <h2><?= t('Add a sub-task') ?></h2> +</div> + +<form method="post" action="?controller=subtask&action=save&task_id=<?= $task['id'] ?>" autocomplete="off"> + + <?= Helper\form_csrf() ?> + + <?= Helper\form_hidden('task_id', $values) ?> + + <?= Helper\form_label(t('Title'), 'title') ?> + <?= Helper\form_text('title', $values, $errors, array('required autofocus')) ?><br/> + + <?= Helper\form_label(t('Assignee'), 'user_id') ?> + <?= Helper\form_select('user_id', $users_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Original Estimate'), 'time_estimated') ?> + <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/> + + <?= Helper\form_checkbox('another_subtask', t('Create another sub-task'), 1, isset($values['another_subtask']) && $values['another_subtask'] == 1) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> + </div> +</form> diff --git a/app/Templates/subtask_edit.php b/app/Templates/subtask_edit.php new file mode 100644 index 00000000..fc65d3b3 --- /dev/null +++ b/app/Templates/subtask_edit.php @@ -0,0 +1,32 @@ +<div class="page-header"> + <h2><?= t('Edit a sub-task') ?></h2> +</div> + +<form method="post" action="?controller=subtask&action=update&task_id=<?= $task['id'] ?>&subtask_id=<?= $subtask['id'] ?>" autocomplete="off"> + + <?= Helper\form_csrf() ?> + + <?= Helper\form_hidden('id', $values) ?> + <?= Helper\form_hidden('task_id', $values) ?> + + <?= Helper\form_label(t('Title'), 'title') ?> + <?= Helper\form_text('title', $values, $errors, array('required autofocus')) ?><br/> + + <?= Helper\form_label(t('Status'), 'status') ?> + <?= Helper\form_select('status', $status_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Assignee'), 'user_id') ?> + <?= Helper\form_select('user_id', $users_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Original Estimate'), 'time_estimated') ?> + <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/> + + <?= Helper\form_label(t('Time Spent'), 'time_spent') ?> + <?= Helper\form_numeric('time_spent', $values, $errors) ?> <?= t('hours') ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> + </div> +</form> diff --git a/app/Templates/subtask_remove.php b/app/Templates/subtask_remove.php new file mode 100644 index 00000000..12c99cf1 --- /dev/null +++ b/app/Templates/subtask_remove.php @@ -0,0 +1,16 @@ +<div class="page-header"> + <h2><?= t('Remove a sub-task') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this sub-task?') ?> + </p> + + <p><strong><?= Helper\escape($subtask['title']) ?></strong></p> + + <div class="form-actions"> + <a href="?controller=subtask&action=remove&task_id=<?= $task['id'] ?>&subtask_id=<?= $subtask['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>#subtasks"><?= t('cancel') ?></a> + </div> +</div>
\ No newline at end of file diff --git a/app/Templates/subtask_show.php b/app/Templates/subtask_show.php new file mode 100644 index 00000000..b9385c7e --- /dev/null +++ b/app/Templates/subtask_show.php @@ -0,0 +1,60 @@ +<div class="page-header"> + <h2><?= t('Sub-Tasks') ?></h2> +</div> + +<?php + +$total_spent = 0; +$total_estimated = 0; +$total_remaining = 0; + +?> + +<table class="subtasks-table"> + <tr> + <th width="40%"><?= t('Title') ?></th> + <th><?= t('Status') ?></th> + <th><?= t('Assignee') ?></th> + <th><?= t('Time tracking') ?></th> + <th><?= t('Actions') ?></th> + </tr> + <?php foreach ($subtasks as $subtask): ?> + <tr> + <td><?= Helper\escape($subtask['title']) ?></td> + <td><?= Helper\escape($subtask['status_name']) ?></td> + <td> + <?php if (! empty($subtask['username'])): ?> + <?= Helper\escape($subtask['username']) ?> + <?php endif ?> + </td> + <td> + <?php if (! empty($subtask['time_spent'])): ?> + <strong><?= Helper\escape($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> + <?php endif ?> + + <?php if (! empty($subtask['time_estimated'])): ?> + <strong><?= Helper\escape($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <?php endif ?> + </td> + <td> + <a href="?controller=subtask&action=edit&task_id=<?= $task['id'] ?>&subtask_id=<?= $subtask['id'] ?>"><?= t('Edit') ?></a> + <?= t('or') ?> + <a href="?controller=subtask&action=confirm&task_id=<?= $task['id'] ?>&subtask_id=<?= $subtask['id'] ?>"><?= t('Remove') ?></a> + </td> + </tr> + <?php + $total_estimated += $subtask['time_estimated']; + $total_spent += $subtask['time_spent']; + $total_remaining = $total_estimated - $total_spent; + ?> + <?php endforeach ?> +</table> + +<div class="subtasks-time-tracking"> + <h4><?= t('Time tracking') ?></h4> + <ul> + <li><?= t('Estimate:') ?> <strong><?= Helper\escape($total_estimated) ?></strong> <?= t('hours') ?></li> + <li><?= t('Spent:') ?> <strong><?= Helper\escape($total_spent) ?></strong> <?= t('hours') ?></li> + <li><?= t('Remaining:') ?> <strong><?= Helper\escape($total_remaining > 0 ? $total_remaining : 0) ?></strong> <?= t('hours') ?></li> + </ul> +</div>
\ No newline at end of file diff --git a/app/Templates/task_close.php b/app/Templates/task_close.php index 6843c2f6..5c75b72b 100644 --- a/app/Templates/task_close.php +++ b/app/Templates/task_close.php @@ -8,7 +8,7 @@ </p> <div class="form-actions"> - <a href="?controller=task&action=close&task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <a href="?controller=task&action=close&task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> </div> </div>
\ No newline at end of file diff --git a/app/Templates/task_edit.php b/app/Templates/task_edit.php index d698c21d..c03c7d9a 100644 --- a/app/Templates/task_edit.php +++ b/app/Templates/task_edit.php @@ -8,6 +8,8 @@ <section> <form method="post" action="?controller=task&action=update&task_id=<?= $task['id'] ?>" autocomplete="off"> + <?= Helper\form_csrf() ?> + <div class="form-column"> <?= Helper\form_label(t('Title'), 'title') ?> diff --git a/app/Templates/task_edit_description.php b/app/Templates/task_edit_description.php index 0bdc40a2..550dac73 100644 --- a/app/Templates/task_edit_description.php +++ b/app/Templates/task_edit_description.php @@ -4,6 +4,8 @@ <form method="post" action="?controller=task&action=saveDescription&task_id=<?= $task['id'] ?>" autocomplete="off"> + <?= Helper\form_csrf() ?> + <?= Helper\form_hidden('id', $values) ?> <?= Helper\form_textarea('description', $values, $errors, array('required', 'placeholder="'.t('Leave a description').'"'), 'description-textarea') ?><br/> <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> diff --git a/app/Templates/task_layout.php b/app/Templates/task_layout.php index ce5f36c5..cc711b78 100644 --- a/app/Templates/task_layout.php +++ b/app/Templates/task_layout.php @@ -14,4 +14,5 @@ </div> </section> </section> -<script type="text/javascript" src="assets/js/task.js"></script>
\ No newline at end of file + +<?= Helper\js('assets/js/task.js') ?> diff --git a/app/Templates/task_new.php b/app/Templates/task_new.php index d233efd2..2938c4ca 100644 --- a/app/Templates/task_new.php +++ b/app/Templates/task_new.php @@ -5,6 +5,8 @@ <section> <form method="post" action="?controller=task&action=save" autocomplete="off"> + <?= Helper\form_csrf() ?> + <div class="form-column"> <?= Helper\form_label(t('Title'), 'title') ?> <?= Helper\form_text('title', $values, $errors, array('autofocus', 'required')) ?><br/> diff --git a/app/Templates/task_open.php b/app/Templates/task_open.php index 59ea0b54..3526ec81 100644 --- a/app/Templates/task_open.php +++ b/app/Templates/task_open.php @@ -8,7 +8,7 @@ </p> <div class="form-actions"> - <a href="?controller=task&action=open&task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <a href="?controller=task&action=open&task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> </div> </div>
\ No newline at end of file diff --git a/app/Templates/task_remove.php b/app/Templates/task_remove.php index 60e4e8e7..dd4841db 100644 --- a/app/Templates/task_remove.php +++ b/app/Templates/task_remove.php @@ -8,7 +8,7 @@ </p> <div class="form-actions"> - <a href="?controller=task&action=remove&task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <a href="?controller=task&action=remove&task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> </div> </div>
\ No newline at end of file diff --git a/app/Templates/task_show.php b/app/Templates/task_show.php index 53cdbae8..4c3d4697 100644 --- a/app/Templates/task_show.php +++ b/app/Templates/task_show.php @@ -62,23 +62,14 @@ <?php if (! empty($files)): ?> <div id="attachments" class="task-show-section"> - <div class="page-header"> - <h2><?= t('Attachments') ?></h2> - </div> + <?= Helper\template('file_show', array('task' => $task, 'files' => $files)) ?> +</div> +<?php endif ?> - <ul class="task-show-files"> - <?php foreach ($files as $file): ?> - <li> - <a href="?controller=file&action=download&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a> - <span class="task-show-file-actions"> - <?php if ($file['is_image']): ?> - <a href="?controller=file&action=open&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>" class="popover"><?= t('open') ?></a>, - <?php endif ?> - <a href="?controller=file&action=confirm&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>"><?= t('remove') ?></a> - </span> - </li> - <?php endforeach ?> - </ul> + +<?php if (! empty($subtasks)): ?> +<div id="subtasks" class="task-show-section"> + <?= Helper\template('subtask_show', array('task' => $task, 'subtasks' => $subtasks)) ?> </div> <?php endif ?> diff --git a/app/Templates/task_sidebar.php b/app/Templates/task_sidebar.php index 8a3939b8..d97c44e2 100644 --- a/app/Templates/task_sidebar.php +++ b/app/Templates/task_sidebar.php @@ -5,6 +5,7 @@ <li><a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('Summary') ?></a></li> <li><a href="?controller=task&action=edit&task_id=<?= $task['id'] ?>"><?= t('Edit the task') ?></a></li> <li><a href="?controller=task&action=editDescription&task_id=<?= $task['id'] ?>"><?= t('Edit the description') ?></a></li> + <li><a href="?controller=subtask&action=create&task_id=<?= $task['id'] ?>"><?= t('Add a sub-task') ?></a></li> <li><a href="?controller=comment&action=create&task_id=<?= $task['id'] ?>"><?= t('Add a comment') ?></a></li> <li><a href="?controller=file&action=create&task_id=<?= $task['id'] ?>"><?= t('Attach a document') ?></a></li> <li><a href="?controller=task&action=duplicate&project_id=<?= $task['project_id'] ?>&task_id=<?= $task['id'] ?>"><?= t('Duplicate') ?></a></li> diff --git a/app/Templates/user_edit.php b/app/Templates/user_edit.php index c857fe1c..6b83f748 100644 --- a/app/Templates/user_edit.php +++ b/app/Templates/user_edit.php @@ -8,6 +8,8 @@ <section> <form method="post" action="?controller=user&action=update" autocomplete="off"> + <?= Helper\form_csrf() ?> + <div class="form-column"> <?= Helper\form_hidden('id', $values) ?> @@ -48,9 +50,9 @@ <?php if (GOOGLE_AUTH && Helper\is_current_user($values['id'])): ?> <?php if (empty($values['google_id'])): ?> - <a href="?controller=user&action=google"><?= t('Link my Google Account') ?></a> + <a href="?controller=user&action=google<?= Helper\param_csrf() ?>"><?= t('Link my Google Account') ?></a> <?php else: ?> - <a href="?controller=user&action=unlinkGoogle"><?= t('Unlink my Google Account') ?></a> + <a href="?controller=user&action=unlinkGoogle<?= Helper\param_csrf() ?>"><?= t('Unlink my Google Account') ?></a> <?php endif ?> <?php endif ?> diff --git a/app/Templates/user_login.php b/app/Templates/user_login.php index 878170e3..49902ebb 100644 --- a/app/Templates/user_login.php +++ b/app/Templates/user_login.php @@ -8,6 +8,8 @@ <form method="post" action="?controller=user&action=check" class="form-login"> + <?= Helper\form_csrf() ?> + <?= Helper\form_label(t('Username'), 'username') ?> <?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/> diff --git a/app/Templates/user_new.php b/app/Templates/user_new.php index 6ad976f2..3e22b7ee 100644 --- a/app/Templates/user_new.php +++ b/app/Templates/user_new.php @@ -8,6 +8,8 @@ <section> <form method="post" action="?controller=user&action=save" autocomplete="off"> + <?= Helper\form_csrf() ?> + <div class="form-column"> <?= Helper\form_label(t('Username'), 'username') ?> diff --git a/app/Templates/user_remove.php b/app/Templates/user_remove.php index a4db2e4a..61d4163b 100644 --- a/app/Templates/user_remove.php +++ b/app/Templates/user_remove.php @@ -7,7 +7,7 @@ <p class="alert alert-info"><?= t('Do you really want to remove this user: "%s"?', $user['username']) ?></p> <div class="form-actions"> - <a href="?controller=user&action=remove&user_id=<?= $user['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <a href="?controller=user&action=remove&user_id=<?= $user['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> <?= t('or') ?> <a href="?controller=user"><?= t('cancel') ?></a> </div> </div> diff --git a/app/helpers.php b/app/helpers.php index 8351328a..2df4d839 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -2,6 +2,21 @@ namespace Helper; +function param_csrf() +{ + return '&csrf_token='.\Core\Security::getCSRFToken(); +} + +function js($filename) +{ + return '<script type="text/javascript" src="'.$filename.'?'.filemtime($filename).'"></script>'; +} + +function css($filename) +{ + return '<link rel="stylesheet" href="'.$filename.'?'.filemtime($filename).'" media="screen">'; +} + function template($name, array $args = array()) { $tpl = new \Core\Template; @@ -153,6 +168,11 @@ function form_value($values, $name) return isset($values[$name]) ? 'value="'.escape($values[$name]).'"' : ''; } +function form_csrf() +{ + return '<input type="hidden" name="csrf_token" value="'.\Core\Security::getCSRFToken().'"/>'; +} + function form_hidden($name, $values = array()) { return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).'/>'; @@ -260,3 +280,8 @@ function form_number($name, $values = array(), array $errors = array(), array $a { return form_input('number', $name, $values, $errors, $attributes, $class); } + +function form_numeric($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('text', $name, $values, $errors, $attributes, $class.' form-numeric'); +} diff --git a/assets/css/app.css b/assets/css/app.css index 73c45dd9..017a64fa 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -158,6 +158,7 @@ textarea:focus { box-shadow: 0 0 8px rgba(82, 168, 236, 0.6); } +input.form-numeric, input[type="number"] { width: 70px; } @@ -797,6 +798,24 @@ a.task-board-nobody { max-width: 800px; } +/* subtasks */ +.subtasks-table { + font-size: 0.85em; +} + +.subtasks-table td { + vertical-align: middle; +} + +.subtasks-time-tracking h4 { + margin-bottom: 5px; +} + +.subtasks-time-tracking li { + list-style-type: square; + margin-left: 30px; +} + /* markdown content */ .markdown { line-height: 1.4em; diff --git a/assets/js/board.js b/assets/js/board.js index 49dab9fa..7ff7445b 100644 --- a/assets/js/board.js +++ b/assets/js/board.js @@ -70,8 +70,9 @@ }); $.ajax({ + cache: false, url: "?controller=board&action=save&project_id=" + projectId, - data: {positions: data}, + data: {"positions": data, "csrf_token": $("#board").attr("data-csrf-token")}, type: "POST", success: function(data) { $("#board").remove(); @@ -90,6 +91,7 @@ if (is_visible() && projectId != undefined && timestamp != undefined) { $.ajax({ + cache: false, url: "?controller=board&action=check&project_id=" + projectId + "×tamp=" + timestamp, statusCode: { 200: function(data) { diff --git a/docs/automatic-actions.markdown b/docs/automatic-actions.markdown index 9526e9af..e903e0b1 100644 --- a/docs/automatic-actions.markdown +++ b/docs/automatic-actions.markdown @@ -34,7 +34,8 @@ List of available actions - Assign the task to the person who does the action - Duplicate the task to another project - Assign a color to a specific user -- Assign a color to a specific category +- Assign automatically a color based on a category +- Assign automatically a category based on a color Examples -------- @@ -67,14 +68,14 @@ Let's say we have two projects "Customer orders" and "Production", once the orde - Choose the action: **Duplicate the task to another project** - Define the action parameters: **Column = Validated** and **Project = Production** -### I want to assign a specific color to the user Bob +### I want to assign automatically a color to the user Bob - Choose the event: **Task creation** - Choose the action: **Assign a color to a specific user** - Define the action parameters: **Color = Green** and **Assignee = Bob** -### I want to assign a specific color to the category "Feature Request" +### I want to assign automatically a color to the defined category "Feature Request" - Choose the event: **Task creation or modification** -- Choose the action: **Assign a color to a specific category** +- Choose the action: **Assign automatically a color based on a category** - Define the action parameters: **Color = Blue** and **Category = Feature Request** diff --git a/docs/centos-installation.markdown b/docs/centos-installation.markdown index 8f8c4d3e..f8963ecd 100644 --- a/docs/centos-installation.markdown +++ b/docs/centos-installation.markdown @@ -25,10 +25,10 @@ Install Kanboard: ```bash cd /var/www/html -wget http://kanboard.net/kanboard-VERSION.zip -unzip kanboard-VERSION.zip +wget http://kanboard.net/kanboard-latest.zip +unzip kanboard-latest.zip chown -R apache:apache kanboard/data -rm kanboard-VERSION.zip +rm kanboard-latest.zip ``` Go to `http://your_server/kanboard/`.
\ No newline at end of file diff --git a/docs/debian-installation.markdown b/docs/debian-installation.markdown index 8a6067f1..10cab44a 100644 --- a/docs/debian-installation.markdown +++ b/docs/debian-installation.markdown @@ -15,10 +15,10 @@ Install Kanboard: ```bash cd /var/www -wget http://kanboard.net/kanboard-VERSION.zip -unzip kanboard-VERSION.zip +wget http://kanboard.net/kanboard-latest.zip +unzip kanboard-latest.zip chown -R www-data:www-data kanboard/data -rm kanboard-VERSION.zip +rm kanboard-latest.zip ``` Debian 6 (Squeeze) @@ -35,8 +35,8 @@ Install Kanboard: ```bash cd /var/www -wget http://kanboard.net/kanboard-VERSION.zip -unzip kanboard-VERSION.zip +wget http://kanboard.net/kanboard-latest.zip +unzip kanboard-latest.zip chown -R www-data:www-data kanboard/data -rm kanboard-VERSION.zip +rm kanboard-latest.zip ```
\ No newline at end of file diff --git a/docs/ubuntu-installation.markdown b/docs/ubuntu-installation.markdown index 38fa3e8d..f5670758 100644 --- a/docs/ubuntu-installation.markdown +++ b/docs/ubuntu-installation.markdown @@ -15,8 +15,8 @@ Install Kanboard: ```bash cd /var/www/html -sudo wget http://kanboard.net/kanboard-VERSION.zip -sudo unzip kanboard-VERSION.zip +sudo wget http://kanboard.net/kanboard-latest.zip +sudo unzip kanboard-latest.zip sudo chown -R www-data:www-data kanboard/data -sudo rm kanboard-VERSION.zip +sudo rm kanboard-latest.zip ``` diff --git a/scripts/make-archive.sh b/scripts/make-archive.sh index 0f8ecb78..0c1d8fee 100755 --- a/scripts/make-archive.sh +++ b/scripts/make-archive.sh @@ -6,9 +6,8 @@ APP="kanboard" cd /tmp rm -rf /tmp/$APP /tmp/$APP-*.zip 2>/dev/null git clone git@github.com:fguillot/$APP.git -rm -rf $APP/data/*.sqlite $APP/.git $APP/.gitignore $APP/scripts $APP/tests $APP/Vagrantfile -sed -i.bak s/master/$VERSION/g $APP/common.php && rm -f $APP/*.bak +rm -rf $APP/data/*.sqlite $APP/.git $APP/.gitignore $APP/scripts $APP/tests $APP/Vagrantfile $APP/.*.yml $APP/phpunit.xml $APP/README.markdown $APP/docs +sed -i.bak s/master/$VERSION/g $APP/app/common.php && rm -f $APP/app/*.bak zip -r $APP-$VERSION.zip $APP mv $APP-*.zip ~/Devel/websites/$APP rm -rf /tmp/$APP 2>/dev/null - diff --git a/tests/ActionTaskAssignColorCategoryTest.php b/tests/ActionTaskAssignColorCategoryTest.php new file mode 100644 index 00000000..18b4311e --- /dev/null +++ b/tests/ActionTaskAssignColorCategoryTest.php @@ -0,0 +1,76 @@ +<?php + +require_once __DIR__.'/Base.php'; + +use Model\Task; +use Model\Project; +use Model\Category; + +class ActionTaskAssignColorCategory extends Base +{ + public function testBadProject() + { + $action = new Action\TaskAssignColorCategory(3, new Task($this->db, $this->event)); + + $event = array( + 'project_id' => 2, + 'task_id' => 3, + 'column_id' => 5, + ); + + $this->assertFalse($action->isExecutable($event)); + $this->assertFalse($action->execute($event)); + } + + public function testExecute() + { + $action = new Action\TaskAssignColorCategory(1, new Task($this->db, $this->event)); + $action->setParam('category_id', 1); + $action->setParam('color_id', 'blue'); + + // We create a task in the first column + $t = new Task($this->db, $this->event); + $p = new Project($this->db, $this->event); + $c = new Category($this->db, $this->event); + + $this->assertEquals(1, $p->create(array('name' => 'test'))); + $this->assertEquals(1, $c->create(array('name' => 'c1'))); + $this->assertEquals(2, $c->create(array('name' => 'c2'))); + $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1, 'color_id' => 'green', 'category_id' => 2))); + + // We create an event but we don't do anything + $event = array( + 'project_id' => 1, + 'task_id' => 1, + 'column_id' => 1, + 'category_id' => 2, + 'position' => 2, + ); + + // Our event should NOT be executed + $this->assertFalse($action->execute($event)); + + // Our task should be assigned to the ategory_id=1 and have the green color + $task = $t->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals(2, $task['category_id']); + $this->assertEquals('green', $task['color_id']); + + // We create an event to move the task + $event = array( + 'project_id' => 1, + 'task_id' => 1, + 'column_id' => 1, + 'position' => 5, + 'category_id' => 1, + ); + + // Our event should be executed + $this->assertTrue($action->execute($event)); + + // Our task should have the blue color + $task = $t->getById(1); + $this->assertNotEmpty($task); + $this->assertEquals('blue', $task['color_id']); + } +} diff --git a/tests/ActionTaskAssignColorUserTest.php b/tests/ActionTaskAssignColorUserTest.php index 769ecc66..04e3172f 100644 --- a/tests/ActionTaskAssignColorUserTest.php +++ b/tests/ActionTaskAssignColorUserTest.php @@ -10,7 +10,6 @@ class ActionTaskAssignColorUser extends Base public function testBadProject() { $action = new Action\TaskAssignColorUser(3, new Task($this->db, $this->event)); - $action->setParam('column_id', 5); $event = array( 'project_id' => 2, @@ -22,24 +21,9 @@ class ActionTaskAssignColorUser extends Base $this->assertFalse($action->execute($event)); } - public function testBadColumn() - { - $action = new Action\TaskAssignColorUser(3, new Task($this->db, $this->event)); - $action->setParam('column_id', 5); - - $event = array( - 'project_id' => 3, - 'task_id' => 3, - 'column_id' => 3, - ); - - $this->assertFalse($action->execute($event)); - } - public function testExecute() { $action = new Action\TaskAssignColorUser(1, new Task($this->db, $this->event)); - $action->setParam('column_id', 2); $action->setParam('user_id', 1); $action->setParam('color_id', 'blue'); diff --git a/tests/ActionTest.php b/tests/ActionTest.php index 2757a069..2eb12784 100644 --- a/tests/ActionTest.php +++ b/tests/ActionTest.php @@ -6,6 +6,7 @@ use Model\Action; use Model\Project; use Model\Board; use Model\Task; +use Model\Category; class ActionTest extends Base { @@ -45,7 +46,7 @@ class ActionTest extends Base $this->assertEquals(4, $actions[0]['params'][0]['value']); } - public function testExecuteAction() + public function testEventMoveColumn() { $task = new Task($this->db, $this->event); $board = new Board($this->db, $this->event); @@ -85,12 +86,89 @@ class ActionTest extends Base // We move our task $task->move(1, 4, 1); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_COLUMN)); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_UPDATE)); + // Our task should be closed $t1 = $task->getById(1); $this->assertEquals(4, $t1['column_id']); $this->assertEquals(0, $t1['is_active']); } + public function testEventMovePosition() + { + $task = new Task($this->db, $this->event); + $board = new Board($this->db, $this->event); + $project = new Project($this->db, $this->event); + $action = new Action($this->db, $this->event); + + // We create a project + $this->assertEquals(1, $project->create(array('name' => 'unit_test'))); + + // We create a task + $this->assertEquals(1, $task->create(array( + 'title' => 'unit_test 0', + 'project_id' => 1, + 'owner_id' => 1, + 'color_id' => 'red', + 'column_id' => 1, + 'category_id' => 1, + ))); + + $this->assertEquals(2, $task->create(array( + 'title' => 'unit_test 1', + 'project_id' => 1, + 'owner_id' => 1, + 'color_id' => 'yellow', + 'column_id' => 1, + 'category_id' => 1, + ))); + + // We create a new action, when the category_id=2 then the color_id should be green + $this->assertTrue($action->create(array( + 'project_id' => 1, + 'event_name' => Task::EVENT_MOVE_POSITION, + 'action_name' => 'TaskAssignColorCategory', + 'params' => array( + 'category_id' => 1, + 'color_id' => 'green', + ) + ))); + + // We bind events + $action->attachEvents(); + + $this->assertTrue($this->event->hasListener(Task::EVENT_MOVE_POSITION, 'Action\TaskAssignColorCategory')); + + // Our task should have the color red and position=0 + $t1 = $task->getById(1); + $this->assertEquals(0, $t1['position']); + $this->assertEquals(1, $t1['is_active']); + $this->assertEquals('red', $t1['color_id']); + + $t1 = $task->getById(2); + $this->assertEquals(1, $t1['position']); + $this->assertEquals(1, $t1['is_active']); + $this->assertEquals('yellow', $t1['color_id']); + + // We move our tasks + $task->move(1, 1, 1); // task #1 to position 1 + $task->move(2, 1, 0); // task #2 to position 0 + + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_POSITION)); + + // Both tasks should be green + $t1 = $task->getById(1); + $this->assertEquals(1, $t1['position']); + $this->assertEquals(1, $t1['is_active']); + $this->assertEquals('green', $t1['color_id']); + + $t1 = $task->getById(2); + $this->assertEquals(0, $t1['position']); + $this->assertEquals(1, $t1['is_active']); + $this->assertEquals('green', $t1['color_id']); + } + public function testExecuteMultipleActions() { $task = new Task($this->db, $this->event); @@ -146,7 +224,9 @@ class ActionTest extends Base // We move our task $task->move(1, 4, 1); - $this->assertEquals(Task::EVENT_CREATE, $this->event->getLastTriggeredEvent()); + + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CLOSE)); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_COLUMN)); // Our task should be closed $t1 = $task->getById(1); diff --git a/tests/Base.php b/tests/Base.php index 9c8cfc4a..d4065982 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -4,6 +4,8 @@ if (version_compare(PHP_VERSION, '5.5.0', '<')) { require __DIR__.'/../vendor/password.php'; } +require_once __DIR__.'/../app/Core/Security.php'; + require_once __DIR__.'/../vendor/PicoDb/Database.php'; require_once __DIR__.'/../app/Schema/Sqlite.php'; @@ -20,11 +22,13 @@ require_once __DIR__.'/../app/Model/Project.php'; require_once __DIR__.'/../app/Model/User.php'; require_once __DIR__.'/../app/Model/Board.php'; require_once __DIR__.'/../app/Model/Action.php'; +require_once __DIR__.'/../app/Model/Category.php'; require_once __DIR__.'/../app/Action/Base.php'; require_once __DIR__.'/../app/Action/TaskClose.php'; require_once __DIR__.'/../app/Action/TaskAssignSpecificUser.php'; require_once __DIR__.'/../app/Action/TaskAssignColorUser.php'; +require_once __DIR__.'/../app/Action/TaskAssignColorCategory.php'; require_once __DIR__.'/../app/Action/TaskAssignCurrentUser.php'; require_once __DIR__.'/../app/Action/TaskDuplicateAnotherProject.php'; diff --git a/tests/TaskTest.php b/tests/TaskTest.php index 2f645131..da7e6a70 100644 --- a/tests/TaskTest.php +++ b/tests/TaskTest.php @@ -110,7 +110,7 @@ class TaskTest extends Base // We duplicate our task $this->assertEquals(2, $t->duplicate(1)); - $this->assertEquals(Task::EVENT_CREATE, $this->event->getLastTriggeredEvent()); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CREATE)); // Check the values of the duplicated task $task = $t->getById(2); @@ -136,7 +136,7 @@ class TaskTest extends Base // We duplicate our task to the 2nd project $this->assertEquals(2, $t->duplicateToAnotherProject(1, 2)); - $this->assertEquals(Task::EVENT_CREATE, $this->event->getLastTriggeredEvent()); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CREATE)); // Check the values of the duplicated task $task = $t->getById(2); @@ -157,30 +157,31 @@ class TaskTest extends Base // We create task $this->assertEquals(1, $t->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 1))); - $this->assertEquals(Task::EVENT_CREATE, $this->event->getLastTriggeredEvent()); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CREATE)); // We update a task $this->assertTrue($t->update(array('title' => 'test2', 'id' => 1))); - $this->assertEquals(Task::EVENT_UPDATE, $this->event->getLastTriggeredEvent()); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_UPDATE)); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CREATE_UPDATE)); // We close our task $this->assertTrue($t->close(1)); - $this->assertEquals(Task::EVENT_CLOSE, $this->event->getLastTriggeredEvent()); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_CLOSE)); // We open our task $this->assertTrue($t->open(1)); - $this->assertEquals(Task::EVENT_OPEN, $this->event->getLastTriggeredEvent()); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_OPEN)); // We change the column of our task $this->assertTrue($t->move(1, 2, 1)); - $this->assertEquals(Task::EVENT_MOVE_COLUMN, $this->event->getLastTriggeredEvent()); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_COLUMN)); // We change the position of our task $this->assertTrue($t->move(1, 2, 2)); - $this->assertEquals(Task::EVENT_MOVE_POSITION, $this->event->getLastTriggeredEvent()); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_POSITION)); // We change the column and the position of our task $this->assertTrue($t->move(1, 1, 3)); - $this->assertEquals(Task::EVENT_MOVE_COLUMN, $this->event->getLastTriggeredEvent()); + $this->assertTrue($this->event->isEventTriggered(Task::EVENT_MOVE_COLUMN)); } } |