diff options
author | Frédéric Guillot <fred@kanboard.net> | 2014-08-15 17:23:41 -0700 |
---|---|---|
committer | Frédéric Guillot <fred@kanboard.net> | 2014-08-15 17:23:41 -0700 |
commit | 9eeded33f68872515954a2fc177fcb47a9273ae9 (patch) | |
tree | f3ef9507e087ca6bf3ce624232da240a8689b051 /app | |
parent | c539bdc8ab746c5afd48cf87de057dc38d50adac (diff) |
Add email notifications
Diffstat (limited to 'app')
50 files changed, 1192 insertions, 181 deletions
diff --git a/app/Controller/Base.php b/app/Controller/Base.php index 7b1cfd85..11841e09 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -2,6 +2,7 @@ namespace Controller; +use Core\Tool; use Core\Registry; use Core\Security; use Core\Translator; @@ -24,6 +25,7 @@ use Model\LastLogin; * @property \Model\GitHub $gitHub * @property \Model\LastLogin $lastLogin * @property \Model\Ldap $ldap + * @property \Model\Notification $notification * @property \Model\Project $project * @property \Model\RememberMe $rememberMe * @property \Model\ReverseProxyAuth $reverseProxyAuth @@ -93,9 +95,7 @@ abstract class Base */ public function __get($name) { - $class = '\Model\\'.ucfirst($name); - $this->registry->$name = new $class($this->registry->shared('db'), $this->registry->shared('event')); - return $this->registry->shared($name); + return Tool::loadModel($this->registry, $name); } /** @@ -157,6 +157,7 @@ abstract class Base $this->action->attachEvents(); $this->project->attachEvents(); $this->webhook->attachEvents(); + $this->notification->attachEvents(); } /** diff --git a/app/Controller/Board.php b/app/Controller/Board.php index 14b1c02c..7fe9c4ae 100644 --- a/app/Controller/Board.php +++ b/app/Controller/Board.php @@ -373,7 +373,7 @@ class Board extends Base } if (isset($values['positions'])) { - $this->board->saveTasksPosition($values['positions']); + $this->board->saveTasksPosition($values['positions'], $values['selected_task_id']); } $this->response->html( diff --git a/app/Controller/Config.php b/app/Controller/Config.php index daa57790..498f3214 100644 --- a/app/Controller/Config.php +++ b/app/Controller/Config.php @@ -20,7 +20,8 @@ class Config extends Base $this->response->html($this->template->layout('config_index', array( 'db_size' => $this->config->getDatabaseSize(), 'user' => $_SESSION['user'], - 'projects' => $this->project->getList(), + 'user_projects' => $this->project->getAvailableList($this->acl->getUserId()), + 'notifications' => $this->notification->readSettings($this->acl->getUserId()), 'languages' => $this->config->getLanguages(), 'values' => $this->config->getAll(), 'errors' => array(), @@ -32,6 +33,13 @@ class Config extends Base ))); } + public function notifications() + { + $values = $this->request->getValues(); + $this->notification->saveSettings($this->acl->getUserId(), $values); + $this->response->redirect('?controller=config#notifications'); + } + /** * Validate and save settings * @@ -57,7 +65,8 @@ class Config extends Base $this->response->html($this->template->layout('config_index', array( 'db_size' => $this->config->getDatabaseSize(), 'user' => $_SESSION['user'], - 'projects' => $this->project->getList(), + 'user_projects' => $this->project->getAvailableList($this->acl->getUserId()), + 'notifications' => $this->notification->readSettings($this->acl->getUserId()), 'languages' => $this->config->getLanguages(), 'values' => $values, 'errors' => $errors, diff --git a/app/Core/Tool.php b/app/Core/Tool.php index ade99cad..1a2e9904 100644 --- a/app/Core/Tool.php +++ b/app/Core/Tool.php @@ -31,4 +31,11 @@ class Tool fclose($fp); } } + + public static function loadModel(Registry $registry, $name) + { + $class = '\Model\\'.ucfirst($name); + $registry->$name = new $class($registry); + return $registry->shared($name); + } } diff --git a/app/Event/BaseNotificationListener.php b/app/Event/BaseNotificationListener.php new file mode 100644 index 00000000..6c1728cb --- /dev/null +++ b/app/Event/BaseNotificationListener.php @@ -0,0 +1,76 @@ +<?php + +namespace Event; + +use Core\Listener; +use Model\Notification; + +/** + * Base notification listener + * + * @package event + * @author Frederic Guillot + */ +abstract class BaseNotificationListener implements Listener +{ + /** + * Notification model + * + * @accesss protected + * @var Model\Notification + */ + protected $notification; + + /** + * Template name + * + * @accesss private + * @var string + */ + private $template = ''; + + /** + * Fetch data for the mail template + * + * @access public + * @param array $data Event data + * @return array + */ + abstract public function getTemplateData(array $data); + + /** + * Constructor + * + * @access public + * @param \Model\Notification $notification Notification model instance + * @param string $template Template name + */ + public function __construct(Notification $notification, $template) + { + $this->template = $template; + $this->notification = $notification; + } + + /** + * 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 execute(array $data) + { + $values = $this->getTemplateData($data); + + // Get the list of users to be notified + $users = $this->notification->getUsersList($values['task']['project_id']); + + // Send notifications + if ($users) { + $this->notification->sendEmails($this->template, $users, $values); + return true; + } + + return false; + } +} diff --git a/app/Event/CommentNotificationListener.php b/app/Event/CommentNotificationListener.php new file mode 100644 index 00000000..3771ea7e --- /dev/null +++ b/app/Event/CommentNotificationListener.php @@ -0,0 +1,30 @@ +<?php + +namespace Event; + +use Event\BaseNotificationListener; + +/** + * Comment notification listener + * + * @package event + * @author Frederic Guillot + */ +class CommentNotificationListener extends BaseNotificationListener +{ + /** + * Fetch data for the mail template + * + * @access public + * @param array $data Event data + * @return array + */ + public function getTemplateData(array $data) + { + $values = array(); + $values['comment'] = $this->notification->comment->getById($data['id']); + $values['task'] = $this->notification->task->getById($data['task_id'], true); + + return $values; + } +} diff --git a/app/Event/FileNotificationListener.php b/app/Event/FileNotificationListener.php new file mode 100644 index 00000000..98fc4260 --- /dev/null +++ b/app/Event/FileNotificationListener.php @@ -0,0 +1,30 @@ +<?php + +namespace Event; + +use Event\BaseNotificationListener; + +/** + * File notification listener + * + * @package event + * @author Frederic Guillot + */ +class FileNotificationListener extends BaseNotificationListener +{ + /** + * Fetch data for the mail template + * + * @access public + * @param array $data Event data + * @return array + */ + public function getTemplateData(array $data) + { + $values = array(); + $values['file'] = $data; + $values['task'] = $this->notification->task->getById($data['task_id'], true); + + return $values; + } +} diff --git a/app/Event/SubTaskNotificationListener.php b/app/Event/SubTaskNotificationListener.php new file mode 100644 index 00000000..0a239421 --- /dev/null +++ b/app/Event/SubTaskNotificationListener.php @@ -0,0 +1,30 @@ +<?php + +namespace Event; + +use Event\BaseNotificationListener; + +/** + * SubTask notification listener + * + * @package event + * @author Frederic Guillot + */ +class SubTaskNotificationListener extends BaseNotificationListener +{ + /** + * Fetch data for the mail template + * + * @access public + * @param array $data Event data + * @return array + */ + public function getTemplateData(array $data) + { + $values = array(); + $values['subtask'] = $this->notification->subtask->getById($data['id'], true); + $values['task'] = $this->notification->task->getById($data['task_id'], true); + + return $values; + } +} diff --git a/app/Event/TaskNotificationListener.php b/app/Event/TaskNotificationListener.php new file mode 100644 index 00000000..ffbe7a8c --- /dev/null +++ b/app/Event/TaskNotificationListener.php @@ -0,0 +1,29 @@ +<?php + +namespace Event; + +use Event\BaseNotificationListener; + +/** + * Task notification listener + * + * @package event + * @author Frederic Guillot + */ +class TaskNotificationListener extends BaseNotificationListener +{ + /** + * Fetch data for the mail template + * + * @access public + * @param array $data Event data + * @return array + */ + public function getTemplateData(array $data) + { + $values = array(); + $values['task'] = $this->notification->task->getById($data['task_id'], true); + + return $values; + } +} diff --git a/app/Locales/de_DE/translations.php b/app/Locales/de_DE/translations.php index 73aed160..df5a359f 100644 --- a/app/Locales/de_DE/translations.php +++ b/app/Locales/de_DE/translations.php @@ -402,4 +402,31 @@ return array( // 'Clone Project' => '', // 'Project cloned successfully.' => '', // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', ); diff --git a/app/Locales/es_ES/translations.php b/app/Locales/es_ES/translations.php index a8a8024c..b91e95c3 100644 --- a/app/Locales/es_ES/translations.php +++ b/app/Locales/es_ES/translations.php @@ -401,4 +401,31 @@ return array( // 'Clone Project' => '', // 'Project cloned successfully.' => '', // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', ); diff --git a/app/Locales/fi_FI/translations.php b/app/Locales/fi_FI/translations.php index 801ec5c1..2b228960 100644 --- a/app/Locales/fi_FI/translations.php +++ b/app/Locales/fi_FI/translations.php @@ -401,4 +401,31 @@ return array( // 'Clone Project' => '', // 'Project cloned successfully.' => '', // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', ); diff --git a/app/Locales/fr_FR/translations.php b/app/Locales/fr_FR/translations.php index 552060c5..4bb4359e 100644 --- a/app/Locales/fr_FR/translations.php +++ b/app/Locales/fr_FR/translations.php @@ -399,4 +399,31 @@ return array( 'Clone Project' => 'Cloner le projet', 'Project cloned successfully.' => 'Projet cloné avec succès.', 'Unable to clone this project.' => 'Impossible de cloner ce projet.', + 'Email notifications' => 'Notifications par email', + 'Enable email notifications' => 'Activer les notifications par emails', + 'Task position:' => 'Position de la tâche :', + 'The task #%d have been opened.' => 'La tâche #%d a été ouverte.', + 'The task #%d have been closed.' => 'La tâche #%d a été fermée.', + 'Sub-task updated' => 'Sous-tâche mise à jour', + 'Title:' => 'Titre :', + 'Status:' => 'État :', + 'Assignee:' => 'Assigné :', + 'Time tracking:' => 'Gestion du temps :', + 'New sub-task' => 'Nouvelle sous-tâche', + 'New attachment added "%s"' => 'Nouvelle pièce-jointe ajoutée « %s »', + 'Comment updated' => 'Commentaire ajouté', + 'New comment posted by %s' => 'Nouveau commentaire ajouté par « %s »', + 'List of due tasks for the project "%s"' => 'Liste des tâches expirées pour le projet « %s »', + '[%s][New attachment] %s (#%d)' => '[%s][Pièce-jointe] %s (#%d)', + '[%s][New comment] %s (#%d)' => '[%s][Nouveau commentaire] %s (#%d)', + '[%s][Comment updated] %s (#%d)' => '[%s][Commentaire mis à jour] %s (#%d)', + '[%s][New subtask] %s (#%d)' => '[%s][Nouvelle sous-tâche] %s (#%d)', + '[%s][Subtask updated] %s (#%d)' => '[%s][Sous-tâche mise à jour] %s (#%d)', + '[%s][New task] %s (#%d)' => '[%s][Nouvelle tâche] %s (#%d)', + '[%s][Task updated] %s (#%d)' => '[%s][Tâche mise à jour] %s (#%d)', + '[%s][Task closed] %s (#%d)' => '[%s][Tâche fermée] %s (#%d)', + '[%s][Task opened] %s (#%d)' => '[%s][Tâche ouverte] %s (#%d)', + '[%s][Due tasks]' => '[%s][Tâches expirées]', + '[Kanboard] Notification' => '[Kanboard] Notification', + 'I want to receive notifications only for those projects:' => 'Je souhaite reçevoir les notifications uniquement pour les projets sélectionnés :', ); diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php index 41e7f80a..53d57974 100644 --- a/app/Locales/pl_PL/translations.php +++ b/app/Locales/pl_PL/translations.php @@ -402,4 +402,31 @@ return array( // 'Clone Project' => '', // 'Project cloned successfully.' => '', // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', ); diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php index 6ebb78c9..04117b50 100644 --- a/app/Locales/pt_BR/translations.php +++ b/app/Locales/pt_BR/translations.php @@ -404,4 +404,31 @@ return array( 'Clone Project' => 'Clonar Projeto', 'Project cloned successfully.' => 'Projeto clonado com sucesso.', 'Unable to clone this project.' => 'Impossível clonar este projeto.', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', ); diff --git a/app/Locales/sv_SE/translations.php b/app/Locales/sv_SE/translations.php index 68248b72..dddccc2f 100644 --- a/app/Locales/sv_SE/translations.php +++ b/app/Locales/sv_SE/translations.php @@ -401,4 +401,31 @@ return array( // 'Clone Project' => '', // 'Project cloned successfully.' => '', // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + // 'I want to receive notifications only for those projects:' => '', ); diff --git a/app/Locales/zh_CN/translations.php b/app/Locales/zh_CN/translations.php index 1d759336..823075fe 100644 --- a/app/Locales/zh_CN/translations.php +++ b/app/Locales/zh_CN/translations.php @@ -407,4 +407,30 @@ return array( // 'Clone Project' => '', // 'Project cloned successfully.' => '', // 'Unable to clone this project.' => '', + // 'Email notifications' => '', + // 'Enable email notifications' => '', + // 'Task position:' => '', + // 'The task #%d have been opened.' => '', + // 'The task #%d have been closed.' => '', + // 'Sub-task updated' => '', + // 'Title:' => '', + // 'Status:' => '', + // 'Assignee:' => '', + // 'Time tracking:' => '', + // 'New sub-task' => '', + // 'New attachment added "%s"' => '', + // 'Comment updated' => '', + // 'New comment posted by %s' => '', + // 'List of due tasks for the project "%s"' => '', + // '[%s][New attachment] %s (#%d)' => '', + // '[%s][New comment] %s (#%d)' => '', + // '[%s][Comment updated] %s (#%d)' => '', + // '[%s][New subtask] %s (#%d)' => '', + // '[%s][Subtask updated] %s (#%d)' => '', + // '[%s][New task] %s (#%d)' => '', + // '[%s][Task updated] %s (#%d)' => '', + // '[%s][Task closed] %s (#%d)' => '', + // '[%s][Task opened] %s (#%d)' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', ); diff --git a/app/Model/Acl.php b/app/Model/Acl.php index 8a87a6b2..c4b31079 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -33,7 +33,7 @@ class Acl extends Base 'board' => array('index', 'show', 'assign', 'assigntask', 'save', 'check'), 'project' => array('tasks', 'index', 'forbidden', 'search'), 'user' => array('index', 'edit', 'update', 'forbidden', 'logout', 'index', 'unlinkgoogle', 'unlinkgithub'), - 'config' => array('index', 'removeremembermetoken'), + 'config' => array('index', 'removeremembermetoken', 'notifications'), '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'), diff --git a/app/Model/Action.php b/app/Model/Action.php index 25e72f58..effe8707 100644 --- a/app/Model/Action.php +++ b/app/Model/Action.php @@ -224,25 +224,25 @@ class Action extends Base switch ($name) { case 'TaskClose': $className = '\Action\TaskClose'; - return new $className($project_id, new Task($this->db, $this->event)); + return new $className($project_id, new Task($this->registry)); case 'TaskAssignCurrentUser': $className = '\Action\TaskAssignCurrentUser'; - return new $className($project_id, new Task($this->db, $this->event), new Acl($this->db, $this->event)); + return new $className($project_id, new Task($this->registry), new Acl($this->registry)); case 'TaskAssignSpecificUser': $className = '\Action\TaskAssignSpecificUser'; - return new $className($project_id, new Task($this->db, $this->event)); + return new $className($project_id, new Task($this->registry)); case 'TaskDuplicateAnotherProject': $className = '\Action\TaskDuplicateAnotherProject'; - return new $className($project_id, new Task($this->db, $this->event)); + return new $className($project_id, new Task($this->registry)); case 'TaskAssignColorUser': $className = '\Action\TaskAssignColorUser'; - return new $className($project_id, new Task($this->db, $this->event)); + return new $className($project_id, new Task($this->registry)); case 'TaskAssignColorCategory': $className = '\Action\TaskAssignColorCategory'; - return new $className($project_id, new Task($this->db, $this->event)); + return new $className($project_id, new Task($this->registry)); case 'TaskAssignCategoryColor': $className = '\Action\TaskAssignCategoryColor'; - return new $className($project_id, new Task($this->db, $this->event)); + return new $className($project_id, new Task($this->registry)); default: throw new LogicException('Action not found: '.$name); } diff --git a/app/Model/Base.php b/app/Model/Base.php index 66185aeb..92578ffc 100644 --- a/app/Model/Base.php +++ b/app/Model/Base.php @@ -17,6 +17,8 @@ require __DIR__.'/../../vendor/SimpleValidator/Validators/Email.php'; require __DIR__.'/../../vendor/SimpleValidator/Validators/Numeric.php'; use Core\Event; +use Core\Tool; +use Core\Registry; use PicoDb\Database; /** @@ -24,6 +26,21 @@ use PicoDb\Database; * * @package model * @author Frederic Guillot + * + * @property \Model\Acl $acl + * @property \Model\Action $action + * @property \Model\Board $board + * @property \Model\Category $category + * @property \Model\Comment $comment + * @property \Model\Config $config + * @property \Model\File $file + * @property \Model\LastLogin $lastLogin + * @property \Model\Ldap $ldap + * @property \Model\Notification $notification + * @property \Model\Project $project + * @property \Model\SubTask $subTask + * @property \Model\Task $task + * @property \Model\User $user */ abstract class Base { @@ -44,15 +61,35 @@ abstract class Base protected $event; /** + * Registry instance + * + * @access protected + * @var \Core\Registry + */ + protected $registry; + + /** * Constructor * * @access public - * @param \PicoDb\Database $db Database instance - * @param \Core\Event $event Event dispatcher instance + * @param \Core\Registry $registry Registry instance + */ + public function __construct(Registry $registry) + { + $this->registry = $registry; + $this->db = $this->registry->shared('db'); + $this->event = $this->registry->shared('event'); + } + + /** + * Load automatically models + * + * @access public + * @param string $name Model name + * @return mixed */ - public function __construct(Database $db, Event $event) + public function __get($name) { - $this->db = $db; - $this->event = $event; + return Tool::loadModel($this->registry, $name); } } diff --git a/app/Model/Board.php b/app/Model/Board.php index a4e0a345..168b185f 100644 --- a/app/Model/Board.php +++ b/app/Model/Board.php @@ -24,17 +24,18 @@ class Board extends Base * Save task positions for each column * * @access public - * @param array $values [['task_id' => X, 'column_id' => X, 'position' => X], ...] + * @param array $positions [['task_id' => X, 'column_id' => X, 'position' => X], ...] + * @param integer $selected_task_id The selected task id * @return boolean */ - public function saveTasksPosition(array $values) + public function saveTasksPosition(array $positions, $selected_task_id) { - $taskModel = new Task($this->db, $this->event); - $this->db->startTransaction(); - foreach ($values as $value) { - if (! $taskModel->move($value['task_id'], $value['column_id'], $value['position'])) { + foreach ($positions as $value) { + + // We trigger events only for the selected task + if (! $this->task->move($value['task_id'], $value['column_id'], $value['position'], $value['task_id'] == $selected_task_id)) { $this->db->cancelTransaction(); return false; } @@ -201,8 +202,7 @@ class Board extends Base $filters[] = array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id); $filters[] = array('column' => 'is_active', 'operator' => 'eq', 'value' => Task::STATUS_OPEN); - $taskModel = new Task($this->db, $this->event); - $tasks = $taskModel->find($filters); + $tasks = $this->task->find($filters); foreach ($columns as &$column) { diff --git a/app/Model/Comment.php b/app/Model/Comment.php index b5102070..b849fc23 100644 --- a/app/Model/Comment.php +++ b/app/Model/Comment.php @@ -21,6 +21,14 @@ class Comment extends Base const TABLE = 'comments'; /** + * Events + * + * @var string + */ + const EVENT_UPDATE = 'comment.update'; + const EVENT_CREATE = 'comment.create'; + + /** * Get all comments for a given task * * @access public @@ -95,7 +103,14 @@ class Comment extends Base { $values['date'] = time(); - return $this->db->table(self::TABLE)->save($values); + if ($this->db->table(self::TABLE)->save($values)) { + + $values['id'] = $this->db->getConnection()->getLastId(); + $this->event->trigger(self::EVENT_CREATE, $values); + return true; + } + + return false; } /** @@ -107,10 +122,14 @@ class Comment extends Base */ public function update(array $values) { - return $this->db + $result = $this->db ->table(self::TABLE) ->eq('id', $values['id']) ->update(array('comment' => $values['comment'])); + + $this->event->trigger(self::EVENT_UPDATE, $values); + + return $result; } /** diff --git a/app/Model/File.php b/app/Model/File.php index 2a793217..d5a0c7cd 100644 --- a/app/Model/File.php +++ b/app/Model/File.php @@ -25,6 +25,13 @@ class File extends Base const BASE_PATH = 'data/files/'; /** + * Events + * + * @var string + */ + const EVENT_CREATE = 'file.create'; + + /** * Get a file by the id * * @access public @@ -82,6 +89,8 @@ class File extends Base */ public function create($task_id, $name, $path, $is_image) { + $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id, 'name' => $name)); + return $this->db->table(self::TABLE)->save(array( 'task_id' => $task_id, 'name' => $name, diff --git a/app/Model/GitHub.php b/app/Model/GitHub.php index 3380218d..bf4f4c51 100644 --- a/app/Model/GitHub.php +++ b/app/Model/GitHub.php @@ -26,22 +26,19 @@ class GitHub extends Base */
public function authenticate($github_id)
{
- $userModel = new User($this->db, $this->event);
-
- $user = $userModel->getByGitHubId($github_id);
+ $user = $this->user->getByGitHubId($github_id);
if ($user) {
// Create the user session
- $userModel->updateSession($user);
+ $this->user->updateSession($user);
// Update login history
- $lastLogin = new LastLogin($this->db, $this->event);
- $lastLogin->create(
+ $this->lastLogin->create(
LastLogin::AUTH_GITHUB,
$user['id'],
- $userModel->getIpAddress(),
- $userModel->getUserAgent()
+ $this->user->getIpAddress(),
+ $this->user->getUserAgent()
);
return true;
@@ -59,9 +56,7 @@ class GitHub extends Base */
public function unlink($user_id)
{
- $userModel = new User($this->db, $this->event);
-
- return $userModel->update(array(
+ return $this->user->update(array(
'id' => $user_id,
'github_id' => '',
));
@@ -78,9 +73,7 @@ class GitHub extends Base */
public function updateUser($user_id, array $profile)
{
- $userModel = new User($this->db, $this->event);
-
- return $userModel->update(array(
+ return $this->user->update(array(
'id' => $user_id,
'github_id' => $profile['id'],
'email' => $profile['email'],
@@ -141,7 +134,7 @@ class GitHub extends Base try {
$gitHubService = $this->getService();
$gitHubService->requestAccessToken($code);
-
+
return json_decode($gitHubService->request('user'), true);
}
catch (TokenResponseException $e) {
@@ -150,7 +143,7 @@ class GitHub extends Base return false;
}
-
+
/**
* Revokes this user's GitHub tokens for Kanboard
*
diff --git a/app/Model/Google.php b/app/Model/Google.php index f5beb8f8..cca4f668 100644 --- a/app/Model/Google.php +++ b/app/Model/Google.php @@ -27,21 +27,19 @@ class Google extends Base */ public function authenticate($google_id) { - $userModel = new User($this->db, $this->event); - $user = $userModel->getByGoogleId($google_id); + $user = $this->user->getByGoogleId($google_id); if ($user) { // Create the user session - $userModel->updateSession($user); + $this->user->updateSession($user); // Update login history - $lastLogin = new LastLogin($this->db, $this->event); - $lastLogin->create( + $this->lastLogin->create( LastLogin::AUTH_GOOGLE, $user['id'], - $userModel->getIpAddress(), - $userModel->getUserAgent() + $this->user->getIpAddress(), + $this->user->getUserAgent() ); return true; @@ -59,9 +57,7 @@ class Google extends Base */ public function unlink($user_id) { - $userModel = new User($this->db, $this->event); - - return $userModel->update(array( + return $this->user->update(array( 'id' => $user_id, 'google_id' => '', )); @@ -77,9 +73,7 @@ class Google extends Base */ public function updateUser($user_id, array $profile) { - $userModel = new User($this->db, $this->event); - - return $userModel->update(array( + return $this->user->update(array( 'id' => $user_id, 'google_id' => $profile['id'], 'email' => $profile['email'], diff --git a/app/Model/Ldap.php b/app/Model/Ldap.php index dabcd5ff..007f7171 100644 --- a/app/Model/Ldap.php +++ b/app/Model/Ldap.php @@ -73,8 +73,7 @@ class Ldap extends Base */ public function create($username, $name, $email) { - $userModel = new User($this->db, $this->event); - $user = $userModel->getByUsername($username); + $user = $this->user->getByUsername($username); // There is an existing user account if ($user) { diff --git a/app/Model/Notification.php b/app/Model/Notification.php new file mode 100644 index 00000000..e0f932a4 --- /dev/null +++ b/app/Model/Notification.php @@ -0,0 +1,215 @@ +<?php + +namespace Model; + +use Core\Template; +use Event\TaskNotificationListener; +use Event\CommentNotificationListener; +use Event\FileNotificationListener; +use Event\SubTaskNotificationListener; +use Swift_Message; +use Swift_Mailer; + +/** + * Notification model + * + * @package model + * @author Frederic Guillot + */ +class Notification extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'user_has_notifications'; + + /** + * Get the list of users to send the notification for a given project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getUsersList($project_id) + { + $users = $this->db->table(User::TABLE) + ->columns('id', 'username', 'name', 'email') + ->eq('notifications_enabled', '1') + ->neq('email', '') + ->findAll(); + + foreach ($users as $index => $user) { + + $projects = $this->db->table(self::TABLE) + ->eq('user_id', $user['id']) + ->findAllByColumn('project_id'); + + // The user have selected only some projects + if (! empty($projects)) { + + // If the user didn't select this project we remove that guy from the list + if (! in_array($project_id, $projects)) { + unset($users[$index]); + } + } + } + + return $users; + } + + /** + * Attach events + * + * @access public + */ + public function attachEvents() + { + $this->event->attach(File::EVENT_CREATE, new FileNotificationListener($this, 'notification_file_creation')); + + $this->event->attach(Comment::EVENT_CREATE, new CommentNotificationListener($this, 'notification_comment_creation')); + $this->event->attach(Comment::EVENT_UPDATE, new CommentNotificationListener($this, 'notification_comment_update')); + + $this->event->attach(SubTask::EVENT_CREATE, new SubTaskNotificationListener($this, 'notification_subtask_creation')); + $this->event->attach(SubTask::EVENT_UPDATE, new SubTaskNotificationListener($this, 'notification_subtask_update')); + + $this->event->attach(Task::EVENT_CREATE, new TaskNotificationListener($this, 'notification_task_creation')); + $this->event->attach(Task::EVENT_UPDATE, new TaskNotificationListener($this, 'notification_task_update')); + $this->event->attach(Task::EVENT_CLOSE, new TaskNotificationListener($this, 'notification_task_close')); + $this->event->attach(Task::EVENT_OPEN, new TaskNotificationListener($this, 'notification_task_open')); + } + + /** + * Send the email notifications + * + * @access public + * @param string $template Template name + * @param array $users List of users + * @param array $data Template data + */ + public function sendEmails($template, array $users, array $data) + { + $transport = $this->registry->shared('mailer'); + $mailer = Swift_Mailer::newInstance($transport); + + $message = Swift_Message::newInstance() + ->setSubject($this->getMailSubject($template, $data)) + ->setFrom(array(MAIL_FROM => 'Kanboard')) + //->setTo(array($user['email'] => $user['name'])) + ->setBody($this->getMailContent($template, $data), 'text/html'); + + foreach ($users as $user) { + $message->setTo(array($user['email'] => $user['name'] ?: $user['username'])); + $mailer->send($message); + } + } + + /** + * Get the mail subject for a given template name + * + * @access public + * @param string $template Template name + * @param array $data Template data + */ + public function getMailSubject($template, array $data) + { + switch ($template) { + case 'notification_file_creation': + return t('[%s][New attachment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case 'notification_comment_creation': + return t('[%s][New comment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case 'notification_comment_update': + return t('[%s][Comment updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case 'notification_subtask_creation': + return t('[%s][New subtask] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case 'notification_subtask_update': + return t('[%s][Subtask updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case 'notification_task_creation': + return t('[%s][New task] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case 'notification_task_update': + return t('[%s][Task updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case 'notification_task_close': + return t('[%s][Task closed] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case 'notification_task_open': + return t('[%s][Task opened] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case 'notification_task_due': + return t('[%s][Due tasks]', $data['project']); + } + + return t('[Kanboard] Notification'); + } + + /** + * Get the mail content for a given template name + * + * @access public + * @param string $template Template name + * @param array $data Template data + */ + public function getMailContent($template, array $data) + { + $tpl = new Template; + return $tpl->load($template, $data); + } + + /** + * Save settings for the given user + * + * @access public + * @param integer $user_id User id + * @param array $values Form values + */ + public function saveSettings($user_id, array $values) + { + // Delete all selected projects + $this->db->table(self::TABLE)->eq('user_id', $user_id)->remove(); + + if (isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1) { + + // Activate notifications + $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + 'notifications_enabled' => '1' + )); + + // Save selected projects + if (! empty($values['projects'])) { + + foreach ($values['projects'] as $project_id => $checkbox_value) { + $this->db->table(self::TABLE)->insert(array( + 'user_id' => $user_id, + 'project_id' => $project_id, + )); + } + } + } + else { + + // Disable notifications + $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + 'notifications_enabled' => '0' + )); + } + } + + /** + * Read user settings to display the form + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function readSettings($user_id) + { + $values = array(); + $values['notifications_enabled'] = $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('notifications_enabled'); + + $projects = $this->db->table(self::TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id'); + + foreach ($projects as $project_id) { + $values['project_'.$project_id] = true; + } + + return $values; + } +} diff --git a/app/Model/Project.php b/app/Model/Project.php index f598c96f..63458fa3 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -55,10 +55,9 @@ class Project extends Base public function getUsersList($project_id, $prepend_unassigned = true, $prepend_everybody = false) { $allowed_users = $this->getAllowedUsers($project_id); - $userModel = new User($this->db, $this->event); if (empty($allowed_users)) { - $allowed_users = $userModel->getList(); + $allowed_users = $this->user->getList(); } if ($prepend_unassigned) { @@ -103,8 +102,7 @@ class Project extends Base 'not_allowed' => array(), ); - $userModel = new User($this->db, $this->event); - $all_users = $userModel->getList(); + $all_users = $this->user->getList(); $users['allowed'] = $this->getAllowedUsers($project_id); @@ -253,27 +251,23 @@ class Project extends Base ->asc('name') ->findAll(); - $boardModel = new Board($this->db, $this->event); - $taskModel = new Task($this->db, $this->event); - $aclModel = new Acl($this->db, $this->event); - foreach ($projects as $pkey => &$project) { - if ($check_permissions && ! $this->isUserAllowed($project['id'], $aclModel->getUserId())) { + if ($check_permissions && ! $this->isUserAllowed($project['id'], $this->acl->getUserId())) { unset($projects[$pkey]); } else { - $columns = $boardModel->getcolumns($project['id']); + $columns = $this->board->getcolumns($project['id']); $project['nb_active_tasks'] = 0; foreach ($columns as &$column) { - $column['nb_active_tasks'] = $taskModel->countByColumnId($project['id'], $column['id']); + $column['nb_active_tasks'] = $this->task->countByColumnId($project['id'], $column['id']); $project['nb_active_tasks'] += $column['nb_active_tasks']; } $project['columns'] = $columns; - $project['nb_tasks'] = $taskModel->countByProjectId($project['id']); + $project['nb_tasks'] = $this->task->countByProjectId($project['id']); $project['nb_inactive_tasks'] = $project['nb_tasks'] - $project['nb_active_tasks']; } } @@ -416,9 +410,8 @@ class Project extends Base */ public function copyBoardFromAnotherProject($project_from, $project_to) { - $boardModel = new Board($this->db, $this->event); $columns = $this->db->table(Board::TABLE)->eq('project_id', $project_from)->asc('position')->findAllByColumn('title'); - return $boardModel->create($project_to, $columns); + return $this->board->create($project_to, $columns); } /** @@ -431,8 +424,7 @@ class Project extends Base */ public function copyCategoriesFromAnotherProject($project_from, $project_to) { - $categoryModel = new Category($this->db, $this->event); - $categoriesTemplate = $categoryModel->getAll($project_from); + $categoriesTemplate = $this->category->getAll($project_from); foreach ($categoriesTemplate as $category) { @@ -478,8 +470,7 @@ class Project extends Base */ public function copyActionsFromAnotherProject($project_from, $project_to) { - $actionModel = new Action($this->db, $this->event); - $actionTemplate = $actionModel->getAllByProject($project_from); + $actionTemplate = $this->action->getAllByProject($project_from); foreach ($actionTemplate as $action) { @@ -522,13 +513,11 @@ class Project extends Base case 'project_id': return $project_to; case 'category_id': - $categoryModel = new Category($this->db, $this->event); - $categoryTemplate = $categoryModel->getById($param['value']); + $categoryTemplate = $this->category->getById($param['value']); $categoryFromNewProject = $this->db->table(Category::TABLE)->eq('project_id', $project_to)->eq('name', $categoryTemplate['name'])->findOne(); return $categoryFromNewProject['id']; case 'column_id': - $boardModel = new Board($this->db, $this->event); - $boardTemplate = $boardModel->getColumn($param['value']); + $boardTemplate = $this->board->getColumn($param['value']); $boardFromNewProject = $this->db->table(Board::TABLE)->eq('project_id', $project_to)->eq('title', $boardTemplate['title'])->findOne(); return $boardFromNewProject['id']; default: @@ -603,8 +592,7 @@ class Project extends Base $project_id = $this->db->getConnection()->getLastId(); - $boardModel = new Board($this->db, $this->event); - $boardModel->create($project_id, array( + $this->board->create($project_id, array( t('Backlog'), t('Ready'), t('Work in progress'), diff --git a/app/Model/RememberMe.php b/app/Model/RememberMe.php index 272b4916..e23ed887 100644 --- a/app/Model/RememberMe.php +++ b/app/Model/RememberMe.php @@ -92,11 +92,8 @@ class RememberMe extends Base ); // Create the session - $user = new User($this->db, $this->event); - $acl = new Acl($this->db, $this->event); - - $user->updateSession($user->getById($record['user_id'])); - $acl->isRememberMe(true); + $this->user->updateSession($this->user->getById($record['user_id'])); + $this->acl->isRememberMe(true); return true; } diff --git a/app/Model/ReverseProxyAuth.php b/app/Model/ReverseProxyAuth.php index 1b9ed06c..14d18ba3 100644 --- a/app/Model/ReverseProxyAuth.php +++ b/app/Model/ReverseProxyAuth.php @@ -23,24 +23,22 @@ class ReverseProxyAuth extends Base if (isset($_SERVER[REVERSE_PROXY_USER_HEADER])) { $login = $_SERVER[REVERSE_PROXY_USER_HEADER]; - $userModel = new User($this->db, $this->event); - $user = $userModel->getByUsername($login); + $user = $this->user->getByUsername($login); if (! $user) { $this->createUser($login); - $user = $userModel->getByUsername($login); + $user = $this->user->getByUsername($login); } // Create the user session - $userModel->updateSession($user); + $this->user->updateSession($user); // Update login history - $lastLogin = new LastLogin($this->db, $this->event); - $lastLogin->create( + $this->lastLogin->create( LastLogin::AUTH_REVERSE_PROXY, $user['id'], - $userModel->getIpAddress(), - $userModel->getUserAgent() + $this->user->getIpAddress(), + $this->user->getUserAgent() ); return true; @@ -58,9 +56,7 @@ class ReverseProxyAuth extends Base */ private function createUser($login) { - $userModel = new User($this->db, $this->event); - - return $userModel->create(array( + return $this->user->create(array( 'email' => strpos($login, '@') !== false ? $login : '', 'username' => $login, 'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login, diff --git a/app/Model/SubTask.php b/app/Model/SubTask.php index 21ccdaac..c7bab44b 100644 --- a/app/Model/SubTask.php +++ b/app/Model/SubTask.php @@ -42,6 +42,14 @@ class SubTask extends Base const STATUS_TODO = 0; /** + * Events + * + * @var string + */ + const EVENT_UPDATE = 'subtask.update'; + const EVENT_CREATE = 'subtask.create'; + + /** * Get available status * * @access public @@ -88,10 +96,27 @@ class SubTask extends Base * * @access public * @param integer $subtask_id Subtask id + * @param bool $more Fetch more data * @return array */ - public function getById($subtask_id) + public function getById($subtask_id, $more = false) { + if ($more) { + + $subtask = $this->db->table(self::TABLE) + ->eq(self::TABLE.'.id', $subtask_id) + ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') + ->join(User::TABLE, 'id', 'user_id') + ->findOne(); + + if ($subtask) { + $status = $this->getStatusList(); + $subtask['status_name'] = $status[$subtask['status']]; + } + + return $subtask; + } + return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne(); } @@ -116,7 +141,14 @@ class SubTask extends Base $values['time_spent'] = 0; } - return $this->db->table(self::TABLE)->save($values); + $result = $this->db->table(self::TABLE)->save($values); + + if ($result) { + $values['id'] = $this->db->getConnection()->getLastId(); + $this->event->trigger(self::EVENT_CREATE, $values); + } + + return $result; } /** @@ -136,7 +168,13 @@ class SubTask extends Base $values['time_spent'] = 0; } - return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); + $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); + + if ($result) { + $this->event->trigger(self::EVENT_UPDATE, $values); + } + + return $result; } /** diff --git a/app/Model/Task.php b/app/Model/Task.php index 0b2b9cf9..6647f041 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -63,6 +63,35 @@ class Task extends Base } /** + * Get a list of due tasks for all projects + * + * @access public + * @return array + */ + public function getTasksDue() + { + $tasks = $this->db->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.title', + self::TABLE.'.date_due', + self::TABLE.'.project_id', + Project::TABLE.'.name AS project_name', + User::TABLE.'.username AS assignee_username', + User::TABLE.'.name AS assignee_name' + ) + ->join(Project::TABLE, 'id', 'project_id') + ->join(User::TABLE, 'id', 'owner_id') + ->eq(Project::TABLE.'.is_active', 1) + ->eq(self::TABLE.'.is_active', 1) + ->neq(self::TABLE.'.date_due', '') + ->lte(self::TABLE.'.date_due', mktime(23, 59, 59)) + ->findAll(); + + return $tasks; + } + + /** * Fetch one task * * @access public @@ -182,7 +211,7 @@ class Task extends Base 'tasks.category_id', 'users.username' ) - ->join('users', 'id', 'owner_id'); + ->join(User::TABLE, 'id', 'owner_id'); foreach ($filters as $key => $filter) { @@ -282,8 +311,6 @@ class Task extends Base { $this->db->startTransaction(); - $boardModel = new Board($this->db, $this->event); - // Get the original task $task = $this->getById($task_id); @@ -296,7 +323,7 @@ class Task extends Base $task['owner_id'] = 0; $task['category_id'] = 0; $task['is_active'] = 1; - $task['column_id'] = $boardModel->getFirstColumn($project_id); + $task['column_id'] = $this->board->getFirstColumn($project_id); $task['project_id'] = $project_id; $task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']); @@ -318,17 +345,13 @@ class Task extends Base } /** - * Create a task + * Prepare data before task creation or modification * * @access public - * @param array $values Form values - * @return boolean + * @param array $values Form values */ - public function create(array $values) + public function prepare(array &$values) { - $this->db->startTransaction(); - - // Prepare data if (isset($values['another_task'])) { unset($values['another_task']); } @@ -336,14 +359,30 @@ class Task extends Base if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) { $values['date_due'] = $this->parseDate($values['date_due']); } - else { + + // Force integer fields at 0 (for Postgresql) + if (isset($values['date_due']) && empty($values['date_due'])) { $values['date_due'] = 0; } - if (empty($values['score'])) { + if (isset($values['score']) && empty($values['score'])) { $values['score'] = 0; } + } + /** + * Create a task + * + * @access public + * @param array $values Form values + * @return boolean + */ + public function create(array $values) + { + $this->db->startTransaction(); + + // Prepare data + $this->prepare($values); $values['date_creation'] = time(); $values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']); @@ -368,31 +407,21 @@ class Task extends Base * Update a task * * @access public - * @param array $values Form values + * @param array $values Form values + * @param boolean $trigger_events Flag to trigger events * @return boolean */ - public function update(array $values) + public function update(array $values, $trigger_events = true) { - // Prepare data - if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) { - $values['date_due'] = $this->parseDate($values['date_due']); - } - - // Force integer fields at 0 (for Postgresql) - if (isset($values['date_due']) && empty($values['date_due'])) { - $values['date_due'] = 0; - } - - if (isset($values['score']) && empty($values['score'])) { - $values['score'] = 0; - } - + // Fetch original task $original_task = $this->getById($values['id']); if ($original_task === false) { return false; } + // Prepare data + $this->prepare($values); $updated_task = $values; $updated_task['date_modification'] = time(); unset($updated_task['id']); @@ -400,29 +429,40 @@ class Task extends Base $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task); // Trigger events - if ($result) { - - $events = array( - self::EVENT_CREATE_UPDATE, - self::EVENT_UPDATE, - ); + if ($result && $trigger_events) { + $this->triggerUpdateEvents($original_task, $updated_task); + } - if (isset($values['column_id']) && $original_task['column_id'] != $values['column_id']) { - $events[] = self::EVENT_MOVE_COLUMN; - } - else if (isset($values['position']) && $original_task['position'] != $values['position']) { - $events[] = self::EVENT_MOVE_POSITION; - } + return $result; + } - $event_data = array_merge($original_task, $values); - $event_data['task_id'] = $original_task['id']; + /** + * Trigger events for task modification + * + * @access public + * @param array $original_task Original task data + * @param array $updated_task Updated task data + */ + public function triggerUpdateEvents(array $original_task, array $updated_task) + { + $events = array( + self::EVENT_CREATE_UPDATE, + self::EVENT_UPDATE, + ); - foreach ($events as $event) { - $this->event->trigger($event, $event_data); - } + if (isset($updated_task['column_id']) && $original_task['column_id'] != $updated_task['column_id']) { + $events[] = self::EVENT_MOVE_COLUMN; + } + else if (isset($updated_task['position']) && $original_task['position'] != $updated_task['position']) { + $events[] = self::EVENT_MOVE_POSITION; } - return $result; + $event_data = array_merge($original_task, $updated_task); + $event_data['task_id'] = $original_task['id']; + + foreach ($events as $event) { + $this->event->trigger($event, $event_data); + } } /** @@ -482,8 +522,7 @@ class Task extends Base */ public function remove($task_id) { - $file = new File($this->db, $this->event); - $file->removeAll($task_id); + $this->file->removeAll($task_id); return $this->db->table(self::TABLE)->eq('id', $task_id)->remove(); } @@ -492,20 +531,23 @@ class Task extends Base * Move a task to another column or to another position * * @access public - * @param integer $task_id Task id - * @param integer $column_id Column id - * @param integer $position Position (must be greater than 1) + * @param integer $task_id Task id + * @param integer $column_id Column id + * @param integer $position Position (must be greater than 1) + * @param boolean $trigger_events Flag to trigger events * @return boolean */ - public function move($task_id, $column_id, $position) + public function move($task_id, $column_id, $position, $trigger_events = true) { $this->event->clearTriggeredEvents(); - return $this->update(array( + $values = array( 'id' => $task_id, 'column_id' => $column_id, 'position' => $position, - )); + ); + + return $this->update($values, $trigger_events); } /** diff --git a/app/Model/User.php b/app/Model/User.php index b5744c44..d0e33fd0 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -340,8 +340,7 @@ class User extends Base $this->updateSession($user); // Update login history - $lastLogin = new LastLogin($this->db, $this->event); - $lastLogin->create( + $this->lastLogin->create( $method, $user['id'], $this->getIpAddress(), @@ -350,9 +349,8 @@ class User extends Base // Setup the remember me feature if (! empty($values['remember_me'])) { - $rememberMe = new RememberMe($this->db, $this->event); - $credentials = $rememberMe->create($user['id'], $this->getIpAddress(), $this->getUserAgent()); - $rememberMe->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']); + $credentials = $this->rememberMe->create($user['id'], $this->getIpAddress(), $this->getUserAgent()); + $this->rememberMe->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']); } } else { @@ -384,8 +382,7 @@ class User extends Base // LDAP authentication if (! $authenticated && LDAP_AUTH) { - $ldap = new Ldap($this->db, $this->event); - $authenticated = $ldap->authenticate($username, $password); + $authenticated = $this->ldap->authenticate($username, $password); $method = LastLogin::AUTH_LDAP; } diff --git a/app/Model/Webhook.php b/app/Model/Webhook.php index 679d3edc..872031cc 100644 --- a/app/Model/Webhook.php +++ b/app/Model/Webhook.php @@ -64,11 +64,9 @@ class Webhook extends Base */ public function attachEvents() { - $config = new Config($this->db, $this->event); - - $this->url_task_creation = $config->get('webhooks_url_task_creation'); - $this->url_task_modification = $config->get('webhooks_url_task_modification'); - $this->token = $config->get('webhooks_token'); + $this->url_task_creation = $this->config->get('webhooks_url_task_creation'); + $this->url_task_modification = $this->config->get('webhooks_url_task_modification'); + $this->token = $this->config->get('webhooks_token'); if ($this->url_task_creation) { $this->attachCreateEvents(); diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 46fc6d43..8f3ae5a1 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -4,7 +4,22 @@ namespace Schema; use Core\Security; -const VERSION = 22; +const VERSION = 23; + +function version_23($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN notifications_enabled TINYINT(1) DEFAULT '0'"); + + $pdo->exec(" + CREATE TABLE user_has_notifications ( + user_id INT, + project_id INT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE(project_id, user_id) + ); + "); +} function version_22($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index a9eea531..ce77a4ed 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -4,7 +4,22 @@ namespace Schema; use Core\Security; -const VERSION = 3; +const VERSION = 4; + +function version_4($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN notifications_enabled BOOLEAN DEFAULT '0'"); + + $pdo->exec(" + CREATE TABLE user_has_notifications ( + user_id INTEGER, + project_id INTEGER, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE(project_id, user_id) + ); + "); +} function version_3($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 4660251f..c3a3f10e 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -4,7 +4,22 @@ namespace Schema; use Core\Security; -const VERSION = 22; +const VERSION = 23; + +function version_23($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN notifications_enabled INTEGER DEFAULT '0'"); + + $pdo->exec(" + CREATE TABLE user_has_notifications ( + user_id INTEGER, + project_id INTEGER, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE(project_id, user_id) + ); + "); +} function version_22($pdo) { diff --git a/app/Templates/comment_edit.php b/app/Templates/comment_edit.php index 52695f00..4ce48964 100644 --- a/app/Templates/comment_edit.php +++ b/app/Templates/comment_edit.php @@ -6,6 +6,7 @@ <?= Helper\form_csrf() ?> <?= Helper\form_hidden('id', $values) ?> + <?= Helper\form_hidden('task_id', $values) ?> <?= Helper\form_textarea('comment', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/> <div class="form-actions"> diff --git a/app/Templates/config_index.php b/app/Templates/config_index.php index 91919776..11662c87 100644 --- a/app/Templates/config_index.php +++ b/app/Templates/config_index.php @@ -31,14 +31,25 @@ <div class="page-header"> <h2><?= t('User settings') ?></h2> </div> - <section class="settings"> - <ul> - <li> - <strong><?= t('My default project:') ?> </strong> - <?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? Helper\escape($projects[$user['default_project_id']]) : t('None') ?>, - <a href="?controller=user&action=edit&user_id=<?= $user['id'] ?>"><?= t('edit') ?></a> - </li> - </ul> + <section> + <h3 id="notifications"><?= t('Email notifications') ?></h3> + <form method="post" action="?controller=config&action=notifications" autocomplete="off"> + + <?= Helper\form_csrf() ?> + + <?= Helper\form_checkbox('notifications_enabled', t('Enable email notifications'), '1', $notifications['notifications_enabled'] == 1) ?><br/> + + <p><?= t('I want to receive notifications only for those projects:') ?><br/><br/></p> + + <div class="form-checkbox-group"> + <?php foreach ($user_projects as $project_id => $project_name): ?> + <?= Helper\form_checkbox('projects['.$project_id.']', $project_name, '1', isset($notifications['project_'.$project_id])) ?> + <?php endforeach ?> + </div> + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </form> </section> <?php if ($user['is_admin']): ?> diff --git a/app/Templates/notification_comment_creation.php b/app/Templates/notification_comment_creation.php new file mode 100644 index 00000000..ff286790 --- /dev/null +++ b/app/Templates/notification_comment_creation.php @@ -0,0 +1,8 @@ +<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('New comment posted by %s', $comment['username']) ?></h3> + +<?= Helper\parse($comment['comment']) ?> + +<hr/> +<p>Kanboard</p>
\ No newline at end of file diff --git a/app/Templates/notification_comment_update.php b/app/Templates/notification_comment_update.php new file mode 100644 index 00000000..9dbd2332 --- /dev/null +++ b/app/Templates/notification_comment_update.php @@ -0,0 +1,8 @@ +<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('Comment updated') ?></h3> + +<?= Helper\parse($comment['comment']) ?> + +<hr/> +<p>Kanboard</p>
\ No newline at end of file diff --git a/app/Templates/notification_file_creation.php b/app/Templates/notification_file_creation.php new file mode 100644 index 00000000..57d69f51 --- /dev/null +++ b/app/Templates/notification_file_creation.php @@ -0,0 +1,6 @@ +<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('New attachment added "%s"', $file['name']) ?></h3> + +<hr/> +<p>Kanboard</p>
\ No newline at end of file diff --git a/app/Templates/notification_subtask_creation.php b/app/Templates/notification_subtask_creation.php new file mode 100644 index 00000000..e02ceaf0 --- /dev/null +++ b/app/Templates/notification_subtask_creation.php @@ -0,0 +1,18 @@ +<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('New sub-task') ?></h3> + +<ul> + <li><?= t('Title:') ?> <?= Helper\escape($subtask['title']) ?></li> + <li><?= t('Status:') ?> <?= Helper\escape($subtask['status_name']) ?></li> + <li><?= t('Assignee:') ?> <?= Helper\escape($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> + <li> + <?= t('Time tracking:') ?> + <?php if (! empty($subtask['time_estimated'])): ?> + <strong><?= Helper\escape($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <?php endif ?> + </li> +</ul> + +<hr/> +<p>Kanboard</p> diff --git a/app/Templates/notification_subtask_update.php b/app/Templates/notification_subtask_update.php new file mode 100644 index 00000000..5ec5ed05 --- /dev/null +++ b/app/Templates/notification_subtask_update.php @@ -0,0 +1,22 @@ +<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('Sub-task updated') ?></h3> + +<ul> + <li><?= t('Title:') ?> <?= Helper\escape($subtask['title']) ?></li> + <li><?= t('Status:') ?> <?= Helper\escape($subtask['status_name']) ?></li> + <li><?= t('Assignee:') ?> <?= Helper\escape($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> + <li> + <?= t('Time tracking:') ?> + <?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 ?> + </li> +</ul> + +<hr/> +<p>Kanboard</p>
\ No newline at end of file diff --git a/app/Templates/notification_task_close.php b/app/Templates/notification_task_close.php new file mode 100644 index 00000000..8b8cedb3 --- /dev/null +++ b/app/Templates/notification_task_close.php @@ -0,0 +1,6 @@ +<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<p><?= t('The task #%d have been closed.', $task['id']) ?></p> + +<hr/> +<p>Kanboard</p>
\ No newline at end of file diff --git a/app/Templates/notification_task_creation.php b/app/Templates/notification_task_creation.php new file mode 100644 index 00000000..9515e889 --- /dev/null +++ b/app/Templates/notification_task_creation.php @@ -0,0 +1,44 @@ +<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<ul> + <li> + <?= dt('Created on %B %e, %Y at %k:%M %p', $task['date_creation']) ?> + </li> + <?php if ($task['date_due']): ?> + <li> + <strong><?= dt('Must be done before %B %e, %Y', $task['date_due']) ?></strong> + </li> + <?php endif ?> + <?php if ($task['creator_username']): ?> + <li> + <?= t('Created by %s', $task['creator_username']) ?> + </li> + <?php endif ?> + <li> + <strong> + <?php if ($task['assignee_username']): ?> + <?= t('Assigned to %s', $task['assignee_username']) ?> + <?php else: ?> + <?= t('There is nobody assigned') ?> + <?php endif ?> + </strong> + </li> + <li> + <?= t('Column on the board:') ?> + <strong><?= Helper\escape($task['column_title']) ?></strong> + </li> + <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li> + <?php if ($task['category_name']): ?> + <li> + <?= t('Category:') ?> <strong><?= Helper\escape($task['category_name']) ?></strong> + </li> + <?php endif ?> +</ul> + +<?php if (! empty($task['description'])): ?> + <h2><?= t('Description') ?></h2> + <?= Helper\parse($task['description']) ?> +<?php endif ?> + +<hr/> +<p>Kanboard</p>
\ No newline at end of file diff --git a/app/Templates/notification_task_due.php b/app/Templates/notification_task_due.php new file mode 100644 index 00000000..1686c7de --- /dev/null +++ b/app/Templates/notification_task_due.php @@ -0,0 +1,10 @@ +<h2><?= t('List of due tasks for the project "%s"', $project) ?></h2> + +<ul> + <?php foreach ($tasks as $task): ?> + <li>(<strong>#<?= $task['id'] ?></strong>) <?= Helper\escape($task['title']) ?> (<strong><?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?></strong>)</li> + <?php endforeach ?> +</ul> + +<hr/> +<p>Kanboard</p>
\ No newline at end of file diff --git a/app/Templates/notification_task_open.php b/app/Templates/notification_task_open.php new file mode 100644 index 00000000..2ef0f04f --- /dev/null +++ b/app/Templates/notification_task_open.php @@ -0,0 +1,6 @@ +<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<p><?= t('The task #%d have been opened.', $task['id']) ?></p> + +<hr/> +<p>Kanboard</p>
\ No newline at end of file diff --git a/app/Templates/notification_task_update.php b/app/Templates/notification_task_update.php new file mode 100644 index 00000000..28be9db2 --- /dev/null +++ b/app/Templates/notification_task_update.php @@ -0,0 +1,44 @@ +<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<ul> + <li> + <?= dt('Created on %B %e, %Y at %k:%M %p', $task['date_creation']) ?> + </li> + <?php if ($task['date_due']): ?> + <li> + <strong><?= dt('Must be done before %B %e, %Y', $task['date_due']) ?></strong> + </li> + <?php endif ?> + <?php if ($task['creator_username']): ?> + <li> + <?= t('Created by %s', $task['creator_username']) ?> + </li> + <?php endif ?> + <li> + <strong> + <?php if ($task['assignee_username']): ?> + <?= t('Assigned to %s', $task['assignee_username']) ?> + <?php else: ?> + <?= t('There is nobody assigned') ?> + <?php endif ?> + </strong> + </li> + <li> + <?= t('Column on the board:') ?> + <strong><?= Helper\escape($task['column_title']) ?></strong> + </li> + <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li> + <?php if ($task['category_name']): ?> + <li> + <?= t('Category:') ?> <strong><?= Helper\escape($task['category_name']) ?></strong> + </li> + <?php endif ?> +</ul> + +<?php if (! empty($task['description'])): ?> + <h2><?= t('Description') ?></h2> + <?= Helper\parse($task['description']) ?: t('There is no description.') ?> +<?php endif ?> + +<hr/> +<p>Kanboard</p>
\ No newline at end of file diff --git a/app/common.php b/app/common.php index 312b930b..9ce0016a 100644 --- a/app/common.php +++ b/app/common.php @@ -4,6 +4,8 @@ require __DIR__.'/Core/Loader.php'; require __DIR__.'/helpers.php'; require __DIR__.'/translator.php'; +require 'vendor/swiftmailer/swift_required.php'; + use Core\Event; use Core\Loader; use Core\Registry; @@ -63,6 +65,15 @@ defined('REVERSE_PROXY_AUTH') or define('REVERSE_PROXY_AUTH', false); defined('REVERSE_PROXY_USER_HEADER') or define('REVERSE_PROXY_USER_HEADER', 'REMOTE_USER'); defined('REVERSE_PROXY_DEFAULT_ADMIN') or define('REVERSE_PROXY_DEFAULT_ADMIN', ''); +// Mail configuration +defined('MAIL_FROM') or define('MAIL_FROM', 'notifications@kanboard.net'); +defined('MAIL_TRANSPORT') or define('MAIL_TRANSPORT', 'mail'); +defined('MAIL_SMTP_HOSTNAME') or define('MAIL_SMTP_HOSTNAME', ''); +defined('MAIL_SMTP_PORT') or define('MAIL_SMTP_PORT', 25); +defined('MAIL_SMTP_USERNAME') or define('MAIL_SMTP_USERNAME', ''); +defined('MAIL_SMTP_PASSWORD') or define('MAIL_SMTP_PASSWORD', ''); +defined('MAIL_SENDMAIL_COMMAND') or define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'); + $loader = new Loader; $loader->execute(); @@ -126,3 +137,25 @@ $registry->db = function() use ($registry) { $registry->event = function() use ($registry) { return new Event; }; + +$registry->mailer = function() use ($registry) { + + require_once 'vendor/swiftmailer/swift_required.php'; + + $transport = null; + + switch (MAIL_TRANSPORT) { + case 'smtp': + $transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT); + $transport->setUsername(MAIL_SMTP_USERNAME); + $transport->setPassword(MAIL_SMTP_PASSWORD); + break; + case 'sendmail': + $transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND); + break; + default: + $transport = Swift_MailTransport::newInstance(); + } + + return $transport; +}; |