diff options
author | Frédéric Guillot <fred@kanboard.net> | 2014-05-22 12:28:28 -0400 |
---|---|---|
committer | Frédéric Guillot <fred@kanboard.net> | 2014-05-22 12:28:28 -0400 |
commit | 2230dd4e6b148346c0ec596b9e3e12996a762ed8 (patch) | |
tree | ef99ccde4f8b18592a3fb06a6ec45162c501fe38 /app/Model | |
parent | a750b8ab2a0cb715da6fd9025a7ec8375db68a4d (diff) |
Code refactoring (add autoloader and change files organization)
Diffstat (limited to 'app/Model')
-rw-r--r-- | app/Model/Acl.php | 159 | ||||
-rw-r--r-- | app/Model/Action.php | 267 | ||||
-rw-r--r-- | app/Model/Base.php | 76 | ||||
-rw-r--r-- | app/Model/Board.php | 340 | ||||
-rw-r--r-- | app/Model/Category.php | 150 | ||||
-rw-r--r-- | app/Model/Comment.php | 171 | ||||
-rw-r--r-- | app/Model/Config.php | 182 | ||||
-rw-r--r-- | app/Model/Google.php | 152 | ||||
-rw-r--r-- | app/Model/LastLogin.php | 91 | ||||
-rw-r--r-- | app/Model/Ldap.php | 79 | ||||
-rw-r--r-- | app/Model/Project.php | 558 | ||||
-rw-r--r-- | app/Model/RememberMe.php | 333 | ||||
-rw-r--r-- | app/Model/Task.php | 627 | ||||
-rw-r--r-- | app/Model/User.php | 426 |
14 files changed, 3611 insertions, 0 deletions
diff --git a/app/Model/Acl.php b/app/Model/Acl.php new file mode 100644 index 00000000..ad2118f4 --- /dev/null +++ b/app/Model/Acl.php @@ -0,0 +1,159 @@ +<?php + +namespace Model; + +/** + * Acl model + * + * @package model + * @author Frederic Guillot + */ +class Acl extends Base +{ + /** + * Controllers and actions allowed from outside + * + * @access private + * @var array + */ + private $public_actions = array( + 'user' => array('login', 'check', 'google'), + 'task' => array('add'), + 'board' => array('readonly'), + ); + + /** + * Controllers and actions allowed for regular users + * + * @access private + * @var array + */ + private $user_actions = array( + 'app' => array('index'), + 'board' => array('index', 'show', 'assign', 'assigntask', 'save', 'check'), + 'project' => array('tasks', 'index', 'forbidden', 'search'), + 'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'confirmclose', 'open', 'confirmopen', 'description', 'duplicate', 'remove', 'confirmremove'), + 'comment' => array('save', 'confirm', 'remove', 'update', 'edit'), + 'user' => array('index', 'edit', 'update', 'forbidden', 'logout', 'index', 'unlinkgoogle'), + 'config' => array('index', 'removeremembermetoken'), + ); + + /** + * Return true if the specified controller/action is allowed according to the given acl + * + * @access public + * @param array $acl Acl list + * @param string $controller Controller name + * @param string $action Action name + * @return bool + */ + public function isAllowedAction(array $acl, $controller, $action) + { + if (isset($acl[$controller])) { + return in_array($action, $acl[$controller]); + } + + return false; + } + + /** + * Return true if the given action is public + * + * @access public + * @param string $controller Controller name + * @param string $action Action name + * @return bool + */ + public function isPublicAction($controller, $action) + { + return $this->isAllowedAction($this->public_actions, $controller, $action); + } + + /** + * Return true if the given action is allowed for a regular user + * + * @access public + * @param string $controller Controller name + * @param string $action Action name + * @return bool + */ + public function isUserAction($controller, $action) + { + return $this->isAllowedAction($this->user_actions, $controller, $action); + } + + /** + * Return true if the logged user is admin + * + * @access public + * @return bool + */ + public function isAdminUser() + { + return isset($_SESSION['user']['is_admin']) && $_SESSION['user']['is_admin'] === true; + } + + /** + * Return true if the logged user is not admin + * + * @access public + * @return bool + */ + public function isRegularUser() + { + return isset($_SESSION['user']['is_admin']) && $_SESSION['user']['is_admin'] === false; + } + + /** + * Get the connected user id + * + * @access public + * @return integer + */ + public function getUserId() + { + return isset($_SESSION['user']['id']) ? (int) $_SESSION['user']['id'] : 0; + } + + /** + * Check is the user is connected + * + * @access public + * @return bool + */ + public function isLogged() + { + return ! empty($_SESSION['user']); + } + + /** + * Check is the user was authenticated with the RememberMe or set the value + * + * @access public + * @param bool $value Set true if the user use the RememberMe + * @return bool + */ + public function isRememberMe($value = null) + { + if ($value !== null) { + $_SESSION['is_remember_me'] = $value; + } + + return empty($_SESSION['is_remember_me']) ? false : $_SESSION['is_remember_me']; + } + + /** + * Check if an action is allowed for the logged user + * + * @access public + * @param string $controller Controller name + * @param string $action Action name + * @return bool + */ + public function isPageAccessAllowed($controller, $action) + { + return $this->isPublicAction($controller, $action) || + $this->isAdminUser() || + ($this->isRegularUser() && $this->isUserAction($controller, $action)); + } +} diff --git a/app/Model/Action.php b/app/Model/Action.php new file mode 100644 index 00000000..d1b97ebc --- /dev/null +++ b/app/Model/Action.php @@ -0,0 +1,267 @@ +<?php + +namespace Model; + +use LogicException; +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Action model + * + * @package model + * @author Frederic Guillot + */ +class Action extends Base +{ + /** + * SQL table name for actions + * + * @var string + */ + const TABLE = 'actions'; + + /** + * SQL table name for action parameters + * + * @var string + */ + const TABLE_PARAMS = 'action_has_params'; + + /** + * Return the name and description of available actions + * + * @access public + * @return array + */ + public function getAvailableActions() + { + return array( + 'TaskClose' => t('Close the task'), + 'TaskAssignSpecificUser' => t('Assign the task to a specific user'), + 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'), + 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'), + 'TaskAssignColorUser' => t('Assign a color to a specific user'), + 'TaskAssignColorCategory' => t('Assign a color to a specific category'), + ); + } + + /** + * Return the name and description of available actions + * + * @access public + * @return array + */ + public function getAvailableEvents() + { + return array( + Task::EVENT_MOVE_COLUMN => t('Move a task to another column'), + Task::EVENT_MOVE_POSITION => t('Move a task to another position in the same column'), + Task::EVENT_UPDATE => t('Task modification'), + Task::EVENT_CREATE => t('Task creation'), + Task::EVENT_OPEN => t('Open a closed task'), + Task::EVENT_CLOSE => t('Closing a task'), + Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), + ); + } + + /** + * Return actions and parameters for a given project + * + * @access public + * @return array + */ + public function getAllByProject($project_id) + { + $actions = $this->db->table(self::TABLE)->eq('project_id', $project_id)->findAll(); + + foreach ($actions as &$action) { + $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll(); + } + + return $actions; + } + + /** + * Return all actions and parameters + * + * @access public + * @return array + */ + public function getAll() + { + $actions = $this->db->table(self::TABLE)->findAll(); + + foreach ($actions as &$action) { + $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll(); + } + + return $actions; + } + + /** + * Get all required action parameters for all registered actions + * + * @access public + * @return array All required parameters for all actions + */ + public function getAllActionParameters() + { + $params = array(); + + foreach ($this->getAll() as $action) { + + $action = $this->load($action['action_name'], $action['project_id']); + $params += $action->getActionRequiredParameters(); + } + + return $params; + } + + /** + * Fetch an action + * + * @access public + * @param integer $action_id Action id + * @return array Action data + */ + public function getById($action_id) + { + $action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne(); + $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action_id)->findAll(); + + return $action; + } + + /** + * Remove an action + * + * @access public + * @param integer $action_id Action id + * @return bool Success or not + */ + public function remove($action_id) + { + return $this->db->table(self::TABLE)->eq('id', $action_id)->remove(); + } + + /** + * Create an action + * + * @access public + * @param array $values Required parameters to save an action + * @return bool Success or not + */ + public function create(array $values) + { + $this->db->startTransaction(); + + $action = array( + 'project_id' => $values['project_id'], + 'event_name' => $values['event_name'], + 'action_name' => $values['action_name'], + ); + + if (! $this->db->table(self::TABLE)->save($action)) { + $this->db->cancelTransaction(); + return false; + } + + $action_id = $this->db->getConnection()->getLastId(); + + foreach ($values['params'] as $param_name => $param_value) { + + $action_param = array( + 'action_id' => $action_id, + 'name' => $param_name, + 'value' => $param_value, + ); + + if (! $this->db->table(self::TABLE_PARAMS)->save($action_param)) { + $this->db->cancelTransaction(); + return false; + } + } + + $this->db->closeTransaction(); + + return true; + } + + /** + * Load all actions and attach events + * + * @access public + */ + public function attachEvents() + { + foreach ($this->getAll() as $action) { + + $listener = $this->load($action['action_name'], $action['project_id']); + + foreach ($action['params'] as $param) { + $listener->setParam($param['name'], $param['value']); + } + + $this->event->attach($action['event_name'], $listener); + } + } + + /** + * Load an action + * + * @access public + * @param string $name Action class name + * @param integer $project_id Project id + * @return mixed Action Instance + * @throw LogicException + */ + public function load($name, $project_id) + { + switch ($name) { + case 'TaskClose': + $className = '\Action\TaskClose'; + return new $className($project_id, new Task($this->db, $this->event)); + case 'TaskAssignCurrentUser': + $className = '\Action\TaskAssignCurrentUser'; + return new $className($project_id, new Task($this->db, $this->event), new Acl($this->db, $this->event)); + case 'TaskAssignSpecificUser': + $className = '\Action\TaskAssignSpecificUser'; + return new $className($project_id, new Task($this->db, $this->event)); + case 'TaskDuplicateAnotherProject': + $className = '\Action\TaskDuplicateAnotherProject'; + return new $className($project_id, new Task($this->db, $this->event)); + case 'TaskAssignColorUser': + $className = '\Action\TaskAssignColorUser'; + return new $className($project_id, new Task($this->db, $this->event)); + case 'TaskAssignColorCategory': + $className = '\Action\TaskAssignColorCategory'; + return new $className($project_id, new Task($this->db, $this->event)); + default: + throw new LogicException('Action not found: '.$name); + } + } + + /** + * Validate action creation + * + * @access public + * @param array $values Required parameters to save an action + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('project_id', t('The project id is required')), + new Validators\Integer('project_id', t('This value must be an integer')), + new Validators\Required('event_name', t('This value is required')), + new Validators\Required('action_name', t('This value is required')), + new Validators\Required('params', t('This value is required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/Base.php b/app/Model/Base.php new file mode 100644 index 00000000..fef2ddbb --- /dev/null +++ b/app/Model/Base.php @@ -0,0 +1,76 @@ +<?php + +namespace Model; + +require __DIR__.'/../../vendor/SimpleValidator/Validator.php'; +require __DIR__.'/../../vendor/SimpleValidator/Base.php'; +require __DIR__.'/../../vendor/SimpleValidator/Validators/Required.php'; +require __DIR__.'/../../vendor/SimpleValidator/Validators/Unique.php'; +require __DIR__.'/../../vendor/SimpleValidator/Validators/MaxLength.php'; +require __DIR__.'/../../vendor/SimpleValidator/Validators/MinLength.php'; +require __DIR__.'/../../vendor/SimpleValidator/Validators/Integer.php'; +require __DIR__.'/../../vendor/SimpleValidator/Validators/Equals.php'; +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'; + +use Core\Event; +use PicoDb\Database; + +/** + * Base model class + * + * @package model + * @author Frederic Guillot + */ +abstract class Base +{ + /** + * Database instance + * + * @access protected + * @var PicoDb + */ + protected $db; + + /** + * Event dispatcher instance + * + * @access protected + * @var Core\Event + */ + protected $event; + + /** + * Constructor + * + * @access public + * @param \PicoDb\Database $db Database instance + * @param \Core\Event $event Event dispatcher instance + */ + public function __construct(Database $db, Event $event) + { + $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/Board.php b/app/Model/Board.php new file mode 100644 index 00000000..59a98923 --- /dev/null +++ b/app/Model/Board.php @@ -0,0 +1,340 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Board model + * + * @package model + * @author Frederic Guillot + */ +class Board extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'columns'; + + /** + * Save task positions for each column + * + * @access public + * @param array $values [['task_id' => X, 'column_id' => X, 'position' => X], ...] + * @return boolean + */ + public function saveTasksPosition(array $values) + { + $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'])) { + $this->db->cancelTransaction(); + return false; + } + } + + $this->db->closeTransaction(); + + return true; + } + + /** + * Create a board with default columns, must be executed inside a transaction + * + * @access public + * @param integer $project_id Project id + * @param array $columns List of columns title ['column1', 'column2', ...] + * @return boolean + */ + public function create($project_id, array $columns) + { + $position = 0; + + foreach ($columns as $title) { + + $values = array( + 'title' => $title, + 'position' => ++$position, + 'project_id' => $project_id, + ); + + if (! $this->db->table(self::TABLE)->save($values)) { + return false; + } + } + + return true; + } + + /** + * Add a new column to the board + * + * @access public + * @param array $values ['title' => X, 'project_id' => X] + * @return boolean + */ + public function add(array $values) + { + $values['position'] = $this->getLastColumnPosition($values['project_id']) + 1; + return $this->db->table(self::TABLE)->save($values); + } + + /** + * Update columns + * + * @access public + * @param array $values Form values + * @return boolean + */ + public function update(array $values) + { + $this->db->startTransaction(); + + foreach (array('title', 'task_limit') as $field) { + foreach ($values[$field] as $column_id => $field_value) { + $this->db->table(self::TABLE)->eq('id', $column_id)->update(array($field => $field_value)); + } + } + + $this->db->closeTransaction(); + + return true; + } + + /** + * Move a column down, increment the column position value + * + * @access public + * @param integer $project_id Project id + * @param integer $column_id Column id + * @return boolean + */ + public function moveDown($project_id, $column_id) + { + $columns = $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'position'); + $positions = array_flip($columns); + + if (isset($columns[$column_id]) && $columns[$column_id] < count($columns)) { + + $position = ++$columns[$column_id]; + $columns[$positions[$position]]--; + + $this->db->startTransaction(); + $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position)); + $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $columns[$positions[$position]])); + $this->db->closeTransaction(); + + return true; + } + + return false; + } + + /** + * Move a column up, decrement the column position value + * + * @access public + * @param integer $project_id Project id + * @param integer $column_id Column id + * @return boolean + */ + public function moveUp($project_id, $column_id) + { + $columns = $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'position'); + $positions = array_flip($columns); + + if (isset($columns[$column_id]) && $columns[$column_id] > 1) { + + $position = --$columns[$column_id]; + $columns[$positions[$position]]++; + + $this->db->startTransaction(); + $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position)); + $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $columns[$positions[$position]])); + $this->db->closeTransaction(); + + return true; + } + + return false; + } + + /** + * Get all columns and tasks for a given project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function get($project_id, array $filters = array()) + { + $this->db->startTransaction(); + + $columns = $this->getColumns($project_id); + + $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); + + foreach ($columns as &$column) { + + $column['tasks'] = array(); + + foreach ($tasks as &$task) { + if ($task['column_id'] == $column['id']) { + $column['tasks'][] = $task; + } + } + } + + $this->db->closeTransaction(); + + return $columns; + } + + /** + * Get the first column id for a given project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function getFirstColumn($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findOneColumn('id'); + } + + /** + * Get the list of columns sorted by position [ column_id => title ] + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getColumnsList($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'title'); + } + + /** + * Get all columns sorted by position for a given project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getColumns($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->findAll(); + } + + /** + * Get the number of columns for a given project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function countColumns($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->count(); + } + + /** + * Get a column by the id + * + * @access public + * @param integer $column_id Column id + * @return array + */ + public function getColumn($column_id) + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne(); + } + + /** + * Get the position of the last column for a given project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function getLastColumnPosition($project_id) + { + return (int) $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->desc('position') + ->findOneColumn('position'); + } + + /** + * Remove a column and all tasks associated to this column + * + * @access public + * @param integer $column_id Column id + * @return boolean + */ + public function removeColumn($column_id) + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->remove(); + } + + /** + * Validate column modification + * + * @access public + * @param array $columns Original columns List + * @param array $values Required parameters to update a column + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $columns, array $values) + { + $rules = array(); + + foreach ($columns as $column_id => $column_title) { + $rules[] = new Validators\Integer('task_limit['.$column_id.']', t('This value must be an integer')); + $rules[] = new Validators\GreaterThan('task_limit['.$column_id.']', t('This value must be greater than %d', 0), 0); + $rules[] = new Validators\Required('title['.$column_id.']', t('The title is required')); + $rules[] = new Validators\MaxLength('title['.$column_id.']', t('The maximum length is %d characters', 50), 50); + } + + $v = new Validator($values, $rules); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate column creation + * + * @access public + * @param array $values Required parameters to save an action + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('project_id', t('The project id is required')), + new Validators\Integer('project_id', t('This value must be an integer')), + new Validators\Required('title', t('The title is required')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/Category.php b/app/Model/Category.php new file mode 100644 index 00000000..9be37f9d --- /dev/null +++ b/app/Model/Category.php @@ -0,0 +1,150 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Category model + * + * @package model + * @author Frederic Guillot + */ +class Category extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'project_has_categories'; + + /** + * Get a category by the id + * + * @access public + * @param integer $category_id Category id + * @return array + */ + public function getById($category_id) + { + return $this->db->table(self::TABLE)->eq('id', $category_id)->findOne(); + } + + /** + * Return the list of all categories + * + * @access public + * @param integer $project_id Project id + * @param bool $prepend_none If true, prepend to the list the value 'None' + * @param bool $prepend_all If true, prepend to the list the value 'All' + * @return array + */ + public function getList($project_id, $prepend_none = true, $prepend_all = false) + { + $listing = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->asc('name') + ->listing('id', 'name'); + + $prepend = array(); + + if ($prepend_all) { + $prepend[-1] = t('All categories'); + } + + if ($prepend_none) { + $prepend[0] = t('No category'); + } + + return $prepend + $listing; + } + + /** + * Create a category + * + * @access public + * @param array $values Form values + * @return bool + */ + public function create(array $values) + { + return $this->db->table(self::TABLE)->save($values); + } + + /** + * Update a category + * + * @access public + * @param array $values Form values + * @return bool + */ + public function update(array $values) + { + return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); + } + + /** + * Remove a category + * + * @access public + * @param integer $category_id Category id + * @return bool + */ + public function remove($category_id) + { + $this->db->startTransaction(); + $r1 = $this->db->table(Task::TABLE)->eq('category_id', $category_id)->update(array('category_id' => 0)); + $r2 = $this->db->table(self::TABLE)->eq('id', $category_id)->remove(); + $this->db->closeTransaction(); + + return $r1 && $r2; + } + + /** + * Validate category creation + * + * @access public + * @param array $array Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('project_id', t('The project id is required')), + new Validators\Integer('project_id', t('The project id must be an integer')), + new Validators\Required('name', t('The name is required')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate category modification + * + * @access public + * @param array $array Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $v = new Validator($values, array( + new Validators\Required('id', t('The id is required')), + new Validators\Integer('id', t('The id must be an integer')), + new Validators\Required('project_id', t('The project id is required')), + new Validators\Integer('project_id', t('The project id must be an integer')), + new Validators\Required('name', t('The name is required')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/Comment.php b/app/Model/Comment.php new file mode 100644 index 00000000..b5102070 --- /dev/null +++ b/app/Model/Comment.php @@ -0,0 +1,171 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Comment model + * + * @package model + * @author Frederic Guillot + */ +class Comment extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'comments'; + + /** + * Get all comments for a given task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getAll($task_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.date', + self::TABLE.'.task_id', + self::TABLE.'.user_id', + self::TABLE.'.comment', + User::TABLE.'.username' + ) + ->join(User::TABLE, 'id', 'user_id') + ->orderBy(self::TABLE.'.date', 'ASC') + ->eq(self::TABLE.'.task_id', $task_id) + ->findAll(); + } + + /** + * Get a comment + * + * @access public + * @param integer $comment_id Comment id + * @return array + */ + public function getById($comment_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.task_id', + self::TABLE.'.user_id', + self::TABLE.'.date', + self::TABLE.'.comment', + User::TABLE.'.username' + ) + ->join(User::TABLE, 'id', 'user_id') + ->eq(self::TABLE.'.id', $comment_id) + ->findOne(); + } + + /** + * Get the number of comments for a given task + * + * @access public + * @param integer $task_id Task id + * @return integer + */ + public function count($task_id) + { + return $this->db + ->table(self::TABLE) + ->eq(self::TABLE.'.task_id', $task_id) + ->count(); + } + + /** + * Save a comment in the database + * + * @access public + * @param array $values Form values + * @return boolean + */ + public function create(array $values) + { + $values['date'] = time(); + + return $this->db->table(self::TABLE)->save($values); + } + + /** + * Update a comment in the database + * + * @access public + * @param array $values Form values + * @return boolean + */ + public function update(array $values) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $values['id']) + ->update(array('comment' => $values['comment'])); + } + + /** + * Remove a comment + * + * @access public + * @param integer $comment_id Comment id + * @return boolean + */ + public function remove($comment_id) + { + return $this->db->table(self::TABLE)->eq('id', $comment_id)->remove(); + } + + /** + * Validate comment creation + * + * @access public + * @param array $values Required parameters to save an action + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('task_id', t('This value is required')), + new Validators\Integer('task_id', t('This value must be an integer')), + new Validators\Required('user_id', t('This value is required')), + new Validators\Integer('user_id', t('This value must be an integer')), + new Validators\Required('comment', t('Comment is required')) + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate comment modification + * + * @access public + * @param array $values Required parameters to save an action + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $v = new Validator($values, array( + new Validators\Required('id', t('This value is required')), + new Validators\Integer('id', t('This value must be an integer')), + new Validators\Required('comment', t('Comment is required')) + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/Config.php b/app/Model/Config.php new file mode 100644 index 00000000..994f0bc8 --- /dev/null +++ b/app/Model/Config.php @@ -0,0 +1,182 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Config model + * + * @package model + * @author Frederic Guillot + */ +class Config extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'config'; + + /** + * Get available timezones + * + * @access public + * @return array + */ + public function getTimezones() + { + $timezones = \timezone_identifiers_list(); + return array_combine(array_values($timezones), $timezones); + } + + /** + * Get available languages + * + * @access public + * @return array + */ + public function getLanguages() + { + $languages = array( + 'en_US' => t('English'), + 'es_ES' => t('Spanish'), + 'fr_FR' => t('French'), + 'pl_PL' => t('Polish'), + 'pt_BR' => t('Portuguese (Brazilian)'), + ); + + asort($languages); + + return $languages; + } + + /** + * Get a config variable from the session or the database + * + * @access public + * @param string $name Parameter name + * @param string $default_value Default value of the parameter + * @return mixed + */ + public function get($name, $default_value = '') + { + if (! isset($_SESSION['config'][$name])) { + $_SESSION['config'] = $this->getAll(); + } + + if (isset($_SESSION['config'][$name])) { + return $_SESSION['config'][$name]; + } + + return $default_value; + } + + /** + * Get all settings + * + * @access public + * @return array + */ + public function getAll() + { + return $this->db->table(self::TABLE)->findOne(); + } + + /** + * Save settings in the database + * + * @access public + * @param $values array Settings values + * @return boolean + */ + public function save(array $values) + { + $_SESSION['config'] = $values; + return $this->db->table(self::TABLE)->update($values); + } + + /** + * Reload settings in the session and the translations + * + * @access public + */ + public function reload() + { + $_SESSION['config'] = $this->getAll(); + + $language = $this->get('language', 'en_US'); + if ($language !== 'en_US') \Translator\load($language); + } + + /** + * Validate settings modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $v = new Validator($values, array( + new Validators\Required('language', t('The language is required')), + new Validators\Required('timezone', t('The timezone is required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Optimize the Sqlite database + * + * @access public + * @return boolean + */ + public function optimizeDatabase() + { + return $this->db->getconnection()->exec("VACUUM"); + } + + /** + * Compress the Sqlite database + * + * @access public + * @return string + */ + public function downloadDatabase() + { + return gzencode(file_get_contents(DB_FILENAME)); + } + + /** + * Get the Sqlite database size in bytes + * + * @access public + * @return integer + */ + public function getDatabaseSize() + { + return DB_DRIVER === 'sqlite' ? filesize(DB_FILENAME) : 0; + } + + /** + * Regenerate all tokens (projects and webhooks) + * + * @access public + */ + public function regenerateTokens() + { + $this->db->table(self::TABLE)->update(array('webhooks_token' => $this->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())); + } + } +} diff --git a/app/Model/Google.php b/app/Model/Google.php new file mode 100644 index 00000000..f5beb8f8 --- /dev/null +++ b/app/Model/Google.php @@ -0,0 +1,152 @@ +<?php + +namespace Model; + +require __DIR__.'/../../vendor/OAuth/bootstrap.php'; + +use OAuth\Common\Storage\Session; +use OAuth\Common\Consumer\Credentials; +use OAuth\Common\Http\Uri\UriFactory; +use OAuth\ServiceFactory; +use OAuth\Common\Http\Exception\TokenResponseException; + +/** + * Google model + * + * @package model + * @author Frederic Guillot + */ +class Google extends Base +{ + /** + * Authenticate a Google user + * + * @access public + * @param string $google_id Google unique id + * @return boolean + */ + public function authenticate($google_id) + { + $userModel = new User($this->db, $this->event); + $user = $userModel->getByGoogleId($google_id); + + if ($user) { + + // Create the user session + $userModel->updateSession($user); + + // Update login history + $lastLogin = new LastLogin($this->db, $this->event); + $lastLogin->create( + LastLogin::AUTH_GOOGLE, + $user['id'], + $userModel->getIpAddress(), + $userModel->getUserAgent() + ); + + return true; + } + + return false; + } + + /** + * Unlink a Google account for a given user + * + * @access public + * @param integer $user_id User id + * @return boolean + */ + public function unlink($user_id) + { + $userModel = new User($this->db, $this->event); + + return $userModel->update(array( + 'id' => $user_id, + 'google_id' => '', + )); + } + + /** + * Update the user table based on the Google profile information + * + * @access public + * @param integer $user_id User id + * @param array $profile Google profile + * @return boolean + */ + public function updateUser($user_id, array $profile) + { + $userModel = new User($this->db, $this->event); + + return $userModel->update(array( + 'id' => $user_id, + 'google_id' => $profile['id'], + 'email' => $profile['email'], + 'name' => $profile['name'], + )); + } + + /** + * Get the Google service instance + * + * @access public + * @return \OAuth\OAuth2\Service\Google + */ + public function getService() + { + $uriFactory = new UriFactory(); + $currentUri = $uriFactory->createFromSuperGlobalArray($_SERVER); + $currentUri->setQuery('controller=user&action=google'); + + $storage = new Session(false); + + $credentials = new Credentials( + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + $currentUri->getAbsoluteUri() + ); + + $serviceFactory = new ServiceFactory(); + + return $serviceFactory->createService( + 'google', + $credentials, + $storage, + array('userinfo_email', 'userinfo_profile') + ); + } + + /** + * Get the authorization URL + * + * @access public + * @return \OAuth\Common\Http\Uri\Uri + */ + public function getAuthorizationUrl() + { + return $this->getService()->getAuthorizationUri(); + } + + /** + * Get Google profile information from the API + * + * @access public + * @param string $code Google authorization code + * @return bool|array + */ + public function getGoogleProfile($code) + { + try { + + $googleService = $this->getService(); + $googleService->requestAccessToken($code); + return json_decode($googleService->request('https://www.googleapis.com/oauth2/v1/userinfo'), true); + } + catch (TokenResponseException $e) { + return false; + } + + return false; + } +} diff --git a/app/Model/LastLogin.php b/app/Model/LastLogin.php new file mode 100644 index 00000000..56739b48 --- /dev/null +++ b/app/Model/LastLogin.php @@ -0,0 +1,91 @@ +<?php + +namespace Model; + +/** + * LastLogin model + * + * @package model + * @author Frederic Guillot + */ +class LastLogin extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'last_logins'; + + /** + * Number of connections to keep for history + * + * @var integer + */ + const NB_LOGINS = 10; + + /** + * Authentication methods + * + * @var string + */ + const AUTH_DATABASE = 'database'; + const AUTH_REMEMBER_ME = 'remember_me'; + const AUTH_LDAP = 'ldap'; + const AUTH_GOOGLE = 'google'; + + /** + * Create a new record + * + * @access public + * @param string $auth_type Authentication method + * @param integer $user_id User id + * @param string $ip IP Address + * @param string $user_agent User Agent + * @return array + */ + public function create($auth_type, $user_id, $ip, $user_agent) + { + // Cleanup old sessions if necessary + $connections = $this->db + ->table(self::TABLE) + ->eq('user_id', $user_id) + ->desc('date_creation') + ->findAllByColumn('id'); + + if (count($connections) >= self::NB_LOGINS) { + + $this->db->table(self::TABLE) + ->eq('user_id', $user_id) + ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1)) + ->remove(); + } + + return $this->db + ->table(self::TABLE) + ->insert(array( + 'auth_type' => $auth_type, + 'user_id' => $user_id, + 'ip' => $ip, + 'user_agent' => $user_agent, + 'date_creation' => time(), + )); + } + + /** + * Get the last connections for a given user + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getAll($user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('user_id', $user_id) + ->desc('date_creation') + ->columns('id', 'auth_type', 'ip', 'user_agent', 'date_creation') + ->findAll(); + } +} diff --git a/app/Model/Ldap.php b/app/Model/Ldap.php new file mode 100644 index 00000000..3359318c --- /dev/null +++ b/app/Model/Ldap.php @@ -0,0 +1,79 @@ +<?php + +namespace Model; + +/** + * LDAP model + * + * @package model + * @author Frederic Guillot + */ +class Ldap extends Base +{ + /** + * Authenticate a user + * + * @access public + * @param string $username Username + * @param string $password Password + * @return null|boolean + */ + public function authenticate($username, $password) + { + if (! function_exists('ldap_connect')) { + die('The PHP LDAP extension is required'); + } + + $ldap = ldap_connect(LDAP_SERVER, LDAP_PORT); + + if (! is_resource($ldap)) { + die('Unable to connect to the LDAP server: "'.LDAP_SERVER.'"'); + } + + ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3); + ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0); + + if (@ldap_bind($ldap, sprintf(LDAP_USER_DN, $username), $password)) { + return $this->create($username); + } + + return false; + } + + /** + * Create automatically a new local user after the LDAP authentication + * + * @access public + * @param string $username Username + * @return bool + */ + public function create($username) + { + $userModel = new User($this->db, $this->event); + $user = $userModel->getByUsername($username); + + // There is an existing user account + if ($user) { + + if ($user['is_ldap_user'] == 1) { + + // LDAP user already created + return true; + } + else { + + // There is already a local user with that username + return false; + } + } + + // Create a LDAP user + $values = array( + 'username' => $username, + 'is_admin' => 0, + 'is_ldap_user' => 1, + ); + + return $userModel->create($values); + } +} diff --git a/app/Model/Project.php b/app/Model/Project.php new file mode 100644 index 00000000..85294830 --- /dev/null +++ b/app/Model/Project.php @@ -0,0 +1,558 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; +use Event\TaskModification; + +/** + * Project model + * + * @package model + * @author Frederic Guillot + */ +class Project extends Base +{ + /** + * SQL table name for projects + * + * @var string + */ + const TABLE = 'projects'; + + /** + * SQL table name for users + * + * @var string + */ + const TABLE_USERS = 'project_has_users'; + + /** + * Value for active project + * + * @var integer + */ + const ACTIVE = 1; + + /** + * Value for inactive project + * + * @var integer + */ + const INACTIVE = 0; + + /** + * Get a list of people that can be assigned for tasks + * + * @access public + * @param integer $project_id Project id + * @param bool $prepend_unassigned Prepend the 'Unassigned' value + * @param bool $prepend_everybody Prepend the 'Everbody' value + * @return array + */ + 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(); + } + + if ($prepend_unassigned) { + $allowed_users = array(t('Unassigned')) + $allowed_users; + } + + if ($prepend_everybody) { + $allowed_users = array(User::EVERYBODY_ID => t('Everybody')) + $allowed_users; + } + + return $allowed_users; + } + + /** + * Get a list of allowed people for a project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getAllowedUsers($project_id) + { + return $this->db + ->table(self::TABLE_USERS) + ->join(User::TABLE, 'id', 'user_id') + ->eq('project_id', $project_id) + ->asc('username') + ->listing('user_id', 'username'); + } + + /** + * Get allowed and not allowed users for a project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getAllUsers($project_id) + { + $users = array( + 'allowed' => array(), + 'not_allowed' => array(), + ); + + $userModel = new User($this->db, $this->event); + $all_users = $userModel->getList(); + + $users['allowed'] = $this->getAllowedUsers($project_id); + + foreach ($all_users as $user_id => $username) { + + if (! isset($users['allowed'][$user_id])) { + $users['not_allowed'][$user_id] = $username; + } + } + + return $users; + } + + /** + * Allow a specific user for a given project + * + * @access public + * @param integer $project_id Project id + * @param integer $user_id User id + * @return bool + */ + public function allowUser($project_id, $user_id) + { + return $this->db + ->table(self::TABLE_USERS) + ->save(array('project_id' => $project_id, 'user_id' => $user_id)); + } + + /** + * Revoke a specific user for a given project + * + * @access public + * @param integer $project_id Project id + * @param integer $user_id User id + * @return bool + */ + public function revokeUser($project_id, $user_id) + { + return $this->db + ->table(self::TABLE_USERS) + ->eq('project_id', $project_id) + ->eq('user_id', $user_id) + ->remove(); + } + + /** + * Check if a specific user is allowed to access to a given project + * + * @access public + * @param integer $project_id Project id + * @param integer $user_id User id + * @return bool + */ + public function isUserAllowed($project_id, $user_id) + { + // If there is nobody specified, everybody have access to the project + $nb_users = $this->db + ->table(self::TABLE_USERS) + ->eq('project_id', $project_id) + ->count(); + + if ($nb_users < 1) return true; + + // Check if user has admin rights + $nb_users = $this->db + ->table(User::TABLE) + ->eq('id', $user_id) + ->eq('is_admin', 1) + ->count(); + + if ($nb_users > 0) return true; + + // Otherwise, allow only specific users + return (bool) $this->db + ->table(self::TABLE_USERS) + ->eq('project_id', $project_id) + ->eq('user_id', $user_id) + ->count(); + } + + /** + * Get a project by the id + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getById($project_id) + { + return $this->db->table(self::TABLE)->eq('id', $project_id)->findOne(); + } + + /** + * Fetch project data by using the token + * + * @access public + * @param string $token Token + * @return array + */ + public function getByToken($token) + { + return $this->db->table(self::TABLE)->eq('token', $token)->findOne(); + } + + /** + * Return the first project from the database (no sorting) + * + * @access public + * @return array + */ + public function getFirst() + { + return $this->db->table(self::TABLE)->findOne(); + } + + /** + * Get all projects, optionaly fetch stats for each project and can check users permissions + * + * @access public + * @param bool $fetch_stats If true, return metrics about each projects + * @param bool $check_permissions If true, remove projects not allowed for the current user + * @return array + */ + public function getAll($fetch_stats = false, $check_permissions = false) + { + if (! $fetch_stats) { + return $this->db->table(self::TABLE)->asc('name')->findAll(); + } + + $this->db->startTransaction(); + + $projects = $this->db + ->table(self::TABLE) + ->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())) { + unset($projects[$pkey]); + } + else { + + $columns = $boardModel->getcolumns($project['id']); + $project['nb_active_tasks'] = 0; + + foreach ($columns as &$column) { + $column['nb_active_tasks'] = $taskModel->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_inactive_tasks'] = $project['nb_tasks'] - $project['nb_active_tasks']; + } + } + + $this->db->closeTransaction(); + + return $projects; + } + + /** + * Return the list of all projects + * + * @access public + * @param bool $prepend If true, prepend to the list the value 'None' + * @return array + */ + public function getList($prepend = true) + { + if ($prepend) { + return array(t('None')) + $this->db->table(self::TABLE)->asc('name')->listing('id', 'name'); + } + + return $this->db->table(self::TABLE)->asc('name')->listing('id', 'name'); + } + + /** + * Get all projects with all its data for a given status + * + * @access public + * @param integer $status Proejct status: self::ACTIVE or self:INACTIVE + * @return array + */ + public function getAllByStatus($status) + { + return $this->db + ->table(self::TABLE) + ->asc('name') + ->eq('is_active', $status) + ->findAll(); + } + + /** + * Get a list of project by status + * + * @access public + * @param integer $status Proejct status: self::ACTIVE or self:INACTIVE + * @return array + */ + public function getListByStatus($status) + { + return $this->db + ->table(self::TABLE) + ->asc('name') + ->eq('is_active', $status) + ->listing('id', 'name'); + } + + /** + * Return the number of projects by status + * + * @access public + * @param integer $status Status: self::ACTIVE or self:INACTIVE + * @return integer + */ + public function countByStatus($status) + { + return $this->db + ->table(self::TABLE) + ->eq('is_active', $status) + ->count(); + } + + /** + * Return a list of projects for a given user + * + * @access public + * @param array $projects Project list: ['project_id' => 'project_name'] + * @param integer $user_id User id + * @return array + */ + public function filterListByAccess(array $projects, $user_id) + { + foreach ($projects as $project_id => $project_name) { + if (! $this->isUserAllowed($project_id, $user_id)) { + unset($projects[$project_id]); + } + } + + return $projects; + } + + /** + * Create a project + * + * @access public + * @param array $values Form values + * @return integer Project id + */ + public function create(array $values) + { + $this->db->startTransaction(); + + $values['token'] = self::generateToken(); + + if (! $this->db->table(self::TABLE)->save($values)) { + $this->db->cancelTransaction(); + return false; + } + + $project_id = $this->db->getConnection()->getLastId(); + + $boardModel = new Board($this->db, $this->event); + $boardModel->create($project_id, array( + t('Backlog'), + t('Ready'), + t('Work in progress'), + t('Done'), + )); + + $this->db->closeTransaction(); + + return (int) $project_id; + } + + /** + * Check if the project have been modified + * + * @access public + * @param integer $project_id Project id + * @param integer $timestamp Timestamp + * @return bool + */ + public function isModifiedSince($project_id, $timestamp) + { + return (bool) $this->db->table(self::TABLE) + ->eq('id', $project_id) + ->gt('last_modified', $timestamp) + ->count(); + } + + /** + * Update modification date + * + * @access public + * @param integer $project_id Project id + * @return bool + */ + public function updateModificationDate($project_id) + { + return $this->db->table(self::TABLE)->eq('id', $project_id)->save(array( + 'last_modified' => time() + )); + } + + /** + * Update a project + * + * @access public + * @param array $values Form values + * @return bool + */ + public function update(array $values) + { + return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); + } + + /** + * Remove a project + * + * @access public + * @param integer $project_id Project id + * @return bool + */ + public function remove($project_id) + { + return $this->db->table(self::TABLE)->eq('id', $project_id)->remove(); + } + + /** + * Enable a project + * + * @access public + * @param integer $project_id Project id + * @return bool + */ + public function enable($project_id) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $project_id) + ->save(array('is_active' => 1)); + } + + /** + * Disable a project + * + * @access public + * @param integer $project_id Project id + * @return bool + */ + public function disable($project_id) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $project_id) + ->save(array('is_active' => 0)); + } + + /** + * Validate project creation + * + * @access public + * @param array $array Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('name', t('The project name is required')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50), + new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE) + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate project modification + * + * @access public + * @param array $array Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $v = new Validator($values, array( + new Validators\Required('id', t('The project id is required')), + new Validators\Integer('id', t('This value must be an integer')), + new Validators\Required('name', t('The project name is required')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50), + new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE) + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate allowed users + * + * @access public + * @param array $array Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateUserAccess(array $values) + { + $v = new Validator($values, array( + new Validators\Required('project_id', t('The project id is required')), + new Validators\Integer('project_id', t('This value must be an integer')), + new Validators\Required('user_id', t('The user id is required')), + new Validators\Integer('user_id', t('This value must be an integer')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Attach events + * + * @access public + */ + public function attachEvents() + { + $events = array( + Task::EVENT_UPDATE, + Task::EVENT_CREATE, + Task::EVENT_CLOSE, + Task::EVENT_OPEN, + ); + + $listener = new TaskModification($this); + + foreach ($events as $event_name) { + $this->event->attach($event_name, $listener); + } + } +} diff --git a/app/Model/RememberMe.php b/app/Model/RememberMe.php new file mode 100644 index 00000000..1494b14a --- /dev/null +++ b/app/Model/RememberMe.php @@ -0,0 +1,333 @@ +<?php + +namespace Model; + +/** + * RememberMe model + * + * @package model + * @author Frederic Guillot + */ +class RememberMe extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'remember_me'; + + /** + * Cookie name + * + * @var string + */ + const COOKIE_NAME = '__R'; + + /** + * Expiration (60 days) + * + * @var integer + */ + const EXPIRATION = 5184000; + + /** + * Get a remember me record + * + * @access public + * @return mixed + */ + public function find($token, $sequence) + { + return $this->db + ->table(self::TABLE) + ->eq('token', $token) + ->eq('sequence', $sequence) + ->gt('expiration', time()) + ->findOne(); + } + + /** + * Get all sessions for a given user + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getAll($user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('user_id', $user_id) + ->desc('date_creation') + ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration') + ->findAll(); + } + + /** + * Authenticate the user with the cookie + * + * @access public + * @return bool + */ + public function authenticate() + { + $credentials = $this->readCookie(); + + if ($credentials !== false) { + + $record = $this->find($credentials['token'], $credentials['sequence']); + + if ($record) { + + // Update the sequence + $this->writeCookie( + $record['token'], + $this->update($record['token'], $record['sequence']), + $record['expiration'] + ); + + // 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); + + return true; + } + } + + return false; + } + + /** + * Update the database and the cookie with a new sequence + * + * @access public + */ + public function refresh() + { + $credentials = $this->readCookie(); + + if ($credentials !== false) { + + $record = $this->find($credentials['token'], $credentials['sequence']); + + if ($record) { + + // Update the sequence + $this->writeCookie( + $record['token'], + $this->update($record['token'], $record['sequence']), + $record['expiration'] + ); + } + } + } + + /** + * Remove a session record + * + * @access public + * @param integer $session_id Session id + * @return mixed + */ + public function remove($session_id) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $session_id) + ->remove(); + } + + /** + * Remove the current RememberMe session and the cookie + * + * @access public + * @param integer $user_id User id + */ + public function destroy($user_id) + { + $credentials = $this->readCookie(); + + if ($credentials !== false) { + + $this->deleteCookie(); + + $this->db + ->table(self::TABLE) + ->eq('user_id', $user_id) + ->eq('token', $credentials['token']) + ->remove(); + } + } + + /** + * Create a new RememberMe session + * + * @access public + * @param integer $user_id User id + * @param string $ip IP Address + * @param string $user_agent User Agent + * @return array + */ + public function create($user_id, $ip, $user_agent) + { + $token = hash('sha256', $user_id.$user_agent.$ip.$this->generateToken()); + $sequence = $this->generateToken(); + $expiration = time() + self::EXPIRATION; + + $this->cleanup($user_id); + + $this->db + ->table(self::TABLE) + ->insert(array( + 'user_id' => $user_id, + 'ip' => $ip, + 'user_agent' => $user_agent, + 'token' => $token, + 'sequence' => $sequence, + 'expiration' => $expiration, + 'date_creation' => time(), + )); + + return array( + 'token' => $token, + 'sequence' => $sequence, + 'expiration' => $expiration, + ); + } + + /** + * Remove old sessions for a given user + * + * @access public + * @param integer $user_id User id + * @return bool + */ + public function cleanup($user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('user_id', $user_id) + ->lt('expiration', time()) + ->remove(); + } + + /** + * Return a new sequence token and update the database + * + * @access public + * @param string $token Session token + * @param string $sequence Sequence token + * @return string + */ + public function update($token, $sequence) + { + $new_sequence = $this->generateToken(); + + $this->db + ->table(self::TABLE) + ->eq('token', $token) + ->eq('sequence', $sequence) + ->update(array('sequence' => $new_sequence)); + + return $new_sequence; + } + + /** + * Encode the cookie + * + * @access public + * @param string $token Session token + * @param string $sequence Sequence token + * @return string + */ + public function encodeCookie($token, $sequence) + { + return implode('|', array($token, $sequence)); + } + + /** + * Decode the value of a cookie + * + * @access public + * @param string $value Raw cookie data + * @return array + */ + public function decodeCookie($value) + { + list($token, $sequence) = explode('|', $value); + + return array( + 'token' => $token, + 'sequence' => $sequence, + ); + } + + /** + * Return true if the current user has a RememberMe cookie + * + * @access public + * @return bool + */ + public function hasCookie() + { + return ! empty($_COOKIE[self::COOKIE_NAME]); + } + + /** + * Write and encode the cookie + * + * @access public + * @param string $token Session token + * @param string $sequence Sequence token + * @param string $expiration Cookie expiration + */ + public function writeCookie($token, $sequence, $expiration) + { + setcookie( + self::COOKIE_NAME, + $this->encodeCookie($token, $sequence), + $expiration, + BASE_URL_DIRECTORY, + null, + ! empty($_SERVER['HTTPS']), + true + ); + } + + /** + * Read and decode the cookie + * + * @access public + * @return mixed + */ + public function readCookie() + { + if (empty($_COOKIE[self::COOKIE_NAME])) { + return false; + } + + return $this->decodeCookie($_COOKIE[self::COOKIE_NAME]); + } + + /** + * Remove the cookie + * + * @access public + */ + public function deleteCookie() + { + setcookie( + self::COOKIE_NAME, + '', + time() - 3600, + BASE_URL_DIRECTORY, + null, + ! empty($_SERVER['HTTPS']), + true + ); + } +} diff --git a/app/Model/Task.php b/app/Model/Task.php new file mode 100644 index 00000000..bd67d272 --- /dev/null +++ b/app/Model/Task.php @@ -0,0 +1,627 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; +use DateTime; + +/** + * Task model + * + * @package model + * @author Frederic Guillot + */ +class Task extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'tasks'; + + /** + * Task status + * + * @var integer + */ + const STATUS_OPEN = 1; + const STATUS_CLOSED = 0; + + /** + * Events + * + * @var string + */ + const EVENT_MOVE_COLUMN = 'task.move.column'; + const EVENT_MOVE_POSITION = 'task.move.position'; + const EVENT_UPDATE = 'task.update'; + const EVENT_CREATE = 'task.create'; + const EVENT_CLOSE = 'task.close'; + const EVENT_OPEN = 'task.open'; + const EVENT_CREATE_UPDATE = 'task.create_update'; + + /** + * Get available colors + * + * @access public + * @return array + */ + public function getColors() + { + return array( + 'yellow' => t('Yellow'), + 'blue' => t('Blue'), + 'green' => t('Green'), + 'purple' => t('Purple'), + 'red' => t('Red'), + 'orange' => t('Orange'), + 'grey' => t('Grey'), + ); + } + + /** + * Fetch one task + * + * @access public + * @param integer $task_id Task id + * @param boolean $more If true, fetch all related information + * @return array + */ + public function getById($task_id, $more = false) + { + if ($more) { + + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.title', + self::TABLE.'.description', + self::TABLE.'.date_creation', + self::TABLE.'.date_completed', + self::TABLE.'.date_due', + self::TABLE.'.color_id', + self::TABLE.'.project_id', + self::TABLE.'.column_id', + self::TABLE.'.owner_id', + self::TABLE.'.position', + self::TABLE.'.is_active', + self::TABLE.'.score', + self::TABLE.'.category_id', + Category::TABLE.'.name AS category_name', + Project::TABLE.'.name AS project_name', + Board::TABLE.'.title AS column_title', + User::TABLE.'.username' + ) + ->join(Category::TABLE, 'id', 'category_id') + ->join(Project::TABLE, 'id', 'project_id') + ->join(Board::TABLE, 'id', 'column_id') + ->join(User::TABLE, 'id', 'owner_id') + ->eq(self::TABLE.'.id', $task_id) + ->findOne(); + } + else { + + return $this->db->table(self::TABLE)->eq('id', $task_id)->findOne(); + } + } + + /** + * Count all tasks for a given project and status + * + * @access public + * @param integer $project_id Project id + * @param array $status List of status id + * @return integer + */ + public function countByProjectId($project_id, array $status = array(self::STATUS_OPEN, self::STATUS_CLOSED)) + { + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->in('is_active', $status) + ->count(); + } + + /** + * Get tasks that match defined filters + * + * @access public + * @param array $filters Filters: [ ['column' => '...', 'operator' => '...', 'value' => '...'], ... ] + * @param array $sorting Sorting: [ 'column' => 'date_creation', 'direction' => 'asc'] + * @return array + */ + public function find(array $filters, array $sorting = array()) + { + $table = $this->db + ->table(self::TABLE) + ->columns( + '(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments', + 'tasks.id', + 'tasks.title', + 'tasks.description', + 'tasks.date_creation', + 'tasks.date_completed', + 'tasks.date_due', + 'tasks.color_id', + 'tasks.project_id', + 'tasks.column_id', + 'tasks.owner_id', + 'tasks.position', + 'tasks.is_active', + 'tasks.score', + 'tasks.category_id', + 'users.username' + ) + ->join('users', 'id', 'owner_id'); + + foreach ($filters as $key => $filter) { + + if ($key === 'or') { + + $table->beginOr(); + + foreach ($filter as $subfilter) { + $table->$subfilter['operator']($subfilter['column'], $subfilter['value']); + } + + $table->closeOr(); + } + else if (isset($filter['operator']) && isset($filter['column']) && isset($filter['value'])) { + $table->$filter['operator']($filter['column'], $filter['value']); + } + } + + if (empty($sorting)) { + $table->orderBy('tasks.position', 'ASC'); + } + else { + $table->orderBy($sorting['column'], $sorting['direction']); + } + + return $table->findAll(); + } + + /** + * Count the number of tasks for a given column and status + * + * @access public + * @param integer $project_id Project id + * @param integer $column_id Column id + * @param array $status List of status id + * @return integer + */ + public function countByColumnId($project_id, $column_id, array $status = array(self::STATUS_OPEN)) + { + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->in('is_active', $status) + ->count(); + } + + /** + * Duplicate a task + * + * @access public + * @param integer $task_id Task id + * @return boolean + */ + public function duplicate($task_id) + { + $this->db->startTransaction(); + + $boardModel = new Board($this->db, $this->event); + + // Get the original task + $task = $this->getById($task_id); + + // Cleanup data + unset($task['id']); + unset($task['date_completed']); + + // Assign new values + $task['date_creation'] = time(); + $task['is_active'] = 1; + $task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']); + + // Save task + if (! $this->db->table(self::TABLE)->save($task)) { + $this->db->cancelTransaction(); + return false; + } + + $task_id = $this->db->getConnection()->getLastId(); + + $this->db->closeTransaction(); + + // Trigger events + $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task); + $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task); + + return $task_id; + } + + /** + * Duplicate a task to another project (always copy to the first column) + * + * @access public + * @param integer $task_id Task id + * @param integer $project_id Destination project id + * @return boolean + */ + public function duplicateToAnotherProject($task_id, $project_id) + { + $this->db->startTransaction(); + + $boardModel = new Board($this->db, $this->event); + + // Get the original task + $task = $this->getById($task_id); + + // Cleanup data + unset($task['id']); + unset($task['date_completed']); + + // Assign new values + $task['date_creation'] = time(); + $task['owner_id'] = 0; + $task['category_id'] = 0; + $task['is_active'] = 1; + $task['column_id'] = $boardModel->getFirstColumn($project_id); + $task['project_id'] = $project_id; + $task['position'] = $this->countByColumnId($task['project_id'], $task['column_id']); + + // Save task + if (! $this->db->table(self::TABLE)->save($task)) { + $this->db->cancelTransaction(); + return false; + } + + $task_id = $this->db->getConnection()->getLastId(); + + $this->db->closeTransaction(); + + // Trigger events + $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $task); + $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $task); + + return $task_id; + } + + /** + * Create a task + * + * @access public + * @param array $values Form values + * @return boolean + */ + public function create(array $values) + { + $this->db->startTransaction(); + + // Prepare data + if (isset($values['another_task'])) { + unset($values['another_task']); + } + + if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) { + $values['date_due'] = $this->parseDate($values['date_due']); + } + + $values['date_creation'] = time(); + $values['position'] = $this->countByColumnId($values['project_id'], $values['column_id']); + + // Save task + if (! $this->db->table(self::TABLE)->save($values)) { + $this->db->cancelTransaction(); + return false; + } + + $task_id = $this->db->getConnection()->getLastId(); + + $this->db->closeTransaction(); + + // Trigger events + $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values); + $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $values); + + return $task_id; + } + + /** + * Update a task + * + * @access public + * @param array $values Form values + * @return boolean + */ + public function update(array $values) + { + // Prepare data + if (! empty($values['date_due']) && ! is_numeric($values['date_due'])) { + $values['date_due'] = $this->parseDate($values['date_due']); + } + + $original_task = $this->getById($values['id']); + + if ($original_task === false) { + return false; + } + + $updated_task = $values; + unset($updated_task['id']); + + $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task); + + // 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; + } + + 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; + } + + $event_data = array_merge($original_task, $values); + $event_data['task_id'] = $original_task['id']; + + foreach ($events as $event) { + $this->event->trigger($event, $event_data); + } + } + + return $result; + } + + /** + * Mark a task closed + * + * @access public + * @param integer $task_id Task id + * @return boolean + */ + public function close($task_id) + { + $result = $this->db + ->table(self::TABLE) + ->eq('id', $task_id) + ->update(array( + 'is_active' => 0, + 'date_completed' => time() + )); + + if ($result) { + $this->event->trigger(self::EVENT_CLOSE, array('task_id' => $task_id) + $this->getById($task_id)); + } + + return $result; + } + + /** + * Mark a task open + * + * @access public + * @param integer $task_id Task id + * @return boolean + */ + public function open($task_id) + { + $result = $this->db + ->table(self::TABLE) + ->eq('id', $task_id) + ->update(array( + 'is_active' => 1, + 'date_completed' => '' + )); + + if ($result) { + $this->event->trigger(self::EVENT_OPEN, array('task_id' => $task_id) + $this->getById($task_id)); + } + + return $result; + } + + /** + * Remove a task + * + * @access public + * @param integer $task_id Task id + * @return boolean + */ + public function remove($task_id) + { + return $this->db->table(self::TABLE)->eq('id', $task_id)->remove(); + } + + /** + * 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) + * @return boolean + */ + public function move($task_id, $column_id, $position) + { + return $this->update(array( + 'id' => $task_id, + 'column_id' => $column_id, + 'position' => $position, + )); + } + + /** + * Validate task creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('color_id', t('The color is required')), + new Validators\Required('project_id', t('The project is required')), + new Validators\Integer('project_id', t('This value must be an integer')), + new Validators\Required('column_id', t('The column is required')), + new Validators\Integer('column_id', t('This value must be an integer')), + new Validators\Integer('owner_id', t('This value must be an integer')), + new Validators\Integer('score', t('This value must be an integer')), + new Validators\Required('title', t('The title is required')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), + new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate description creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateDescriptionCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('id', t('The id is required')), + new Validators\Integer('id', t('This value must be an integer')), + new Validators\Required('description', t('The description is required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate task modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $v = new Validator($values, array( + new Validators\Required('id', t('The id is required')), + new Validators\Integer('id', t('This value must be an integer')), + new Validators\Required('color_id', t('The color is required')), + new Validators\Required('project_id', t('The project is required')), + new Validators\Integer('project_id', t('This value must be an integer')), + new Validators\Required('column_id', t('The column is required')), + new Validators\Integer('column_id', t('This value must be an integer')), + new Validators\Integer('owner_id', t('This value must be an integer')), + new Validators\Integer('score', t('This value must be an integer')), + new Validators\Required('title', t('The title is required')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), + new Validators\Date('date_due', t('Invalid date'), $this->getDateFormats()), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate assignee change + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateAssigneeModification(array $values) + { + $v = new Validator($values, array( + new Validators\Required('id', t('The id is required')), + new Validators\Integer('id', t('This value must be an integer')), + new Validators\Required('project_id', t('The project is required')), + new Validators\Integer('project_id', t('This value must be an integer')), + new Validators\Required('owner_id', t('This value is required')), + new Validators\Integer('owner_id', t('This value must be an integer')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Return a timestamp if the given date format is correct otherwise return 0 + * + * @access public + * @param string $value Date to parse + * @param string $format Date format + * @return integer + */ + public function getValidDate($value, $format) + { + $date = DateTime::createFromFormat($format, $value); + + if ($date !== false) { + $errors = DateTime::getLastErrors(); + if ($errors['error_count'] === 0 && $errors['warning_count'] === 0) { + $timestamp = $date->getTimestamp(); + return $timestamp > 0 ? $timestamp : 0; + } + } + + return 0; + } + + /** + * Parse a date ad return a unix timestamp, try different date formats + * + * @access public + * @param string $value Date to parse + * @return integer + */ + public function parseDate($value) + { + foreach ($this->getDateFormats() as $format) { + + $timestamp = $this->getValidDate($value, $format); + + if ($timestamp !== 0) { + return $timestamp; + } + } + + return null; + } + + /** + * Return the list of supported date formats + * + * @access public + * @return array + */ + public function getDateFormats() + { + return array( + t('m/d/Y'), + 'Y-m-d', + 'Y_m_d', + ); + } +} diff --git a/app/Model/User.php b/app/Model/User.php new file mode 100644 index 00000000..bce717a7 --- /dev/null +++ b/app/Model/User.php @@ -0,0 +1,426 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * User model + * + * @package model + * @author Frederic Guillot + */ +class User extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'users'; + + /** + * Id used for everbody (filtering) + * + * @var integer + */ + const EVERYBODY_ID = -1; + + /** + * Get a specific user by id + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getById($user_id) + { + return $this->db->table(self::TABLE)->eq('id', $user_id)->findOne(); + } + + /** + * Get a specific user by the Google id + * + * @access public + * @param string $google_id Google unique id + * @return array + */ + public function getByGoogleId($google_id) + { + return $this->db->table(self::TABLE)->eq('google_id', $google_id)->findOne(); + } + + /** + * Get a specific user by the username + * + * @access public + * @param string $username Username + * @return array + */ + public function getByUsername($username) + { + return $this->db->table(self::TABLE)->eq('username', $username)->findOne(); + } + + /** + * Get all users + * + * @access public + * @return array + */ + public function getAll() + { + return $this->db + ->table(self::TABLE) + ->asc('username') + ->columns('id', 'username', 'name', 'email', 'is_admin', 'default_project_id', 'is_ldap_user') + ->findAll(); + } + + /** + * List all users (key-value pairs with id/username) + * + * @access public + * @return array + */ + public function getList() + { + return $this->db->table(self::TABLE)->asc('username')->listing('id', 'username'); + } + + /** + * Add a new user in the database + * + * @access public + * @param array $values Form values + * @return boolean + */ + public function create(array $values) + { + if (isset($values['confirmation'])) { + unset($values['confirmation']); + } + + if (isset($values['password'])) { + $values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT); + } + + return $this->db->table(self::TABLE)->save($values); + } + + /** + * Modify a new user + * + * @access public + * @param array $values Form values + * @return array + */ + public function update(array $values) + { + if (! empty($values['password'])) { + $values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT); + } + else { + unset($values['password']); + } + + if (isset($values['confirmation'])) { + unset($values['confirmation']); + } + + if (isset($values['current_password'])) { + unset($values['current_password']); + } + + $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); + + if ($_SESSION['user']['id'] == $values['id']) { + $this->updateSession(); + } + + return $result; + } + + /** + * Remove a specific user + * + * @access public + * @param integer $user_id User id + * @return boolean + */ + public function remove($user_id) + { + $this->db->startTransaction(); + + // All tasks assigned to this user will be unassigned + $this->db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => '')); + $this->db->table(self::TABLE)->eq('id', $user_id)->remove(); + + $this->db->closeTransaction(); + + return true; + } + + /** + * Update user session information + * + * @access public + * @param array $user User data + */ + public function updateSession(array $user = array()) + { + if (empty($user)) { + $user = $this->getById($_SESSION['user']['id']); + } + + if (isset($user['password'])) { + unset($user['password']); + } + + $user['id'] = (int) $user['id']; + $user['default_project_id'] = (int) $user['default_project_id']; + $user['is_admin'] = (bool) $user['is_admin']; + $user['is_ldap_user'] = (bool) $user['is_ldap_user']; + + $_SESSION['user'] = $user; + } + + /** + * Validate user creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('username', t('The username is required')), + new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), + new Validators\AlphaNumeric('username', t('The username must be alphanumeric')), + new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'), + 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\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')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate user modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + if (! empty($values['password'])) { + return $this->validatePasswordModification($values); + } + + $v = new Validator($values, array( + new Validators\Required('id', t('The user id is required')), + new Validators\Required('username', t('The username is required')), + new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), + new Validators\AlphaNumeric('username', t('The username must be alphanumeric')), + new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'), + 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')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate password modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validatePasswordModification(array $values) + { + $v = new Validator($values, array( + new Validators\Required('id', t('The user id is required')), + new Validators\Required('username', t('The username is required')), + new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), + new Validators\AlphaNumeric('username', t('The username must be alphanumeric')), + new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'), + new Validators\Required('current_password', t('The current password is required')), + 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\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')), + )); + + if ($v->execute()) { + + // Check password + list($authenticated,) = $this->authenticate($_SESSION['user']['username'], $values['current_password']); + + if ($authenticated) { + return array(true, array()); + } + else { + return array(false, array('current_password' => array(t('Wrong password')))); + } + } + + return array(false, $v->getErrors()); + } + + /** + * Validate user login + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateLogin(array $values) + { + $v = new Validator($values, array( + new Validators\Required('username', t('The username is required')), + new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), + new Validators\Required('password', t('The password is required')), + )); + + $result = $v->execute(); + $errors = $v->getErrors(); + + if ($result) { + + list($authenticated, $method) = $this->authenticate($values['username'], $values['password']); + + if ($authenticated === true) { + + // Create the user session + $user = $this->getByUsername($values['username']); + $this->updateSession($user); + + // Update login history + $lastLogin = new LastLogin($this->db, $this->event); + $lastLogin->create( + $method, + $user['id'], + $this->getIpAddress(), + $this->getUserAgent() + ); + + // 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']); + } + } + else { + $result = false; + $errors['login'] = t('Bad username or password'); + } + } + + return array( + $result, + $errors + ); + } + + /** + * Authenticate a user + * + * @access public + * @param string $username Username + * @param string $password Password + * @return array + */ + public function authenticate($username, $password) + { + // Database authentication + $user = $this->db->table(self::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne(); + $authenticated = $user && \password_verify($password, $user['password']); + $method = LastLogin::AUTH_DATABASE; + + // 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; + } + + return array($authenticated, $method); + } + + /** + * Get the user agent of the connected user + * + * @access public + * @return string + */ + public function getUserAgent() + { + return empty($_SERVER['HTTP_USER_AGENT']) ? t('Unknown') : $_SERVER['HTTP_USER_AGENT']; + } + + /** + * Get the real IP address of the connected user + * + * @access public + * @param bool $only_public Return only public IP address + * @return string + */ + public function getIpAddress($only_public = false) + { + $keys = array( + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED', + 'HTTP_X_CLUSTER_CLIENT_IP', + 'HTTP_FORWARDED_FOR', + 'HTTP_FORWARDED', + 'REMOTE_ADDR' + ); + + foreach ($keys as $key) { + + if (isset($_SERVER[$key])) { + + foreach (explode(',', $_SERVER[$key]) as $ip_address) { + + $ip_address = trim($ip_address); + + if ($only_public) { + + // Return only public IP address + if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { + return $ip_address; + } + } + else { + + return $ip_address; + } + } + } + } + + return t('Unknown'); + } +} |