1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/kanboard_ynh.git synced 2024-09-03 19:36:17 +02:00

Fix update to kanboard v1.0.18 (some file were out of vcs)

This commit is contained in:
mbugeia 2015-09-13 12:36:05 +02:00
parent ad4eff4f91
commit 161eebeaab
642 changed files with 61370 additions and 0 deletions

9
sources/.htaccess Normal file
View file

@ -0,0 +1,9 @@
<IfModule mod_rewrite.c>
Options -MultiViews
SetEnv HTTP_MOD_REWRITE On
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]
</IfModule>

143
sources/ChangeLog Normal file
View file

@ -0,0 +1,143 @@
Version 1.0.18
--------------
New features:
* Include documentation in the application
* Add Gitlab authentication
* Add users and categories filters on the board
* Add hide/show columns
* Add Gantt chart for projects and tasks
* Add new role "Project Administrator"
* Add login bruteforce protection with captcha and account lockdown
* Add new api procedures: getDefaultTaskColor(), getDefaultTaskColors() and getColorList()
* Add user api access
* Add config parameter to define session duration
* Add config parameter to disable/enable RememberMe authentication
* Add start/end date for projects
* Add new automated action to change task color based on the task link
* Add milestone marker in board task
* Add search in task title when using an integer only input
* Add Portuguese (European) translation
* Add Norwegian translation
Improvements:
* Add handle to move tasks on touch devices
* Improve file attachments tooltip on the board
* Adjust automatically the height of the placeholder during drag and drop
* Show all tasks when using no search criteria
* Add column vertical scrolling
* Set dynamically column height based on viewport size
* Enable support for Github Enterprise when using Github Authentication
* Update iCalendar library to display organizer name
* Improve sidebar menus
* Add no referrer policy in meta tags
* Run automated unit tests with Sqlite/Mysql/Postgres on Travis-ci
* Add Makefile and remove the scripts directory
Bug fixes:
* Wrong template name for subtasks tooltip due to previous refactoring
* Fix broken url for closed tasks in project view
* Fix permission issue when changing the url manually
* Fix bug task estimate is reseted when using subtask timer
* Fix screenshot feature with Firefox 40
* Fix bug when uploading files with cyrilic characters
Version 1.0.17
--------------
New features:
* Added url rewrite and new routes
* Added new search engine with advanced syntax
* Added global search section
* Added search form on the dashboard
* Added new dashboard layout
* Added new layout for board/calendar/list views
* Added filters helper for search forms
* Added settings option to disable subtask timer
* Added settings option to include or exclude closed tasks into CFD
* Added settings option to define the default task color
* Added new config option to disable automatic creation of LDAP accounts
* Added loading icon on board view
* Prompt user when moving or duplicate a task to another project
* Added current values when moving/duplicate a task to another project and add a loading icon
* Added memory consumption in debug log
* Added form to create remote user
* Added edit form for user authentication
* Added config option to hide login form
* Display OAuth2 urls on integration page
* Added keyboard shortcuts to switch between board/calendar/list view
* Added keyboard shortcut to focus on the search box
* Added Slack channel override
* Added new report: Lead and cycle time for projects
* Added new report: Average time spent into each column
* Added task analytics
* Added icon to set automatically the start date
* Added datetime picker for start date
Improvements:
* Updated documentation
* Display user initials when tasks are in collapsed mode
* Show title in tooltip for collapsed tasks
* Improve alert box fadeout to avoid an empty space
* Set focus on the dropdown for category popover
* Make escape keyboard shorcut global
* Check the box remember me by default
* Store redirect login url in session instead of using url parameter
* Update Gitlab webhook
* Do not rewrite remember me cookie for each request
* Set the assignee as organizer for ical events
* Increase date range for ics export
* Reduce spacing on cards
* Move board collapse/expand mode to server side to avoid board flickering
* Use ajax requests for board collapse/expand
* Do not set anchor for the default swimlane on the link back to board
* Replace timeserie axis to category axis for charts
* Hide task age in compact mode
* Improve quick-add subtasks form
* Reduce the size of the filter box for smaller screen
* Added icon to hide/show sidebar
* Update GitLab logo
* Improve Dockerfile
Translations:
* Added Czech translation
* Updated Spanish translation
* Updated German Translation
Bug fixes:
* Screenshot dropdown: unexpected scroll down on the board view and focus lost when clicking on the drop zone
* No creator when duplicating a task
* Avoid the creation of multiple subtask timer for the same task and user
Code refactoring:
* Split task controller into smaller classes
* Remove method Category::getBoardCategories()
* Rewrite movePosition() to improve performances
* Refactoring of Github and Google authentication
Breaking changes:
* New OAuth url for Google and Github authentication
API:
* Add urls in api response for tasks and projects
Other:
* Added automated Docker build
* Remove edit recurrence from the task menu on the board
* Switch to MIT License instead of AGPLv3
Version 1.0.0 to 1.0.16
-----------------------
* See commit history and website news

View file

@ -0,0 +1,84 @@
<?php
namespace Action;
use Model\TaskLink;
/**
* Assign a color to a specific task link
*
* @package action
* @author Frederic Guillot
*/
class TaskAssignColorLink extends Base
{
/**
* Get the list of compatible events
*
* @access public
* @return array
*/
public function getCompatibleEvents()
{
return array(
TaskLink::EVENT_CREATE_UPDATE,
);
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'color_id' => t('Color'),
'link_id' => t('Link type'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'link_id',
);
}
/**
* Execute the action (change the task color)
*
* @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)
{
$values = array(
'id' => $data['task_id'],
'color_id' => $this->getParam('color_id'),
);
return $this->taskModification->update($values);
}
/**
* Check if the event data meet the action condition
*
* @access public
* @param array $data Event data dictionary
* @return bool
*/
public function hasRequiredCondition(array $data)
{
return $data['link_id'] == $this->getParam('link_id');
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Action;
use Model\Task;
/**
* Email a task to someone
*
* @package action
* @author Frederic Guillot
*/
class TaskEmail extends Base
{
/**
* Get the list of compatible events
*
* @access public
* @return array
*/
public function getCompatibleEvents()
{
return array(
Task::EVENT_MOVE_COLUMN,
Task::EVENT_CLOSE,
);
}
/**
* 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('User that will receive the email'),
'subject' => t('Email subject'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'column_id',
);
}
/**
* Execute the action (move the task to another column)
*
* @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)
{
$user = $this->user->getById($this->getParam('user_id'));
if (! empty($user['email'])) {
$task = $this->taskFinder->getDetails($data['task_id']);
$this->emailClient->send(
$user['email'],
$user['name'] ?: $user['username'],
$this->getParam('subject'),
$this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->config->get('application_url')))
);
return true;
}
return false;
}
/**
* Check if the event data meet the action condition
*
* @access public
* @param array $data Event data dictionary
* @return bool
*/
public function hasRequiredCondition(array $data)
{
return $data['column_id'] == $this->getParam('column_id');
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Action;
use Model\Task;
/**
* Move a task to another column when the category is changed
*
* @package action
* @author Francois Ferrand
*/
class TaskMoveColumnCategoryChange extends Base
{
/**
* Get the list of compatible events
*
* @access public
* @return array
*/
public function getCompatibleEvents()
{
return array(
Task::EVENT_UPDATE,
);
}
/**
* Get the required parameter for the action (defined by the user)
*
* @access public
* @return array
*/
public function getActionRequiredParameters()
{
return array(
'dest_column_id' => t('Destination column'),
'category_id' => t('Category'),
);
}
/**
* Get the required parameter for the event
*
* @access public
* @return string[]
*/
public function getEventRequiredParameters()
{
return array(
'task_id',
'column_id',
'project_id',
'category_id',
);
}
/**
* Execute the action (move the task to another column)
*
* @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)
{
$original_task = $this->taskFinder->getById($data['task_id']);
return $this->taskPosition->movePosition(
$data['project_id'],
$data['task_id'],
$this->getParam('dest_column_id'),
$original_task['position'],
$original_task['swimlane_id']
);
}
/**
* Check if the event data meet the action condition
*
* @access public
* @param array $data Event data dictionary
* @return bool
*/
public function hasRequiredCondition(array $data)
{
return $data['column_id'] != $this->getParam('dest_column_id') && $data['category_id'] == $this->getParam('category_id');
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Action;
use Model\Task;
/**
* Set the start date of task
*
* @package action
* @author Frederic Guillot
*/
class TaskUpdateStartDate extends Base
{
/**
* Get the list of compatible events
*
* @access public
* @return array
*/
public function getCompatibleEvents()
{
return array(
Task::EVENT_MOVE_COLUMN,
);
}
/**
* 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 (set the task color)
*
* @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)
{
$values = array(
'id' => $data['task_id'],
'date_started' => time(),
);
return $this->taskModification->update($values);
}
/**
* Check if the event data meet the action condition
*
* @access public
* @param array $data Event data dictionary
* @return bool
*/
public function hasRequiredCondition(array $data)
{
return $data['column_id'] == $this->getParam('column_id');
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Api;
/**
* Action API controller
*
* @package api
* @author Frederic Guillot
*/
class Action extends \Core\Base
{
public function getAvailableActions()
{
return $this->action->getAvailableActions();
}
public function getAvailableActionEvents()
{
return $this->action->getAvailableEvents();
}
public function getCompatibleActionEvents($action_name)
{
return $this->action->getCompatibleEvents($action_name);
}
public function removeAction($action_id)
{
return $this->action->remove($action_id);
}
public function getActions($project_id)
{
$actions = $this->action->getAllByProject($project_id);
foreach ($actions as $index => $action) {
$params = array();
foreach($action['params'] as $param) {
$params[$param['name']] = $param['value'];
}
$actions[$index]['params'] = $params;
}
return $actions;
}
public function createAction($project_id, $event_name, $action_name, $params)
{
$values = array(
'project_id' => $project_id,
'event_name' => $event_name,
'action_name' => $action_name,
'params' => $params,
);
list($valid,) = $this->action->validateCreation($values);
if (! $valid) {
return false;
}
// Check if the action exists
$actions = $this->action->getAvailableActions();
if (! isset($actions[$action_name])) {
return false;
}
// Check the event
$action = $this->action->load($action_name, $project_id, $event_name);
if (! in_array($event_name, $action->getCompatibleEvents())) {
return false;
}
$required_params = $action->getActionRequiredParameters();
// Check missing parameters
foreach($required_params as $param => $value) {
if (! isset($params[$param])) {
return false;
}
}
// Check extra parameters
foreach($params as $param => $value) {
if (! isset($required_params[$param])) {
return false;
}
}
return $this->action->create($values);
}
}

37
sources/app/Api/App.php Normal file
View file

@ -0,0 +1,37 @@
<?php
namespace Api;
/**
* App API controller
*
* @package api
* @author Frederic Guillot
*/
class App extends \Core\Base
{
public function getTimezone()
{
return $this->config->get('application_timezone');
}
public function getVersion()
{
return APP_VERSION;
}
public function getDefaultTaskColor()
{
return $this->color->getDefaultColor();
}
public function getDefaultTaskColors()
{
return $this->color->getDefaultColors();
}
public function getColorList()
{
return $this->color->getList();
}
}

40
sources/app/Api/Auth.php Normal file
View file

@ -0,0 +1,40 @@
<?php
namespace Api;
use JsonRPC\AuthenticationFailure;
use Symfony\Component\EventDispatcher\Event;
/**
* Base class
*
* @package api
* @author Frederic Guillot
*/
class Auth extends Base
{
/**
* Check api credentials
*
* @access public
* @param string $username
* @param string $password
* @param string $class
* @param string $method
*/
public function checkCredentials($username, $password, $class, $method)
{
$this->container['dispatcher']->dispatch('api.bootstrap', new Event);
if ($username !== 'jsonrpc' && ! $this->authentication->hasCaptcha($username) && $this->authentication->authenticate($username, $password)) {
$this->checkProcedurePermission(true, $method);
$this->userSession->refresh($this->user->getByUsername($username));
}
else if ($username === 'jsonrpc' && $password === $this->config->get('api_token')) {
$this->checkProcedurePermission(false, $method);
}
else {
throw new AuthenticationFailure('Wrong credentials');
}
}
}

113
sources/app/Api/Base.php Normal file
View file

@ -0,0 +1,113 @@
<?php
namespace Api;
use JsonRPC\AuthenticationFailure;
use JsonRPC\AccessDeniedException;
/**
* Base class
*
* @package api
* @author Frederic Guillot
*/
abstract class Base extends \Core\Base
{
private $user_allowed_procedures = array(
'getMe',
'getMyDashboard',
'getMyActivityStream',
'createMyPrivateProject',
'getMyProjectsList',
);
private $both_allowed_procedures = array(
'getTimezone',
'getVersion',
'getDefaultTaskColor',
'getDefaultTaskColors',
'getColorList',
'getProjectById',
'getTask',
'getTaskByReference',
'getAllTasks',
'openTask',
'closeTask',
'moveTaskPosition',
'createTask',
'updateTask',
'getBoard',
);
public function checkProcedurePermission($is_user, $procedure)
{
$is_both_procedure = in_array($procedure, $this->both_allowed_procedures);
$is_user_procedure = in_array($procedure, $this->user_allowed_procedures);
if ($is_user && ! $is_both_procedure && ! $is_user_procedure) {
throw new AccessDeniedException('Permission denied');
}
else if (! $is_user && ! $is_both_procedure && $is_user_procedure) {
throw new AccessDeniedException('Permission denied');
}
}
public function checkProjectPermission($project_id)
{
if ($this->userSession->isLogged() && ! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
throw new AccessDeniedException('Permission denied');
}
}
public function checkTaskPermission($task_id)
{
if ($this->userSession->isLogged()) {
$this->checkProjectPermission($this->taskFinder->getProjectId($task_id));
}
}
protected function formatTask($task)
{
if (! empty($task)) {
$task['url'] = $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), '', true);
$task['color'] = $this->color->getColorProperties($task['color_id']);
}
return $task;
}
protected function formatTasks($tasks)
{
if (! empty($tasks)) {
foreach ($tasks as &$task) {
$task = $this->formatTask($task);
}
}
return $tasks;
}
protected function formatProject($project)
{
if (! empty($project)) {
$project['url'] = array(
'board' => $this->helper->url->to('board', 'show', array('project_id' => $project['id']), '', true),
'calendar' => $this->helper->url->to('calendar', 'show', array('project_id' => $project['id']), '', true),
'list' => $this->helper->url->to('listing', 'show', array('project_id' => $project['id']), '', true),
);
}
return $project;
}
protected function formatProjects($projects)
{
if (! empty($projects)) {
foreach ($projects as &$project) {
$project = $this->formatProject($project);
}
}
return $projects;
}
}

53
sources/app/Api/Board.php Normal file
View file

@ -0,0 +1,53 @@
<?php
namespace Api;
/**
* Board API controller
*
* @package api
* @author Frederic Guillot
*/
class Board extends Base
{
public function getBoard($project_id)
{
$this->checkProjectPermission($project_id);
return $this->board->getBoard($project_id);
}
public function getColumns($project_id)
{
return $this->board->getColumns($project_id);
}
public function getColumn($column_id)
{
return $this->board->getColumn($column_id);
}
public function moveColumnUp($project_id, $column_id)
{
return $this->board->moveUp($project_id, $column_id);
}
public function moveColumnDown($project_id, $column_id)
{
return $this->board->moveDown($project_id, $column_id);
}
public function updateColumn($column_id, $title, $task_limit = 0, $description = '')
{
return $this->board->updateColumn($column_id, $title, $task_limit, $description);
}
public function addColumn($project_id, $title, $task_limit = 0, $description = '')
{
return $this->board->addColumn($project_id, $title, $task_limit, $description);
}
public function removeColumn($column_id)
{
return $this->board->removeColumn($column_id);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Api;
/**
* Category API controller
*
* @package api
* @author Frederic Guillot
*/
class Category extends \Core\Base
{
public function getCategory($category_id)
{
return $this->category->getById($category_id);
}
public function getAllCategories($project_id)
{
return $this->category->getAll($project_id);
}
public function removeCategory($category_id)
{
return $this->category->remove($category_id);
}
public function createCategory($project_id, $name)
{
$values = array(
'project_id' => $project_id,
'name' => $name,
);
list($valid,) = $this->category->validateCreation($values);
return $valid ? $this->category->create($values) : false;
}
public function updateCategory($id, $name)
{
$values = array(
'id' => $id,
'name' => $name,
);
list($valid,) = $this->category->validateModification($values);
return $valid && $this->category->update($values);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Api;
/**
* Comment API controller
*
* @package api
* @author Frederic Guillot
*/
class Comment extends \Core\Base
{
public function getComment($comment_id)
{
return $this->comment->getById($comment_id);
}
public function getAllComments($task_id)
{
return $this->comment->getAll($task_id);
}
public function removeComment($comment_id)
{
return $this->comment->remove($comment_id);
}
public function createComment($task_id, $user_id, $content)
{
$values = array(
'task_id' => $task_id,
'user_id' => $user_id,
'comment' => $content,
);
list($valid,) = $this->comment->validateCreation($values);
return $valid ? $this->comment->create($values) : false;
}
public function updateComment($id, $content)
{
$values = array(
'id' => $id,
'comment' => $content,
);
list($valid,) = $this->comment->validateModification($values);
return $valid && $this->comment->update($values);
}
}

53
sources/app/Api/File.php Normal file
View file

@ -0,0 +1,53 @@
<?php
namespace Api;
/**
* File API controller
*
* @package api
* @author Frederic Guillot
*/
class File extends \Core\Base
{
public function getFile($file_id)
{
return $this->file->getById($file_id);
}
public function getAllFiles($task_id)
{
return $this->file->getAll($task_id);
}
public function downloadFile($file_id)
{
$file = $this->file->getById($file_id);
if (! empty($file)) {
$filename = FILES_DIR.$file['path'];
if (file_exists($filename)) {
return base64_encode(file_get_contents($filename));
}
}
return '';
}
public function createFile($project_id, $task_id, $filename, $blob)
{
return $this->file->uploadContent($project_id, $task_id, $filename, $blob);
}
public function removeFile($file_id)
{
return $this->file->remove($file_id);
}
public function removeAllFiles($task_id)
{
return $this->file->removeAll($task_id);
}
}

111
sources/app/Api/Link.php Normal file
View file

@ -0,0 +1,111 @@
<?php
namespace Api;
/**
* Link API controller
*
* @package api
* @author Frederic Guillot
*/
class Link extends \Core\Base
{
/**
* Get a link by id
*
* @access public
* @param integer $link_id Link id
* @return array
*/
public function getLinkById($link_id)
{
return $this->link->getById($link_id);
}
/**
* Get a link by name
*
* @access public
* @param string $label
* @return array
*/
public function getLinkByLabel($label)
{
return $this->link->getByLabel($label);
}
/**
* Get the opposite link id
*
* @access public
* @param integer $link_id Link id
* @return integer
*/
public function getOppositeLinkId($link_id)
{
return $this->link->getOppositeLinkId($link_id);
}
/**
* Get all links
*
* @access public
* @return array
*/
public function getAllLinks()
{
return $this->link->getAll();
}
/**
* Create a new link label
*
* @access public
* @param string $label
* @param string $opposite_label
* @return boolean|integer
*/
public function createLink($label, $opposite_label = '')
{
$values = array(
'label' => $label,
'opposite_label' => $opposite_label,
);
list($valid,) = $this->link->validateCreation($values);
return $valid ? $this->link->create($label, $opposite_label) : false;
}
/**
* Update a link
*
* @access public
* @param integer $link_id
* @param integer $opposite_link_id
* @param string $label
* @return boolean
*/
public function updateLink($link_id, $opposite_link_id, $label)
{
$values = array(
'id' => $link_id,
'opposite_id' => $opposite_link_id,
'label' => $label,
);
list($valid,) = $this->link->validateModification($values);
return $valid && $this->link->update($values);
}
/**
* Remove a link a the relation to its opposite
*
* @access public
* @param integer $link_id
* @return boolean
*/
public function removeLink($link_id)
{
return $this->link->remove($link_id);
}
}

55
sources/app/Api/Me.php Normal file
View file

@ -0,0 +1,55 @@
<?php
namespace Api;
use Model\Subtask as SubtaskModel;
use Model\Task as TaskModel;
/**
* Me API controller
*
* @package api
* @author Frederic Guillot
*/
class Me extends Base
{
public function getMe()
{
return $this->session['user'];
}
public function getMyDashboard()
{
$user_id = $this->userSession->getId();
$projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id))->findAll();
$tasks = $this->taskFinder->getUserQuery($user_id)->findAll();
return array(
'projects' => $this->formatProjects($projects),
'tasks' => $this->formatTasks($tasks),
'subtasks' => $this->subtask->getUserQuery($user_id, array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))->findAll(),
);
}
public function getMyActivityStream()
{
return $this->projectActivity->getProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId()), 100);
}
public function createMyPrivateProject($name, $description = null)
{
$values = array(
'name' => $name,
'description' => $description,
'is_private' => 1,
);
list($valid,) = $this->project->validateCreation($values);
return $valid ? $this->project->create($values, $this->userSession->getId(), true) : false;
}
public function getMyProjectsList()
{
return $this->projectPermission->getMemberProjects($this->userSession->getId());
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace Api;
/**
* Project API controller
*
* @package api
* @author Frederic Guillot
*/
class Project extends Base
{
public function getProjectById($project_id)
{
$this->checkProjectPermission($project_id);
return $this->formatProject($this->project->getById($project_id));
}
public function getProjectByName($name)
{
return $this->formatProject($this->project->getByName($name));
}
public function getAllProjects()
{
return $this->formatProjects($this->project->getAll());
}
public function removeProject($project_id)
{
return $this->project->remove($project_id);
}
public function enableProject($project_id)
{
return $this->project->enable($project_id);
}
public function disableProject($project_id)
{
return $this->project->disable($project_id);
}
public function enableProjectPublicAccess($project_id)
{
return $this->project->enablePublicAccess($project_id);
}
public function disableProjectPublicAccess($project_id)
{
return $this->project->disablePublicAccess($project_id);
}
public function getProjectActivities(array $project_ids)
{
return $this->projectActivity->getProjects($project_ids);
}
public function getProjectActivity($project_id)
{
return $this->projectActivity->getProject($project_id);
}
public function createProject($name, $description = null)
{
$values = array(
'name' => $name,
'description' => $description
);
list($valid,) = $this->project->validateCreation($values);
return $valid ? $this->project->create($values) : false;
}
public function updateProject($id, $name, $description = null)
{
$values = array(
'id' => $id,
'name' => $name,
'description' => $description
);
list($valid,) = $this->project->validateModification($values);
return $valid && $this->project->update($values);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Api;
/**
* ProjectPermission API controller
*
* @package api
* @author Frederic Guillot
*/
class ProjectPermission extends \Core\Base
{
public function getMembers($project_id)
{
return $this->projectPermission->getMembers($project_id);
}
public function revokeUser($project_id, $user_id)
{
return $this->projectPermission->revokeMember($project_id, $user_id);
}
public function allowUser($project_id, $user_id)
{
return $this->projectPermission->addMember($project_id, $user_id);
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Api;
/**
* Subtask API controller
*
* @package api
* @author Frederic Guillot
*/
class Subtask extends \Core\Base
{
public function getSubtask($subtask_id)
{
return $this->subtask->getById($subtask_id);
}
public function getAllSubtasks($task_id)
{
return $this->subtask->getAll($task_id);
}
public function removeSubtask($subtask_id)
{
return $this->subtask->remove($subtask_id);
}
public function createSubtask($task_id, $title, $user_id = 0, $time_estimated = 0, $time_spent = 0, $status = 0)
{
$values = array(
'title' => $title,
'task_id' => $task_id,
'user_id' => $user_id,
'time_estimated' => $time_estimated,
'time_spent' => $time_spent,
'status' => $status,
);
list($valid,) = $this->subtask->validateCreation($values);
return $valid ? $this->subtask->create($values) : false;
}
public function updateSubtask($id, $task_id, $title = null, $user_id = null, $time_estimated = null, $time_spent = null, $status = null)
{
$values = array(
'id' => $id,
'task_id' => $task_id,
'title' => $title,
'user_id' => $user_id,
'time_estimated' => $time_estimated,
'time_spent' => $time_spent,
'status' => $status,
);
foreach ($values as $key => $value) {
if (is_null($value)) {
unset($values[$key]);
}
}
list($valid,) = $this->subtask->validateApiModification($values);
return $valid && $this->subtask->update($values);
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Api;
/**
* Swimlane API controller
*
* @package api
* @author Frederic Guillot
*/
class Swimlane extends \Core\Base
{
public function getActiveSwimlanes($project_id)
{
return $this->swimlane->getSwimlanes($project_id);
}
public function getAllSwimlanes($project_id)
{
return $this->swimlane->getAll($project_id);
}
public function getSwimlaneById($swimlane_id)
{
return $this->swimlane->getById($swimlane_id);
}
public function getSwimlaneByName($project_id, $name)
{
return $this->swimlane->getByName($project_id, $name);
}
public function getSwimlane($swimlane_id)
{
return $this->swimlane->getById($swimlane_id);
}
public function getDefaultSwimlane($project_id)
{
return $this->swimlane->getDefault($project_id);
}
public function addSwimlane($project_id, $name)
{
return $this->swimlane->create($project_id, $name);
}
public function updateSwimlane($swimlane_id, $name)
{
return $this->swimlane->rename($swimlane_id, $name);
}
public function removeSwimlane($project_id, $swimlane_id)
{
return $this->swimlane->remove($project_id, $swimlane_id);
}
public function disableSwimlane($project_id, $swimlane_id)
{
return $this->swimlane->disable($project_id, $swimlane_id);
}
public function enableSwimlane($project_id, $swimlane_id)
{
return $this->swimlane->enable($project_id, $swimlane_id);
}
public function moveSwimlaneUp($project_id, $swimlane_id)
{
return $this->swimlane->moveUp($project_id, $swimlane_id);
}
public function moveSwimlaneDown($project_id, $swimlane_id)
{
return $this->swimlane->moveDown($project_id, $swimlane_id);
}
}

128
sources/app/Api/Task.php Normal file
View file

@ -0,0 +1,128 @@
<?php
namespace Api;
use Model\Task as TaskModel;
/**
* Task API controller
*
* @package api
* @author Frederic Guillot
*/
class Task extends Base
{
public function getTask($task_id)
{
$this->checkTaskPermission($task_id);
return $this->formatTask($this->taskFinder->getById($task_id));
}
public function getTaskByReference($project_id, $reference)
{
$this->checkProjectPermission($project_id);
return $this->formatTask($this->taskFinder->getByReference($project_id, $reference));
}
public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN)
{
$this->checkProjectPermission($project_id);
return $this->formatTasks($this->taskFinder->getAll($project_id, $status_id));
}
public function getOverdueTasks()
{
return $this->taskFinder->getOverdueTasks();
}
public function openTask($task_id)
{
$this->checkTaskPermission($task_id);
return $this->taskStatus->open($task_id);
}
public function closeTask($task_id)
{
$this->checkTaskPermission($task_id);
return $this->taskStatus->close($task_id);
}
public function removeTask($task_id)
{
return $this->task->remove($task_id);
}
public function moveTaskPosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0)
{
$this->checkProjectPermission($project_id);
return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id);
}
public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0,
$date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0,
$recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0,
$recurrence_basedate = 0, $reference = '')
{
$this->checkProjectPermission($project_id);
$values = array(
'title' => $title,
'project_id' => $project_id,
'color_id' => $color_id,
'column_id' => $column_id,
'owner_id' => $owner_id,
'creator_id' => $creator_id,
'date_due' => $date_due,
'description' => $description,
'category_id' => $category_id,
'score' => $score,
'swimlane_id' => $swimlane_id,
'recurrence_status' => $recurrence_status,
'recurrence_trigger' => $recurrence_trigger,
'recurrence_factor' => $recurrence_factor,
'recurrence_timeframe' => $recurrence_timeframe,
'recurrence_basedate' => $recurrence_basedate,
'reference' => $reference,
);
list($valid,) = $this->taskValidator->validateCreation($values);
return $valid ? $this->taskCreation->create($values) : false;
}
public function updateTask($id, $title = null, $project_id = null, $color_id = null, $owner_id = null,
$creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null,
$recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null,
$recurrence_timeframe = null, $recurrence_basedate = null, $reference = null)
{
$this->checkTaskPermission($id);
$values = array(
'id' => $id,
'title' => $title,
'project_id' => $project_id,
'color_id' => $color_id,
'owner_id' => $owner_id,
'creator_id' => $creator_id,
'date_due' => $date_due,
'description' => $description,
'category_id' => $category_id,
'score' => $score,
'recurrence_status' => $recurrence_status,
'recurrence_trigger' => $recurrence_trigger,
'recurrence_factor' => $recurrence_factor,
'recurrence_timeframe' => $recurrence_timeframe,
'recurrence_basedate' => $recurrence_basedate,
'reference' => $reference,
);
foreach ($values as $key => $value) {
if (is_null($value)) {
unset($values[$key]);
}
}
list($valid) = $this->taskValidator->validateApiModification($values);
return $valid && $this->taskModification->update($values);
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Api;
/**
* TaskLink API controller
*
* @package api
* @author Frederic Guillot
*/
class TaskLink extends \Core\Base
{
/**
* Get a task link
*
* @access public
* @param integer $task_link_id Task link id
* @return array
*/
public function getTaskLinkById($task_link_id)
{
return $this->taskLink->getById($task_link_id);
}
/**
* Get all links attached to a task
*
* @access public
* @param integer $task_id Task id
* @return array
*/
public function getAllTaskLinks($task_id)
{
return $this->taskLink->getAll($task_id);
}
/**
* Create a new link
*
* @access public
* @param integer $task_id Task id
* @param integer $opposite_task_id Opposite task id
* @param integer $link_id Link id
* @return integer Task link id
*/
public function createTaskLink($task_id, $opposite_task_id, $link_id)
{
return $this->taskLink->create($task_id, $opposite_task_id, $link_id);
}
/**
* Update a task link
*
* @access public
* @param integer $task_link_id Task link id
* @param integer $task_id Task id
* @param integer $opposite_task_id Opposite task id
* @param integer $link_id Link id
* @return boolean
*/
public function updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id)
{
return $this->taskLink->update($task_link_id, $task_id, $opposite_task_id, $link_id);
}
/**
* Remove a link between two tasks
*
* @access public
* @param integer $task_link_id
* @return boolean
*/
public function removeTaskLink($task_link_id)
{
return $this->taskLink->remove($task_link_id);
}
}

87
sources/app/Api/User.php Normal file
View file

@ -0,0 +1,87 @@
<?php
namespace Api;
use Auth\Ldap;
/**
* User API controller
*
* @package api
* @author Frederic Guillot
*/
class User extends \Core\Base
{
public function getUser($user_id)
{
return $this->user->getById($user_id);
}
public function getAllUsers()
{
return $this->user->getAll();
}
public function removeUser($user_id)
{
return $this->user->remove($user_id);
}
public function createUser($username, $password, $name = '', $email = '', $is_admin = 0, $is_project_admin = 0)
{
$values = array(
'username' => $username,
'password' => $password,
'confirmation' => $password,
'name' => $name,
'email' => $email,
'is_admin' => $is_admin,
'is_project_admin' => $is_project_admin,
);
list($valid,) = $this->user->validateCreation($values);
return $valid ? $this->user->create($values) : false;
}
public function createLdapUser($username = '', $email = '', $is_admin = 0, $is_project_admin = 0)
{
$ldap = new Ldap($this->container);
$user = $ldap->lookup($username, $email);
if (! $user) {
return false;
}
$values = array(
'username' => $user['username'],
'name' => $user['name'],
'email' => $user['email'],
'is_ldap_user' => 1,
'is_admin' => $is_admin,
'is_project_admin' => $is_project_admin,
);
return $this->user->create($values);
}
public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $is_project_admin = null)
{
$values = array(
'id' => $id,
'username' => $username,
'name' => $name,
'email' => $email,
'is_admin' => $is_admin,
'is_project_admin' => $is_project_admin,
);
foreach ($values as $key => $value) {
if (is_null($value)) {
unset($values[$key]);
}
}
list($valid,) = $this->user->validateApiModification($values);
return $valid && $this->user->update($values);
}
}

122
sources/app/Auth/Github.php Normal file
View file

@ -0,0 +1,122 @@
<?php
namespace Auth;
use Event\AuthEvent;
/**
* Github backend
*
* @package auth
*/
class Github extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Github';
/**
* OAuth2 instance
*
* @access private
* @var \Core\OAuth2
*/
private $service;
/**
* Authenticate a Github user
*
* @access public
* @param string $github_id Github user id
* @return boolean
*/
public function authenticate($github_id)
{
$user = $this->user->getByGithubId($github_id);
if (! empty($user)) {
$this->userSession->refresh($user);
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
return false;
}
/**
* Unlink a Github account for a given user
*
* @access public
* @param integer $user_id User id
* @return boolean
*/
public function unlink($user_id)
{
return $this->user->update(array(
'id' => $user_id,
'github_id' => '',
));
}
/**
* Update the user table based on the Github profile information
*
* @access public
* @param integer $user_id User id
* @param array $profile Github profile
* @return boolean
*/
public function updateUser($user_id, array $profile)
{
$user = $this->user->getById($user_id);
return $this->user->update(array(
'id' => $user_id,
'github_id' => $profile['id'],
'email' => empty($user['email']) ? $profile['email'] : $user['email'],
'name' => empty($user['name']) ? $profile['name'] : $user['name'],
));
}
/**
* Get OAuth2 configured service
*
* @access public
* @return \Core\OAuth2
*/
public function getService()
{
if (empty($this->service)) {
$this->service = $this->oauth->createService(
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
$this->helper->url->to('oauth', 'github', array(), '', true),
GITHUB_OAUTH_AUTHORIZE_URL,
GITHUB_OAUTH_TOKEN_URL,
array()
);
}
return $this->service;
}
/**
* Get Github profile
*
* @access public
* @param string $code
* @return array
*/
public function getProfile($code)
{
$this->getService()->getAccessToken($code);
return $this->httpClient->getJson(
GITHUB_API_URL.'user',
array($this->getService()->getAuthorizationHeader())
);
}
}

122
sources/app/Auth/Gitlab.php Normal file
View file

@ -0,0 +1,122 @@
<?php
namespace Auth;
use Event\AuthEvent;
/**
* Gitlab backend
*
* @package auth
*/
class Gitlab extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Gitlab';
/**
* OAuth2 instance
*
* @access private
* @var \Core\OAuth2
*/
private $service;
/**
* Authenticate a Gitlab user
*
* @access public
* @param string $gitlab_id Gitlab user id
* @return boolean
*/
public function authenticate($gitlab_id)
{
$user = $this->user->getByGitlabId($gitlab_id);
if (! empty($user)) {
$this->userSession->refresh($user);
$this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
return false;
}
/**
* Unlink a Gitlab account for a given user
*
* @access public
* @param integer $user_id User id
* @return boolean
*/
public function unlink($user_id)
{
return $this->user->update(array(
'id' => $user_id,
'gitlab_id' => '',
));
}
/**
* Update the user table based on the Gitlab profile information
*
* @access public
* @param integer $user_id User id
* @param array $profile Gitlab profile
* @return boolean
*/
public function updateUser($user_id, array $profile)
{
$user = $this->user->getById($user_id);
return $this->user->update(array(
'id' => $user_id,
'gitlab_id' => $profile['id'],
'email' => empty($user['email']) ? $profile['email'] : $user['email'],
'name' => empty($user['name']) ? $profile['name'] : $user['name'],
));
}
/**
* Get OAuth2 configured service
*
* @access public
* @return \Core\OAuth2
*/
public function getService()
{
if (empty($this->service)) {
$this->service = $this->oauth->createService(
GITLAB_CLIENT_ID,
GITLAB_CLIENT_SECRET,
$this->helper->url->to('oauth', 'gitlab', array(), '', true),
GITLAB_OAUTH_AUTHORIZE_URL,
GITLAB_OAUTH_TOKEN_URL,
array()
);
}
return $this->service;
}
/**
* Get Gitlab profile
*
* @access public
* @param string $code
* @return array
*/
public function getProfile($code)
{
$this->getService()->getAccessToken($code);
return $this->httpClient->getJson(
GITLAB_API_URL.'user',
array($this->getService()->getAuthorizationHeader())
);
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace Console;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LocaleComparator extends Base
{
const REF_LOCALE = 'fr_FR';
protected function configure()
{
$this
->setName('locale:compare')
->setDescription('Compare application translations with the '.self::REF_LOCALE.' locale');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$strings = array();
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('app'));
$it->rewind();
while ($it->valid()) {
if (! $it->isDot() && substr($it->key(), -4) === '.php') {
$strings = array_merge($strings, $this->search($it->key()));
}
$it->next();
}
$this->compare(array_unique($strings));
}
public function show(array $strings)
{
foreach ($strings as $string) {
echo " '".str_replace("'", "\'", $string)."' => '',".PHP_EOL;
}
}
public function compare(array $strings)
{
$reference_file = 'app/Locale/'.self::REF_LOCALE.'/translations.php';
$reference = include $reference_file;
echo str_repeat('#', 70).PHP_EOL;
echo 'MISSING STRINGS'.PHP_EOL;
echo str_repeat('#', 70).PHP_EOL;
$this->show(array_diff($strings, array_keys($reference)));
echo str_repeat('#', 70).PHP_EOL;
echo 'USELESS STRINGS'.PHP_EOL;
echo str_repeat('#', 70).PHP_EOL;
$this->show(array_diff(array_keys($reference), $strings));
}
public function search($filename)
{
$content = file_get_contents($filename);
$strings = array();
if (preg_match_all('/\b[et]\((\'\K.*?\') *[\)\,]/', $content, $matches) && isset($matches[1])) {
$strings = $matches[1];
}
if (preg_match_all('/\bdt\((\'\K.*?\') *[\)\,]/', $content, $matches) && isset($matches[1])) {
$strings = array_merge($strings, $matches[1]);
}
array_walk($strings, function(&$value) {
$value = trim($value, "'");
$value = str_replace("\'", "'", $value);
});
return $strings;
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Console;
use DirectoryIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LocaleSync extends Base
{
const REF_LOCALE = 'fr_FR';
protected function configure()
{
$this
->setName('locale:sync')
->setDescription('Synchronize all translations based on the '.self::REF_LOCALE.' locale');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$reference_file = 'app/Locale/'.self::REF_LOCALE.'/translations.php';
$reference = include $reference_file;
foreach (new DirectoryIterator('app/Locale') as $fileInfo) {
if (! $fileInfo->isDot() && $fileInfo->isDir() && $fileInfo->getFilename() !== self::REF_LOCALE) {
$filename = 'app/Locale/'.$fileInfo->getFilename().'/translations.php';
echo $fileInfo->getFilename().' ('.$filename.')'.PHP_EOL;
file_put_contents($filename, $this->updateFile($reference, $filename));
}
}
}
public function updateFile(array $reference, $outdated_file)
{
$outdated = include $outdated_file;
$output = '<?php'.PHP_EOL.PHP_EOL;
$output .= 'return array('.PHP_EOL;
foreach ($reference as $key => $value) {
if (! empty($outdated[$key])) {
$output .= " '".str_replace("'", "\'", $key)."' => '".str_replace("'", "\'", $outdated[$key])."',\n";
}
else {
$output .= " // '".str_replace("'", "\'", $key)."' => '',\n";
}
}
$output .= ");\n";
return $output;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Console;
use Core\Tool;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ProjectDailyColumnStatsExport extends Base
{
protected function configure()
{
$this
->setName('export:daily-project-column-stats')
->setDescription('Daily project column stats CSV export (number of tasks per column and per day)')
->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$data = $this->projectDailyColumnStats->getAggregatedMetrics(
$input->getArgument('project_id'),
$input->getArgument('start_date'),
$input->getArgument('end_date')
);
if (is_array($data)) {
Tool::csv($data);
}
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Console;
use Model\Project;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ProjectDailyStatsCalculation extends Base
{
protected function configure()
{
$this
->setName('projects:daily-stats')
->setDescription('Calculate daily statistics for all projects');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$projects = $this->project->getAllByStatus(Project::ACTIVE);
foreach ($projects as $project) {
$output->writeln('Run calculation for '.$project['name']);
$this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d'));
$this->projectDailyStats->updateTotals($project['id'], date('Y-m-d'));
}
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace Controller;
/**
* Activity stream
*
* @package controller
* @author Frederic Guillot
*/
class Activity extends Base
{
/**
* Activity page for a project
*
* @access public
*/
public function project()
{
$project = $this->getProject();
$this->response->html($this->template->layout('activity/project', array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'events' => $this->projectActivity->getProject($project['id']),
'project' => $project,
'title' => t('%s\'s activity', $project['name'])
)));
}
/**
* Display task activities
*
* @access public
*/
public function task()
{
$task = $this->getTask();
$this->response->html($this->taskLayout('activity/task', array(
'title' => $task['title'],
'task' => $task,
'events' => $this->projectActivity->getTask($task['id']),
)));
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Controller;
use Gregwar\Captcha\CaptchaBuilder;
/**
* Authentication controller
*
* @package controller
* @author Frederic Guillot
*/
class Auth extends Base
{
/**
* Display the form login
*
* @access public
*/
public function login(array $values = array(), array $errors = array())
{
if ($this->userSession->isLogged()) {
$this->response->redirect($this->helper->url->to('app', 'index'));
}
$this->response->html($this->template->layout('auth/index', array(
'captcha' => isset($values['username']) && $this->authentication->hasCaptcha($values['username']),
'errors' => $errors,
'values' => $values,
'no_layout' => true,
'title' => t('Login')
)));
}
/**
* Check credentials
*
* @access public
*/
public function check()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->authentication->validateForm($values);
if ($valid) {
if (! empty($this->session['login_redirect']) && ! filter_var($this->session['login_redirect'], FILTER_VALIDATE_URL)) {
$redirect = $this->session['login_redirect'];
unset($this->session['login_redirect']);
$this->response->redirect($redirect);
}
$this->response->redirect($this->helper->url->to('app', 'index'));
}
$this->login($values, $errors);
}
/**
* Logout and destroy session
*
* @access public
*/
public function logout()
{
$this->authentication->backend('rememberMe')->destroy($this->userSession->getId());
$this->session->close();
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
/**
* Display captcha image
*
* @access public
*/
public function captcha()
{
$this->response->contentType('image/jpeg');
$builder = new CaptchaBuilder;
$builder->build();
$this->session['captcha'] = $builder->getPhrase();
$builder->output();
}
}

View file

@ -0,0 +1,170 @@
<?php
namespace Controller;
/**
* Column controller
*
* @package controller
* @author Frederic Guillot
*/
class Column extends Base
{
/**
* Display columns list
*
* @access public
*/
public function index(array $values = array(), array $errors = array())
{
$project = $this->getProject();
$columns = $this->board->getColumns($project['id']);
foreach ($columns as $column) {
$values['title['.$column['id'].']'] = $column['title'];
$values['description['.$column['id'].']'] = $column['description'];
$values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null;
}
$this->response->html($this->projectLayout('column/index', array(
'errors' => $errors,
'values' => $values + array('project_id' => $project['id']),
'columns' => $columns,
'project' => $project,
'title' => t('Edit board')
)));
}
/**
* Validate and add a new column
*
* @access public
*/
public function create()
{
$project = $this->getProject();
$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->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) {
$this->session->flash(t('Board updated successfully.'));
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
}
else {
$this->session->flashError(t('Unable to update this board.'));
}
}
$this->index($values, $errors);
}
/**
* Display a form to edit a column
*
* @access public
*/
public function edit(array $values = array(), array $errors = array())
{
$project = $this->getProject();
$column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
$this->response->html($this->projectLayout('column/edit', array(
'errors' => $errors,
'values' => $values ?: $column,
'project' => $project,
'column' => $column,
'title' => t('Edit column "%s"', $column['title'])
)));
}
/**
* Validate and update a column
*
* @access public
*/
public function update()
{
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->board->validateModification($values);
if ($valid) {
if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) {
$this->session->flash(t('Board updated successfully.'));
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
}
else {
$this->session->flashError(t('Unable to update this board.'));
}
}
$this->edit($values, $errors);
}
/**
* Move a column up or down
*
* @access public
*/
public function move()
{
$this->checkCSRFParam();
$project = $this->getProject();
$column_id = $this->request->getIntegerParam('column_id');
$direction = $this->request->getStringParam('direction');
if ($direction === 'up' || $direction === 'down') {
$this->board->{'move'.$direction}($project['id'], $column_id);
}
$this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
}
/**
* Confirm column suppression
*
* @access public
*/
public function confirm()
{
$project = $this->getProject();
$this->response->html($this->projectLayout('column/remove', array(
'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')),
'project' => $project,
'title' => t('Remove a column from a board')
)));
}
/**
* Remove a column
*
* @access public
*/
public function remove()
{
$project = $this->getProject();
$this->checkCSRFParam();
$column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
if (! empty($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($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Controller;
use Parsedown;
/**
* Documentation controller
*
* @package controller
* @author Frederic Guillot
*/
class Doc extends Base
{
private function readFile($filename)
{
$url = $this->helper->url;
$data = file_get_contents($filename);
list($title,, $content) = explode("\n", $data, 3);
$replaceUrl = function (array $matches) use ($url) {
return '('.$url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')';
};
$content = preg_replace_callback('/\((.*.markdown)\)/', $replaceUrl, $data);
return array(
'content' => Parsedown::instance()->text($content),
'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title,
);
}
public function show()
{
$filename = $this->request->getStringParam('file', 'index');
if (! preg_match('/^[a-z0-9\-]+/', $filename)) {
$filename = 'index';
}
$filename = __DIR__.'/../../doc/'.$filename.'.markdown';
if (! file_exists($filename)) {
$filename = __DIR__.'/../../doc/index.markdown';
}
$this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array(
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
)));
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Controller;
/**
* Atom/RSS Feed controller
*
* @package controller
* @author Frederic Guillot
*/
class Feed extends Base
{
/**
* RSS feed for a user
*
* @access public
*/
public function user()
{
$token = $this->request->getStringParam('token');
$user = $this->user->getByToken($token);
// Token verification
if (empty($user)) {
$this->forbidden(true);
}
$projects = $this->projectPermission->getActiveMemberProjects($user['id']);
$this->response->xml($this->template->render('feed/user', array(
'events' => $this->projectActivity->getProjects(array_keys($projects)),
'user' => $user,
)));
}
/**
* RSS feed for a project
*
* @access public
*/
public function project()
{
$token = $this->request->getStringParam('token');
$project = $this->project->getByToken($token);
// Token verification
if (empty($project)) {
$this->forbidden(true);
}
$this->response->xml($this->template->render('feed/project', array(
'events' => $this->projectActivity->getProject($project['id']),
'project' => $project,
)));
}
}

View file

@ -0,0 +1,151 @@
<?php
namespace Controller;
use Model\Task as TaskModel;
/**
* Gantt controller
*
* @package controller
* @author Frederic Guillot
*/
class Gantt extends Base
{
/**
* Show Gantt chart for all projects
*/
public function projects()
{
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
}
else {
$project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
}
$this->response->html($this->template->layout('gantt/projects', array(
'projects' => $this->project->getGanttBars($project_ids),
'title' => t('Gantt chart for all projects'),
'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
)));
}
/**
* Save new project start date and end date
*/
public function saveProjectDate()
{
$values = $this->request->getJson();
$result = $this->project->update(array(
'id' => $values['id'],
'start_date' => $this->dateParser->getIsoDate(strtotime($values['start'])),
'end_date' => $this->dateParser->getIsoDate(strtotime($values['end'])),
));
if (! $result) {
$this->response->json(array('message' => 'Unable to save project'), 400);
}
$this->response->json(array('message' => 'OK'), 201);
}
/**
* Show Gantt chart for one project
*/
public function project()
{
$params = $this->getProjectFilters('gantt', 'project');
$filter = $this->taskFilter->search($params['filters']['search'])->filterByProject($params['project']['id']);
$sorting = $this->request->getStringParam('sorting', 'board');
if ($sorting === 'date') {
$filter->getQuery()->asc(TaskModel::TABLE.'.date_started')->asc(TaskModel::TABLE.'.date_creation');
}
else {
$filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position');
}
$this->response->html($this->template->layout('gantt/project', $params + array(
'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false),
'sorting' => $sorting,
'tasks' => $filter->toGanttBars(),
)));
}
/**
* Save new task start date and due date
*/
public function saveTaskDate()
{
$this->getProject();
$values = $this->request->getJson();
$result = $this->taskModification->update(array(
'id' => $values['id'],
'date_started' => strtotime($values['start']),
'date_due' => strtotime($values['end']),
));
if (! $result) {
$this->response->json(array('message' => 'Unable to save task'), 400);
}
$this->response->json(array('message' => 'OK'), 201);
}
/**
* Simplified form to create a new task
*
* @access public
*/
public function task(array $values = array(), array $errors = array())
{
$project = $this->getProject();
$this->response->html($this->template->render('gantt/task_creation', array(
'errors' => $errors,
'values' => $values + array(
'project_id' => $project['id'],
'column_id' => $this->board->getFirstColumn($project['id']),
'position' => 1
),
'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'swimlanes_list' => $this->swimlane->getList($project['id'], false, true),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'title' => $project['name'].' &gt; '.t('New task')
)));
}
/**
* Validate and save a new task
*
* @access public
*/
public function saveTask()
{
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->taskValidator->validateCreation($values);
if ($valid) {
$task_id = $this->taskCreation->create($values);
if ($task_id !== false) {
$this->session->flash(t('Task created successfully.'));
$this->response->redirect($this->helper->url->to('gantt', 'project', array('project_id' => $project['id'])));
}
else {
$this->session->flashError(t('Unable to create your task.'));
}
}
$this->task($values, $errors);
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Controller;
use Model\TaskFilter;
use Eluceo\iCal\Component\Calendar as iCalendar;
/**
* iCalendar controller
*
* @package controller
* @author Frederic Guillot
*/
class Ical extends Base
{
/**
* Get user iCalendar
*
* @access public
*/
public function user()
{
$token = $this->request->getStringParam('token');
$user = $this->user->getByToken($token);
// Token verification
if (empty($user)) {
$this->forbidden(true);
}
// Common filter
$filter = $this->taskFilter
->create()
->filterByOwner($user['id']);
// Calendar properties
$calendar = new iCalendar('Kanboard');
$calendar->setName($user['name'] ?: $user['username']);
$calendar->setDescription($user['name'] ?: $user['username']);
$calendar->setPublishedTTL('PT1H');
$this->renderCalendar($filter, $calendar);
}
/**
* Get project iCalendar
*
* @access public
*/
public function project()
{
$token = $this->request->getStringParam('token');
$project = $this->project->getByToken($token);
// Token verification
if (empty($project)) {
$this->forbidden(true);
}
// Common filter
$filter = $this->taskFilter
->create()
->filterByProject($project['id']);
// Calendar properties
$calendar = new iCalendar('Kanboard');
$calendar->setName($project['name']);
$calendar->setDescription($project['name']);
$calendar->setPublishedTTL('PT1H');
$this->renderCalendar($filter, $calendar);
}
/**
* Common method to render iCal events
*
* @access private
*/
private function renderCalendar(TaskFilter $filter, iCalendar $calendar)
{
$start = $this->request->getStringParam('start', strtotime('-2 month'));
$end = $this->request->getStringParam('end', strtotime('+6 months'));
// Tasks
if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
$filter->copy()->filterByCreationDateRange($start, $end)->addDateTimeIcalEvents('date_creation', 'date_completed', $calendar);
}
else {
$filter->copy()->filterByStartDateRange($start, $end)->addDateTimeIcalEvents('date_started', 'date_completed', $calendar);
}
// Tasks with due date
$filter->copy()->filterByDueDateRange($start, $end)->addAllDayIcalEvents('date_due', $calendar);
$this->response->contentType('text/calendar; charset=utf-8');
echo $calendar->render();
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Controller;
use Model\Task as TaskModel;
/**
* List view controller
*
* @package controller
* @author Frederic Guillot
*/
class Listing extends Base
{
/**
* Show list view for projects
*
* @access public
*/
public function show()
{
$params = $this->getProjectFilters('listing', 'show');
$query = $this->taskFilter->search($params['filters']['search'])->filterByProject($params['project']['id'])->getQuery();
$paginator = $this->paginator
->setUrl('listing', 'show', array('project_id' => $params['project']['id']))
->setMax(30)
->setOrder(TaskModel::TABLE.'.id')
->setDirection('DESC')
->setQuery($query)
->calculate();
$this->response->html($this->template->layout('listing/show', $params + array(
'paginator' => $paginator,
)));
}
}

View file

@ -0,0 +1,133 @@
<?php
namespace Controller;
/**
* OAuth controller
*
* @package controller
* @author Frederic Guillot
*/
class Oauth extends Base
{
/**
* Link or authenticate a Google account
*
* @access public
*/
public function google()
{
$this->step1('google');
}
/**
* Link or authenticate a Github account
*
* @access public
*/
public function github()
{
$this->step1('github');
}
/**
* Link or authenticate a Gitlab account
*
* @access public
*/
public function gitlab()
{
$this->step1('gitlab');
}
/**
* Unlink external account
*
* @access public
*/
public function unlink($backend = '')
{
$backend = $this->request->getStringParam('backend', $backend);
$this->checkCSRFParam();
if ($this->authentication->backend($backend)->unlink($this->userSession->getId())) {
$this->session->flash(t('Your external account is not linked anymore to your profile.'));
}
else {
$this->session->flashError(t('Unable to unlink your external account.'));
}
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
}
/**
* Redirect to the provider if no code received
*
* @access private
*/
private function step1($backend)
{
$code = $this->request->getStringParam('code');
if (! empty($code)) {
$this->step2($backend, $code);
}
else {
$this->response->redirect($this->authentication->backend($backend)->getService()->getAuthorizationUrl());
}
}
/**
* Link or authenticate the user
*
* @access private
*/
private function step2($backend, $code)
{
$profile = $this->authentication->backend($backend)->getProfile($code);
if ($this->userSession->isLogged()) {
$this->link($backend, $profile);
}
$this->authenticate($backend, $profile);
}
/**
* Link the account
*
* @access private
*/
private function link($backend, $profile)
{
if (empty($profile)) {
$this->session->flashError(t('External authentication failed'));
}
else {
$this->session->flash(t('Your external account is linked to your profile successfully.'));
$this->authentication->backend($backend)->updateUser($this->userSession->getId(), $profile);
}
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
}
/**
* Authenticate the account
*
* @access private
*/
private function authenticate($backend, $profile)
{
if (! empty($profile) && $this->authentication->backend($backend)->authenticate($profile['id'])) {
$this->response->redirect($this->helper->url->to('app', 'index'));
}
else {
$this->response->html($this->template->layout('auth/index', array(
'errors' => array('login' => t('External authentication failed')),
'values' => array(),
'no_layout' => true,
'title' => t('Login')
)));
}
}
}

View file

@ -0,0 +1,134 @@
<?php
namespace Controller;
use Model\User as UserModel;
use Model\Task as TaskModel;
/**
* Project User overview
*
* @package controller
* @author Frederic Guillot
*/
class Projectuser extends Base
{
/**
* Common layout for users overview views
*
* @access private
* @param string $template Template name
* @param array $params Template parameters
* @return string
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['content_for_sublayout'] = $this->template->render($template, $params);
$params['filter'] = array('user_id' => $params['user_id']);
return $this->template->layout('project_user/layout', $params);
}
private function common()
{
$user_id = $this->request->getIntegerParam('user_id', UserModel::EVERYBODY_ID);
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
}
else {
$project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
}
return array($user_id, $project_ids, $this->user->getList(true));
}
private function role($is_owner, $action, $title, $title_user)
{
list($user_id, $project_ids, $users) = $this->common();
$query = $this->projectPermission->getQueryByRole($project_ids, $is_owner)->callback(array($this->project, 'applyColumnStats'));
if ($user_id !== UserModel::EVERYBODY_ID) {
$query->eq(UserModel::TABLE.'.id', $user_id);
$title = t($title_user, $users[$user_id]);
}
$paginator = $this->paginator
->setUrl('projectuser', $action, array('user_id' => $user_id))
->setMax(30)
->setOrder('projects.name')
->setQuery($query)
->calculate();
$this->response->html($this->layout('project_user/roles', array(
'paginator' => $paginator,
'title' => $title,
'user_id' => $user_id,
'users' => $users,
)));
}
private function tasks($is_active, $action, $title, $title_user)
{
list($user_id, $project_ids, $users) = $this->common();
$query = $this->taskFinder->getProjectUserOverviewQuery($project_ids, $is_active);
if ($user_id !== UserModel::EVERYBODY_ID) {
$query->eq(TaskModel::TABLE.'.owner_id', $user_id);
$title = t($title_user, $users[$user_id]);
}
$paginator = $this->paginator
->setUrl('projectuser', $action, array('user_id' => $user_id))
->setMax(50)
->setOrder(TaskModel::TABLE.'.id')
->setQuery($query)
->calculate();
$this->response->html($this->layout('project_user/tasks', array(
'paginator' => $paginator,
'title' => $title,
'user_id' => $user_id,
'users' => $users,
)));
}
/**
* Display the list of project managers
*
*/
public function managers()
{
$this->role(1, 'managers', t('People who are project managers'), 'Projects where "%s" is manager');
}
/**
* Display the list of project members
*
*/
public function members()
{
$this->role(0, 'members', t('People who are project members'), 'Projects where "%s" is member');
}
/**
* Display the list of open taks
*
*/
public function opens()
{
$this->tasks(TaskModel::STATUS_OPEN, 'opens', t('Open tasks'), 'Open tasks assigned to "%s"');
}
/**
* Display the list of closed tasks
*
*/
public function closed()
{
$this->tasks(TaskModel::STATUS_CLOSED, 'closed', t('Closed tasks'), 'Closed tasks assigned to "%s"');
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Controller;
/**
* Search controller
*
* @package controller
* @author Frederic Guillot
*/
class Search extends Base
{
public function index()
{
$projects = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$search = urldecode($this->request->getStringParam('search'));
$nb_tasks = 0;
$paginator = $this->paginator
->setUrl('search', 'index', array('search' => $search))
->setMax(30)
->setOrder('tasks.id')
->setDirection('DESC');
if ($search !== '') {
$query = $this
->taskFilter
->search($search)
->filterByProjects(array_keys($projects))
->getQuery();
$paginator
->setQuery($query)
->calculate();
$nb_tasks = $paginator->getTotal();
}
$this->response->html($this->template->layout('search/index', array(
'board_selector' => $projects,
'values' => array(
'search' => $search,
'controller' => 'search',
'action' => 'index',
),
'paginator' => $paginator,
'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
)));
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Controller;
/**
* Task Creation controller
*
* @package controller
* @author Frederic Guillot
*/
class Taskcreation extends Base
{
/**
* Display a form to create a new task
*
* @access public
*/
public function create(array $values = array(), array $errors = array())
{
$project = $this->getProject();
$method = $this->request->isAjax() ? 'render' : 'layout';
$swimlanes_list = $this->swimlane->getList($project['id'], false, true);
if (empty($values)) {
$values = array(
'swimlane_id' => $this->request->getIntegerParam('swimlane_id', key($swimlanes_list)),
'column_id' => $this->request->getIntegerParam('column_id'),
'color_id' => $this->request->getStringParam('color_id', $this->color->getDefaultColor()),
'owner_id' => $this->request->getIntegerParam('owner_id'),
'another_task' => $this->request->getIntegerParam('another_task'),
);
}
$this->response->html($this->template->$method('task_creation/form', array(
'ajax' => $this->request->isAjax(),
'errors' => $errors,
'values' => $values + array('project_id' => $project['id']),
'columns_list' => $this->board->getColumnsList($project['id']),
'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'swimlanes_list' => $swimlanes_list,
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'title' => $project['name'].' &gt; '.t('New task')
)));
}
/**
* Validate and save a new task
*
* @access public
*/
public function save()
{
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->taskValidator->validateCreation($values);
if ($valid) {
if ($this->taskCreation->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($this->helper->url->to('taskcreation', 'create', $values));
}
else {
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id'])));
}
}
else {
$this->session->flashError(t('Unable to create your task.'));
}
}
$this->create($values, $errors);
}
}

View file

@ -0,0 +1,145 @@
<?php
namespace Controller;
/**
* Task Duplication controller
*
* @package controller
* @author Frederic Guillot
*/
class Taskduplication extends Base
{
/**
* Duplicate a task
*
* @access public
*/
public function duplicate()
{
$task = $this->getTask();
if ($this->request->getStringParam('confirmation') === 'yes') {
$this->checkCSRFParam();
$task_id = $this->taskDuplication->duplicate($task['id']);
if ($task_id > 0) {
$this->session->flash(t('Task created successfully.'));
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
} else {
$this->session->flashError(t('Unable to create this task.'));
$this->response->redirect($this->helper->url->to('taskduplication', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
}
}
$this->response->html($this->taskLayout('task_duplication/duplicate', array(
'task' => $task,
)));
}
/**
* Move a task to another project
*
* @access public
*/
public function move()
{
$task = $this->getTask();
if ($this->request->isPost()) {
$values = $this->request->getValues();
list($valid,) = $this->taskValidator->validateProjectModification($values);
if ($valid && $this->taskDuplication->moveToProject($task['id'],
$values['project_id'],
$values['swimlane_id'],
$values['column_id'],
$values['category_id'],
$values['owner_id'])) {
$this->session->flash(t('Task updated successfully.'));
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $values['project_id'], 'task_id' => $task['id'])));
}
$this->session->flashError(t('Unable to update your task.'));
}
$this->chooseDestination($task, 'task_duplication/move');
}
/**
* Duplicate a task to another project
*
* @access public
*/
public function copy()
{
$task = $this->getTask();
if ($this->request->isPost()) {
$values = $this->request->getValues();
list($valid,) = $this->taskValidator->validateProjectModification($values);
if ($valid && $this->taskDuplication->duplicateToProject($task['id'],
$values['project_id'],
$values['swimlane_id'],
$values['column_id'],
$values['category_id'],
$values['owner_id'])) {
$this->session->flash(t('Task created successfully.'));
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
}
$this->session->flashError(t('Unable to create your task.'));
}
$this->chooseDestination($task, 'task_duplication/copy');
}
/**
* Choose destination when move/copy task to another project
*
* @access private
* @param array $task
* @param string $template
*/
private function chooseDestination(array $task, $template)
{
$values = array();
$projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId());
unset($projects_list[$task['project_id']]);
if (! empty($projects_list)) {
$dst_project_id = $this->request->getIntegerParam('dst_project_id', key($projects_list));
$swimlanes_list = $this->swimlane->getList($dst_project_id, false, true);
$columns_list = $this->board->getColumnsList($dst_project_id);
$categories_list = $this->category->getList($dst_project_id);
$users_list = $this->projectPermission->getMemberList($dst_project_id);
$values = $this->taskDuplication->checkDestinationProjectValues($task);
$values['project_id'] = $dst_project_id;
}
else {
$swimlanes_list = array();
$columns_list = array();
$categories_list = array();
$users_list = array();
}
$this->response->html($this->taskLayout($template, array(
'values' => $values,
'task' => $task,
'projects_list' => $projects_list,
'swimlanes_list' => $swimlanes_list,
'columns_list' => $columns_list,
'categories_list' => $categories_list,
'users_list' => $users_list,
)));
}
}

View file

@ -0,0 +1,212 @@
<?php
namespace Controller;
/**
* Task Modification controller
*
* @package controller
* @author Frederic Guillot
*/
class Taskmodification extends Base
{
/**
* Set automatically the start date
*
* @access public
*/
public function start()
{
$task = $this->getTask();
$this->taskModification->update(array('id' => $task['id'], 'date_started' => time()));
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
}
/**
* Update time tracking information
*
* @access public
*/
public function time()
{
$task = $this->getTask();
$values = $this->request->getValues();
list($valid,) = $this->taskValidator->validateTimeModification($values);
if ($valid && $this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
}
/**
* Edit description form
*
* @access public
*/
public function description()
{
$task = $this->getTask();
$ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
if ($this->request->isPost()) {
$values = $this->request->getValues();
list($valid, $errors) = $this->taskValidator->validateDescriptionCreation($values);
if ($valid) {
if ($this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
if ($ajax) {
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
}
else {
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
}
}
}
else {
$values = $task;
$errors = array();
}
$params = array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'ajax' => $ajax,
);
if ($ajax) {
$this->response->html($this->template->render('task_modification/edit_description', $params));
}
else {
$this->response->html($this->taskLayout('task_modification/edit_description', $params));
}
}
/**
* Display a form to edit a task
*
* @access public
*/
public function edit(array $values = array(), array $errors = array())
{
$task = $this->getTask();
$ajax = $this->request->isAjax();
if (empty($values)) {
$values = $task;
}
$this->dateParser->format($values, array('date_due'));
$params = array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'users_list' => $this->projectPermission->getMemberList($task['project_id']),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($task['project_id']),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'ajax' => $ajax,
);
if ($ajax) {
$this->response->html($this->template->render('task_modification/edit_task', $params));
}
else {
$this->response->html($this->taskLayout('task_modification/edit_task', $params));
}
}
/**
* Validate and update a task
*
* @access public
*/
public function update()
{
$task = $this->getTask();
$values = $this->request->getValues();
list($valid, $errors) = $this->taskValidator->validateModification($values);
if ($valid) {
if ($this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
if ($this->request->getIntegerParam('ajax')) {
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
}
else {
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
}
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
}
$this->edit($values, $errors);
}
/**
* Edit recurrence form
*
* @access public
*/
public function recurrence()
{
$task = $this->getTask();
if ($this->request->isPost()) {
$values = $this->request->getValues();
list($valid, $errors) = $this->taskValidator->validateEditRecurrence($values);
if ($valid) {
if ($this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your task.'));
}
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
}
}
else {
$values = $task;
$errors = array();
}
$params = array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'recurrence_status_list' => $this->task->getRecurrenceStatusList(),
'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
);
$this->response->html($this->taskLayout('task_modification/edit_recurrence', $params));
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Controller;
/**
* Task Status controller
*
* @package controller
* @author Frederic Guillot
*/
class Taskstatus extends Base
{
/**
* Close a task
*
* @access public
*/
public function close()
{
$task = $this->getTask();
$redirect = $this->request->getStringParam('redirect');
if ($this->request->getStringParam('confirmation') === 'yes') {
$this->checkCSRFParam();
if ($this->taskStatus->close($task['id'])) {
$this->session->flash(t('Task closed successfully.'));
} else {
$this->session->flashError(t('Unable to close this task.'));
}
if ($redirect === 'board') {
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
}
$this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
if ($this->request->isAjax()) {
$this->response->html($this->template->render('task_status/close', array(
'task' => $task,
'redirect' => $redirect,
)));
}
$this->response->html($this->taskLayout('task_status/close', array(
'task' => $task,
'redirect' => $redirect,
)));
}
/**
* Open a task
*
* @access public
*/
public function open()
{
$task = $this->getTask();
if ($this->request->getStringParam('confirmation') === 'yes') {
$this->checkCSRFParam();
if ($this->taskStatus->open($task['id'])) {
$this->session->flash(t('Task opened successfully.'));
} else {
$this->session->flashError(t('Unable to open this task.'));
}
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])));
}
$this->response->html($this->taskLayout('task_status/open', array(
'task' => $task,
)));
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Controller;
/**
* Time Tracking controller
*
* @package controller
* @author Frederic Guillot
*/
class Timer extends Base
{
/**
* Start/stop timer for subtasks
*
* @access public
*/
public function subtask()
{
$project_id = $this->request->getIntegerParam('project_id');
$task_id = $this->request->getIntegerParam('task_id');
$subtask_id = $this->request->getIntegerParam('subtask_id');
$timer = $this->request->getStringParam('timer');
if ($timer === 'start') {
$this->subtaskTimeTracking->logStartTime($subtask_id, $this->userSession->getId());
}
else if ($timer === 'stop') {
$this->subtaskTimeTracking->logEndTime($subtask_id, $this->userSession->getId());
$this->subtaskTimeTracking->updateTaskTimeTracking($task_id);
}
$this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $project_id, 'task_id' => $task_id)).'#subtasks');
}
}

118
sources/app/Core/Base.php Normal file
View file

@ -0,0 +1,118 @@
<?php
namespace Core;
use Pimple\Container;
/**
* Base class
*
* @package core
* @author Frederic Guillot
*
* @property \Core\Helper $helper
* @property \Core\EmailClient $emailClient
* @property \Core\HttpClient $httpClient
* @property \Core\Paginator $paginator
* @property \Core\Request $request
* @property \Core\Session $session
* @property \Core\Template $template
* @property \Core\MemoryCache $memoryCache
* @property \Core\OAuth2 $oauth
* @property \Core\Router $router
* @property \Core\Lexer $lexer
* @property \Integration\BitbucketWebhook $bitbucketWebhook
* @property \Integration\GithubWebhook $githubWebhook
* @property \Integration\GitlabWebhook $gitlabWebhook
* @property \Integration\HipchatWebhook $hipchatWebhook
* @property \Integration\Jabber $jabber
* @property \Integration\Mailgun $mailgun
* @property \Integration\Postmark $postmark
* @property \Integration\Sendgrid $sendgrid
* @property \Integration\SlackWebhook $slackWebhook
* @property \Integration\Smtp $smtp
* @property \Model\Acl $acl
* @property \Model\Action $action
* @property \Model\Authentication $authentication
* @property \Model\Board $board
* @property \Model\Budget $budget
* @property \Model\Category $category
* @property \Model\Color $color
* @property \Model\Comment $comment
* @property \Model\Config $config
* @property \Model\Currency $currency
* @property \Model\DateParser $dateParser
* @property \Model\File $file
* @property \Model\HourlyRate $hourlyRate
* @property \Model\LastLogin $lastLogin
* @property \Model\Link $link
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\ProjectActivity $projectActivity
* @property \Model\ProjectAnalytic $projectAnalytic
* @property \Model\ProjectDuplication $projectDuplication
* @property \Model\ProjectDailyColumnStats $projectDailyColumnStats
* @property \Model\ProjectDailyStats $projectDailyStats
* @property \Model\ProjectIntegration $projectIntegration
* @property \Model\ProjectPermission $projectPermission
* @property \Model\Subtask $subtask
* @property \Model\SubtaskExport $subtaskExport
* @property \Model\SubtaskForecast $subtaskForecast
* @property \Model\SubtaskTimeTracking $subtaskTimeTracking
* @property \Model\Swimlane $swimlane
* @property \Model\Task $task
* @property \Model\TaskAnalytic $taskAnalytic
* @property \Model\TaskCreation $taskCreation
* @property \Model\TaskDuplication $taskDuplication
* @property \Model\TaskExport $taskExport
* @property \Model\TaskFinder $taskFinder
* @property \Model\TaskFilter $taskFilter
* @property \Model\TaskLink $taskLink
* @property \Model\TaskModification $taskModification
* @property \Model\TaskPermission $taskPermission
* @property \Model\TaskPosition $taskPosition
* @property \Model\TaskStatus $taskStatus
* @property \Model\TaskValidator $taskValidator
* @property \Model\Timetable $timetable
* @property \Model\TimetableDay $timetableDay
* @property \Model\TimetableExtra $timetableExtra
* @property \Model\TimetableOff $timetableOff
* @property \Model\TimetableWeek $timetableWeek
* @property \Model\Transition $transition
* @property \Model\User $user
* @property \Model\UserSession $userSession
* @property \Model\Webhook $webhook
*/
abstract class Base
{
/**
* Container instance
*
* @access protected
* @var \Pimple\Container
*/
protected $container;
/**
* Constructor
*
* @access public
* @param \Pimple\Container $container
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Load automatically models
*
* @access public
* @param string $name Model name
* @return mixed
*/
public function __get($name)
{
return $this->container[$name];
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Core;
/**
* Mail client
*
* @package core
* @author Frederic Guillot
*/
class EmailClient extends Base
{
/**
* Send a HTML email
*
* @access public
* @param string $email
* @param string $name
* @param string $subject
* @param string $html
*/
public function send($email, $name, $subject, $html)
{
$this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
$start_time = microtime(true);
$author = 'Kanboard';
if (Session::isOpen() && $this->userSession->isLogged()) {
$author = e('%s via Kanboard', $this->user->getFullname($this->session['user']));
}
switch (MAIL_TRANSPORT) {
case 'sendgrid':
$this->sendgrid->sendEmail($email, $name, $subject, $html, $author);
break;
case 'mailgun':
$this->mailgun->sendEmail($email, $name, $subject, $html, $author);
break;
case 'postmark':
$this->postmark->sendEmail($email, $name, $subject, $html, $author);
break;
default:
$this->smtp->sendEmail($email, $name, $subject, $html, $author);
}
$this->container['logger']->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
}
}

162
sources/app/Core/Lexer.php Normal file
View file

@ -0,0 +1,162 @@
<?php
namespace Core;
/**
* Lexer
*
* @package core
* @author Frederic Guillot
*/
class Lexer
{
/**
* Current position
*
* @access private
* @var integer
*/
private $offset = 0;
/**
* Token map
*
* @access private
* @var array
*/
private $tokenMap = array(
"/^(assignee:)/" => 'T_ASSIGNEE',
"/^(color:)/" => 'T_COLOR',
"/^(due:)/" => 'T_DUE',
"/^(updated:)/" => 'T_UPDATED',
"/^(modified:)/" => 'T_UPDATED',
"/^(created:)/" => 'T_CREATED',
"/^(status:)/" => 'T_STATUS',
"/^(description:)/" => 'T_DESCRIPTION',
"/^(category:)/" => 'T_CATEGORY',
"/^(column:)/" => 'T_COLUMN',
"/^(project:)/" => 'T_PROJECT',
"/^(swimlane:)/" => 'T_SWIMLANE',
"/^(ref:)/" => 'T_REFERENCE',
"/^(reference:)/" => 'T_REFERENCE',
"/^(\s+)/" => 'T_WHITESPACE',
'/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
'/^(yesterday|tomorrow|today)/' => 'T_DATE',
'/^("(.*?)")/' => 'T_STRING',
"/^(\w+)/" => 'T_STRING',
"/^(#\d+)/" => 'T_STRING',
);
/**
* Tokenize input string
*
* @access public
* @param string $input
* @return array
*/
public function tokenize($input)
{
$tokens = array();
$this->offset = 0;
while (isset($input[$this->offset])) {
$result = $this->match(substr($input, $this->offset));
if ($result === false) {
return array();
}
$tokens[] = $result;
}
return $tokens;
}
/**
* Find a token that match and move the offset
*
* @access public
* @param string $string
* @return array|boolean
*/
public function match($string)
{
foreach ($this->tokenMap as $pattern => $name) {
if (preg_match($pattern, $string, $matches)) {
$this->offset += strlen($matches[1]);
return array(
'match' => trim($matches[1], '"'),
'token' => $name,
);
}
}
return false;
}
/**
* Change the output of tokenizer to be easily parsed by the database filter
*
* Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title']
*
* @access public
* @param array $tokens
* @return array
*/
public function map(array $tokens)
{
$map = array(
'T_TITLE' => '',
);
while (false !== ($token = current($tokens))) {
switch ($token['token']) {
case 'T_ASSIGNEE':
case 'T_COLOR':
case 'T_CATEGORY':
case 'T_COLUMN':
case 'T_PROJECT':
case 'T_SWIMLANE':
$next = next($tokens);
if ($next !== false && $next['token'] === 'T_STRING') {
$map[$token['token']][] = $next['match'];
}
break;
case 'T_STATUS':
case 'T_DUE':
case 'T_UPDATED':
case 'T_CREATED':
case 'T_DESCRIPTION':
case 'T_REFERENCE':
$next = next($tokens);
if ($next !== false && ($next['token'] === 'T_DATE' || $next['token'] === 'T_STRING')) {
$map[$token['token']] = $next['match'];
}
break;
default:
$map['T_TITLE'] .= $token['match'];
break;
}
next($tokens);
}
$map['T_TITLE'] = trim($map['T_TITLE']);
if (empty($map['T_TITLE'])) {
unset($map['T_TITLE']);
}
return $map;
}
}

120
sources/app/Core/OAuth2.php Normal file
View file

@ -0,0 +1,120 @@
<?php
namespace Core;
/**
* OAuth2 client
*
* @package core
* @author Frederic Guillot
*/
class OAuth2 extends Base
{
private $clientId;
private $secret;
private $callbackUrl;
private $authUrl;
private $tokenUrl;
private $scopes;
private $tokenType;
private $accessToken;
/**
* Create OAuth2 service
*
* @access public
* @param string $clientId
* @param string $secret
* @param string $callbackUrl
* @param string $authUrl
* @param string $tokenUrl
* @param array $scopes
* @return OAuth2
*/
public function createService($clientId, $secret, $callbackUrl, $authUrl, $tokenUrl, array $scopes)
{
$this->clientId = $clientId;
$this->secret = $secret;
$this->callbackUrl = $callbackUrl;
$this->authUrl = $authUrl;
$this->tokenUrl = $tokenUrl;
$this->scopes = $scopes;
return $this;
}
/**
* Get authorization url
*
* @access public
* @return string
*/
public function getAuthorizationUrl()
{
$params = array(
'response_type' => 'code',
'client_id' => $this->clientId,
'redirect_uri' => $this->callbackUrl,
'scope' => implode(' ', $this->scopes),
);
return $this->authUrl.'?'.http_build_query($params);
}
/**
* Get authorization header
*
* @access public
* @return string
*/
public function getAuthorizationHeader()
{
if (strtolower($this->tokenType) === 'bearer') {
return 'Authorization: Bearer '.$this->accessToken;
}
return '';
}
/**
* Get access token
*
* @access public
* @param string $code
* @return string
*/
public function getAccessToken($code)
{
if (empty($this->accessToken) && ! empty($code)) {
$params = array(
'code' => $code,
'client_id' => $this->clientId,
'client_secret' => $this->secret,
'redirect_uri' => $this->callbackUrl,
'grant_type' => 'authorization_code',
);
$response = json_decode($this->httpClient->postForm($this->tokenUrl, $params, array('Accept: application/json')), true);
$this->tokenType = isset($response['token_type']) ? $response['token_type'] : '';
$this->accessToken = isset($response['access_token']) ? $response['access_token'] : '';
}
return $this->accessToken;
}
/**
* Set access token
*
* @access public
* @param string $token
* @param string $type
* @return string
*/
public function setAccessToken($token, $type = 'bearer')
{
$this->accessToken = $token;
$this->tokenType = $type;
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace Event;
class TaskLinkEvent extends GenericEvent
{
}

View file

@ -0,0 +1,78 @@
<?php
namespace Helper;
/**
* Application helpers
*
* @package helper
* @author Frederic Guillot
*/
class App extends \Core\Base
{
/**
* Get router controller
*
* @access public
* @return string
*/
public function getRouterController()
{
return $this->router->getController();
}
/**
* Get router action
*
* @access public
* @return string
*/
public function getRouterAction()
{
return $this->router->getAction();
}
/**
* Get javascript language code
*
* @access public
* @return string
*/
public function jsLang()
{
return $this->config->getJsLanguageCode();
}
/**
* Get current timezone
*
* @access public
* @return string
*/
public function getTimezone()
{
return $this->config->getCurrentTimezone();
}
/**
* Get session flash message
*
* @access public
* @return string
*/
public function flashMessage()
{
$html = '';
if (isset($this->session['flash_message'])) {
$html = '<div class="alert alert-success alert-fade-out">'.$this->helper->e($this->session['flash_message']).'</div>';
unset($this->session['flash_message']);
}
else if (isset($this->session['flash_error_message'])) {
$html = '<div class="alert alert-error">'.$this->helper->e($this->session['flash_error_message']).'</div>';
unset($this->session['flash_error_message']);
}
return $html;
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Helper;
/**
* Assets helpers
*
* @package helper
* @author Frederic Guillot
*/
class Asset extends \Core\Base
{
/**
* Add a Javascript asset
*
* @param string $filename Filename
* @return string
*/
public function js($filename, $async = false)
{
return '<script '.($async ? 'async' : '').' type="text/javascript" src="'.$this->helper->url->dir().$filename.'?'.filemtime($filename).'"></script>';
}
/**
* Add a stylesheet asset
*
* @param string $filename Filename
* @param boolean $is_file Add file timestamp
* @param string $media Media
* @return string
*/
public function css($filename, $is_file = true, $media = 'screen')
{
return '<link rel="stylesheet" href="'.$this->helper->url->dir().$filename.($is_file ? '?'.filemtime($filename) : '').'" media="'.$media.'">';
}
/**
* Get custom css
*
* @access public
* @return string
*/
public function customCss()
{
if ($this->config->get('application_stylesheet')) {
return '<style>'.$this->config->get('application_stylesheet').'</style>';
}
return '';
}
/**
* Get CSS for task colors
*
* @access public
* @return string
*/
public function colorCss()
{
return '<style>'.$this->color->getCss().'</style>';
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Helper;
/**
* Board Helper
*
* @package helper
* @author Frederic Guillot
*/
class Board extends \Core\Base
{
/**
* Return true if tasks are collapsed
*
* @access public
* @param integer $project_id
* @return boolean
*/
public function isCollapsed($project_id)
{
return $this->userSession->isBoardCollapsed($project_id);
}
}

114
sources/app/Helper/Dt.php Normal file
View file

@ -0,0 +1,114 @@
<?php
namespace Helper;
use DateTime;
/**
* DateTime helpers
*
* @package helper
* @author Frederic Guillot
*/
class Dt extends \Core\Base
{
/**
* Get duration in seconds into human format
*
* @access public
* @param integer $seconds
* @return string
*/
public function duration($seconds)
{
if ($seconds == 0) {
return 0;
}
$dtF = new DateTime("@0");
$dtT = new DateTime("@$seconds");
return $dtF->diff($dtT)->format('%a days, %h hours, %i minutes and %s seconds');
}
/**
* Get the age of an item in quasi human readable format.
* It's in this format: <1h , NNh, NNd
*
* @access public
* @param integer $timestamp Unix timestamp of the artifact for which age will be calculated
* @param integer $now Compare with this timestamp (Default value is the current unix timestamp)
* @return string
*/
public function age($timestamp, $now = null)
{
if ($now === null) {
$now = time();
}
$diff = $now - $timestamp;
if ($diff < 900) {
return t('<15m');
}
if ($diff < 1200) {
return t('<30m');
}
else if ($diff < 3600) {
return t('<1h');
}
else if ($diff < 86400) {
return '~'.t('%dh', $diff / 3600);
}
return t('%dd', ($now - $timestamp) / 86400);
}
/**
* Get all hours for day
*
* @access public
* @return array
*/
public function getDayHours()
{
$values = array();
foreach (range(0, 23) as $hour) {
foreach (array(0, 30) as $minute) {
$time = sprintf('%02d:%02d', $hour, $minute);
$values[$time] = $time;
}
}
return $values;
}
/**
* Get all days of a week
*
* @access public
* @return array
*/
public function getWeekDays()
{
$values = array();
foreach (range(1, 7) as $day) {
$values[$day] = $this->getWeekDay($day);
}
return $values;
}
/**
* Get the localized day name from the day number
*
* @access public
* @param integer $day Day number
* @return string
*/
public function getWeekDay($day)
{
return dt('%A', strtotime('next Monday +'.($day - 1).' days'));
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Helper;
/**
* File helpers
*
* @package helper
* @author Frederic Guillot
*/
class File extends \Core\Base
{
/**
* Get file icon
*
* @access public
* @param string $filename Filename
* @return string Font-Awesome-Icon-Name
*/
public function icon($filename){
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
switch ($extension) {
case 'jpeg':
case 'jpg':
case 'png':
case 'gif':
return 'fa-file-image-o';
case 'xls':
case 'xlsx':
return 'fa-file-excel-o';
case 'doc':
case 'docx':
return 'fa-file-word-o';
case 'ppt':
case 'pptx':
return 'fa-file-powerpoint-o';
case 'zip':
case 'rar':
return 'fa-file-archive-o';
case 'mp3':
return 'fa-audio-o';
case 'avi':
return 'fa-video-o';
case 'php':
case 'html':
case 'css':
return 'fa-code-o';
case 'pdf':
return 'fa-file-pdf-o';
}
return 'fa-file-o';
}
}

323
sources/app/Helper/Form.php Normal file
View file

@ -0,0 +1,323 @@
<?php
namespace Helper;
use Core\Security;
/**
* Form helpers
*
* @package helper
* @author Frederic Guillot
*/
class Form extends \Core\Base
{
/**
* Hidden CSRF token field
*
* @access public
* @return string
*/
public function csrf()
{
return '<input type="hidden" name="csrf_token" value="'.Security::getCSRFToken().'"/>';
}
/**
* Display a hidden form field
*
* @access public
* @param string $name Field name
* @param array $values Form values
* @return string
*/
public function hidden($name, array $values = array())
{
return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.$this->formValue($values, $name).'/>';
}
/**
* Display a select field
*
* @access public
* @param string $name Field name
* @param array $options Options
* @param array $values Form values
* @param array $errors Form errors
* @param string $class CSS class
* @return string
*/
public function select($name, array $options, array $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'" '.implode(' ', $attributes).'>';
foreach ($options as $id => $value) {
$html .= '<option value="'.$this->helper->e($id).'"';
if (isset($values->$name) && $id == $values->$name) $html .= ' selected="selected"';
if (isset($values[$name]) && $id == $values[$name]) $html .= ' selected="selected"';
$html .= '>'.$this->helper->e($value).'</option>';
}
$html .= '</select>';
$html .= $this->errorList($errors, $name);
return $html;
}
/**
* Display a radio field group
*
* @access public
* @param string $name Field name
* @param array $options Options
* @param array $values Form values
* @return string
*/
public function radios($name, array $options, array $values = array())
{
$html = '';
foreach ($options as $value => $label) {
$html .= $this->radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value);
}
return $html;
}
/**
* Display a radio field
*
* @access public
* @param string $name Field name
* @param string $label Form label
* @param string $value Form value
* @param boolean $selected Field selected or not
* @param string $class CSS class
* @return string
*/
public function radio($name, $label, $value, $selected = false, $class = '')
{
return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.$this->helper->e($value).'" '.($selected ? 'checked="checked"' : '').'> '.$this->helper->e($label).'</label>';
}
/**
* Display a checkbox field
*
* @access public
* @param string $name Field name
* @param string $label Form label
* @param string $value Form value
* @param boolean $checked Field selected or not
* @param string $class CSS class
* @return string
*/
public function checkbox($name, $label, $value, $checked = false, $class = '')
{
return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.$this->helper->e($value).'" '.($checked ? 'checked="checked"' : '').'>&nbsp;'.$this->helper->e($label).'</label>';
}
/**
* Display a form label
*
* @access public
* @param string $name Field name
* @param string $label Form label
* @param array $attributes HTML attributes
* @return string
*/
public function label($label, $name, array $attributes = array())
{
return '<label for="form-'.$name.'" '.implode(' ', $attributes).'>'.$this->helper->e($label).'</label>';
}
/**
* Display a textarea
*
* @access public
* @param string $name Field name
* @param array $values Form values
* @param array $errors Form errors
* @param array $attributes HTML attributes
* @param string $class CSS class
* @return string
*/
public function textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= $this->errorClass($errors, $name);
$html = '<textarea name="'.$name.'" id="form-'.$name.'" class="'.$class.'" ';
$html .= implode(' ', $attributes).'>';
$html .= isset($values->$name) ? $this->helper->e($values->$name) : isset($values[$name]) ? $values[$name] : '';
$html .= '</textarea>';
$html .= $this->errorList($errors, $name);
return $html;
}
/**
* Display a input field
*
* @access public
* @param string $type HMTL input tag type
* @param string $name Field name
* @param array $values Form values
* @param array $errors Form errors
* @param array $attributes HTML attributes
* @param string $class CSS class
* @return string
*/
public function input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= $this->errorClass($errors, $name);
$html = '<input type="'.$type.'" name="'.$name.'" id="form-'.$name.'" '.$this->formValue($values, $name).' class="'.$class.'" ';
$html .= implode(' ', $attributes).'>';
if (in_array('required', $attributes)) {
$html .= '<span class="form-required">*</span>';
}
$html .= $this->errorList($errors, $name);
return $html;
}
/**
* Display a text field
*
* @access public
* @param string $name Field name
* @param array $values Form values
* @param array $errors Form errors
* @param array $attributes HTML attributes
* @param string $class CSS class
* @return string
*/
public function text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
return $this->input('text', $name, $values, $errors, $attributes, $class);
}
/**
* Display a password field
*
* @access public
* @param string $name Field name
* @param array $values Form values
* @param array $errors Form errors
* @param array $attributes HTML attributes
* @param string $class CSS class
* @return string
*/
public function password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
return $this->input('password', $name, $values, $errors, $attributes, $class);
}
/**
* Display an email field
*
* @access public
* @param string $name Field name
* @param array $values Form values
* @param array $errors Form errors
* @param array $attributes HTML attributes
* @param string $class CSS class
* @return string
*/
public function email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
return $this->input('email', $name, $values, $errors, $attributes, $class);
}
/**
* Display a number field
*
* @access public
* @param string $name Field name
* @param array $values Form values
* @param array $errors Form errors
* @param array $attributes HTML attributes
* @param string $class CSS class
* @return string
*/
public function number($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
return $this->input('number', $name, $values, $errors, $attributes, $class);
}
/**
* Display a numeric field (allow decimal number)
*
* @access public
* @param string $name Field name
* @param array $values Form values
* @param array $errors Form errors
* @param array $attributes HTML attributes
* @param string $class CSS class
* @return string
*/
public function numeric($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
return $this->input('text', $name, $values, $errors, $attributes, $class.' form-numeric');
}
/**
* Display the form error class
*
* @access private
* @param array $errors Error list
* @param string $name Field name
* @return string
*/
private function errorClass(array $errors, $name)
{
return ! isset($errors[$name]) ? '' : ' form-error';
}
/**
* Display a list of form errors
*
* @access private
* @param array $errors List of errors
* @param string $name Field name
* @return string
*/
private function errorList(array $errors, $name)
{
$html = '';
if (isset($errors[$name])) {
$html .= '<ul class="form-errors">';
foreach ($errors[$name] as $error) {
$html .= '<li>'.$this->helper->e($error).'</li>';
}
$html .= '</ul>';
}
return $html;
}
/**
* Get an escaped form value
*
* @access private
* @param mixed $values Values
* @param string $name Field name
* @return string
*/
private function formValue($values, $name)
{
if (isset($values->$name)) {
return 'value="'.$this->helper->e($values->$name).'"';
}
return isset($values[$name]) ? 'value="'.$this->helper->e($values[$name]).'"' : '';
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Helper;
/**
* Subtask helpers
*
* @package helper
* @author Frederic Guillot
*/
class Subtask extends \Core\Base
{
/**
* Get the link to toggle subtask status
*
* @access public
* @param array $subtask
* @param string $redirect
* @return string
*/
public function toggleStatus(array $subtask, $redirect)
{
if ($subtask['status'] == 0 && isset($this->session['has_subtask_inprogress']) && $this->session['has_subtask_inprogress'] === true) {
return $this->helper->url->link(
trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']),
'subtask',
'subtaskRestriction',
array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect),
false,
'popover task-board-popover'
);
}
return $this->helper->url->link(
trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']),
'subtask',
'toggleStatus',
array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect)
);
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Helper;
/**
* Task helpers
*
* @package helper
* @author Frederic Guillot
*/
class Task extends \Core\Base
{
public function getColors()
{
return $this->color->getList();
}
public function recurrenceTriggers()
{
return $this->task->getRecurrenceTriggerList();
}
public function recurrenceTimeframes()
{
return $this->task->getRecurrenceTimeframeList();
}
public function recurrenceBasedates()
{
return $this->task->getRecurrenceBasedateList();
}
public function canRemove(array $task)
{
return $this->taskPermission->canRemoveTask($task);
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Helper;
use Core\Markdown;
/**
* Text helpers
*
* @package helper
* @author Frederic Guillot
*/
class Text extends \Core\Base
{
/**
* Markdown transformation
*
* @param string $text Markdown content
* @param array $link Link parameters for replacement
* @return string
*/
public function markdown($text, array $link = array())
{
$parser = new Markdown($link, $this->helper->url);
$parser->setMarkupEscaped(MARKDOWN_ESCAPE_HTML);
return $parser->text($text);
}
/**
* Format a file size
*
* @param integer $size Size in bytes
* @param integer $precision Precision
* @return string
*/
public function bytes($size, $precision = 2)
{
$base = log($size) / log(1024);
$suffixes = array('', 'k', 'M', 'G', 'T');
return round(pow(1024, $base - floor($base)), $precision).$suffixes[(int)floor($base)];
}
/**
* Return true if needle is contained in the haystack
*
* @param string $haystack Haystack
* @param string $needle Needle
* @return boolean
*/
public function contains($haystack, $needle)
{
return strpos($haystack, $needle) !== false;
}
/**
* Return a value from a dictionary
*
* @param mixed $id Key
* @param array $listing Dictionary
* @param string $default_value Value displayed when the key doesn't exists
* @return string
*/
public function in($id, array $listing, $default_value = '?')
{
if (isset($listing[$id])) {
return $this->helper->e($listing[$id]);
}
return $default_value;
}
}

170
sources/app/Helper/Url.php Normal file
View file

@ -0,0 +1,170 @@
<?php
namespace Helper;
use Core\Request;
use Core\Security;
/**
* Url helpers
*
* @package helper
* @author Frederic Guillot
*/
class Url extends \Core\Base
{
private $base = '';
private $directory = '';
/**
* Helper to generate a link to the documentation
*
* @access public
* @param string $label
* @param string $file
* @return string
*/
public function doc($label, $file)
{
return $this->link($label, 'doc', 'show', array('file' => $file), false, '', '', true);
}
/**
* HTML Link tag
*
* @access public
* @param string $label Link label
* @param string $controller Controller name
* @param string $action Action name
* @param array $params Url parameters
* @param boolean $csrf Add a CSRF token
* @param string $class CSS class attribute
* @param boolean $new_tab Open the link in a new tab
* @param string $anchor Link Anchor
* @return string
*/
public function link($label, $controller, $action, array $params = array(), $csrf = false, $class = '', $title = '', $new_tab = false, $anchor = '')
{
return '<a href="'.$this->href($controller, $action, $params, $csrf, $anchor).'" class="'.$class.'" title="'.$title.'" '.($new_tab ? 'target="_blank"' : '').'>'.$label.'</a>';
}
/**
* HTML Hyperlink
*
* @access public
* @param string $controller Controller name
* @param string $action Action name
* @param array $params Url parameters
* @param boolean $csrf Add a CSRF token
* @param string $anchor Link Anchor
* @param boolean $absolute Absolute or relative link
* @return string
*/
public function href($controller, $action, array $params = array(), $csrf = false, $anchor = '', $absolute = false)
{
return $this->build('&amp;', $controller, $action, $params, $csrf, $anchor, $absolute);
}
/**
* Generate controller/action url
*
* @access public
* @param string $controller Controller name
* @param string $action Action name
* @param array $params Url parameters
* @param string $anchor Link Anchor
* @param boolean $absolute Absolute or relative link
* @return string
*/
public function to($controller, $action, array $params = array(), $anchor = '', $absolute = false)
{
return $this->build('&', $controller, $action, $params, false, $anchor, $absolute);
}
/**
* Get application base url
*
* @access public
* @return string
*/
public function base()
{
if (empty($this->base)) {
$this->base = $this->config->get('application_url') ?: $this->server();
}
return $this->base;
}
/**
* Get application base directory
*
* @access public
* @return string
*/
public function dir()
{
if (empty($this->directory) && isset($_SERVER['REQUEST_METHOD'])) {
$this->directory = str_replace('\\', '/', dirname($_SERVER['PHP_SELF']));
$this->directory = $this->directory !== '/' ? $this->directory.'/' : '/';
$this->directory = str_replace('//', '/', $this->directory);
}
return $this->directory;
}
/**
* Get current server base url
*
* @access public
* @return string
*/
public function server()
{
if (empty($_SERVER['SERVER_NAME'])) {
return 'http://localhost/';
}
$url = Request::isHTTPS() ? 'https://' : 'http://';
$url .= $_SERVER['SERVER_NAME'];
$url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
$url .= $this->dir() ?: '/';
return $url;
}
/**
* Build relative url
*
* @access private
* @param string $separator Querystring argument separator
* @param string $controller Controller name
* @param string $action Action name
* @param array $params Url parameters
* @param boolean $csrf Add a CSRF token
* @param string $anchor Link Anchor
* @param boolean $absolute Absolute or relative link
* @return string
*/
private function build($separator, $controller, $action, array $params = array(), $csrf = false, $anchor = '', $absolute = false)
{
$path = $this->router->findUrl($controller, $action, $params);
$qs = array();
if (empty($path)) {
$qs['controller'] = $controller;
$qs['action'] = $action;
$qs += $params;
}
if ($csrf) {
$qs['csrf_token'] = Security::getCSRFToken();
}
if (! empty($qs)) {
$path .= '?'.http_build_query($qs, '', $separator);
}
return ($absolute ? $this->base() : $this->dir()).$path.(empty($anchor) ? '' : '#'.$anchor);
}
}

147
sources/app/Helper/User.php Normal file
View file

@ -0,0 +1,147 @@
<?php
namespace Helper;
/**
* User helpers
*
* @package helper
* @author Frederic Guillot
*/
class User extends \Core\Base
{
/**
* Get initials from a user
*
* @access public
* @param string $name
* @return string
*/
public function getInitials($name)
{
$initials = '';
foreach (explode(' ', $name) as $string) {
$initials .= mb_substr($string, 0, 1);
}
return mb_strtoupper($initials);
}
/**
* Get user id
*
* @access public
* @return integer
*/
public function getId()
{
return $this->userSession->getId();
}
/**
* Get user profile
*
* @access public
* @return string
*/
public function getProfileLink()
{
return $this->helper->url->link(
$this->helper->e($this->getFullname()),
'user',
'show',
array('user_id' => $this->userSession->getId())
);
}
/**
* Check if the given user_id is the connected user
*
* @param integer $user_id User id
* @return boolean
*/
public function isCurrentUser($user_id)
{
return $this->userSession->getId() == $user_id;
}
/**
* Return if the logged user is admin
*
* @access public
* @return boolean
*/
public function isAdmin()
{
return $this->userSession->isAdmin();
}
/**
* Return if the logged user is project admin
*
* @access public
* @return boolean
*/
public function isProjectAdmin()
{
return $this->userSession->isProjectAdmin();
}
/**
* Check for project administration actions access (Project Admin group)
*
* @access public
* @return boolean
*/
public function isProjectAdministrationAllowed($project_id)
{
if ($this->userSession->isAdmin()) {
return true;
}
return $this->memoryCache->proxy('acl', 'handleProjectAdminPermissions', $project_id);
}
/**
* Check for project management actions access (Regular users who are Project Managers)
*
* @access public
* @return boolean
*/
public function isProjectManagementAllowed($project_id)
{
if ($this->userSession->isAdmin()) {
return true;
}
return $this->memoryCache->proxy('acl', 'handleProjectManagerPermissions', $project_id);
}
/**
* Return the user full name
*
* @param array $user User properties
* @return string
*/
public function getFullname(array $user = array())
{
return $this->user->getFullname(empty($user) ? $_SESSION['user'] : $user);
}
/**
* Display gravatar image
*
* @access public
* @param string $email
* @param string $alt
* @return string
*/
public function avatar($email, $alt = '')
{
if (! empty($email) && $this->config->get('integration_gravatar') == 1) {
return '<img class="avatar" src="https://www.gravatar.com/avatar/'.md5(strtolower($email)).'?s=25" alt="'.$this->helper->e($alt).'" title="'.$this->helper->e($alt).'">';
}
return '';
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Integration;
/**
* Hipchat webhook
*
* @package integration
* @author Frederic Guillot
*/
class HipchatWebhook extends \Core\Base
{
/**
* Return true if Hipchat is enabled for this project or globally
*
* @access public
* @param integer $project_id
* @return boolean
*/
public function isActivated($project_id)
{
return $this->config->get('integration_hipchat') == 1 || $this->projectIntegration->hasValue($project_id, 'hipchat', 1);
}
/**
* Get API parameters
*
* @access public
* @param integer $project_id
* @return array
*/
public function getParameters($project_id)
{
if ($this->config->get('integration_hipchat') == 1) {
return array(
'api_url' => $this->config->get('integration_hipchat_api_url'),
'room_id' => $this->config->get('integration_hipchat_room_id'),
'room_token' => $this->config->get('integration_hipchat_room_token'),
);
}
$options = $this->projectIntegration->getParameters($project_id);
return array(
'api_url' => $options['hipchat_api_url'],
'room_id' => $options['hipchat_room_id'],
'room_token' => $options['hipchat_room_token'],
);
}
/**
* Send the notification if activated
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param string $event_name Event name
* @param array $event Event data
*/
public function notify($project_id, $task_id, $event_name, array $event)
{
if ($this->isActivated($project_id)) {
$params = $this->getParameters($project_id);
$project = $this->project->getbyId($project_id);
$event['event_name'] = $event_name;
$event['author'] = $this->user->getFullname($this->session['user']);
$html = '<img src="http://kanboard.net/assets/img/favicon-32x32.png"/>';
$html .= '<strong>'.$project['name'].'</strong>'.(isset($event['task']['title']) ? '<br/>'.$event['task']['title'] : '').'<br/>';
$html .= $this->projectActivity->getTitle($event);
if ($this->config->get('application_url')) {
$html .= '<br/><a href="'.$this->helper->url->href('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id), false, '', true).'">';
$html .= t('view the task on Kanboard').'</a>';
}
$payload = array(
'message' => $html,
'color' => 'yellow',
);
$url = sprintf(
'%s/v2/room/%s/notification?auth_token=%s',
$params['api_url'],
$params['room_id'],
$params['room_token']
);
$this->httpClient->postJson($url, $payload);
}
}
}

View file

@ -0,0 +1,129 @@
<?php
namespace Integration;
use Exception;
use Fabiang\Xmpp\Options;
use Fabiang\Xmpp\Client;
use Fabiang\Xmpp\Protocol\Message;
use Fabiang\Xmpp\Protocol\Presence;
/**
* Jabber
*
* @package integration
* @author Frederic Guillot
*/
class Jabber extends \Core\Base
{
/**
* Return true if Jabber is enabled for this project or globally
*
* @access public
* @param integer $project_id
* @return boolean
*/
public function isActivated($project_id)
{
return $this->config->get('integration_jabber') == 1 || $this->projectIntegration->hasValue($project_id, 'jabber', 1);
}
/**
* Get connection parameters
*
* @access public
* @param integer $project_id
* @return array
*/
public function getParameters($project_id)
{
if ($this->config->get('integration_jabber') == 1) {
return array(
'server' => $this->config->get('integration_jabber_server'),
'domain' => $this->config->get('integration_jabber_domain'),
'username' => $this->config->get('integration_jabber_username'),
'password' => $this->config->get('integration_jabber_password'),
'nickname' => $this->config->get('integration_jabber_nickname'),
'room' => $this->config->get('integration_jabber_room'),
);
}
$options = $this->projectIntegration->getParameters($project_id);
return array(
'server' => $options['jabber_server'],
'domain' => $options['jabber_domain'],
'username' => $options['jabber_username'],
'password' => $options['jabber_password'],
'nickname' => $options['jabber_nickname'],
'room' => $options['jabber_room'],
);
}
/**
* Build and send the message
*
* @access public
* @param integer $project_id Project id
* @param integer $task_id Task id
* @param string $event_name Event name
* @param array $event Event data
*/
public function notify($project_id, $task_id, $event_name, array $event)
{
if ($this->isActivated($project_id)) {
$project = $this->project->getbyId($project_id);
$event['event_name'] = $event_name;
$event['author'] = $this->user->getFullname($this->session['user']);
$payload = '['.$project['name'].'] '.str_replace('&quot;', '"', $this->projectActivity->getTitle($event)).(isset($event['task']['title']) ? ' ('.$event['task']['title'].')' : '');
if ($this->config->get('application_url')) {
$payload .= ' '.$this->helper->url->to('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id), '', true);
}
$this->sendMessage($project_id, $payload);
}
}
/**
* Send message to the XMPP server
*
* @access public
* @param integer $project_id
* @param string $payload
*/
public function sendMessage($project_id, $payload)
{
try {
$params = $this->getParameters($project_id);
$options = new Options($params['server']);
$options->setUsername($params['username']);
$options->setPassword($params['password']);
$options->setTo($params['domain']);
$options->setLogger($this->container['logger']);
$client = new Client($options);
$channel = new Presence;
$channel->setTo($params['room'])->setNickName($params['nickname']);
$client->send($channel);
$message = new Message;
$message->setMessage($payload)
->setTo($params['room'])
->setType(Message::TYPE_GROUPCHAT);
$client->send($message);
$client->disconnect();
}
catch (Exception $e) {
$this->container['logger']->error('Jabber error: '.$e->getMessage());
}
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Integration;
use HTML_To_Markdown;
use Core\Tool;
/**
* Mailgun Integration
*
* @package integration
* @author Frederic Guillot
*/
class Mailgun extends \Core\Base
{
/**
* Send a HTML email
*
* @access public
* @param string $email
* @param string $name
* @param string $subject
* @param string $html
* @param string $author
*/
public function sendEmail($email, $name, $subject, $html, $author)
{
$headers = array(
'Authorization: Basic '.base64_encode('api:'.MAILGUN_API_TOKEN)
);
$payload = array(
'from' => sprintf('%s <%s>', $author, MAIL_FROM),
'to' => sprintf('%s <%s>', $name, $email),
'subject' => $subject,
'html' => $html,
);
$this->httpClient->postForm('https://api.mailgun.net/v3/'.MAILGUN_DOMAIN.'/messages', $payload, $headers);
}
/**
* Parse incoming email
*
* @access public
* @param array $payload Incoming email
* @return boolean
*/
public function receiveEmail(array $payload)
{
if (empty($payload['sender']) || empty($payload['subject']) || empty($payload['recipient'])) {
return false;
}
// The user must exists in Kanboard
$user = $this->user->getByEmail($payload['sender']);
if (empty($user)) {
$this->container['logger']->debug('Mailgun: ignored => user not found');
return false;
}
// The project must have a short name
$project = $this->project->getByIdentifier(Tool::getMailboxHash($payload['recipient']));
if (empty($project)) {
$this->container['logger']->debug('Mailgun: ignored => project not found');
return false;
}
// The user must be member of the project
if (! $this->projectPermission->isMember($project['id'], $user['id'])) {
$this->container['logger']->debug('Mailgun: ignored => user is not member of the project');
return false;
}
// Get the Markdown contents
if (! empty($payload['stripped-html'])) {
$markdown = new HTML_To_Markdown($payload['stripped-html'], array('strip_tags' => true));
$description = $markdown->output();
}
else if (! empty($payload['stripped-text'])) {
$description = $payload['stripped-text'];
}
else {
$description = '';
}
// Finally, we create the task
return (bool) $this->taskCreation->create(array(
'project_id' => $project['id'],
'title' => $payload['subject'],
'description' => $description,
'creator_id' => $user['id'],
));
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Integration;
use HTML_To_Markdown;
/**
* Postmark integration
*
* @package integration
* @author Frederic Guillot
*/
class Postmark extends \Core\Base
{
/**
* Send a HTML email
*
* @access public
* @param string $email
* @param string $name
* @param string $subject
* @param string $html
* @param string $author
*/
public function sendEmail($email, $name, $subject, $html, $author)
{
$headers = array(
'Accept: application/json',
'X-Postmark-Server-Token: '.POSTMARK_API_TOKEN,
);
$payload = array(
'From' => sprintf('%s <%s>', $author, MAIL_FROM),
'To' => sprintf('%s <%s>', $name, $email),
'Subject' => $subject,
'HtmlBody' => $html,
);
$this->httpClient->postJson('https://api.postmarkapp.com/email', $payload, $headers);
}
/**
* Parse incoming email
*
* @access public
* @param array $payload Incoming email
* @return boolean
*/
public function receiveEmail(array $payload)
{
if (empty($payload['From']) || empty($payload['Subject']) || empty($payload['MailboxHash'])) {
return false;
}
// The user must exists in Kanboard
$user = $this->user->getByEmail($payload['From']);
if (empty($user)) {
$this->container['logger']->debug('Postmark: ignored => user not found');
return false;
}
// The project must have a short name
$project = $this->project->getByIdentifier($payload['MailboxHash']);
if (empty($project)) {
$this->container['logger']->debug('Postmark: ignored => project not found');
return false;
}
// The user must be member of the project
if (! $this->projectPermission->isMember($project['id'], $user['id'])) {
$this->container['logger']->debug('Postmark: ignored => user is not member of the project');
return false;
}
// Get the Markdown contents
if (! empty($payload['HtmlBody'])) {
$markdown = new HTML_To_Markdown($payload['HtmlBody'], array('strip_tags' => true));
$description = $markdown->output();
}
else if (! empty($payload['TextBody'])) {
$description = $payload['TextBody'];
}
else {
$description = '';
}
// Finally, we create the task
return (bool) $this->taskCreation->create(array(
'project_id' => $project['id'],
'title' => $payload['Subject'],
'description' => $description,
'creator_id' => $user['id'],
));
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace Integration;
use HTML_To_Markdown;
use Core\Tool;
/**
* Sendgrid Integration
*
* @package integration
* @author Frederic Guillot
*/
class Sendgrid extends \Core\Base
{
/**
* Send a HTML email
*
* @access public
* @param string $email
* @param string $name
* @param string $subject
* @param string $html
* @param string $author
*/
public function sendEmail($email, $name, $subject, $html, $author)
{
$payload = array(
'api_user' => SENDGRID_API_USER,
'api_key' => SENDGRID_API_KEY,
'to' => $email,
'toname' => $name,
'from' => MAIL_FROM,
'fromname' => $author,
'html' => $html,
'subject' => $subject,
);
$this->httpClient->postForm('https://api.sendgrid.com/api/mail.send.json', $payload);
}
/**
* Parse incoming email
*
* @access public
* @param array $payload Incoming email
* @return boolean
*/
public function receiveEmail(array $payload)
{
if (empty($payload['envelope']) || empty($payload['subject'])) {
return false;
}
$envelope = json_decode($payload['envelope'], true);
$sender = isset($envelope['to'][0]) ? $envelope['to'][0] : '';
// The user must exists in Kanboard
$user = $this->user->getByEmail($envelope['from']);
if (empty($user)) {
$this->container['logger']->debug('SendgridWebhook: ignored => user not found');
return false;
}
// The project must have a short name
$project = $this->project->getByIdentifier(Tool::getMailboxHash($sender));
if (empty($project)) {
$this->container['logger']->debug('SendgridWebhook: ignored => project not found');
return false;
}
// The user must be member of the project
if (! $this->projectPermission->isMember($project['id'], $user['id'])) {
$this->container['logger']->debug('SendgridWebhook: ignored => user is not member of the project');
return false;
}
// Get the Markdown contents
if (! empty($payload['html'])) {
$markdown = new HTML_To_Markdown($payload['html'], array('strip_tags' => true));
$description = $markdown->output();
}
else if (! empty($payload['text'])) {
$description = $payload['text'];
}
else {
$description = '';
}
// Finally, we create the task
return (bool) $this->taskCreation->create(array(
'project_id' => $project['id'],
'title' => $payload['subject'],
'description' => $description,
'creator_id' => $user['id'],
));
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Integration;
use Swift_Message;
use Swift_Mailer;
use Swift_MailTransport;
use Swift_SendmailTransport;
use Swift_SmtpTransport;
use Swift_TransportException;
/**
* Smtp
*
* @package integration
* @author Frederic Guillot
*/
class Smtp extends \Core\Base
{
/**
* Send a HTML email
*
* @access public
* @param string $email
* @param string $name
* @param string $subject
* @param string $html
* @param string $author
*/
public function sendEmail($email, $name, $subject, $html, $author)
{
try {
$message = Swift_Message::newInstance()
->setSubject($subject)
->setFrom(array(MAIL_FROM => $author))
->setBody($html, 'text/html')
->setTo(array($email => $name));
Swift_Mailer::newInstance($this->getTransport())->send($message);
}
catch (Swift_TransportException $e) {
$this->container['logger']->error($e->getMessage());
}
}
/**
* Get SwiftMailer transport
*
* @access private
* @return \Swift_Transport
*/
private function getTransport()
{
switch (MAIL_TRANSPORT) {
case 'smtp':
$transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT);
$transport->setUsername(MAIL_SMTP_USERNAME);
$transport->setPassword(MAIL_SMTP_PASSWORD);
$transport->setEncryption(MAIL_SMTP_ENCRYPTION);
break;
case 'sendmail':
$transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND);
break;
default:
$transport = Swift_MailTransport::newInstance();
}
return $transport;
}
}

View file

@ -0,0 +1,227 @@
<?php
/**
* A Compatibility library with PHP 5.5's simplified password hashing API.
*
* @author Anthony Ferrara <ircmaxell@php.net>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @copyright 2012 The Authors
*/
if (!defined('PASSWORD_BCRYPT')) {
define('PASSWORD_BCRYPT', 1);
define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
if (version_compare(PHP_VERSION, '5.3.7', '<')) {
define('PASSWORD_PREFIX', '$2a$');
}
else {
define('PASSWORD_PREFIX', '$2y$');
}
/**
* Hash the password using the specified algorithm
*
* @param string $password The password to hash
* @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
* @param array $options The options for the algorithm to use
*
* @return string|false The hashed password, or false on error.
*/
function password_hash($password, $algo, array $options = array()) {
if (!function_exists('crypt')) {
trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
return null;
}
if (!is_string($password)) {
trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
return null;
}
if (!is_int($algo)) {
trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
return null;
}
switch ($algo) {
case PASSWORD_BCRYPT:
// Note that this is a C constant, but not exposed to PHP, so we don't define it here.
$cost = 10;
if (isset($options['cost'])) {
$cost = $options['cost'];
if ($cost < 4 || $cost > 31) {
trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
return null;
}
}
$required_salt_len = 22;
$hash_format = sprintf("%s%02d$", PASSWORD_PREFIX, $cost);
break;
default:
trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
return null;
}
if (isset($options['salt'])) {
switch (gettype($options['salt'])) {
case 'NULL':
case 'boolean':
case 'integer':
case 'double':
case 'string':
$salt = (string) $options['salt'];
break;
case 'object':
if (method_exists($options['salt'], '__tostring')) {
$salt = (string) $options['salt'];
break;
}
case 'array':
case 'resource':
default:
trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
return null;
}
if (strlen($salt) < $required_salt_len) {
trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
return null;
} elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
$salt = str_replace('+', '.', base64_encode($salt));
}
} else {
$buffer = '';
$raw_length = (int) ($required_salt_len * 3 / 4 + 1);
$buffer_valid = false;
if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
$buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
if ($buffer) {
$buffer_valid = true;
}
}
if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
$buffer = openssl_random_pseudo_bytes($raw_length);
if ($buffer) {
$buffer_valid = true;
}
}
if (!$buffer_valid && is_readable('/dev/urandom')) {
$f = fopen('/dev/urandom', 'r');
$read = strlen($buffer);
while ($read < $raw_length) {
$buffer .= fread($f, $raw_length - $read);
$read = strlen($buffer);
}
fclose($f);
if ($read >= $raw_length) {
$buffer_valid = true;
}
}
if (!$buffer_valid || strlen($buffer) < $raw_length) {
$bl = strlen($buffer);
for ($i = 0; $i < $raw_length; $i++) {
if ($i < $bl) {
$buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
} else {
$buffer .= chr(mt_rand(0, 255));
}
}
}
$salt = str_replace('+', '.', base64_encode($buffer));
}
$salt = substr($salt, 0, $required_salt_len);
$hash = $hash_format . $salt;
$ret = crypt($password, $hash);
if (!is_string($ret) || strlen($ret) <= 13) {
return false;
}
return $ret;
}
/**
* Get information about the password hash. Returns an array of the information
* that was used to generate the password hash.
*
* array(
* 'algo' => 1,
* 'algoName' => 'bcrypt',
* 'options' => array(
* 'cost' => 10,
* ),
* )
*
* @param string $hash The password hash to extract info from
*
* @return array The array of information about the hash.
*/
function password_get_info($hash) {
$return = array(
'algo' => 0,
'algoName' => 'unknown',
'options' => array(),
);
if (substr($hash, 0, 4) == PASSWORD_PREFIX && strlen($hash) == 60) {
$return['algo'] = PASSWORD_BCRYPT;
$return['algoName'] = 'bcrypt';
list($cost) = sscanf($hash, PASSWORD_PREFIX."%d$");
$return['options']['cost'] = $cost;
}
return $return;
}
/**
* Determine if the password hash needs to be rehashed according to the options provided
*
* If the answer is true, after validating the password using password_verify, rehash it.
*
* @param string $hash The hash to test
* @param int $algo The algorithm used for new password hashes
* @param array $options The options array passed to password_hash
*
* @return boolean True if the password needs to be rehashed.
*/
function password_needs_rehash($hash, $algo, array $options = array()) {
$info = password_get_info($hash);
if ($info['algo'] != $algo) {
return true;
}
switch ($algo) {
case PASSWORD_BCRYPT:
$cost = isset($options['cost']) ? $options['cost'] : 10;
if ($cost != $info['options']['cost']) {
return true;
}
break;
}
return false;
}
/**
* Verify a password against a hash using a timing attack resistant approach
*
* @param string $password The password to verify
* @param string $hash The hash to verify against
*
* @return boolean If the password matches the hash
*/
function password_verify($password, $hash) {
if (!function_exists('crypt')) {
trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
return false;
}
$ret = crypt($password, $hash);
if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
return false;
}
$status = 0;
for ($i = 0; $i < strlen($ret); $i++) {
$status |= (ord($ret[$i]) ^ ord($hash[$i]));
}
return $status === 0;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,196 @@
<?php
namespace Model;
/**
* Project Daily Column Stats
*
* @package model
* @author Frederic Guillot
*/
class ProjectDailyColumnStats extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_daily_column_stats';
/**
* Update daily totals for the project and foreach column
*
* "total" is the number open of tasks in the column
* "score" is the sum of tasks score in the column
*
* @access public
* @param integer $project_id Project id
* @param string $date Record date (YYYY-MM-DD)
* @return boolean
*/
public function updateTotals($project_id, $date)
{
$status = $this->config->get('cfd_include_closed_tasks') == 1 ? array(Task::STATUS_OPEN, Task::STATUS_CLOSED) : array(Task::STATUS_OPEN);
return $this->db->transaction(function($db) use ($project_id, $date, $status) {
$column_ids = $db->table(Board::TABLE)->eq('project_id', $project_id)->findAllByColumn('id');
foreach ($column_ids as $column_id) {
// This call will fail if the record already exists
// (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
$db->table(ProjectDailyColumnStats::TABLE)->insert(array(
'day' => $date,
'project_id' => $project_id,
'column_id' => $column_id,
'total' => 0,
'score' => 0,
));
$db->table(ProjectDailyColumnStats::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
->eq('day', $date)
->update(array(
'score' => $db->table(Task::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
->eq('is_active', Task::STATUS_OPEN)
->sum('score'),
'total' => $db->table(Task::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
->in('is_active', $status)
->count()
));
}
});
}
/**
* Count the number of recorded days for the data range
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @return integer
*/
public function countDays($project_id, $from, $to)
{
$rq = $this->db->execute(
'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ? AND project_id=?',
array($from, $to, $project_id)
);
return $rq !== false ? $rq->fetchColumn(0) : 0;
}
/**
* Get raw metrics for the project within a data range
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @return array
*/
public function getRawMetrics($project_id, $from, $to)
{
return $this->db->table(ProjectDailyColumnStats::TABLE)
->columns(
ProjectDailyColumnStats::TABLE.'.column_id',
ProjectDailyColumnStats::TABLE.'.day',
ProjectDailyColumnStats::TABLE.'.total',
ProjectDailyColumnStats::TABLE.'.score',
Board::TABLE.'.title AS column_title'
)
->join(Board::TABLE, 'id', 'column_id')
->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
->asc(ProjectDailyColumnStats::TABLE.'.day')
->findAll();
}
/**
* Get raw metrics for the project within a data range grouped by day
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @return array
*/
public function getRawMetricsByDay($project_id, $from, $to)
{
return $this->db->table(ProjectDailyColumnStats::TABLE)
->columns(
ProjectDailyColumnStats::TABLE.'.day',
'SUM('.ProjectDailyColumnStats::TABLE.'.total) AS total',
'SUM('.ProjectDailyColumnStats::TABLE.'.score) AS score'
)
->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
->asc(ProjectDailyColumnStats::TABLE.'.day')
->groupBy(ProjectDailyColumnStats::TABLE.'.day')
->findAll();
}
/**
* Get aggregated metrics for the project within a data range
*
* [
* ['Date', 'Column1', 'Column2'],
* ['2014-11-16', 2, 5],
* ['2014-11-17', 20, 15],
* ]
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @param string $column Column to aggregate
* @return array
*/
public function getAggregatedMetrics($project_id, $from, $to, $column = 'total')
{
$columns = $this->board->getColumnsList($project_id);
$column_ids = array_keys($columns);
$metrics = array(array_merge(array(e('Date')), array_values($columns)));
$aggregates = array();
// Fetch metrics for the project
$records = $this->db->table(ProjectDailyColumnStats::TABLE)
->eq('project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
->findAll();
// Aggregate by day
foreach ($records as $record) {
if (! isset($aggregates[$record['day']])) {
$aggregates[$record['day']] = array($record['day']);
}
$aggregates[$record['day']][$record['column_id']] = $record[$column];
}
// Aggregate by row
foreach ($aggregates as $aggregate) {
$row = array($aggregate[0]);
foreach ($column_ids as $column_id) {
$row[] = (int) $aggregate[$column_id];
}
$metrics[] = $row;
}
return $metrics;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Model;
/**
* Project Daily Stats
*
* @package model
* @author Frederic Guillot
*/
class ProjectDailyStats extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_daily_stats';
/**
* Update daily totals for the project
*
* @access public
* @param integer $project_id Project id
* @param string $date Record date (YYYY-MM-DD)
* @return boolean
*/
public function updateTotals($project_id, $date)
{
$lead_cycle_time = $this->projectAnalytic->getAverageLeadAndCycleTime($project_id);
return $this->db->transaction(function($db) use ($project_id, $date, $lead_cycle_time) {
// This call will fail if the record already exists
// (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
$db->table(ProjectDailyStats::TABLE)->insert(array(
'day' => $date,
'project_id' => $project_id,
'avg_lead_time' => 0,
'avg_cycle_time' => 0,
));
$db->table(ProjectDailyStats::TABLE)
->eq('project_id', $project_id)
->eq('day', $date)
->update(array(
'avg_lead_time' => $lead_cycle_time['avg_lead_time'],
'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'],
));
});
}
/**
* Get raw metrics for the project within a data range
*
* @access public
* @param integer $project_id Project id
* @param string $from Start date (ISO format YYYY-MM-DD)
* @param string $to End date
* @return array
*/
public function getRawMetrics($project_id, $from, $to)
{
return $this->db->table(self::TABLE)
->columns('day', 'avg_lead_time', 'avg_cycle_time')
->eq(self::TABLE.'.project_id', $project_id)
->gte('day', $from)
->lte('day', $to)
->asc(self::TABLE.'.day')
->findAll();
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Model;
/**
* Project integration
*
* @package model
* @author Frederic Guillot
*/
class ProjectIntegration extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'project_integrations';
/**
* Get all parameters for a project
*
* @access public
* @param integer $project_id
* @return array
*/
public function getParameters($project_id)
{
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->findOne() ?: array();
}
/**
* Save parameters for a project
*
* @access public
* @param integer $project_id
* @param array $values
* @return boolean
*/
public function saveParameters($project_id, array $values)
{
if ($this->db->table(self::TABLE)->eq('project_id', $project_id)->exists()) {
return $this->db->table(self::TABLE)->eq('project_id', $project_id)->update($values);
}
return $this->db->table(self::TABLE)->insert($values + array('project_id' => $project_id));
}
/**
* Check if a project has the given parameter/value
*
* @access public
* @param integer $project_id
* @param string $option
* @param string $value
* @return boolean
*/
public function hasValue($project_id, $option, $value)
{
return $this->db
->table(self::TABLE)
->eq('project_id', $project_id)
->eq($option, $value)
->exists();
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Model;
/**
* Task Analytic
*
* @package model
* @author Frederic Guillot
*/
class TaskAnalytic extends Base
{
/**
* Get the time between date_creation and date_completed or now if empty
*
* @access public
* @param array $task
* @return integer
*/
public function getLeadTime(array $task)
{
return ($task['date_completed'] ?: time()) - $task['date_creation'];
}
/**
* Get the time between date_started and date_completed or now if empty
*
* @access public
* @param array $task
* @return integer
*/
public function getCycleTime(array $task)
{
if (empty($task['date_started'])) {
return 0;
}
return ($task['date_completed'] ?: time()) - $task['date_started'];
}
/**
* Get the average time spent in each column
*
* @access public
* @param array $task
* @return array
*/
public function getTimeSpentByColumn(array $task)
{
$result = array();
$columns = $this->board->getColumnsList($task['project_id']);
$sums = $this->transition->getTimeSpentByTask($task['id']);
foreach ($columns as $column_id => $column_title) {
$time_spent = isset($sums[$column_id]) ? $sums[$column_id] : 0;
if ($task['column_id'] == $column_id) {
$time_spent += ($task['date_completed'] ?: time()) - $task['date_moved'];
}
$result[] = array(
'id' => $column_id,
'title' => $column_title,
'time_spent' => $time_spent,
);
}
return $result;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Subscriber;
use Event\TaskEvent;
use Model\Task;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class RecurringTaskSubscriber extends \Core\Base implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
Task::EVENT_MOVE_COLUMN => array('onMove', 0),
Task::EVENT_CLOSE => array('onClose', 0),
);
}
public function onMove(TaskEvent $event)
{
if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING) {
if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->board->getFirstColumn($event['project_id']) == $event['src_column_id']) {
$this->taskDuplication->duplicateRecurringTask($event['task_id']);
}
else if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_LAST_COLUMN && $this->board->getLastColumn($event['project_id']) == $event['dst_column_id']) {
$this->taskDuplication->duplicateRecurringTask($event['task_id']);
}
}
}
public function onClose(TaskEvent $event)
{
if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == Task::RECURRING_TRIGGER_CLOSE) {
$this->taskDuplication->duplicateRecurringTask($event['task_id']);
}
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Subscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Model\Subtask;
use Event\SubtaskEvent;
class SubtaskTimeTrackingSubscriber extends \Core\Base implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
Subtask::EVENT_CREATE => array('updateTaskTime', 0),
Subtask::EVENT_UPDATE => array(
array('logStartEnd', 10),
array('updateTaskTime', 0),
)
);
}
public function updateTaskTime(SubtaskEvent $event)
{
if (isset($event['task_id'])) {
$this->subtaskTimeTracking->updateTaskTimeTracking($event['task_id']);
}
}
public function logStartEnd(SubtaskEvent $event)
{
if (isset($event['status']) && $this->config->get('subtask_time_tracking') == 1) {
$subtask = $this->subtask->getById($event['id']);
if (empty($subtask['user_id'])) {
return false;
}
if ($subtask['status'] == Subtask::STATUS_INPROGRESS) {
return $this->subtaskTimeTracking->logStartTime($subtask['id'], $subtask['user_id']);
}
else {
return $this->subtaskTimeTracking->logEndTime($subtask['id'], $subtask['user_id']);
}
}
}
}

View file

@ -0,0 +1,40 @@
<section id="main">
<div class="page-header">
<ul>
<li>
<span class="dropdown">
<span>
<i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
<ul>
<?= $this->render('project/dropdown', array('project' => $project)) ?>
</ul>
</span>
</span>
</li>
<li>
<i class="fa fa-th fa-fw"></i>
<?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
</li>
<li>
<i class="fa fa-calendar fa-fw"></i>
<?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
</li>
<?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
<li>
<i class="fa fa-cog fa-fw"></i>
<?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
</li>
<?php endif ?>
<li>
<i class="fa fa-folder fa-fw"></i>
<?= $this->url->link(t('All projects'), 'project', 'index') ?>
</li>
<?php if ($project['is_public']): ?>
<li><i class="fa fa-rss-square fa-fw"></i><?= $this->url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?></li>
<li><i class="fa fa-calendar fa-fw"></i><?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li>
<?php endif ?>
</ul>
</div>
<?= $this->render('event/events', array('events' => $events)) ?>
</section>

View file

@ -0,0 +1,5 @@
<div class="page-header">
<h2><?= t('Activity stream') ?></h2>
</div>
<?= $this->render('event/events', array('events' => $events)) ?>

View file

@ -0,0 +1,29 @@
<div class="page-header">
<h2><?= t('Average time spent into each column') ?></h2>
</div>
<?php if (empty($metrics)): ?>
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
<?php else: ?>
<section id="analytic-avg-time-column">
<div id="chart" data-metrics='<?= json_encode($metrics, JSON_HEX_APOS) ?>' data-label="<?= t('Average time spent') ?>"></div>
<table class="table-stripped">
<tr>
<th><?= t('Column') ?></th>
<th><?= t('Average time spent') ?></th>
</tr>
<?php foreach ($metrics as $column): ?>
<tr>
<td><?= $this->e($column['title']) ?></td>
<td><?= $this->dt->duration($column['average']) ?></td>
</tr>
<?php endforeach ?>
</table>
<p class="alert alert-info">
<?= t('This chart show the average time spent into each column for the last %d tasks.', 1000) ?>
</p>
</section>
<?php endif ?>

View file

@ -0,0 +1,34 @@
<div class="page-header">
<h2><?= t('Burndown chart') ?></h2>
</div>
<?php if (! $display_graph): ?>
<p class="alert"><?= t('You need at least 2 days of data to show the chart.') ?></p>
<?php else: ?>
<section id="analytic-burndown">
<div id="chart" data-metrics='<?= json_encode($metrics, JSON_HEX_APOS) ?>' data-date-format="<?= e('%%Y-%%m-%%d') ?>" data-label-total="<?= t('Total for all columns') ?>"></div>
</section>
<?php endif ?>
<hr/>
<form method="post" class="form-inline" action="<?= $this->url->href('analytic', 'burndown', array('project_id' => $project['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<div class="form-inline-group">
<?= $this->form->label(t('Start Date'), 'from') ?>
<?= $this->form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
</div>
<div class="form-inline-group">
<?= $this->form->label(t('End Date'), 'to') ?>
<?= $this->form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
</div>
<div class="form-inline-group">
<input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
</div>
</form>
<p class="alert alert-info"><?= t('This chart show the task complexity over the time (Work Remaining).') ?></p>

View file

@ -0,0 +1,42 @@
<div class="page-header">
<h2><?= t('Average Lead and Cycle time') ?></h2>
</div>
<div class="listing">
<ul>
<li><?= t('Average lead time: ').'<strong>'.$this->dt->duration($average['avg_lead_time']) ?></strong></li>
<li><?= t('Average cycle time: ').'<strong>'.$this->dt->duration($average['avg_cycle_time']) ?></strong></li>
</ul>
</div>
<?php if (empty($metrics)): ?>
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
<?php else: ?>
<section id="analytic-lead-cycle-time">
<div id="chart" data-metrics='<?= json_encode($metrics, JSON_HEX_APOS) ?>' data-label-cycle="<?= t('Cycle Time') ?>" data-label-lead="<?= t('Lead Time') ?>"></div>
<form method="post" class="form-inline" action="<?= $this->url->href('analytic', 'leadAndCycleTime', array('project_id' => $project['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<div class="form-inline-group">
<?= $this->form->label(t('Start Date'), 'from') ?>
<?= $this->form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
</div>
<div class="form-inline-group">
<?= $this->form->label(t('End Date'), 'to') ?>
<?= $this->form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
</div>
<div class="form-inline-group">
<input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
</div>
</form>
<p class="alert alert-info">
<?= t('This chart show the average lead and cycle time for the last %d tasks over the time.', 1000) ?>
</p>
</section>
<?php endif ?>

View file

@ -0,0 +1,4 @@
<div class="page-header">
<h2><?= t('My activity stream') ?></h2>
</div>
<?= $this->render('event/events', array('events' => $events)) ?>

View file

@ -0,0 +1,5 @@
<div id="calendar"
data-check-url="<?= $this->url->href('calendar', 'user', array('user_id' => $user['id'])) ?>"
data-save-url="<?= $this->url->href('calendar', 'save') ?>"
>
</div>

View file

@ -0,0 +1,18 @@
<div class="dropdown filters">
<i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Filters') ?></a>
<ul>
<li><a href="#" class="filter-helper" data-filter="<?= isset($reset) ? $reset : '' ?>" title="<?= t('Keyboard shortcut: "%s"', 'r') ?>"><?= t('Reset filters') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:open assignee:me"><?= t('My tasks') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:open assignee:me due:tomorrow"><?= t('My tasks due tomorrow') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:open due:today"><?= t('Tasks due today') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:open due:tomorrow"><?= t('Tasks due tomorrow') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:open due:yesterday"><?= t('Tasks due yesterday') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:closed"><?= t('Closed tasks') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:open"><?= t('Open tasks') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:open assignee:nobody"><?= t('Not assigned') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:open category:none"><?= t('No category') ?></a></li>
<li>
<?= $this->url->doc(t('View advanced search syntax'), 'search') ?>
</li>
</ul>
</div>

View file

@ -0,0 +1,40 @@
<section id="main">
<div class="page-header page-header-mobile">
<ul>
<?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
<li>
<i class="fa fa-plus fa-fw"></i>
<?= $this->url->link(t('New project'), 'project', 'create') ?>
</li>
<?php endif ?>
<li>
<i class="fa fa-lock fa-fw"></i>
<?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?>
</li>
<li>
<i class="fa fa-search fa-fw"></i>
<?= $this->url->link(t('Search'), 'search', 'index') ?>
</li>
<li>
<i class="fa fa-folder fa-fw"></i>
<?= $this->url->link(t('Project management'), 'project', 'index') ?>
</li>
<?php if ($this->user->isAdmin()): ?>
<li>
<i class="fa fa-user fa-fw"></i>
<?= $this->url->link(t('User management'), 'user', 'index') ?>
</li>
<li>
<i class="fa fa-cog fa-fw"></i>
<?= $this->url->link(t('Settings'), 'config', 'index') ?>
</li>
<?php endif ?>
</ul>
</div>
<section class="sidebar-container" id="dashboard">
<?= $this->render('app/sidebar', array('user' => $user)) ?>
<div class="sidebar-content">
<?= $content_for_sublayout ?>
</div>
</section>
</section>

View file

@ -0,0 +1,13 @@
<div class="search">
<form method="get" action="<?= $this->url->dir() ?>" class="search">
<?= $this->form->hidden('controller', array('controller' => 'search')) ?>
<?= $this->form->hidden('action', array('action' => 'index')) ?>
<?= $this->form->text('search', array(), array(), array('placeholder="'.t('Search').'"'), 'form-input-large') ?>
</form>
<?= $this->render('app/filters_helper') ?>
</div>
<?= $this->render('app/projects', array('paginator' => $project_paginator)) ?>
<?= $this->render('app/tasks', array('paginator' => $task_paginator)) ?>
<?= $this->render('app/subtasks', array('paginator' => $subtask_paginator)) ?>

View file

@ -0,0 +1,25 @@
<div class="sidebar">
<h2><?= $this->e($user['name'] ?: $user['username']) ?></h2>
<ul>
<li <?= $this->app->getRouterAction() === 'index' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Overview'), 'app', 'index', array('user_id' => $user['id'])) ?>
</li>
<li <?= $this->app->getRouterAction() === 'projects' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('My projects'), 'app', 'projects', array('user_id' => $user['id'])) ?>
</li>
<li <?= $this->app->getRouterAction() === 'tasks' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('My tasks'), 'app', 'tasks', array('user_id' => $user['id'])) ?>
</li>
<li <?= $this->app->getRouterAction() === 'subtasks' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('My subtasks'), 'app', 'subtasks', array('user_id' => $user['id'])) ?>
</li>
<li <?= $this->app->getRouterAction() === 'calendar' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('My calendar'), 'app', 'calendar', array('user_id' => $user['id'])) ?>
</li>
<li <?= $this->app->getRouterAction() === 'activity' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('My activity stream'), 'app', 'activity', array('user_id' => $user['id'])) ?>
</li>
</ul>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
</div>

View file

@ -0,0 +1,50 @@
<div class="form-login">
<?php if (isset($errors['login'])): ?>
<p class="alert alert-error"><?= $this->e($errors['login']) ?></p>
<?php endif ?>
<?php if (! HIDE_LOGIN_FORM): ?>
<form method="post" action="<?= $this->url->href('auth', 'check') ?>">
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Username'), 'username') ?>
<?= $this->form->text('username', $values, $errors, array('autofocus', 'required')) ?>
<?= $this->form->label(t('Password'), 'password') ?>
<?= $this->form->password('password', $values, $errors, array('required')) ?>
<?php if (isset($captcha) && $captcha): ?>
<?= $this->form->label(t('Enter the text below'), 'captcha') ?>
<img src="<?= $this->url->href('auth', 'captcha') ?>"/>
<?= $this->form->text('captcha', $values, $errors, array('required')) ?>
<?php endif ?>
<?php if (REMEMBER_ME_AUTH): ?>
<?= $this->form->checkbox('remember_me', t('Remember Me'), 1, true) ?><br/>
<?php endif ?>
<div class="form-actions">
<input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
</div>
</form>
<?php endif ?>
<?php if (GOOGLE_AUTH || GITHUB_AUTH || GITLAB_AUTH): ?>
<ul class="no-bullet">
<?php if (GOOGLE_AUTH): ?>
<li><?= $this->url->link(t('Login with my Google Account'), 'oauth', 'google') ?></li>
<?php endif ?>
<?php if (GITHUB_AUTH): ?>
<li><?= $this->url->link(t('Login with my Github Account'), 'oauth', 'github') ?></li>
<?php endif ?>
<?php if (GITLAB_AUTH): ?>
<li><?= $this->url->link(t('Login with my Gitlab Account'), 'oauth', 'gitlab') ?></li>
<?php endif ?>
</ul>
<?php endif ?>
</div>

View file

@ -0,0 +1,21 @@
<section id="main">
<section>
<h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3>
<form method="post" action="<?= $this->url->href('board', 'updateAssignee', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?>
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Assignee'), 'owner_id') ?>
<?= $this->form->select('owner_id', $users_list, $values, array(), array('autofocus')) ?><br/>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</form>
</section>
</section>

View file

@ -0,0 +1,21 @@
<section id="main">
<section>
<h3><?= t('Change category for the task "%s"', $values['title']) ?></h3>
<form method="post" action="<?= $this->url->href('board', 'updateCategory', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?>
<?= $this->form->hidden('project_id', $values) ?>
<?= $this->form->label(t('Category'), 'category_id') ?>
<?= $this->form->select('category_id', $categories_list, $values, array(), array('autofocus')) ?><br/>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
</div>
</form>
</section>
</section>

View file

@ -0,0 +1,18 @@
<section id="main">
<?= $this->render('project/filters', array(
'project' => $project,
'filters' => $filters,
'categories_list' => $categories_list,
'users_list' => $users_list,
'is_board' => true,
)) ?>
<?= $this->render('board/table_container', array(
'project' => $project,
'swimlanes' => $swimlanes,
'board_private_refresh_interval' => $board_private_refresh_interval,
'board_highlight_period' => $board_highlight_period,
)) ?>
</section>

View file

@ -0,0 +1,11 @@
<section id="main" class="public-board">
<?= $this->render('board/table_container', array(
'project' => $project,
'swimlanes' => $swimlanes,
'board_private_refresh_interval' => $board_private_refresh_interval,
'board_highlight_period' => $board_highlight_period,
'not_editable' => true,
)) ?>
</section>

View file

@ -0,0 +1,31 @@
<div id="board-container">
<?php if (isset($not_editable)): ?>
<table id="board" class="board-project-<?= $project['id'] ?>">
<?php else: ?>
<table id="board"
class="board-project-<?= $project['id'] ?>"
data-project-id="<?= $project['id'] ?>"
data-check-interval="<?= $board_private_refresh_interval ?>"
data-save-url="<?= $this->url->href('board', 'save', array('project_id' => $project['id'])) ?>"
data-reload-url="<?= $this->url->href('board', 'reload', array('project_id' => $project['id'])) ?>"
data-check-url="<?= $this->url->href('board', 'check', array('project_id' => $project['id'], 'timestamp' => time())) ?>"
data-task-creation-url="<?= $this->url->href('taskcreation', 'create', array('project_id' => $project['id'])) ?>"
>
<?php endif ?>
<?php foreach ($swimlanes as $swimlane): ?>
<?php if (empty($swimlane['columns'])): ?>
<p class="alert alert-error"><?= t('There is no column in your project!') ?></p>
<?php break ?>
<?php else: ?>
<?= $this->render('board/table_swimlane', array(
'project' => $project,
'swimlane' => $swimlane,
'board_highlight_period' => $board_highlight_period,
'hide_swimlane' => count($swimlanes) === 1,
'not_editable' => isset($not_editable),
)) ?>
<?php endif ?>
<?php endforeach ?>
</table>
</div>

View file

@ -0,0 +1,94 @@
<tr id="swimlane-<?= $swimlane['id'] ?>">
<!-- swimlane toggle -->
<?php if (! $hide_swimlane): ?>
<th class="board-swimlane-header">
<?php if (! $not_editable): ?>
<a href="#" class="board-swimlane-toggle" data-swimlane-id="<?= $swimlane['id'] ?>">
<i class="fa fa-minus-circle hide-icon-swimlane-<?= $swimlane['id'] ?>"></i>
<i class="fa fa-plus-circle show-icon-swimlane-<?= $swimlane['id'] ?>" style="display: none"></i>
</a>
<span class="board-swimlane-toggle-title show-icon-swimlane-<?= $swimlane['id'] ?>"><?= $this->e($swimlane['name']) ?></span>
<?php endif ?>
</th>
<?php endif ?>
<!-- column header title -->
<?php foreach ($swimlane['columns'] as $column): ?>
<th class="board-column-header board-column-header-<?= $column['id'] ?>" data-column-id="<?= $column['id'] ?>">
<div class="board-column-collapsed">
<span title="<?= t('Task count') ?>" class="board-column-header-task-count" title="<?= t('Show this column') ?>">
<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>
</span>
</div>
<div class="board-column-expanded">
<?php if (! $not_editable): ?>
<div class="board-add-icon">
<?= $this->url->link('+', 'taskcreation', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover', t('Add a new task')) ?>
</div>
<?php endif ?>
<span class="board-column-title" data-column-id="<?= $column['id'] ?>" title="<?= t('Hide this column') ?>">
<?= $this->e($column['title']) ?>
</span>
<?php if (! $not_editable && ! empty($column['description'])): ?>
<span class="tooltip pull-right" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
<i class="fa fa-info-circle"></i>
</span>
<?php endif ?>
<?php if (! empty($column['score'])): ?>
<span class="pull-right" title="<?= t('Score') ?>">
<?= $column['score'] ?>&nbsp;
</span>
<?php endif ?>
<?php if ($column['task_limit']): ?>
<span title="<?= t('Task limit') ?>">
(<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>/<?= $this->e($column['task_limit']) ?>)
</span>
<?php else: ?>
<span title="<?= t('Task count') ?>" class="board-column-header-task-count">
(<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>)
</span>
<?php endif ?>
</div>
</th>
<?php endforeach ?>
</tr>
<tr class="board-swimlane swimlane-row-<?= $swimlane['id'] ?>">
<!-- swimlane title -->
<?php if (! $hide_swimlane): ?>
<th class="board-swimlane-title">
<?= $this->e($swimlane['name']) ?>
<div title="<?= t('Task count') ?>" class="board-column-header-task-count">
(<span><?= $swimlane['nb_tasks'] ?></span>)
</div>
</th>
<?php endif ?>
<!-- task list -->
<?php foreach ($swimlane['columns'] as $column): ?>
<td class="board-column-<?= $column['id'] ?> <?= $column['task_limit'] && $column['nb_tasks'] > $column['task_limit'] ? 'board-task-list-limit' : '' ?>">
<div class="board-task-list board-column-expanded" data-column-id="<?= $column['id'] ?>" data-swimlane-id="<?= $swimlane['id'] ?>" data-task-limit="<?= $column['task_limit'] ?>">
<?php foreach ($column['tasks'] as $task): ?>
<?= $this->render($not_editable ? 'board/task_public' : 'board/task_private', array(
'project' => $project,
'task' => $task,
'board_highlight_period' => $board_highlight_period,
'not_editable' => $not_editable,
)) ?>
<?php endforeach ?>
</div>
<div class="board-column-collapsed">
<div class="board-rotation-wrapper">
<div class="board-column-title board-rotation" data-column-id="<?= $column['id'] ?>" title="<?= t('Show this column') ?>">
<?= $this->e($column['title']) ?>
</div>
</div>
</div>
</td>
<?php endforeach ?>
</tr>

View file

@ -0,0 +1,16 @@
<section>
<?php foreach ($comments as $comment): ?>
<p class="comment-title">
<?php if (! empty($comment['username'])): ?>
<span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @
<?php endif ?>
<span class="comment-date"><?= dt('%b %e, %Y, %k:%M %p', $comment['date_creation']) ?></span>
</p>
<div class="comment-inner">
<div class="markdown">
<?= $this->text->markdown($comment['comment']) ?>
</div>
</div>
<?php endforeach ?>
</section>

View file

@ -0,0 +1,5 @@
<section class="tooltip-large">
<div class="markdown">
<?= $this->text->markdown($task['description']) ?>
</div>
</section>

View file

@ -0,0 +1,18 @@
<table class="table-small">
<?php foreach($files as $file): ?>
<tr>
<th>
<i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i>
<?= $this->e($file['name']) ?>
</th>
</tr>
<tr>
<td>
<i class="fa fa-download fa-fw"></i><?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
<?php if ($file['is_image'] == 1): ?>
&nbsp;<i class="fa fa-eye"></i> <?= $this->url->link(t('open file'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
</table>

View file

@ -0,0 +1,7 @@
<section id="tooltip-subtasks">
<?php foreach ($subtasks as $subtask): ?>
<?= $this->subtask->toggleStatus($subtask, 'board') ?>
<?= $this->e(empty($subtask['username']) ? '' : ' ['.$this->user->getFullname($subtask).']') ?>
<br/>
<?php endforeach ?>
</section>

View file

@ -0,0 +1,18 @@
<div class="tooltip-tasklinks">
<ul>
<?php foreach($links as $link): ?>
<li>
<strong><?= t($link['label']) ?></strong>
<?= $this->url->link(
$this->e('#'.$link['task_id'].' - '.$link['title']),
'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']),
false,
$link['is_active'] ? '' : 'task-link-closed'
) ?>
<?php if (! empty($link['task_assignee_username'])): ?>
[<?= $this->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?>]
<?php endif ?>
</li>
<?php endforeach ?>
</ul>
</div>

Some files were not shown because too many files have changed in this diff Show more