diff options
Diffstat (limited to 'app')
93 files changed, 11503 insertions, 0 deletions
diff --git a/app/.htaccess b/app/.htaccess new file mode 100644 index 00000000..14249c50 --- /dev/null +++ b/app/.htaccess @@ -0,0 +1 @@ +Deny from all
\ No newline at end of file diff --git a/app/Action/Base.php b/app/Action/Base.php new file mode 100644 index 00000000..14b0a3c0 --- /dev/null +++ b/app/Action/Base.php @@ -0,0 +1,142 @@ +<?php + +namespace Action; + +use Core\Listener; + +/** + * Base class for automatic actions + * + * @package action + * @author Frederic Guillot + */ +abstract class Base implements Listener +{ + /** + * Project id + * + * @access private + * @var integer + */ + private $project_id = 0; + + /** + * User parameters + * + * @access private + * @var array + */ + private $params = array(); + + /** + * Execute the action + * + * @abstract + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + abstract public function doAction(array $data); + + /** + * Get the required parameter for the action (defined by the user) + * + * @abstract + * @access public + * @return array + */ + abstract public function getActionRequiredParameters(); + + /** + * Get the required parameter for the event (check if for the event data) + * + * @abstract + * @access public + * @return array + */ + abstract public function getEventRequiredParameters(); + + /** + * Constructor + * + * @access public + * @param integer $project_id Project id + */ + public function __construct($project_id) + { + $this->project_id = $project_id; + } + + /** + * Set an user defined parameter + * + * @access public + * @param string $name Parameter name + * @param mixed $value Value + */ + public function setParam($name, $value) + { + $this->params[$name] = $value; + } + + /** + * Get an user defined parameter + * + * @access public + * @param string $name Parameter name + * @param mixed $default_value Default value + * @return mixed + */ + public function getParam($name, $default_value = null) + { + return isset($this->params[$name]) ? $this->params[$name] : $default_value; + } + + /** + * Check if an action is executable (right project and required parameters) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action is executable + */ + public function isExecutable(array $data) + { + if (isset($data['project_id']) && $data['project_id'] == $this->project_id && $this->hasRequiredParameters($data)) { + return true; + } + + return false; + } + + /** + * Check if the event data has required parameters to execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if all keys are there + */ + public function hasRequiredParameters(array $data) + { + foreach ($this->getEventRequiredParameters() as $parameter) { + if (! isset($data[$parameter])) return false; + } + + return true; + } + + /** + * 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) + { + if ($this->isExecutable($data)) { + return $this->doAction($data); + } + + return false; + } +} diff --git a/app/Action/TaskAssignColorCategory.php b/app/Action/TaskAssignColorCategory.php new file mode 100644 index 00000000..4304d084 --- /dev/null +++ b/app/Action/TaskAssignColorCategory.php @@ -0,0 +1,85 @@ +<?php + +namespace Action; + +use Model\Task; + +/** + * Assign a color to a specific category + * + * @package action + * @author Frederic Guillot + */ +class TaskAssignColorCategory extends Base +{ + /** + * Task model + * + * @accesss private + * @var \Model\Task + */ + private $task; + + /** + * Constructor + * + * @access public + * @param integer $project_id Project id + * @param \Model\Task $task Task model instance + */ + public function __construct($project_id, Task $task) + { + parent::__construct($project_id); + $this->task = $task; + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'color_id' => t('Color'), + 'category_id' => t('Category'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'category_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if ($data['category_id'] == $this->getParam('category_id')) { + + $this->task->update(array( + 'id' => $data['task_id'], + 'color_id' => $this->getParam('color_id'), + )); + + return true; + } + + return false; + } +} diff --git a/app/Action/TaskAssignColorUser.php b/app/Action/TaskAssignColorUser.php new file mode 100644 index 00000000..9ff140b3 --- /dev/null +++ b/app/Action/TaskAssignColorUser.php @@ -0,0 +1,85 @@ +<?php + +namespace Action; + +use Model\Task; + +/** + * Assign a color to a specific user + * + * @package action + * @author Frederic Guillot + */ +class TaskAssignColorUser extends Base +{ + /** + * Task model + * + * @accesss private + * @var \Model\Task + */ + private $task; + + /** + * Constructor + * + * @access public + * @param integer $project_id Project id + * @param \Model\Task $task Task model instance + */ + public function __construct($project_id, Task $task) + { + parent::__construct($project_id); + $this->task = $task; + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'color_id' => t('Color'), + 'user_id' => t('Assignee'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'owner_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if ($data['owner_id'] == $this->getParam('user_id')) { + + $this->task->update(array( + 'id' => $data['task_id'], + 'color_id' => $this->getParam('color_id'), + )); + + return true; + } + + return false; + } +} diff --git a/app/Action/TaskAssignCurrentUser.php b/app/Action/TaskAssignCurrentUser.php new file mode 100644 index 00000000..1c038966 --- /dev/null +++ b/app/Action/TaskAssignCurrentUser.php @@ -0,0 +1,95 @@ +<?php + +namespace Action; + +use Model\Task; +use Model\Acl; + +/** + * Assign a task to the logged user + * + * @package action + * @author Frederic Guillot + */ +class TaskAssignCurrentUser extends Base +{ + /** + * Task model + * + * @accesss private + * @var \Model\Task + */ + private $task; + + /** + * Acl model + * + * @accesss private + * @var \Model\Acl + */ + private $acl; + + /** + * Constructor + * + * @access public + * @param integer $project_id Project id + * @param \Model\Task $task Task model instance + * @param \Model\Acl $acl Acl model instance + */ + public function __construct($project_id, Task $task, Acl $acl) + { + parent::__construct($project_id); + $this->task = $task; + $this->acl = $acl; + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if ($data['column_id'] == $this->getParam('column_id')) { + + $this->task->update(array( + 'id' => $data['task_id'], + 'owner_id' => $this->acl->getUserId(), + )); + + return true; + } + + return false; + } +} diff --git a/app/Action/TaskAssignSpecificUser.php b/app/Action/TaskAssignSpecificUser.php new file mode 100644 index 00000000..8c379bcc --- /dev/null +++ b/app/Action/TaskAssignSpecificUser.php @@ -0,0 +1,85 @@ +<?php + +namespace Action; + +use Model\Task; + +/** + * Assign a task to a specific user + * + * @package action + * @author Frederic Guillot + */ +class TaskAssignSpecificUser extends Base +{ + /** + * Task model + * + * @accesss private + * @var \Model\Task + */ + private $task; + + /** + * Constructor + * + * @access public + * @param integer $project_id Project id + * @param \Model\Task $task Task model instance + */ + public function __construct($project_id, Task $task) + { + parent::__construct($project_id); + $this->task = $task; + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + 'user_id' => t('Assignee'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if ($data['column_id'] == $this->getParam('column_id')) { + + $this->task->update(array( + 'id' => $data['task_id'], + 'owner_id' => $this->getParam('user_id'), + )); + + return true; + } + + return false; + } +} diff --git a/app/Action/TaskClose.php b/app/Action/TaskClose.php new file mode 100644 index 00000000..32887d3c --- /dev/null +++ b/app/Action/TaskClose.php @@ -0,0 +1,79 @@ +<?php + +namespace Action; + +use Model\Task; + +/** + * Close automatically a task + * + * @package action + * @author Frederic Guillot + */ +class TaskClose extends Base +{ + /** + * Task model + * + * @accesss private + * @var \Model\Task + */ + private $task; + + /** + * Constructor + * + * @access public + * @param integer $project_id Project id + * @param \Model\Task $task Task model instance + */ + public function __construct($project_id, Task $task) + { + parent::__construct($project_id); + $this->task = $task; + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if ($data['column_id'] == $this->getParam('column_id')) { + $this->task->close($data['task_id']); + return true; + } + + return false; + } +} diff --git a/app/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php new file mode 100644 index 00000000..7ef0f6ab --- /dev/null +++ b/app/Action/TaskDuplicateAnotherProject.php @@ -0,0 +1,83 @@ +<?php + +namespace Action; + +use Model\Task; + +/** + * Duplicate a task to another project + * + * @package action + * @author Frederic Guillot + */ +class TaskDuplicateAnotherProject extends Base +{ + /** + * Task model + * + * @accesss private + * @var \Model\Task + */ + private $task; + + /** + * Constructor + * + * @access public + * @param integer $project_id Project id + * @param \Model\Task $task Task model instance + */ + public function __construct($project_id, Task $task) + { + parent::__construct($project_id); + $this->task = $task; + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + 'project_id' => t('Project'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + 'project_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if ($data['column_id'] == $this->getParam('column_id') && $data['project_id'] != $this->getParam('project_id')) { + + $this->task->duplicateToAnotherProject($data['task_id'], $this->getParam('project_id')); + + return true; + } + + return false; + } +} diff --git a/app/Controller/Action.php b/app/Controller/Action.php new file mode 100644 index 00000000..b32a8906 --- /dev/null +++ b/app/Controller/Action.php @@ -0,0 +1,142 @@ +<?php + +namespace Controller; + +/** + * Automatic actions management + * + * @package controller + * @author Frederic Guillot + */ +class Action extends Base +{ + /** + * List of automatic actions for a given project + * + * @access public + */ + public function index() + { + $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->project->getById($project_id); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + $this->response->html($this->template->layout('action_index', array( + 'values' => array('project_id' => $project['id']), + 'project' => $project, + 'actions' => $this->action->getAllByProject($project['id']), + 'available_actions' => $this->action->getAvailableActions(), + 'available_events' => $this->action->getAvailableEvents(), + 'available_params' => $this->action->getAllActionParameters(), + 'columns_list' => $this->board->getColumnsList($project['id']), + 'users_list' => $this->project->getUsersList($project['id'], false), + 'projects_list' => $this->project->getList(false), + 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($project['id'], false), + 'menu' => 'projects', + 'title' => t('Automatic actions') + ))); + } + + /** + * Define action parameters (step 2) + * + * @access public + */ + public function params() + { + $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->project->getById($project_id); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + $values = $this->request->getValues(); + $action = $this->action->load($values['action_name'], $values['project_id']); + + $this->response->html($this->template->layout('action_params', array( + 'values' => $values, + 'action_params' => $action->getActionRequiredParameters(), + 'columns_list' => $this->board->getColumnsList($project['id']), + 'users_list' => $this->project->getUsersList($project['id'], false), + 'projects_list' => $this->project->getList(false), + 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($project['id'], false), + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Automatic actions') + ))); + } + + /** + * Create a new action (last step) + * + * @access public + */ + public function create() + { + $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->project->getById($project_id); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + $values = $this->request->getValues(); + + list($valid, $errors) = $this->action->validateCreation($values); + + if ($valid) { + + if ($this->action->create($values)) { + $this->session->flash(t('Your automatic action have been created successfully.')); + } + else { + $this->session->flashError(t('Unable to create your automatic action.')); + } + } + + $this->response->redirect('?controller=action&action=index&project_id='.$project['id']); + } + + /** + * Confirmation dialog before removing an action + * + * @access public + */ + public function confirm() + { + $this->response->html($this->template->layout('action_remove', array( + 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), + 'available_events' => $this->action->getAvailableEvents(), + 'available_actions' => $this->action->getAvailableActions(), + 'menu' => 'projects', + 'title' => t('Remove an action') + ))); + } + + /** + * Remove an action + * + * @access public + */ + public function remove() + { + $action = $this->action->getById($this->request->getIntegerParam('action_id')); + + if ($action && $this->action->remove($action['id'])) { + $this->session->flash(t('Action removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this action.')); + } + + $this->response->redirect('?controller=action&action=index&project_id='.$action['project_id']); + } +} diff --git a/app/Controller/App.php b/app/Controller/App.php new file mode 100644 index 00000000..64f9461f --- /dev/null +++ b/app/Controller/App.php @@ -0,0 +1,29 @@ +<?php + +namespace Controller; + +use Model\Project; + +/** + * Application controller + * + * @package controller + * @author Frederic Guillot + */ +class App extends Base +{ + /** + * Redirect to the project creation page or the board controller + * + * @access public + */ + public function index() + { + if ($this->project->countByStatus(Project::ACTIVE)) { + $this->response->redirect('?controller=board'); + } + else { + $this->redirectNoProject(); + } + } +} diff --git a/app/Controller/Base.php b/app/Controller/Base.php new file mode 100644 index 00000000..bb9add4f --- /dev/null +++ b/app/Controller/Base.php @@ -0,0 +1,238 @@ +<?php + +namespace Controller; + +use Core\Registry; +use Core\Translator; +use Model\LastLogin; + +/** + * Base controller + * + * @package controller + * @author Frederic Guillot + */ +abstract class Base +{ + /** + * Request instance + * + * @accesss public + * @var \Core\Request + */ + public $request; + + /** + * Response instance + * + * @accesss public + * @var \Core\Response + */ + public $response; + + /** + * Template instance + * + * @accesss public + * @var \Core\Template + */ + public $template; + + /** + * Session instance + * + * @accesss public + * @var \Core\Session + */ + public $session; + + /** + * Registry instance + * + * @access private + * @var Core\Registry + */ + private $registry; + + /** + * Constructor + * + * @access public + * @param \Core\Registry $registry Registry instance + */ + public function __construct(Registry $registry) + { + $this->registry = $registry; + } + + /** + * Load automatically models + * + * @access public + * @param string $name Model name + */ + 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); + } + + /** + * Method executed before each action + * + * @access public + */ + public function beforeAction($controller, $action) + { + // Start the session + $this->session->open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH); + + // HTTP secure headers + $this->response->csp(); + $this->response->nosniff(); + $this->response->xss(); + $this->response->hsts(); + $this->response->xframe(); + + // Load translations + $language = $this->config->get('language', 'en_US'); + if ($language !== 'en_US') Translator::load($language); + + // Set timezone + date_default_timezone_set($this->config->get('timezone', 'UTC')); + + // Authentication + if (! $this->acl->isLogged() && ! $this->acl->isPublicAction($controller, $action)) { + + // Try the remember me authentication first + if (! $this->rememberMe->authenticate()) { + + // Redirect to the login form if not authenticated + $this->response->redirect('?controller=user&action=login'); + } + else { + + $this->lastLogin->create( + LastLogin::AUTH_REMEMBER_ME, + $this->acl->getUserId(), + $this->user->getIpAddress(), + $this->user->getUserAgent() + ); + } + } + else if ($this->rememberMe->hasCookie()) { + $this->rememberMe->refresh(); + } + + // Check if the user is allowed to see this page + if (! $this->acl->isPageAccessAllowed($controller, $action)) { + $this->response->redirect('?controller=user&action=forbidden'); + } + + // Attach events + $this->action->attachEvents(); + $this->project->attachEvents(); + } + + /** + * Application not found page (404 error) + * + * @access public + */ + public function notfound() + { + $this->response->html($this->template->layout('app_notfound', array('title' => t('Page not found')))); + } + + /** + * Check if the current user have access to the given project + * + * @access protected + * @param integer $project_id Project id + */ + protected function checkProjectPermissions($project_id) + { + if ($this->acl->isRegularUser()) { + + if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { + $this->response->redirect('?controller=project&action=forbidden'); + } + } + } + + /** + * Redirection when there is no project in the database + * + * @access protected + */ + protected function redirectNoProject() + { + $this->session->flash(t('There is no active project, the first step is to create a new project.')); + $this->response->redirect('?controller=project&action=create'); + } + + /** + * Display the template show task (common between different actions) + * + * @access protected + * @param array $task Task data + * @param array $comment_form Comment form data + * @param array $description_form Description form data + * @param array $comment_edit_form Comment edit form data + */ + protected function showTask(array $task, array $comment_form = array(), array $description_form = array(), array $comment_edit_form = array()) + { + if (empty($comment_form)) { + $comment_form = array( + 'values' => array('task_id' => $task['id'], 'user_id' => $this->acl->getUserId()), + 'errors' => array() + ); + } + + if (empty($description_form)) { + $description_form = array( + 'values' => array('id' => $task['id']), + 'errors' => array() + ); + } + + if (empty($comment_edit_form)) { + $comment_edit_form = array( + 'values' => array('id' => 0), + 'errors' => array() + ); + } + else { + $hide_comment_form = true; + } + + $this->response->html($this->taskLayout('task_show', array( + 'hide_comment_form' => isset($hide_comment_form), + 'comment_edit_form' => $comment_edit_form, + 'comment_form' => $comment_form, + 'description_form' => $description_form, + 'comments' => $this->comment->getAll($task['id']), + 'task' => $task, + 'columns_list' => $this->board->getColumnsList($task['project_id']), + 'colors_list' => $this->task->getColors(), + 'menu' => 'tasks', + 'title' => $task['title'], + ))); + } + + /** + * Common layout for task views + * + * @access protected + * @param string $template Template name + * @param array $params Template parameters + */ + protected function taskLayout($template, array $params) + { + $content = $this->template->load($template, $params); + $params['task_content_for_layout'] = $content; + + return $this->template->layout('task_layout', $params); + } +} diff --git a/app/Controller/Board.php b/app/Controller/Board.php new file mode 100644 index 00000000..c727a422 --- /dev/null +++ b/app/Controller/Board.php @@ -0,0 +1,411 @@ +<?php + +namespace Controller; + +use Model\Project; +use Model\User; + +/** + * Board controller + * + * @package controller + * @author Frederic Guillot + */ +class Board extends Base +{ + /** + * Move a column up + * + * @access public + */ + public function moveUp() + { + $project_id = $this->request->getIntegerParam('project_id'); + $column_id = $this->request->getIntegerParam('column_id'); + + $this->board->moveUp($project_id, $column_id); + + $this->response->redirect('?controller=board&action=edit&project_id='.$project_id); + } + + /** + * Move a column down + * + * @access public + */ + public function moveDown() + { + $project_id = $this->request->getIntegerParam('project_id'); + $column_id = $this->request->getIntegerParam('column_id'); + + $this->board->moveDown($project_id, $column_id); + + $this->response->redirect('?controller=board&action=edit&project_id='.$project_id); + } + + /** + * Change a task assignee directly from the board + * + * @access public + */ + public function assign() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id')); + $project = $this->project->getById($task['project_id']); + $projects = $this->project->getListByStatus(Project::ACTIVE); + + if ($this->acl->isRegularUser()) { + $projects = $this->project->filterListByAccess($projects, $this->acl->getUserId()); + } + + if (! $project) $this->notfound(); + $this->checkProjectPermissions($project['id']); + + if ($this->request->isAjax()) { + + $this->response->html($this->template->load('board_assign', array( + 'errors' => array(), + 'values' => $task, + 'users_list' => $this->project->getUsersList($project['id']), + 'projects' => $projects, + 'current_project_id' => $project['id'], + 'current_project_name' => $project['name'], + ))); + } + else { + + $this->response->html($this->template->layout('board_assign', array( + 'errors' => array(), + 'values' => $task, + 'users_list' => $this->project->getUsersList($project['id']), + 'projects' => $projects, + 'current_project_id' => $project['id'], + 'current_project_name' => $project['name'], + 'menu' => 'boards', + 'title' => t('Change assignee').' - '.$task['title'], + ))); + } + } + + /** + * Validate an assignee modification + * + * @access public + */ + public function assignTask() + { + $values = $this->request->getValues(); + $this->checkProjectPermissions($values['project_id']); + + list($valid,) = $this->task->validateAssigneeModification($values); + + if ($valid && $this->task->update($values)) { + $this->session->flash(t('Task updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } + + $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); + } + + /** + * Display the public version of a board + * Access checked by a simple token, no user login, read only, auto-refresh + * + * @access public + */ + public function readonly() + { + $token = $this->request->getStringParam('token'); + $project = $this->project->getByToken($token); + + // Token verification + if (! $project) { + $this->response->text('Not Authorized', 401); + } + + // Display the board with a specific layout + $this->response->html($this->template->layout('board_public', array( + 'project' => $project, + 'columns' => $this->board->get($project['id']), + 'categories' => $this->category->getList($project['id'], false), + 'title' => $project['name'], + 'no_layout' => true, + 'auto_refresh' => true, + ))); + } + + /** + * Redirect the user to the default project + * + * @access public + */ + public function index() + { + $projects = $this->project->getListByStatus(Project::ACTIVE); + + if ($this->acl->isRegularUser()) { + $projects = $this->project->filterListByAccess($projects, $this->acl->getUserId()); + } + + if (empty($projects)) { + + if ($this->acl->isAdminUser()) { + $this->redirectNoProject(); + } + else { + $this->response->redirect('?controller=project&action=forbidden'); + } + } + else if (! empty($_SESSION['user']['default_project_id']) && isset($projects[$_SESSION['user']['default_project_id']])) { + $project_id = $_SESSION['user']['default_project_id']; + $project_name = $projects[$_SESSION['user']['default_project_id']]; + } + else { + list($project_id, $project_name) = each($projects); + } + + $this->response->redirect('?controller=board&action=show&project_id='.$project_id); + } + + /** + * Show a board for a given project + * + * @access public + */ + public function show() + { + $project_id = $this->request->getIntegerParam('project_id'); + $user_id = $this->request->getIntegerParam('user_id', User::EVERYBODY_ID); + + $this->checkProjectPermissions($project_id); + $projects = $this->project->getListByStatus(Project::ACTIVE); + + if ($this->acl->isRegularUser()) { + $projects = $this->project->filterListByAccess($projects, $this->acl->getUserId()); + } + + if (! isset($projects[$project_id])) { + $this->notfound(); + } + + $this->response->html($this->template->layout('board_index', array( + 'users' => $this->project->getUsersList($project_id, true, true), + 'filters' => array('user_id' => $user_id), + 'projects' => $projects, + 'current_project_id' => $project_id, + 'current_project_name' => $projects[$project_id], + 'board' => $this->board->get($project_id), + 'categories' => $this->category->getList($project_id, true, true), + 'menu' => 'boards', + 'title' => $projects[$project_id] + ))); + } + + /** + * Display a form to edit a board + * + * @access public + */ + public function edit() + { + $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->project->getById($project_id); + + if (! $project) $this->notfound(); + + $columns = $this->board->getColumns($project_id); + $values = array(); + + foreach ($columns as $column) { + $values['title['.$column['id'].']'] = $column['title']; + $values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null; + } + + $this->response->html($this->template->layout('board_edit', array( + 'errors' => array(), + 'values' => $values + array('project_id' => $project_id), + 'columns' => $columns, + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Edit board') + ))); + } + + /** + * Validate and update a board + * + * @access public + */ + public function update() + { + $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->project->getById($project_id); + + if (! $project) $this->notfound(); + + $columns = $this->board->getColumns($project_id); + $data = $this->request->getValues(); + $values = $columns_list = array(); + + foreach ($columns as $column) { + $columns_list[$column['id']] = $column['title']; + $values['title['.$column['id'].']'] = isset($data['title'][$column['id']]) ? $data['title'][$column['id']] : ''; + $values['task_limit['.$column['id'].']'] = isset($data['task_limit'][$column['id']]) ? $data['task_limit'][$column['id']] : 0; + } + + list($valid, $errors) = $this->board->validateModification($columns_list, $values); + + if ($valid) { + + if ($this->board->update($data)) { + $this->session->flash(t('Board updated successfully.')); + $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to update this board.')); + } + } + + $this->response->html($this->template->layout('board_edit', array( + 'errors' => $errors, + 'values' => $values + array('project_id' => $project_id), + 'columns' => $columns, + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Edit board') + ))); + } + + /** + * Validate and add a new column + * + * @access public + */ + public function add() + { + $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->project->getById($project_id); + + if (! $project) $this->notfound(); + + $columns = $this->board->getColumnsList($project_id); + $data = $this->request->getValues(); + $values = array(); + + foreach ($columns as $column_id => $column_title) { + $values['title['.$column_id.']'] = $column_title; + } + + list($valid, $errors) = $this->board->validateCreation($data); + + if ($valid) { + + if ($this->board->add($data)) { + $this->session->flash(t('Board updated successfully.')); + $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to update this board.')); + } + } + + $this->response->html($this->template->layout('board_edit', array( + 'errors' => $errors, + 'values' => $values + $data, + 'columns' => $columns, + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Edit board') + ))); + } + + /** + * Confirmation dialog before removing a column + * + * @access public + */ + public function confirm() + { + $this->response->html($this->template->layout('board_remove', array( + 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')), + 'menu' => 'projects', + 'title' => t('Remove a column from a board') + ))); + } + + /** + * Remove a column + * + * @access public + */ + public function remove() + { + $column = $this->board->getColumn($this->request->getIntegerParam('column_id')); + + if ($column && $this->board->removeColumn($column['id'])) { + $this->session->flash(t('Column removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this column.')); + } + + $this->response->redirect('?controller=board&action=edit&project_id='.$column['project_id']); + } + + /** + * Save the board (Ajax request made by the drag and drop) + * + * @access public + */ + public function save() + { + $project_id = $this->request->getIntegerParam('project_id'); + $values = $this->request->getValues(); + + if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { + $this->response->text('Not Authorized', 401); + } + + if (isset($values['positions'])) { + $this->board->saveTasksPosition($values['positions']); + } + + $this->response->html( + $this->template->load('board_show', array( + 'current_project_id' => $project_id, + 'board' => $this->board->get($project_id), + 'categories' => $this->category->getList($project_id, false), + )), + 201 + ); + } + + /** + * Check if the board have been changed + * + * @access public + */ + public function check() + { + $project_id = $this->request->getIntegerParam('project_id'); + $timestamp = $this->request->getIntegerParam('timestamp'); + + if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { + $this->response->text('Not Authorized', 401); + } + + if ($this->project->isModifiedSince($project_id, $timestamp)) { + $this->response->html( + $this->template->load('board_show', array( + 'current_project_id' => $project_id, + 'board' => $this->board->get($project_id), + 'categories' => $this->category->getList($project_id, false), + )) + ); + } + else { + $this->response->status(304); + } + } +} diff --git a/app/Controller/Category.php b/app/Controller/Category.php new file mode 100644 index 00000000..f96c1d4a --- /dev/null +++ b/app/Controller/Category.php @@ -0,0 +1,189 @@ +<?php + +namespace Controller; + +/** + * Categories management + * + * @package controller + * @author Frederic Guillot + */ +class Category extends Base +{ + /** + * Get the current project (common method between actions) + * + * @access private + * @return array + */ + private function getProject() + { + $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->project->getById($project_id); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + return $project; + } + + /** + * Get the category (common method between actions) + * + * @access private + * @return array + */ + private function getCategory($project_id) + { + $category = $this->category->getById($this->request->getIntegerParam('category_id')); + + if (! $category) { + $this->session->flashError(t('Category not found.')); + $this->response->redirect('?controller=category&action=index&project_id='.$project_id); + } + + return $category; + } + + /** + * List of categories for a given project + * + * @access public + */ + public function index() + { + $project = $this->getProject(); + + $this->response->html($this->template->layout('category_index', array( + 'categories' => $this->category->getList($project['id'], false), + 'values' => array('project_id' => $project['id']), + 'errors' => array(), + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Categories') + ))); + } + + /** + * Validate and save a new project + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->category->validateCreation($values); + + if ($valid) { + + if ($this->category->create($values)) { + $this->session->flash(t('Your category have been created successfully.')); + $this->response->redirect('?controller=category&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to create your category.')); + } + } + + $this->response->html($this->template->layout('category_index', array( + 'categories' => $this->category->getList($project['id'], false), + 'values' => $values, + 'errors' => $errors, + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Categories') + ))); + } + + /** + * Edit a category (display the form) + * + * @access public + */ + public function edit() + { + $project = $this->getProject(); + $category = $this->getCategory($project['id']); + + $this->response->html($this->template->layout('category_edit', array( + 'values' => $category, + 'errors' => array(), + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Categories') + ))); + } + + /** + * Edit a category (validate the form and update the database) + * + * @access public + */ + public function update() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->category->validateModification($values); + + if ($valid) { + + if ($this->category->update($values)) { + $this->session->flash(t('Your category have been updated successfully.')); + $this->response->redirect('?controller=category&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to update your category.')); + } + } + + $this->response->html($this->template->layout('category_edit', array( + 'values' => $values, + 'errors' => $errors, + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Categories') + ))); + } + + /** + * Confirmation dialog before removing a category + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + $category = $this->getCategory($project['id']); + + $this->response->html($this->template->layout('category_remove', array( + 'project' => $project, + 'category' => $category, + 'menu' => 'projects', + 'title' => t('Remove a category') + ))); + } + + /** + * Remove a category + * + * @access public + */ + public function remove() + { + $project = $this->getProject(); + $category = $this->getCategory($project['id']); + + if ($this->category->remove($category['id'])) { + $this->session->flash(t('Category removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this category.')); + } + + $this->response->redirect('?controller=category&action=index&project_id='.$project['id']); + } +} diff --git a/app/Controller/Comment.php b/app/Controller/Comment.php new file mode 100644 index 00000000..c9f226f7 --- /dev/null +++ b/app/Controller/Comment.php @@ -0,0 +1,189 @@ +<?php + +namespace Controller; + +/** + * Comment controller + * + * @package controller + * @author Frederic Guillot + */ +class Comment extends Base +{ + /** + * Forbidden page for comments + * + * @access public + */ + public function forbidden() + { + $this->response->html($this->template->layout('comment_forbidden', array( + 'menu' => 'tasks', + 'title' => t('Access Forbidden') + ))); + } + + /** + * Add a comment + * + * @access public + */ + public function save() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); + $values = $this->request->getValues(); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + list($valid, $errors) = $this->comment->validateCreation($values); + + if ($valid) { + + if ($this->comment->create($values)) { + $this->session->flash(t('Comment added successfully.')); + } + else { + $this->session->flashError(t('Unable to create your comment.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + } + + $this->showTask( + $task, + array('values' => $values, 'errors' => $errors) + ); + } + + /** + * Edit a comment + * + * @access public + */ + public function edit() + { + $task_id = $this->request->getIntegerParam('task_id'); + $comment_id = $this->request->getIntegerParam('comment_id'); + + $task = $this->task->getById($task_id, true); + $comment = $this->comment->getById($comment_id); + + if (! $task || ! $comment) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + if ($this->acl->isAdminUser() || $comment['user_id'] == $this->acl->getUserId()) { + + $this->showTask( + $task, + array(), + array(), + array('values' => array('id' => $comment['id']), 'errors' => array()) + ); + } + + $this->forbidden(); + } + + /** + * Update and validate a comment + * + * @access public + */ + public function update() + { + $task_id = $this->request->getIntegerParam('task_id'); + $comment_id = $this->request->getIntegerParam('comment_id'); + + $task = $this->task->getById($task_id, true); + $comment = $this->comment->getById($comment_id); + + $values = $this->request->getValues(); + + if (! $task || ! $comment) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + if ($this->acl->isAdminUser() || $comment['user_id'] == $this->acl->getUserId()) { + + list($valid, $errors) = $this->comment->validateModification($values); + + if ($valid) { + + if ($this->comment->update($values)) { + $this->session->flash(t('Comment updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your comment.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comment-'.$comment_id); + } + + $this->showTask( + $task, + array(), + array(), + array('values' => $values, 'errors' => $errors) + ); + } + + $this->forbidden(); + } + + /** + * Confirmation dialog before removing a comment + * + * @access public + */ + public function confirm() + { + $project_id = $this->request->getIntegerParam('project_id'); + $comment_id = $this->request->getIntegerParam('comment_id'); + + $this->checkProjectPermissions($project_id); + + $comment = $this->comment->getById($comment_id); + if (! $comment) $this->notfound(); + + if ($this->acl->isAdminUser() || $comment['user_id'] == $this->acl->getUserId()) { + + $this->response->html($this->template->layout('comment_remove', array( + 'comment' => $comment, + 'project_id' => $project_id, + 'menu' => 'tasks', + 'title' => t('Remove a comment') + ))); + } + + $this->forbidden(); + } + + /** + * Remove a comment + * + * @access public + */ + public function remove() + { + $project_id = $this->request->getIntegerParam('project_id'); + $comment_id = $this->request->getIntegerParam('comment_id'); + + $this->checkProjectPermissions($project_id); + + $comment = $this->comment->getById($comment_id); + if (! $comment) $this->notfound(); + + if ($this->acl->isAdminUser() || $comment['user_id'] == $this->acl->getUserId()) { + + if ($this->comment->remove($comment['id'])) { + $this->session->flash(t('Comment removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this comment.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$comment['task_id']); + } + + $this->forbidden(); + } +} diff --git a/app/Controller/Config.php b/app/Controller/Config.php new file mode 100644 index 00000000..b4a5b8d3 --- /dev/null +++ b/app/Controller/Config.php @@ -0,0 +1,117 @@ +<?php + +namespace Controller; + +/** + * Config controller + * + * @package controller + * @author Frederic Guillot + */ +class Config extends Base +{ + /** + * Display the settings page + * + * @access public + */ + public function index() + { + $this->response->html($this->template->layout('config_index', array( + 'db_size' => $this->config->getDatabaseSize(), + 'user' => $_SESSION['user'], + 'projects' => $this->project->getList(), + 'languages' => $this->config->getLanguages(), + 'values' => $this->config->getAll(), + 'errors' => array(), + 'menu' => 'config', + 'title' => t('Settings'), + 'timezones' => $this->config->getTimezones(), + 'remember_me_sessions' => $this->rememberMe->getAll($this->acl->getUserId()), + 'last_logins' => $this->lastLogin->getAll($this->acl->getUserId()), + ))); + } + + /** + * Validate and save settings + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->config->validateModification($values); + + if ($valid) { + + if ($this->config->save($values)) { + $this->config->reload(); + $this->session->flash(t('Settings saved successfully.')); + } else { + $this->session->flashError(t('Unable to save your settings.')); + } + + $this->response->redirect('?controller=config'); + } + + $this->response->html($this->template->layout('config_index', array( + 'db_size' => $this->config->getDatabaseSize(), + 'user' => $_SESSION['user'], + 'projects' => $this->project->getList(), + 'languages' => $this->config->getLanguages(), + 'values' => $values, + 'errors' => $errors, + 'menu' => 'config', + 'title' => t('Settings'), + 'timezones' => $this->config->getTimezones(), + 'remember_me_sessions' => $this->rememberMe->getAll($this->acl->getUserId()), + 'last_logins' => $this->lastLogin->getAll($this->acl->getUserId()), + ))); + } + + /** + * Download the Sqlite database + * + * @access public + */ + public function downloadDb() + { + $this->response->forceDownload('db.sqlite.gz'); + $this->response->binary($this->config->downloadDatabase()); + } + + /** + * Optimize the Sqlite database + * + * @access public + */ + public function optimizeDb() + { + $this->config->optimizeDatabase(); + $this->session->flash(t('Database optimization done.')); + $this->response->redirect('?controller=config'); + } + + /** + * Regenerate all application tokens + * + * @access public + */ + public function tokens() + { + $this->config->regenerateTokens(); + $this->session->flash(t('All tokens have been regenerated.')); + $this->response->redirect('?controller=config'); + } + + /** + * Remove a "RememberMe" token + * + * @access public + */ + public function removeRememberMeToken() + { + $this->rememberMe->remove($this->request->getIntegerParam('id')); + $this->response->redirect('?controller=config&action=index#remember-me'); + } +} diff --git a/app/Controller/Project.php b/app/Controller/Project.php new file mode 100644 index 00000000..5cb244a2 --- /dev/null +++ b/app/Controller/Project.php @@ -0,0 +1,375 @@ +<?php + +namespace Controller; + +use Model\Task; + +/** + * Project controller + * + * @package controller + * @author Frederic Guillot + */ +class Project extends Base +{ + /** + * Display access forbidden page + * + * @access public + */ + public function forbidden() + { + $this->response->html($this->template->layout('project_forbidden', array( + 'menu' => 'projects', + 'title' => t('Access Forbidden') + ))); + } + + /** + * Task search for a given project + * + * @access public + */ + public function search() + { + $project_id = $this->request->getIntegerParam('project_id'); + $search = $this->request->getStringParam('search'); + + $project = $this->project->getById($project_id); + $tasks = array(); + $nb_tasks = 0; + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + $this->checkProjectPermissions($project['id']); + + if ($search !== '') { + + $filters = array( + array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id), + 'or' => array( + array('column' => 'title', 'operator' => 'like', 'value' => '%'.$search.'%'), + //array('column' => 'description', 'operator' => 'like', 'value' => '%'.$search.'%'), + ) + ); + + $tasks = $this->task->find($filters); + $nb_tasks = count($tasks); + } + + $this->response->html($this->template->layout('project_search', array( + 'tasks' => $tasks, + 'nb_tasks' => $nb_tasks, + 'values' => array( + 'search' => $search, + 'controller' => 'project', + 'action' => 'search', + 'project_id' => $project['id'], + ), + 'menu' => 'projects', + 'project' => $project, + 'columns' => $this->board->getColumnsList($project_id), + 'categories' => $this->category->getList($project['id'], false), + 'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') + ))); + } + + /** + * List of completed tasks for a given project + * + * @access public + */ + public function tasks() + { + $project_id = $this->request->getIntegerParam('project_id'); + $project = $this->project->getById($project_id); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + $this->checkProjectPermissions($project['id']); + + $filters = array( + array('column' => 'project_id', 'operator' => 'eq', 'value' => $project_id), + array('column' => 'is_active', 'operator' => 'eq', 'value' => Task::STATUS_CLOSED), + ); + + $tasks = $this->task->find($filters); + $nb_tasks = count($tasks); + + $this->response->html($this->template->layout('project_tasks', array( + 'menu' => 'projects', + 'project' => $project, + 'columns' => $this->board->getColumnsList($project_id), + 'categories' => $this->category->getList($project['id'], false), + 'tasks' => $tasks, + 'nb_tasks' => $nb_tasks, + 'title' => $project['name'].' ('.$nb_tasks.')' + ))); + } + + /** + * List of projects + * + * @access public + */ + public function index() + { + $projects = $this->project->getAll(true, $this->acl->isRegularUser()); + $nb_projects = count($projects); + + $this->response->html($this->template->layout('project_index', array( + 'projects' => $projects, + 'nb_projects' => $nb_projects, + 'menu' => 'projects', + 'title' => t('Projects').' ('.$nb_projects.')' + ))); + } + + /** + * Display a form to create a new project + * + * @access public + */ + public function create() + { + $this->response->html($this->template->layout('project_new', array( + 'errors' => array(), + 'values' => array(), + 'menu' => 'projects', + 'title' => t('New project') + ))); + } + + /** + * Validate and save a new project + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->project->validateCreation($values); + + if ($valid) { + + if ($this->project->create($values)) { + $this->session->flash(t('Your project have been created successfully.')); + $this->response->redirect('?controller=project'); + } + else { + $this->session->flashError(t('Unable to create your project.')); + } + } + + $this->response->html($this->template->layout('project_new', array( + 'errors' => $errors, + 'values' => $values, + 'menu' => 'projects', + 'title' => t('New Project') + ))); + } + + /** + * Display a form to edit a project + * + * @access public + */ + public function edit() + { + $project = $this->project->getById($this->request->getIntegerParam('project_id')); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + $this->response->html($this->template->layout('project_edit', array( + 'errors' => array(), + 'values' => $project, + 'menu' => 'projects', + 'title' => t('Edit project') + ))); + } + + /** + * Validate and update a project + * + * @access public + */ + public function update() + { + $values = $this->request->getValues() + array('is_active' => 0); + list($valid, $errors) = $this->project->validateModification($values); + + if ($valid) { + + if ($this->project->update($values)) { + $this->session->flash(t('Project updated successfully.')); + $this->response->redirect('?controller=project'); + } + else { + $this->session->flashError(t('Unable to update this project.')); + } + } + + $this->response->html($this->template->layout('project_edit', array( + 'errors' => $errors, + 'values' => $values, + 'menu' => 'projects', + 'title' => t('Edit Project') + ))); + } + + /** + * Confirmation dialog before to remove a project + * + * @access public + */ + public function confirm() + { + $project = $this->project->getById($this->request->getIntegerParam('project_id')); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + $this->response->html($this->template->layout('project_remove', array( + 'project' => $project, + 'menu' => 'projects', + 'title' => t('Remove project') + ))); + } + + /** + * Remove a project + * + * @access public + */ + public function remove() + { + $project_id = $this->request->getIntegerParam('project_id'); + + if ($project_id && $this->project->remove($project_id)) { + $this->session->flash(t('Project removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this project.')); + } + + $this->response->redirect('?controller=project'); + } + + /** + * Enable a project + * + * @access public + */ + public function enable() + { + $project_id = $this->request->getIntegerParam('project_id'); + + if ($project_id && $this->project->enable($project_id)) { + $this->session->flash(t('Project activated successfully.')); + } else { + $this->session->flashError(t('Unable to activate this project.')); + } + + $this->response->redirect('?controller=project'); + } + + /** + * Disable a project + * + * @access public + */ + public function disable() + { + $project_id = $this->request->getIntegerParam('project_id'); + + if ($project_id && $this->project->disable($project_id)) { + $this->session->flash(t('Project disabled successfully.')); + } else { + $this->session->flashError(t('Unable to disable this project.')); + } + + $this->response->redirect('?controller=project'); + } + + /** + * Users list for the selected project + * + * @access public + */ + public function users() + { + $project = $this->project->getById($this->request->getIntegerParam('project_id')); + + if (! $project) { + $this->session->flashError(t('Project not found.')); + $this->response->redirect('?controller=project'); + } + + $this->response->html($this->template->layout('project_users', array( + 'project' => $project, + 'users' => $this->project->getAllUsers($project['id']), + 'menu' => 'projects', + 'title' => t('Edit project access list') + ))); + } + + /** + * Allow a specific user for the selected project + * + * @access public + */ + public function allow() + { + $values = $this->request->getValues(); + list($valid,) = $this->project->validateUserAccess($values); + + if ($valid) { + + if ($this->project->allowUser($values['project_id'], $values['user_id'])) { + $this->session->flash(t('Project updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update this project.')); + } + } + + $this->response->redirect('?controller=project&action=users&project_id='.$values['project_id']); + } + + /** + * Revoke user access + * + * @access public + */ + public function revoke() + { + $values = array( + 'project_id' => $this->request->getIntegerParam('project_id'), + 'user_id' => $this->request->getIntegerParam('user_id'), + ); + + list($valid,) = $this->project->validateUserAccess($values); + + if ($valid) { + + if ($this->project->revokeUser($values['project_id'], $values['user_id'])) { + $this->session->flash(t('Project updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update this project.')); + } + } + + $this->response->redirect('?controller=project&action=users&project_id='.$values['project_id']); + } +} diff --git a/app/Controller/Task.php b/app/Controller/Task.php new file mode 100644 index 00000000..2291ad43 --- /dev/null +++ b/app/Controller/Task.php @@ -0,0 +1,397 @@ +<?php + +namespace Controller; + +use Model\Project; + +/** + * Task controller + * + * @package controller + * @author Frederic Guillot + */ +class Task extends Base +{ + /** + * Webhook to create a task (useful for external software) + * + * @access public + */ + public function add() + { + $token = $this->request->getStringParam('token'); + + if ($this->config->get('webhooks_token') !== $token) { + $this->response->text('Not Authorized', 401); + } + + $defaultProject = $this->project->getFirst(); + + $values = array( + 'title' => $this->request->getStringParam('title'), + 'description' => $this->request->getStringParam('description'), + 'color_id' => $this->request->getStringParam('color_id', 'blue'), + 'project_id' => $this->request->getIntegerParam('project_id', $defaultProject['id']), + 'owner_id' => $this->request->getIntegerParam('owner_id'), + 'column_id' => $this->request->getIntegerParam('column_id'), + 'category_id' => $this->request->getIntegerParam('category_id'), + ); + + if ($values['column_id'] == 0) { + $values['column_id'] = $this->board->getFirstColumn($values['project_id']); + } + + list($valid,) = $this->task->validateCreation($values); + + if ($valid && $this->task->create($values)) { + $this->response->text('OK'); + } + + $this->response->text('FAILED'); + } + + /** + * Show a task + * + * @access public + */ + public function show() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + $this->showTask($task); + } + + /** + * Add a description from the show task page + * + * @access public + */ + public function description() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); + $values = $this->request->getValues(); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + list($valid, $errors) = $this->task->validateDescriptionCreation($values); + + if ($valid) { + + if ($this->task->update($values)) { + $this->session->flash(t('Task updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + } + + $this->showTask( + $task, + array(), + array('values' => $values, 'errors' => $errors) + ); + } + + /** + * Display a form to create a new task + * + * @access public + */ + public function create() + { + $project_id = $this->request->getIntegerParam('project_id'); + $this->checkProjectPermissions($project_id); + + $this->response->html($this->template->layout('task_new', array( + 'errors' => array(), + 'values' => array( + 'project_id' => $project_id, + 'column_id' => $this->request->getIntegerParam('column_id'), + 'color_id' => $this->request->getStringParam('color_id'), + 'owner_id' => $this->request->getIntegerParam('owner_id'), + 'another_task' => $this->request->getIntegerParam('another_task'), + ), + 'projects_list' => $this->project->getListByStatus(\Model\Project::ACTIVE), + 'columns_list' => $this->board->getColumnsList($project_id), + 'users_list' => $this->project->getUsersList($project_id), + 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($project_id), + 'menu' => 'tasks', + 'title' => t('New task') + ))); + } + + /** + * Validate and save a new task + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + $this->checkProjectPermissions($values['project_id']); + + list($valid, $errors) = $this->task->validateCreation($values); + + if ($valid) { + + if ($this->task->create($values)) { + $this->session->flash(t('Task created successfully.')); + + if (isset($values['another_task']) && $values['another_task'] == 1) { + unset($values['title']); + unset($values['description']); + $this->response->redirect('?controller=task&action=create&'.http_build_query($values)); + } + else { + $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); + } + } + else { + $this->session->flashError(t('Unable to create your task.')); + } + } + + $this->response->html($this->template->layout('task_new', array( + 'errors' => $errors, + 'values' => $values, + 'projects_list' => $this->project->getListByStatus(Project::ACTIVE), + 'columns_list' => $this->board->getColumnsList($values['project_id']), + 'users_list' => $this->project->getUsersList($values['project_id']), + 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($values['project_id']), + 'menu' => 'tasks', + 'title' => t('New task') + ))); + } + + /** + * Display a form to edit a task + * + * @access public + */ + public function edit() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id')); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + if (! empty($task['date_due'])) { + $task['date_due'] = date(t('m/d/Y'), $task['date_due']); + } + else { + $task['date_due'] = ''; + } + + $task['score'] = $task['score'] ?: ''; + + $this->response->html($this->template->layout('task_edit', array( + 'errors' => array(), + 'values' => $task, + 'columns_list' => $this->board->getColumnsList($task['project_id']), + 'users_list' => $this->project->getUsersList($task['project_id']), + 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($task['project_id']), + 'menu' => 'tasks', + 'title' => t('Edit a task') + ))); + } + + /** + * Validate and update a task + * + * @access public + */ + public function update() + { + $values = $this->request->getValues(); + $this->checkProjectPermissions($values['project_id']); + + list($valid, $errors) = $this->task->validateModification($values); + + if ($valid) { + + if ($this->task->update($values)) { + $this->session->flash(t('Task updated successfully.')); + $this->response->redirect('?controller=task&action=show&task_id='.$values['id']); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } + } + + $this->response->html($this->template->layout('task_edit', array( + 'errors' => $errors, + 'values' => $values, + 'columns_list' => $this->board->getColumnsList($values['project_id']), + 'users_list' => $this->project->getUsersList($values['project_id']), + 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($values['project_id']), + 'menu' => 'tasks', + 'title' => t('Edit a task') + ))); + } + + /** + * Hide a task + * + * @access public + */ + public function close() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id')); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + if ($this->task->close($task['id'])) { + $this->session->flash(t('Task closed successfully.')); + } else { + $this->session->flashError(t('Unable to close this task.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + } + + /** + * Confirmation dialog before to close a task + * + * @access public + */ + public function confirmClose() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + $this->response->html($this->taskLayout('task_close', array( + 'task' => $task, + 'menu' => 'tasks', + 'title' => t('Close a task') + ))); + } + + /** + * Open a task + * + * @access public + */ + public function open() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id')); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + if ($this->task->open($task['id'])) { + $this->session->flash(t('Task opened successfully.')); + } else { + $this->session->flashError(t('Unable to open this task.')); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + } + + /** + * Confirmation dialog before to open a task + * + * @access public + */ + public function confirmOpen() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + $this->response->html($this->taskLayout('task_open', array( + 'task' => $task, + 'menu' => 'tasks', + 'title' => t('Open a task') + ))); + } + + /** + * Remove a task + * + * @access public + */ + public function remove() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id')); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + if ($this->task->remove($task['id'])) { + $this->session->flash(t('Task removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this task.')); + } + + $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); + } + + /** + * Confirmation dialog before removing a task + * + * @access public + */ + public function confirmRemove() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id'), true); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + $this->response->html($this->taskLayout('task_remove', array( + 'task' => $task, + 'menu' => 'tasks', + 'title' => t('Remove a task') + ))); + } + + /** + * Duplicate a task (fill the form for a new task) + * + * @access public + */ + public function duplicate() + { + $task = $this->task->getById($this->request->getIntegerParam('task_id')); + + if (! $task) $this->notfound(); + $this->checkProjectPermissions($task['project_id']); + + if (! empty($task['date_due'])) { + $task['date_due'] = date(t('m/d/Y'), $task['date_due']); + } + else { + $task['date_due'] = ''; + } + + $task['score'] = $task['score'] ?: ''; + + $this->response->html($this->template->layout('task_new', array( + 'errors' => array(), + 'values' => $task, + 'projects_list' => $this->project->getListByStatus(Project::ACTIVE), + 'columns_list' => $this->board->getColumnsList($task['project_id']), + 'users_list' => $this->project->getUsersList($task['project_id']), + 'colors_list' => $this->task->getColors(), + 'categories_list' => $this->category->getList($task['project_id']), + 'duplicate' => true, + 'menu' => 'tasks', + 'title' => t('New task') + ))); + } +} diff --git a/app/Controller/User.php b/app/Controller/User.php new file mode 100644 index 00000000..e3fd8253 --- /dev/null +++ b/app/Controller/User.php @@ -0,0 +1,310 @@ +<?php + +namespace Controller; + +/** + * User controller + * + * @package controller + * @author Frederic Guillot + */ +class User extends Base +{ + /** + * Display access forbidden page + * + * @access public + */ + public function forbidden() + { + $this->response->html($this->template->layout('user_forbidden', array( + 'menu' => 'users', + 'title' => t('Access Forbidden') + ))); + } + + /** + * Logout and destroy session + * + * @access public + */ + public function logout() + { + $this->rememberMe->destroy($this->acl->getUserId()); + $this->session->close(); + $this->response->redirect('?controller=user&action=login'); + } + + /** + * Display the form login + * + * @access public + */ + public function login() + { + if (isset($_SESSION['user'])) $this->response->redirect('?controller=app'); + + $this->response->html($this->template->layout('user_login', array( + 'errors' => array(), + 'values' => array(), + 'no_layout' => true, + 'title' => t('Login') + ))); + } + + /** + * Check credentials + * + * @access public + */ + public function check() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->user->validateLogin($values); + + if ($valid) { + $this->response->redirect('?controller=app'); + } + + $this->response->html($this->template->layout('user_login', array( + 'errors' => $errors, + 'values' => $values, + 'no_layout' => true, + 'title' => t('Login') + ))); + } + + /** + * List all users + * + * @access public + */ + public function index() + { + $users = $this->user->getAll(); + $nb_users = count($users); + + $this->response->html( + $this->template->layout('user_index', array( + 'projects' => $this->project->getList(), + 'users' => $users, + 'nb_users' => $nb_users, + 'menu' => 'users', + 'title' => t('Users').' ('.$nb_users.')' + ))); + } + + /** + * Display a form to create a new user + * + * @access public + */ + public function create() + { + $this->response->html($this->template->layout('user_new', array( + 'projects' => $this->project->getList(), + 'errors' => array(), + 'values' => array(), + 'menu' => 'users', + 'title' => t('New user') + ))); + } + + /** + * Validate and save a new user + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->user->validateCreation($values); + + if ($valid) { + + if ($this->user->create($values)) { + $this->session->flash(t('User created successfully.')); + $this->response->redirect('?controller=user'); + } + else { + $this->session->flashError(t('Unable to create your user.')); + } + } + + $this->response->html($this->template->layout('user_new', array( + 'projects' => $this->project->getList(), + 'errors' => $errors, + 'values' => $values, + 'menu' => 'users', + 'title' => t('New user') + ))); + } + + /** + * Display a form to edit a user + * + * @access public + */ + public function edit() + { + $user = $this->user->getById($this->request->getIntegerParam('user_id')); + + if (! $user) $this->notfound(); + + if ($this->acl->isRegularUser() && $this->acl->getUserId() != $user['id']) { + $this->forbidden(); + } + + unset($user['password']); + + $this->response->html($this->template->layout('user_edit', array( + 'projects' => $this->project->filterListByAccess($this->project->getList(), $user['id']), + 'errors' => array(), + 'values' => $user, + 'menu' => 'users', + 'title' => t('Edit user') + ))); + } + + /** + * Validate and update a user + * + * @access public + */ + public function update() + { + $values = $this->request->getValues(); + + if ($this->acl->isAdminUser()) { + $values += array('is_admin' => 0); + } + else { + + if ($this->acl->getUserId() != $values['id']) { + $this->forbidden(); + } + + if (isset($values['is_admin'])) { + unset($values['is_admin']); // Regular users can't be admin + } + } + + list($valid, $errors) = $this->user->validateModification($values); + + if ($valid) { + + if ($this->user->update($values)) { + $this->session->flash(t('User updated successfully.')); + $this->response->redirect('?controller=user'); + } + else { + $this->session->flashError(t('Unable to update your user.')); + } + } + + $this->response->html($this->template->layout('user_edit', array( + 'projects' => $this->project->filterListByAccess($this->project->getList(), $values['id']), + 'errors' => $errors, + 'values' => $values, + 'menu' => 'users', + 'title' => t('Edit user') + ))); + } + + /** + * Confirmation dialog before to remove a user + * + * @access public + */ + public function confirm() + { + $user = $this->user->getById($this->request->getIntegerParam('user_id')); + + if (! $user) $this->notfound(); + + $this->response->html($this->template->layout('user_remove', array( + 'user' => $user, + 'menu' => 'users', + 'title' => t('Remove user') + ))); + } + + /** + * Remove a user + * + * @access public + */ + public function remove() + { + $user_id = $this->request->getIntegerParam('user_id'); + + if ($user_id && $this->user->remove($user_id)) { + $this->session->flash(t('User removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this user.')); + } + + $this->response->redirect('?controller=user'); + } + + /** + * Google authentication + * + * @access public + */ + public function google() + { + $code = $this->request->getStringParam('code'); + + if ($code) { + + $profile = $this->google->getGoogleProfile($code); + + if (is_array($profile)) { + + // If the user is already logged, link the account otherwise authenticate + if ($this->acl->isLogged()) { + + if ($this->google->updateUser($this->acl->getUserId(), $profile)) { + $this->session->flash(t('Your Google Account is linked to your profile successfully.')); + } + else { + $this->session->flashError(t('Unable to link your Google Account.')); + } + + $this->response->redirect('?controller=user'); + } + else if ($this->google->authenticate($profile['id'])) { + $this->response->redirect('?controller=app'); + } + else { + $this->response->html($this->template->layout('user_login', array( + 'errors' => array('login' => t('Google authentication failed')), + 'values' => array(), + 'no_layout' => true, + 'title' => t('Login') + ))); + } + } + } + + $this->response->redirect($this->google->getAuthorizationUrl()); + } + + /** + * Unlink a Google account + * + * @access public + */ + public function unlinkGoogle() + { + if ($this->google->unlink($this->acl->getUserId())) { + $this->session->flash(t('Your Google Account is not linked anymore to your profile.')); + } + else { + $this->session->flashError(t('Unable to unlink your Google Account.')); + } + + $this->response->redirect('?controller=user'); + } +} diff --git a/app/Core/Event.php b/app/Core/Event.php new file mode 100644 index 00000000..2c029b49 --- /dev/null +++ b/app/Core/Event.php @@ -0,0 +1,135 @@ +<?php + +namespace Core; + +/** + * Event dispatcher class + * + * @package core + * @author Frederic Guillot + */ +class Event +{ + /** + * Contains all listeners + * + * @access private + * @var array + */ + private $listeners = array(); + + /** + * The last listener executed + * + * @access private + * @var string + */ + private $lastListener = ''; + + /** + * The last triggered event + * + * @access private + * @var string + */ + private $lastEvent = ''; + + /** + * Triggered events list + * + * @access private + * @var array + */ + private $events = array(); + + /** + * Attach a listener object to an event + * + * @access public + * @param string $eventName Event name + * @param Listener $listener Object that implements the Listener interface + */ + public function attach($eventName, Listener $listener) + { + if (! isset($this->listeners[$eventName])) { + $this->listeners[$eventName] = array(); + } + + $this->listeners[$eventName][] = $listener; + } + + /** + * Trigger an event + * + * @access public + * @param string $eventName Event name + * @param array $data Event data + */ + public function trigger($eventName, array $data) + { + $this->lastEvent = $eventName; + $this->events[] = $eventName; + + if (isset($this->listeners[$eventName])) { + foreach ($this->listeners[$eventName] as $listener) { + if ($listener->execute($data)) { + $this->lastListener = get_class($listener); + } + } + } + } + + /** + * Get the last listener executed + * + * @access public + * @return string Event name + */ + public function getLastListenerExecuted() + { + return $this->lastListener; + } + + /** + * Get the last fired event + * + * @access public + * @return string Event name + */ + public function getLastTriggeredEvent() + { + return $this->lastEvent; + } + + /** + * Get a list of triggered events + * + * @access public + * @return array + */ + public function getTriggeredEvents() + { + return $this->events; + } + + /** + * Check if a listener bind to an event + * + * @access public + * @param string $eventName Event name + * @param mixed $instance Instance name or object itself + * @return bool Yes or no + */ + public function hasListener($eventName, $instance) + { + if (isset($this->listeners[$eventName])) { + foreach ($this->listeners[$eventName] as $listener) { + if ($listener instanceof $instance) { + return true; + } + } + } + + return false; + } +} diff --git a/app/Core/Listener.php b/app/Core/Listener.php new file mode 100644 index 00000000..b8bdd680 --- /dev/null +++ b/app/Core/Listener.php @@ -0,0 +1,17 @@ +<?php + +namespace Core; + +/** + * Event listener interface + * + * @package core + * @author Frederic Guillot + */ +interface Listener { + + /** + * @return boolean + */ + public function execute(array $data); +} diff --git a/app/Core/Loader.php b/app/Core/Loader.php new file mode 100644 index 00000000..7c437654 --- /dev/null +++ b/app/Core/Loader.php @@ -0,0 +1,37 @@ +<?php + +namespace Core; + +/** + * Loader class + * + * @package core + * @author Frederic Guillot + */ +class Loader +{ + /** + * Load the missing class + * + * @access public + * @param string $class Class name + */ + public function load($class) + { + $filename = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php'; + + if (file_exists($filename)) { + require $filename; + } + } + + /** + * Register the autoloader + * + * @access public + */ + public function execute() + { + spl_autoload_register(array($this, 'load')); + } +} diff --git a/app/Core/Registry.php b/app/Core/Registry.php new file mode 100644 index 00000000..f11d427c --- /dev/null +++ b/app/Core/Registry.php @@ -0,0 +1,79 @@ +<?php + +namespace Core; + +/** + * The registry class is a dependency injection container + * + * @package core + * @author Frederic Guillot + */ +class Registry +{ + /** + * Contains all dependencies + * + * @access private + * @var array + */ + private $container = array(); + + /** + * Contains all instances + * + * @access private + * @var array + */ + private $instances = array(); + + /** + * Set a dependency + * + * @access public + * @param string $name Unique identifier for the service/parameter + * @param mixed $value The value of the parameter or a closure to define an object + */ + public function __set($name, $value) + { + $this->container[$name] = $value; + } + + /** + * Get a dependency + * + * @access public + * @param string $name Unique identifier for the service/parameter + * @return mixed The value of the parameter or an object + * @throws RuntimeException If the identifier is not found + */ + public function __get($name) + { + if (isset($this->container[$name])) { + + if (is_callable($this->container[$name])) { + return $this->container[$name](); + } + else { + return $this->container[$name]; + } + } + + throw new \RuntimeException('Identifier not found in the registry: '.$name); + } + + /** + * Return a shared instance of a dependency + * + * @access public + * @param string $name Unique identifier for the service/parameter + * @return mixed Same object instance of the dependency + */ + public function shared($name) + { + if (! isset($this->instances[$name])) { + $this->instances[$name] = $this->$name; + } + + return $this->instances[$name]; + } +} diff --git a/app/Core/Request.php b/app/Core/Request.php new file mode 100644 index 00000000..df8ea41a --- /dev/null +++ b/app/Core/Request.php @@ -0,0 +1,56 @@ +<?php + +namespace Core; + +class Request +{ + public function getStringParam($name, $default_value = '') + { + return isset($_GET[$name]) ? $_GET[$name] : $default_value; + } + + public function getIntegerParam($name, $default_value = 0) + { + return isset($_GET[$name]) && ctype_digit($_GET[$name]) ? (int) $_GET[$name] : $default_value; + } + + public function getValue($name) + { + $values = $this->getValues(); + return isset($values[$name]) ? $values[$name] : null; + } + + public function getValues() + { + if (! empty($_POST)) return $_POST; + + $result = json_decode($this->getBody(), true); + if ($result) return $result; + + return array(); + } + + public function getBody() + { + return file_get_contents('php://input'); + } + + public function getFileContent($name) + { + if (isset($_FILES[$name])) { + return file_get_contents($_FILES[$name]['tmp_name']); + } + + return ''; + } + + public function isPost() + { + return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST'; + } + + public function isAjax() + { + return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'; + } +} diff --git a/app/Core/Response.php b/app/Core/Response.php new file mode 100644 index 00000000..a5f0e4dc --- /dev/null +++ b/app/Core/Response.php @@ -0,0 +1,138 @@ +<?php + +namespace Core; + +class Response +{ + public function forceDownload($filename) + { + header('Content-Disposition: attachment; filename="'.$filename.'"'); + } + + /** + * @param integer $status_code + */ + public function status($status_code) + { + header('Status: '.$status_code); + header($_SERVER['SERVER_PROTOCOL'].' '.$status_code); + } + + public function redirect($url) + { + header('Location: '.$url); + exit; + } + + public function json(array $data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: application/json'); + echo json_encode($data); + + exit; + } + + public function text($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/plain; charset=utf-8'); + echo $data; + + exit; + } + + public function html($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/html; charset=utf-8'); + echo $data; + + exit; + } + + public function xml($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/xml; charset=utf-8'); + echo $data; + + exit; + } + + public function js($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/javascript; charset=utf-8'); + echo $data; + + exit; + } + + public function binary($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Transfer-Encoding: binary'); + header('Content-Type: application/octet-stream'); + echo $data; + + exit; + } + + public function csp(array $policies = array()) + { + $policies['default-src'] = "'self'"; + $values = ''; + + foreach ($policies as $policy => $hosts) { + + if (is_array($hosts)) { + + $acl = ''; + + foreach ($hosts as &$host) { + + if ($host === '*' || $host === 'self' || strpos($host, 'http') === 0) { + $acl .= $host.' '; + } + } + } + else { + + $acl = $hosts; + } + + $values .= $policy.' '.trim($acl).'; '; + } + + header('Content-Security-Policy: '.$values); + } + + public function nosniff() + { + header('X-Content-Type-Options: nosniff'); + } + + public function xss() + { + header('X-XSS-Protection: 1; mode=block'); + } + + public function hsts() + { + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') { + header('Strict-Transport-Security: max-age=31536000'); + } + } + + public function xframe($mode = 'DENY', array $urls = array()) + { + header('X-Frame-Options: '.$mode.' '.implode(' ', $urls)); + } +} diff --git a/app/Core/Router.php b/app/Core/Router.php new file mode 100644 index 00000000..3a5df715 --- /dev/null +++ b/app/Core/Router.php @@ -0,0 +1,111 @@ +<?php + +namespace Core; + +/** + * Router class + * + * @package core + * @author Frederic Guillot + */ +class Router +{ + /** + * Controller name + * + * @access private + * @var string + */ + private $controller = ''; + + /** + * Action name + * + * @access private + * @var string + */ + private $action = ''; + + /** + * Registry instance + * + * @access private + * @var Core\Registry + */ + private $registry; + + /** + * Constructor + * + * @access public + * @param Core\Registry $registry Registry instance + * @param string $controller Controller name + * @param string $action Action name + */ + public function __construct(Registry $registry, $controller = '', $action = '') + { + $this->registry = $registry; + $this->controller = empty($_GET['controller']) ? $controller : $_GET['controller']; + $this->action = empty($_GET['action']) ? $controller : $_GET['action']; + } + + /** + * Check controller and action parameter + * + * @access public + * @param string $value Controller or action name + * @param string $default_value Default value if validation fail + */ + public function sanitize($value, $default_value) + { + return ! ctype_alpha($value) || empty($value) ? $default_value : strtolower($value); + } + + /** + * Load a controller and execute the action + * + * @access public + * @param string $filename Controller filename + * @param string $class Class name + * @param string $method Method name + */ + public function load($filename, $class, $method) + { + if (file_exists($filename)) { + + require $filename; + + if (! method_exists($class, $method)) { + return false; + } + + $instance = new $class($this->registry); + $instance->request = new Request; + $instance->response = new Response; + $instance->session = new Session; + $instance->template = new Template; + $instance->beforeAction($this->controller, $this->action); + $instance->$method(); + + return true; + } + + return false; + } + + /** + * Find a route + * + * @access public + */ + public function execute() + { + $this->controller = $this->sanitize($this->controller, 'app'); + $this->action = $this->sanitize($this->action, 'index'); + $filename = __DIR__.'/../Controller/'.ucfirst($this->controller).'.php'; + + if (! $this->load($filename, '\Controller\\'.$this->controller, $this->action)) { + die('Page not found!'); + } + } +} diff --git a/app/Core/Session.php b/app/Core/Session.php new file mode 100644 index 00000000..0c3ec2d9 --- /dev/null +++ b/app/Core/Session.php @@ -0,0 +1,56 @@ +<?php + +namespace Core; + +class Session +{ + const SESSION_LIFETIME = 86400; // 1 day + + public function open($base_path = '/', $save_path = '') + { + if ($save_path !== '') session_save_path($save_path); + + // HttpOnly and secure flags for session cookie + session_set_cookie_params( + self::SESSION_LIFETIME, + $base_path ?: '/', + null, + isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', + true + ); + + // Avoid session id in the URL + ini_set('session.use_only_cookies', '1'); + + // Ensure session ID integrity + ini_set('session.entropy_file', '/dev/urandom'); + ini_set('session.entropy_length', '32'); + ini_set('session.hash_bits_per_character', 6); + + // Custom session name + session_name('__S'); + + session_start(); + + // Regenerate the session id to avoid session fixation issue + if (empty($_SESSION['__validated'])) { + session_regenerate_id(true); + $_SESSION['__validated'] = 1; + } + } + + public function close() + { + session_destroy(); + } + + public function flash($message) + { + $_SESSION['flash_message'] = $message; + } + + public function flashError($message) + { + $_SESSION['flash_error_message'] = $message; + } +} diff --git a/app/Core/Template.php b/app/Core/Template.php new file mode 100644 index 00000000..8740a685 --- /dev/null +++ b/app/Core/Template.php @@ -0,0 +1,72 @@ +<?php + +namespace Core; + +/** + * Template class + * + * @package core + * @author Frederic Guillot + */ +class Template +{ + /** + * Template path + * + * @var string + */ + const PATH = 'app/Templates/'; + + /** + * Load a template + * + * Example: + * + * $template->load('template_name', ['bla' => 'value']); + * + * @access public + * @return string + */ + public function load() + { + if (func_num_args() < 1 || func_num_args() > 2) { + die('Invalid template arguments'); + } + + if (! file_exists(self::PATH.func_get_arg(0).'.php')) { + die('Unable to load the template: "'.func_get_arg(0).'"'); + } + + if (func_num_args() === 2) { + + if (! is_array(func_get_arg(1))) { + die('Template variables must be an array'); + } + + extract(func_get_arg(1)); + } + + ob_start(); + + include self::PATH.func_get_arg(0).'.php'; + + return ob_get_clean(); + } + + /** + * Render a page layout + * + * @access public + * @param string $template_name Template name + * @param array $template_args Key/value map + * @param string $layout_name Layout name + * @return string + */ + public function layout($template_name, array $template_args = array(), $layout_name = 'layout') + { + return $this->load( + $layout_name, + $template_args + array('content_for_layout' => $this->load($template_name, $template_args)) + ); + } +} diff --git a/app/Core/Translator.php b/app/Core/Translator.php new file mode 100644 index 00000000..be0be66a --- /dev/null +++ b/app/Core/Translator.php @@ -0,0 +1,155 @@ +<?php + +namespace Core; + +/** + * Translator class + * + * @package core + * @author Frederic Guillot + */ +class Translator +{ + /** + * Locales path + * + * @var string + */ + const PATH = 'app/Locales/'; + + /** + * Locales + * + * @static + * @access private + * @var array + */ + private static $locales = array(); + + /** + * Get a translation + * + * $translator->translate('I have %d kids', 5); + * + * @access public + * @return string + */ + public function translate($identifier) + { + $args = func_get_args(); + + array_shift($args); + array_unshift($args, $this->get($identifier, $identifier)); + + foreach ($args as &$arg) { + $arg = htmlspecialchars($arg, ENT_QUOTES, 'UTF-8', false); + } + + return call_user_func_array( + 'sprintf', + $args + ); + } + + /** + * Get a formatted number + * + * $translator->number(1234.56); + * + * @access public + * @param float $number Number to format + * @return string + */ + public function number($number) + { + return number_format( + $number, + $this->get('number.decimals', 2), + $this->get('number.decimals_separator', '.'), + $this->get('number.thousands_separator', ',') + ); + } + + /** + * Get a formatted currency number + * + * $translator->currency(1234.56); + * + * @access public + * @param float $amount Number to format + * @return string + */ + public function currency($amount) + { + $position = $this->get('currency.position', 'before'); + $symbol = $this->get('currency.symbol', '$'); + $str = ''; + + if ($position === 'before') { + $str .= $symbol; + } + + $str .= $this->number($amount); + + if ($position === 'after') { + $str .= ' '.$symbol; + } + + return $str; + } + + /** + * Get a formatted datetime + * + * $translator->datetime('%Y-%m-%d', time()); + * + * @access public + * @param string $format Format defined by the strftime function + * @param integer $timestamp Unix timestamp + * @return string + */ + public function datetime($format, $timestamp) + { + if (! $timestamp) { + return ''; + } + + return strftime($this->get($format, $format), (int) $timestamp); + } + + /** + * Get an identifier from the translations or return the default + * + * @access public + * @param string $idendifier Locale identifier + * @param string $default Default value + * @return string + */ + public function get($identifier, $default = '') + { + if (isset(self::$locales[$identifier])) { + return self::$locales[$identifier]; + } + else { + return $default; + } + } + + /** + * Load translations + * + * @static + * @access public + * @param string $language Locale code: fr_FR + */ + public static function load($language) + { + setlocale(LC_TIME, $language.'.UTF-8', $language); + + $filename = self::PATH.$language.DIRECTORY_SEPARATOR.'translations.php'; + + if (file_exists($filename)) { + self::$locales = require $filename; + } + } +} diff --git a/app/Event/TaskModification.php b/app/Event/TaskModification.php new file mode 100644 index 00000000..b1d412c7 --- /dev/null +++ b/app/Event/TaskModification.php @@ -0,0 +1,51 @@ +<?php + +namespace Event; + +use Core\Listener; +use Model\Project; + +/** + * Task modification listener + * + * @package events + * @author Frederic Guillot + */ +class TaskModification implements Listener +{ + /** + * Project model + * + * @accesss private + * @var \Model\Project + */ + private $project; + + /** + * Constructor + * + * @access public + * @param \Model\Project $project Project model instance + */ + public function __construct(Project $project) + { + $this->project = $project; + } + + /** + * 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) + { + if (isset($data['project_id'])) { + $this->project->updateModificationDate($data['project_id']); + return true; + } + + return false; + } +} diff --git a/app/Locales/es_ES/translations.php b/app/Locales/es_ES/translations.php new file mode 100644 index 00000000..ce797972 --- /dev/null +++ b/app/Locales/es_ES/translations.php @@ -0,0 +1,334 @@ +<?php + +return array( + 'English' => 'Inglés', + 'French' => 'Francés', + 'Polish' => 'Polaco', + 'Portuguese (Brazilian)' => 'Portugués (Brasil)', + 'Spanish' => 'Español', + 'None' => 'Ninguno', + 'edit' => 'modificar', + 'Edit' => 'Modificar', + 'remove' => 'suprimir', + 'Remove' => 'Suprimir', + 'Update' => 'Actualizar', + 'Yes' => 'Si', + 'No' => 'No', + 'cancel' => 'cancelar', + 'or' => 'o', + 'Yellow' => 'Amarillo', + 'Blue' => 'Azul', + 'Green' => 'Verde', + 'Purple' => 'Púrpura', + 'Red' => 'Rojo', + 'Orange' => 'Naranja', + 'Grey' => 'Gris', + 'Save' => 'Guardar', + 'Login' => 'Iniciar sesión', + 'Official website:' => 'Pagina web oficial :', + 'Unassigned' => 'No asignado', + 'View this task' => 'Ver esta tarea', + 'Remove user' => 'Eliminar un usuario', + 'Do you really want to remove this user: "%s"?' => '¿Realmente desea suprimir este usuario: « %s » ?', + 'New user' => 'Añadir un usuario', + 'All users' => 'Todos los usuarios', + 'Username' => 'Nombre de usuario', + 'Password' => 'Contraseña', + 'Default Project' => 'Proyecto por defecto', + 'Administrator' => 'Administrador', + 'Sign in' => 'Iniciar sesión', + 'Users' => 'Usuarios', + 'No user' => 'Ningún usuario', + 'Forbidden' => 'Acceso denegado', + 'Access Forbidden' => 'Acceso denegado', + 'Only administrators can access to this page.' => 'Solo los administradores pueden acceder a esta pagina.', + 'Edit user' => 'Editar un usuario', + 'Logout' => 'Salir', + 'Bad username or password' => 'Usuario o contraseña incorecto', + 'users' => 'usuarios', + 'projects' => 'proyectos', + 'Edit project' => 'Editar el proyecto', + 'Name' => 'Nombre', + 'Activated' => 'Activado', + 'Projects' => 'Proyectos', + 'No project' => 'Ningún proyecto', + 'Project' => 'Proyecto', + 'Status' => 'Estado', + 'Tasks' => 'Tareas', + 'Board' => 'Tablero', + 'Inactive' => 'Inactivo', + 'Active' => 'Activo', + 'Column %d' => 'Columna %d', + 'Add this column' => 'Añadir esta columna', + '%d tasks on the board' => '%d tareas en el tablero', + '%d tasks in total' => '%d tareas en total', + 'Unable to update this board.' => 'No se puede actualizar este tablero.', + 'Edit board' => 'Editar este tablero', + 'Disable' => 'Desactivar', + 'Enable' => 'Activar', + 'New project' => 'Nuevo proyecto', + 'Do you really want to remove this project: "%s"?' => '¿Realmente desea eliminar este proyecto: « %s » ?', + 'Remove project' => 'Suprimir el proyecto', + 'Boards' => 'Tableros', + 'Edit the board for "%s"' => 'Modificar el tablero por « %s »', + 'All projects' => 'Todos los proyectos', + 'Change columns' => 'Cambiar las columnas', + 'Add a new column' => 'Añadir una nueva columna', + 'Title' => 'Titulo', + 'Add Column' => 'Nueva columna', + 'Project "%s"' => 'Proyecto « %s »', + 'Nobody assigned' => 'Persona asignada', + 'Assigned to %s' => 'Asignada a %s', + 'Remove a column' => 'Suprimir esta columna', + 'Remove a column from a board' => 'Suprimir una columna de un tablero', + 'Unable to remove this column.' => 'No se puede suprimir esta columna.', + 'Do you really want to remove this column: "%s"?' => '¿Realmente desea eliminar esta columna : « %s » ?', + 'This action will REMOVE ALL TASKS associated to this column!' => '¡Esta acción suprimirá todas las tareas asociadas a esta columna!', + 'Settings' => 'Preferencias', + 'Application settings' => 'Parámetros de la aplicación', + 'Language' => 'Idioma', + 'Webhooks token:' => 'Identificador (token) para los webhooks :', + 'More information' => 'Más informaciones', + 'Database size:' => 'Tamaño de la base de datos:', + 'Download the database' => 'Descargar la base de datos', + 'Optimize the database' => 'Optimisar la base de datos', + '(VACUUM command)' => '(Comando VACUUM)', + '(Gzip compressed Sqlite file)' => '(Archivo Sqlite comprimido en Gzip)', + 'User settings' => 'Parámetros de usuario', + 'My default project:' => 'Mi proyecto por defecto : ', + 'Close a task' => 'Cerrar una tarea', + 'Do you really want to close this task: "%s"?' => '¿Realmente desea cerrar esta tarea: « %s » ?', + 'Edit a task' => 'Editar una tarea', + 'Column' => 'Columna', + 'Color' => 'Coulor', + 'Assignee' => 'Persona asignada', + 'Create another task' => 'Crear una nueva tarea', + 'New task' => 'Nueva tarea', + 'Open a task' => 'Abrir una tarea', + 'Do you really want to open this task: "%s"?' => '¿Realomente desea abrir esta tarea: « %s » ?', + 'Back to the board' => 'Volver al tablero', + 'Created on %B %e, %G at %k:%M %p' => 'Creado el %d/%m/%Y a las %H:%M', + 'There is nobody assigned' => 'No hay nadie asignado a esta tarea', + 'Column on the board:' => 'Columna en el tablero: ', + 'Status is open' => 'Estado abierto', + 'Status is closed' => 'Estado cerrado', + 'Close this task' => 'Cerrar esta tarea', + 'Open this task' => 'Abrir esta tarea', + 'There is no description.' => 'No hay descripción.', + 'Add a new task' => 'Añadir una nueva tarea', + 'The username is required' => 'El nombre de usuario es obligatorio', + 'The maximum length is %d characters' => 'La longitud máxima es de %d caractères', + 'The minimum length is %d characters' => 'La longitud mÃnima es de %d caractères', + 'The password is required' => 'La contraseña es obligatoria', + 'This value must be an integer' => 'Este valor debe ser un entero', + 'The username must be unique' => 'El nombre de usuario debe ser único', + 'The username must be alphanumeric' => 'El nombre de usuario debe ser alfanumérico', + 'The user id is required' => 'El identificador del usuario es obligatorio', + 'Passwords doesn\'t matches' => 'Las contraseñas no corresponden', + 'The confirmation is required' => 'La confirmación es obligatoria', + 'The column is required' => 'La columna es obligatoria', + 'The project is required' => 'El proyecto es obligatorio', + 'The color is required' => 'El color es obligatorio', + 'The id is required' => 'El identificador es obligatorio', + 'The project id is required' => 'El identificador del proyecto es obligatorio', + 'The project name is required' => 'El nombre del proyecto es obligatorio', + 'This project must be unique' => 'El nombre del proyecto debe ser único', + 'The title is required' => 'El titulo es obligatorio', + 'The language is required' => 'El idioma es obligatorio', + 'There is no active project, the first step is to create a new project.' => 'No hay proyectos activados, la primera etapa consiste en crear un nuevo proyecto.', + 'Settings saved successfully.' => 'Parámetros guardados.', + 'Unable to save your settings.' => 'No se puede guardar sus parámetros.', + 'Database optimization done.' => 'Optimización de la base de datos terminada.', + 'Your project have been created successfully.' => 'El proyecto ha sido creado.', + 'Unable to create your project.' => 'No se puede crear el proyecto.', + 'Project updated successfully.' => 'El proyecto ha sido actualizado.', + 'Unable to update this project.' => 'No se puede actualizar el proyecto.', + 'Unable to remove this project.' => 'No se puede suprimir este proyecto.', + 'Project removed successfully.' => 'El proyecto ha sido borrado.', + 'Project activated successfully.' => 'El proyecto ha sido activado.', + 'Unable to activate this project.' => 'No se puede activar el proyecto.', + 'Project disabled successfully.' => 'El proyecto ha sido desactivado.', + 'Unable to disable this project.' => 'No se puede desactivar el proyecto.', + 'Unable to open this task.' => 'No se puede abrir esta tarea.', + 'Task opened successfully.' => 'La tarea ha sido abierta.', + 'Unable to close this task.' => 'No se puede cerrar esta tarea.', + 'Task closed successfully.' => 'La tarea ha sido cerrada.', + 'Unable to update your task.' => 'No se puede modificar esta tarea.', + 'Task updated successfully.' => 'La tarea ha sido actualizada.', + 'Unable to create your task.' => 'No se puede crear esta tarea.', + 'Task created successfully.' => 'La tarea ha sido creada.', + 'User created successfully.' => 'El usuario ha sido creado.', + 'Unable to create your user.' => 'No se puede crear este usuario.', + 'User updated successfully.' => 'El usuario ha sido actualizado.', + 'Unable to update your user.' => 'No se puede actualizar este usuario.', + 'User removed successfully.' => 'El usuario ha sido creado.', + 'Unable to remove this user.' => 'No se puede crear este usuario.', + 'Board updated successfully.' => 'El tablero ha sido actualizado.', + 'Ready' => 'Listo', + 'Backlog' => 'En espera', + 'Work in progress' => 'En curso', + 'Done' => 'Terminado', + 'Application version:' => 'Versión de la aplicación:', + 'Completed on %B %e, %G at %k:%M %p' => 'Terminado el %d/%m/%Y a las %H:%M', + '%B %e, %G at %k:%M %p' => '%d/%m/%Y a las %H:%M', + 'Date created' => 'Fecha de creación', + 'Date completed' => 'Fecha terminada', + 'Id' => 'Identificador', + 'No task' => 'Ninguna tarea', + 'Completed tasks' => 'Tareas terminadas', + 'List of projects' => 'Lista de los proyectos', + 'Completed tasks for "%s"' => 'Tarea completada por « %s »', + '%d closed tasks' => '%d tareas completadas', + 'no task for this project' => 'ninguna tarea para este proyecto', + 'Public link' => 'Enlace publico', + 'There is no column in your project!' => '¡No hay ninguna columna para este proyecto!', + 'Change assignee' => 'Cambiar la persona asignada', + 'Change assignee for the task "%s"' => 'Cambiar la persona asignada por la tarea « %s »', + 'Timezone' => 'Zona horaria', + 'Sorry, I didn\'t found this information in my database!' => 'Lo siento no he encontrado información en la base de datos!', + 'Page not found' => 'Pagina no encontrada', + 'Story Points' => 'Complejidad', + 'limit' => 'limite', + 'Task limit' => 'Número máximo de tareas', + 'This value must be greater than %d' => 'Este valor no debe ser más grande que %d', + 'Edit project access list' => 'Editar los permisos del proyecto', + 'Edit users access' => 'Editar los permisos de usuario', + 'Allow this user' => 'Autorizar este usuario', + 'Project access list for "%s"' => 'Permisos del proyecto « %s »', + 'Only those users have access to this project:' => 'Solo estos usuarios tienen acceso a este proyecto:', + 'Don\'t forget that administrators have access to everything.' => 'No olvide que los administradores tienen acceso a todo', + 'revoke' => 'revocar', + 'List of authorized users' => 'Lista de los usuarios autorizados', + 'User' => 'Usuario', + 'Everybody have access to this project.' => 'Todo el mundo tiene acceso al proyecto.', + 'You are not allowed to access to this project.' => 'No esta autorizado a acceder a este proyecto.', + 'Comments' => 'Comentarios', + 'Post comment' => 'Commentar', + 'Write your text in Markdown' => 'Redacta el texto en Markdown', + 'Leave a comment' => 'Dejar un comentario', + 'Comment is required' => 'El comentario es obligatorio', + 'Leave a description' => 'Dejar una descripción', + 'Comment added successfully.' => 'El comentario ha sido añadido.', + 'Unable to create your comment.' => 'No se puede crear este comentario.', + 'The description is required' => 'La descripción es obligatoria', + 'Edit this task' => 'Editar esta tarea', + 'Due Date' => 'Fecha lÃmite', + 'm/d/Y' => 'd/m/Y', // Date format parsed with php + 'month/day/year' => 'dÃa/mes/año', // Help shown to the user + 'Invalid date' => 'Fecha no valida', + 'Must be done before %B %e, %G' => 'Debe estar hecho antes del %d/%m/%Y', + '%B %e, %G' => '%d/%m/%Y', + 'Automatic actions' => 'Acciones automatizadas', + 'Your automatic action have been created successfully.' => 'La acción automatizada ha sido creada', + 'Unable to create your automatic action.' => 'No se puede crear esta acción automatizada.', + 'Remove an action' => 'Suprimir una acción', + 'Unable to remove this action.' => 'No se puede suprimir esta accción', + 'Action removed successfully.' => 'La acción ha sido borrada.', + 'Automatic actions for the project "%s"' => 'Acciones automatizadas para este proyecto « %s »', + 'Defined actions' => 'Acciones definidas', + 'Event name' => 'Nombre del evento', + 'Action name' => 'Nombre de la acción', + 'Action parameters' => 'Parámetros de la acción', + 'Action' => 'Acción', + 'Event' => 'Evento', + 'When the selected event occurs execute the corresponding action.' => 'Cuando el evento seleccionado ocurre ejecutar la acción correspondiente.', + 'Next step' => 'Etapa siguiente', + 'Define action parameters' => 'Definición de los parametros de la acción', + 'Save this action' => 'Guardar esta acción', + 'Do you really want to remove this action: "%s"?' => '¿Realmente desea suprimir esta acción « %s » ?', + 'Remove an automatic action' => 'Suprimir una acción automatizada', + 'Close the task' => 'Cerrar esta tarea', + 'Assign the task to a specific user' => 'Asignar una tarea a un usuario especifico', + 'Assign the task to the person who does the action' => 'Asignar la tarea al usuario que hace la acción', + 'Duplicate the task to another project' => 'Duplicar la tarea a otro proyecto', + 'Move a task to another column' => 'Mover una tarea a otra columna', + 'Move a task to another position in the same column' => 'Mover una tarea a otra posición en la misma columna', + 'Task modification' => 'Modificación de una tarea', + 'Task creation' => 'Creación de una tarea', + 'Open a closed task' => 'Abrir una tarea cerrada', + 'Closing a task' => 'Cerrar una tarea', + 'Assign a color to a specific user' => 'Asignar un color a un usuario especifico', + 'Column title' => 'Titulo de la columna', + 'Position' => 'Posición', + 'Move Up' => 'Mover hacia arriba', + 'Move Down' => 'Mover hacia abajo', + 'Duplicate to another project' => 'Duplicar a otro proyecto', + 'Duplicate' => 'Duplicar', + 'link' => 'enlace', + 'Update this comment' => 'Actualizar este comentario', + 'Comment updated successfully.' => 'El comentario ha sido actualizado.', + 'Unable to update your comment.' => 'No se puede actualizar este comentario.', + 'Remove a comment' => 'Suprimir un comentario', + 'Comment removed successfully.' => 'El comentario ha sido suprimido.', + 'Unable to remove this comment.' => 'No se puede suprimir este comentario.', + 'Do you really want to remove this comment?' => '¿Desea suprimir este comentario?', + 'Only administrators or the creator of the comment can access to this page.' => 'Solo los administradores o el autor del comentario tienen acceso a esta pagina.', + 'Details' => 'Detalles', + 'Current password for the user "%s"' => 'Contraseña actual para el usuario: « %s »', + 'The current password is required' => 'La contraseña es obligatoria', + 'Wrong password' => 'contraseña incorrecta', + 'Reset all tokens' => 'Reiniciar los identificadores (tokens) de seguridad ', + 'All tokens have been regenerated.' => 'Todos los identificadores (tokens) han sido reiniciados.', + // 'Unknown' => '', + // 'Last logins' => '', + // 'Login date' => '', + // 'Authentication method' => '', + // 'IP address' => '', + // 'User agent' => '', + // 'Persistent connections' => '', + // 'No session' => '', + // 'Expiration date' => '', + // 'Remember Me' => '', + // 'Creation date' => '', + // 'Filter by user' => '', + // 'Filter by due date' => ', + // 'Everybody' => '', + // 'Open' => '', + // 'Closed' => '', + // 'Search' => '', + // 'Nothing found.' => '', + // 'Search in the project "%s"' => '', + // 'Due date' => '', + // 'Others formats accepted: %s and %s' => '', + // 'Description' => '', + // '%d comments' => '', + // '%d comment' => '', + // 'Email address invalid' => '', + // 'Your Google Account is not linked anymore to your profile.' => '', + // 'Unable to unlink your Google Account.' => '', + // 'Google authentication failed' => '', + // 'Unable to link your Google Account.' => '', + // 'Your Google Account is linked to your profile successfully.' => '', + // 'Email' => '', + // 'Link my Google Account' => '', + // 'Unlink my Google Account' => '', + // 'Login with my Google Account' => '', + // 'Project not found.' => '', + // 'Task #%d' => '', + // 'Task removed successfully.' => '', + // 'Unable to remove this task.' => '', + // 'Remove a task' => '', + // 'Do you really want to remove this task: "%s"?' => '', + // 'Assign a color to a specific category' => '', + // 'Task creation or modification' => '', + // 'Category' => '', + // 'Category:' => '', + // 'Categories' => '', + // 'Category not found.' => '', + // 'Your category have been created successfully.' => '', + // 'Unable to create your category.' => '', + // 'Your category have been updated successfully.' => '', + // 'Unable to update your category.' => '', + // 'Remove a category' => '', + // 'Category removed successfully.' => '', + // 'Unable to remove this category.' => '', + // 'Category modification for the project "%s"' => '', + // 'Category Name' => '', + // 'Categories for the project "%s"' => '', + // 'Add a new category' => '', + // 'Do you really want to remove this category: "%s"?' => '', + // 'Filter by category' => '', + // 'All categories' => '', + // 'No category' => '', + // 'The name is required' => '', +); diff --git a/app/Locales/fr_FR/translations.php b/app/Locales/fr_FR/translations.php new file mode 100644 index 00000000..c93a83ae --- /dev/null +++ b/app/Locales/fr_FR/translations.php @@ -0,0 +1,334 @@ +<?php + +return array( + 'English' => 'Anglais', + 'French' => 'Français', + 'Polish' => 'Polonais', + 'Portuguese (Brazilian)' => 'Portugais (Brésil)', + 'Spanish' => 'Espagnol', + 'None' => 'Aucun', + 'edit' => 'modifier', + 'Edit' => 'Modifier', + 'remove' => 'supprimer', + 'Remove' => 'Supprimer', + 'Update' => 'Mettre à jour', + 'Yes' => 'Oui', + 'No' => 'Non', + 'cancel' => 'annuler', + 'or' => 'ou', + 'Yellow' => 'Jaune', + 'Blue' => 'Bleu', + 'Green' => 'Vert', + 'Purple' => 'Violet', + 'Red' => 'Rouge', + 'Orange' => 'Orange', + 'Grey' => 'Gris', + 'Save' => 'Enregistrer', + 'Login' => 'Connexion', + 'Official website:' => 'Site web officiel :', + 'Unassigned' => 'Non assigné', + 'View this task' => 'Voir cette tâche', + 'Remove user' => 'Supprimer un utilisateur', + 'Do you really want to remove this user: "%s"?' => 'Voulez-vous vraiment supprimer cet utilisateur : « %s » ?', + 'New user' => 'Ajouter un utilisateur', + 'All users' => 'Tous les utilisateurs', + 'Username' => 'Identifiant', + 'Password' => 'Mot de passe', + 'Default Project' => 'Projet par défaut', + 'Administrator' => 'Administrateur', + 'Sign in' => 'Connexion', + 'Users' => 'Utilisateurs', + 'No user' => 'Aucun utilisateur', + 'Forbidden' => 'Accès interdit', + 'Access Forbidden' => 'Accès interdit', + 'Only administrators can access to this page.' => 'Uniquement les administrateurs peuvent accéder à cette page.', + 'Edit user' => 'Modifier un utilisateur', + 'Logout' => 'Déconnexion', + 'Bad username or password' => 'Identifiant ou mot de passe incorrect', + 'users' => 'utilisateurs', + 'projects' => 'projets', + 'Edit project' => 'Modifier le projet', + 'Name' => 'Nom', + 'Activated' => 'Actif', + 'Projects' => 'Projets', + 'No project' => 'Aucun projet', + 'Project' => 'Projet', + 'Status' => 'État', + 'Tasks' => 'Tâches', + 'Board' => 'Tableau', + 'Inactive' => 'Inactif', + 'Active' => 'Actif', + 'Column %d' => 'Colonne %d', + 'Add this column' => 'Ajouter cette colonne', + '%d tasks on the board' => '%d tâches sur le tableau', + '%d tasks in total' => '%d tâches au total', + 'Unable to update this board.' => 'Impossible de mettre à jour ce tableau.', + 'Edit board' => 'Modifier le tableau', + 'Disable' => 'Désactiver', + 'Enable' => 'Activer', + 'New project' => 'Nouveau projet', + 'Do you really want to remove this project: "%s"?' => 'Voulez-vous vraiment supprimer ce projet : « %s » ?', + 'Remove project' => 'Supprimer le projet', + 'Boards' => 'Tableaux', + 'Edit the board for "%s"' => 'Modifier le tableau pour « %s »', + 'All projects' => 'Tous les projets', + 'Change columns' => 'Changer les colonnes', + 'Add a new column' => 'Ajouter une nouvelle colonne', + 'Title' => 'Titre', + 'Add Column' => 'Nouvelle colonne', + 'Project "%s"' => 'Projet « %s »', + 'Nobody assigned' => 'Personne assigné', + 'Assigned to %s' => 'Assigné à %s', + 'Remove a column' => 'Supprimer une colonne', + 'Remove a column from a board' => 'Supprimer une colonne d\'un tableau', + 'Unable to remove this column.' => 'Impossible de supprimer cette colonne.', + 'Do you really want to remove this column: "%s"?' => 'Voulez vraiment supprimer cette colonne : « %s » ?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Cette action va supprimer toutes les tâches associées à cette colonne !', + 'Settings' => 'Préférences', + 'Application settings' => 'Paramètres de l\'application', + 'Language' => 'Langue', + 'Webhooks token:' => 'Jeton de securité pour les webhooks :', + 'More information' => 'Plus d\'informations', + 'Database size:' => 'Taille de la base de données :', + 'Download the database' => 'Télécharger la base de données', + 'Optimize the database' => 'Optimiser la base de données', + '(VACUUM command)' => '(Commande VACUUM)', + '(Gzip compressed Sqlite file)' => '(Fichier Sqlite compressé en Gzip)', + 'User settings' => 'Paramètres utilisateur', + 'My default project:' => 'Mon projet par défaut : ', + 'Close a task' => 'Fermer une tâche', + 'Do you really want to close this task: "%s"?' => 'Voulez-vous vraiment fermer cettre tâche : « %s » ?', + 'Edit a task' => 'Modifier une tâche', + 'Column' => 'Colonne', + 'Color' => 'Couleur', + 'Assignee' => 'Personne assignée', + 'Create another task' => 'Créer une autre tâche', + 'New task' => 'Nouvelle tâche', + 'Open a task' => 'Ouvrir une tâche', + 'Do you really want to open this task: "%s"?' => 'Voulez-vous vraiment ouvrir cette tâche : « %s » ?', + 'Back to the board' => 'Retour au tableau', + 'Created on %B %e, %G at %k:%M %p' => 'Créé le %d/%m/%Y à %H:%M', + 'There is nobody assigned' => 'Il n\'y a personne d\'assigné à cette tâche', + 'Column on the board:' => 'Colonne sur le tableau : ', + 'Status is open' => 'État ouvert', + 'Status is closed' => 'État fermé', + 'Close this task' => 'Fermer cette tâche', + 'Open this task' => 'Ouvrir cette tâche', + 'There is no description.' => 'Il n\'y a pas de description.', + 'Add a new task' => 'Ajouter une nouvelle tâche', + 'The username is required' => 'Le nom d\'utilisateur est obligatoire', + 'The maximum length is %d characters' => 'La longueur maximale est de %d caractères', + 'The minimum length is %d characters' => 'La longueur minimale est de %d caractères', + 'The password is required' => 'Le mot de passe est obligatoire', + 'This value must be an integer' => 'Cette valeur doit être un entier', + 'The username must be unique' => 'Le nom d\'utilisateur doit être unique', + 'The username must be alphanumeric' => 'Le nom d\'utilisateur doit être alpha-numérique', + 'The user id is required' => 'L\'id de l\'utilisateur est obligatoire', + 'Passwords doesn\'t matches' => 'Les mots de passe ne correspondent pas', + 'The confirmation is required' => 'Le confirmation est requise', + 'The column is required' => 'La colonne est obligatoire', + 'The project is required' => 'Le projet est obligatoire', + 'The color is required' => 'La couleur est obligatoire', + 'The id is required' => 'L\'identifiant est obligatoire', + 'The project id is required' => 'L\'identifiant du projet est obligatoire', + 'The project name is required' => 'Le nom du projet est obligatoire', + 'This project must be unique' => 'Le nom du projet doit être unique', + 'The title is required' => 'Le titre est obligatoire', + 'The language is required' => 'La langue est obligatoire', + 'There is no active project, the first step is to create a new project.' => 'Il n\'y a aucun projet actif, la première étape est de créer un nouveau projet.', + 'Settings saved successfully.' => 'Paramètres sauvegardés avec succès.', + 'Unable to save your settings.' => 'Impossible de sauvegarder vos réglages.', + 'Database optimization done.' => 'Optmisation de la base de données terminée.', + 'Your project have been created successfully.' => 'Votre projet a été créé avec succès.', + 'Unable to create your project.' => 'Impossible de créer un projet.', + 'Project updated successfully.' => 'Votre projet a été mis à jour avec succès.', + 'Unable to update this project.' => 'Impossible de mettre à jour ce projet.', + 'Unable to remove this project.' => 'Impossible de supprimer ce projet.', + 'Project removed successfully.' => 'Votre projet a été supprimé avec succès.', + 'Project activated successfully.' => 'Votre projet a été activé avec succès.', + 'Unable to activate this project.' => 'Impossible d\'activer ce projet.', + 'Project disabled successfully.' => 'Votre projet a été désactivé avec succès.', + 'Unable to disable this project.' => 'Impossible de désactiver ce projet.', + 'Unable to open this task.' => 'Impossible d\'ouvrir cette tâche.', + 'Task opened successfully.' => 'Tâche ouverte avec succès.', + 'Unable to close this task.' => 'Impossible de fermer cette tâche.', + 'Task closed successfully.' => 'Tâche fermé avec succès.', + 'Unable to update your task.' => 'Impossible de modifier cette tâche.', + 'Task updated successfully.' => 'Tâche mise à jour avec succès.', + 'Unable to create your task.' => 'Impossible de créer cette tâche.', + 'Task created successfully.' => 'Tâche créée avec succès.', + 'User created successfully.' => 'Utilisateur créé avec succès.', + 'Unable to create your user.' => 'Impossible de créer cet utilisateur.', + 'User updated successfully.' => 'Utilisateur mis à jour avec succès.', + 'Unable to update your user.' => 'Impossible de mettre à jour cet utilisateur.', + 'User removed successfully.' => 'Utilisateur supprimé avec succès.', + 'Unable to remove this user.' => 'Impossible de supprimer cet utilisateur.', + 'Board updated successfully.' => 'Tableau mis à jour avec succès.', + 'Ready' => 'Prêt', + 'Backlog' => 'En attente', + 'Work in progress' => 'En cours', + 'Done' => 'Terminé', + 'Application version:' => 'Version de l\'application :', + 'Completed on %B %e, %G at %k:%M %p' => 'Terminé le %d/%m/%Y à %H:%M', + '%B %e, %G at %k:%M %p' => '%d/%m/%Y à %H:%M', + 'Date created' => 'Date de création', + 'Date completed' => 'Date de clôture', + 'Id' => 'Identifiant', + 'No task' => 'Aucune tâche', + 'Completed tasks' => 'Tâches terminées', + 'List of projects' => 'Liste des projets', + 'Completed tasks for "%s"' => 'Tâches terminées pour « %s »', + '%d closed tasks' => '%d tâches terminées', + 'no task for this project' => 'aucune tâche pour ce projet', + 'Public link' => 'Accès public', + 'There is no column in your project!' => 'Il n\'y a aucune colonne dans votre projet !', + 'Change assignee' => 'Changer la personne assignée', + 'Change assignee for the task "%s"' => 'Changer la personne assignée pour la tâche « %s »', + 'Timezone' => 'Fuseau horaire', + 'Sorry, I didn\'t found this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !', + 'Page not found' => 'Page introuvable', + 'Story Points' => 'Complexité', + 'limit' => 'limite', + 'Task limit' => 'Nombre maximum de tâches', + 'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d', + 'Edit project access list' => 'Modifier l\'accès au projet', + 'Edit users access' => 'Modifier les utilisateurs autorisés', + 'Allow this user' => 'Autoriser cet utilisateur', + 'Project access list for "%s"' => 'Liste des accès au projet « %s »', + 'Only those users have access to this project:' => 'Seulement ces utilisateurs ont accès à ce projet :', + 'Don\'t forget that administrators have access to everything.' => 'N\'oubliez pas que les administrateurs ont accès à tout.', + 'revoke' => 'révoquer', + 'List of authorized users' => 'Liste des utilisateurs autorisés', + 'User' => 'Utilisateur', + 'Everybody have access to this project.' => 'Tout le monde a accès au projet.', + 'You are not allowed to access to this project.' => 'Vous n\'êtes pas autorisé à accéder à ce projet.', + 'Comments' => 'Commentaires', + 'Post comment' => 'Commenter', + 'Write your text in Markdown' => 'Écrivez votre texte en Markdown', + 'Leave a comment' => 'Laissez un commentaire', + 'Comment is required' => 'Le commentaire est obligatoire', + 'Leave a description' => 'Laissez une description', + 'Comment added successfully.' => 'Commentaire ajouté avec succès.', + 'Unable to create your comment.' => 'Impossible de sauvegarder votre commentaire.', + 'The description is required' => 'La description est obligatoire', + 'Edit this task' => 'Modifier cette tâche', + 'Due Date' => 'Date d\'échéance', + 'm/d/Y' => 'd/m/Y', // Date format parsed with php + 'month/day/year' => 'jour/mois/année', // Help shown to the user + 'Invalid date' => 'Date invalide', + 'Must be done before %B %e, %G' => 'Doit être fait avant le %d/%m/%Y', + '%B %e, %G' => '%d/%m/%Y', + 'Automatic actions' => 'Actions automatisées', + 'Your automatic action have been created successfully.' => 'Votre action automatisée a été ajouté avec succès.', + 'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.', + 'Remove an action' => 'Supprimer une action', + 'Unable to remove this action.' => 'Impossible de supprimer cette action', + 'Action removed successfully.' => 'Action supprimée avec succès.', + 'Automatic actions for the project "%s"' => 'Actions automatisées pour le projet « %s »', + 'Defined actions' => 'Actions définies', + 'Event name' => 'Nom de l\'événement', + 'Action name' => 'Nom de l\'action', + 'Action parameters' => 'Paramètres de l\'action', + 'Action' => 'Action', + 'Event' => 'Événement', + 'When the selected event occurs execute the corresponding action.' => 'Lorsque l\'événement sélectionné se déclenche, executer l\'action correspondante.', + 'Next step' => 'Étape suivante', + 'Define action parameters' => 'Définition des paramètres de l\'action', + 'Save this action' => 'Sauvegarder cette action', + 'Do you really want to remove this action: "%s"?' => 'Voulez-vous vraiment supprimer cette action « %s » ?', + 'Remove an automatic action' => 'Supprimer une action automatisée', + 'Close the task' => 'Fermer cette tâche', + 'Assign the task to a specific user' => 'Assigner la tâche à un utilisateur spécifique', + 'Assign the task to the person who does the action' => 'Assigner la tâche à la personne qui fait l\'action', + 'Duplicate the task to another project' => 'Dupliquer la tâche vers un autre projet', + 'Move a task to another column' => 'Déplacement d\'une tâche vers un autre colonne', + 'Move a task to another position in the same column' => 'Déplacement d\'une tâche à une autre position mais dans la même colonne', + 'Task modification' => 'Modification d\'une tâche', + 'Task creation' => 'Création d\'une tâche', + 'Open a closed task' => 'Ouverture d\'une tâche fermée', + 'Closing a task' => 'Fermeture d\'une tâche', + 'Assign a color to a specific user' => 'Assigner une couleur à un utilisateur', + 'Column title' => 'Titre de la colonne', + 'Position' => 'Position', + 'Move Up' => 'Déplacer vers le haut', + 'Move Down' => 'Déplacer vers le bas', + 'Duplicate to another project' => 'Dupliquer dans un autre projet', + 'Duplicate' => 'Dupliquer', + 'link' => 'lien', + 'Update this comment' => 'Mettre à jour ce commentaire', + 'Comment updated successfully.' => 'Commentaire mis à jour avec succès.', + 'Unable to update your comment.' => 'Impossible de supprimer votre commentaire.', + 'Remove a comment' => 'Supprimer un commentaire', + 'Comment removed successfully.' => 'Commentaire supprimé avec succès.', + 'Unable to remove this comment.' => 'Impossible de supprimer ce commentaire.', + 'Do you really want to remove this comment?' => 'Voulez-vous vraiment supprimer ce commentaire ?', + 'Only administrators or the creator of the comment can access to this page.' => 'Uniquement les administrateurs ou le créateur du commentaire peuvent accéder à cette page.', + 'Details' => 'Détails', + 'Current password for the user "%s"' => 'Mot de passe actuel pour l\'utilisateur « %s »', + 'The current password is required' => 'Le mot de passe actuel est obligatoire', + 'Wrong password' => 'Mauvais mot de passe', + 'Reset all tokens' => 'Réinitialiser tous les jetons de sécurité', + 'All tokens have been regenerated.' => 'Tous les jetons de sécurité ont été réinitialisés.', + 'Unknown' => 'Inconnu', + 'Last logins' => 'Dernières connexions', + 'Login date' => 'Date de connexion', + 'Authentication method' => 'Méthode d\'authentification', + 'IP address' => 'Adresse IP', + 'User agent' => 'Agent utilisateur', + 'Persistent connections' => 'Connexions persistantes', + 'No session' => 'Aucune session', + 'Expiration date' => 'Date d\'expiration', + 'Remember Me' => 'Connexion automatique', + 'Creation date' => 'Date de création', + 'Filter by user' => 'Filtrer par utilisateur', + 'Filter by due date' => 'Filtrer par date d\'échéance', + 'Everybody' => 'Tout le monde', + 'Open' => 'Ouvert', + 'Closed' => 'Fermé', + 'Search' => 'Rechercher', + 'Nothing found.' => 'Rien trouvé.', + 'Search in the project "%s"' => 'Rechercher dans le projet « %s »', + 'Due date' => 'Date d\'échéance', + 'Others formats accepted: %s and %s' => 'Autres formats acceptés : %s et %s', + 'Description' => 'Description', + '%d comments' => '%d commentaires', + '%d comment' => '%d commentaire', + 'Email address invalid' => 'Adresse email invalide', + 'Your Google Account is not linked anymore to your profile.' => 'Votre compte Google n\'est plus relié à votre profile.', + 'Unable to unlink your Google Account.' => 'Impossible de supprimer votre compte Google.', + 'Google authentication failed' => 'Authentification Google échouée', + 'Unable to link your Google Account.' => 'Impossible de lier votre compte Google.', + 'Your Google Account is linked to your profile successfully.' => 'Votre compte Google est désormais lié à votre profile.', + 'Email' => 'Email', + 'Link my Google Account' => 'Lier mon compte Google', + 'Unlink my Google Account' => 'Ne plus utiliser mon compte Google', + 'Login with my Google Account' => 'Se connecter avec mon compte Google', + 'Project not found.' => 'Projet introuvable.', + 'Task #%d' => 'Tâche n°%d', + 'Task removed successfully.' => 'Tâche supprimée avec succès.', + 'Unable to remove this task.' => 'Impossible de supprimer cette tâche.', + 'Remove a task' => 'Supprimer une tâche', + 'Do you really want to remove this task: "%s"?' => 'Voulez-vous vraiment supprimer cette tâche « %s » ?', + 'Assign a color to a specific category' => 'Assigner une couleur à une catégorie spécifique', + 'Task creation or modification' => 'Création ou modification d\'une tâche', + 'Category' => 'Catégorie', + 'Category:' => 'Catégorie :', + 'Categories' => 'Catégories', + 'Category not found.' => 'Catégorie introuvable', + 'Your category have been created successfully.' => 'Votre catégorie a été créé avec succès.', + 'Unable to create your category.' => 'Impossible de créer votre catégorie.', + 'Your category have been updated successfully.' => 'Votre catégorie a été mise à jour avec succès.', + 'Unable to update your category.' => 'Impossible de mettre à jour votre catégorie.', + 'Remove a category' => 'Supprimer une catégorie', + 'Category removed successfully.' => 'Catégorie supprimée avec succès.', + 'Unable to remove this category.' => 'Impossible de supprimer cette catégorie.', + 'Category modification for the project "%s"' => 'Modification d\'une catégorie pour le projet « %s »', + 'Category Name' => 'Nom de la catégorie', + 'Categories for the project "%s"' => 'Catégories du projet « %s »', + 'Add a new category' => 'Ajouter une nouvelle catégorie', + 'Do you really want to remove this category: "%s"?' => 'Voulez-vous vraiment supprimer cette catégorie « %s » ?', + 'Filter by category' => 'Filtrer par catégorie', + 'All categories' => 'Toutes les catégories', + 'No category' => 'Aucune catégorie', + 'The name is required' => 'Le nom est requis', +); diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php new file mode 100644 index 00000000..81ecaf01 --- /dev/null +++ b/app/Locales/pl_PL/translations.php @@ -0,0 +1,339 @@ +<?php + +return array( + 'English' => 'angielski', + 'French' => 'francuski', + 'Polish' => 'polski', + 'Portuguese (Brazilian)' => 'Portugalski (brazylijski)', + 'Spanish' => 'HiszpaÅ„ski', + 'None' => 'Brak', + 'edit' => 'edytuj', + 'Edit' => 'Edytuj', + 'remove' => 'usuÅ„', + 'Remove' => 'UsuÅ„', + 'Update' => 'Aktualizuj', + 'Yes' => 'Tak', + 'No' => 'Nie', + 'cancel' => 'anuluj', + 'or' => 'lub', + 'Yellow' => 'Żółty', + 'Blue' => 'Niebieski', + 'Green' => 'Zielony', + 'Purple' => 'Fioletowy', + 'Red' => 'Czerwony', + 'Orange' => 'PomaraÅ„czowy', + 'Grey' => 'Szary', + 'Save' => 'Zapisz', + 'Login' => 'Login', + 'Official website:' => 'Oficjalna strona:', + 'Unassigned' => 'Nieprzypisany', + 'View this task' => 'Zobacz zadanie', + 'Remove user' => 'UsuÅ„ użytkownika', + 'Do you really want to remove this user: "%s"?' => 'Na pewno chcesz usunąć użytkownika: "%s"?', + 'New user' => 'Nowy użytkownik', + 'All users' => 'Wszyscy użytkownicy', + 'Username' => 'Nazwa użytkownika', + 'Password' => 'HasÅ‚o', + 'Default Project' => 'DomyÅ›lny projekt', + 'Administrator' => 'Administrator', + 'Sign in' => 'Zaloguj', + 'Users' => 'Użytkownicy', + 'No user' => 'Brak użytkowników', + 'Forbidden' => 'Zabroniony', + 'Access Forbidden' => 'DostÄ™p zabroniony', + 'Only administrators can access to this page.' => 'Tylko administrator może wejść na tÄ… stronÄ™.', + 'Edit user' => 'Edytuj użytkownika', + 'Logout' => 'Wyloguj', + 'Bad username or password' => 'ZÅ‚a nazwa uyżytkownika lub hasÅ‚o', + 'users' => 'użytkownicy', + 'projects' => 'projekty', + 'Edit project' => 'Edytuj projekt', + 'Name' => 'Nazwa', + 'Activated' => 'Aktywny', + 'Projects' => 'Projekty', + 'No project' => 'Brak projektów', + 'Project' => 'Projekt', + 'Status' => 'Status', + 'Tasks' => 'Zadania', + 'Board' => 'Tablica', + 'Inactive' => 'Nieaktywny', + 'Active' => 'Aktywny', + 'Column %d' => 'Kolumna %d', + 'Add this column' => 'Dodaj kolumnÄ™', + '%d tasks on the board' => '%d zadaÅ„ na tablicy', + '%d tasks in total' => '%d wszystkich zadaÅ„', + 'Unable to update this board.' => 'Nie można zaktualizować tablicy.', + 'Edit board' => 'Edytuj tablicÄ™', + 'Disable' => 'WyÅ‚Ä…cz', + 'Enable' => 'WÅ‚Ä…cz', + 'New project' => 'Nowy projekt', + 'Do you really want to remove this project: "%s"?' => 'Na pewno chcesz usunąć projekt: "%s"?', + 'Remove project' => 'UsuÅ„ projekt', + 'Boards' => 'Tablice', + 'Edit the board for "%s"' => 'Edytuj tabliÄ™ dla "%s"', + 'All projects' => 'Wszystkie projekty', + 'Change columns' => 'ZmieÅ„ kolumny', + 'Add a new column' => 'Dodaj nowÄ… kolumnÄ™', + 'Title' => 'TytuÅ‚', + 'Add Column' => 'Dodaj kolumnÄ™', + 'Project "%s"' => 'Projekt "%s"', + 'Nobody assigned' => 'Nikt nie przypisany', + 'Assigned to %s' => 'Przypisane do %s', + 'Remove a column' => 'UsuÅ„ kolumnÄ™', + 'Remove a column from a board' => 'UsuÅ„ kolumnÄ™ z tablicy', + 'Unable to remove this column.' => 'Nie udaÅ‚o siÄ™ usunąć kolumny.', + 'Do you really want to remove this column: "%s"?' => 'Na pewno chcesz usunąć kolumnÄ™: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Wszystkie zadania w kolumnie zostanÄ… usuniÄ™te!', + 'Settings' => 'Ustawienia', + 'Application settings' => 'Ustawienia aplikacji', + 'Language' => 'JÄ™zyk', + 'Webhooks token:' => 'Token :', + 'More information' => 'WiÄ™cej informacji', + 'Database size:' => 'Rozmiar bazy danych :', + 'Download the database' => 'Pobierz bazÄ™ danych', + 'Optimize the database' => 'Optymalizuj bazÄ™ danych', + '(VACUUM command)' => '(komenda VACUUM)', + '(Gzip compressed Sqlite file)' => '(baza danych spakowana Gzip)', + 'User settings' => 'Ustawienia użytkownika', + 'My default project:' => 'Mój domyÅ›lny projekt:', + 'Close a task' => 'ZakoÅ„cz zadanie', + 'Do you really want to close this task: "%s"?' => 'Na pewno chcesz zakoÅ„czyć to zadanie: "%s"?', + 'Edit a task' => 'Edytuj zadanie', + 'Column' => 'Kolumna', + 'Color' => 'Kolor', + 'Assignee' => 'Odpowiedzialny', + 'Create another task' => 'Dodaj kolejne zadanie', + 'New task' => 'Nowe zadanie', + 'Open a task' => 'Otwórz zadanie', + 'Do you really want to open this task: "%s"?' => 'Na pewno chcesz otworzyć zadanie: "%s"?', + 'Back to the board' => 'Powrót do tablicy', + 'Created on %B %e, %G at %k:%M %p' => 'Utworzono dnia %e %B %G o %k:%M', + 'There is nobody assigned' => 'Nikt nie jest przypisany', + 'Column on the board:' => 'Kolumna na tablicy:', + 'Status is open' => 'Status otwarty', + 'Status is closed' => 'Status zamkniÄ™ty', + 'Close this task' => 'Zamknij zadanie', + 'Open this task' => 'Otwórz zadanie', + 'There is no description.' => 'Brak opisu.', + 'Add a new task' => 'Dodaj zadanie', + 'The username is required' => 'Nazwa użytkownika jest wymagana', + 'The maximum length is %d characters' => 'Maksymalna dÅ‚ugość wynosi %d znaków', + 'The minimum length is %d characters' => 'Minimalna dÅ‚ugość wynosi %d znaków', + 'The password is required' => 'HasÅ‚o jest wymagane', + 'This value must be an integer' => 'Wartość musi być liczbÄ… caÅ‚kowitÄ…', + 'The username must be unique' => 'Nazwa użytkownika musi być unikalna', + 'The username must be alphanumeric' => 'Nazwa użytkownika musi być alfanumeryczna', + 'The user id is required' => 'ID użytkownika jest wymagane', + 'Passwords doesn\'t matches' => 'HasÅ‚a nie pasujÄ… do siebie', + 'The confirmation is required' => 'Wymagane jest potwierdzenie', + 'The column is required' => 'Kolumna jest wymagana', + 'The project is required' => 'Projekt jest wymagany', + 'The color is required' => 'Kolor jest wymagany', + 'The id is required' => 'ID jest wymagane', + 'The project id is required' => 'ID projektu jest wymagane', + 'The project name is required' => 'Nazwa projektu jest wymagana', + 'This project must be unique' => 'Projekt musi być unikalny', + 'The title is required' => 'TutyÅ‚ jest wymagany', + 'The language is required' => 'JÄ™zyk jest wymagany', + 'There is no active project, the first step is to create a new project.' => 'Brak aktywnych projektów. Pierwszym krokiem jest utworzenie nowego projektu.', + 'Settings saved successfully.' => 'Ustawienia zapisane.', + 'Unable to save your settings.' => 'Nie udaÅ‚o siÄ™ zapisać ustawieÅ„.', + 'Database optimization done.' => 'Optymalizacja bazy danych zakoÅ„czona.', + 'Your project have been created successfully.' => 'Projekt zostaÅ‚ pomyÅ›lnie utworzony.', + 'Unable to create your project.' => 'Nie udaÅ‚o siÄ™ stworzyć projektu.', + 'Project updated successfully.' => 'Projekt zaktualizowany.', + 'Unable to update this project.' => 'Nie można zaktualizować projektu.', + 'Unable to remove this project.' => 'Nie można usunąć projektu.', + 'Project removed successfully.' => 'Projekt usuniÄ™ty.', + 'Project activated successfully.' => 'Projekt aktywowany.', + 'Unable to activate this project.' => 'Nie można aktywować projektu.', + 'Project disabled successfully.' => 'Projekt wyÅ‚Ä…czony.', + 'Unable to disable this project.' => 'Nie można wyÅ‚Ä…czyć projektu.', + 'Unable to open this task.' => 'Nie można otworzyć tego zadania.', + 'Task opened successfully.' => 'Zadanie otwarte.', + 'Unable to close this task.' => 'Nie można zamknąć tego zadania.', + 'Task closed successfully.' => 'Zadanie zamkniÄ™te.', + 'Unable to update your task.' => 'Nie można zaktualizować tego zadania.', + 'Task updated successfully.' => 'Zadanie zaktualizowane.', + 'Unable to create your task.' => 'Nie można dodać zadania.', + 'Task created successfully.' => 'Zadanie zostaÅ‚o utworzone.', + 'User created successfully.' => 'Użytkownik dodany', + 'Unable to create your user.' => 'Nie udaÅ‚o siÄ™ dodać użytkownika.', + 'User updated successfully.' => 'Użytkownik zaktualizowany.', + 'Unable to update your user.' => 'Nie udaÅ‚o siÄ™ zaktualizować użytkownika.', + 'User removed successfully.' => 'Użytkownik usuniÄ™ty.', + 'Unable to remove this user.' => 'Nie udaÅ‚o siÄ™ usunąć użytkownika.', + 'Board updated successfully.' => 'Tablica zostaÅ‚a zaktualizowana.', + 'Ready' => 'Gotowe', + 'Backlog' => 'Log', + 'Work in progress' => 'W trakcie', + 'Done' => 'ZakoÅ„czone', + 'Application version:' => 'Wersja aplikacji:', + 'Completed on %B %e, %G at %k:%M %p' => 'ZakoÅ„czono dnia %e %B %G o %k:%M', + '%B %e, %G at %k:%M %p' => '%e %B %G o %k:%M', + 'Date created' => 'Data utworzenia', + 'Date completed' => 'Data zakoÅ„czenia', + 'Id' => 'Ident', + 'No task' => 'Brak zadaÅ„', + 'Completed tasks' => 'UkoÅ„czone zadania', + 'List of projects' => 'Lista projektów', + 'Completed tasks for "%s"' => 'Zadania zakoÅ„czone dla "%s"', + '%d closed tasks' => '%d zamkniÄ™tych zadaÅ„', + 'no task for this project' => 'brak zadaÅ„ dla tego projektu', + 'Public link' => 'Link publiczny', + 'There is no column in your project!' => 'Brak kolumny w Twoim projekcie', + 'Change assignee' => 'ZmieÅ„ odpowiedzialnÄ… osobÄ™', + 'Change assignee for the task "%s"' => 'ZmieÅ„ odpowiedzialnÄ… osobÄ™ dla zadania "%s"', + 'Timezone' => 'Strefa czasowa', + 'Actions' => 'Akcje', + 'Confirmation' => 'Powtórzenie hasÅ‚a', + 'Description' => 'Opis', + 'Details' => 'Informacje', + 'Sorry, I didn\'t found this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych', + 'Page not found' => 'Strona nie istnieje', + 'Story Points' => 'Poziom trudnoÅ›ci', + 'limit' => 'limit', + 'Task limit' => 'Limit zadaÅ„', + 'This value must be greater than %d' => 'Wartość musi być wiÄ™ksza niż %d', + 'Edit project access list' => 'Edycja list dostÄ™pu dla projektu', + 'Edit users access' => 'Edytuj dostÄ™p', + 'Allow this user' => 'Dodaj użytkownika', + 'Project access list for "%s"' => 'Lista uprawnionych dla projektu "%s"', + 'Only those users have access to this project:' => 'Użytkownicy majÄ…cy dostÄ™p:', + 'Don\'t forget that administrators have access to everything.' => 'PamiÄ™taj: Administratorzy majÄ… zawsze dostÄ™p do wszystkiego!', + 'revoke' => 'odbierz dostÄ™p', + 'List of authorized users' => 'Lista użytkowników majÄ…cych dostÄ™p', + 'User' => 'Użytkownik', + 'Everybody have access to this project.' => 'Każdy ma dostÄ™p do tego projektu.', + 'You are not allowed to access to this project.' => 'Nie masz dostÄ™pu do tego projektu.', + '%B %e, %G at %k:%M %p' => '%e %B %G o %k:%M', + 'Comments' => 'Komentarze', + 'Post comment' => 'Dodaj komentarz', + 'Write your text in Markdown' => 'Możesz użyć Markdown', + 'Leave a comment' => 'Zostaw komentarz', + 'Comment is required' => 'Komentarz jest wymagany', + 'Comment added successfully.' => 'Komentarz dodany', + 'Unable to create your comment.' => 'Nie udaÅ‚o siÄ™ dodać komentarza', + 'The description is required' => 'Opis jest wymagany', + 'Edit this task' => 'Edytuj zadanie', + 'Due Date' => 'Termin', + 'm/d/Y' => 'd/m/Y', // Date format parsed with php + 'month/day/year' => 'dzieÅ„/miesiÄ…c/rok', // Help shown to the user + 'Invalid date' => 'BÅ‚Ä™dna data', + 'Must be done before %B %e, %G' => 'Termin do %e %B %G', + '%B %e, %G' => '%e %B %G', + 'Automatic actions' => 'Akcje automatyczne', + 'Your automatic action have been created successfully.' => 'Twoja akcja zostaÅ‚a dodana', + 'Unable to create your automatic action.' => 'Nie udaÅ‚o siÄ™ utworzyć akcji', + 'Remove an action' => 'UsuÅ„ akcjÄ™', + 'Unable to remove this action.' => 'Nie można usunąć akcji', + 'Action removed successfully.' => 'Akcja usuniÄ™ta', + 'Automatic actions for the project "%s"' => 'Akcje automatyczne dla projektu "%s"', + 'Defined actions' => 'Zdefiniowane akcje', + 'Event name' => 'Nazwa zdarzenia', + 'Action name' => 'Nazwa akcji', + 'Action parameters' => 'Parametry akcji', + 'Action' => 'Akcja', + 'Event' => 'Zdarzenie', + 'When the selected event occurs execute the corresponding action.' => 'Gdy nastÄ™puje wybrane zdarzenie, uruchom odpowiedniÄ… akcjÄ™', + 'Next step' => 'NastÄ™pny krok', + 'Define action parameters' => 'Zdefiniuj parametry akcji', + 'Save this action' => 'Zapisz akcjÄ™', + 'Do you really want to remove this action: "%s"?' => 'Na pewno chcesz usunąć akcjÄ™ "%s"?', + 'Remove an automatic action' => 'UsuÅ„ akcjÄ™ automatycznÄ…', + 'Close the task' => 'Zamknij zadanie', + 'Assign the task to a specific user' => 'Przypisz zadanie do wybranego użytkownika', + 'Assign the task to the person who does the action' => 'Przypisz zadanie to osoby wykonujÄ…cej akcjÄ™', + 'Duplicate the task to another project' => 'Kopiuj zadanie do innego projektu', + 'Move a task to another column' => 'Przeniesienie zadania do innej kolumny', + 'Move a task to another position in the same column' => 'Zmiania pozycji zadania w kolumnie', + 'Task modification' => 'Modyfikacja zadania', + 'Task creation' => 'Tworzenie zadania', + 'Open a closed task' => 'Otwarcie zamkniÄ™tego zadania', + 'Closing a task' => 'ZamkniÄ™cie zadania', + 'Assign a color to a specific user' => 'Przypisz kolor do wybranego użytkownika', + 'Add an action' => 'Nowa akcja', + 'Column title' => 'TytuÅ‚ kolumny', + 'Position' => 'Pozycja', + 'Move Up' => 'PrzenieÅ› wyżej', + 'Move Down' => 'PrzenieÅ› niżej', + 'Duplicate to another project' => 'Skopiuj do innego projektu', + 'Duplicate' => 'Utwórz kopiÄ™', + 'link' => 'link', + 'Update this comment' => 'Zapisz komentarz', + 'Comment updated successfully.' => 'Komentarz zostaÅ‚ zapisany.', + 'Unable to update your comment.' => 'Nie udaÅ‚o siÄ™ zapisanie komentarza.', + 'Remove a comment' => 'UsuÅ„ komentarz', + 'Comment removed successfully.' => 'Komentarz zostaÅ‚ usuniÄ™ty.', + 'Unable to remove this comment.' => 'Nie udaÅ‚o siÄ™ usunąć komentarza.', + 'Do you really want to remove this comment?' => 'Czy na pewno usunąć ten komentarz?', + 'Only administrators or the creator of the comment can access to this page.' => 'Tylko administratorzy oraz autor komentarza ma dostÄ™p do tej strony.', + 'Details' => 'Szczegóły', + 'Current password for the user "%s"' => 'Aktualne hasÅ‚o dla użytkownika "%s"', + 'The current password is required' => 'Wymanage jest aktualne hasÅ‚o', + 'Wrong password' => 'BÅ‚Ä™dne hasÅ‚o', + 'Reset all tokens' => 'Zresetuj wszystkie tokeny', + 'All tokens have been regenerated.' => 'Wszystkie tokeny zostaÅ‚y zresetowane.', + 'Unknown' => 'Nieznany', + 'Last logins' => 'Ostatnie logowania', + 'Login date' => 'Data logowania', + 'Authentication method' => 'Sposób autentykacji', + 'IP address' => 'Adres IP', + 'User agent' => 'PrzeglÄ…darka', + 'Persistent connections' => 'StaÅ‚e poÅ‚Ä…czenia', + 'No session' => 'Brak sesji', + 'Expiration date' => 'Data zakoÅ„czenia', + 'Remember Me' => 'PamiÄ™taj mnie', + 'Creation date' => 'Data utworzenia', + // 'Filter by user' => '', + // 'Filter by due date' => ', + // 'Everybody' => '', + // 'Open' => '', + // 'Closed' => '', + // 'Search' => '', + // 'Nothing found.' => '', + // 'Search in the project "%s"' => '', + // 'Due date' => '', + // 'Others formats accepted: %s and %s' => '', + // 'Description' => '', + // '%d comments' => '', + // '%d comment' => '', + // 'Email address invalid' => '', + // 'Your Google Account is not linked anymore to your profile.' => '', + // 'Unable to unlink your Google Account.' => '', + // 'Google authentication failed' => '', + // 'Unable to link your Google Account.' => '', + // 'Your Google Account is linked to your profile successfully.' => '', + // 'Email' => '', + // 'Link my Google Account' => '', + // 'Unlink my Google Account' => '', + // 'Login with my Google Account' => '', + // 'Project not found.' => '', + // 'Task #%d' => '', + // 'Task removed successfully.' => '', + // 'Unable to remove this task.' => '', + // 'Remove a task' => '', + // 'Do you really want to remove this task: "%s"?' => '', + // 'Assign a color to a specific category' => '', + // 'Task creation or modification' => '', + // 'Category' => '', + // 'Category:' => '', + // 'Categories' => '', + // 'Category not found.' => '', + // 'Your category have been created successfully.' => '', + // 'Unable to create your category.' => '', + // 'Your category have been updated successfully.' => '', + // 'Unable to update your category.' => '', + // 'Remove a category' => '', + // 'Category removed successfully.' => '', + // 'Unable to remove this category.' => '', + // 'Category modification for the project "%s"' => '', + // 'Category Name' => '', + // 'Categories for the project "%s"' => '', + // 'Add a new category' => '', + // 'Do you really want to remove this category: "%s"?' => '', + // 'Filter by category' => '', + // 'All categories' => '', + // 'No category' => '', + // 'The name is required' => '', +); diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php new file mode 100644 index 00000000..7c9a6c17 --- /dev/null +++ b/app/Locales/pt_BR/translations.php @@ -0,0 +1,335 @@ +<?php + +return array( + 'English' => 'Inglês', + 'French' => 'Francês', + 'Polish' => 'Polonês', + 'Portuguese (Brazilian)' => 'Português (Brasil)', + 'Spanish' => 'Espanhol', + 'None' => 'Nenhum', + 'edit' => 'editar', + 'Edit' => 'Editar', + 'remove' => 'apagar', + 'Remove' => 'Apagar', + 'Update' => 'Atualizar', + 'Yes' => 'Sim', + 'No' => 'Não', + 'cancel' => 'cancelar', + 'or' => 'ou', + 'Yellow' => 'Amarelo', + 'Blue' => 'Azul', + 'Green' => 'Verde', + 'Purple' => 'Violeta', + 'Red' => 'Vermelho', + 'Orange' => 'Laranja', + 'Grey' => 'Cinza', + 'Save' => 'Salvar', + 'Login' => 'Login', + 'Official website:' => 'Site web oficial :', + 'Unassigned' => 'Não AtribuÃda', + 'View this task' => 'Ver esta tarefa', + 'Remove user' => 'Remover usuário', + 'Do you really want to remove this user: "%s"?' => 'Quer realmente remover este usuário: "%s"?', + 'New user' => 'Novo usuário', + 'All users' => 'Todos os usuários', + 'Username' => 'Nome do usuário', + 'Password' => 'Senha', + 'Default Project' => 'Projeto default', + 'Administrator' => 'Administrador', + 'Sign in' => 'Logar', + 'Users' => 'Usuários', + 'No user' => 'Sem usuário', + 'Forbidden' => 'Proibido', + 'Access Forbidden' => 'Acesso negado', + 'Only administrators can access to this page.' => 'Somente administradores têm acesso a esta página.', + 'Edit user' => 'Editar usuário', + 'Logout' => 'Logout', + 'Bad username or password' => 'Usuário ou senha inválidos', + 'users' => 'usuários', + 'projects' => 'projetos', + 'Edit project' => 'Editar projeto', + 'Name' => 'Nome', + 'Activated' => 'Ativo', + 'Projects' => 'Projetos', + 'No project' => 'Nenhum projeto', + 'Project' => 'Projeto', + 'Status' => 'Status', + 'Tasks' => 'Tarefas', + 'Board' => 'Quadro', + 'Inactive' => 'Inativo', + 'Active' => 'Ativo', + 'Column %d' => 'Coluna %d', + 'Add this column' => 'Adicionar esta coluna', + '%d tasks on the board' => '%d tarefas no quadro', + '%d tasks in total' => '%d tarefas no total', + 'Unable to update this board.' => 'ImpossÃvel atualizar este quadro.', + 'Edit board' => 'Modificar quadro', + 'Disable' => 'Desativar', + 'Enable' => 'Ativar', + 'New project' => 'Novo projeto', + 'Do you really want to remove this project: "%s"?' => 'Quer realmente remover este projeto: "%s" ?', + 'Remove project' => 'Remover projeto', + 'Boards' => 'Quadros', + 'Edit the board for "%s"' => 'Editar o quadro para "%s"', + 'All projects' => 'Todos os projetos', + 'Change columns' => 'Modificar colunas', + 'Add a new column' => 'Adicionar uma nova coluna', + 'Title' => 'TÃtulo', + 'Add Column' => 'Adicionar coluna', + 'Project "%s"' => 'Projeto "%s"', + 'Nobody assigned' => 'Ninguém designado', + 'Assigned to %s' => 'Designado para %s', + 'Remove a column' => 'Remover uma coluna', + 'Remove a column from a board' => 'Remover uma coluna do quadro', + 'Unable to remove this column.' => 'ImpossÃvel remover esta coluna.', + 'Do you really want to remove this column: "%s"?' => 'Quer realmente remover esta coluna: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Esta ação vai REMOVER TODAS AS TAREFAS associadas a esta coluna!', + 'Settings' => 'Preferências', + 'Application settings' => 'Preferências da aplicação', + 'Language' => 'Idioma', + 'Webhooks token:' => 'Token de webhooks:', + 'More information' => 'Mais informação', + 'Database size:' => 'Tamanho do banco de dados:', + 'Download the database' => 'Download do banco de dados', + 'Optimize the database' => 'Otimizar o banco de dados', + '(VACUUM command)' => '(Comando VACUUM)', + '(Gzip compressed Sqlite file)' => '(Arquivo Sqlite comprimido com Gzip)', + 'User settings' => 'Configurações do usuário', + 'My default project:' => 'Meu projeto default:', + 'Close a task' => 'Encerrar uma tarefa', + 'Do you really want to close this task: "%s"?' => 'Quer realmente encerrar esta tarefa: "%s"?', + 'Edit a task' => 'Editar uma tarefa', + 'Column' => 'Coluna', + 'Color' => 'Cor', + 'Assignee' => 'Designação', + 'Create another task' => 'Criar uma outra tarefa (aproveitando os dados preenchidos)', + 'New task' => 'Nova tarefa', + 'Open a task' => 'Abrir uma tarefa', + 'Do you really want to open this task: "%s"?' => 'Quer realmente abrir esta tarefa: "%s"?', + 'Back to the board' => 'Voltar ao quadro', + 'Created on %B %e, %G at %k:%M %p' => 'Criado em %d %B %G à s %H:%M', + 'There is nobody assigned' => 'Não há ninguém designado', + 'Column on the board:' => 'Coluna no quadro:', + 'Status is open' => 'Status está aberto', + 'Status is closed' => 'Status está fechado', + 'Close this task' => 'Fechar esta tarefa', + 'Open this task' => 'Abrir esta tarefa', + 'There is no description.' => 'Não há descrição.', + 'Add a new task' => 'Adicionar uma nova tarefa', + 'The username is required' => 'O nome de usuário é obrigatório', + 'The maximum length is %d characters' => 'O tamanho máximo são %d caracteres', + 'The minimum length is %d characters' => 'O tamanho mÃnimo são %d caracteres', + 'The password is required' => 'A senha é obrigatória', + 'This value must be an integer' => 'O valor deve ser um inteiro', + 'The username must be unique' => 'O nome de usuário deve ser único', + 'The username must be alphanumeric' => 'O nome de usuário deve ser alfanumérico, sem espaços ou _', + 'The user id is required' => 'O id de usuário é obrigatório', + 'Passwords doesn\'t matches' => 'As senhas não conferem', + 'The confirmation is required' => 'A confirmação é obrigatória', + 'The column is required' => 'A coluna é obrigatória', + 'The project is required' => 'O projeto é obrigatório', + 'The color is required' => 'A cor é obrigatória', + 'The id is required' => 'O id é obrigatório', + 'The project id is required' => 'O id do projeto é obrigatório', + 'The project name is required' => 'O nome do projeto é obrigatório', + 'This project must be unique' => 'Este projeto deve ser único', + 'The title is required' => 'O tÃtulo é obrigatório', + 'The language is required' => 'O idioma é obrigatório', + 'There is no active project, the first step is to create a new project.' => 'Não há projeto ativo. O primeiro passo é criar um novo projeto.', + 'Settings saved successfully.' => 'Configurações salvas com sucesso.', + 'Unable to save your settings.' => 'ImpossÃvel salvar suas configurações.', + 'Database optimization done.' => 'Otimização do banco de dados terminada.', + 'Your project have been created successfully.' => 'Seu projeto foi criado com sucesso.', + 'Unable to create your project.' => 'ImpossÃvel criar seu projeto.', + 'Project updated successfully.' => 'Projeto atualizado com sucesso.', + 'Unable to update this project.' => 'ImpossÃvel atualizar este projeto.', + 'Unable to remove this project.' => 'ImpossÃvel remover este projeto.', + 'Project removed successfully.' => 'Projeto removido com sucesso.', + 'Project activated successfully.' => 'Projeto ativado com sucesso.', + 'Unable to activate this project.' => 'ImpossÃvel ativar este projeto.', + 'Project disabled successfully.' => 'Projeto desabilitado com sucesso.', + 'Unable to disable this project.' => 'ImpossÃvel desabilitar este projeto.', + 'Unable to open this task.' => 'ImpossÃvel abrir esta tarefa.', + 'Task opened successfully.' => 'Tarefa aberta com sucesso.', + 'Unable to close this task.' => 'ImpossÃvel encerrar esta tarefa.', + 'Task closed successfully.' => 'Tarefa encerrada com sucesso.', + 'Unable to update your task.' => 'ImpossÃvel atualizar sua tarefa.', + 'Task updated successfully.' => 'Tarefa atualizada com sucesso.', + 'Unable to create your task.' => 'ImpossÃvel criar sua tarefa.', + 'Task created successfully.' => 'Tarefa criada com sucesso.', + 'User created successfully.' => 'Usuário criado com sucesso.', + 'Unable to create your user.' => 'ImpossÃvel criar seu usuário.', + 'User updated successfully.' => 'Usuário atualizado com sucesso.', + 'Unable to update your user.' => 'ImpossÃvel atualizar seu usuário.', + 'User removed successfully.' => 'Usuário removido com sucesso.', + 'Unable to remove this user.' => 'ImpossÃvel remover este usuário.', + 'Board updated successfully.' => 'Quadro atualizado com sucesso.', + 'Ready' => 'Pronto', + 'Backlog' => 'Backlog', + 'Work in progress' => 'Em andamento', + 'Done' => 'Encerrado', + 'Application version:' => 'Versão da aplicação:', + 'Completed on %B %e, %G at %k:%M %p' => 'Encerrado em %d %B %G à s %H:%M', + '%B %e, %G at %k:%M %p' => '%d %B %G à s %H:%M', + 'Date created' => 'Data de criação', + 'Date completed' => 'Data de encerramento', + 'Id' => 'Id', + 'No task' => 'Nenhuma tarefa', + 'Completed tasks' => 'tarefas completadas', + 'List of projects' => 'Lista de projetos', + 'Completed tasks for "%s"' => 'Tarefas completadas por "%s"', + '%d closed tasks' => '%d tarefas encerradas', + 'no task for this project' => 'nenhuma tarefa para este projeto', + 'Public link' => 'Link público', + 'There is no column in your project!' => 'Não há colunas no seu projeto!', + 'Change assignee' => 'Mudar a designação', + 'Change assignee for the task "%s"' => 'Modificar designação para a tarefa "%s"', + 'Timezone' => 'Fuso horário', + 'Sorry, I didn\'t found this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!', + 'Page not found' => 'Página não encontrada', + 'Story Points' => 'Complexidade', + 'limit' => 'limite', + 'Task limit' => 'Limite da tarefa', + 'This value must be greater than %d' => 'Este valor deve ser maior que %d', + 'Edit project access list' => 'Editar lista de acesso ao projeto', // new translations to brazilian portuguese starts here + 'Edit users access' => 'Editar acesso de usuários', + 'Allow this user' => 'Permitir esse usuário', + 'Project access list for "%s"' => 'Lista de acesso ao projeto para "%s"', + 'Only those users have access to this project:' => 'Somente estes usuários têm acesso a este projeto:', + 'Don\'t forget that administrators have access to everything.' => 'Não esqueça que administradores têm acesso a tudo.', + 'revoke' => 'revogar', + 'List of authorized users' => 'Lista de usuários autorizados', + 'User' => 'Usuário', + 'Everybody have access to this project.' => 'Todos têm acesso a este projeto.', + 'You are not allowed to access to this project.' => 'Você não está autorizado a acessar este projeto.', + '%B %e, %G at %k:%M %p' => '%d %B %G à s %H:%M', + 'Comments' => 'Comentários', + 'Post comment' => 'Postar comentário', + 'Write your text in Markdown' => 'Escreva seu texto em Markdown', + 'Leave a comment' => 'Deixe um comentário', + 'Comment is required' => 'Comentário é obrigatório', + 'Leave a description' => 'Deixe uma descrição', + 'Comment added successfully.' => 'Cpmentário adicionado com sucesso.', + 'Unable to create your comment.' => 'ImpossÃvel criar seu comentário.', + 'The description is required' => 'A descrição é obrigatória', + 'Edit this task' => 'Editar esta tarefa', + 'Due Date' => 'Data de vencimento', + 'm/d/Y' => 'd/m/Y', // Date format parsed with php + 'month/day/year' => 'dia/mês/ano', // Help shown to the user + 'Invalid date' => 'Data inválida', + 'Must be done before %B %e, %G' => 'Deve ser feito antes de %d %B %G', + '%B %e, %G' => '%d %B %G', + 'Automatic actions' => 'Ações automáticas', + 'Your automatic action have been created successfully.' => 'Sua ação automética foi criada com sucesso.', + 'Unable to create your automatic action.' => 'ImpossÃvel criar sua ação automática.', + 'Remove an action' => 'Remover uma ação', + 'Unable to remove this action.' => 'ImpossÃvel remover esta ação', + 'Action removed successfully.' => 'Ação removida com sucesso.', + 'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"', + 'Defined actions' => 'Ações definidas', + 'Event name' => 'Nome do evento', + 'Action name' => 'Nome da ação', + 'Action parameters' => 'Parâmetros da ação', + 'Action' => 'Ação', + 'Event' => 'Evento', + 'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer, execute a ação correspondente', + 'Next step' => 'Próximo passo', + 'Define action parameters' => 'Definir parêmetros da ação', + 'Save this action' => 'Salvar esta ação', + 'Do you really want to remove this action: "%s"?' => 'Você quer realmente remover esta ação: "%s"?', + 'Remove an automatic action' => 'Remove uma ação automática', + 'Close the task' => 'Fechar tarefa', + 'Assign the task to a specific user' => 'Designar a tarefa para um usuário especÃfico', + 'Assign the task to the person who does the action' => 'Designar a tarefa para a pessoa que executa a ação', + 'Duplicate the task to another project' => 'Duplicar a tarefa para um outro projeto', + 'Move a task to another column' => 'Mover a tarefa para outra coluna', + 'Move a task to another position in the same column' => 'Mover a tarefa para outra posição, na mesma coluna', + 'Task modification' => 'Modificação de tarefa', + 'Task creation' => 'Criação de tarefa', + 'Open a closed task' => 'Reabrir uma tarefa fechada', + 'Closing a task' => 'Fechando uma tarefa', + 'Assign a color to a specific user' => 'Designar uma cor para um usuário especÃfico', + // 'Column title' => '', + // 'Position' => '', + // 'Move Up' => '', + // 'Move Down' => '', + // 'Duplicate to another project' => '', + // 'Duplicate' => '', + // 'link' => '', + // 'Update this comment' => '', + // 'Comment updated successfully.' => '', + // 'Unable to update your comment.' => '', + // 'Remove a comment' => '', + // 'Comment removed successfully.' => '', + // 'Unable to remove this comment.' => '', + // 'Do you really want to remove this comment?' => '', + // 'Only administrators or the creator of the comment can access to this page.' => '', + // 'Details' => '', + // 'Current password for the user "%s"' => '', + // 'The current password is required' => '', + // 'Wrong password' => '', + // 'Reset all tokens' => '', + // 'All tokens have been regenerated.' => '', + // 'Unknown' => '', + // 'Last logins' => '', + // 'Login date' => '', + // 'Authentication method' => '', + // 'IP address' => '', + // 'User agent' => '', + // 'Persistent connections' => '', + // 'No session' => '', + // 'Expiration date' => '', + // 'Remember Me' => '', + // 'Creation date' => '', + // 'Filter by user' => '', + // 'Filter by due date' => ', + // 'Everybody' => '', + // 'Open' => '', + // 'Closed' => '', + // 'Search' => '', + // 'Nothing found.' => '', + // 'Search in the project "%s"' => '', + // 'Due date' => '', + // 'Others formats accepted: %s and %s' => '', + // 'Description' => '', + // '%d comments' => '', + // '%d comment' => '', + // 'Email address invalid' => '', + // 'Your Google Account is not linked anymore to your profile.' => '', + // 'Unable to unlink your Google Account.' => '', + // 'Google authentication failed' => '', + // 'Unable to link your Google Account.' => '', + // 'Your Google Account is linked to your profile successfully.' => '', + // 'Email' => '', + // 'Link my Google Account' => '', + // 'Unlink my Google Account' => '', + // 'Login with my Google Account' => '', + // 'Project not found.' => '', + // 'Task #%d' => '', + // 'Task removed successfully.' => '', + // 'Unable to remove this task.' => '', + // 'Remove a task' => '', + // 'Do you really want to remove this task: "%s"?' => '', + // 'Assign a color to a specific category' => '', + // 'Task creation or modification' => '', + // 'Category' => '', + // 'Category:' => '', + // 'Categories' => '', + // 'Category not found.' => '', + // 'Your category have been created successfully.' => '', + // 'Unable to create your category.' => '', + // 'Your category have been updated successfully.' => '', + // 'Unable to update your category.' => '', + // 'Remove a category' => '', + // 'Category removed successfully.' => '', + // 'Unable to remove this category.' => '', + // 'Category modification for the project "%s"' => '', + // 'Category Name' => '', + // 'Categories for the project "%s"' => '', + // 'Add a new category' => '', + // 'Do you really want to remove this category: "%s"?' => '', + // 'Filter by category' => '', + // 'All categories' => '', + // 'No category' => '', + // 'The name is required' => '', +); 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'); + } +} diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php new file mode 100644 index 00000000..6764ad5d --- /dev/null +++ b/app/Schema/Mysql.php @@ -0,0 +1,236 @@ +<?php + +namespace Schema; + +const VERSION = 16; + +function version_16($pdo) +{ + $pdo->exec(" + CREATE TABLE project_has_categories ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(255), + project_id INT, + PRIMARY KEY (id), + UNIQUE KEY `idx_project_category` (project_id, name), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8" + ); + + $pdo->exec("ALTER TABLE tasks ADD COLUMN category_id INT DEFAULT 0"); +} + +function version_15($pdo) +{ + $pdo->exec("ALTER TABLE projects ADD COLUMN last_modified INT DEFAULT 0"); +} + +function version_14($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN name VARCHAR(255)"); + $pdo->exec("ALTER TABLE users ADD COLUMN email VARCHAR(255)"); + $pdo->exec("ALTER TABLE users ADD COLUMN google_id VARCHAR(30)"); +} + +function version_13($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN is_ldap_user TINYINT(1) DEFAULT 0"); +} + +function version_12($pdo) +{ + $pdo->exec(" + CREATE TABLE remember_me ( + id INT NOT NULL AUTO_INCREMENT, + user_id INT, + ip VARCHAR(40), + user_agent VARCHAR(255), + token VARCHAR(255), + sequence VARCHAR(255), + expiration INT, + date_creation INT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY (id) + ) ENGINE=InnoDB CHARSET=utf8" + ); + + $pdo->exec(" + CREATE TABLE last_logins ( + id INT NOT NULL AUTO_INCREMENT, + auth_type VARCHAR(25), + user_id INT, + ip VARCHAR(40), + user_agent VARCHAR(255), + date_creation INT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY (id), + INDEX (user_id) + ) ENGINE=InnoDB CHARSET=utf8" + ); +} + +function version_11($pdo) +{ +} + +function version_10($pdo) +{ +} + +function version_9($pdo) +{ +} + +function version_8($pdo) +{ +} + +function version_7($pdo) +{ +} + +function version_6($pdo) +{ +} + +function version_5($pdo) +{ +} + +function version_4($pdo) +{ +} + +function version_3($pdo) +{ +} + +function version_2($pdo) +{ +} + +function version_1($pdo) +{ + $pdo->exec(" + CREATE TABLE config ( + language CHAR(5) DEFAULT 'en_US', + webhooks_token VARCHAR(255), + timezone VARCHAR(50) DEFAULT 'UTC' + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE users ( + id INT NOT NULL AUTO_INCREMENT, + username VARCHAR(50), + password VARCHAR(255), + is_admin TINYINT DEFAULT 0, + default_project_id INT DEFAULT 0, + PRIMARY KEY (id) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE projects ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) UNIQUE, + is_active TINYINT DEFAULT 1, + token VARCHAR(255), + PRIMARY KEY (id) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE project_has_users ( + id INT NOT NULL AUTO_INCREMENT, + project_id INT, + user_id INT, + PRIMARY KEY (id), + UNIQUE KEY `idx_project_user` (project_id, user_id), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE columns ( + id INT NOT NULL AUTO_INCREMENT, + title VARCHAR(255), + position INT NOT NULL, + project_id INT NOT NULL, + task_limit INT DEFAULT '0', + UNIQUE KEY `idx_title_project` (title, project_id), + PRIMARY KEY (id), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE tasks ( + id INT NOT NULL AUTO_INCREMENT, + title VARCHAR(255), + description TEXT, + date_creation INT, + date_completed INT, + date_due INT, + color_id VARCHAR(50), + project_id INT, + column_id INT, + owner_id INT DEFAULT '0', + position INT, + score INT, + is_active TINYINT DEFAULT 1, + PRIMARY KEY (id), + INDEX `idx_task_active` (is_active), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE comments ( + id INT NOT NULL AUTO_INCREMENT, + task_id INT, + user_id INT, + date INT, + comment TEXT, + PRIMARY KEY (id), + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE actions ( + id INT NOT NULL AUTO_INCREMENT, + project_id INT, + event_name VARCHAR(50), + action_name VARCHAR(50), + PRIMARY KEY (id), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE action_has_params ( + id INT NOT NULL AUTO_INCREMENT, + action_id INT, + name VARCHAR(50), + value VARCHAR(50), + PRIMARY KEY (id), + FOREIGN KEY(action_id) REFERENCES actions(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + INSERT INTO users + (username, password, is_admin) + VALUES ('admin', '".\password_hash('admin', PASSWORD_BCRYPT)."', '1') + "); + + $pdo->exec(" + INSERT INTO config + (webhooks_token) + VALUES ('".\Model\Base::generateToken()."') + "); +} diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php new file mode 100644 index 00000000..0bb4de8d --- /dev/null +++ b/app/Schema/Sqlite.php @@ -0,0 +1,259 @@ +<?php + +namespace Schema; + +const VERSION = 16; + +function version_16($pdo) +{ + $pdo->exec(" + CREATE TABLE project_has_categories ( + id INTEGER PRIMARY KEY, + name TEXT COLLATE NOCASE, + project_id INT, + UNIQUE (project_id, name), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + )" + ); + + $pdo->exec("ALTER TABLE tasks ADD COLUMN category_id INTEGER DEFAULT 0"); +} + +function version_15($pdo) +{ + $pdo->exec("ALTER TABLE projects ADD COLUMN last_modified INTEGER DEFAULT 0"); +} + +function version_14($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN name TEXT"); + $pdo->exec("ALTER TABLE users ADD COLUMN email TEXT"); + $pdo->exec("ALTER TABLE users ADD COLUMN google_id TEXT"); +} + +function version_13($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN is_ldap_user INTEGER DEFAULT 0"); +} + +function version_12($pdo) +{ + $pdo->exec( + 'CREATE TABLE remember_me ( + id INTEGER PRIMARY KEY, + user_id INTEGER, + ip TEXT, + user_agent TEXT, + token TEXT, + sequence TEXT, + expiration INTEGER, + date_creation INTEGER, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )' + ); + + $pdo->exec( + 'CREATE TABLE last_logins ( + id INTEGER PRIMARY KEY, + auth_type TEXT, + user_id INTEGER, + ip TEXT, + user_agent TEXT, + date_creation INTEGER, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )' + ); + + $pdo->exec('CREATE INDEX last_logins_user_idx ON last_logins(user_id)'); +} + +function version_11($pdo) +{ + $pdo->exec( + 'ALTER TABLE comments RENAME TO comments_bak' + ); + + $pdo->exec( + 'CREATE TABLE comments ( + id INTEGER PRIMARY KEY, + task_id INTEGER, + user_id INTEGER, + date INTEGER, + comment TEXT, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )' + ); + + $pdo->exec( + 'INSERT INTO comments SELECT * FROM comments_bak' + ); + + $pdo->exec( + 'DROP TABLE comments_bak' + ); +} + +function version_10($pdo) +{ + $pdo->exec( + 'CREATE TABLE actions ( + id INTEGER PRIMARY KEY, + project_id INTEGER, + event_name TEXT, + action_name TEXT, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + )' + ); + + $pdo->exec( + 'CREATE TABLE action_has_params ( + id INTEGER PRIMARY KEY, + action_id INTEGER, + name TEXT, + value TEXT, + FOREIGN KEY(action_id) REFERENCES actions(id) ON DELETE CASCADE + )' + ); +} + +function version_9($pdo) +{ + $pdo->exec("ALTER TABLE tasks ADD COLUMN date_due INTEGER"); +} + +function version_8($pdo) +{ + $pdo->exec( + 'CREATE TABLE comments ( + id INTEGER PRIMARY KEY, + task_id INTEGER, + user_id INTEGER, + date INTEGER, + comment TEXT, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES tasks(id) ON DELETE CASCADE + )' + ); +} + +function version_7($pdo) +{ + $pdo->exec(" + CREATE TABLE project_has_users ( + id INTEGER PRIMARY KEY, + project_id INTEGER, + user_id INTEGER, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(project_id, user_id) + ) + "); +} + +function version_6($pdo) +{ + $pdo->exec("ALTER TABLE columns ADD COLUMN task_limit INTEGER DEFAULT '0'"); +} + +function version_5($pdo) +{ + $pdo->exec("ALTER TABLE tasks ADD COLUMN score INTEGER"); +} + +function version_4($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN timezone TEXT DEFAULT 'UTC'"); +} + +function version_3($pdo) +{ + $pdo->exec('ALTER TABLE projects ADD COLUMN token TEXT'); + + // For each existing project, assign a different token + $rq = $pdo->prepare("SELECT id FROM projects WHERE token IS NULL"); + $rq->execute(); + $results = $rq->fetchAll(\PDO::FETCH_ASSOC); + + if ($results !== false) { + + foreach ($results as &$result) { + $rq = $pdo->prepare('UPDATE projects SET token=? WHERE id=?'); + $rq->execute(array(\Model\Base::generateToken(), $result['id'])); + } + } +} + +function version_2($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN date_completed INTEGER'); + $pdo->exec('UPDATE tasks SET date_completed=date_creation WHERE is_active=0'); +} + +function version_1($pdo) +{ + $pdo->exec(" + CREATE TABLE config ( + language TEXT, + webhooks_token TEXT + ) + "); + + $pdo->exec(" + CREATE TABLE users ( + id INTEGER PRIMARY KEY, + username TEXT, + password TEXT, + is_admin INTEGER DEFAULT 0, + default_project_id INTEGER DEFAULT 0 + ) + "); + + $pdo->exec(" + CREATE TABLE projects ( + id INTEGER PRIMARY KEY, + name TEXT NOCASE UNIQUE, + is_active INTEGER DEFAULT 1 + ) + "); + + $pdo->exec(" + CREATE TABLE columns ( + id INTEGER PRIMARY KEY, + title TEXT, + position INTEGER, + project_id INTEGER, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE (title, project_id) + ) + "); + + $pdo->exec(" + CREATE TABLE tasks ( + id INTEGER PRIMARY KEY, + title TEXT, + description TEXT, + date_creation INTEGER, + color_id TEXT, + project_id INTEGER, + column_id INTEGER, + owner_id INTEGER DEFAULT '0', + position INTEGER, + is_active INTEGER DEFAULT 1, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE + ) + "); + + $pdo->exec(" + INSERT INTO users + (username, password, is_admin) + VALUES ('admin', '".\password_hash('admin', PASSWORD_BCRYPT)."', '1') + "); + + $pdo->exec(" + INSERT INTO config + (language, webhooks_token) + VALUES ('en_US', '".\Model\Base::generateToken()."') + "); +} diff --git a/app/Templates/action_index.php b/app/Templates/action_index.php new file mode 100644 index 00000000..b515ccaa --- /dev/null +++ b/app/Templates/action_index.php @@ -0,0 +1,77 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + + <?php if (! empty($actions)): ?> + + <h3><?= t('Defined actions') ?></h3> + <table> + <tr> + <th><?= t('Event name') ?></th> + <th><?= t('Action name') ?></th> + <th><?= t('Action parameters') ?></th> + <th><?= t('Action') ?></th> + </tr> + + <?php foreach ($actions as $action): ?> + <tr> + <td><?= Helper\in_list($action['event_name'], $available_events) ?></td> + <td><?= Helper\in_list($action['action_name'], $available_actions) ?></td> + <td> + <ul> + <?php foreach ($action['params'] as $param): ?> + <li> + <?= Helper\in_list($param['name'], $available_params) ?> = + <strong> + <?php if (Helper\contains($param['name'], 'column_id')): ?> + <?= Helper\in_list($param['value'], $columns_list) ?> + <?php elseif (Helper\contains($param['name'], 'user_id')): ?> + <?= Helper\in_list($param['value'], $users_list) ?> + <?php elseif (Helper\contains($param['name'], 'project_id')): ?> + <?= Helper\in_list($param['value'], $projects_list) ?> + <?php elseif (Helper\contains($param['name'], 'color_id')): ?> + <?= Helper\in_list($param['value'], $colors_list) ?> + <?php elseif (Helper\contains($param['name'], 'category_id')): ?> + <?= Helper\in_list($param['value'], $categories_list) ?> + <?php endif ?> + </strong> + </li> + <?php endforeach ?> + </ul> + </td> + <td> + <a href="?controller=action&action=confirm&action_id=<?= $action['id'] ?>"><?= t('Remove') ?></a> + </td> + </tr> + <?php endforeach ?> + + </table> + + <?php endif ?> + + <h3><?= t('Add an action') ?></h3> + <form method="post" action="?controller=action&action=params&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('project_id', $values) ?> + + <?= Helper\form_label(t('Event'), 'event_name') ?> + <?= Helper\form_select('event_name', $available_events, $values) ?><br/> + + <?= Helper\form_label(t('Action'), 'action_name') ?> + <?= Helper\form_select('action_name', $available_actions, $values) ?><br/> + + <div class="form-help"> + <?= t('When the selected event occurs execute the corresponding action.') ?> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/action_params.php b/app/Templates/action_params.php new file mode 100644 index 00000000..15a1d420 --- /dev/null +++ b/app/Templates/action_params.php @@ -0,0 +1,43 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + + <h3><?= t('Define action parameters') ?></h3> + <form method="post" action="?controller=action&action=create&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('project_id', $values) ?> + <?= Helper\form_hidden('event_name', $values) ?> + <?= Helper\form_hidden('action_name', $values) ?> + + <?php foreach ($action_params as $param_name => $param_desc): ?> + + <?php if (Helper\contains($param_name, 'column_id')): ?> + <?= Helper\form_label($param_desc, $param_name) ?> + <?= Helper\form_select('params['.$param_name.']', $columns_list, $values) ?><br/> + <?php elseif (Helper\contains($param_name, 'user_id')): ?> + <?= Helper\form_label($param_desc, $param_name) ?> + <?= Helper\form_select('params['.$param_name.']', $users_list, $values) ?><br/> + <?php elseif (Helper\contains($param_name, 'project_id')): ?> + <?= Helper\form_label($param_desc, $param_name) ?> + <?= Helper\form_select('params['.$param_name.']', $projects_list, $values) ?><br/> + <?php elseif (Helper\contains($param_name, 'color_id')): ?> + <?= Helper\form_label($param_desc, $param_name) ?> + <?= Helper\form_select('params['.$param_name.']', $colors_list, $values) ?><br/> + <?php elseif (Helper\contains($param_name, 'category_id')): ?> + <?= Helper\form_label($param_desc, $param_name) ?> + <?= Helper\form_select('params['.$param_name.']', $categories_list, $values) ?><br/> + <?php endif ?> + <?php endforeach ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save this action') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=action&action=index&project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/action_remove.php b/app/Templates/action_remove.php new file mode 100644 index 00000000..b90136e8 --- /dev/null +++ b/app/Templates/action_remove.php @@ -0,0 +1,16 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Remove an automatic action') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this action: "%s"?', Helper\in_list($action['event_name'], $available_events).'/'.Helper\in_list($action['action_name'], $available_actions)) ?> + </p> + + <div class="form-actions"> + <a href="?controller=action&action=remove&action_id=<?= $action['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=action&action=index&project_id=<?= $action['project_id'] ?>"><?= t('cancel') ?></a> + </div> + </div> +</section>
\ No newline at end of file diff --git a/app/Templates/app_notfound.php b/app/Templates/app_notfound.php new file mode 100644 index 00000000..734d16a4 --- /dev/null +++ b/app/Templates/app_notfound.php @@ -0,0 +1,9 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Page not found') ?></h2> + </div> + + <p class="alert alert-error"> + <?= t('Sorry, I didn\'t found this information in my database!') ?> + </p> +</section>
\ No newline at end of file diff --git a/app/Templates/board_assign.php b/app/Templates/board_assign.php new file mode 100644 index 00000000..74448a5c --- /dev/null +++ b/app/Templates/board_assign.php @@ -0,0 +1,35 @@ +<section id="main"> + + <div class="page-header board"> + <h2> + <?= t('Project "%s"', $current_project_name) ?> + </h2> + <ul> + <?php foreach ($projects as $project_id => $project_name): ?> + <?php if ($project_id != $current_project_id): ?> + <li> + <a href="?controller=board&action=show&project_id=<?= $project_id ?>"><?= Helper\escape($project_name) ?></a> + </li> + <?php endif ?> + <?php endforeach ?> + </ul> + </div> + + <section> + <h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3> + <form method="post" action="?controller=board&action=assignTask" autocomplete="off"> + + <?= Helper\form_hidden('id', $values) ?> + <?= Helper\form_hidden('project_id', $values) ?> + + <?= Helper\form_label(t('Assignee'), 'owner_id') ?> + <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=board&action=show&project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a> + </div> + </form> + </section> + +</div>
\ No newline at end of file diff --git a/app/Templates/board_edit.php b/app/Templates/board_edit.php new file mode 100644 index 00000000..575536a8 --- /dev/null +++ b/app/Templates/board_edit.php @@ -0,0 +1,66 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Edit the board for "%s"', $project['name']) ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + + <h3><?= t('Change columns') ?></h3> + <form method="post" action="?controller=board&action=update&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?php $i = 0; ?> + <table> + <tr> + <th><?= t('Position') ?></th> + <th><?= t('Column title') ?></th> + <th><?= t('Task limit') ?></th> + <th><?= t('Actions') ?></th> + </tr> + <?php foreach ($columns as $column): ?> + <tr> + <td><?= Helper\form_label(t('Column %d', ++$i), 'title['.$column['id'].']', array('title="column_id='.$column['id'].'"')) ?></td> + <td><?= Helper\form_text('title['.$column['id'].']', $values, $errors, array('required')) ?></td> + <td><?= Helper\form_number('task_limit['.$column['id'].']', $values, $errors, array('placeholder="'.t('limit').'"')) ?></td> + <td> + <ul> + <?php if ($column['position'] != 1): ?> + <li> + <a href="?controller=board&action=moveUp&project_id=<?= $project['id'] ?>&column_id=<?= $column['id'] ?>"><?= t('Move Up') ?></a> + </li> + <?php endif ?> + <?php if ($column['position'] != count($columns)): ?> + <li> + <a href="?controller=board&action=moveDown&project_id=<?= $project['id'] ?>&column_id=<?= $column['id'] ?>"><?= t('Move Down') ?></a> + </li> + <?php endif ?> + <li> + <a href="?controller=board&action=confirm&project_id=<?= $project['id'] ?>&column_id=<?= $column['id'] ?>"><?= t('Remove') ?></a> + </li> + </ul> + </td> + </tr> + <?php endforeach ?> + </table> + + <div class="form-actions"> + <input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a> + </div> + </form> + + <h3><?= t('Add a new column') ?></h3> + <form method="post" action="?controller=board&action=add&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('project_id', $values) ?> + <?= Helper\form_label(t('Title'), 'title') ?> + <?= Helper\form_text('title', $values, $errors, array('required')) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Add this column') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/board_index.php b/app/Templates/board_index.php new file mode 100644 index 00000000..989c2e06 --- /dev/null +++ b/app/Templates/board_index.php @@ -0,0 +1,42 @@ +<section id="main"> + + <div class="page-header board"> + <h2> + <?= t('Project "%s"', $current_project_name) ?> + </h2> + <ul> + <?php foreach ($projects as $project_id => $project_name): ?> + <?php if ($project_id != $current_project_id): ?> + <li> + <a href="?controller=board&action=show&project_id=<?= $project_id ?>"><?= Helper\escape($project_name) ?></a> + </li> + <?php endif ?> + <?php endforeach ?> + </ul> + </div> + + <div class="project-menu"> + <ul> + <li> + <?= t('Filter by user') ?> + <?= Helper\form_select('user_id', $users, $filters) ?> + </li> + <li> + <?= t('Filter by category') ?> + <?= Helper\form_select('category_id', $categories, $filters) ?> + </li> + <li><a href="#" id="filter-due-date"><?= t('Filter by due date') ?></a></li> + <li><a href="?controller=project&action=search&project_id=<?= $current_project_id ?>"><?= t('Search') ?></a></li> + <li><a href="?controller=project&action=tasks&project_id=<?= $current_project_id ?>"><?= t('Completed tasks') ?></a></li> + </ul> + </div> + + <?php if (empty($board)): ?> + <p class="alert alert-error"><?= t('There is no column in your project!') ?></p> + <?php else: ?> + <?= Helper\template('board_show', array('current_project_id' => $current_project_id, 'board' => $board, 'categories' => $categories)) ?> + <?php endif ?> + +</section> + +<script type="text/javascript" src="assets/js/board.js"></script> diff --git a/app/Templates/board_public.php b/app/Templates/board_public.php new file mode 100644 index 00000000..0808079e --- /dev/null +++ b/app/Templates/board_public.php @@ -0,0 +1,79 @@ +<section id="main" class="public-board"> + + <?php if (empty($columns)): ?> + <p class="alert alert-error"><?= t('There is no column in your project!') ?></p> + <?php else: ?> + <table id="board"> + <tr> + <?php $column_with = round(100 / count($columns), 2); ?> + <?php foreach ($columns as $column): ?> + <th width="<?= $column_with ?>%"> + <?= Helper\escape($column['title']) ?> + <?php if ($column['task_limit']): ?> + <span title="<?= t('Task limit') ?>" class="task-limit">(<?= Helper\escape(count($column['tasks']).'/'.$column['task_limit']) ?>)</span> + <?php endif ?> + </th> + <?php endforeach ?> + </tr> + <tr> + <?php foreach ($columns as $column): ?> + <td class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>"> + <?php foreach ($column['tasks'] as $task): ?> + <div class="task task-<?= $task['color_id'] ?>"> + + #<?= $task['id'] ?> - + + <span class="task-user"> + <?php if (! empty($task['owner_id'])): ?> + <?= t('Assigned to %s', $task['username']) ?> + <?php else: ?> + <span class="task-nobody"><?= t('Nobody assigned') ?></span> + <?php endif ?> + </span> + + <?php if ($task['score']): ?> + <span class="task-score"><?= Helper\escape($task['score']) ?></span> + <?php endif ?> + + <div class="task-title"> + <?= Helper\escape($task['title']) ?> + </div> + + <?php if ($task['category_id']): ?> + <div class="task-category-container"> + <span class="task-category"> + <?= Helper\in_list($task['category_id'], $categories) ?> + </span> + </div> + <?php endif ?> + + <?php if (! empty($task['date_due']) || ! empty($task['nb_comments']) || ! empty($task['description'])): ?> + <div class="task-footer"> + + <?php if (! empty($task['date_due'])): ?> + <div class="task-date"> + <?= dt('%B %e, %G', $task['date_due']) ?> + </div> + <?php endif ?> + + <div class="task-icons"> + <?php if (! empty($task['nb_comments'])): ?> + <?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i> + <?php endif ?> + + <?php if (! empty($task['description'])): ?> + <i class="fa fa-file-text-o" title="<?= t('Description') ?>"></i> + <?php endif ?> + </div> + </div> + <?php endif ?> + + </div> + <?php endforeach ?> + </td> + <?php endforeach ?> + </tr> + </table> + <?php endif ?> + +</section>
\ No newline at end of file diff --git a/app/Templates/board_remove.php b/app/Templates/board_remove.php new file mode 100644 index 00000000..b406eb38 --- /dev/null +++ b/app/Templates/board_remove.php @@ -0,0 +1,17 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Remove a column') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this column: "%s"?', $column['title']) ?> + <?= t('This action will REMOVE ALL TASKS associated to this column!') ?> + </p> + + <div class="form-actions"> + <a href="?controller=board&action=remove&column_id=<?= $column['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=board&action=edit&project_id=<?= $column['project_id'] ?>"><?= t('cancel') ?></a> + </div> + </div> +</section>
\ No newline at end of file diff --git a/app/Templates/board_show.php b/app/Templates/board_show.php new file mode 100644 index 00000000..719e3bdd --- /dev/null +++ b/app/Templates/board_show.php @@ -0,0 +1,88 @@ +<table id="board" data-project-id="<?= $current_project_id ?>" data-time="<?= time() ?>" data-check-interval="<?= BOARD_CHECK_INTERVAL ?>"> +<tr> + <?php $column_with = round(100 / count($board), 2); ?> + <?php foreach ($board as $column): ?> + <th width="<?= $column_with ?>%"> + <a href="?controller=task&action=create&project_id=<?= $column['project_id'] ?>&column_id=<?= $column['id'] ?>" title="<?= t('Add a new task') ?>">+</a> + <?= Helper\escape($column['title']) ?> + <?php if ($column['task_limit']): ?> + <span title="<?= t('Task limit') ?>" class="task-limit"> + ( + <span id="task-number-column-<?= $column['id'] ?>"><?= count($column['tasks']) ?></span> + / + <?= Helper\escape($column['task_limit']) ?> + ) + </span> + <?php endif ?> + </th> + <?php endforeach ?> +</tr> +<tr> + <?php foreach ($board as $column): ?> + <td + id="column-<?= $column['id'] ?>" + class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>" + data-column-id="<?= $column['id'] ?>" + data-task-limit="<?= $column['task_limit'] ?>" + > + <?php foreach ($column['tasks'] as $task): ?> + <div class="task draggable-item task-<?= $task['color_id'] ?>" + data-task-id="<?= $task['id'] ?>" + data-owner-id="<?= $task['owner_id'] ?>" + data-category-id="<?= $task['category_id'] ?>" + data-due-date="<?= $task['date_due'] ?>" + title="<?= t('View this task') ?>"> + + <a href="?controller=task&action=edit&task_id=<?= $task['id'] ?>" title="<?= t('Edit this task') ?>">#<?= $task['id'] ?></a> - + + <span class="task-user"> + <?php if (! empty($task['owner_id'])): ?> + <a href="?controller=board&action=assign&task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>"><?= t('Assigned to %s', $task['username']) ?></a> + <?php else: ?> + <a href="?controller=board&action=assign&task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>" class="task-nobody"><?= t('Nobody assigned') ?></a> + <?php endif ?> + </span> + + <?php if ($task['score']): ?> + <span class="task-score"><?= Helper\escape($task['score']) ?></span> + <?php endif ?> + + <div class="task-title"> + <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a> + </div> + + <?php if ($task['category_id']): ?> + <div class="task-category-container"> + <span class="task-category"> + <?= Helper\in_list($task['category_id'], $categories) ?> + </span> + </div> + <?php endif ?> + + <?php if (! empty($task['date_due']) || ! empty($task['nb_comments']) || ! empty($task['description'])): ?> + <div class="task-footer"> + + <?php if (! empty($task['date_due'])): ?> + <div class="task-date"> + <?= dt('%B %e, %G', $task['date_due']) ?> + </div> + <?php endif ?> + + <div class="task-icons"> + <?php if (! empty($task['nb_comments'])): ?> + <?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i> + <?php endif ?> + + <?php if (! empty($task['description'])): ?> + <i class="fa fa-file-text-o" title="<?= t('Description') ?>"></i> + <?php endif ?> + </div> + </div> + <?php endif ?> + + </div> + <?php endforeach ?> + </td> + <?php endforeach ?> +</tr> +</table> diff --git a/app/Templates/category_edit.php b/app/Templates/category_edit.php new file mode 100644 index 00000000..99ba0c7c --- /dev/null +++ b/app/Templates/category_edit.php @@ -0,0 +1,24 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Category modification for the project "%s"', $project['name']) ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + + <form method="post" action="?controller=category&action=update&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('id', $values) ?> + <?= Helper\form_hidden('project_id', $values) ?> + + <?= Helper\form_label(t('Category Name'), 'name') ?> + <?= Helper\form_text('name', $values, $errors, array('required')) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </form> + + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/category_index.php b/app/Templates/category_index.php new file mode 100644 index 00000000..db986143 --- /dev/null +++ b/app/Templates/category_index.php @@ -0,0 +1,48 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Categories for the project "%s"', $project['name']) ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + + <?php if (! empty($categories)): ?> + <table> + <tr> + <th><?= t('Category Name') ?></th> + <th><?= t('Actions') ?></th> + </tr> + <?php foreach ($categories as $category_id => $category_name): ?> + <tr> + <td><?= Helper\escape($category_name) ?></td> + <td> + <ul> + <li> + <a href="?controller=category&action=edit&project_id=<?= $project['id'] ?>&category_id=<?= $category_id ?>"><?= t('Edit') ?></a> + </li> + <li> + <a href="?controller=category&action=confirm&project_id=<?= $project['id'] ?>&category_id=<?= $category_id ?>"><?= t('Remove') ?></a> + </li> + </ul> + </td> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> + + <h3><?= t('Add a new category') ?></h3> + <form method="post" action="?controller=category&action=save&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('project_id', $values) ?> + + <?= Helper\form_label(t('Category Name'), 'name') ?> + <?= Helper\form_text('name', $values, $errors, array('required')) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </form> + + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/category_remove.php b/app/Templates/category_remove.php new file mode 100644 index 00000000..cc2eb678 --- /dev/null +++ b/app/Templates/category_remove.php @@ -0,0 +1,16 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Remove a category') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this category: "%s"?', $category['name']) ?> + </p> + + <div class="form-actions"> + <a href="?controller=category&action=remove&project_id=<?= $project['id'] ?>&category_id=<?= $category['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=category&project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a> + </div> + </div> +</section>
\ No newline at end of file diff --git a/app/Templates/comment_forbidden.php b/app/Templates/comment_forbidden.php new file mode 100644 index 00000000..eeea8404 --- /dev/null +++ b/app/Templates/comment_forbidden.php @@ -0,0 +1,9 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Forbidden') ?></h2> + </div> + + <p class="alert alert-error"> + <?= t('Only administrators or the creator of the comment can access to this page.') ?> + </p> +</section>
\ No newline at end of file diff --git a/app/Templates/comment_remove.php b/app/Templates/comment_remove.php new file mode 100644 index 00000000..ad1b8e4a --- /dev/null +++ b/app/Templates/comment_remove.php @@ -0,0 +1,18 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Remove a comment') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this comment?') ?> + </p> + + <?= Helper\template('comment_show', array('comment' => $comment)) ?> + + <div class="form-actions"> + <a href="?controller=comment&action=remove&project_id=<?= $project_id ?>&comment_id=<?= $comment['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $comment['task_id'] ?>#comment-<?= $comment['id'] ?>"><?= t('cancel') ?></a> + </div> + </div> +</section>
\ No newline at end of file diff --git a/app/Templates/comment_show.php b/app/Templates/comment_show.php new file mode 100644 index 00000000..24bf9070 --- /dev/null +++ b/app/Templates/comment_show.php @@ -0,0 +1,36 @@ +<div class="<?= isset($display_edit_form) && $display_edit_form === true ? 'comment-edit' : 'comment' ?>" id="comment-<?= $comment['id'] ?>"> + <p class="comment-title"> + <span class="comment-username"><?= Helper\escape($comment['username']) ?></span> @ <span class="comment-date"><?= dt('%B %e, %G at %k:%M %p', $comment['date']) ?></span> + </p> + <?php if (isset($task)): ?> + <ul class="comment-actions"> + <li><a href="#comment-<?= $comment['id'] ?>"><?= t('link') ?></a></li> + <?php if (Helper\is_admin() || Helper\is_current_user($comment['user_id'])): ?> + <li> + <a href="?controller=comment&action=confirm&project_id=<?= $task['project_id'] ?>&comment_id=<?= $comment['id'] ?>"><?= t('remove') ?></a> + </li> + <li> + <a href="?controller=comment&action=edit&task_id=<?= $task['id'] ?>&comment_id=<?= $comment['id'] ?>#comment-<?= $comment['id'] ?>"><?= t('edit') ?></a> + </li> + <?php endif ?> + </ul> + <?php endif ?> + + <?php if (isset($display_edit_form) && $display_edit_form === true): ?> + <form method="post" action="?controller=comment&action=update&task_id=<?= $task['id'] ?>&comment_id=<?= $comment['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('id', $values) ?> + <?= Helper\form_textarea('comment', $values, $errors, array('required', 'placeholder="'.t('Leave a comment').'"')) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Update this comment') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> + </div> + </form> + <?php else: ?> + <div class="markdown"> + <?= Helper\markdown($comment['comment']) ?> + </div> + <?php endif ?> +</div>
\ No newline at end of file diff --git a/app/Templates/config_index.php b/app/Templates/config_index.php new file mode 100644 index 00000000..6c610d2b --- /dev/null +++ b/app/Templates/config_index.php @@ -0,0 +1,120 @@ +<section id="main"> + + <?php if ($user['is_admin']): ?> + <div class="page-header"> + <h2><?= t('Application settings') ?></h2> + </div> + <section> + <form method="post" action="?controller=config&action=save" autocomplete="off"> + + <?= Helper\form_label(t('Language'), 'language') ?> + <?= Helper\form_select('language', $languages, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Timezone'), 'timezone') ?> + <?= Helper\form_select('timezone', $timezones, $values, $errors) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </form> + </section> + <?php endif ?> + + <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> + + <?php if ($user['is_admin']): ?> + <div class="page-header"> + <h2><?= t('More information') ?></h2> + </div> + <section class="settings"> + <ul> + <li><a href="?controller=config&action=tokens"><?= t('Reset all tokens') ?></a></li> + <li> + <?= t('Webhooks token:') ?> + <strong><?= Helper\escape($values['webhooks_token']) ?></strong> + </li> + <?php if (DB_DRIVER === 'sqlite'): ?> + <li> + <?= t('Database size:') ?> + <strong><?= Helper\format_bytes($db_size) ?></strong> + </li> + <li> + <a href="?controller=config&action=downloadDb"><?= t('Download the database') ?></a> + <?= t('(Gzip compressed Sqlite file)') ?> + </li> + <li> + <a href="?controller=config&action=optimizeDb"><?= t('Optimize the database') ?></a> + <?= t('(VACUUM command)') ?> + </li> + <?php endif ?> + <li> + <?= t('Official website:') ?> + <a href="http://kanboard.net/" target="_blank" rel="noreferer">http://kanboard.net/</a> + </li> + <li> + <?= t('Application version:') ?> + <?= APP_VERSION ?> + </li> + </ul> + </section> + <?php endif ?> + + <div class="page-header" id="last-logins"> + <h2><?= t('Last logins') ?></h2> + </div> + <?php if (! empty($last_logins)): ?> + <table class="table-small table-hover"> + <tr> + <th><?= t('Login date') ?></th> + <th><?= t('Authentication method') ?></th> + <th><?= t('IP address') ?></th> + <th><?= t('User agent') ?></th> + </tr> + <?php foreach($last_logins as $login): ?> + <tr> + <td><?= dt('%B %e, %G at %k:%M %p', $login['date_creation']) ?></td> + <td><?= Helper\escape($login['auth_type']) ?></td> + <td><?= Helper\escape($login['ip']) ?></td> + <td><?= Helper\escape($login['user_agent']) ?></td> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> + + <div class="page-header" id="remember-me"> + <h2><?= t('Persistent connections') ?></h2> + </div> + <?php if (empty($remember_me_sessions)): ?> + <p class="alert alert-info"><?= t('No session') ?></p> + <?php else: ?> + <table class="table-small table-hover"> + <tr> + <th><?= t('Creation date') ?></th> + <th><?= t('Expiration date') ?></th> + <th><?= t('IP address') ?></th> + <th><?= t('User agent') ?></th> + <th><?= t('Action') ?></th> + </tr> + <?php foreach($remember_me_sessions as $session): ?> + <tr> + <td><?= dt('%B %e, %G at %k:%M %p', $session['date_creation']) ?></td> + <td><?= dt('%B %e, %G at %k:%M %p', $session['expiration']) ?></td> + <td><?= Helper\escape($session['ip']) ?></td> + <td><?= Helper\escape($session['user_agent']) ?></td> + <td><a href="?controller=config&action=removeRememberMeToken&id=<?= $session['id'] ?>"><?= t('Remove') ?></a></td> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> +</section> diff --git a/app/Templates/layout.php b/app/Templates/layout.php new file mode 100644 index 00000000..0bb8446d --- /dev/null +++ b/app/Templates/layout.php @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + + <meta name="viewport" content="width=device-width"> + <meta name="mobile-web-app-capable" content="yes"> + + <script src="assets/js/jquery-1.11.1.min.js"></script> + <script src="assets/js/jquery-ui-1.10.4.custom.min.js"></script> + <script src="assets/js/jquery.ui.touch-punch.min.js"></script> + + <link rel="stylesheet" href="assets/css/app.css" media="screen"> + <link rel="stylesheet" href="assets/css/font-awesome.min.css" media="screen"> + + <link rel="icon" type="image/png" href="assets/img/favicon.png"> + <link rel="apple-touch-icon" href="assets/img/touch-icon-iphone.png"> + <link rel="apple-touch-icon" sizes="72x72" href="assets/img/touch-icon-ipad.png"> + <link rel="apple-touch-icon" sizes="114x114" href="assets/img/touch-icon-iphone-retina.png"> + <link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png"> + + <title><?= isset($title) ? Helper\escape($title).' - Kanboard' : 'Kanboard' ?></title> + <?php if (isset($auto_refresh)): ?> + <meta http-equiv="refresh" content="<?= BOARD_PUBLIC_CHECK_INTERVAL ?>" > + <?php endif ?> + </head> + <body> + <?php if (isset($no_layout)): ?> + <?= $content_for_layout ?> + <?php else: ?> + <header> + <nav> + <a class="logo" href="?">kan<span>board</span></a> + <ul> + <li <?= isset($menu) && $menu === 'boards' ? 'class="active"' : '' ?>> + <a href="?controller=board"><?= t('Boards') ?></a> + </li> + <li <?= isset($menu) && $menu === 'projects' ? 'class="active"' : '' ?>> + <a href="?controller=project"><?= t('Projects') ?></a> + </li> + <li <?= isset($menu) && $menu === 'users' ? 'class="active"' : '' ?>> + <a href="?controller=user"><?= t('Users') ?></a> + </li> + <li <?= isset($menu) && $menu === 'config' ? 'class="active"' : '' ?>> + <a href="?controller=config"><?= t('Settings') ?></a> + </li> + <li> + <a href="?controller=user&action=logout"><?= t('Logout') ?></a> + (<?= Helper\escape(Helper\get_username()) ?>) + </li> + </ul> + </nav> + </header> + <section class="page"> + <?= Helper\flash('<div class="alert alert-success alert-fade-out">%s</div>') ?> + <?= Helper\flash_error('<div class="alert alert-error">%s</div>') ?> + <?= $content_for_layout ?> + </section> + <?php endif ?> + </body> +</html>
\ No newline at end of file diff --git a/app/Templates/project_edit.php b/app/Templates/project_edit.php new file mode 100644 index 00000000..557986bf --- /dev/null +++ b/app/Templates/project_edit.php @@ -0,0 +1,24 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Edit project') ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + <form method="post" action="?controller=project&action=update&project_id=<?= $values['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('id', $values) ?> + + <?= Helper\form_label(t('Name'), 'name') ?> + <?= Helper\form_text('name', $values, $errors, array('required')) ?> + + <?= Helper\form_checkbox('is_active', t('Activated'), 1, isset($values['is_active']) && $values['is_active'] == 1 ? true : false) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/project_forbidden.php b/app/Templates/project_forbidden.php new file mode 100644 index 00000000..1cba7b58 --- /dev/null +++ b/app/Templates/project_forbidden.php @@ -0,0 +1,9 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Forbidden') ?></h2> + </div> + + <p class="alert alert-error"> + <?= t('You are not allowed to access to this project.') ?> + </p> +</section>
\ No newline at end of file diff --git a/app/Templates/project_index.php b/app/Templates/project_index.php new file mode 100644 index 00000000..df153fe7 --- /dev/null +++ b/app/Templates/project_index.php @@ -0,0 +1,98 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Projects') ?><span id="page-counter"> (<?= $nb_projects ?>)</span></h2> + <?php if (Helper\is_admin()): ?> + <ul> + <li><a href="?controller=project&action=create"><?= t('New project') ?></a></li> + </ul> + <?php endif ?> + </div> + <section> + <?php if (empty($projects)): ?> + <p class="alert"><?= t('No project') ?></p> + <?php else: ?> + <table> + <tr> + <th><?= t('Project') ?></th> + <th><?= t('Status') ?></th> + <th><?= t('Tasks') ?></th> + <th><?= t('Board') ?></th> + + <?php if (Helper\is_admin()): ?> + <th><?= t('Actions') ?></th> + <?php endif ?> + </tr> + <?php foreach ($projects as $project): ?> + <tr> + <td> + <a href="?controller=board&action=show&project_id=<?= $project['id'] ?>" title="project_id=<?= $project['id'] ?>"><?= Helper\escape($project['name']) ?></a> + </td> + <td> + <?= $project['is_active'] ? t('Active') : t('Inactive') ?> + </td> + <td> + <?php if ($project['nb_tasks'] > 0): ?> + + <?php if ($project['nb_active_tasks'] > 0): ?> + <a href="?controller=board&action=show&project_id=<?= $project['id'] ?>"><?= t('%d tasks on the board', $project['nb_active_tasks']) ?></a>, + <?php endif ?> + + <?php if ($project['nb_inactive_tasks'] > 0): ?> + <a href="?controller=project&action=tasks&project_id=<?= $project['id'] ?>"><?= t('%d closed tasks', $project['nb_inactive_tasks']) ?></a>, + <?php endif ?> + + <?= t('%d tasks in total', $project['nb_tasks']) ?> + + <?php else: ?> + <?= t('no task for this project') ?> + <?php endif ?> + </td> + <td> + <ul> + <?php foreach ($project['columns'] as $column): ?> + <li> + <span title="column_id=<?= $column['id'] ?>"><?= Helper\escape($column['title']) ?></span> (<?= $column['nb_active_tasks'] ?>) + </li> + <?php endforeach ?> + </ul> + </td> + <?php if (Helper\is_admin()): ?> + <td> + <ul> + <li> + <a href="?controller=category&action=index&project_id=<?= $project['id'] ?>"><?= t('Categories') ?></a> + </li> + <li> + <a href="?controller=project&action=edit&project_id=<?= $project['id'] ?>"><?= t('Edit project') ?></a> + </li> + <li> + <a href="?controller=project&action=users&project_id=<?= $project['id'] ?>"><?= t('Edit users access') ?></a> + </li> + <li> + <a href="?controller=board&action=edit&project_id=<?= $project['id'] ?>"><?= t('Edit board') ?></a> + </li> + <li> + <a href="?controller=action&action=index&project_id=<?= $project['id'] ?>"><?= t('Automatic actions') ?></a> + </li> + <li> + <?php if ($project['is_active']): ?> + <a href="?controller=project&action=disable&project_id=<?= $project['id'] ?>"><?= t('Disable') ?></a> + <?php else: ?> + <a href="?controller=project&action=enable&project_id=<?= $project['id'] ?>"><?= t('Enable') ?></a> + <?php endif ?> + </li> + <li> + <a href="?controller=project&action=confirm&project_id=<?= $project['id'] ?>"><?= t('Remove') ?></a> + </li> + <li> + <a href="?controller=board&action=readonly&token=<?= $project['token'] ?>" target="_blank"><?= t('Public link') ?></a> + </li> + </ul> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/project_new.php b/app/Templates/project_new.php new file mode 100644 index 00000000..2026d461 --- /dev/null +++ b/app/Templates/project_new.php @@ -0,0 +1,20 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('New project') ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + <form method="post" action="?controller=project&action=save" autocomplete="off"> + + <?= Helper\form_label(t('Name'), 'name') ?> + <?= Helper\form_text('name', $values, $errors, array('autofocus', 'required')) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/project_remove.php b/app/Templates/project_remove.php new file mode 100644 index 00000000..e9f213b5 --- /dev/null +++ b/app/Templates/project_remove.php @@ -0,0 +1,16 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Remove project') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this project: "%s"?', $project['name']) ?> + </p> + + <div class="form-actions"> + <a href="?controller=project&action=remove&project_id=<?= $project['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a> + </div> + </div> +</section>
\ No newline at end of file diff --git a/app/Templates/project_search.php b/app/Templates/project_search.php new file mode 100644 index 00000000..3594fd09 --- /dev/null +++ b/app/Templates/project_search.php @@ -0,0 +1,93 @@ +<section id="main"> + <div class="page-header"> + <h2> + <?= t('Search in the project "%s"', $project['name']) ?> + <?php if (! empty($nb_tasks)): ?> + <span id="page-counter"> (<?= $nb_tasks ?>)</span> + <?php endif ?> + </h2> + <ul> + <li><a href="?controller=board&action=show&project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li> + <li><a href="?controller=project&action=tasks&project_id=<?= $project['id'] ?>"><?= t('Completed tasks') ?></a></li> + <li><a href="?controller=project&action=index"><?= t('List of projects') ?></a></li> + </ul> + </div> + <section> + <form method="get" action="?" autocomplete="off"> + <?= Helper\form_hidden('controller', $values) ?> + <?= Helper\form_hidden('action', $values) ?> + <?= Helper\form_hidden('project_id', $values) ?> + <?= Helper\form_text('search', $values, array(), array('autofocus', 'required', 'placeholder="'.t('Search').'"')) ?> + <input type="submit" value="<?= t('Search') ?>" class="btn btn-blue"/> + </form> + + <?php if (empty($tasks) && ! empty($values['search'])): ?> + <p class="alert"><?= t('Nothing found.') ?></p> + <?php elseif (! empty($tasks)): ?> + <table> + <tr> + <th><?= t('Id') ?></th> + <th><?= t('Column') ?></th> + <th><?= t('Category') ?></th> + <th><?= t('Title') ?></th> + <th><?= t('Assignee') ?></th> + <th><?= t('Due date') ?></th> + <th><?= t('Date created') ?></th> + <th><?= t('Date completed') ?></th> + <th><?= t('Status') ?></th> + </tr> + <?php foreach ($tasks as $task): ?> + <tr> + <td class="task task-<?= $task['color_id'] ?>"> + <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['id']) ?></a> + </td> + <td> + <?= Helper\in_list($task['column_id'], $columns) ?> + </td> + <td> + <?= Helper\in_list($task['category_id'], $categories, '') ?> + </td> + <td> + <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a> + <div class="task-table-icons"> + <?php if (! empty($task['nb_comments'])): ?> + <?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i> + <?php endif ?> + + <?php if (! empty($task['description'])): ?> + <i class="fa fa-file-text-o" title="<?= t('Description') ?>"></i> + <?php endif ?> + </div> + </td> + <td> + <?php if ($task['username']): ?> + <?= Helper\escape($task['username']) ?> + <?php else: ?> + <?= t('Unassigned') ?> + <?php endif ?> + </td> + <td> + <?= dt('%B %e, %G', $task['date_due']) ?> + </td> + <td> + <?= dt('%B %e, %G at %k:%M %p', $task['date_creation']) ?> + </td> + <td> + <?php if ($task['date_completed']): ?> + <?= dt('%B %e, %G at %k:%M %p', $task['date_completed']) ?> + <?php endif ?> + </td> + <td> + <?php if ($task['is_active'] == \Model\Task::STATUS_OPEN): ?> + <?= t('Open') ?> + <?php else: ?> + <?= t('Closed') ?> + <?php endif ?> + </td> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> + + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/project_tasks.php b/app/Templates/project_tasks.php new file mode 100644 index 00000000..9f4263b8 --- /dev/null +++ b/app/Templates/project_tasks.php @@ -0,0 +1,71 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Completed tasks for "%s"', $project['name']) ?><span id="page-counter"> (<?= $nb_tasks ?>)</span></h2> + <ul> + <li><a href="?controller=board&action=show&project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li> + <li><a href="?controller=project&action=search&project_id=<?= $project['id'] ?>"><?= t('Search') ?></a></li> + <li><a href="?controller=project&action=index"><?= t('List of projects') ?></a></li> + </ul> + </div> + <section> + <?php if (empty($tasks)): ?> + <p class="alert"><?= t('No task') ?></p> + <?php else: ?> + <table> + <tr> + <th><?= t('Id') ?></th> + <th><?= t('Column') ?></th> + <th><?= t('Category') ?></th> + <th><?= t('Title') ?></th> + <th><?= t('Assignee') ?></th> + <th><?= t('Due date') ?></th> + <th><?= t('Date created') ?></th> + <th><?= t('Date completed') ?></th> + </tr> + <?php foreach ($tasks as $task): ?> + <tr> + <td class="task task-<?= $task['color_id'] ?>"> + <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['id']) ?></a> + </td> + <td> + <?= Helper\in_list($task['column_id'], $columns) ?> + </td> + <td> + <?= Helper\in_list($task['category_id'], $categories, '') ?> + </td> + <td> + <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a> + <div class="task-table-icons"> + <?php if (! empty($task['nb_comments'])): ?> + <?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i> + <?php endif ?> + + <?php if (! empty($task['description'])): ?> + <i class="fa fa-file-text-o" title="<?= t('Description') ?>"></i> + <?php endif ?> + </div> + </td> + <td> + <?php if ($task['username']): ?> + <?= Helper\escape($task['username']) ?> + <?php else: ?> + <?= t('Unassigned') ?> + <?php endif ?> + </td> + <td> + <?= dt('%B %e, %G', $task['date_due']) ?> + </td> + <td> + <?= dt('%B %e, %G at %k:%M %p', $task['date_creation']) ?> + </td> + <td> + <?php if ($task['date_completed']): ?> + <?= dt('%B %e, %G at %k:%M %p', $task['date_completed']) ?> + <?php endif ?> + </td> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/project_users.php b/app/Templates/project_users.php new file mode 100644 index 00000000..0448004f --- /dev/null +++ b/app/Templates/project_users.php @@ -0,0 +1,44 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Project access list for "%s"', $project['name']) ?></h2> + <ul> + <li><a href="?controller=project"><?= t('All projects') ?></a></li> + </ul> + </div> + <section> + + <?php if (! empty($users['not_allowed'])): ?> + <form method="post" action="?controller=project&action=allow&project_id=<?= $project['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('project_id', array('project_id' => $project['id'])) ?> + + <?= Helper\form_label(t('User'), 'user_id') ?> + <?= Helper\form_select('user_id', $users['not_allowed']) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Allow this user') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=project"><?= t('cancel') ?></a> + </div> + </form> + <?php endif ?> + + <h3><?= t('List of authorized users') ?></h3> + <?php if (empty($users['allowed'])): ?> + <div class="alert alert-info"><?= t('Everybody have access to this project.') ?></div> + <?php else: ?> + <div class="listing"> + <p><?= t('Only those users have access to this project:') ?></p> + <ul> + <?php foreach ($users['allowed'] as $user_id => $username): ?> + <li> + <strong><?= Helper\escape($username) ?></strong> + (<a href="?controller=project&action=revoke&project_id=<?= $project['id'] ?>&user_id=<?= $user_id ?>"><?= t('revoke') ?></a>) + </li> + <?php endforeach ?> + </ul> + <p><?= t('Don\'t forget that administrators have access to everything.') ?></p> + </div> + <?php endif ?> + + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/task_close.php b/app/Templates/task_close.php new file mode 100644 index 00000000..3531b37d --- /dev/null +++ b/app/Templates/task_close.php @@ -0,0 +1,10 @@ +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to close this task: "%s"?', Helper\escape($task['title'])) ?> + </p> + + <div class="form-actions"> + <a href="?controller=task&action=close&task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> + </div> +</div>
\ No newline at end of file diff --git a/app/Templates/task_edit.php b/app/Templates/task_edit.php new file mode 100644 index 00000000..8c8bc107 --- /dev/null +++ b/app/Templates/task_edit.php @@ -0,0 +1,51 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Edit a task') ?></h2> + </div> + <section> + <form method="post" action="?controller=task&action=update" autocomplete="off"> + + <div class="form-column"> + + <?= Helper\form_label(t('Title'), 'title') ?> + <?= Helper\form_text('title', $values, $errors, array('required')) ?><br/> + + <?= Helper\form_label(t('Description'), 'description') ?> + <?= Helper\form_textarea('description', $values, $errors) ?><br/> + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + </div> + + <div class="form-column"> + + <?= Helper\form_hidden('id', $values) ?> + <?= Helper\form_hidden('project_id', $values) ?> + + <?= Helper\form_label(t('Assignee'), 'owner_id') ?> + <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Category'), 'category_id') ?> + <?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Column'), 'column_id') ?> + <?= Helper\form_select('column_id', $columns_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Color'), 'color_id') ?> + <?= Helper\form_select('color_id', $colors_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Story Points'), 'score') ?> + <?= Helper\form_number('score', $values, $errors) ?><br/> + + <?= Helper\form_label(t('Due Date'), 'date_due') ?> + <?= Helper\form_text('date_due', $values, $errors, array('placeholder="'.t('month/day/year').'"'), 'form-date') ?><br/> + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=board&action=show&project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/task_layout.php b/app/Templates/task_layout.php new file mode 100644 index 00000000..9a6bbd00 --- /dev/null +++ b/app/Templates/task_layout.php @@ -0,0 +1,16 @@ +<section id="main"> + <div class="page-header"> + <h2><?= Helper\escape($task['project_name']) ?> > <?= t('Task #%d', $task['id']) ?></h2> + <ul> + <li><a href="?controller=board&action=show&project_id=<?= $task['project_id'] ?>"><?= t('Back to the board') ?></a></li> + </ul> + </div> + <section class="task-show"> + + <?= Helper\template('task_sidebar', array('task' => $task)) ?> + + <div class="task-show-main"> + <?= $task_content_for_layout ?> + </div> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/task_new.php b/app/Templates/task_new.php new file mode 100644 index 00000000..d233efd2 --- /dev/null +++ b/app/Templates/task_new.php @@ -0,0 +1,51 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('New task') ?></h2> + </div> + <section> + <form method="post" action="?controller=task&action=save" autocomplete="off"> + + <div class="form-column"> + <?= Helper\form_label(t('Title'), 'title') ?> + <?= Helper\form_text('title', $values, $errors, array('autofocus', 'required')) ?><br/> + + <?= Helper\form_label(t('Description'), 'description') ?> + <?= Helper\form_textarea('description', $values, $errors) ?><br/> + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <?php if (! isset($duplicate)): ?> + <?= Helper\form_checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?> + <?php endif ?> + </div> + + <div class="form-column"> + + <?= Helper\form_hidden('project_id', $values) ?> + + <?= Helper\form_label(t('Assignee'), 'owner_id') ?> + <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Category'), 'category_id') ?> + <?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Column'), 'column_id') ?> + <?= Helper\form_select('column_id', $columns_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Color'), 'color_id') ?> + <?= Helper\form_select('color_id', $colors_list, $values, $errors) ?><br/> + + <?= Helper\form_label(t('Story Points'), 'score') ?> + <?= Helper\form_number('score', $values, $errors) ?><br/> + + <?= Helper\form_label(t('Due Date'), 'date_due') ?> + <?= Helper\form_text('date_due', $values, $errors, array('placeholder="'.t('month/day/year').'"'), 'form-date') ?><br/> + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=board&action=show&project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/task_open.php b/app/Templates/task_open.php new file mode 100644 index 00000000..54cc11f0 --- /dev/null +++ b/app/Templates/task_open.php @@ -0,0 +1,16 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Open a task') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to open this task: "%s"?', Helper\escape($task['title'])) ?> + </p> + + <div class="form-actions"> + <a href="?controller=task&action=open&task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> + </div> + </div> +</section>
\ No newline at end of file diff --git a/app/Templates/task_remove.php b/app/Templates/task_remove.php new file mode 100644 index 00000000..1aa9503b --- /dev/null +++ b/app/Templates/task_remove.php @@ -0,0 +1,10 @@ +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this task: "%s"?', Helper\escape($task['title'])) ?> + </p> + + <div class="form-actions"> + <a href="?controller=task&action=remove&task_id=<?= $task['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> + </div> +</div>
\ No newline at end of file diff --git a/app/Templates/task_show.php b/app/Templates/task_show.php new file mode 100644 index 00000000..a5b79359 --- /dev/null +++ b/app/Templates/task_show.php @@ -0,0 +1,94 @@ +<article class="task task-<?= $task['color_id'] ?> task-show-details"> + <h2><?= Helper\escape($task['title']) ?></h2> +<?php if ($task['score']): ?> + <span class="task-score"><?= Helper\escape($task['score']) ?></span> +<?php endif ?> +<ul> + <li> + <?= dt('Created on %B %e, %G at %k:%M %p', $task['date_creation']) ?> + </li> + <?php if ($task['date_completed']): ?> + <li> + <?= dt('Completed on %B %e, %G at %k:%M %p', $task['date_completed']) ?> + </li> + <?php endif ?> + <?php if ($task['date_due']): ?> + <li> + <strong><?= dt('Must be done before %B %e, %G', $task['date_due']) ?></strong> + </li> + <?php endif ?> + <li> + <strong> + <?php if ($task['username']): ?> + <?= t('Assigned to %s', $task['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> + (<?= Helper\escape($task['project_name']) ?>) + </li> + <?php if ($task['category_name']): ?> + <li> + <?= t('Category:') ?> <strong><?= Helper\escape($task['category_name']) ?></strong> + </li> + <?php endif ?> + <li> + <?php if ($task['is_active'] == 1): ?> + <?= t('Status is open') ?> + <?php else: ?> + <?= t('Status is closed') ?> + <?php endif ?> + </li> +</ul> +</article> + +<h2><?= t('Description') ?></h2> +<?php if ($task['description']): ?> + <article class="markdown task-show-description"> + <?= Helper\parse($task['description']) ?: t('There is no description.') ?> + </article> +<?php else: ?> + <form method="post" action="?controller=task&action=description&task_id=<?= $task['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('id', $description_form['values']) ?> + <?= Helper\form_textarea('description', $description_form['values'], $description_form['errors'], array('required', 'placeholder="'.t('Leave a description').'"')) ?><br/> + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </form> +<?php endif ?> + +<h2><?= t('Comments') ?></h2> +<?php if ($comments): ?> + <ul id="comments"> + <?php foreach ($comments as $comment): ?> + <?= Helper\template('comment_show', array( + 'comment' => $comment, + 'task' => $task, + 'display_edit_form' => $comment['id'] == $comment_edit_form['values']['id'], + 'values' => $comment_edit_form['values'] + array('comment' => $comment['comment']), + 'errors' => $comment_edit_form['errors'] + )) ?> + <?php endforeach ?> + </ul> +<?php endif ?> + +<?php if (! isset($hide_comment_form) || $hide_comment_form === false): ?> +<form method="post" action="?controller=comment&action=save&task_id=<?= $task['id'] ?>" autocomplete="off"> + + <?= Helper\form_hidden('task_id', $comment_form['values']) ?> + <?= Helper\form_hidden('user_id', $comment_form['values']) ?> + <?= Helper\form_textarea('comment', $comment_form['values'], $comment_form['errors'], array('required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/> + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Post comment') ?>" class="btn btn-blue"/> + </div> +</form> +<?php endif ?>
\ No newline at end of file diff --git a/app/Templates/task_sidebar.php b/app/Templates/task_sidebar.php new file mode 100644 index 00000000..314d5214 --- /dev/null +++ b/app/Templates/task_sidebar.php @@ -0,0 +1,17 @@ +<div class="task-show-sidebar"> + <h2><?= t('Actions') ?></h2> + <div class="task-show-actions"> + <ul> + <li><a href="?controller=task&action=duplicate&project_id=<?= $task['project_id'] ?>&task_id=<?= $task['id'] ?>"><?= t('Duplicate') ?></a></li> + <li><a href="?controller=task&action=edit&task_id=<?= $task['id'] ?>"><?= t('Edit') ?></a></li> + <li> + <?php if ($task['is_active'] == 1): ?> + <a href="?controller=task&action=confirmClose&task_id=<?= $task['id'] ?>"><?= t('Close this task') ?></a> + <?php else: ?> + <a href="?controller=task&action=confirmOpen&task_id=<?= $task['id'] ?>"><?= t('Open this task') ?></a> + <?php endif ?> + </li> + <li><a href="?controller=task&action=confirmRemove&task_id=<?= $task['id'] ?>"><?= t('Remove') ?></a></li> + </ul> + </div> +</div>
\ No newline at end of file diff --git a/app/Templates/user_edit.php b/app/Templates/user_edit.php new file mode 100644 index 00000000..c857fe1c --- /dev/null +++ b/app/Templates/user_edit.php @@ -0,0 +1,64 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Edit user') ?></h2> + <ul> + <li><a href="?controller=user"><?= t('All users') ?></a></li> + </ul> + </div> + <section> + <form method="post" action="?controller=user&action=update" autocomplete="off"> + + <div class="form-column"> + + <?= Helper\form_hidden('id', $values) ?> + <?= Helper\form_hidden('is_ldap_user', $values) ?> + + <?= Helper\form_label(t('Username'), 'username') ?> + <?= Helper\form_text('username', $values, $errors, array('required', $values['is_ldap_user'] == 1 ? 'readonly' : '')) ?><br/> + + <?= Helper\form_label(t('Name'), 'name') ?> + <?= Helper\form_text('name', $values, $errors) ?><br/> + + <?= Helper\form_label(t('Email'), 'email') ?> + <?= Helper\form_email('email', $values, $errors) ?><br/> + + <?= Helper\form_label(t('Default Project'), 'default_project_id') ?> + <?= Helper\form_select('default_project_id', $projects, $values, $errors) ?><br/> + + </div> + + <div class="form-column"> + + <?php if ($values['is_ldap_user'] == 0): ?> + + <?= Helper\form_label(t('Current password for the user "%s"', Helper\get_username()), 'current_password') ?> + <?= Helper\form_password('current_password', $values, $errors) ?><br/> + + <?= Helper\form_label(t('Password'), 'password') ?> + <?= Helper\form_password('password', $values, $errors) ?><br/> + + <?= Helper\form_label(t('Confirmation'), 'confirmation') ?> + <?= Helper\form_password('confirmation', $values, $errors) ?><br/> + + <?php endif ?> + + <?php if (Helper\is_admin()): ?> + <?= Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?><br/> + <?php endif ?> + + <?php if (GOOGLE_AUTH && Helper\is_current_user($values['id'])): ?> + <?php if (empty($values['google_id'])): ?> + <a href="?controller=user&action=google"><?= t('Link my Google Account') ?></a> + <?php else: ?> + <a href="?controller=user&action=unlinkGoogle"><?= t('Unlink my Google Account') ?></a> + <?php endif ?> + <?php endif ?> + + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> <?= t('or') ?> <a href="?controller=user"><?= t('cancel') ?></a> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/user_forbidden.php b/app/Templates/user_forbidden.php new file mode 100644 index 00000000..853159ba --- /dev/null +++ b/app/Templates/user_forbidden.php @@ -0,0 +1,9 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Forbidden') ?></h2> + </div> + + <p class="alert alert-error"> + <?= t('Only administrators can access to this page.') ?> + </p> +</section>
\ No newline at end of file diff --git a/app/Templates/user_index.php b/app/Templates/user_index.php new file mode 100644 index 00000000..f6302a6b --- /dev/null +++ b/app/Templates/user_index.php @@ -0,0 +1,56 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Users') ?><span id="page-counter"> (<?= $nb_users ?>)</span></h2> + <?php if (Helper\is_admin()): ?> + <ul> + <li><a href="?controller=user&action=create"><?= t('New user') ?></a></li> + </ul> + <?php endif ?> + </div> + <section> + <?php if (empty($users)): ?> + <p class="alert"><?= t('No user') ?></p> + <?php else: ?> + <table> + <tr> + <th><?= t('Username') ?></th> + <th><?= t('Name') ?></th> + <th><?= t('Email') ?></th> + <th><?= t('Administrator') ?></th> + <th><?= t('Default Project') ?></th> + <th><?= t('Actions') ?></th> + </tr> + <?php foreach ($users as $user): ?> + <tr> + <td> + <span title="user_id=<?= $user['id'] ?>"><?= Helper\escape($user['username']) ?></span> + </td> + <td> + <?= Helper\escape($user['name']) ?> + </td> + <td> + <?= Helper\escape($user['email']) ?> + </td> + <td> + <?= $user['is_admin'] ? t('Yes') : t('No') ?> + </td> + <td> + <?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? Helper\escape($projects[$user['default_project_id']]) : t('None'); ?> + </td> + <td> + <?php if (Helper\is_admin() || Helper\is_current_user($user['id'])): ?> + <a href="?controller=user&action=edit&user_id=<?= $user['id'] ?>"><?= t('edit') ?></a> + <?php endif ?> + <?php if (Helper\is_admin()): ?> + <?php if (count($users) > 1): ?> + <?= t('or') ?> + <a href="?controller=user&action=confirm&user_id=<?= $user['id'] ?>"><?= t('remove') ?></a> + <?php endif ?> + <?php endif ?> + </td> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> + </section> +</section> diff --git a/app/Templates/user_login.php b/app/Templates/user_login.php new file mode 100644 index 00000000..878170e3 --- /dev/null +++ b/app/Templates/user_login.php @@ -0,0 +1,28 @@ +<div class="page-header"> + <h1><?= t('Sign in') ?></h1> +</div> + +<?php if (isset($errors['login'])): ?> + <p class="alert alert-error"><?= Helper\escape($errors['login']) ?></p> +<?php endif ?> + +<form method="post" action="?controller=user&action=check" class="form-login"> + + <?= Helper\form_label(t('Username'), 'username') ?> + <?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/> + + <?= Helper\form_label(t('Password'), 'password') ?> + <?= Helper\form_password('password', $values, $errors, array('required')) ?> + + <?= Helper\form_checkbox('remember_me', t('Remember Me'), 1) ?><br/> + + <?php if (GOOGLE_AUTH): ?> + <p> + <a href="?controller=user&action=google"><?= t('Login with my Google Account') ?></a> + </p> + <?php endif ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Templates/user_new.php b/app/Templates/user_new.php new file mode 100644 index 00000000..6ad976f2 --- /dev/null +++ b/app/Templates/user_new.php @@ -0,0 +1,45 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('New user') ?></h2> + <ul> + <li><a href="?controller=user"><?= t('All users') ?></a></li> + </ul> + </div> + <section> + <form method="post" action="?controller=user&action=save" autocomplete="off"> + + <div class="form-column"> + + <?= Helper\form_label(t('Username'), 'username') ?> + <?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/> + + <?= Helper\form_label(t('Name'), 'name') ?> + <?= Helper\form_text('name', $values, $errors) ?><br/> + + <?= Helper\form_label(t('Email'), 'email') ?> + <?= Helper\form_email('email', $values, $errors) ?><br/> + + <?= Helper\form_label(t('Default Project'), 'default_project_id') ?> + <?= Helper\form_select('default_project_id', $projects, $values, $errors) ?><br/> + + </div> + + <div class="form-column"> + + <?= Helper\form_label(t('Password'), 'password') ?> + <?= Helper\form_password('password', $values, $errors, array('required')) ?><br/> + + <?= Helper\form_label(t('Confirmation'), 'confirmation') ?> + <?= Helper\form_password('confirmation', $values, $errors, array('required')) ?><br/> + + <?= Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?> + + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> <a href="?controller=user"><?= t('cancel') ?></a> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/user_remove.php b/app/Templates/user_remove.php new file mode 100644 index 00000000..a4db2e4a --- /dev/null +++ b/app/Templates/user_remove.php @@ -0,0 +1,14 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Remove user') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to remove this user: "%s"?', $user['username']) ?></p> + + <div class="form-actions"> + <a href="?controller=user&action=remove&user_id=<?= $user['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a> + <?= t('or') ?> <a href="?controller=user"><?= t('cancel') ?></a> + </div> + </div> +</section>
\ No newline at end of file diff --git a/app/check_setup.php b/app/check_setup.php new file mode 100644 index 00000000..9ed16967 --- /dev/null +++ b/app/check_setup.php @@ -0,0 +1,40 @@ +<?php + +// PHP 5.3.3 minimum +if (version_compare(PHP_VERSION, '5.3.3', '<')) { + die('This software require PHP 5.3.3 minimum'); +} + +// Checks for PHP < 5.4 +if (version_compare(PHP_VERSION, '5.4.0', '<')) { + + // Short tags must be enabled for PHP < 5.4 + if (! ini_get('short_open_tag')) { + die('This software require to have short tags enabled if you have PHP < 5.4 ("short_open_tag = On")'); + } + + // Magic quotes are deprecated since PHP 5.4 + if (get_magic_quotes_gpc()) { + die('This software require to have "Magic quotes" disabled, it\'s deprecated since PHP 5.4 ("magic_quotes_gpc = Off")'); + } +} + +// Check extension: PDO +if (! extension_loaded('pdo_sqlite') && ! extension_loaded('pdo_mysql')) { + die('PHP extension required: pdo_sqlite or pdo_mysql'); +} + +// Check extension: mbstring +if (! extension_loaded('mbstring')) { + die('PHP extension required: mbstring'); +} + +// Check if /data is writeable +if (! is_writable('data')) { + die('The directory "data" must be writeable by your web server user'); +} + +// Include password_compat for PHP < 5.5 +if (version_compare(PHP_VERSION, '5.5.0', '<')) { + require __DIR__.'/../vendor/password.php'; +} diff --git a/app/common.php b/app/common.php new file mode 100644 index 00000000..5a26860f --- /dev/null +++ b/app/common.php @@ -0,0 +1,98 @@ +<?php + +require __DIR__.'/Core/Loader.php'; +require __DIR__.'/helpers.php'; +require __DIR__.'/translator.php'; + +use Core\Event; +use Core\Loader; +use Core\Registry; + +// Include custom config file +if (file_exists('config.php')) { + require 'config.php'; +} + +// Board refresh frequency in seconds for the public board view +defined('BOARD_PUBLIC_CHECK_INTERVAL') or define('BOARD_PUBLIC_CHECK_INTERVAL', 60); + +// Board refresh frequency in seconds (the value 0 disable this feature) +defined('BOARD_CHECK_INTERVAL') or define('BOARD_CHECK_INTERVAL', 10); + +// Custom session save path +defined('SESSION_SAVE_PATH') or define('SESSION_SAVE_PATH', ''); + +// Application version +defined('APP_VERSION') or define('APP_VERSION', 'master'); + +// Base directory +define('BASE_URL_DIRECTORY', dirname($_SERVER['PHP_SELF'])); + +// Database driver: sqlite or mysql +defined('DB_DRIVER') or define('DB_DRIVER', 'sqlite'); + +// Sqlite configuration +defined('DB_FILENAME') or define('DB_FILENAME', 'data/db.sqlite'); + +// Mysql configuration +defined('DB_USERNAME') or define('DB_USERNAME', 'root'); +defined('DB_PASSWORD') or define('DB_PASSWORD', ''); +defined('DB_HOSTNAME') or define('DB_HOSTNAME', 'localhost'); +defined('DB_NAME') or define('DB_NAME', 'kanboard'); + +// LDAP configuration +defined('LDAP_AUTH') or define('LDAP_AUTH', false); +defined('LDAP_SERVER') or define('LDAP_SERVER', ''); +defined('LDAP_PORT') or define('LDAP_PORT', 389); +defined('LDAP_USER_DN') or define('LDAP_USER_DN', '%s'); + +// Google authentication +defined('GOOGLE_AUTH') or define('GOOGLE_AUTH', false); +defined('GOOGLE_CLIENT_ID') or define('GOOGLE_CLIENT_ID', ''); +defined('GOOGLE_CLIENT_SECRET') or define('GOOGLE_CLIENT_SECRET', ''); + +$loader = new Loader; +$loader->execute(); + +$registry = new Registry; + +$registry->db = function() use ($registry) { + require __DIR__.'/../vendor/PicoDb/Database.php'; + + if (DB_DRIVER === 'sqlite') { + + require __DIR__.'/Schema/Sqlite.php'; + + $db = new \PicoDb\Database(array( + 'driver' => 'sqlite', + 'filename' => DB_FILENAME + )); + } + elseif (DB_DRIVER === 'mysql') { + + require __DIR__.'/Schema/Mysql.php'; + + $db = new \PicoDb\Database(array( + 'driver' => 'mysql', + 'hostname' => DB_HOSTNAME, + 'username' => DB_USERNAME, + 'password' => DB_PASSWORD, + 'database' => DB_NAME, + 'charset' => 'utf8', + )); + } + else { + die('Database driver not supported'); + } + + if ($db->schema()->check(Schema\VERSION)) { + return $db; + } + else { + die('Unable to migrate database schema!'); + } +}; + +$registry->event = function() use ($registry) { + return new Event; +}; diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 00000000..8351328a --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,262 @@ +<?php + +namespace Helper; + +function template($name, array $args = array()) +{ + $tpl = new \Core\Template; + return $tpl->load($name, $args); +} + +function is_current_user($user_id) +{ + return $_SESSION['user']['id'] == $user_id; +} + +function is_admin() +{ + return $_SESSION['user']['is_admin'] == 1; +} + +function get_username() +{ + return $_SESSION['user']['username']; +} + +function parse($text) +{ + $text = markdown($text); + $text = preg_replace('!#(\d+)!i', '<a href="?controller=task&action=show&task_id=$1">$0</a>', $text); + return $text; +} + +function markdown($text) +{ + require_once __DIR__.'/../vendor/Michelf/MarkdownExtra.inc.php'; + + $parser = new \Michelf\MarkdownExtra; + $parser->no_markup = true; + $parser->no_entities = true; + + return $parser->transform($text); +} + +function get_current_base_url() +{ + $url = isset($_SERVER['HTTPS']) ? 'https://' : 'http://'; + $url .= $_SERVER['SERVER_NAME']; + $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; + $url .= dirname($_SERVER['PHP_SELF']) !== '/' ? dirname($_SERVER['PHP_SELF']).'/' : '/'; + + return $url; +} + +function escape($value) +{ + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); +} + +function flash($html) +{ + $data = ''; + + if (isset($_SESSION['flash_message'])) { + $data = sprintf($html, escape($_SESSION['flash_message'])); + unset($_SESSION['flash_message']); + } + + return $data; +} + +function flash_error($html) +{ + $data = ''; + + if (isset($_SESSION['flash_error_message'])) { + $data = sprintf($html, escape($_SESSION['flash_error_message'])); + unset($_SESSION['flash_error_message']); + } + + return $data; +} + +function format_bytes($size, $precision = 2) +{ + $base = log($size) / log(1024); + $suffixes = array('', 'k', 'M', 'G', 'T'); + + return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)]; +} + +function get_host_from_url($url) +{ + return escape(parse_url($url, PHP_URL_HOST)) ?: $url; +} + +function summary($value, $min_length = 5, $max_length = 120, $end = '[...]') +{ + $length = strlen($value); + + if ($length > $max_length) { + return substr($value, 0, strpos($value, ' ', $max_length)).' '.$end; + } + else if ($length < $min_length) { + return ''; + } + + return $value; +} + +function contains($haystack, $needle) +{ + return strpos($haystack, $needle) !== false; +} + +function in_list($id, array $listing, $default_value = '?') +{ + if (isset($listing[$id])) { + return escape($listing[$id]); + } + + return $default_value; +} + +function error_class(array $errors, $name) +{ + return ! isset($errors[$name]) ? '' : ' form-error'; +} + +function error_list(array $errors, $name) +{ + $html = ''; + + if (isset($errors[$name])) { + + $html .= '<ul class="form-errors">'; + + foreach ($errors[$name] as $error) { + $html .= '<li>'.escape($error).'</li>'; + } + + $html .= '</ul>'; + } + + return $html; +} + +function form_value($values, $name) +{ + if (isset($values->$name)) { + return 'value="'.escape($values->$name).'"'; + } + + return isset($values[$name]) ? 'value="'.escape($values[$name]).'"' : ''; +} + +function form_hidden($name, $values = array()) +{ + return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).'/>'; +} + +function form_default_select($name, array $options, $values = array(), array $errors = array(), $class = '') +{ + $options = array('' => '?') + $options; + return form_select($name, $options, $values, $errors, $class); +} + +function form_select($name, array $options, $values = array(), array $errors = array(), $class = '') +{ + $html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'">'; + + foreach ($options as $id => $value) { + + $html .= '<option value="'.escape($id).'"'; + + if (isset($values->$name) && $id == $values->$name) $html .= ' selected="selected"'; + if (isset($values[$name]) && $id == $values[$name]) $html .= ' selected="selected"'; + + $html .= '>'.escape($value).'</option>'; + } + + $html .= '</select>'; + $html .= error_list($errors, $name); + + return $html; +} + +function form_radios($name, array $options, array $values = array()) +{ + $html = ''; + + foreach ($options as $value => $label) { + $html .= form_radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value); + } + + return $html; +} + +function form_radio($name, $label, $value, $selected = false, $class = '') +{ + return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.escape($value).'" '.($selected ? 'selected="selected"' : '').'>'.escape($label).'</label>'; +} + +function form_checkbox($name, $label, $value, $checked = false, $class = '') +{ + return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.escape($value).'" '.($checked ? 'checked="checked"' : '').'> '.escape($label).'</label>'; +} + +function form_label($label, $name, array $attributes = array()) +{ + return '<label for="form-'.$name.'" '.implode(' ', $attributes).'>'.escape($label).'</label>'; +} + +function form_textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + $class .= error_class($errors, $name); + + $html = '<textarea name="'.$name.'" id="form-'.$name.'" class="'.$class.'" '; + $html .= implode(' ', $attributes).'>'; + $html .= isset($values->$name) ? escape($values->$name) : isset($values[$name]) ? $values[$name] : ''; + $html .= '</textarea>'; + if (in_array('required', $attributes)) $html .= '<span class="form-required">*</span>'; + $html .= error_list($errors, $name); + + return $html; +} + +function form_input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + $class .= error_class($errors, $name); + + $html = '<input type="'.$type.'" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).' class="'.$class.'" '; + $html .= implode(' ', $attributes).'/>'; + if (in_array('required', $attributes)) $html .= '<span class="form-required">*</span>'; + $html .= error_list($errors, $name); + + return $html; +} + +function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('text', $name, $values, $errors, $attributes, $class); +} + +function form_password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('password', $name, $values, $errors, $attributes, $class); +} + +function form_email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('email', $name, $values, $errors, $attributes, $class); +} + +function form_date($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('date', $name, $values, $errors, $attributes, $class); +} + +function form_number($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + return form_input('number', $name, $values, $errors, $attributes, $class); +} diff --git a/app/translator.php b/app/translator.php new file mode 100644 index 00000000..338821d3 --- /dev/null +++ b/app/translator.php @@ -0,0 +1,36 @@ +<?php + +use Core\Translator; + +// Get a translation +function t() +{ + $t = new Translator; + return call_user_func_array(array($t, 'translate'), func_get_args()); +} + +// Get a locale currency +function c($value) +{ + $t = new Translator; + return $t->currency($value); +} + +// Get a formatted number +function n($value) +{ + $t = new Translator; + return $t->number($value); +} + +// Get a locale date +function dt($format, $timestamp) +{ + $t = new Translator; + return $t->datetime($format, $timestamp); +} + +// Plurals, return $t2 if $value > 1 +function p($value, $t1, $t2) { + return $value > 1 ? $t2 : $t1; +} |