summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorNala Ginrut <nalaginrut@gmail.com>2014-06-19 15:18:13 +0800
committerNala Ginrut <nalaginrut@gmail.com>2014-06-19 15:18:13 +0800
commitbfd1db41367f7931016931a94cf1b67396481c79 (patch)
tree2d696f2d8eca9ed2e4561c61c16584952d9f7b0b /app
parentd0944e682d5a3491f72c5b566248b87fbaff032a (diff)
parentefdc959c555872677e599d2ff12e1263d719f3f2 (diff)
Merge remote-tracking branch 'upstream/master'
Diffstat (limited to 'app')
-rw-r--r--app/Action/TaskAssignCategoryColor.php85
-rw-r--r--app/Controller/Action.php1
-rw-r--r--app/Controller/Base.php26
-rw-r--r--app/Controller/Board.php76
-rw-r--r--app/Controller/Category.php1
-rw-r--r--app/Controller/Comment.php1
-rw-r--r--app/Controller/Config.php4
-rw-r--r--app/Controller/File.php12
-rw-r--r--app/Controller/Project.php18
-rw-r--r--app/Controller/Subtask.php186
-rw-r--r--app/Controller/Task.php4
-rw-r--r--app/Controller/User.php20
-rw-r--r--app/Core/Event.php38
-rw-r--r--app/Core/Request.php24
-rw-r--r--app/Core/Response.php24
-rw-r--r--app/Core/Security.php87
-rw-r--r--app/Core/Session.php23
-rw-r--r--app/Core/Translator.php10
-rw-r--r--app/Locales/de_DE/translations.php32
-rw-r--r--app/Locales/es_ES/translations.php32
-rw-r--r--app/Locales/fr_FR/translations.php32
-rw-r--r--app/Locales/pl_PL/translations.php32
-rw-r--r--app/Locales/pt_BR/translations.php32
-rw-r--r--app/Model/Acl.php1
-rw-r--r--app/Model/Action.php6
-rw-r--r--app/Model/Base.php20
-rw-r--r--app/Model/Config.php7
-rw-r--r--app/Model/File.php21
-rw-r--r--app/Model/Project.php3
-rw-r--r--app/Model/RememberMe.php8
-rw-r--r--app/Model/SubTask.php179
-rw-r--r--app/Model/Task.php15
-rw-r--r--app/Model/User.php5
-rw-r--r--app/Schema/Mysql.php21
-rw-r--r--app/Schema/Sqlite.php22
-rw-r--r--app/Templates/action_index.php2
-rw-r--r--app/Templates/action_params.php2
-rw-r--r--app/Templates/action_remove.php2
-rw-r--r--app/Templates/app_forbidden.php (renamed from app/Templates/user_forbidden.php)2
-rw-r--r--app/Templates/board_assign.php2
-rw-r--r--app/Templates/board_edit.php8
-rw-r--r--app/Templates/board_index.php2
-rw-r--r--app/Templates/board_remove.php2
-rw-r--r--app/Templates/board_show.php2
-rw-r--r--app/Templates/category_edit.php2
-rw-r--r--app/Templates/category_index.php1
-rw-r--r--app/Templates/category_remove.php2
-rw-r--r--app/Templates/comment_create.php2
-rw-r--r--app/Templates/comment_edit.php1
-rw-r--r--app/Templates/comment_remove.php4
-rw-r--r--app/Templates/config_index.php10
-rw-r--r--app/Templates/file_new.php2
-rw-r--r--app/Templates/file_remove.php2
-rw-r--r--app/Templates/file_show.php17
-rw-r--r--app/Templates/layout.php12
-rw-r--r--app/Templates/project_edit.php1
-rw-r--r--app/Templates/project_forbidden.php9
-rw-r--r--app/Templates/project_index.php4
-rw-r--r--app/Templates/project_new.php1
-rw-r--r--app/Templates/project_remove.php2
-rw-r--r--app/Templates/project_users.php4
-rw-r--r--app/Templates/subtask_create.php27
-rw-r--r--app/Templates/subtask_edit.php32
-rw-r--r--app/Templates/subtask_remove.php16
-rw-r--r--app/Templates/subtask_show.php60
-rw-r--r--app/Templates/task_close.php2
-rw-r--r--app/Templates/task_edit.php2
-rw-r--r--app/Templates/task_edit_description.php2
-rw-r--r--app/Templates/task_layout.php3
-rw-r--r--app/Templates/task_new.php2
-rw-r--r--app/Templates/task_open.php2
-rw-r--r--app/Templates/task_remove.php2
-rw-r--r--app/Templates/task_show.php23
-rw-r--r--app/Templates/task_sidebar.php1
-rw-r--r--app/Templates/user_edit.php6
-rw-r--r--app/Templates/user_login.php2
-rw-r--r--app/Templates/user_new.php2
-rw-r--r--app/Templates/user_remove.php2
-rw-r--r--app/helpers.php25
79 files changed, 1227 insertions, 192 deletions
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&amp;action=params&amp;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&amp;action=create&amp;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&amp;action=remove&amp;action_id=<?= $action['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <a href="?controller=action&amp;action=remove&amp;action_id=<?= $action['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=action&amp;action=index&amp;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&amp;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&amp;action=update&amp;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&amp;action=moveUp&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'] ?>"><?= t('Move Up') ?></a>
+ <a href="?controller=board&amp;action=moveUp&amp;project_id=<?= $project['id'] ?>&amp;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&amp;action=moveDown&amp;project_id=<?= $project['id'] ?>&amp;column_id=<?= $column['id'] ?>"><?= t('Move Down') ?></a>
+ <a href="?controller=board&amp;action=moveDown&amp;project_id=<?= $project['id'] ?>&amp;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&amp;action=add&amp;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&amp;action=remove&amp;column_id=<?= $column['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <a href="?controller=board&amp;action=remove&amp;column_id=<?= $column['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=board&amp;action=edit&amp;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&amp;action=update&amp;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&amp;action=save&amp;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&amp;action=remove&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <a href="?controller=category&amp;action=remove&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=category&amp;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&amp;action=save&amp;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&amp;action=update&amp;task_id=<?= $task['id'] ?>&amp;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&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <a href="?controller=comment&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=task&amp;action=show&amp;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&amp;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&amp;action=tokens"><?= t('Reset all tokens') ?></a></li>
+ <li><a href="?controller=config&amp;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&amp;action=downloadDb"><?= t('Download the database') ?></a>
+ <a href="?controller=config&amp;action=downloadDb<?= Helper\param_csrf() ?>"><?= t('Download the database') ?></a>
<?= t('(Gzip compressed Sqlite file)') ?>
</li>
<li>
- <a href="?controller=config&amp;action=optimizeDb"><?= t('Optimize the database') ?></a>
+ <a href="?controller=config&amp;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&amp;action=removeRememberMeToken&amp;id=<?= $session['id'] ?>"><?= t('Remove') ?></a></td>
+ <td><a href="?controller=config&amp;action=removeRememberMeToken&amp;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&amp;action=save&amp;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&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;file_id=<?= $file['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <a href="?controller=file&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;file_id=<?= $file['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=task&amp;action=show&amp;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&amp;action=download&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a>
+ <span class="task-show-file-actions">
+ <?php if ($file['is_image']): ?>
+ <a href="?controller=file&amp;action=open&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>" class="popover"><?= t('open') ?></a>,
+ <?php endif ?>
+ <a href="?controller=file&amp;action=confirm&amp;file_id=<?= $file['id'] ?>&amp;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&amp;action=logout"><?= t('Logout') ?></a>
+ <a href="?controller=user&amp;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&amp;action=update&amp;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&amp;action=disable&amp;project_id=<?= $project['id'] ?>"><?= t('Disable') ?></a>
+ <a href="?controller=project&amp;action=disable&amp;project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Disable') ?></a>
<?php else: ?>
- <a href="?controller=project&amp;action=enable&amp;project_id=<?= $project['id'] ?>"><?= t('Enable') ?></a>
+ <a href="?controller=project&amp;action=enable&amp;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&amp;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&amp;action=remove&amp;project_id=<?= $project['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <a href="?controller=project&amp;action=remove&amp;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&amp;action=allow&amp;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&amp;action=revoke&amp;project_id=<?= $project['id'] ?>&amp;user_id=<?= $user_id ?>"><?= t('revoke') ?></a>)
+ (<a href="?controller=project&amp;action=revoke&amp;project_id=<?= $project['id'] ?>&amp;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&amp;action=save&amp;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&amp;action=show&amp;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&amp;action=update&amp;task_id=<?= $task['id'] ?>&amp;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&amp;action=show&amp;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&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;subtask_id=<?= $subtask['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;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&amp;action=edit&amp;task_id=<?= $task['id'] ?>&amp;subtask_id=<?= $subtask['id'] ?>"><?= t('Edit') ?></a>
+ <?= t('or') ?>
+ <a href="?controller=subtask&amp;action=confirm&amp;task_id=<?= $task['id'] ?>&amp;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&amp;action=close&amp;task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <a href="?controller=task&amp;action=close&amp;task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=task&amp;action=show&amp;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&amp;action=update&amp;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&amp;action=saveDescription&amp;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&amp;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&amp;action=open&amp;task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <a href="?controller=task&amp;action=open&amp;task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=task&amp;action=show&amp;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&amp;action=remove&amp;task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <a href="?controller=task&amp;action=remove&amp;task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?controller=task&amp;action=show&amp;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&amp;action=download&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a>
- <span class="task-show-file-actions">
- <?php if ($file['is_image']): ?>
- <a href="?controller=file&amp;action=open&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>" class="popover"><?= t('open') ?></a>,
- <?php endif ?>
- <a href="?controller=file&amp;action=confirm&amp;file_id=<?= $file['id'] ?>&amp;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&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('Summary') ?></a></li>
<li><a href="?controller=task&amp;action=edit&amp;task_id=<?= $task['id'] ?>"><?= t('Edit the task') ?></a></li>
<li><a href="?controller=task&amp;action=editDescription&amp;task_id=<?= $task['id'] ?>"><?= t('Edit the description') ?></a></li>
+ <li><a href="?controller=subtask&amp;action=create&amp;task_id=<?= $task['id'] ?>"><?= t('Add a sub-task') ?></a></li>
<li><a href="?controller=comment&amp;action=create&amp;task_id=<?= $task['id'] ?>"><?= t('Add a comment') ?></a></li>
<li><a href="?controller=file&amp;action=create&amp;task_id=<?= $task['id'] ?>"><?= t('Attach a document') ?></a></li>
<li><a href="?controller=task&amp;action=duplicate&amp;project_id=<?= $task['project_id'] ?>&amp;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&amp;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&amp;action=google"><?= t('Link my Google Account') ?></a>
+ <a href="?controller=user&amp;action=google<?= Helper\param_csrf() ?>"><?= t('Link my Google Account') ?></a>
<?php else: ?>
- <a href="?controller=user&amp;action=unlinkGoogle"><?= t('Unlink my Google Account') ?></a>
+ <a href="?controller=user&amp;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&amp;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&amp;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&amp;action=remove&amp;user_id=<?= $user['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <a href="?controller=user&amp;action=remove&amp;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 '&amp;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');
+}