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

Update to kanboard v1.0.24

This commit is contained in:
mbugeia 2016-01-24 17:50:51 +01:00
parent 039a8de71a
commit 8690250086
481 changed files with 14744 additions and 14617 deletions

View file

@ -1,3 +1,80 @@
Version 1.0.24
--------------
New features:
* Forgot Password
* Add drop-down menu on each board column title to close all tasks
* Add Malay language
* Add new API procedures for groups, roles, project permissions and to move/duplicate tasks to another project
Improvements:
* Avoid to send XHR request when a task has not moved after a drag and drop
* Set maximum dropzone height when the individual column scrolling is disabled
* Always show the search box in board selector
* Replace logout link by a drop-down menu
* Handle notification for group members attached to a project
* Return the highest role for a project when a user is member of multiple groups
* Show in user interface the saving state of the task
* Add drop-down menu for subtasks, categories, swimlanes, columns, custom filters, task links and groups
* Add new template hooks
* Application settings are not cached anymore in the session
* Do not check board status during task move
* Move validators to a separate namespace
* Improve and write unit tests for reports
* Reduce the number of SQL queries for project daily column stats
* Remove event subscriber to update date_moved field
* Make sure that some event subscribers are not executed multiple times
* Show rendering time of individual templates when debug mode is enabled
* Make sure that no events are fired if nothing has been modified in the task
* Make dashboard section title clickable
* Add unit tests for LastLogin
Bug fixes:
* Automatic action listeners were using the same instance
* Fix wrong link for category in task footer
* Unable to set currency rate with Postgres database
* Avoid automatic actions that change the color to fire subsequent events
* Unable to unassign a task from the API
* Revert back previous optimizations of TaskPosition (incompatibility with some environment)
Version 1.0.23
--------------
Breaking changes:
* Plugin API changes for Automatic Actions
* Automatic Action to close a task doesn't have the column parameter anymore (use the action "Close a task in a specific column")
* Action name stored in the database is now the absolute class name
* Core functionalities moved to external plugins:
- Github Webhook: https://github.com/kanboard/plugin-github-webhook
- Gitlab Webhook: https://github.com/kanboard/plugin-gitlab-webhook
- Bitbucket Webhook: https://github.com/kanboard/plugin-bitbucket-webhook
New features:
* Added support of user mentions (@username)
* Added report to compare working hours between open and closed tasks
* Added the possibility to define custom routes from plugins
* Added new method to remove metadata
Improvements:
* Improve Two-Factor activation and plugin API
* Improving performance during task position change (SQL queries are 3 times faster than before)
* Do not show window scrollbars when individual column scrolling is enabled
* Automatic Actions code improvements and unit tests
* Increase action name column length in actions table
Bug fixes:
* Fix compatibility issue with FreeBSD for session.hash_function parameter
* Fix wrong constant name that causes a PHP error in project management section
* Fix pagination in group members listing
* Avoid PHP error when enabling LDAP group provider with PHP < 5.5
Version 1.0.22
--------------
@ -33,7 +110,7 @@ Version 1.0.21
Breaking changes:
* Projects with duplicate name are now allowed:
* Projects with duplicate names are now allowed:
- For Postgres and Mysql the unique constraint is removed by database migration
- However Sqlite does not support alter table, only new databases will have the unique constraint removed
@ -44,7 +121,7 @@ New features:
Improvements:
* Dropdown menu entry are now clickable outside of the html link
* Dropdown menu entries are now clickable outside of the html link
* Improve error handling of plugins
* Use PHP7 function random_bytes() to generate tokens if available
* CSV task export show the assignee name in addition to the assignee username
@ -92,7 +169,7 @@ Improvements:
Bug fixes:
* People should not see any tasks during a search when they are not associated to a project
* Avoid to disable the default swimlane during renaming when there is no other activated swimlane
* Avoid disabling the default swimlane during renaming when there is no other activated swimlane
Version 1.0.19
--------------
@ -124,15 +201,15 @@ Improvements:
* Offer alternative method to create Mysql and Postgres databases (import sql dump)
* Make sure there is always a trailing slash for application_url
* Do not show the checkbox "Show default swimlane" when there is no active swimlanes
* Append filters instead of replacing value for users and categories dropdowns
* Append filters instead of replacing value for users and categories drop-downs
* Do not show empty swimlanes in public view
* Change swimlane layout to save space on the screen
* Add the possibility to set/unset max column height (column scrolling)
* Show "Open this task" in dropdown menu for closed tasks
* Show "Open this task" in drop-down menu for closed tasks
* Show assignee on card only when someone is assigned (hide nobody text)
* Highlight selected item in dropdown menus
* Highlight selected item in drop-down menus
* Gantt chart: change bar color according to task progress
* Replace color dropdown by color picker in task forms
* Replace color drop-down by color picker in task forms
* Creating another task stay in the popover (no full page refresh anymore)
* Avoid scrollbar in Gantt chart for row title on Windows platform
* Remove unnecessary margin for calendar header
@ -144,14 +221,14 @@ Improvements:
Others:
* Data directory permissions are not checked anymore
* Data directory permission are not checked anymore
* Data directory is not mandatory anymore for people that use a remote database and remote object storage
Bug fixes:
* Fix typo in template that prevent the Gitlab OAuth link to be displayed
* Fix typo in template that prevents Gitlab OAuth link to be displayed
* Fix Markdown preview links focus
* Avoid dropdown menu to be truncated inside a column with scrolling
* Avoid drop-down menu to be truncated inside a column with scrolling
* Deleting subtask doesn't update task time tracking
* Fix Mysql error about gitlab_id when creating remote user
* Fix subtask timer bug (event called recursively)
@ -169,7 +246,7 @@ New features:
* 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 login brute force 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
@ -177,7 +254,7 @@ New features:
* 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 search for task title when using an integer only input
* Add Portuguese (European) translation
* Add Norwegian translation
@ -194,16 +271,16 @@ Improvements:
* 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
* 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 bug task estimate is reset when using subtask timer
* Fix screenshot feature with Firefox 40
* Fix bug when uploading files with cyrilic characters
* Fix bug when uploading files with Cyrilic characters
Version 1.0.17
--------------
@ -217,14 +294,14 @@ New features:
* 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 setting option to disable subtask timer
* Added setting option to include or exclude closed tasks into CFD
* Added setting 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 memory consumption to debug log
* Added form to create remote user
* Added edit form for user authentication
* Added config option to hide login form
@ -235,7 +312,7 @@ New features:
* 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 icon to set the start date automatically
* Added datetime picker for start date
Improvements:
@ -244,8 +321,8 @@ Improvements:
* 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
* Set focus on the drop-down for category popover
* Make escape keyboard shortcut global
* Check the box remember me by default
* Store redirect login url in session instead of using url parameter
* Update Gitlab webhook
@ -272,7 +349,7 @@ Translations:
Bug fixes:
* Screenshot dropdown: unexpected scroll down on the board view and focus lost when clicking on the drop zone
* Screenshot drop-down: 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

View file

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2015 Frédéric Guillot
Copyright (c) 2014-2016 Frédéric Guillot
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -3,7 +3,6 @@
namespace Kanboard\Action;
use Kanboard\Event\GenericEvent;
use Pimple\Container;
/**
* Base class for automatic actions
@ -13,6 +12,14 @@ use Pimple\Container;
*/
abstract class Base extends \Kanboard\Core\Base
{
/**
* Extended events
*
* @access private
* @var array
*/
private $compatibleEvents = array();
/**
* Flag for called listener
*
@ -27,7 +34,7 @@ abstract class Base extends \Kanboard\Core\Base
* @access private
* @var integer
*/
private $project_id = 0;
private $projectId = 0;
/**
* User parameters
@ -38,20 +45,25 @@ abstract class Base extends \Kanboard\Core\Base
private $params = array();
/**
* Attached event name
* Get automatic action name
*
* @access protected
* @var string
* @final
* @access public
* @return string
*/
protected $event_name = '';
final public function getName()
{
return '\\'.get_called_class();
}
/**
* Container instance
* Get automatic action description
*
* @access protected
* @var \Pimple\Container
* @abstract
* @access public
* @return string
*/
protected $container;
abstract public function getDescription();
/**
* Execute the action
@ -99,22 +111,6 @@ abstract class Base extends \Kanboard\Core\Base
*/
abstract public function hasRequiredCondition(array $data);
/**
* Constructor
*
* @access public
* @param \Pimple\Container $container Container
* @param integer $project_id Project id
* @param string $event_name Attached event name
*/
public function __construct(Container $container, $project_id, $event_name)
{
$this->container = $container;
$this->project_id = $project_id;
$this->event_name = $event_name;
$this->called = false;
}
/**
* Return class information
*
@ -123,7 +119,25 @@ abstract class Base extends \Kanboard\Core\Base
*/
public function __toString()
{
return get_called_class();
$params = array();
foreach ($this->params as $key => $value) {
$params[] = $key.'='.var_export($value, true);
}
return $this->getName().'('.implode('|', $params).'])';
}
/**
* Set project id
*
* @access public
* @return Base
*/
public function setProjectId($project_id)
{
$this->projectId = $project_id;
return $this;
}
/**
@ -134,7 +148,7 @@ abstract class Base extends \Kanboard\Core\Base
*/
public function getProjectId()
{
return $this->project_id;
return $this->projectId;
}
/**
@ -143,10 +157,12 @@ abstract class Base extends \Kanboard\Core\Base
* @access public
* @param string $name Parameter name
* @param mixed $value Value
* @param Base
*/
public function setParam($name, $value)
{
$this->params[$name] = $value;
return $this;
}
/**
@ -154,24 +170,25 @@ abstract class Base extends \Kanboard\Core\Base
*
* @access public
* @param string $name Parameter name
* @param mixed $default_value Default value
* @param mixed $default Default value
* @return mixed
*/
public function getParam($name, $default_value = null)
public function getParam($name, $default = null)
{
return isset($this->params[$name]) ? $this->params[$name] : $default_value;
return isset($this->params[$name]) ? $this->params[$name] : $default;
}
/**
* Check if an action is executable (right project and required parameters)
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action is executable
* @param array $data
* @param string $eventName
* @return bool
*/
public function isExecutable(array $data)
public function isExecutable(array $data, $eventName)
{
return $this->hasCompatibleEvent() &&
return $this->hasCompatibleEvent($eventName) &&
$this->hasRequiredProject($data) &&
$this->hasRequiredParameters($data) &&
$this->hasRequiredCondition($data);
@ -181,11 +198,12 @@ abstract class Base extends \Kanboard\Core\Base
* Check if the event is compatible with the action
*
* @access public
* @param string $eventName
* @return bool
*/
public function hasCompatibleEvent()
public function hasCompatibleEvent($eventName)
{
return in_array($this->event_name, $this->getCompatibleEvents());
return in_array($eventName, $this->getEvents());
}
/**
@ -197,7 +215,7 @@ abstract class Base extends \Kanboard\Core\Base
*/
public function hasRequiredProject(array $data)
{
return isset($data['project_id']) && $data['project_id'] == $this->project_id;
return isset($data['project_id']) && $data['project_id'] == $this->getProjectId();
}
/**
@ -222,10 +240,11 @@ abstract class Base extends \Kanboard\Core\Base
* Execute the action
*
* @access public
* @param \Event\GenericEvent $event Event data dictionary
* @return bool True if the action was executed or false when not executed
* @param \Kanboard\Event\GenericEvent $event
* @param string $eventName
* @return bool
*/
public function execute(GenericEvent $event)
public function execute(GenericEvent $event, $eventName)
{
// Avoid infinite loop, a listener instance can be called only one time
if ($this->called) {
@ -233,17 +252,44 @@ abstract class Base extends \Kanboard\Core\Base
}
$data = $event->getAll();
$result = false;
$executable = $this->isExecutable($data, $eventName);
$executed = false;
if ($this->isExecutable($data)) {
if ($executable) {
$this->called = true;
$result = $this->doAction($data);
$executed = $this->doAction($data);
}
if (DEBUG) {
$this->logger->debug(get_called_class().' => '.($result ? 'true' : 'false'));
$this->logger->debug($this.' ['.$eventName.'] => executable='.var_export($executable, true).' exec_success='.var_export($executed, true));
return $executed;
}
/**
* Register a new event for the automatic action
*
* @access public
* @param string $event
* @param string $description
*/
public function addEvent($event, $description = '')
{
if ($description !== '') {
$this->eventManager->register($event, $description);
}
return $result;
$this->compatibleEvents[] = $event;
return $this;
}
/**
* Get all compatible events of an automatic action
*
* @access public
* @return array
*/
public function getEvents()
{
return array_unique(array_merge($this->getCompatibleEvents(), $this->compatibleEvents));
}
}

View file

@ -2,10 +2,6 @@
namespace Kanboard\Action;
use Kanboard\Integration\BitbucketWebhook;
use Kanboard\Integration\GithubWebhook;
use Kanboard\Integration\GitlabWebhook;
/**
* Create automatically a comment from a webhook
*
@ -14,6 +10,17 @@ use Kanboard\Integration\GitlabWebhook;
*/
class CommentCreation extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Create a comment from an external provider');
}
/**
* Get the list of compatible events
*
@ -22,14 +29,7 @@ class CommentCreation extends Base
*/
public function getCompatibleEvents()
{
return array(
GithubWebhook::EVENT_ISSUE_COMMENT,
GithubWebhook::EVENT_COMMIT,
BitbucketWebhook::EVENT_ISSUE_COMMENT,
BitbucketWebhook::EVENT_COMMIT,
GitlabWebhook::EVENT_COMMIT,
GitlabWebhook::EVENT_ISSUE_COMMENT,
);
return array();
}
/**
@ -67,9 +67,9 @@ class CommentCreation extends Base
{
return (bool) $this->comment->create(array(
'reference' => isset($data['reference']) ? $data['reference'] : '',
'comment' => empty($data['comment']) ? $data['commit_comment'] : $data['comment'],
'comment' => $data['comment'],
'task_id' => $data['task_id'],
'user_id' => empty($data['user_id']) ? 0 : $data['user_id'],
'user_id' => isset($data['user_id']) && $this->projectPermission->isAssignable($this->getProjectId(), $data['user_id']) ? $data['user_id'] : 0,
));
}
@ -82,6 +82,6 @@ class CommentCreation extends Base
*/
public function hasRequiredCondition(array $data)
{
return ! empty($data['comment']) || ! empty($data['commit_comment']);
return ! empty($data['comment']);
}
}

View file

@ -5,13 +5,24 @@ namespace Kanboard\Action;
use Kanboard\Model\Task;
/**
* Add a log of the triggering event to the task description.
* Add a comment of the triggering event to the task description.
*
* @package action
* @author Oren Ben-Kiki
*/
class TaskLogMoveAnotherColumn extends Base
class CommentCreationMoveTaskColumn extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Add a comment log when moving the task between columns');
}
/**
* Get the list of compatible events
*

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskAssignCategoryColor extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Assign automatically a category based on a color');
}
/**
* Get the list of compatible events
*

View file

@ -2,8 +2,6 @@
namespace Kanboard\Action;
use Kanboard\Integration\GithubWebhook;
/**
* Set a category automatically according to a label
*
@ -12,6 +10,17 @@ use Kanboard\Integration\GithubWebhook;
*/
class TaskAssignCategoryLabel extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Change the category based on an external label');
}
/**
* Get the list of compatible events
*
@ -20,9 +29,7 @@ class TaskAssignCategoryLabel extends Base
*/
public function getCompatibleEvents()
{
return array(
GithubWebhook::EVENT_ISSUE_LABEL_CHANGE,
);
return array();
}
/**
@ -64,7 +71,7 @@ class TaskAssignCategoryLabel extends Base
{
$values = array(
'id' => $data['task_id'],
'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'),
'category_id' => $this->getParam('category_id'),
);
return $this->taskModification->update($values);
@ -79,6 +86,6 @@ class TaskAssignCategoryLabel extends Base
*/
public function hasRequiredCondition(array $data)
{
return $data['label'] == $this->getParam('label');
return $data['label'] == $this->getParam('label') && empty($data['category_id']);
}
}

View file

@ -13,6 +13,17 @@ use Kanboard\Model\TaskLink;
*/
class TaskAssignCategoryLink extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Assign automatically a category based on a link');
}
/**
* Get the list of compatible events
*
@ -65,7 +76,7 @@ class TaskAssignCategoryLink extends Base
{
$values = array(
'id' => $data['task_id'],
'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'),
'category_id' => $this->getParam('category_id'),
);
return $this->taskModification->update($values);

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskAssignColorCategory extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Assign automatically a color based on a category');
}
/**
* Get the list of compatible events
*
@ -67,7 +78,7 @@ class TaskAssignColorCategory extends Base
'color_id' => $this->getParam('color_id'),
);
return $this->taskModification->update($values);
return $this->taskModification->update($values, false);
}
/**

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskAssignColorColumn extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Assign a color when the task is moved to a specific column');
}
/**
* Get the list of compatible events
*
@ -68,7 +79,7 @@ class TaskAssignColorColumn extends Base
'color_id' => $this->getParam('color_id'),
);
return $this->taskModification->update($values);
return $this->taskModification->update($values, false);
}
/**

View file

@ -12,6 +12,17 @@ use Kanboard\Model\TaskLink;
*/
class TaskAssignColorLink extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Change task color when using a specific task link');
}
/**
* Get the list of compatible events
*
@ -67,7 +78,7 @@ class TaskAssignColorLink extends Base
'color_id' => $this->getParam('color_id'),
);
return $this->taskModification->update($values);
return $this->taskModification->update($values, false);
}
/**

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskAssignColorUser extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Assign a color to a specific user');
}
/**
* Get the list of compatible events
*
@ -68,7 +79,7 @@ class TaskAssignColorUser extends Base
'color_id' => $this->getParam('color_id'),
);
return $this->taskModification->update($values);
return $this->taskModification->update($values, false);
}
/**

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskAssignCurrentUser extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Assign the task to the person who does the action');
}
/**
* Get the list of compatible events
*
@ -22,7 +33,6 @@ class TaskAssignCurrentUser extends Base
{
return array(
Task::EVENT_CREATE,
Task::EVENT_MOVE_COLUMN,
);
}
@ -34,9 +44,7 @@ class TaskAssignCurrentUser extends Base
*/
public function getActionRequiredParameters()
{
return array(
'column_id' => t('Column'),
);
return array();
}
/**
@ -49,7 +57,6 @@ class TaskAssignCurrentUser extends Base
{
return array(
'task_id',
'column_id',
);
}
@ -83,6 +90,6 @@ class TaskAssignCurrentUser extends Base
*/
public function hasRequiredCondition(array $data)
{
return $data['column_id'] == $this->getParam('column_id');
return true;
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Kanboard\Action;
use Kanboard\Model\Task;
/**
* Assign a task to the logged user on column change
*
* @package action
* @author Frederic Guillot
*/
class TaskAssignCurrentUserColumn extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Assign the task to the person who does the action when the column is changed');
}
/**
* 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
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function doAction(array $data)
{
if (! $this->userSession->isLogged()) {
return false;
}
$values = array(
'id' => $data['task_id'],
'owner_id' => $this->userSession->getId(),
);
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

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskAssignSpecificUser extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Assign the task to a specific user');
}
/**
* Get the list of compatible events
*

View file

@ -2,9 +2,6 @@
namespace Kanboard\Action;
use Kanboard\Integration\GithubWebhook;
use Kanboard\Integration\BitbucketWebhook;
/**
* Assign a task to someone
*
@ -13,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook;
*/
class TaskAssignUser extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Change the assignee based on an external username');
}
/**
* Get the list of compatible events
*
@ -21,10 +29,7 @@ class TaskAssignUser extends Base
*/
public function getCompatibleEvents()
{
return array(
GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE,
BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE,
);
return array();
}
/**
@ -78,6 +83,6 @@ class TaskAssignUser extends Base
*/
public function hasRequiredCondition(array $data)
{
return true;
return $this->projectPermission->isAssignable($this->getProjectId(), $data['owner_id']);
}
}

View file

@ -2,11 +2,6 @@
namespace Kanboard\Action;
use Kanboard\Integration\GitlabWebhook;
use Kanboard\Integration\GithubWebhook;
use Kanboard\Integration\BitbucketWebhook;
use Kanboard\Model\Task;
/**
* Close automatically a task
*
@ -15,6 +10,17 @@ use Kanboard\Model\Task;
*/
class TaskClose extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Close a task');
}
/**
* Get the list of compatible events
*
@ -23,15 +29,7 @@ class TaskClose extends Base
*/
public function getCompatibleEvents()
{
return array(
Task::EVENT_MOVE_COLUMN,
GithubWebhook::EVENT_COMMIT,
GithubWebhook::EVENT_ISSUE_CLOSED,
GitlabWebhook::EVENT_COMMIT,
GitlabWebhook::EVENT_ISSUE_CLOSED,
BitbucketWebhook::EVENT_COMMIT,
BitbucketWebhook::EVENT_ISSUE_CLOSED,
);
return array();
}
/**
@ -42,17 +40,7 @@ class TaskClose extends Base
*/
public function getActionRequiredParameters()
{
switch ($this->event_name) {
case GithubWebhook::EVENT_COMMIT:
case GithubWebhook::EVENT_ISSUE_CLOSED:
case GitlabWebhook::EVENT_COMMIT:
case GitlabWebhook::EVENT_ISSUE_CLOSED:
case BitbucketWebhook::EVENT_COMMIT:
case BitbucketWebhook::EVENT_ISSUE_CLOSED:
return array();
default:
return array('column_id' => t('Column'));
}
return array();
}
/**
@ -63,17 +51,7 @@ class TaskClose extends Base
*/
public function getEventRequiredParameters()
{
switch ($this->event_name) {
case GithubWebhook::EVENT_COMMIT:
case GithubWebhook::EVENT_ISSUE_CLOSED:
case GitlabWebhook::EVENT_COMMIT:
case GitlabWebhook::EVENT_ISSUE_CLOSED:
case BitbucketWebhook::EVENT_COMMIT:
case BitbucketWebhook::EVENT_ISSUE_CLOSED:
return array('task_id');
default:
return array('task_id', 'column_id');
}
return array('task_id');
}
/**
@ -97,16 +75,6 @@ class TaskClose extends Base
*/
public function hasRequiredCondition(array $data)
{
switch ($this->event_name) {
case GithubWebhook::EVENT_COMMIT:
case GithubWebhook::EVENT_ISSUE_CLOSED:
case GitlabWebhook::EVENT_COMMIT:
case GitlabWebhook::EVENT_ISSUE_CLOSED:
case BitbucketWebhook::EVENT_COMMIT:
case BitbucketWebhook::EVENT_ISSUE_CLOSED:
return true;
default:
return $data['column_id'] == $this->getParam('column_id');
}
return true;
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Kanboard\Action;
use Kanboard\Model\Task;
/**
* Close automatically a task in a specific column
*
* @package action
* @author Frederic Guillot
*/
class TaskCloseColumn extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Close a task in a specific column');
}
/**
* 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 (close the task)
*
* @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)
{
return $this->taskStatus->close($data['task_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('column_id');
}
}

View file

@ -2,10 +2,6 @@
namespace Kanboard\Action;
use Kanboard\Integration\GithubWebhook;
use Kanboard\Integration\GitlabWebhook;
use Kanboard\Integration\BitbucketWebhook;
/**
* Create automatically a task from a webhook
*
@ -14,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook;
*/
class TaskCreation extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Create a task from an external provider');
}
/**
* Get the list of compatible events
*
@ -22,11 +29,7 @@ class TaskCreation extends Base
*/
public function getCompatibleEvents()
{
return array(
GithubWebhook::EVENT_ISSUE_OPENED,
GitlabWebhook::EVENT_ISSUE_OPENED,
BitbucketWebhook::EVENT_ISSUE_OPENED,
);
return array();
}
/**

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskDuplicateAnotherProject extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Duplicate the task to another project');
}
/**
* Get the list of compatible events
*
@ -51,7 +62,6 @@ class TaskDuplicateAnotherProject extends Base
return array(
'task_id',
'column_id',
'project_id',
);
}
@ -65,7 +75,6 @@ class TaskDuplicateAnotherProject extends Base
public function doAction(array $data)
{
$destination_column_id = $this->board->getFirstColumn($this->getParam('project_id'));
return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id);
}

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskEmail extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Send a task by email to someone');
}
/**
* Get the list of compatible events
*

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskMoveAnotherProject extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Move the task to another project');
}
/**
* Get the list of compatible events
*

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskMoveColumnAssigned extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Move the task to another column when assigned to a user');
}
/**
* Get the list of compatible events
*
@ -51,7 +62,6 @@ class TaskMoveColumnAssigned extends Base
return array(
'task_id',
'column_id',
'project_id',
'owner_id'
);
}

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskMoveColumnCategoryChange extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Move the task to another column when the category is changed');
}
/**
* Get the list of compatible events
*
@ -50,7 +61,6 @@ class TaskMoveColumnCategoryChange extends Base
return array(
'task_id',
'column_id',
'project_id',
'category_id',
);
}
@ -71,7 +81,8 @@ class TaskMoveColumnCategoryChange extends Base
$data['task_id'],
$this->getParam('dest_column_id'),
$original_task['position'],
$original_task['swimlane_id']
$original_task['swimlane_id'],
false
);
}

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskMoveColumnUnAssigned extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Move the task to another column when assignee is cleared');
}
/**
* Get the list of compatible events
*
@ -51,7 +62,6 @@ class TaskMoveColumnUnAssigned extends Base
return array(
'task_id',
'column_id',
'project_id',
'owner_id'
);
}

View file

@ -2,10 +2,6 @@
namespace Kanboard\Action;
use Kanboard\Integration\GithubWebhook;
use Kanboard\Integration\GitlabWebhook;
use Kanboard\Integration\BitbucketWebhook;
/**
* Open automatically a task
*
@ -14,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook;
*/
class TaskOpen extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Open a task');
}
/**
* Get the list of compatible events
*
@ -22,11 +29,7 @@ class TaskOpen extends Base
*/
public function getCompatibleEvents()
{
return array(
GithubWebhook::EVENT_ISSUE_REOPENED,
GitlabWebhook::EVENT_ISSUE_REOPENED,
BitbucketWebhook::EVENT_ISSUE_REOPENED,
);
return array();
}
/**

View file

@ -12,6 +12,17 @@ use Kanboard\Model\Task;
*/
class TaskUpdateStartDate extends Base
{
/**
* Get automatic action description
*
* @access public
* @return string
*/
public function getDescription()
{
return t('Automatically update the start date');
}
/**
* Get the list of compatible events
*
@ -66,7 +77,7 @@ class TaskUpdateStartDate extends Base
'date_started' => time(),
);
return $this->taskModification->update($values);
return $this->taskModification->update($values, false);
}
/**

View file

@ -0,0 +1,114 @@
<?php
namespace Kanboard\Analytic;
use Kanboard\Core\Base;
use Kanboard\Model\Task;
/**
* Average Lead and Cycle Time
*
* @package analytic
* @author Frederic Guillot
*/
class AverageLeadCycleTimeAnalytic extends Base
{
/**
* Build report
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function build($project_id)
{
$stats = array(
'count' => 0,
'total_lead_time' => 0,
'total_cycle_time' => 0,
'avg_lead_time' => 0,
'avg_cycle_time' => 0,
);
$tasks = $this->getTasks($project_id);
foreach ($tasks as &$task) {
$stats['count']++;
$stats['total_lead_time'] += $this->calculateLeadTime($task);
$stats['total_cycle_time'] += $this->calculateCycleTime($task);
}
$stats['avg_lead_time'] = $this->calculateAverage($stats, 'total_lead_time');
$stats['avg_cycle_time'] = $this->calculateAverage($stats, 'total_cycle_time');
return $stats;
}
/**
* Calculate average
*
* @access private
* @param array &$stats
* @param string $field
* @return float
*/
private function calculateAverage(array &$stats, $field)
{
if ($stats['count'] > 0) {
return (int) ($stats[$field] / $stats['count']);
}
return 0;
}
/**
* Calculate lead time
*
* @access private
* @param array &$task
* @return integer
*/
private function calculateLeadTime(array &$task)
{
$end = $task['date_completed'] ?: time();
$start = $task['date_creation'];
return $end - $start;
}
/**
* Calculate cycle time
*
* @access private
* @param array &$task
* @return integer
*/
private function calculateCycleTime(array &$task)
{
if (empty($task['date_started'])) {
return 0;
}
$end = $task['date_completed'] ?: time();
$start = $task['date_started'];
return $end - $start;
}
/**
* Get the 1000 last created tasks
*
* @access private
* @return array
*/
private function getTasks($project_id)
{
return $this->db
->table(Task::TABLE)
->columns('date_completed', 'date_creation', 'date_started')
->eq('project_id', $project_id)
->desc('id')
->limit(1000)
->findAll();
}
}

View file

@ -0,0 +1,153 @@
<?php
namespace Kanboard\Analytic;
use Kanboard\Core\Base;
use Kanboard\Model\Task;
/**
* Average Time Spent by Column
*
* @package analytic
* @author Frederic Guillot
*/
class AverageTimeSpentColumnAnalytic extends Base
{
/**
* Build report
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function build($project_id)
{
$stats = $this->initialize($project_id);
$this->processTasks($stats, $project_id);
$this->calculateAverage($stats);
return $stats;
}
/**
* Initialize default values for each column
*
* @access private
* @param integer $project_id
* @return array
*/
private function initialize($project_id)
{
$stats = array();
$columns = $this->board->getColumnsList($project_id);
foreach ($columns as $column_id => $column_title) {
$stats[$column_id] = array(
'count' => 0,
'time_spent' => 0,
'average' => 0,
'title' => $column_title,
);
}
return $stats;
}
/**
* Calculate time spent for each tasks for each columns
*
* @access private
* @param array $stats
* @param integer $project_id
*/
private function processTasks(array &$stats, $project_id)
{
$tasks = $this->getTasks($project_id);
foreach ($tasks as &$task) {
foreach ($this->getTaskTimeByColumns($task) as $column_id => $time_spent) {
if (isset($stats[$column_id])) {
$stats[$column_id]['count']++;
$stats[$column_id]['time_spent'] += $time_spent;
}
}
}
}
/**
* Calculate averages
*
* @access private
* @param array $stats
*/
private function calculateAverage(array &$stats)
{
foreach ($stats as &$column) {
$this->calculateColumnAverage($column);
}
}
/**
* Calculate column average
*
* @access private
* @param array $column
*/
private function calculateColumnAverage(array &$column)
{
if ($column['count'] > 0) {
$column['average'] = (int) ($column['time_spent'] / $column['count']);
}
}
/**
* Get time spent for each column for a given task
*
* @access private
* @param array $task
* @return array
*/
private function getTaskTimeByColumns(array &$task)
{
$columns = $this->transition->getTimeSpentByTask($task['id']);
if (! isset($columns[$task['column_id']])) {
$columns[$task['column_id']] = 0;
}
$columns[$task['column_id']] += $this->getTaskTimeSpentInCurrentColumn($task);
return $columns;
}
/**
* Calculate time spent of a task in the current column
*
* @access private
* @param array $task
*/
private function getTaskTimeSpentInCurrentColumn(array &$task)
{
$end = $task['date_completed'] ?: time();
return $end - $task['date_moved'];
}
/**
* Fetch the last 1000 tasks
*
* @access private
* @param integer $project_id
* @return array
*/
private function getTasks($project_id)
{
return $this->db
->table(Task::TABLE)
->columns('id', 'date_completed', 'date_moved', 'column_id')
->eq('project_id', $project_id)
->desc('id')
->limit(1000)
->findAll();
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Kanboard\Analytic;
use Kanboard\Core\Base;
use Kanboard\Model\Task;
/**
* Estimated/Spent Time Comparison
*
* @package analytic
* @author Frederic Guillot
*/
class EstimatedTimeComparisonAnalytic extends Base
{
/**
* Build report
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function build($project_id)
{
$rows = $this->db->table(Task::TABLE)
->columns('SUM(time_estimated) AS time_estimated', 'SUM(time_spent) AS time_spent', 'is_active')
->eq('project_id', $project_id)
->groupBy('is_active')
->findAll();
$metrics = array(
'open' => array(
'time_spent' => 0,
'time_estimated' => 0,
),
'closed' => array(
'time_spent' => 0,
'time_estimated' => 0,
),
);
foreach ($rows as $row) {
$key = $row['is_active'] == Task::STATUS_OPEN ? 'open' : 'closed';
$metrics[$key]['time_spent'] = $row['time_spent'];
$metrics[$key]['time_estimated'] = $row['time_estimated'];
}
return $metrics;
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace Kanboard\Analytic;
use Kanboard\Core\Base;
/**
* Task Distribution
*
* @package analytic
* @author Frederic Guillot
*/
class TaskDistributionAnalytic extends Base
{
/**
* Build report
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function build($project_id)
{
$metrics = array();
$total = 0;
$columns = $this->board->getColumns($project_id);
foreach ($columns as $column) {
$nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']);
$total += $nb_tasks;
$metrics[] = array(
'column_title' => $column['title'],
'nb_tasks' => $nb_tasks,
);
}
if ($total === 0) {
return array();
}
foreach ($metrics as &$metric) {
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
}
return $metrics;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Kanboard\Analytic;
use Kanboard\Core\Base;
/**
* User Distribution
*
* @package analytic
* @author Frederic Guillot
*/
class UserDistributionAnalytic extends Base
{
/**
* Build Report
*
* @access public
* @param integer $project_id
* @return array
*/
public function build($project_id)
{
$metrics = array();
$total = 0;
$tasks = $this->taskFinder->getAll($project_id);
$users = $this->projectUserRole->getAssignableUsersList($project_id);
foreach ($tasks as $task) {
$user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0];
$total++;
if (! isset($metrics[$user])) {
$metrics[$user] = array(
'nb_tasks' => 0,
'percentage' => 0,
'user' => $user,
);
}
$metrics[$user]['nb_tasks']++;
}
if ($total === 0) {
return array();
}
foreach ($metrics as &$metric) {
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
}
ksort($metrics);
return array_values($metrics);
}
}

View file

@ -12,17 +12,17 @@ class Action extends \Kanboard\Core\Base
{
public function getAvailableActions()
{
return $this->action->getAvailableActions();
return $this->actionManager->getAvailableActions();
}
public function getAvailableActionEvents()
{
return $this->action->getAvailableEvents();
return $this->eventManager->getAll();
}
public function getCompatibleActionEvents($action_name)
{
return $this->action->getCompatibleEvents($action_name);
return $this->actionManager->getCompatibleEvents($action_name);
}
public function removeAction($action_id)
@ -32,22 +32,10 @@ class Action extends \Kanboard\Core\Base
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;
return $this->action->getAllByProject($project_id);
}
public function createAction($project_id, $event_name, $action_name, $params)
public function createAction($project_id, $event_name, $action_name, array $params)
{
$values = array(
'project_id' => $project_id,
@ -56,23 +44,23 @@ class Action extends \Kanboard\Core\Base
'params' => $params,
);
list($valid, ) = $this->action->validateCreation($values);
list($valid, ) = $this->actionValidator->validateCreation($values);
if (! $valid) {
return false;
}
// Check if the action exists
$actions = $this->action->getAvailableActions();
$actions = $this->actionManager->getAvailableActions();
if (! isset($actions[$action_name])) {
return false;
}
// Check the event
$action = $this->action->load($action_name, $project_id, $event_name);
$action = $this->actionManager->getAction($action_name);
if (! in_array($event_name, $action->getCompatibleEvents())) {
if (! in_array($event_name, $action->getEvents())) {
return false;
}

View file

@ -34,4 +34,14 @@ class App extends \Kanboard\Core\Base
{
return $this->color->getList();
}
public function getApplicationRoles()
{
return $this->role->getApplicationRoles();
}
public function getProjectRoles()
{
return $this->role->getProjectRoles();
}
}

View file

@ -23,7 +23,7 @@ class Auth extends Base
*/
public function checkCredentials($username, $password, $class, $method)
{
$this->container['dispatcher']->dispatch('app.bootstrap');
$this->dispatcher->dispatch('app.bootstrap');
if ($this->isUserAuthenticated($username, $password)) {
$this->checkProcedurePermission(true, $method);

View file

@ -32,7 +32,7 @@ class Category extends \Kanboard\Core\Base
'name' => $name,
);
list($valid, ) = $this->category->validateCreation($values);
list($valid, ) = $this->categoryValidator->validateCreation($values);
return $valid ? $this->category->create($values) : false;
}
@ -43,7 +43,7 @@ class Category extends \Kanboard\Core\Base
'name' => $name,
);
list($valid, ) = $this->category->validateModification($values);
list($valid, ) = $this->categoryValidator->validateModification($values);
return $valid && $this->category->update($values);
}
}

View file

@ -25,15 +25,16 @@ class Comment extends \Kanboard\Core\Base
return $this->comment->remove($comment_id);
}
public function createComment($task_id, $user_id, $content)
public function createComment($task_id, $user_id, $content, $reference = '')
{
$values = array(
'task_id' => $task_id,
'user_id' => $user_id,
'comment' => $content,
'reference' => $reference,
);
list($valid, ) = $this->comment->validateCreation($values);
list($valid, ) = $this->commentValidator->validateCreation($values);
return $valid ? $this->comment->create($values) : false;
}
@ -45,7 +46,7 @@ class Comment extends \Kanboard\Core\Base
'comment' => $content,
);
list($valid, ) = $this->comment->validateModification($values);
list($valid, ) = $this->commentValidator->validateModification($values);
return $valid && $this->comment->update($values);
}
}

View file

@ -32,14 +32,18 @@ class File extends \Kanboard\Core\Base
}
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
return '';
}
return '';
}
public function createFile($project_id, $task_id, $filename, $blob)
{
return $this->file->uploadContent($project_id, $task_id, $filename, $blob);
try {
return $this->file->uploadContent($project_id, $task_id, $filename, $blob);
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
return false;
}
}
public function removeFile($file_id)

49
sources/app/Api/Group.php Normal file
View file

@ -0,0 +1,49 @@
<?php
namespace Kanboard\Api;
/**
* Group API controller
*
* @package api
* @author Frederic Guillot
*/
class Group extends \Kanboard\Core\Base
{
public function createGroup($name, $external_id = '')
{
return $this->group->create($name, $external_id);
}
public function updateGroup($group_id, $name = null, $external_id = null)
{
$values = array(
'id' => $group_id,
'name' => $name,
'external_id' => $external_id,
);
foreach ($values as $key => $value) {
if (is_null($value)) {
unset($values[$key]);
}
}
return $this->group->update($values);
}
public function removeGroup($group_id)
{
return $this->group->remove($group_id);
}
public function getGroup($group_id)
{
return $this->group->getById($group_id);
}
public function getAllGroups()
{
return $this->group->getAll();
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Kanboard\Api;
/**
* Group Member API controller
*
* @package api
* @author Frederic Guillot
*/
class GroupMember extends \Kanboard\Core\Base
{
public function getGroupMembers($group_id)
{
return $this->groupMember->getMembers($group_id);
}
public function addGroupMember($group_id, $user_id)
{
return $this->groupMember->addUser($group_id, $user_id);
}
public function removeGroupMember($group_id, $user_id)
{
return $this->groupMember->removeUser($group_id, $user_id);
}
public function isGroupMember($group_id, $user_id)
{
return $this->groupMember->isMember($group_id, $user_id);
}
}

View file

@ -72,7 +72,7 @@ class Link extends \Kanboard\Core\Base
'opposite_label' => $opposite_label,
);
list($valid, ) = $this->link->validateCreation($values);
list($valid, ) = $this->linkValidator->validateCreation($values);
return $valid ? $this->link->create($label, $opposite_label) : false;
}
@ -93,7 +93,7 @@ class Link extends \Kanboard\Core\Base
'label' => $label,
);
list($valid, ) = $this->link->validateModification($values);
list($valid, ) = $this->linkValidator->validateModification($values);
return $valid && $this->link->update($values);
}

View file

@ -44,7 +44,7 @@ class Me extends Base
'is_private' => 1,
);
list($valid, ) = $this->project->validateCreation($values);
list($valid, ) = $this->projectValidator->validateCreation($values);
return $valid ? $this->project->create($values, $this->userSession->getId(), true) : false;
}

View file

@ -69,7 +69,7 @@ class Project extends Base
'description' => $description
);
list($valid, ) = $this->project->validateCreation($values);
list($valid, ) = $this->projectValidator->validateCreation($values);
return $valid ? $this->project->create($values) : false;
}
@ -81,7 +81,7 @@ class Project extends Base
'description' => $description
);
list($valid, ) = $this->project->validateModification($values);
list($valid, ) = $this->projectValidator->validateModification($values);
return $valid && $this->project->update($values);
}
}

View file

@ -5,25 +5,68 @@ namespace Kanboard\Api;
use Kanboard\Core\Security\Role;
/**
* ProjectPermission API controller
* Project Permission API controller
*
* @package api
* @author Frederic Guillot
*/
class ProjectPermission extends \Kanboard\Core\Base
{
public function getMembers($project_id)
public function getProjectUsers($project_id)
{
return $this->projectUserRole->getAllUsers($project_id);
}
public function revokeUser($project_id, $user_id)
public function getAssignableUsers($project_id, $prepend_unassigned = false)
{
return $this->projectUserRole->getAssignableUsersList($project_id, $prepend_unassigned);
}
public function addProjectUser($project_id, $user_id, $role = Role::PROJECT_MEMBER)
{
return $this->projectUserRole->addUser($project_id, $user_id, $role);
}
public function addProjectGroup($project_id, $group_id, $role = Role::PROJECT_MEMBER)
{
return $this->projectGroupRole->addGroup($project_id, $group_id, $role);
}
public function removeProjectUser($project_id, $user_id)
{
return $this->projectUserRole->removeUser($project_id, $user_id);
}
public function removeProjectGroup($project_id, $group_id)
{
return $this->projectGroupRole->removeGroup($project_id, $group_id);
}
public function changeProjectUserRole($project_id, $user_id, $role)
{
return $this->projectUserRole->changeUserRole($project_id, $user_id, $role);
}
public function changeProjectGroupRole($project_id, $group_id, $role)
{
return $this->projectGroupRole->changeGroupRole($project_id, $group_id, $role);
}
// Deprecated
public function getMembers($project_id)
{
return $this->getProjectUsers($project_id);
}
// Deprecated
public function revokeUser($project_id, $user_id)
{
return $this->removeProjectUser($project_id, $user_id);
}
// Deprecated
public function allowUser($project_id, $user_id)
{
return $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MEMBER);
return $this->addProjectUser($project_id, $user_id);
}
}

View file

@ -36,7 +36,7 @@ class Subtask extends \Kanboard\Core\Base
'status' => $status,
);
list($valid, ) = $this->subtask->validateCreation($values);
list($valid, ) = $this->subtaskValidator->validateCreation($values);
return $valid ? $this->subtask->create($values) : false;
}
@ -58,7 +58,7 @@ class Subtask extends \Kanboard\Core\Base
}
}
list($valid, ) = $this->subtask->validateApiModification($values);
list($valid, ) = $this->subtaskValidator->validateApiModification($values);
return $valid && $this->subtask->update($values);
}
}

View file

@ -64,6 +64,16 @@ class Task extends Base
return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id);
}
public function moveTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
{
return $this->taskDuplication->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
}
public function duplicateTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
{
return $this->taskDuplication->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_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,
@ -71,7 +81,7 @@ class Task extends Base
{
$this->checkProjectPermission($project_id);
if ($owner_id !== 0 && ! $this->projectPermission->isMember($project_id, $owner_id)) {
if ($owner_id !== 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) {
return false;
}
@ -117,7 +127,7 @@ class Task extends Base
return false;
}
if ($owner_id !== null && ! $this->projectPermission->isMember($project_id, $owner_id)) {
if ($owner_id !== null && $owner_id != 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) {
return false;
}

View file

@ -42,7 +42,7 @@ class User extends \Kanboard\Core\Base
'role' => $role,
);
list($valid, ) = $this->user->validateCreation($values);
list($valid, ) = $this->userValidator->validateCreation($values);
return $valid ? $this->user->create($values) : false;
}
@ -94,7 +94,7 @@ class User extends \Kanboard\Core\Base
}
}
list($valid, ) = $this->user->validateApiModification($values);
list($valid, ) = $this->userValidator->validateApiModification($values);
return $valid && $this->user->update($values);
}
}

View file

@ -19,26 +19,26 @@ class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterfa
/**
* User properties
*
* @access private
* @access protected
* @var array
*/
private $userInfo = array();
protected $userInfo = array();
/**
* Username
*
* @access private
* @access protected
* @var string
*/
private $username = '';
protected $username = '';
/**
* Password
*
* @access private
* @access protected
* @var string
*/
private $password = '';
protected $password = '';
/**
* Get authentication provider name

View file

@ -17,26 +17,26 @@ class GithubAuth extends Base implements OAuthAuthenticationProviderInterface
/**
* User properties
*
* @access private
* @access protected
* @var \Kanboard\User\GithubUserProvider
*/
private $userInfo = null;
protected $userInfo = null;
/**
* OAuth2 instance
*
* @access private
* @access protected
* @var \Kanboard\Core\Http\OAuth2
*/
private $service;
protected $service;
/**
* OAuth2 code
*
* @access private
* @access protected
* @var string
*/
private $code = '';
protected $code = '';
/**
* Get authentication provider name

View file

@ -25,18 +25,18 @@ class GitlabAuth extends Base implements OAuthAuthenticationProviderInterface
/**
* OAuth2 instance
*
* @access private
* @access protected
* @var \Kanboard\Core\Http\OAuth2
*/
private $service;
protected $service;
/**
* OAuth2 code
*
* @access private
* @access protected
* @var string
*/
private $code = '';
protected $code = '';
/**
* Get authentication provider name

View file

@ -17,26 +17,26 @@ class GoogleAuth extends Base implements OAuthAuthenticationProviderInterface
/**
* User properties
*
* @access private
* @access protected
* @var \Kanboard\User\GoogleUserProvider
*/
private $userInfo = null;
protected $userInfo = null;
/**
* OAuth2 instance
*
* @access private
* @access protected
* @var \Kanboard\Core\Http\OAuth2
*/
private $service;
protected $service;
/**
* OAuth2 code
*
* @access private
* @access protected
* @var string
*/
private $code = '';
protected $code = '';
/**
* Get authentication provider name

View file

@ -20,26 +20,26 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
/**
* User properties
*
* @access private
* @access protected
* @var \Kanboard\User\LdapUserProvider
*/
private $userInfo = null;
protected $userInfo = null;
/**
* Username
*
* @access private
* @access protected
* @var string
*/
private $username = '';
protected $username = '';
/**
* Password
*
* @access private
* @access protected
* @var string
*/
private $password = '';
protected $password = '';
/**
* Get authentication provider name

View file

@ -17,10 +17,10 @@ class RememberMeAuth extends Base implements PreAuthenticationProviderInterface
/**
* User properties
*
* @access private
* @access protected
* @var array
*/
private $userInfo = array();
protected $userInfo = array();
/**
* Get authentication provider name

View file

@ -19,18 +19,18 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface
/**
* User pin code
*
* @access private
* @access protected
* @var string
*/
private $code = '';
protected $code = '';
/**
* Private key
*
* @access private
* @access protected
* @var string
*/
private $secret = '';
protected $secret = '';
/**
* Get authentication provider name
@ -40,7 +40,7 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface
*/
public function getName()
{
return 'Time-based One-time Password Algorithm';
return t('Time-based One-time Password Algorithm');
}
/**
@ -55,6 +55,16 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface
return $otp->checkTotp(Base32::decode($this->secret), $this->code);
}
/**
* Called before to prompt the user
*
* @access public
*/
public function beforeCode()
{
}
/**
* Set validation code
*
@ -66,6 +76,18 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface
$this->code = $code;
}
/**
* Generate secret
*
* @access public
* @return string
*/
public function generateSecret()
{
$this->secret = GoogleAuthenticator::generateRandom();
return $this->secret;
}
/**
* Set secret token
*
@ -85,10 +107,6 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface
*/
public function getSecret()
{
if (empty($this->secret)) {
$this->secret = GoogleAuthenticator::generateRandom();
}
return $this->secret;
}

View file

@ -18,17 +18,18 @@ class Action extends Base
public function index()
{
$project = $this->getProject();
$actions = $this->action->getAllByProject($project['id']);
$this->response->html($this->projectLayout('action/index', array(
'values' => array('project_id' => $project['id']),
'project' => $project,
'actions' => $this->action->getAllByProject($project['id']),
'available_actions' => $this->action->getAvailableActions(),
'available_events' => $this->action->getAvailableEvents(),
'available_params' => $this->action->getAllActionParameters(),
'actions' => $actions,
'available_actions' => $this->actionManager->getAvailableActions(),
'available_events' => $this->eventManager->getAll(),
'available_params' => $this->actionManager->getAvailableParameters($actions),
'columns_list' => $this->board->getColumnsList($project['id']),
'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
'projects_list' => $this->project->getList(false),
'projects_list' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'links_list' => $this->link->getList(0, false),
@ -53,7 +54,7 @@ class Action extends Base
$this->response->html($this->projectLayout('action/event', array(
'values' => $values,
'project' => $project,
'events' => $this->action->getCompatibleEvents($values['action_name']),
'events' => $this->actionManager->getCompatibleEvents($values['action_name']),
'title' => t('Automatic actions')
)));
}
@ -72,14 +73,14 @@ class Action extends Base
$this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id'])));
}
$action = $this->action->load($values['action_name'], $values['project_id'], $values['event_name']);
$action = $this->actionManager->getAction($values['action_name']);
$action_params = $action->getActionRequiredParameters();
if (empty($action_params)) {
$this->doCreation($project, $values + array('params' => array()));
}
$projects_list = $this->project->getList(false);
$projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
unset($projects_list[$project['id']]);
$this->response->html($this->projectLayout('action/params', array(
@ -115,7 +116,7 @@ class Action extends Base
*/
private function doCreation(array $project, array $values)
{
list($valid, ) = $this->action->validateCreation($values);
list($valid, ) = $this->actionValidator->validateCreation($values);
if ($valid) {
if ($this->action->create($values) !== false) {
@ -139,8 +140,8 @@ class Action extends Base
$this->response->html($this->projectLayout('action/remove', array(
'action' => $this->action->getById($this->request->getIntegerParam('action_id')),
'available_events' => $this->action->getAvailableEvents(),
'available_actions' => $this->action->getAvailableActions(),
'available_events' => $this->eventManager->getAll(),
'available_actions' => $this->actionManager->getAvailableActions(),
'project' => $project,
'title' => t('Remove an action')
)));

View file

@ -20,7 +20,7 @@ class Activity extends Base
$project = $this->getProject();
$this->response->html($this->template->layout('activity/project', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'events' => $this->projectActivity->getProject($project['id']),
'project' => $project,
'title' => t('%s\'s activity', $project['name'])

View file

@ -1,6 +1,7 @@
<?php
namespace Kanboard\Controller;
use Kanboard\Model\Task as TaskModel;
/**
* Project Analytic controller
@ -20,7 +21,7 @@ class Analytic extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
$params['content_for_sublayout'] = $this->template->render($template, $params);
return $this->template->layout('analytic/layout', $params);
@ -34,17 +35,7 @@ class Analytic extends Base
public function leadAndCycleTime()
{
$project = $this->getProject();
$values = $this->request->getValues();
$this->projectDailyStats->updateTotals($project['id'], date('Y-m-d'));
$from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
$to = $this->request->getStringParam('to', date('Y-m-d'));
if (! empty($values)) {
$from = $values['from'];
$to = $values['to'];
}
list($from, $to) = $this->getDates();
$this->response->html($this->layout('analytic/lead_cycle_time', array(
'values' => array(
@ -52,7 +43,7 @@ class Analytic extends Base
'to' => $to,
),
'project' => $project,
'average' => $this->projectAnalytic->getAverageLeadAndCycleTime($project['id']),
'average' => $this->averageLeadCycleTimeAnalytic->build($project['id']),
'metrics' => $this->projectDailyStats->getRawMetrics($project['id'], $from, $to),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
@ -60,6 +51,32 @@ class Analytic extends Base
)));
}
/**
* Show comparison between actual and estimated hours chart
*
* @access public
*/
public function compareHours()
{
$project = $this->getProject();
$params = $this->getProjectFilters('analytic', 'compareHours');
$query = $this->taskFilter->create()->filterByProject($params['project']['id'])->getQuery();
$paginator = $this->paginator
->setUrl('analytic', 'compareHours', array('project_id' => $project['id']))
->setMax(30)
->setOrder(TaskModel::TABLE.'.id')
->setQuery($query)
->calculate();
$this->response->html($this->layout('analytic/compare_hours', array(
'project' => $project,
'paginator' => $paginator,
'metrics' => $this->estimatedTimeComparisonAnalytic->build($project['id']),
'title' => t('Compare hours for "%s"', $project['name']),
)));
}
/**
* Show average time spent by column
*
@ -71,7 +88,7 @@ class Analytic extends Base
$this->response->html($this->layout('analytic/avg_time_columns', array(
'project' => $project,
'metrics' => $this->projectAnalytic->getAverageTimeSpentByColumn($project['id']),
'metrics' => $this->averageTimeSpentColumnAnalytic->build($project['id']),
'title' => t('Average time spent into each column for "%s"', $project['name']),
)));
}
@ -87,7 +104,7 @@ class Analytic extends Base
$this->response->html($this->layout('analytic/tasks', array(
'project' => $project,
'metrics' => $this->projectAnalytic->getTaskRepartition($project['id']),
'metrics' => $this->taskDistributionAnalytic->build($project['id']),
'title' => t('Task repartition for "%s"', $project['name']),
)));
}
@ -103,7 +120,7 @@ class Analytic extends Base
$this->response->html($this->layout('analytic/users', array(
'project' => $project,
'metrics' => $this->projectAnalytic->getUserRepartition($project['id']),
'metrics' => $this->userDistributionAnalytic->build($project['id']),
'title' => t('User repartition for "%s"', $project['name']),
)));
}
@ -139,17 +156,7 @@ class Analytic extends Base
private function commonAggregateMetrics($template, $column, $title)
{
$project = $this->getProject();
$values = $this->request->getValues();
$this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d'));
$from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
$to = $this->request->getStringParam('to', date('Y-m-d'));
if (! empty($values)) {
$from = $values['from'];
$to = $values['to'];
}
list($from, $to) = $this->getDates();
$display_graph = $this->projectDailyColumnStats->countDays($project['id'], $from, $to) >= 2;
@ -166,4 +173,19 @@ class Analytic extends Base
'title' => t($title, $project['name']),
)));
}
private function getDates()
{
$values = $this->request->getValues();
$from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week')));
$to = $this->request->getStringParam('to', date('Y-m-d'));
if (! empty($values)) {
$from = $values['from'];
$to = $values['to'];
}
return array($from, $to);
}
}

View file

@ -22,7 +22,7 @@ class App extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
$params['content_for_sublayout'] = $this->template->render($template, $params);
return $this->template->layout('app/layout', $params);

View file

@ -2,8 +2,6 @@
namespace Kanboard\Controller;
use Gregwar\Captcha\CaptchaBuilder;
/**
* Authentication controller
*
@ -41,7 +39,7 @@ class Auth extends Base
{
$values = $this->request->getValues();
$this->sessionStorage->hasRememberMe = ! empty($values['remember_me']);
list($valid, $errors) = $this->authentication->validateForm($values);
list($valid, $errors) = $this->authValidator->validateForm($values);
if ($valid) {
$this->redirectAfterLogin();
@ -61,21 +59,6 @@ class Auth extends Base
$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->sessionStorage->captcha = $builder->getPhrase();
$builder->output();
}
/**
* Redirect the user after the authentication
*

View file

@ -17,18 +17,18 @@ abstract class Base extends \Kanboard\Core\Base
*
* @access public
*/
public function beforeAction($controller, $action)
public function beforeAction()
{
$this->sessionManager->open();
$this->dispatcher->dispatch('app.bootstrap');
$this->sendHeaders($action);
$this->sendHeaders();
$this->authenticationManager->checkCurrentSession();
if (! $this->applicationAuthorization->isAllowed($controller, $action, Role::APP_PUBLIC)) {
if (! $this->applicationAuthorization->isAllowed($this->router->getController(), $this->router->getAction(), Role::APP_PUBLIC)) {
$this->handleAuthentication();
$this->handlePostAuthentication($controller, $action);
$this->checkApplicationAuthorization($controller, $action);
$this->checkProjectAuthorization($controller, $action);
$this->handlePostAuthentication();
$this->checkApplicationAuthorization();
$this->checkProjectAuthorization();
}
}
@ -37,7 +37,7 @@ abstract class Base extends \Kanboard\Core\Base
*
* @access private
*/
private function sendHeaders($action)
private function sendHeaders()
{
// HTTP secure headers
$this->response->csp($this->container['cspRules']);
@ -45,7 +45,7 @@ abstract class Base extends \Kanboard\Core\Base
$this->response->xss();
// Allow the public board iframe inclusion
if (ENABLE_XFRAME && $action !== 'readonly') {
if (ENABLE_XFRAME && $this->router->getAction() !== 'readonly') {
$this->response->xframe();
}
@ -76,8 +76,10 @@ abstract class Base extends \Kanboard\Core\Base
*
* @access private
*/
private function handlePostAuthentication($controller, $action)
private function handlePostAuthentication()
{
$controller = strtolower($this->router->getController());
$action = strtolower($this->router->getAction());
$ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout');
if ($ignore === false && $this->userSession->hasPostAuthentication() && ! $this->userSession->isPostAuthenticationValidated()) {
@ -94,9 +96,9 @@ abstract class Base extends \Kanboard\Core\Base
*
* @access private
*/
private function checkApplicationAuthorization($controller, $action)
private function checkApplicationAuthorization()
{
if (! $this->helper->user->hasAccess($controller, $action)) {
if (! $this->helper->user->hasAccess($this->router->getController(), $this->router->getAction())) {
$this->forbidden();
}
}
@ -106,7 +108,7 @@ abstract class Base extends \Kanboard\Core\Base
*
* @access private
*/
private function checkProjectAuthorization($controller, $action)
private function checkProjectAuthorization()
{
$project_id = $this->request->getIntegerParam('project_id');
$task_id = $this->request->getIntegerParam('task_id');
@ -116,7 +118,7 @@ abstract class Base extends \Kanboard\Core\Base
$project_id = $this->taskFinder->getProjectId($task_id);
}
if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($controller, $action, $project_id)) {
if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($this->router->getController(), $this->router->getAction(), $project_id)) {
$this->forbidden();
}
}
@ -144,7 +146,7 @@ abstract class Base extends \Kanboard\Core\Base
protected function forbidden($no_layout = false)
{
if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401);
$this->response->text('Access Forbidden', 403);
}
$this->response->html($this->template->layout('app/forbidden', array(
@ -190,7 +192,7 @@ abstract class Base extends \Kanboard\Core\Base
$content = $this->template->render($template, $params);
$params['task_content_for_layout'] = $content;
$params['title'] = $params['task']['project_name'].' &gt; '.$params['task']['title'];
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
return $this->template->layout('task/layout', $params);
}
@ -208,7 +210,7 @@ abstract class Base extends \Kanboard\Core\Base
$content = $this->template->render($template, $params);
$params['project_content_for_layout'] = $content;
$params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' &gt; '.$params['title'];
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
$params['sidebar_template'] = $sidebar_template;
return $this->template->layout('project/layout', $params);
@ -289,7 +291,7 @@ abstract class Base extends \Kanboard\Core\Base
{
$project = $this->getProject();
$search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id']));
$board_selector = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$board_selector = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
unset($board_selector[$project['id']]);
$filters = array(

View file

@ -73,10 +73,6 @@ class Board extends Base
return $this->response->status(403);
}
if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
$this->response->text('Forbidden', 403);
}
$values = $this->request->getJson();
$result =$this->taskPosition->movePosition(
@ -101,22 +97,18 @@ class Board extends Base
*/
public function check()
{
if (! $this->request->isAjax()) {
return $this->response->status(403);
}
$project_id = $this->request->getIntegerParam('project_id');
$timestamp = $this->request->getIntegerParam('timestamp');
if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
$this->response->text('Forbidden', 403);
if (! $project_id || ! $this->request->isAjax()) {
return $this->response->status(403);
}
if (! $this->project->isModifiedSince($project_id, $timestamp)) {
return $this->response->status(304);
}
$this->response->html($this->renderBoard($project_id));
return $this->response->html($this->renderBoard($project_id));
}
/**
@ -126,14 +118,10 @@ class Board extends Base
*/
public function reload()
{
if (! $this->request->isAjax()) {
return $this->response->status(403);
}
$project_id = $this->request->getIntegerParam('project_id');
if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
$this->response->text('Forbidden', 403);
if (! $project_id || ! $this->request->isAjax()) {
return $this->response->status(403);
}
$values = $this->request->getJson();

View file

@ -98,4 +98,39 @@ class BoardPopover extends Base
'redirect' => 'board',
)));
}
/**
* Confirmation before to close all column tasks
*
* @access public
*/
public function confirmCloseColumnTasks()
{
$project = $this->getProject();
$column_id = $this->request->getIntegerParam('column_id');
$swimlane_id = $this->request->getIntegerParam('swimlane_id');
$this->response->html($this->template->render('board/popover_close_all_tasks_column', array(
'project' => $project,
'nb_tasks' => $this->taskFinder->countByColumnAndSwimlaneId($project['id'], $column_id, $swimlane_id),
'column' => $this->board->getColumnTitleById($column_id),
'swimlane' => $this->swimlane->getNameById($swimlane_id) ?: t($project['default_swimlane']),
'values' => array('column_id' => $column_id, 'swimlane_id' => $swimlane_id),
)));
}
/**
* Close all column tasks
*
* @access public
*/
public function closeColumnTasks()
{
$project = $this->getProject();
$values = $this->request->getValues();
$this->taskStatus->closeTasksBySwimlaneAndColumn($values['swimlane_id'], $values['column_id']);
$this->flash->success(t('All tasks of the column "%s" and the swimlane "%s" have been closed successfully.', $this->board->getColumnTitleById($values['column_id']), $this->swimlane->getNameById($values['swimlane_id']) ?: t($project['default_swimlane'])));
$this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id'])));
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Kanboard\Controller;
use Gregwar\Captcha\CaptchaBuilder;
/**
* Captcha Controller
*
* @package controller
* @author Frederic Guillot
*/
class Captcha extends Base
{
/**
* Display captcha image
*
* @access public
*/
public function image()
{
$this->response->contentType('image/jpeg');
$builder = new CaptchaBuilder;
$builder->build();
$this->sessionStorage->captcha = $builder->getPhrase();
$builder->output();
}
}

View file

@ -57,7 +57,7 @@ class Category extends Base
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->category->validateCreation($values);
list($valid, $errors) = $this->categoryValidator->validateCreation($values);
if ($valid) {
if ($this->category->create($values)) {
@ -99,7 +99,7 @@ class Category extends Base
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->category->validateModification($values);
list($valid, $errors) = $this->categoryValidator->validateModification($values);
if ($valid) {
if ($this->category->update($values)) {

View file

@ -51,7 +51,7 @@ class Column extends Base
$values['title['.$column_id.']'] = $column_title;
}
list($valid, $errors) = $this->board->validateCreation($data);
list($valid, $errors) = $this->columnValidator->validateCreation($data);
if ($valid) {
if ($this->board->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) {
@ -94,7 +94,7 @@ class Column extends Base
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->board->validateModification($values);
list($valid, $errors) = $this->columnValidator->validateModification($values);
if ($valid) {
if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) {

View file

@ -78,7 +78,7 @@ class Comment extends Base
$values = $this->request->getValues();
$ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
list($valid, $errors) = $this->comment->validateCreation($values);
list($valid, $errors) = $this->commentValidator->validateCreation($values);
if ($valid) {
if ($this->comment->create($values)) {
@ -127,7 +127,7 @@ class Comment extends Base
$comment = $this->getComment();
$values = $this->request->getValues();
list($valid, $errors) = $this->comment->validateModification($values);
list($valid, $errors) = $this->commentValidator->validateModification($values);
if ($valid) {
if ($this->comment->update($values)) {

View file

@ -20,7 +20,7 @@ class Config extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
$params['values'] = $this->config->getAll();
$params['errors'] = array();
$params['config_content_for_layout'] = $this->template->render($template, $params);
@ -40,6 +40,9 @@ class Config extends Base
$values = $this->request->getValues();
switch ($redirect) {
case 'application':
$values += array('password_reset' => 0);
break;
case 'project':
$values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'cfd_include_closed_tasks' => 0);
break;

View file

@ -20,7 +20,7 @@ class Currency extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
$params['config_content_for_layout'] = $this->template->render($template, $params);
return $this->template->layout('config/layout', $params);
@ -38,7 +38,7 @@ class Currency extends Base
'values' => $values,
'errors' => $errors,
'rates' => $this->currency->getAll(),
'currencies' => $this->config->getCurrencies(),
'currencies' => $this->currency->getCurrencies(),
'title' => t('Settings').' &gt; '.t('Currency rates'),
)));
}
@ -51,7 +51,7 @@ class Currency extends Base
public function create()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->currency->validate($values);
list($valid, $errors) = $this->currencyValidator->validateCreation($values);
if ($valid) {
if ($this->currency->create($values['currency'], $values['rate'])) {

View file

@ -42,7 +42,7 @@ class Customfilter extends Base
$values = $this->request->getValues();
$values['user_id'] = $this->userSession->getId();
list($valid, $errors) = $this->customFilter->validateCreation($values);
list($valid, $errors) = $this->customFilterValidator->validateCreation($values);
if ($valid) {
if ($this->customFilter->create($values)) {
@ -121,7 +121,7 @@ class Customfilter extends Base
$values += array('append' => 0);
}
list($valid, $errors) = $this->customFilter->validateModification($values);
list($valid, $errors) = $this->customFilterValidator->validateModification($values);
if ($valid) {
if ($this->customFilter->update($values)) {

View file

@ -53,7 +53,7 @@ class Doc extends Base
}
$this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
)));
}
}

View file

@ -26,7 +26,7 @@ class Gantt extends Base
$this->response->html($this->template->layout('gantt/projects', array(
'projects' => $this->projectGanttFormatter->filter($project_ids)->format(),
'title' => t('Gantt chart for all projects'),
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
)));
}

View file

@ -25,7 +25,7 @@ class Group extends Base
->calculate();
$this->response->html($this->template->layout('group/index', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'title' => t('Groups').' ('.$paginator->getTotal().')',
'paginator' => $paginator,
)));
@ -42,14 +42,14 @@ class Group extends Base
$group = $this->group->getById($group_id);
$paginator = $this->paginator
->setUrl('group', 'users')
->setUrl('group', 'users', array('group_id' => $group_id))
->setMax(30)
->setOrder('username')
->setQuery($this->groupMember->getQuery($group_id))
->calculate();
$this->response->html($this->template->layout('group/users', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'title' => t('Members of %s', $group['name']).' ('.$paginator->getTotal().')',
'paginator' => $paginator,
'group' => $group,
@ -64,7 +64,7 @@ class Group extends Base
public function create(array $values = array(), array $errors = array())
{
$this->response->html($this->template->layout('group/create', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'errors' => $errors,
'values' => $values,
'title' => t('New group')
@ -79,7 +79,7 @@ class Group extends Base
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->group->validateCreation($values);
list($valid, $errors) = $this->groupValidator->validateCreation($values);
if ($valid) {
if ($this->group->create($values['name']) !== false) {
@ -105,7 +105,7 @@ class Group extends Base
}
$this->response->html($this->template->layout('group/edit', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'errors' => $errors,
'values' => $values,
'title' => t('Edit group')
@ -120,7 +120,7 @@ class Group extends Base
public function update()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->group->validateModification($values);
list($valid, $errors) = $this->groupValidator->validateModification($values);
if ($valid) {
if ($this->group->update($values) !== false) {
@ -149,7 +149,7 @@ class Group extends Base
}
$this->response->html($this->template->layout('group/associate', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'users' => $this->user->prepareList($this->groupMember->getNotMembers($group_id)),
'group' => $group,
'errors' => $errors,

View file

@ -21,7 +21,7 @@ class Link extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
$params['config_content_for_layout'] = $this->template->render($template, $params);
return $this->template->layout('config/layout', $params);
@ -67,7 +67,7 @@ class Link extends Base
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->link->validateCreation($values);
list($valid, $errors) = $this->linkValidator->validateCreation($values);
if ($valid) {
if ($this->link->create($values['label'], $values['opposite_label']) !== false) {
@ -108,7 +108,7 @@ class Link extends Base
public function update()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->link->validateModification($values);
list($valid, $errors) = $this->linkValidator->validateModification($values);
if ($valid) {
if ($this->link->update($values)) {

View file

@ -0,0 +1,120 @@
<?php
namespace Kanboard\Controller;
/**
* Password Reset Controller
*
* @package controller
* @author Frederic Guillot
*/
class PasswordReset extends Base
{
/**
* Show the form to reset the password
*/
public function create(array $values = array(), array $errors = array())
{
$this->checkActivation();
$this->response->html($this->template->layout('password_reset/create', array(
'errors' => $errors,
'values' => $values,
'no_layout' => true,
)));
}
/**
* Validate and send the email
*/
public function save()
{
$this->checkActivation();
$values = $this->request->getValues();
list($valid, $errors) = $this->passwordResetValidator->validateCreation($values);
if ($valid) {
$this->sendEmail($values['username']);
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
$this->create($values, $errors);
}
/**
* Show the form to set a new password
*/
public function change(array $values = array(), array $errors = array())
{
$this->checkActivation();
$token = $this->request->getStringParam('token');
$user_id = $this->passwordReset->getUserIdByToken($token);
if ($user_id !== false) {
$this->response->html($this->template->layout('password_reset/change', array(
'token' => $token,
'errors' => $errors,
'values' => $values,
'no_layout' => true,
)));
}
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
/**
* Set the new password
*/
public function update()
{
$this->checkActivation();
$token = $this->request->getStringParam('token');
$values = $this->request->getValues();
list($valid, $errors) = $this->passwordResetValidator->validateModification($values);
if ($valid) {
$user_id = $this->passwordReset->getUserIdByToken($token);
if ($user_id !== false) {
$this->user->update(array('id' => $user_id, 'password' => $values['password']));
$this->passwordReset->disable($user_id);
}
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
$this->change($values, $errors);
}
/**
* Send the email
*/
private function sendEmail($username)
{
$token = $this->passwordReset->create($username);
if ($token !== false) {
$user = $this->user->getByUsername($username);
$this->emailClient->send(
$user['email'],
$user['name'] ?: $user['username'],
t('Password Reset for Kanboard'),
$this->template->render('password_reset/email', array('token' => $token))
);
}
}
/**
* Check feature availability
*/
private function checkActivation()
{
if ($this->config->get('password_reset', 0) == 0) {
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
}
}

View file

@ -33,7 +33,7 @@ class Project extends Base
->calculate();
$this->response->html($this->template->layout('project/index', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'paginator' => $paginator,
'nb_projects' => $nb_projects,
'title' => t('Projects').' ('.$nb_projects.')'
@ -169,7 +169,7 @@ class Project extends Base
}
}
list($valid, $errors) = $this->project->validateModification($values);
list($valid, $errors) = $this->projectValidator->validateModification($values);
if ($valid) {
if ($this->project->update($values)) {
@ -302,7 +302,7 @@ class Project extends Base
$is_private = isset($values['is_private']) && $values['is_private'] == 1;
$this->response->html($this->template->layout('project/new', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'values' => $values,
'errors' => $errors,
'is_private' => $is_private,
@ -329,7 +329,7 @@ class Project extends Base
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->project->validateCreation($values);
list($valid, $errors) = $this->projectValidator->validateCreation($values);
if ($valid) {
$project_id = $this->project->create($values, $this->userSession->getId(), true);

View file

@ -24,7 +24,7 @@ class Projectuser extends Base
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
$params['content_for_sublayout'] = $this->template->render($template, $params);
$params['filter'] = array('user_id' => $params['user_id']);

View file

@ -63,7 +63,7 @@ class Subtask extends Base
$task = $this->getTask();
$values = $this->request->getValues();
list($valid, $errors) = $this->subtask->validateCreation($values);
list($valid, $errors) = $this->subtaskValidator->validateCreation($values);
if ($valid) {
if ($this->subtask->create($values)) {
@ -113,7 +113,7 @@ class Subtask extends Base
$this->getSubtask();
$values = $this->request->getValues();
list($valid, $errors) = $this->subtask->validateModification($values);
list($valid, $errors) = $this->subtaskValidator->validateModification($values);
if ($valid) {
if ($this->subtask->update($values)) {

View file

@ -60,7 +60,7 @@ class Swimlane extends Base
{
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->swimlane->validateCreation($values);
list($valid, $errors) = $this->swimlaneValidator->validateCreation($values);
if ($valid) {
if ($this->swimlane->create($values)) {
@ -84,7 +84,7 @@ class Swimlane extends Base
$project = $this->getProject();
$values = $this->request->getValues() + array('show_default_swimlane' => 0);
list($valid, ) = $this->swimlane->validateDefaultModification($values);
list($valid, ) = $this->swimlaneValidator->validateDefaultModification($values);
if ($valid) {
if ($this->swimlane->updateDefault($values)) {
@ -126,7 +126,7 @@ class Swimlane extends Base
$project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->swimlane->validateModification($values);
list($valid, $errors) = $this->swimlaneValidator->validateModification($values);
if ($valid) {
if ($this->swimlane->update($values)) {

View file

@ -69,7 +69,7 @@ class Tasklink extends Base
$values = $this->request->getValues();
$ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
list($valid, $errors) = $this->taskLink->validateCreation($values);
list($valid, $errors) = $this->taskLinkValidator->validateCreation($values);
if ($valid) {
if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) {
@ -125,7 +125,7 @@ class Tasklink extends Base
$task = $this->getTask();
$values = $this->request->getValues();
list($valid, $errors) = $this->taskLink->validateModification($values);
list($valid, $errors) = $this->taskLinkValidator->validateModification($values);
if ($valid) {
if ($this->taskLink->update($values['id'], $values['task_id'], $values['opposite_task_id'], $values['link_id'])) {

View file

@ -23,7 +23,7 @@ class Twofactor extends User
}
/**
* Index
* Show form to disable/enable 2FA
*
* @access public
*/
@ -31,54 +31,45 @@ class Twofactor extends User
{
$user = $this->getUser();
$this->checkCurrentUser($user);
$provider = $this->authenticationManager->getPostAuthenticationProvider();
$label = $user['email'] ?: $user['username'];
$provider->setSecret($user['twofactor_secret']);
unset($this->sessionStorage->twoFactorSecret);
$this->response->html($this->layout('twofactor/index', array(
'user' => $user,
'qrcode_url' => $user['twofactor_activated'] == 1 ? $provider->getQrCodeUrl($label) : '',
'key_url' => $user['twofactor_activated'] == 1 ? $provider->getKeyUrl($label) : '',
'provider' => $this->authenticationManager->getPostAuthenticationProvider()->getName(),
)));
}
/**
* Enable/disable 2FA
* Show page with secret and test form
*
* @access public
*/
public function save()
public function show()
{
$user = $this->getUser();
$this->checkCurrentUser($user);
$values = $this->request->getValues();
$label = $user['email'] ?: $user['username'];
$provider = $this->authenticationManager->getPostAuthenticationProvider();
if (isset($values['twofactor_activated']) && $values['twofactor_activated'] == 1) {
$this->user->update(array(
'id' => $user['id'],
'twofactor_activated' => 1,
'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(),
));
if (! isset($this->sessionStorage->twoFactorSecret)) {
$provider->generateSecret();
$provider->beforeCode();
$this->sessionStorage->twoFactorSecret = $provider->getSecret();
} else {
$this->user->update(array(
'id' => $user['id'],
'twofactor_activated' => 0,
'twofactor_secret' => '',
));
$provider->setSecret($this->sessionStorage->twoFactorSecret);
}
// Allow the user to test or disable the feature
$this->userSession->disablePostAuthentication();
$this->flash->success(t('User updated successfully.'));
$this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id'])));
$this->response->html($this->layout('twofactor/show', array(
'user' => $user,
'secret' => $this->sessionStorage->twoFactorSecret,
'qrcode_url' => $provider->getQrCodeUrl($label),
'key_url' => $provider->getKeyUrl($label),
)));
}
/**
* Test code
* Test code and save secret
*
* @access public
*/
@ -91,14 +82,47 @@ class Twofactor extends User
$provider = $this->authenticationManager->getPostAuthenticationProvider();
$provider->setCode(empty($values['code']) ? '' : $values['code']);
$provider->setSecret($user['twofactor_secret']);
$provider->setSecret($this->sessionStorage->twoFactorSecret);
if ($provider->authenticate()) {
$this->flash->success(t('The two factor authentication code is valid.'));
$this->user->update(array(
'id' => $user['id'],
'twofactor_activated' => 1,
'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(),
));
unset($this->sessionStorage->twoFactorSecret);
$this->userSession->disablePostAuthentication();
$this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id'])));
} else {
$this->flash->failure(t('The two factor authentication code is not valid.'));
$this->response->redirect($this->helper->url->to('twofactor', 'show', array('user_id' => $user['id'])));
}
}
/**
* Disable 2FA for the current user
*
* @access public
*/
public function deactivate()
{
$user = $this->getUser();
$this->checkCurrentUser($user);
$this->user->update(array(
'id' => $user['id'],
'twofactor_activated' => 0,
'twofactor_secret' => '',
));
// Allow the user to test or disable the feature
$this->userSession->disablePostAuthentication();
$this->flash->success(t('User updated successfully.'));
$this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id'])));
}
@ -135,6 +159,12 @@ class Twofactor extends User
*/
public function code()
{
if (! isset($this->sessionStorage->twoFactorBeforeCodeCalled)) {
$provider = $this->authenticationManager->getPostAuthenticationProvider();
$provider->beforeCode();
$this->sessionStorage->twoFactorBeforeCodeCalled = true;
}
$this->response->html($this->template->layout('twofactor/check', array(
'title' => t('Check two factor authentication code'),
)));

View file

@ -26,7 +26,7 @@ class User extends Base
{
$content = $this->template->render($template, $params);
$params['user_content_for_layout'] = $content;
$params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
if (isset($params['user'])) {
$params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')';
@ -51,12 +51,34 @@ class User extends Base
$this->response->html(
$this->template->layout('user/index', array(
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'title' => t('Users').' ('.$paginator->getTotal().')',
'paginator' => $paginator,
)));
}
/**
* Public user profile
*
* @access public
*/
public function profile()
{
$user = $this->user->getById($this->request->getIntegerParam('user_id'));
if (empty($user)) {
$this->notfound();
}
$this->response->html(
$this->template->layout('user/profile', array(
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'title' => $user['name'] ?: $user['username'],
'user' => $user,
)
));
}
/**
* Display a form to create a new user
*
@ -70,7 +92,7 @@ class User extends Base
'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(true),
'roles' => $this->role->getApplicationRoles(),
'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()),
'projects' => $this->project->getList(),
'errors' => $errors,
'values' => $values + array('role' => Role::APP_USER),
@ -86,7 +108,7 @@ class User extends Base
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->user->validateCreation($values);
list($valid, $errors) = $this->userValidator->validateCreation($values);
if ($valid) {
$project_id = empty($values['project_id']) ? 0 : $values['project_id'];
@ -150,6 +172,20 @@ class User extends Base
)));
}
/**
* Display last password reset
*
* @access public
*/
public function passwordReset()
{
$user = $this->getUser();
$this->response->html($this->layout('user/password_reset', array(
'tokens' => $this->passwordReset->getAll($user['id']),
'user' => $user,
)));
}
/**
* Display last connections
*
@ -293,7 +329,7 @@ class User extends Base
if ($this->request->isPost()) {
$values = $this->request->getValues();
list($valid, $errors) = $this->user->validatePasswordModification($values);
list($valid, $errors) = $this->userValidator->validatePasswordModification($values);
if ($valid) {
if ($this->user->update($values)) {
@ -335,7 +371,7 @@ class User extends Base
}
}
list($valid, $errors) = $this->user->validateModification($values);
list($valid, $errors) = $this->userValidator->validateModification($values);
if ($valid) {
if ($this->user->update($values)) {
@ -373,7 +409,7 @@ class User extends Base
if ($this->request->isPost()) {
$values = $this->request->getValues() + array('disable_login_form' => 0, 'is_ldap_user' => 0);
list($valid, $errors) = $this->user->validateModification($values);
list($valid, $errors) = $this->userValidator->validateModification($values);
if ($valid) {
if ($this->user->update($values)) {

View file

@ -21,4 +21,17 @@ class UserHelper extends Base
$users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format();
$this->response->json($users);
}
/**
* User mention autocompletion (Ajax)
*
* @access public
*/
public function mention()
{
$project_id = $this->request->getStringParam('project_id');
$query = $this->request->getStringParam('q');
$users = $this->projectPermission->findUsernames($project_id, $query);
$this->response->json($users);
}
}

View file

@ -39,57 +39,4 @@ class Webhook extends Base
$this->response->text('FAILED');
}
/**
* Handle Github webhooks
*
* @access public
*/
public function github()
{
$this->checkWebhookToken();
$this->githubWebhook->setProjectId($this->request->getIntegerParam('project_id'));
$result = $this->githubWebhook->parsePayload(
$this->request->getHeader('X-Github-Event'),
$this->request->getJson()
);
echo $result ? 'PARSED' : 'IGNORED';
}
/**
* Handle Gitlab webhooks
*
* @access public
*/
public function gitlab()
{
$this->checkWebhookToken();
$this->gitlabWebhook->setProjectId($this->request->getIntegerParam('project_id'));
$result = $this->gitlabWebhook->parsePayload($this->request->getJson());
echo $result ? 'PARSED' : 'IGNORED';
}
/**
* Handle Bitbucket webhooks
*
* @access public
*/
public function bitbucket()
{
$this->checkWebhookToken();
$this->bitbucketWebhook->setProjectId($this->request->getIntegerParam('project_id'));
$result = $this->bitbucketWebhook->parsePayload(
$this->request->getHeader('X-Event-Key'),
$this->request->getJson()
);
echo $result ? 'PARSED' : 'IGNORED';
}
}

View file

@ -0,0 +1,142 @@
<?php
namespace Kanboard\Core\Action;
use RuntimeException;
use Kanboard\Core\Base;
use Kanboard\Action\Base as ActionBase;
/**
* Action Manager
*
* @package action
* @author Frederic Guillot
*/
class ActionManager extends Base
{
/**
* List of automatic actions
*
* @access private
* @var array
*/
private $actions = array();
/**
* Register a new automatic action
*
* @access public
* @param ActionBase $action
* @return ActionManager
*/
public function register(ActionBase $action)
{
$this->actions[$action->getName()] = $action;
return $this;
}
/**
* Get automatic action instance
*
* @access public
* @param string $name Absolute class name with namespace
* @return ActionBase
*/
public function getAction($name)
{
if (isset($this->actions[$name])) {
return $this->actions[$name];
}
throw new RuntimeException('Automatic Action Not Found: '.$name);
}
/**
* Get available automatic actions
*
* @access public
* @return array
*/
public function getAvailableActions()
{
$actions = array();
foreach ($this->actions as $action) {
if (count($action->getEvents()) > 0) {
$actions[$action->getName()] = $action->getDescription();
}
}
asort($actions);
return $actions;
}
/**
* Get all available action parameters
*
* @access public
* @param array $actions
* @return array
*/
public function getAvailableParameters(array $actions)
{
$params = array();
foreach ($actions as $action) {
$currentAction = $this->getAction($action['action_name']);
$params[$currentAction->getName()] = $currentAction->getActionRequiredParameters();
}
return $params;
}
/**
* Get list of compatible events for a given action
*
* @access public
* @param string $name
* @return array
*/
public function getCompatibleEvents($name)
{
$events = array();
$actionEvents = $this->getAction($name)->getEvents();
foreach ($this->eventManager->getAll() as $event => $description) {
if (in_array($event, $actionEvents)) {
$events[$event] = $description;
}
}
return $events;
}
/**
* Bind automatic actions to events
*
* @access public
* @return ActionManager
*/
public function attachEvents()
{
if ($this->userSession->isLogged()) {
$actions = $this->action->getAllByUser($this->userSession->getId());
} else {
$actions = $this->action->getAll();
}
foreach ($actions as $action) {
$listener = clone $this->getAction($action['action_name']);
$listener->setProjectId($action['project_id']);
foreach ($action['params'] as $param_name => $param_value) {
$listener->setParam($param_name, $param_value);
}
$this->dispatcher->addListener($action['event_name'], array($listener, 'execute'));
}
return $this;
}
}

View file

@ -10,7 +10,14 @@ use Pimple\Container;
* @package core
* @author Frederic Guillot
*
* @property \Kanboard\Analytic\TaskDistributionAnalytic $taskDistributionAnalytic
* @property \Kanboard\Analytic\UserDistributionAnalytic $userDistributionAnalytic
* @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic
* @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic
* @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic
* @property \Kanboard\Core\Action\ActionManager $actionManager
* @property \Kanboard\Core\Cache\MemoryCache $memoryCache
* @property \Kanboard\Core\Event\EventManager $eventManager
* @property \Kanboard\Core\Group\GroupManager $groupManager
* @property \Kanboard\Core\Http\Client $httpClient
* @property \Kanboard\Core\Http\OAuth2 $oauth
@ -18,6 +25,7 @@ use Pimple\Container;
* @property \Kanboard\Core\Http\Request $request
* @property \Kanboard\Core\Http\Response $response
* @property \Kanboard\Core\Http\Router $router
* @property \Kanboard\Core\Http\Route $route
* @property \Kanboard\Core\Mail\Client $emailClient
* @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage
* @property \Kanboard\Core\Plugin\Hook $hook
@ -42,9 +50,6 @@ use Pimple\Container;
* @property \Kanboard\Core\Lexer $lexer
* @property \Kanboard\Core\Paginator $paginator
* @property \Kanboard\Core\Template $template
* @property \Kanboard\Integration\BitbucketWebhook $bitbucketWebhook
* @property \Kanboard\Integration\GithubWebhook $githubWebhook
* @property \Kanboard\Integration\GitlabWebhook $gitlabWebhook
* @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter
* @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter
* @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter
@ -53,7 +58,7 @@ use Pimple\Container;
* @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter
* @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter
* @property \Kanboard\Model\Action $action
* @property \Kanboard\Model\Authentication $authentication
* @property \Kanboard\Model\ActionParameter $actionParameter
* @property \Kanboard\Model\Board $board
* @property \Kanboard\Model\Category $category
* @property \Kanboard\Model\Color $color
@ -68,6 +73,7 @@ use Pimple\Container;
* @property \Kanboard\Model\Link $link
* @property \Kanboard\Model\Notification $notification
* @property \Kanboard\Model\OverdueNotification $overdueNotification
* @property \Kanboard\Model\PasswordReset $passwordReset
* @property \Kanboard\Model\Project $project
* @property \Kanboard\Model\ProjectActivity $projectActivity
* @property \Kanboard\Model\ProjectAnalytic $projectAnalytic
@ -77,6 +83,7 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectMetadata $projectMetadata
* @property \Kanboard\Model\ProjectPermission $projectPermission
* @property \Kanboard\Model\ProjectUserRole $projectUserRole
* @property \Kanboard\Model\projectUserRoleFilter $projectUserRoleFilter
* @property \Kanboard\Model\ProjectGroupRole $projectGroupRole
* @property \Kanboard\Model\ProjectNotification $projectNotification
* @property \Kanboard\Model\ProjectNotificationType $projectNotificationType
@ -98,20 +105,36 @@ use Pimple\Container;
* @property \Kanboard\Model\TaskPermission $taskPermission
* @property \Kanboard\Model\TaskPosition $taskPosition
* @property \Kanboard\Model\TaskStatus $taskStatus
* @property \Kanboard\Model\TaskValidator $taskValidator
* @property \Kanboard\Model\TaskMetadata $taskMetadata
* @property \Kanboard\Model\Transition $transition
* @property \Kanboard\Model\User $user
* @property \Kanboard\Model\UserImport $userImport
* @property \Kanboard\Model\UserLocking $userLocking
* @property \Kanboard\Model\UserMention $userMention
* @property \Kanboard\Model\UserNotification $userNotification
* @property \Kanboard\Model\UserNotificationType $userNotificationType
* @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter
* @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification
* @property \Kanboard\Model\UserMetadata $userMetadata
* @property \Kanboard\Model\Webhook $webhook
* @property \Kanboard\Validator\ActionValidator $actionValidator
* @property \Kanboard\Validator\AuthValidator $authValidator
* @property \Kanboard\Validator\ColumnValidator $columnValidator
* @property \Kanboard\Validator\CategoryValidator $categoryValidator
* @property \Kanboard\Validator\ColumnValidator $columnValidator
* @property \Kanboard\Validator\CommentValidator $commentValidator
* @property \Kanboard\Validator\CurrencyValidator $currencyValidator
* @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator
* @property \Kanboard\Validator\GroupValidator $groupValidator
* @property \Kanboard\Validator\LinkValidator $linkValidator
* @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
* @property \Kanboard\Validator\ProjectValidator $projectValidator
* @property \Kanboard\Validator\SubtaskValidator $subtaskValidator
* @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator
* @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator
* @property \Kanboard\Validator\TaskValidator $taskValidator
* @property \Kanboard\Validator\UserValidator $userValidator
* @property \Psr\Log\LoggerInterface $logger
* @property \League\HTMLToMarkdown\HtmlConverter $htmlConverter
* @property \PicoDb\Database $db
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/

View file

@ -0,0 +1,62 @@
<?php
namespace Kanboard\Core\Event;
use Kanboard\Model\Task;
use Kanboard\Model\TaskLink;
/**
* Event Manager
*
* @package event
* @author Frederic Guillot
*/
class EventManager
{
/**
* Extended events
*
* @access private
* @var array
*/
private $events = array();
/**
* Add new event
*
* @access public
* @param string $event
* @param string $description
* @return EventManager
*/
public function register($event, $description)
{
$this->events[$event] = $description;
return $this;
}
/**
* Get the list of events and description that can be used from the user interface
*
* @access public
* @return array
*/
public function getAll()
{
$events = array(
TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'),
Task::EVENT_MOVE_COLUMN => t('Move a task to another column'),
Task::EVENT_UPDATE => t('Task modification'),
Task::EVENT_CREATE => t('Task creation'),
Task::EVENT_OPEN => t('Reopen a task'),
Task::EVENT_CLOSE => t('Closing a task'),
Task::EVENT_CREATE_UPDATE => t('Task creation or modification'),
Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'),
);
$events = array_merge($events, $this->events);
asort($events);
return $events;
}
}

View file

@ -41,6 +41,16 @@ class Request extends Base
$this->cookies = empty($cookies) ? $_COOKIE : $cookies;
}
/**
* Set GET parameters
*
* @param array $params
*/
public function setParams(array $params)
{
$this->get = array_merge($this->get, $params);
}
/**
* Get query string string parameter
*
@ -146,6 +156,29 @@ class Request extends Base
return isset($this->files[$name]['tmp_name']) ? $this->files[$name]['tmp_name'] : '';
}
/**
* Get info of an uploaded file
*
* @access public
* @param string $name Form file name
* @return array
*/
public function getFileInfo($name)
{
return isset($this->files[$name]) ? $this->files[$name] : array();
}
/**
* Return HTTP method
*
* @access public
* @return bool
*/
public function getMethod()
{
return $this->getServerVariable('REQUEST_METHOD');
}
/**
* Return true if the HTTP request is sent with the POST method
*
@ -154,7 +187,7 @@ class Request extends Base
*/
public function isPost()
{
return isset($this->server['REQUEST_METHOD']) && $this->server['REQUEST_METHOD'] === 'POST';
return $this->getServerVariable('REQUEST_METHOD') === 'POST';
}
/**
@ -203,7 +236,7 @@ class Request extends Base
public function getHeader($name)
{
$name = 'HTTP_'.str_replace('-', '_', strtoupper($name));
return isset($this->server[$name]) ? $this->server[$name] : '';
return $this->getServerVariable($name);
}
/**
@ -214,18 +247,18 @@ class Request extends Base
*/
public function getRemoteUser()
{
return isset($this->server[REVERSE_PROXY_USER_HEADER]) ? $this->server[REVERSE_PROXY_USER_HEADER] : '';
return $this->getServerVariable(REVERSE_PROXY_USER_HEADER);
}
/**
* Returns current request's query string, useful for redirecting
* Returns query string
*
* @access public
* @return string
*/
public function getQueryString()
{
return isset($this->server['QUERY_STRING']) ? $this->server['QUERY_STRING'] : '';
return $this->getServerVariable('QUERY_STRING');
}
/**
@ -236,7 +269,7 @@ class Request extends Base
*/
public function getUri()
{
return isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
return $this->getServerVariable('REQUEST_URI');
}
/**
@ -269,7 +302,7 @@ class Request extends Base
);
foreach ($keys as $key) {
if (! empty($this->server[$key])) {
if ($this->getServerVariable($key) !== '') {
foreach (explode(',', $this->server[$key]) as $ipAddress) {
return trim($ipAddress);
}
@ -287,6 +320,18 @@ class Request extends Base
*/
public function getStartTime()
{
return isset($this->server['REQUEST_TIME_FLOAT']) ? $this->server['REQUEST_TIME_FLOAT'] : 0;
return $this->getServerVariable('REQUEST_TIME_FLOAT') ?: 0;
}
/**
* Get server variable
*
* @access public
* @param string $variable
* @return string
*/
public function getServerVariable($variable)
{
return isset($this->server[$variable]) ? $this->server[$variable] : '';
}
}

View file

@ -60,7 +60,7 @@ class Response extends Base
public function status($status_code)
{
header('Status: '.$status_code);
header($_SERVER['SERVER_PROTOCOL'].' '.$status_code);
header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$status_code);
}
/**
@ -71,7 +71,7 @@ class Response extends Base
*/
public function redirect($url)
{
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
if ($this->request->getServerVariable('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest') {
header('X-Ajax-Redirect: '.$url);
} else {
header('Location: '.$url);
@ -220,7 +220,6 @@ class Response extends Base
*/
public function csp(array $policies = array())
{
$policies['default-src'] = "'self'";
$values = '';
foreach ($policies as $policy => $acl) {

View file

@ -0,0 +1,187 @@
<?php
namespace Kanboard\Core\Http;
use Kanboard\Core\Base;
/**
* Route Handler
*
* @package http
* @author Frederic Guillot
*/
class Route extends Base
{
/**
* Flag that enable the routing table
*
* @access private
* @var boolean
*/
private $activated = false;
/**
* Store routes for path lookup
*
* @access private
* @var array
*/
private $paths = array();
/**
* Store routes for url lookup
*
* @access private
* @var array
*/
private $urls = array();
/**
* Enable routing table
*
* @access public
* @return Route
*/
public function enable()
{
$this->activated = true;
return $this;
}
/**
* Add route
*
* @access public
* @param string $path
* @param string $controller
* @param string $action
* @param string $plugin
* @return Route
*/
public function addRoute($path, $controller, $action, $plugin = '')
{
if ($this->activated) {
$path = ltrim($path, '/');
$items = explode('/', $path);
$params = $this->findParams($items);
$this->paths[] = array(
'items' => $items,
'count' => count($items),
'controller' => $controller,
'action' => $action,
'plugin' => $plugin,
);
$this->urls[$plugin][$controller][$action][] = array(
'path' => $path,
'params' => $params,
'count' => count($params),
);
}
return $this;
}
/**
* Find a route according to the given path
*
* @access public
* @param string $path
* @return array
*/
public function findRoute($path)
{
$items = explode('/', ltrim($path, '/'));
$count = count($items);
foreach ($this->paths as $route) {
if ($count === $route['count']) {
$params = array();
for ($i = 0; $i < $count; $i++) {
if ($route['items'][$i]{0} === ':') {
$params[substr($route['items'][$i], 1)] = $items[$i];
} elseif ($route['items'][$i] !== $items[$i]) {
break;
}
}
if ($i === $count) {
$this->request->setParams($params);
return array(
'controller' => $route['controller'],
'action' => $route['action'],
'plugin' => $route['plugin'],
);
}
}
}
return array(
'controller' => 'app',
'action' => 'index',
'plugin' => '',
);
}
/**
* Find route url
*
* @access public
* @param string $controller
* @param string $action
* @param array $params
* @param string $plugin
* @return string
*/
public function findUrl($controller, $action, array $params = array(), $plugin = '')
{
if ($plugin === '' && isset($params['plugin'])) {
$plugin = $params['plugin'];
unset($params['plugin']);
}
if (! isset($this->urls[$plugin][$controller][$action])) {
return '';
}
foreach ($this->urls[$plugin][$controller][$action] as $route) {
if (array_diff_key($params, $route['params']) === array()) {
$url = $route['path'];
$i = 0;
foreach ($params as $variable => $value) {
$url = str_replace(':'.$variable, $value, $url);
$i++;
}
if ($i === $route['count']) {
return $url;
}
}
}
return '';
}
/**
* Find url params
*
* @access public
* @param array $items
* @return array
*/
public function findParams(array $items)
{
$params = array();
foreach ($items as $item) {
if ($item !== '' && $item{0} === ':') {
$params[substr($item, 1)] = true;
}
}
return $params;
}
}

View file

@ -6,13 +6,21 @@ use RuntimeException;
use Kanboard\Core\Base;
/**
* Router class
* Route Dispatcher
*
* @package http
* @author Frederic Guillot
*/
class Router extends Base
{
/**
* Plugin name
*
* @access private
* @var string
*/
private $plugin = '';
/**
* Controller
*
@ -30,30 +38,14 @@ class Router extends Base
private $action = '';
/**
* Store routes for path lookup
*
* @access private
* @var array
*/
private $paths = array();
/**
* Store routes for url lookup
*
* @access private
* @var array
*/
private $urls = array();
/**
* Get action
* Get plugin name
*
* @access public
* @return string
*/
public function getAction()
public function getPlugin()
{
return $this->action;
return $this->plugin;
}
/**
@ -67,23 +59,32 @@ class Router extends Base
return $this->controller;
}
/**
* Get action
*
* @access public
* @return string
*/
public function getAction()
{
return $this->action;
}
/**
* Get the path to compare patterns
*
* @access public
* @param string $uri
* @param string $query_string
* @return string
*/
public function getPath($uri, $query_string = '')
public function getPath()
{
$path = substr($uri, strlen($this->helper->url->dir()));
$path = substr($this->request->getUri(), strlen($this->helper->url->dir()));
if (! empty($query_string)) {
$path = substr($path, 0, - strlen($query_string) - 1);
if ($this->request->getQueryString() !== '') {
$path = substr($path, 0, - strlen($this->request->getQueryString()) - 1);
}
if (! empty($path) && $path{0} === '/') {
if ($path !== '' && $path{0} === '/') {
$path = substr($path, 1);
}
@ -91,140 +92,78 @@ class Router extends Base
}
/**
* Add route
* Find controller/action from the route table or from get arguments
*
* @access public
* @param string $path
* @param string $controller
* @param string $action
* @param array $params
*/
public function addRoute($path, $controller, $action, array $params = array())
public function dispatch()
{
$pattern = explode('/', $path);
$controller = $this->request->getStringParam('controller');
$action = $this->request->getStringParam('action');
$plugin = $this->request->getStringParam('plugin');
$this->paths[] = array(
'pattern' => $pattern,
'count' => count($pattern),
'controller' => $controller,
'action' => $action,
);
$this->urls[$controller][$action][] = array(
'path' => $path,
'params' => array_flip($params),
'count' => count($params),
);
}
/**
* Find a route according to the given path
*
* @access public
* @param string $path
* @return array
*/
public function findRoute($path)
{
$parts = explode('/', $path);
$count = count($parts);
foreach ($this->paths as $route) {
if ($count === $route['count']) {
$params = array();
for ($i = 0; $i < $count; $i++) {
if ($route['pattern'][$i]{0} === ':') {
$params[substr($route['pattern'][$i], 1)] = $parts[$i];
} elseif ($route['pattern'][$i] !== $parts[$i]) {
break;
}
}
if ($i === $count) {
$_GET = array_merge($_GET, $params);
return array($route['controller'], $route['action']);
}
}
if ($controller === '') {
$route = $this->route->findRoute($this->getPath());
$controller = $route['controller'];
$action = $route['action'];
$plugin = $route['plugin'];
}
return array('app', 'index');
}
$this->controller = ucfirst($this->sanitize($controller, 'app'));
$this->action = $this->sanitize($action, 'index');
$this->plugin = ucfirst($this->sanitize($plugin));
/**
* Find route url
*
* @access public
* @param string $controller
* @param string $action
* @param array $params
* @return string
*/
public function findUrl($controller, $action, array $params = array())
{
if (! isset($this->urls[$controller][$action])) {
return '';
}
foreach ($this->urls[$controller][$action] as $pattern) {
if (array_diff_key($params, $pattern['params']) === array()) {
$url = $pattern['path'];
$i = 0;
foreach ($params as $variable => $value) {
$url = str_replace(':'.$variable, $value, $url);
$i++;
}
if ($i === $pattern['count']) {
return $url;
}
}
}
return '';
return $this->executeAction();
}
/**
* Check controller and action parameter
*
* @access public
* @param string $value Controller or action name
* @param string $default_value Default value if validation fail
* @param string $value
* @param string $default
* @return string
*/
public function sanitize($value, $default_value)
public function sanitize($value, $default = '')
{
return ! preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $default_value : $value;
return preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $value : $default;
}
/**
* Find controller/action from the route table or from get arguments
* Execute controller action
*
* @access public
* @param string $uri
* @param string $query_string
* @access private
*/
public function dispatch($uri, $query_string = '')
private function executeAction()
{
if (! empty($_GET['controller']) && ! empty($_GET['action'])) {
$this->controller = $this->sanitize($_GET['controller'], 'app');
$this->action = $this->sanitize($_GET['action'], 'index');
$plugin = ! empty($_GET['plugin']) ? $this->sanitize($_GET['plugin'], '') : '';
} else {
list($this->controller, $this->action) = $this->findRoute($this->getPath($uri, $query_string)); // TODO: add plugin for routes
$plugin = '';
$class = $this->getControllerClassName();
if (! class_exists($class)) {
throw new RuntimeException('Controller not found');
}
$class = '\Kanboard\\';
$class .= empty($plugin) ? 'Controller\\'.ucfirst($this->controller) : 'Plugin\\'.ucfirst($plugin).'\Controller\\'.ucfirst($this->controller);
if (! class_exists($class) || ! method_exists($class, $this->action)) {
throw new RuntimeException('Controller or method not found for the given url!');
if (! method_exists($class, $this->action)) {
throw new RuntimeException('Action not implemented');
}
$instance = new $class($this->container);
$instance->beforeAction($this->controller, $this->action);
$instance->beforeAction();
$instance->{$this->action}();
return $instance;
}
/**
* Get controller class name
*
* @access private
* @return string
*/
private function getControllerClassName()
{
if ($this->plugin !== '') {
return '\Kanboard\Plugin\\'.$this->plugin.'\Controller\\'.$this->controller;
}
return '\Kanboard\Controller\\'.$this->controller;
}
}

View file

@ -15,10 +15,10 @@ class Client
/**
* LDAP resource
*
* @access private
* @access protected
* @var resource
*/
private $ldap;
protected $ldap;
/**
* Establish LDAP connection

View file

@ -13,10 +13,10 @@ class Entries
/**
* LDAP entries
*
* @access private
* @access protected
* @var array
*/
private $entries = array();
protected $entries = array();
/**
* Constructor

View file

@ -13,10 +13,10 @@ class Entry
/**
* LDAP entry
*
* @access private
* @access protected
* @var array
*/
private $entry = array();
protected $entry = array();
/**
* Constructor

View file

@ -16,10 +16,10 @@ class Group
/**
* Query
*
* @access private
* @access protected
* @var Query
*/
private $query;
protected $query;
/**
* Constructor
@ -43,7 +43,8 @@ class Group
*/
public static function getGroups(Client $client, $query)
{
$self = new self(new Query($client));
$className = get_called_class();
$self = new $className(new Query($client));
return $self->find($query);
}

View file

@ -13,18 +13,18 @@ class Query
/**
* LDAP client
*
* @access private
* @access protected
* @var Client
*/
private $client = null;
protected $client = null;
/**
* Query result
*
* @access private
* @access protected
* @var array
*/
private $entries = array();
protected $entries = array();
/**
* Constructor

View file

@ -17,10 +17,10 @@ class User
/**
* Query
*
* @access private
* @access protected
* @var Query
*/
private $query;
protected $query;
/**
* Constructor
@ -40,11 +40,12 @@ class User
* @access public
* @param Client $client
* @param string $username
* @return array
* @return LdapUserProvider
*/
public static function getUser(Client $client, $username)
{
$self = new self(new Query($client));
$className = get_called_class();
$self = new $className(new Query($client));
return $self->find($self->getLdapUserPattern($username));
}

View file

@ -45,19 +45,21 @@ class Client extends Base
*/
public function send($email, $name, $subject, $html)
{
$this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
if (! empty($email)) {
$this->logger->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
$start_time = microtime(true);
$author = 'Kanboard';
$start_time = microtime(true);
$author = 'Kanboard';
if ($this->userSession->isLogged()) {
$author = e('%s via Kanboard', $this->helper->user->getFullname());
}
if ($this->userSession->isLogged()) {
$author = e('%s via Kanboard', $this->helper->user->getFullname());
}
$this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author);
$this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author);
if (DEBUG) {
$this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
if (DEBUG) {
$this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
}
}
return $this;

View file

@ -3,7 +3,7 @@
namespace Kanboard\Core;
use Parsedown;
use Kanboard\Helper\Url;
use Pimple\Container;
/**
* Specific Markdown rules for Kanboard
@ -14,22 +14,51 @@ use Kanboard\Helper\Url;
*/
class Markdown extends Parsedown
{
private $link;
private $helper;
/**
* Link params for tasks
*
* @access private
* @var array
*/
private $link = array();
public function __construct($link, Url $helper)
/**
* Container
*
* @access private
* @var Container
*/
private $container;
/**
* Constructor
*
* @access public
* @param Container $container
* @param array $link
*/
public function __construct(Container $container, array $link)
{
$this->link = $link;
$this->helper = $helper;
$this->container = $container;
$this->InlineTypes['#'][] = 'TaskLink';
$this->inlineMarkerList .= '#';
$this->InlineTypes['@'][] = 'UserLink';
$this->inlineMarkerList .= '#@';
}
protected function inlineTaskLink($Excerpt)
/**
* Handle Task Links
*
* Replace "#123" by a link to the task
*
* @access public
* @param array $Excerpt
* @return array
*/
protected function inlineTaskLink(array $Excerpt)
{
// Replace task #123 by a link to the task
if (! empty($this->link) && preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) {
$url = $this->helper->href(
$url = $this->container['helper']->url->href(
$this->link['controller'],
$this->link['action'],
$this->link['params'] + array('task_id' => $matches[1])
@ -40,7 +69,38 @@ class Markdown extends Parsedown
'element' => array(
'name' => 'a',
'text' => $matches[0],
'attributes' => array('href' => $url)));
'attributes' => array('href' => $url)
),
);
}
}
/**
* Handle User Mentions
*
* Replace "@username" by a link to the user
*
* @access public
* @param array $Excerpt
* @return array
*/
protected function inlineUserLink(array $Excerpt)
{
if (preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) {
$user_id = $this->container['user']->getIdByUsername($matches[1]);
if (! empty($user_id)) {
$url = $this->container['helper']->url->href('user', 'profile', array('user_id' => $user_id));
return array(
'extent' => strlen($matches[0]),
'element' => array(
'name' => 'a',
'text' => $matches[0],
'attributes' => array('href' => $url, 'class' => 'user-mention-link'),
),
);
}
}
}
}

View file

@ -86,6 +86,26 @@ class AccessMap
return $roles;
}
/**
* Get the highest role from a list
*
* @access public
* @param array $roles
* @return string
*/
public function getHighestRole(array $roles)
{
$rank = array();
foreach ($roles as $role) {
$rank[$role] = count($this->getRoleHierarchy($role));
}
asort($rank);
return key($rank);
}
/**
* Add new access rules
*

View file

@ -10,6 +10,13 @@ namespace Kanboard\Core\Security;
*/
interface PostAuthenticationProviderInterface extends AuthenticationProviderInterface
{
/**
* Called only one time before to prompt the user for pin code
*
* @access public
*/
public function beforeCode();
/**
* Set user pin-code
*
@ -18,6 +25,14 @@ interface PostAuthenticationProviderInterface extends AuthenticationProviderInte
*/
public function setCode($code);
/**
* Generate secret if necessary
*
* @access public
* @return string
*/
public function generateSecret();
/**
* Set secret token (fetched from user profile)
*

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