summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog26
-rw-r--r--Makefile2
-rw-r--r--app/Api/Auth.php35
-rw-r--r--app/Api/Me.php8
-rw-r--r--app/Api/ProjectPermission.php8
-rw-r--r--app/Api/User.php55
-rw-r--r--app/Auth/DatabaseAuth.php125
-rw-r--r--app/Auth/Github.php123
-rw-r--r--app/Auth/GithubAuth.php143
-rw-r--r--app/Auth/Gitlab.php123
-rw-r--r--app/Auth/GitlabAuth.php143
-rw-r--r--app/Auth/Google.php124
-rw-r--r--app/Auth/GoogleAuth.php143
-rw-r--r--app/Auth/Ldap.php521
-rw-r--r--app/Auth/LdapAuth.php187
-rw-r--r--app/Auth/RememberMe.php323
-rw-r--r--app/Auth/RememberMeAuth.php79
-rw-r--r--app/Auth/ReverseProxy.php83
-rw-r--r--app/Auth/ReverseProxyAuth.php76
-rw-r--r--app/Auth/TotpAuth.php126
-rw-r--r--app/Controller/Action.php4
-rw-r--r--app/Controller/Activity.php2
-rw-r--r--app/Controller/Analytic.php5
-rw-r--r--app/Controller/App.php51
-rw-r--r--app/Controller/Auth.php30
-rw-r--r--app/Controller/Base.php113
-rw-r--r--app/Controller/Board.php193
-rw-r--r--app/Controller/BoardPopover.php101
-rw-r--r--app/Controller/BoardTooltip.php112
-rw-r--r--app/Controller/Config.php2
-rw-r--r--app/Controller/Currency.php2
-rw-r--r--app/Controller/Customfilter.php2
-rw-r--r--app/Controller/Doc.php2
-rw-r--r--app/Controller/Feed.php4
-rw-r--r--app/Controller/Gantt.php8
-rw-r--r--app/Controller/Group.php10
-rw-r--r--app/Controller/GroupHelper.php24
-rw-r--r--app/Controller/Link.php2
-rw-r--r--app/Controller/Oauth.php43
-rw-r--r--app/Controller/Project.php139
-rw-r--r--app/Controller/ProjectPermission.php177
-rw-r--r--app/Controller/Projectuser.php13
-rw-r--r--app/Controller/Search.php2
-rw-r--r--app/Controller/Subtask.php4
-rw-r--r--app/Controller/Task.php2
-rw-r--r--app/Controller/TaskHelper.php57
-rw-r--r--app/Controller/Taskcreation.php2
-rw-r--r--app/Controller/Taskduplication.php6
-rw-r--r--app/Controller/Taskmodification.php2
-rw-r--r--app/Controller/Twofactor.php33
-rw-r--r--app/Controller/User.php35
-rw-r--r--app/Controller/UserHelper.php24
-rw-r--r--app/Core/Base.php50
-rw-r--r--app/Core/Cache/MemoryCache.php2
-rw-r--r--app/Core/Group/GroupBackendProviderInterface.php21
-rw-r--r--app/Core/Group/GroupManager.php71
-rw-r--r--app/Core/Group/GroupProviderInterface.php40
-rw-r--r--app/Core/Http/OAuth2.php (renamed from app/Core/OAuth2.php)8
-rw-r--r--app/Core/Http/RememberMeCookie.php120
-rw-r--r--app/Core/Http/Request.php125
-rw-r--r--app/Core/Http/Response.php2
-rw-r--r--app/Core/Ldap/Client.php119
-rw-r--r--app/Core/Ldap/Entries.php63
-rw-r--r--app/Core/Ldap/Entry.php91
-rw-r--r--app/Core/Ldap/Group.php130
-rw-r--r--app/Core/Ldap/Query.php44
-rw-r--r--app/Core/Ldap/User.php135
-rw-r--r--app/Core/Security/AccessMap.php91
-rw-r--r--app/Core/Security/AuthenticationManager.php187
-rw-r--r--app/Core/Security/AuthenticationProviderInterface.php28
-rw-r--r--app/Core/Security/Authorization.php10
-rw-r--r--app/Core/Security/OAuthAuthenticationProviderInterface.php46
-rw-r--r--app/Core/Security/PasswordAuthenticationProviderInterface.php36
-rw-r--r--app/Core/Security/PostAuthenticationProviderInterface.php54
-rw-r--r--app/Core/Security/PreAuthenticationProviderInterface.php20
-rw-r--r--app/Core/Security/Role.php43
-rw-r--r--app/Core/Security/SessionCheckProviderInterface.php20
-rw-r--r--app/Core/Session/SessionManager.php13
-rw-r--r--app/Core/Session/SessionStorage.php3
-rw-r--r--app/Core/User/GroupSync.php32
-rw-r--r--app/Core/User/UserProfile.php62
-rw-r--r--app/Core/User/UserProperty.php70
-rw-r--r--app/Core/User/UserProviderInterface.php103
-rw-r--r--app/Core/User/UserSession.php (renamed from app/Model/UserSession.php)63
-rw-r--r--app/Core/User/UserSync.php76
-rw-r--r--app/Event/AuthEvent.php27
-rw-r--r--app/Event/AuthFailureEvent.php44
-rw-r--r--app/Event/AuthSuccessEvent.php43
-rw-r--r--app/Formatter/GroupAutoCompleteFormatter.php55
-rw-r--r--app/Formatter/ProjectGanttFormatter.php2
-rw-r--r--app/Formatter/UserFilterAutoCompleteFormatter.php38
-rw-r--r--app/Group/DatabaseBackendGroupProvider.php34
-rw-r--r--app/Group/DatabaseGroupProvider.php66
-rw-r--r--app/Group/LdapBackendGroupProvider.php54
-rw-r--r--app/Group/LdapGroupProvider.php76
-rw-r--r--app/Helper/Url.php2
-rw-r--r--app/Helper/User.php68
-rw-r--r--app/Model/Acl.php289
-rw-r--r--app/Model/Authentication.php194
-rw-r--r--app/Model/Group.php24
-rw-r--r--app/Model/GroupMember.php24
-rw-r--r--app/Model/Project.php5
-rw-r--r--app/Model/ProjectAnalytic.php2
-rw-r--r--app/Model/ProjectGroupRole.php187
-rw-r--r--app/Model/ProjectPermission.php447
-rw-r--r--app/Model/ProjectUserRole.php263
-rw-r--r--app/Model/RememberMeSession.php151
-rw-r--r--app/Model/TaskPermission.php4
-rw-r--r--app/Model/User.php129
-rw-r--r--app/Model/UserFilter.php80
-rw-r--r--app/Model/UserImport.php18
-rw-r--r--app/Model/UserLocking.php103
-rw-r--r--app/Model/UserNotification.php2
-rw-r--r--app/Schema/Mysql.php61
-rw-r--r--app/Schema/Postgres.php61
-rw-r--r--app/Schema/Sqlite.php42
-rw-r--r--app/ServiceProvider/AuthenticationProvider.php149
-rw-r--r--app/ServiceProvider/ClassProvider.php40
-rw-r--r--app/ServiceProvider/GroupProvider.php37
-rw-r--r--app/ServiceProvider/NotificationProvider.php45
-rw-r--r--app/ServiceProvider/PluginProvider.php31
-rw-r--r--app/ServiceProvider/RouteProvider.php151
-rw-r--r--app/ServiceProvider/SessionProvider.php13
-rw-r--r--app/Subscriber/AuthSubscriber.php90
-rw-r--r--app/Subscriber/BootstrapSubscriber.php18
-rw-r--r--app/Template/activity/project.php2
-rw-r--r--app/Template/analytic/layout.php2
-rw-r--r--app/Template/app/layout.php6
-rw-r--r--app/Template/app/projects.php2
-rw-r--r--app/Template/board/popover_assignee.php2
-rw-r--r--app/Template/board/popover_category.php2
-rw-r--r--app/Template/board/table_column.php2
-rw-r--r--app/Template/board/table_swimlane.php2
-rw-r--r--app/Template/board/task_footer.php14
-rw-r--r--app/Template/board/task_menu.php6
-rw-r--r--app/Template/board/task_private.php14
-rw-r--r--app/Template/calendar/show.php2
-rw-r--r--app/Template/custom_filter/add.php2
-rw-r--r--app/Template/custom_filter/edit.php2
-rw-r--r--app/Template/custom_filter/index.php2
-rw-r--r--app/Template/gantt/projects.php4
-rw-r--r--app/Template/group/dissociate.php2
-rw-r--r--app/Template/group/index.php4
-rw-r--r--app/Template/group/remove.php2
-rw-r--r--app/Template/group/users.php2
-rw-r--r--app/Template/layout.php2
-rw-r--r--app/Template/project/dropdown.php11
-rw-r--r--app/Template/project/edit.php2
-rw-r--r--app/Template/project/filters.php2
-rw-r--r--app/Template/project/index.php40
-rw-r--r--app/Template/project/roles.php7
-rw-r--r--app/Template/project/sidebar.php10
-rw-r--r--app/Template/project/users.php82
-rw-r--r--app/Template/project_permission/index.php141
-rw-r--r--app/Template/project_user/layout.php4
-rw-r--r--app/Template/subtask/show.php15
-rw-r--r--app/Template/task/layout.php2
-rw-r--r--app/Template/task/show.php5
-rw-r--r--app/Template/task/sidebar.php2
-rw-r--r--app/Template/tasklink/create.php4
-rw-r--r--app/Template/tasklink/edit.php4
-rw-r--r--app/Template/tasklink/show.php4
-rw-r--r--app/Template/twofactor/index.php12
-rw-r--r--app/Template/user/create_local.php21
-rw-r--r--app/Template/user/create_remote.php23
-rw-r--r--app/Template/user/edit.php14
-rw-r--r--app/Template/user/external.php6
-rw-r--r--app/Template/user/index.php10
-rw-r--r--app/Template/user/layout.php2
-rw-r--r--app/Template/user/sessions.php2
-rw-r--r--app/Template/user/show.php2
-rw-r--r--app/Template/user/sidebar.php6
-rw-r--r--app/Template/user_import/step1.php2
-rw-r--r--app/User/DatabaseUserProvider.php144
-rw-r--r--app/User/GithubUserProvider.php23
-rw-r--r--app/User/GitlabUserProvider.php23
-rw-r--r--app/User/GoogleUserProvider.php23
-rw-r--r--app/User/LdapUserProvider.php206
-rw-r--r--app/User/OAuthUserProvider.php141
-rw-r--r--app/User/ReverseProxyUserProvider.php147
-rw-r--r--app/common.php14
-rw-r--r--app/constants.php28
-rw-r--r--app/routes.php117
-rw-r--r--assets/js/app.js2
-rw-r--r--assets/js/src/App.js29
-rw-r--r--assets/js/src/Project.js18
-rw-r--r--composer.lock8
-rw-r--r--config.default.php67
-rw-r--r--doc/api-action-procedures.markdown245
-rw-r--r--doc/api-application-procedures.markdown231
-rw-r--r--doc/api-authentication.markdown66
-rw-r--r--doc/api-board-procedures.markdown416
-rw-r--r--doc/api-category-procedures.markdown172
-rw-r--r--doc/api-comment-procedures.markdown182
-rw-r--r--doc/api-examples.markdown184
-rw-r--r--doc/api-file-procedures.markdown217
-rw-r--r--doc/api-json-rpc.markdown4526
-rw-r--r--doc/api-link-procedures.markdown470
-rw-r--r--doc/api-me-procedures.markdown385
-rw-r--r--doc/api-project-procedures.markdown512
-rw-r--r--doc/api-subtask-procedures.markdown194
-rw-r--r--doc/api-swimlane-procedures.markdown469
-rw-r--r--doc/api-task-procedures.markdown564
-rw-r--r--doc/api-user-procedures.markdown222
-rwxr-xr-xkanboard2
-rw-r--r--tests/functionals/ApiTest.php19
-rw-r--r--tests/units/Action/TaskAssignCurrentUserTest.php2
-rw-r--r--tests/units/Auth/DatabaseAuthTest.php52
-rw-r--r--tests/units/Auth/LdapTest.php697
-rw-r--r--tests/units/Auth/ReverseProxyTest.php37
-rw-r--r--tests/units/Auth/TotpAuthTest.php63
-rw-r--r--tests/units/Base.php3
-rw-r--r--tests/units/Core/Http/OAuth2Test.php (renamed from tests/units/Core/OAuth2Test.php)4
-rw-r--r--tests/units/Core/Http/RememberMeCookieTest.php108
-rw-r--r--tests/units/Core/Http/RequestTest.php175
-rw-r--r--tests/units/Core/Ldap/ClientTest.php53
-rw-r--r--tests/units/Core/Ldap/EntriesTest.php55
-rw-r--r--tests/units/Core/Ldap/EntryTest.php71
-rw-r--r--tests/units/Core/Ldap/LdapGroupTest.php160
-rw-r--r--tests/units/Core/Ldap/LdapUserTest.php379
-rw-r--r--tests/units/Core/Ldap/QueryTest.php45
-rw-r--r--tests/units/Core/Ldap/UserTest.php95
-rw-r--r--tests/units/Core/Security/AccessMapTest.php29
-rw-r--r--tests/units/Core/Security/AuthenticationManagerTest.php150
-rw-r--r--tests/units/Core/Security/AuthorizationTest.php25
-rw-r--r--tests/units/Core/User/GroupSyncTest.php30
-rw-r--r--tests/units/Core/User/UserProfileTest.php63
-rw-r--r--tests/units/Core/User/UserPropertyTest.php60
-rw-r--r--tests/units/Core/User/UserSessionTest.php (renamed from tests/units/Model/UserSessionTest.php)58
-rw-r--r--tests/units/Core/User/UserSyncTest.php55
-rw-r--r--tests/units/Helper/UserHelperTest.php261
-rw-r--r--tests/units/Integration/BitbucketWebhookTest.php11
-rw-r--r--tests/units/Integration/GithubWebhookTest.php15
-rw-r--r--tests/units/Integration/GitlabWebhookTest.php7
-rw-r--r--tests/units/Model/AclTest.php296
-rw-r--r--tests/units/Model/ActionTest.php27
-rw-r--r--tests/units/Model/AuthenticationTest.php39
-rw-r--r--tests/units/Model/ProjectDuplicationTest.php37
-rw-r--r--tests/units/Model/ProjectGroupRoleTest.php340
-rw-r--r--tests/units/Model/ProjectPermissionTest.php318
-rw-r--r--tests/units/Model/ProjectTest.php1
-rw-r--r--tests/units/Model/ProjectUserRoleTest.php400
-rw-r--r--tests/units/Model/SubtaskTest.php2
-rw-r--r--tests/units/Model/TaskCreationTest.php1
-rw-r--r--tests/units/Model/TaskDuplicationTest.php35
-rw-r--r--tests/units/Model/TaskFinderTest.php1
-rw-r--r--tests/units/Model/TaskModificationTest.php1
-rw-r--r--tests/units/Model/TaskPermissionTest.php2
-rw-r--r--tests/units/Model/TaskStatusTest.php1
-rw-r--r--tests/units/Model/TaskTest.php1
-rw-r--r--tests/units/Model/UserLockingTest.php43
-rw-r--r--tests/units/Model/UserNotificationTest.php22
-rw-r--r--tests/units/Model/UserTest.php108
-rw-r--r--tests/units/Notification/MailTest.php1
-rw-r--r--tests/units/User/DatabaseUserProviderTest.php14
255 files changed, 14066 insertions, 9772 deletions
diff --git a/ChangeLog b/ChangeLog
index 5a858885..230a6b28 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,13 +1,27 @@
Version 1.0.22 (unreleased)
---------------------------
+Breaking changes:
+
+* LDAP configuration parameters changes (See documentation)
+* SQL table changes:
+ - "users" table: added new column "role" and removed columns "is_admin" and "is_project_admin"
+ - "project_has_users" table: replace column "is_owner" by column "role"
+ - Sqlite does not support alter table, old columns still there but unused
+* API procedure changes:
+ - createUser
+ - createLdapUser
+ - updateUser
+
New features:
-* User groups (Teams)
-* Add generic LDAP client library
-* Pluggable authentication and authorization system (Work in progress)
+* Add pluggable authentication and authorization system (Work in progress)
+* Add groups (teams)
+* Add LDAP groups synchronization
+* Add project groups permissions
* Add new project role Viewer (Work in progress)
-* Assign project permissions to a group (Work in progress)
+* Add generic LDAP client library
+* Add search query attribute for task link
Version 1.0.21
--------------
@@ -15,8 +29,8 @@ Version 1.0.21
Breaking changes:
* Projects with duplicate name 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
+ - 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
New features:
diff --git a/Makefile b/Makefile
index 6bc1af83..6dd38659 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ CSS_APP = $(addprefix assets/css/src/, $(addsuffix .css, base links title table
CSS_PRINT = $(addprefix assets/css/src/, $(addsuffix .css, print links table board task comment subtask markdown))
CSS_VENDOR = $(addprefix assets/css/vendor/, $(addsuffix .css, jquery-ui.min jquery-ui-timepicker-addon.min chosen.min fullcalendar.min font-awesome.min c3.min))
-JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Sidebar Search App Screenshot Calendar Board Swimlane Gantt Task TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart Router))
+JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Popover Dropdown Tooltip Markdown Sidebar Search App Screenshot Calendar Board Swimlane Gantt Task Project TaskRepartitionChart UserRepartitionChart CumulativeFlowDiagram BurndownChart AvgTimeColumnChart TaskTimeColumnChart LeadCycleTimeChart Router))
JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min))
JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn))
diff --git a/app/Api/Auth.php b/app/Api/Auth.php
index a084d6eb..0a911796 100644
--- a/app/Api/Auth.php
+++ b/app/Api/Auth.php
@@ -3,7 +3,6 @@
namespace Kanboard\Api;
use JsonRPC\AuthenticationFailure;
-use Symfony\Component\EventDispatcher\Event;
/**
* Base class
@@ -24,15 +23,43 @@ class Auth extends Base
*/
public function checkCredentials($username, $password, $class, $method)
{
- $this->container['dispatcher']->dispatch('api.bootstrap', new Event);
+ $this->container['dispatcher']->dispatch('app.bootstrap');
- if ($username !== 'jsonrpc' && ! $this->authentication->hasCaptcha($username) && $this->authentication->authenticate($username, $password)) {
+ if ($this->isUserAuthenticated($username, $password)) {
$this->checkProcedurePermission(true, $method);
$this->userSession->initialize($this->user->getByUsername($username));
- } elseif ($username === 'jsonrpc' && $password === $this->config->get('api_token')) {
+ } elseif ($this->isAppAuthenticated($username, $password)) {
$this->checkProcedurePermission(false, $method);
} else {
throw new AuthenticationFailure('Wrong credentials');
}
}
+
+ /**
+ * Check user credentials
+ *
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @return boolean
+ */
+ private function isUserAuthenticated($username, $password)
+ {
+ return $username !== 'jsonrpc' &&
+ ! $this->userLocking->isLocked($username) &&
+ $this->authenticationManager->passwordAuthentication($username, $password);
+ }
+
+ /**
+ * Check administrative credentials
+ *
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @return boolean
+ */
+ private function isAppAuthenticated($username, $password)
+ {
+ return $username === 'jsonrpc' && $password === $this->config->get('api_token');
+ }
}
diff --git a/app/Api/Me.php b/app/Api/Me.php
index 2c4161fd..37851731 100644
--- a/app/Api/Me.php
+++ b/app/Api/Me.php
@@ -20,7 +20,7 @@ class Me extends Base
public function getMyDashboard()
{
$user_id = $this->userSession->getId();
- $projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id))->findAll();
+ $projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id))->findAll();
$tasks = $this->taskFinder->getUserQuery($user_id)->findAll();
return array(
@@ -32,7 +32,7 @@ class Me extends Base
public function getMyActivityStream()
{
- $project_ids = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
return $this->projectActivity->getProjects($project_ids, 100);
}
@@ -50,7 +50,7 @@ class Me extends Base
public function getMyProjectsList()
{
- return $this->projectPermission->getMemberProjects($this->userSession->getId());
+ return $this->projectUserRole->getProjectsByUser($this->userSession->getId());
}
public function getMyOverdueTasks()
@@ -60,7 +60,7 @@ class Me extends Base
public function getMyProjects()
{
- $project_ids = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
$projects = $this->project->getAllByIds($project_ids);
return $this->formatProjects($projects);
diff --git a/app/Api/ProjectPermission.php b/app/Api/ProjectPermission.php
index 80323395..d4408197 100644
--- a/app/Api/ProjectPermission.php
+++ b/app/Api/ProjectPermission.php
@@ -2,6 +2,8 @@
namespace Kanboard\Api;
+use Kanboard\Core\Security\Role;
+
/**
* ProjectPermission API controller
*
@@ -12,16 +14,16 @@ class ProjectPermission extends \Kanboard\Core\Base
{
public function getMembers($project_id)
{
- return $this->projectPermission->getMembers($project_id);
+ return $this->projectUserRole->getAllUsers($project_id);
}
public function revokeUser($project_id, $user_id)
{
- return $this->projectPermission->revokeMember($project_id, $user_id);
+ return $this->projectUserRole->removeUser($project_id, $user_id);
}
public function allowUser($project_id, $user_id)
{
- return $this->projectPermission->addMember($project_id, $user_id);
+ return $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MEMBER);
}
}
diff --git a/app/Api/User.php b/app/Api/User.php
index 105723d3..078c82f1 100644
--- a/app/Api/User.php
+++ b/app/Api/User.php
@@ -3,6 +3,10 @@
namespace Kanboard\Api;
use Kanboard\Auth\Ldap;
+use Kanboard\Core\Security\Role;
+use Kanboard\Core\Ldap\Client as LdapClient;
+use Kanboard\Core\Ldap\ClientException as LdapException;
+use Kanboard\Core\Ldap\User as LdapUser;
/**
* User API controller
@@ -27,7 +31,7 @@ class User extends \Kanboard\Core\Base
return $this->user->remove($user_id);
}
- public function createUser($username, $password, $name = '', $email = '', $is_admin = 0, $is_project_admin = 0)
+ public function createUser($username, $password, $name = '', $email = '', $role = Role::APP_USER)
{
$values = array(
'username' => $username,
@@ -35,44 +39,53 @@ class User extends \Kanboard\Core\Base
'confirmation' => $password,
'name' => $name,
'email' => $email,
- 'is_admin' => $is_admin,
- 'is_project_admin' => $is_project_admin,
+ 'role' => $role,
);
list($valid, ) = $this->user->validateCreation($values);
return $valid ? $this->user->create($values) : false;
}
- public function createLdapUser($username = '', $email = '', $is_admin = 0, $is_project_admin = 0)
+ public function createLdapUser($username)
{
- $ldap = new Ldap($this->container);
- $user = $ldap->lookup($username, $email);
+ try {
- if (! $user) {
- return false;
- }
+ $ldap = LdapClient::connect();
+ $user = LdapUser::getUser($ldap, sprintf(LDAP_USER_FILTER, $username));
- $values = array(
- 'username' => $user['username'],
- 'name' => $user['name'],
- 'email' => $user['email'],
- 'is_ldap_user' => 1,
- 'is_admin' => $is_admin,
- 'is_project_admin' => $is_project_admin,
- );
+ if ($user === null) {
+ $this->logger->info('User not found in LDAP server');
+ return false;
+ }
- return $this->user->create($values);
+ if ($user->getUsername() === '') {
+ throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
+ }
+
+ $values = array(
+ 'username' => $user->getUsername(),
+ 'name' => $user->getName(),
+ 'email' => $user->getEmail(),
+ 'role' => $user->getRole(),
+ 'is_ldap_user' => 1,
+ );
+
+ return $this->user->create($values);
+
+ } catch (LdapException $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
}
- public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $is_project_admin = null)
+ public function updateUser($id, $username = null, $name = null, $email = null, $role = null)
{
$values = array(
'id' => $id,
'username' => $username,
'name' => $name,
'email' => $email,
- 'is_admin' => $is_admin,
- 'is_project_admin' => $is_project_admin,
+ 'role' => $role,
);
foreach ($values as $key => $value) {
diff --git a/app/Auth/DatabaseAuth.php b/app/Auth/DatabaseAuth.php
new file mode 100644
index 00000000..727afaf3
--- /dev/null
+++ b/app/Auth/DatabaseAuth.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\PasswordAuthenticationProviderInterface;
+use Kanboard\Core\Security\SessionCheckProviderInterface;
+use Kanboard\Model\User;
+use Kanboard\User\DatabaseUserProvider;
+
+/**
+ * Database Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterface, SessionCheckProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access private
+ * @var array
+ */
+ private $userInfo = array();
+
+ /**
+ * Username
+ *
+ * @access private
+ * @var string
+ */
+ private $username = '';
+
+ /**
+ * Password
+ *
+ * @access private
+ * @var string
+ */
+ private $password = '';
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'Database';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $user = $this->db
+ ->table(User::TABLE)
+ ->columns('id', 'password')
+ ->eq('username', $this->username)
+ ->eq('disable_login_form', 0)
+ ->eq('is_ldap_user', 0)
+ ->findOne();
+
+ if (! empty($user) && password_verify($this->password, $user['password'])) {
+ $this->userInfo = $user;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the user session is valid
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isValidSession()
+ {
+ return $this->user->exists($this->userSession->getId());
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return null|\Kanboard\User\DatabaseUserProvider
+ */
+ public function getUser()
+ {
+ if (empty($this->userInfo)) {
+ return null;
+ }
+
+ return new DatabaseUserProvider($this->userInfo);
+ }
+
+ /**
+ * Set username
+ *
+ * @access public
+ * @param string $username
+ */
+ public function setUsername($username)
+ {
+ $this->username = $username;
+ }
+
+ /**
+ * Set password
+ *
+ * @access public
+ * @param string $password
+ */
+ public function setPassword($password)
+ {
+ $this->password = $password;
+ }
+}
diff --git a/app/Auth/Github.php b/app/Auth/Github.php
deleted file mode 100644
index 4777152a..00000000
--- a/app/Auth/Github.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Event\AuthEvent;
-
-/**
- * Github backend
- *
- * @package auth
- */
-class Github extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'Github';
-
- /**
- * OAuth2 instance
- *
- * @access private
- * @var \Kanboard\Core\OAuth2
- */
- private $service;
-
- /**
- * Authenticate a Github user
- *
- * @access public
- * @param string $github_id Github user id
- * @return boolean
- */
- public function authenticate($github_id)
- {
- $user = $this->user->getByGithubId($github_id);
-
- if (! empty($user)) {
- $this->userSession->initialize($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
- return true;
- }
-
- return false;
- }
-
- /**
- * Unlink a Github account for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return boolean
- */
- public function unlink($user_id)
- {
- return $this->user->update(array(
- 'id' => $user_id,
- 'github_id' => '',
- ));
- }
-
- /**
- * Update the user table based on the Github profile information
- *
- * @access public
- * @param integer $user_id User id
- * @param array $profile Github profile
- * @return boolean
- */
- public function updateUser($user_id, array $profile)
- {
- $user = $this->user->getById($user_id);
-
- return $this->user->update(array(
- 'id' => $user_id,
- 'github_id' => $profile['id'],
- 'email' => empty($user['email']) ? $profile['email'] : $user['email'],
- 'name' => empty($user['name']) ? $profile['name'] : $user['name'],
- ));
- }
-
- /**
- * Get OAuth2 configured service
- *
- * @access public
- * @return Kanboard\Core\OAuth2
- */
- public function getService()
- {
- if (empty($this->service)) {
- $this->service = $this->oauth->createService(
- GITHUB_CLIENT_ID,
- GITHUB_CLIENT_SECRET,
- $this->helper->url->to('oauth', 'github', array(), '', true),
- GITHUB_OAUTH_AUTHORIZE_URL,
- GITHUB_OAUTH_TOKEN_URL,
- array()
- );
- }
-
- return $this->service;
- }
-
- /**
- * Get Github profile
- *
- * @access public
- * @param string $code
- * @return array
- */
- public function getProfile($code)
- {
- $this->getService()->getAccessToken($code);
-
- return $this->httpClient->getJson(
- GITHUB_API_URL.'user',
- array($this->getService()->getAuthorizationHeader())
- );
- }
-}
diff --git a/app/Auth/GithubAuth.php b/app/Auth/GithubAuth.php
new file mode 100644
index 00000000..47da0413
--- /dev/null
+++ b/app/Auth/GithubAuth.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\OAuthAuthenticationProviderInterface;
+use Kanboard\User\GithubUserProvider;
+
+/**
+ * Github Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class GithubAuth extends Base implements OAuthAuthenticationProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access private
+ * @var \Kanboard\User\GithubUserProvider
+ */
+ private $userInfo = null;
+
+ /**
+ * OAuth2 instance
+ *
+ * @access private
+ * @var \Kanboard\Core\Http\OAuth2
+ */
+ private $service;
+
+ /**
+ * OAuth2 code
+ *
+ * @access private
+ * @var string
+ */
+ private $code = '';
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'Github';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $profile = $this->getProfile();
+
+ if (! empty($profile)) {
+ $this->userInfo = new GithubUserProvider($profile);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Set Code
+ *
+ * @access public
+ * @param string $code
+ * @return GithubAuth
+ */
+ public function setCode($code)
+ {
+ $this->code = $code;
+ return $this;
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return null|GithubUserProvider
+ */
+ public function getUser()
+ {
+ return $this->userInfo;
+ }
+
+ /**
+ * Get configured OAuth2 service
+ *
+ * @access public
+ * @return \Kanboard\Core\Http\OAuth2
+ */
+ public function getService()
+ {
+ if (empty($this->service)) {
+ $this->service = $this->oauth->createService(
+ GITHUB_CLIENT_ID,
+ GITHUB_CLIENT_SECRET,
+ $this->helper->url->to('oauth', 'github', array(), '', true),
+ GITHUB_OAUTH_AUTHORIZE_URL,
+ GITHUB_OAUTH_TOKEN_URL,
+ array()
+ );
+ }
+
+ return $this->service;
+ }
+
+ /**
+ * Get Github profile
+ *
+ * @access private
+ * @return array
+ */
+ private function getProfile()
+ {
+ $this->getService()->getAccessToken($this->code);
+
+ return $this->httpClient->getJson(
+ GITHUB_API_URL.'user',
+ array($this->getService()->getAuthorizationHeader())
+ );
+ }
+
+ /**
+ * Unlink user
+ *
+ * @access public
+ * @param integer $userId
+ * @return bool
+ */
+ public function unlink($userId)
+ {
+ return $this->user->update(array('id' => $userId, 'github_id' => ''));
+ }
+}
diff --git a/app/Auth/Gitlab.php b/app/Auth/Gitlab.php
deleted file mode 100644
index 698b59c3..00000000
--- a/app/Auth/Gitlab.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Event\AuthEvent;
-
-/**
- * Gitlab backend
- *
- * @package auth
- */
-class Gitlab extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'Gitlab';
-
- /**
- * OAuth2 instance
- *
- * @access private
- * @var \Kanboard\Core\OAuth2
- */
- private $service;
-
- /**
- * Authenticate a Gitlab user
- *
- * @access public
- * @param string $gitlab_id Gitlab user id
- * @return boolean
- */
- public function authenticate($gitlab_id)
- {
- $user = $this->user->getByGitlabId($gitlab_id);
-
- if (! empty($user)) {
- $this->userSession->initialize($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
- return true;
- }
-
- return false;
- }
-
- /**
- * Unlink a Gitlab account for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return boolean
- */
- public function unlink($user_id)
- {
- return $this->user->update(array(
- 'id' => $user_id,
- 'gitlab_id' => '',
- ));
- }
-
- /**
- * Update the user table based on the Gitlab profile information
- *
- * @access public
- * @param integer $user_id User id
- * @param array $profile Gitlab profile
- * @return boolean
- */
- public function updateUser($user_id, array $profile)
- {
- $user = $this->user->getById($user_id);
-
- return $this->user->update(array(
- 'id' => $user_id,
- 'gitlab_id' => $profile['id'],
- 'email' => empty($user['email']) ? $profile['email'] : $user['email'],
- 'name' => empty($user['name']) ? $profile['name'] : $user['name'],
- ));
- }
-
- /**
- * Get OAuth2 configured service
- *
- * @access public
- * @return Kanboard\Core\OAuth2
- */
- public function getService()
- {
- if (empty($this->service)) {
- $this->service = $this->oauth->createService(
- GITLAB_CLIENT_ID,
- GITLAB_CLIENT_SECRET,
- $this->helper->url->to('oauth', 'gitlab', array(), '', true),
- GITLAB_OAUTH_AUTHORIZE_URL,
- GITLAB_OAUTH_TOKEN_URL,
- array()
- );
- }
-
- return $this->service;
- }
-
- /**
- * Get Gitlab profile
- *
- * @access public
- * @param string $code
- * @return array
- */
- public function getProfile($code)
- {
- $this->getService()->getAccessToken($code);
-
- return $this->httpClient->getJson(
- GITLAB_API_URL.'user',
- array($this->getService()->getAuthorizationHeader())
- );
- }
-}
diff --git a/app/Auth/GitlabAuth.php b/app/Auth/GitlabAuth.php
new file mode 100644
index 00000000..df6e0176
--- /dev/null
+++ b/app/Auth/GitlabAuth.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\OAuthAuthenticationProviderInterface;
+use Kanboard\User\GitlabUserProvider;
+
+/**
+ * Gitlab Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class GitlabAuth extends Base implements OAuthAuthenticationProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access private
+ * @var \Kanboard\User\GitlabUserProvider
+ */
+ private $userInfo = null;
+
+ /**
+ * OAuth2 instance
+ *
+ * @access private
+ * @var \Kanboard\Core\Http\OAuth2
+ */
+ private $service;
+
+ /**
+ * OAuth2 code
+ *
+ * @access private
+ * @var string
+ */
+ private $code = '';
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'Gitlab';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $profile = $this->getProfile();
+
+ if (! empty($profile)) {
+ $this->userInfo = new GitlabUserProvider($profile);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Set Code
+ *
+ * @access public
+ * @param string $code
+ * @return GitlabAuth
+ */
+ public function setCode($code)
+ {
+ $this->code = $code;
+ return $this;
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return null|GitlabUserProvider
+ */
+ public function getUser()
+ {
+ return $this->userInfo;
+ }
+
+ /**
+ * Get configured OAuth2 service
+ *
+ * @access public
+ * @return \Kanboard\Core\Http\OAuth2
+ */
+ public function getService()
+ {
+ if (empty($this->service)) {
+ $this->service = $this->oauth->createService(
+ GITLAB_CLIENT_ID,
+ GITLAB_CLIENT_SECRET,
+ $this->helper->url->to('oauth', 'gitlab', array(), '', true),
+ GITLAB_OAUTH_AUTHORIZE_URL,
+ GITLAB_OAUTH_TOKEN_URL,
+ array()
+ );
+ }
+
+ return $this->service;
+ }
+
+ /**
+ * Get Gitlab profile
+ *
+ * @access private
+ * @return array
+ */
+ private function getProfile()
+ {
+ $this->getService()->getAccessToken($this->code);
+
+ return $this->httpClient->getJson(
+ GITLAB_API_URL.'user',
+ array($this->getService()->getAuthorizationHeader())
+ );
+ }
+
+ /**
+ * Unlink user
+ *
+ * @access public
+ * @param integer $userId
+ * @return bool
+ */
+ public function unlink($userId)
+ {
+ return $this->user->update(array('id' => $userId, 'gitlab_id' => ''));
+ }
+}
diff --git a/app/Auth/Google.php b/app/Auth/Google.php
deleted file mode 100644
index 6c1bc3cd..00000000
--- a/app/Auth/Google.php
+++ /dev/null
@@ -1,124 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Event\AuthEvent;
-
-/**
- * Google backend
- *
- * @package auth
- * @author Frederic Guillot
- */
-class Google extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'Google';
-
- /**
- * OAuth2 instance
- *
- * @access private
- * @var \Kanboard\Core\OAuth2
- */
- private $service;
-
- /**
- * Authenticate a Google user
- *
- * @access public
- * @param string $google_id Google unique id
- * @return boolean
- */
- public function authenticate($google_id)
- {
- $user = $this->user->getByGoogleId($google_id);
-
- if (! empty($user)) {
- $this->userSession->initialize($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
- return true;
- }
-
- return false;
- }
-
- /**
- * Unlink a Google account for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return boolean
- */
- public function unlink($user_id)
- {
- return $this->user->update(array(
- 'id' => $user_id,
- 'google_id' => '',
- ));
- }
-
- /**
- * Update the user table based on the Google profile information
- *
- * @access public
- * @param integer $user_id User id
- * @param array $profile Google profile
- * @return boolean
- */
- public function updateUser($user_id, array $profile)
- {
- $user = $this->user->getById($user_id);
-
- return $this->user->update(array(
- 'id' => $user_id,
- 'google_id' => $profile['id'],
- 'email' => empty($user['email']) ? $profile['email'] : $user['email'],
- 'name' => empty($user['name']) ? $profile['name'] : $user['name'],
- ));
- }
-
- /**
- * Get OAuth2 configured service
- *
- * @access public
- * @return KanboardCore\OAuth2
- */
- public function getService()
- {
- if (empty($this->service)) {
- $this->service = $this->oauth->createService(
- GOOGLE_CLIENT_ID,
- GOOGLE_CLIENT_SECRET,
- $this->helper->url->to('oauth', 'google', array(), '', true),
- 'https://accounts.google.com/o/oauth2/auth',
- 'https://accounts.google.com/o/oauth2/token',
- array('https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile')
- );
- }
-
- return $this->service;
- }
-
- /**
- * Get Google profile
- *
- * @access public
- * @param string $code
- * @return array
- */
- public function getProfile($code)
- {
- $this->getService()->getAccessToken($code);
-
- return $this->httpClient->getJson(
- 'https://www.googleapis.com/oauth2/v1/userinfo',
- array($this->getService()->getAuthorizationHeader())
- );
- }
-}
diff --git a/app/Auth/GoogleAuth.php b/app/Auth/GoogleAuth.php
new file mode 100644
index 00000000..0dc1c62f
--- /dev/null
+++ b/app/Auth/GoogleAuth.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\OAuthAuthenticationProviderInterface;
+use Kanboard\User\GoogleUserProvider;
+
+/**
+ * Google Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class GoogleAuth extends Base implements OAuthAuthenticationProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access private
+ * @var \Kanboard\User\GoogleUserProvider
+ */
+ private $userInfo = null;
+
+ /**
+ * OAuth2 instance
+ *
+ * @access private
+ * @var \Kanboard\Core\Http\OAuth2
+ */
+ private $service;
+
+ /**
+ * OAuth2 code
+ *
+ * @access private
+ * @var string
+ */
+ private $code = '';
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'Google';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $profile = $this->getProfile();
+
+ if (! empty($profile)) {
+ $this->userInfo = new GoogleUserProvider($profile);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Set Code
+ *
+ * @access public
+ * @param string $code
+ * @return GoogleAuth
+ */
+ public function setCode($code)
+ {
+ $this->code = $code;
+ return $this;
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return null|GoogleUserProvider
+ */
+ public function getUser()
+ {
+ return $this->userInfo;
+ }
+
+ /**
+ * Get configured OAuth2 service
+ *
+ * @access public
+ * @return \Kanboard\Core\Http\OAuth2
+ */
+ public function getService()
+ {
+ if (empty($this->service)) {
+ $this->service = $this->oauth->createService(
+ GOOGLE_CLIENT_ID,
+ GOOGLE_CLIENT_SECRET,
+ $this->helper->url->to('oauth', 'google', array(), '', true),
+ 'https://accounts.google.com/o/oauth2/auth',
+ 'https://accounts.google.com/o/oauth2/token',
+ array('https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile')
+ );
+ }
+
+ return $this->service;
+ }
+
+ /**
+ * Get Google profile
+ *
+ * @access private
+ * @return array
+ */
+ private function getProfile()
+ {
+ $this->getService()->getAccessToken($this->code);
+
+ return $this->httpClient->getJson(
+ 'https://www.googleapis.com/oauth2/v1/userinfo',
+ array($this->getService()->getAuthorizationHeader())
+ );
+ }
+
+ /**
+ * Unlink user
+ *
+ * @access public
+ * @param integer $userId
+ * @return bool
+ */
+ public function unlink($userId)
+ {
+ return $this->user->update(array('id' => $userId, 'google_id' => ''));
+ }
+}
diff --git a/app/Auth/Ldap.php b/app/Auth/Ldap.php
deleted file mode 100644
index 3d361aa7..00000000
--- a/app/Auth/Ldap.php
+++ /dev/null
@@ -1,521 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Event\AuthEvent;
-
-/**
- * LDAP model
- *
- * @package auth
- * @author Frederic Guillot
- */
-class Ldap extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'LDAP';
-
- /**
- * Get LDAP server name
- *
- * @access public
- * @return string
- */
- public function getLdapServer()
- {
- return LDAP_SERVER;
- }
-
- /**
- * Get LDAP bind type
- *
- * @access public
- * @return integer
- */
- public function getLdapBindType()
- {
- return LDAP_BIND_TYPE;
- }
-
- /**
- * Get LDAP server port
- *
- * @access public
- * @return integer
- */
- public function getLdapPort()
- {
- return LDAP_PORT;
- }
-
- /**
- * Get LDAP username (proxy auth)
- *
- * @access public
- * @return string
- */
- public function getLdapUsername()
- {
- return LDAP_USERNAME;
- }
-
- /**
- * Get LDAP password (proxy auth)
- *
- * @access public
- * @return string
- */
- public function getLdapPassword()
- {
- return LDAP_PASSWORD;
- }
-
- /**
- * Get LDAP Base DN
- *
- * @access public
- * @return string
- */
- public function getLdapBaseDn()
- {
- return LDAP_ACCOUNT_BASE;
- }
-
- /**
- * Get LDAP account id attribute
- *
- * @access public
- * @return string
- */
- public function getLdapAccountId()
- {
- return LDAP_ACCOUNT_ID;
- }
-
- /**
- * Get LDAP account email attribute
- *
- * @access public
- * @return string
- */
- public function getLdapAccountEmail()
- {
- return LDAP_ACCOUNT_EMAIL;
- }
-
- /**
- * Get LDAP account name attribute
- *
- * @access public
- * @return string
- */
- public function getLdapAccountName()
- {
- return LDAP_ACCOUNT_FULLNAME;
- }
-
- /**
- * Get LDAP account memberof attribute
- *
- * @access public
- * @return string
- */
- public function getLdapAccountMemberOf()
- {
- return LDAP_ACCOUNT_MEMBEROF;
- }
-
- /**
- * Get LDAP admin group DN
- *
- * @access public
- * @return string
- */
- public function getLdapGroupAdmin()
- {
- return LDAP_GROUP_ADMIN_DN;
- }
-
- /**
- * Get LDAP project admin group DN
- *
- * @access public
- * @return string
- */
- public function getLdapGroupProjectAdmin()
- {
- return LDAP_GROUP_PROJECT_ADMIN_DN;
- }
-
- /**
- * Get LDAP username pattern
- *
- * @access public
- * @param string $username
- * @return string
- */
- public function getLdapUserPattern($username)
- {
- return sprintf(LDAP_USER_PATTERN, $username);
- }
-
- /**
- * Return true if the LDAP username is case sensitive
- *
- * @access public
- * @return boolean
- */
- public function isLdapAccountCaseSensitive()
- {
- return LDAP_USERNAME_CASE_SENSITIVE;
- }
-
- /**
- * Return true if the automatic account creation is enabled
- *
- * @access public
- * @return boolean
- */
- public function isLdapAccountCreationEnabled()
- {
- return LDAP_ACCOUNT_CREATION;
- }
-
- /**
- * Ge the list of attributes to fetch when reading the LDAP user entry
- *
- * Must returns array with index that start at 0 otherwise ldap_search returns a warning "Array initialization wrong"
- *
- * @access public
- * @return array
- */
- public function getProfileAttributes()
- {
- return array_values(array_filter(array(
- $this->getLdapAccountId(),
- $this->getLdapAccountName(),
- $this->getLdapAccountEmail(),
- $this->getLdapAccountMemberOf()
- )));
- }
-
- /**
- * Authenticate the user
- *
- * @access public
- * @param string $username Username
- * @param string $password Password
- * @return boolean
- */
- public function authenticate($username, $password)
- {
- $username = $this->isLdapAccountCaseSensitive() ? $username : strtolower($username);
- $result = $this->findUser($username, $password);
-
- if (is_array($result)) {
- $user = $this->user->getByUsername($username);
-
- if (! empty($user)) {
-
- // There is already a local user with that name
- if ($user['is_ldap_user'] == 0) {
- return false;
- }
- } else {
-
- // We create automatically a new user
- if ($this->isLdapAccountCreationEnabled() && $this->user->create($result) !== false) {
- $user = $this->user->getByUsername($username);
- } else {
- return false;
- }
- }
-
- // We open the session
- $this->userSession->initialize($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Find the user from the LDAP server
- *
- * @access public
- * @param string $username Username
- * @param string $password Password
- * @return boolean|array
- */
- public function findUser($username, $password)
- {
- $ldap = $this->connect();
-
- if ($ldap !== false && $this->bind($ldap, $username, $password)) {
- return $this->getProfile($ldap, $username, $password);
- }
-
- return false;
- }
-
- /**
- * LDAP connection
- *
- * @access public
- * @return resource|boolean
- */
- public function connect()
- {
- if (! function_exists('ldap_connect')) {
- $this->logger->error('LDAP: The PHP LDAP extension is required');
- return false;
- }
-
- // Skip SSL certificate verification
- if (! LDAP_SSL_VERIFY) {
- putenv('LDAPTLS_REQCERT=never');
- }
-
- $ldap = ldap_connect($this->getLdapServer(), $this->getLdapPort());
-
- if ($ldap === false) {
- $this->logger->error('LDAP: Unable to connect to the LDAP server');
- return false;
- }
-
- ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
- ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
- ldap_set_option($ldap, LDAP_OPT_NETWORK_TIMEOUT, 1);
- ldap_set_option($ldap, LDAP_OPT_TIMELIMIT, 1);
-
- if (LDAP_START_TLS && ! @ldap_start_tls($ldap)) {
- $this->logger->error('LDAP: Unable to use ldap_start_tls()');
- return false;
- }
-
- return $ldap;
- }
-
- /**
- * LDAP authentication
- *
- * @access public
- * @param resource $ldap
- * @param string $username
- * @param string $password
- * @return boolean
- */
- public function bind($ldap, $username, $password)
- {
- if ($this->getLdapBindType() === 'user') {
- $ldap_username = sprintf($this->getLdapUsername(), $username);
- $ldap_password = $password;
- } elseif ($this->getLdapBindType() === 'proxy') {
- $ldap_username = $this->getLdapUsername();
- $ldap_password = $this->getLdapPassword();
- } else {
- $ldap_username = null;
- $ldap_password = null;
- }
-
- if (! @ldap_bind($ldap, $ldap_username, $ldap_password)) {
- $this->logger->error('LDAP: Unable to bind to server with: '.$ldap_username);
- $this->logger->error('LDAP: bind type='.$this->getLdapBindType());
- return false;
- }
-
- return true;
- }
-
- /**
- * Get LDAP user profile
- *
- * @access public
- * @param resource $ldap
- * @param string $username
- * @param string $password
- * @return boolean|array
- */
- public function getProfile($ldap, $username, $password)
- {
- $user_pattern = $this->getLdapUserPattern($username);
- $entries = $this->executeQuery($ldap, $user_pattern);
-
- if ($entries === false) {
- $this->logger->error('LDAP: Unable to get user profile: '.$user_pattern);
- return false;
- }
-
- if (@ldap_bind($ldap, $entries[0]['dn'], $password)) {
- return $this->prepareProfile($ldap, $entries, $username);
- }
-
- if (DEBUG) {
- $this->logger->debug('LDAP: wrong password for '.$entries[0]['dn']);
- }
-
- return false;
- }
-
- /**
- * Build user profile from LDAP information
- *
- * @access public
- * @param resource $ldap
- * @param array $entries
- * @param string $username
- * @return boolean|array
- */
- public function prepareProfile($ldap, array $entries, $username)
- {
- if ($this->getLdapAccountId() !== '') {
- $username = $this->getEntry($entries, $this->getLdapAccountId(), $username);
- }
-
- return array(
- 'username' => $username,
- 'name' => $this->getEntry($entries, $this->getLdapAccountName()),
- 'email' => $this->getEntry($entries, $this->getLdapAccountEmail()),
- 'is_admin' => (int) $this->isMemberOf($this->getEntries($entries, $this->getLdapAccountMemberOf()), $this->getLdapGroupAdmin()),
- 'is_project_admin' => (int) $this->isMemberOf($this->getEntries($entries, $this->getLdapAccountMemberOf()), $this->getLdapGroupProjectAdmin()),
- 'is_ldap_user' => 1,
- );
- }
-
- /**
- * Check group membership
- *
- * @access public
- * @param array $group_entries
- * @param string $group_dn
- * @return boolean
- */
- public function isMemberOf(array $group_entries, $group_dn)
- {
- if (! isset($group_entries['count']) || empty($group_dn)) {
- return false;
- }
-
- for ($i = 0; $i < $group_entries['count']; $i++) {
- if ($group_entries[$i] === $group_dn) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Retrieve info on LDAP user by username or email
- *
- * @access public
- * @param string $username
- * @param string $email
- * @return boolean|array
- */
- public function lookup($username = null, $email = null)
- {
- $query = $this->getLookupQuery($username, $email);
- if ($query === '') {
- return false;
- }
-
- // Connect and attempt anonymous or proxy binding
- $ldap = $this->connect();
- if ($ldap === false || ! $this->bind($ldap, null, null)) {
- return false;
- }
-
- // Try to find user
- $entries = $this->executeQuery($ldap, $query);
- if ($entries === false) {
- return false;
- }
-
- // User id not retrieved: LDAP_ACCOUNT_ID not properly configured
- if (empty($username) && ! isset($entries[0][$this->getLdapAccountId()][0])) {
- return false;
- }
-
- return $this->prepareProfile($ldap, $entries, $username);
- }
-
- /**
- * Execute LDAP query
- *
- * @access private
- * @param resource $ldap
- * @param string $query
- * @return boolean|array
- */
- private function executeQuery($ldap, $query)
- {
- $sr = @ldap_search($ldap, $this->getLdapBaseDn(), $query, $this->getProfileAttributes());
- if ($sr === false) {
- return false;
- }
-
- $entries = ldap_get_entries($ldap, $sr);
- if ($entries === false || count($entries) === 0 || $entries['count'] == 0) {
- return false;
- }
-
- return $entries;
- }
-
- /**
- * Get the LDAP query to find a user
- *
- * @access private
- * @param string $username
- * @param string $email
- * @return string
- */
- private function getLookupQuery($username, $email)
- {
- if (! empty($username) && ! empty($email)) {
- return '(&('.$this->getLdapUserPattern($username).')('.$this->getLdapAccountEmail().'='.$email.'))';
- } elseif (! empty($username)) {
- return $this->getLdapUserPattern($username);
- } elseif (! empty($email)) {
- return '('.$this->getLdapAccountEmail().'='.$email.')';
- }
-
- return '';
- }
-
- /**
- * Return one entry from a list of entries
- *
- * @access private
- * @param array $entries LDAP entries
- * @param string $key Key
- * @param string $default Default value if key not set in entry
- * @return string
- */
- private function getEntry(array $entries, $key, $default = '')
- {
- return isset($entries[0][$key][0]) ? $entries[0][$key][0] : $default;
- }
-
- /**
- * Return subset of entries
- *
- * @access private
- * @param array $entries
- * @param string $key
- * @param array $default
- * @return array
- */
- private function getEntries(array $entries, $key, $default = array())
- {
- return isset($entries[0][$key]) ? $entries[0][$key] : $default;
- }
-}
diff --git a/app/Auth/LdapAuth.php b/app/Auth/LdapAuth.php
new file mode 100644
index 00000000..eb66e54d
--- /dev/null
+++ b/app/Auth/LdapAuth.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use LogicException;
+use Kanboard\Core\Base;
+use Kanboard\Core\Ldap\Client as LdapClient;
+use Kanboard\Core\Ldap\ClientException as LdapException;
+use Kanboard\Core\Ldap\User as LdapUser;
+use Kanboard\Core\Security\PasswordAuthenticationProviderInterface;
+
+/**
+ * LDAP Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access private
+ * @var \Kanboard\User\LdapUserProvider
+ */
+ private $user = null;
+
+ /**
+ * Username
+ *
+ * @access private
+ * @var string
+ */
+ private $username = '';
+
+ /**
+ * Password
+ *
+ * @access private
+ * @var string
+ */
+ private $password = '';
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'LDAP';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ try {
+
+ $ldap = LdapClient::connect($this->getLdapUsername(), $this->getLdapPassword());
+ $user = LdapUser::getUser($ldap, $this->getLdapUserPattern());
+
+ if ($user === null) {
+ $this->logger->info('User not found in LDAP server');
+ return false;
+ }
+
+ if ($user->getUsername() === '') {
+ throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
+ }
+
+ if ($ldap->authenticate($user->getDn(), $this->password)) {
+ $this->user = $user;
+ return true;
+ }
+
+ } catch (LdapException $e) {
+ $this->logger->error($e->getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return \Kanboard\User\LdapUserProvider
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set username
+ *
+ * @access public
+ * @param string $username
+ */
+ public function setUsername($username)
+ {
+ $this->username = $username;
+ }
+
+ /**
+ * Set password
+ *
+ * @access public
+ * @param string $password
+ */
+ public function setPassword($password)
+ {
+ $this->password = $password;
+ }
+
+ /**
+ * Get LDAP user pattern
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapUserPattern()
+ {
+ if (! LDAP_USER_FILTER) {
+ throw new LogicException('LDAP user filter empty, check the parameter LDAP_USER_FILTER');
+ }
+
+ return sprintf(LDAP_USER_FILTER, $this->username);
+ }
+
+ /**
+ * Get LDAP username (proxy auth)
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapUsername()
+ {
+ switch ($this->getLdapBindType()) {
+ case 'proxy':
+ return LDAP_USERNAME;
+ case 'user':
+ return sprintf(LDAP_USERNAME, $this->username);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Get LDAP password (proxy auth)
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapPassword()
+ {
+ switch ($this->getLdapBindType()) {
+ case 'proxy':
+ return LDAP_PASSWORD;
+ case 'user':
+ return $this->password;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Get LDAP bind type
+ *
+ * @access public
+ * @return integer
+ */
+ public function getLdapBindType()
+ {
+ if (LDAP_BIND_TYPE !== 'user' && LDAP_BIND_TYPE !== 'proxy' && LDAP_BIND_TYPE !== 'anonymous') {
+ throw new LogicException('Wrong value for the parameter LDAP_BIND_TYPE');
+ }
+
+ return LDAP_BIND_TYPE;
+ }
+}
diff --git a/app/Auth/RememberMe.php b/app/Auth/RememberMe.php
deleted file mode 100644
index 0a567cbe..00000000
--- a/app/Auth/RememberMe.php
+++ /dev/null
@@ -1,323 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Core\Http\Request;
-use Kanboard\Event\AuthEvent;
-use Kanboard\Core\Security\Token;
-
-/**
- * RememberMe model
- *
- * @package auth
- * @author Frederic Guillot
- */
-class RememberMe extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'RememberMe';
-
- /**
- * SQL table name
- *
- * @var string
- */
- const TABLE = 'remember_me';
-
- /**
- * Cookie name
- *
- * @var string
- */
- const COOKIE_NAME = '__R';
-
- /**
- * Expiration (60 days)
- *
- * @var integer
- */
- const EXPIRATION = 5184000;
-
- /**
- * Get a remember me record
- *
- * @access public
- * @param $token
- * @param $sequence
- * @return mixed
- */
- public function find($token, $sequence)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('token', $token)
- ->eq('sequence', $sequence)
- ->gt('expiration', time())
- ->findOne();
- }
-
- /**
- * Get all sessions for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return array
- */
- public function getAll($user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('user_id', $user_id)
- ->desc('date_creation')
- ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
- ->findAll();
- }
-
- /**
- * Authenticate the user with the cookie
- *
- * @access public
- * @return bool
- */
- public function authenticate()
- {
- $credentials = $this->readCookie();
-
- if ($credentials !== false) {
- $record = $this->find($credentials['token'], $credentials['sequence']);
-
- if ($record) {
-
- // Update the sequence
- $this->writeCookie(
- $record['token'],
- $this->update($record['token']),
- $record['expiration']
- );
-
- // Create the session
- $this->userSession->initialize($this->user->getById($record['user_id']));
-
- // Do not ask 2FA for remember me session
- $this->sessionStorage->postAuth['validated'] = true;
-
- $this->container['dispatcher']->dispatch(
- 'auth.success',
- new AuthEvent(self::AUTH_NAME, $this->userSession->getId())
- );
-
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Remove a session record
- *
- * @access public
- * @param integer $session_id Session id
- * @return mixed
- */
- public function remove($session_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('id', $session_id)
- ->remove();
- }
-
- /**
- * Remove the current RememberMe session and the cookie
- *
- * @access public
- * @param integer $user_id User id
- */
- public function destroy($user_id)
- {
- $credentials = $this->readCookie();
-
- if ($credentials !== false) {
- $this->deleteCookie();
-
- $this->db
- ->table(self::TABLE)
- ->eq('user_id', $user_id)
- ->eq('token', $credentials['token'])
- ->remove();
- }
- }
-
- /**
- * Create a new RememberMe session
- *
- * @access public
- * @param integer $user_id User id
- * @param string $ip IP Address
- * @param string $user_agent User Agent
- * @return array
- */
- public function create($user_id, $ip, $user_agent)
- {
- $token = hash('sha256', $user_id.$user_agent.$ip.Token::getToken());
- $sequence = Token::getToken();
- $expiration = time() + self::EXPIRATION;
-
- $this->cleanup($user_id);
-
- $this
- ->db
- ->table(self::TABLE)
- ->insert(array(
- 'user_id' => $user_id,
- 'ip' => $ip,
- 'user_agent' => $user_agent,
- 'token' => $token,
- 'sequence' => $sequence,
- 'expiration' => $expiration,
- 'date_creation' => time(),
- ));
-
- return array(
- 'token' => $token,
- 'sequence' => $sequence,
- 'expiration' => $expiration,
- );
- }
-
- /**
- * Remove old sessions for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return bool
- */
- public function cleanup($user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('user_id', $user_id)
- ->lt('expiration', time())
- ->remove();
- }
-
- /**
- * Return a new sequence token and update the database
- *
- * @access public
- * @param string $token Session token
- * @return string
- */
- public function update($token)
- {
- $new_sequence = Token::getToken();
-
- $this->db
- ->table(self::TABLE)
- ->eq('token', $token)
- ->update(array('sequence' => $new_sequence));
-
- return $new_sequence;
- }
-
- /**
- * Encode the cookie
- *
- * @access public
- * @param string $token Session token
- * @param string $sequence Sequence token
- * @return string
- */
- public function encodeCookie($token, $sequence)
- {
- return implode('|', array($token, $sequence));
- }
-
- /**
- * Decode the value of a cookie
- *
- * @access public
- * @param string $value Raw cookie data
- * @return array
- */
- public function decodeCookie($value)
- {
- list($token, $sequence) = explode('|', $value);
-
- return array(
- 'token' => $token,
- 'sequence' => $sequence,
- );
- }
-
- /**
- * Return true if the current user has a RememberMe cookie
- *
- * @access public
- * @return bool
- */
- public function hasCookie()
- {
- return ! empty($_COOKIE[self::COOKIE_NAME]);
- }
-
- /**
- * Write and encode the cookie
- *
- * @access public
- * @param string $token Session token
- * @param string $sequence Sequence token
- * @param string $expiration Cookie expiration
- */
- public function writeCookie($token, $sequence, $expiration)
- {
- setcookie(
- self::COOKIE_NAME,
- $this->encodeCookie($token, $sequence),
- $expiration,
- $this->helper->url->dir(),
- null,
- Request::isHTTPS(),
- true
- );
- }
-
- /**
- * Read and decode the cookie
- *
- * @access public
- * @return mixed
- */
- public function readCookie()
- {
- if (empty($_COOKIE[self::COOKIE_NAME])) {
- return false;
- }
-
- return $this->decodeCookie($_COOKIE[self::COOKIE_NAME]);
- }
-
- /**
- * Remove the cookie
- *
- * @access public
- */
- public function deleteCookie()
- {
- setcookie(
- self::COOKIE_NAME,
- '',
- time() - 3600,
- $this->helper->url->dir(),
- null,
- Request::isHTTPS(),
- true
- );
- }
-}
diff --git a/app/Auth/RememberMeAuth.php b/app/Auth/RememberMeAuth.php
new file mode 100644
index 00000000..02b7b9f6
--- /dev/null
+++ b/app/Auth/RememberMeAuth.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\PreAuthenticationProviderInterface;
+use Kanboard\User\DatabaseUserProvider;
+
+/**
+ * Rember Me Cookie Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class RememberMeAuth extends Base implements PreAuthenticationProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access private
+ * @var array
+ */
+ private $userInfo = array();
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'RememberMe';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $credentials = $this->rememberMeCookie->read();
+
+ if ($credentials !== false) {
+ $session = $this->rememberMeSession->find($credentials['token'], $credentials['sequence']);
+
+ if (! empty($session)) {
+ $this->rememberMeCookie->write(
+ $session['token'],
+ $this->rememberMeSession->updateSequence($session['token']),
+ $session['expiration']
+ );
+
+ $this->userInfo = $this->user->getById($session['user_id']);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return null|DatabaseUserProvider
+ */
+ public function getUser()
+ {
+ if (empty($this->userInfo)) {
+ return null;
+ }
+
+ return new DatabaseUserProvider($this->userInfo);
+ }
+}
diff --git a/app/Auth/ReverseProxy.php b/app/Auth/ReverseProxy.php
deleted file mode 100644
index d119ca98..00000000
--- a/app/Auth/ReverseProxy.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-use Kanboard\Core\Base;
-use Kanboard\Event\AuthEvent;
-
-/**
- * ReverseProxy backend
- *
- * @package auth
- * @author Sylvain Veyrié
- */
-class ReverseProxy extends Base
-{
- /**
- * Backend name
- *
- * @var string
- */
- const AUTH_NAME = 'ReverseProxy';
-
- /**
- * Get username from the reverse proxy
- *
- * @access public
- * @return string
- */
- public function getUsername()
- {
- return isset($_SERVER[REVERSE_PROXY_USER_HEADER]) ? $_SERVER[REVERSE_PROXY_USER_HEADER] : '';
- }
-
- /**
- * Authenticate the user with the HTTP header
- *
- * @access public
- * @return bool
- */
- public function authenticate()
- {
- if (isset($_SERVER[REVERSE_PROXY_USER_HEADER])) {
- $login = $_SERVER[REVERSE_PROXY_USER_HEADER];
- $user = $this->user->getByUsername($login);
-
- if (empty($user)) {
- $this->createUser($login);
- $user = $this->user->getByUsername($login);
- }
-
- $this->userSession->initialize($user);
- $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Create automatically a new local user after the authentication
- *
- * @access private
- * @param string $login Username
- * @return bool
- */
- private function createUser($login)
- {
- $email = strpos($login, '@') !== false ? $login : '';
-
- if (REVERSE_PROXY_DEFAULT_DOMAIN !== '' && empty($email)) {
- $email = $login.'@'.REVERSE_PROXY_DEFAULT_DOMAIN;
- }
-
- return $this->user->create(array(
- 'email' => $email,
- 'username' => $login,
- 'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login,
- 'is_ldap_user' => 1,
- 'disable_login_form' => 1,
- ));
- }
-}
diff --git a/app/Auth/ReverseProxyAuth.php b/app/Auth/ReverseProxyAuth.php
new file mode 100644
index 00000000..8af7f0a2
--- /dev/null
+++ b/app/Auth/ReverseProxyAuth.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\PreAuthenticationProviderInterface;
+use Kanboard\Core\Security\SessionCheckProviderInterface;
+use Kanboard\User\ReverseProxyUserProvider;
+
+/**
+ * ReverseProxy Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterface, SessionCheckProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access private
+ * @var \Kanboard\User\ReverseProxyUserProvider
+ */
+ private $user = null;
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'ReverseProxy';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $username = $this->request->getRemoteUser();
+
+ if (! empty($username)) {
+ $this->user = new ReverseProxyUserProvider($username);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the user session is valid
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isValidSession()
+ {
+ return $this->request->getRemoteUser() === $this->userSession->getUsername();
+ }
+
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return null|ReverseProxyUserProvider
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+}
diff --git a/app/Auth/TotpAuth.php b/app/Auth/TotpAuth.php
new file mode 100644
index 00000000..f41fabd8
--- /dev/null
+++ b/app/Auth/TotpAuth.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Kanboard\Auth;
+
+use Otp\Otp;
+use Otp\GoogleAuthenticator;
+use Base32\Base32;
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\PostAuthenticationProviderInterface;
+
+/**
+ * TOTP Authentication Provider
+ *
+ * @package auth
+ * @author Frederic Guillot
+ */
+class TotpAuth extends Base implements PostAuthenticationProviderInterface
+{
+ /**
+ * User pin code
+ *
+ * @access private
+ * @var string
+ */
+ private $code = '';
+
+ /**
+ * Private key
+ *
+ * @access private
+ * @var string
+ */
+ private $secret = '';
+
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return 'Time-based One-time Password Algorithm';
+ }
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate()
+ {
+ $otp = new Otp;
+ return $otp->checkTotp(Base32::decode($this->secret), $this->code);
+ }
+
+ /**
+ * Set validation code
+ *
+ * @access public
+ * @param string $code
+ */
+ public function setCode($code)
+ {
+ $this->code = $code;
+ }
+
+ /**
+ * Set secret token
+ *
+ * @access public
+ * @param string $secret
+ */
+ public function setSecret($secret)
+ {
+ $this->secret = $secret;
+ }
+
+ /**
+ * Get secret token
+ *
+ * @access public
+ * @return string
+ */
+ public function getSecret()
+ {
+ if (empty($this->secret)) {
+ $this->secret = GoogleAuthenticator::generateRandom();
+ }
+
+ return $this->secret;
+ }
+
+ /**
+ * Get QR code url
+ *
+ * @access public
+ * @param string $label
+ * @return string
+ */
+ public function getQrCodeUrl($label)
+ {
+ if (empty($this->secret)) {
+ return '';
+ }
+
+ return GoogleAuthenticator::getQrCodeUrl('totp', $label, $this->secret);
+ }
+
+ /**
+ * Get key url (empty if no url can be provided)
+ *
+ * @access public
+ * @param string $label
+ * @return string
+ */
+ public function getKeyUrl($label)
+ {
+ if (empty($this->secret)) {
+ return '';
+ }
+
+ return GoogleAuthenticator::getKeyUri('totp', $label, $this->secret);
+ }
+}
diff --git a/app/Controller/Action.php b/app/Controller/Action.php
index ad136067..3caea45c 100644
--- a/app/Controller/Action.php
+++ b/app/Controller/Action.php
@@ -27,7 +27,7 @@ class Action extends Base
'available_events' => $this->action->getAvailableEvents(),
'available_params' => $this->action->getAllActionParameters(),
'columns_list' => $this->board->getColumnsList($project['id']),
- 'users_list' => $this->projectPermission->getMemberList($project['id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
'projects_list' => $this->project->getList(false),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
@@ -86,7 +86,7 @@ class Action extends Base
'values' => $values,
'action_params' => $action_params,
'columns_list' => $this->board->getColumnsList($project['id']),
- 'users_list' => $this->projectPermission->getMemberList($project['id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
'projects_list' => $projects_list,
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
diff --git a/app/Controller/Activity.php b/app/Controller/Activity.php
index 24327c23..71d5e94f 100644
--- a/app/Controller/Activity.php
+++ b/app/Controller/Activity.php
@@ -20,7 +20,7 @@ class Activity extends Base
$project = $this->getProject();
$this->response->html($this->template->layout('activity/project', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'events' => $this->projectActivity->getProject($project['id']),
'project' => $project,
'title' => t('%s\'s activity', $project['name'])
diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php
index 1082b462..e03d8cab 100644
--- a/app/Controller/Analytic.php
+++ b/app/Controller/Analytic.php
@@ -20,7 +20,7 @@ class Analytic extends Base
*/
private function layout($template, array $params)
{
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['content_for_sublayout'] = $this->template->render($template, $params);
return $this->template->layout('analytic/layout', $params);
@@ -132,6 +132,9 @@ class Analytic extends Base
* Common method for CFD and Burdown chart
*
* @access private
+ * @param string $template
+ * @param string $column
+ * @param string $title
*/
private function commonAggregateMetrics($template, $column, $title)
{
diff --git a/app/Controller/App.php b/app/Controller/App.php
index 2fae004c..c596b4a8 100644
--- a/app/Controller/App.php
+++ b/app/Controller/App.php
@@ -22,7 +22,7 @@ class App extends Base
*/
private function layout($template, array $params)
{
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['content_for_sublayout'] = $this->template->render($template, $params);
return $this->template->layout('app/layout', $params);
@@ -42,7 +42,7 @@ class App extends Base
->setUrl('app', $action, array('pagination' => 'projects', 'user_id' => $user_id))
->setMax($max)
->setOrder('name')
- ->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id)))
+ ->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id)))
->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects');
}
@@ -169,7 +169,7 @@ class App extends Base
$this->response->html($this->layout('app/activity', array(
'title' => t('My activity stream'),
- 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveMemberProjectIds($user['id']), 100),
+ 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100),
'user' => $user,
)));
}
@@ -202,49 +202,4 @@ class App extends Base
'user' => $user,
)));
}
-
- /**
- * Render Markdown text and reply with the HTML Code
- *
- * @access public
- */
- public function preview()
- {
- $payload = $this->request->getJson();
-
- if (empty($payload['text'])) {
- $this->response->html('<p>'.t('Nothing to preview...').'</p>');
- }
-
- $this->response->html($this->helper->text->markdown($payload['text']));
- }
-
- /**
- * Task autocompletion (Ajax)
- *
- * @access public
- */
- public function autocomplete()
- {
- $search = $this->request->getStringParam('term');
- $projects = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId());
-
- if (empty($projects)) {
- $this->response->json(array());
- }
-
- $filter = $this->taskFilterAutoCompleteFormatter
- ->create()
- ->filterByProjects($projects)
- ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
-
- // Search by task id or by title
- if (ctype_digit($search)) {
- $filter->filterById($search);
- } else {
- $filter->filterByTitle($search);
- }
-
- $this->response->json($filter->format());
- }
}
diff --git a/app/Controller/Auth.php b/app/Controller/Auth.php
index b90e756d..cd1dd167 100644
--- a/app/Controller/Auth.php
+++ b/app/Controller/Auth.php
@@ -24,7 +24,7 @@ class Auth extends Base
}
$this->response->html($this->template->layout('auth/index', array(
- 'captcha' => isset($values['username']) && $this->authentication->hasCaptcha($values['username']),
+ 'captcha' => ! empty($values['username']) && $this->userLocking->hasCaptcha($values['username']),
'errors' => $errors,
'values' => $values,
'no_layout' => true,
@@ -40,18 +40,11 @@ class Auth extends Base
public function check()
{
$values = $this->request->getValues();
+ $this->sessionStorage->hasRememberMe = ! empty($values['remember_me']);
list($valid, $errors) = $this->authentication->validateForm($values);
if ($valid) {
- if (isset($this->sessionStorage->redirectAfterLogin)
- && ! empty($this->sessionStorage->redirectAfterLogin)
- && ! filter_var($this->sessionStorage->redirectAfterLogin, FILTER_VALIDATE_URL)) {
- $redirect = $this->sessionStorage->redirectAfterLogin;
- unset($this->sessionStorage->redirectAfterLogin);
- $this->response->redirect($redirect);
- }
-
- $this->response->redirect($this->helper->url->to('app', 'index'));
+ $this->redirectAfterLogin();
}
$this->login($values, $errors);
@@ -64,7 +57,6 @@ class Auth extends Base
*/
public function logout()
{
- $this->authentication->backend('rememberMe')->destroy($this->userSession->getId());
$this->sessionManager->close();
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
@@ -83,4 +75,20 @@ class Auth extends Base
$this->sessionStorage->captcha = $builder->getPhrase();
$builder->output();
}
+
+ /**
+ * Redirect the user after the authentication
+ *
+ * @access private
+ */
+ private function redirectAfterLogin()
+ {
+ if (isset($this->sessionStorage->redirectAfterLogin) && ! empty($this->sessionStorage->redirectAfterLogin) && ! filter_var($this->sessionStorage->redirectAfterLogin, FILTER_VALIDATE_URL)) {
+ $redirect = $this->sessionStorage->redirectAfterLogin;
+ unset($this->sessionStorage->redirectAfterLogin);
+ $this->response->redirect($redirect);
+ }
+
+ $this->response->redirect($this->helper->url->to('app', 'index'));
+ }
}
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index 8630f00c..76948a0f 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -3,7 +3,7 @@
namespace Kanboard\Controller;
use Pimple\Container;
-use Symfony\Component\EventDispatcher\Event;
+use Kanboard\Core\Security\Role;
/**
* Base controller
@@ -14,36 +14,22 @@ use Symfony\Component\EventDispatcher\Event;
abstract class Base extends \Kanboard\Core\Base
{
/**
- * Constructor
- *
- * @access public
- * @param \Pimple\Container $container
- */
- public function __construct(Container $container)
- {
- $this->container = $container;
-
- if (DEBUG) {
- $this->logger->debug('START_REQUEST='.$_SERVER['REQUEST_URI']);
- }
- }
-
- /**
- * Destructor
+ * Method executed before each action
*
* @access public
*/
- public function __destruct()
+ public function beforeAction($controller, $action)
{
- if (DEBUG) {
- foreach ($this->db->getLogMessages() as $message) {
- $this->logger->debug($message);
- }
+ $this->sessionManager->open();
+ $this->dispatcher->dispatch('app.bootstrap');
+ $this->sendHeaders($action);
+ $this->authenticationManager->checkCurrentSession();
- $this->logger->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries));
- $this->logger->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT']));
- $this->logger->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage()));
- $this->logger->debug('END_REQUEST='.$_SERVER['REQUEST_URI']);
+ if (! $this->applicationAuthorization->isAllowed($controller, $action, Role::APP_PUBLIC)) {
+ $this->handleAuthentication();
+ $this->handlePostAuthentication($controller, $action);
+ $this->checkApplicationAuthorization($controller, $action);
+ $this->checkProjectAuthorization($controller, $action);
}
}
@@ -70,33 +56,13 @@ abstract class Base extends \Kanboard\Core\Base
}
/**
- * Method executed before each action
- *
- * @access public
- */
- public function beforeAction($controller, $action)
- {
- $this->sessionManager->open();
- $this->sendHeaders($action);
- $this->container['dispatcher']->dispatch('session.bootstrap', new Event);
-
- if (! $this->acl->isPublicAction($controller, $action)) {
- $this->handleAuthentication();
- $this->handle2FA($controller, $action);
- $this->handleAuthorization($controller, $action);
-
- $this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
- }
- }
-
- /**
* Check authentication
*
- * @access public
+ * @access private
*/
- public function handleAuthentication()
+ private function handleAuthentication()
{
- if (! $this->authentication->isAuthenticated()) {
+ if (! $this->userSession->isLogged() && ! $this->authenticationManager->preAuthentication()) {
if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401);
}
@@ -107,15 +73,15 @@ abstract class Base extends \Kanboard\Core\Base
}
/**
- * Check 2FA
+ * Handle Post-Authentication (2FA)
*
- * @access public
+ * @access private
*/
- public function handle2FA($controller, $action)
+ private function handlePostAuthentication($controller, $action)
{
$ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout');
- if ($ignore === false && $this->userSession->has2FA() && ! $this->userSession->check2FA()) {
+ if ($ignore === false && $this->userSession->hasPostAuthentication() && ! $this->userSession->isPostAuthenticationValidated()) {
if ($this->request->isAjax()) {
$this->response->text('Not Authorized', 401);
}
@@ -125,11 +91,23 @@ abstract class Base extends \Kanboard\Core\Base
}
/**
- * Check page access and authorization
+ * Check application authorization
*
- * @access public
+ * @access private
+ */
+ private function checkApplicationAuthorization($controller, $action)
+ {
+ if (! $this->helper->user->hasAccess($controller, $action)) {
+ $this->forbidden();
+ }
+ }
+
+ /**
+ * Check project authorization
+ *
+ * @access private
*/
- public function handleAuthorization($controller, $action)
+ private function checkProjectAuthorization($controller, $action)
{
$project_id = $this->request->getIntegerParam('project_id');
$task_id = $this->request->getIntegerParam('task_id');
@@ -139,7 +117,7 @@ abstract class Base extends \Kanboard\Core\Base
$project_id = $this->taskFinder->getProjectId($task_id);
}
- if (! $this->acl->isAllowed($controller, $action, $project_id)) {
+ if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($controller, $action, $project_id)) {
$this->forbidden();
}
}
@@ -147,10 +125,10 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Application not found page (404 error)
*
- * @access public
+ * @access protected
* @param boolean $no_layout Display the layout or not
*/
- public function notfound($no_layout = false)
+ protected function notfound($no_layout = false)
{
$this->response->html($this->template->layout('app/notfound', array(
'title' => t('Page not found'),
@@ -161,11 +139,15 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Application forbidden page
*
- * @access public
+ * @access protected
* @param boolean $no_layout Display the layout or not
*/
- public function forbidden($no_layout = false)
+ protected function forbidden($no_layout = false)
{
+ if ($this->request->isAjax()) {
+ $this->response->text('Not Authorized', 401);
+ }
+
$this->response->html($this->template->layout('app/forbidden', array(
'title' => t('Access Forbidden'),
'no_layout' => $no_layout,
@@ -209,7 +191,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->projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
return $this->template->layout('task/layout', $params);
}
@@ -227,7 +209,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->projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['sidebar_template'] = $sidebar_template;
return $this->template->layout('project/layout', $params);
@@ -300,12 +282,15 @@ abstract class Base extends \Kanboard\Core\Base
* Common method to get project filters
*
* @access protected
+ * @param string $controller
+ * @param string $action
+ * @return array
*/
protected function getProjectFilters($controller, $action)
{
$project = $this->getProject();
$search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id']));
- $board_selector = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $board_selector = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
unset($board_selector[$project['id']]);
$filters = array(
diff --git a/app/Controller/Board.php b/app/Controller/Board.php
index 7442ff22..a75fea33 100644
--- a/app/Controller/Board.php
+++ b/app/Controller/Board.php
@@ -51,7 +51,7 @@ class Board extends Base
$this->response->html($this->template->layout('board/view_private', array(
'categories_list' => $this->category->getList($params['project']['id'], false),
- 'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()),
'swimlanes' => $this->taskFilter->search($params['filters']['search'])->getBoard($params['project']['id']),
'description' => $params['project']['description'],
@@ -143,195 +143,6 @@ class Board extends Base
}
/**
- * Get links on mouseover
- *
- * @access public
- */
- public function tasklinks()
- {
- $task = $this->getTask();
- $this->response->html($this->template->render('board/tooltip_tasklinks', array(
- 'links' => $this->taskLink->getAll($task['id']),
- 'task' => $task,
- )));
- }
-
- /**
- * Get subtasks on mouseover
- *
- * @access public
- */
- public function subtasks()
- {
- $task = $this->getTask();
- $this->response->html($this->template->render('board/tooltip_subtasks', array(
- 'subtasks' => $this->subtask->getAll($task['id']),
- 'task' => $task,
- )));
- }
-
- /**
- * Display all attachments during the task mouseover
- *
- * @access public
- */
- public function attachments()
- {
- $task = $this->getTask();
-
- $this->response->html($this->template->render('board/tooltip_files', array(
- 'files' => $this->file->getAll($task['id']),
- 'task' => $task,
- )));
- }
-
- /**
- * Display comments during a task mouseover
- *
- * @access public
- */
- public function comments()
- {
- $task = $this->getTask();
-
- $this->response->html($this->template->render('board/tooltip_comments', array(
- 'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting())
- )));
- }
-
- /**
- * Display task description
- *
- * @access public
- */
- public function description()
- {
- $task = $this->getTask();
-
- $this->response->html($this->template->render('board/tooltip_description', array(
- 'task' => $task
- )));
- }
-
- /**
- * Change a task assignee directly from the board
- *
- * @access public
- */
- public function changeAssignee()
- {
- $task = $this->getTask();
- $project = $this->project->getById($task['project_id']);
-
- $this->response->html($this->template->render('board/popover_assignee', array(
- 'values' => $task,
- 'users_list' => $this->projectPermission->getMemberList($project['id']),
- 'project' => $project,
- )));
- }
-
- /**
- * Validate an assignee modification
- *
- * @access public
- */
- public function updateAssignee()
- {
- $values = $this->request->getValues();
-
- list($valid, ) = $this->taskValidator->validateAssigneeModification($values);
-
- if ($valid && $this->taskModification->update($values)) {
- $this->flash->success(t('Task updated successfully.'));
- } else {
- $this->flash->failure(t('Unable to update your task.'));
- }
-
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
- }
-
- /**
- * Change a task category directly from the board
- *
- * @access public
- */
- public function changeCategory()
- {
- $task = $this->getTask();
- $project = $this->project->getById($task['project_id']);
-
- $this->response->html($this->template->render('board/popover_category', array(
- 'values' => $task,
- 'categories_list' => $this->category->getList($project['id']),
- 'project' => $project,
- )));
- }
-
- /**
- * Validate a category modification
- *
- * @access public
- */
- public function updateCategory()
- {
- $values = $this->request->getValues();
-
- list($valid, ) = $this->taskValidator->validateCategoryModification($values);
-
- if ($valid && $this->taskModification->update($values)) {
- $this->flash->success(t('Task updated successfully.'));
- } else {
- $this->flash->failure(t('Unable to update your task.'));
- }
-
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
- }
-
- /**
- * Screenshot popover
- *
- * @access public
- */
- public function screenshot()
- {
- $task = $this->getTask();
-
- $this->response->html($this->template->render('file/screenshot', array(
- 'task' => $task,
- 'redirect' => 'board',
- )));
- }
-
- /**
- * Get recurrence information on mouseover
- *
- * @access public
- */
- public function recurrence()
- {
- $task = $this->getTask();
-
- $this->response->html($this->template->render('task/recurring_info', array(
- 'task' => $task,
- 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
- 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
- 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
- )));
- }
-
- /**
- * Display swimlane description in tooltip
- *
- * @access public
- */
- public function swimlane()
- {
- $this->getProject();
- $swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id'));
- $this->response->html($this->template->render('board/tooltip_description', array('task' => $swimlane)));
- }
-
- /**
* Enable collapsed mode
*
* @access public
@@ -355,6 +166,7 @@ class Board extends Base
* Change display mode
*
* @access private
+ * @param boolean $mode
*/
private function changeDisplayMode($mode)
{
@@ -372,6 +184,7 @@ class Board extends Base
* Render board
*
* @access private
+ * @param integer $project_id
*/
private function renderBoard($project_id)
{
diff --git a/app/Controller/BoardPopover.php b/app/Controller/BoardPopover.php
new file mode 100644
index 00000000..51ec9bc4
--- /dev/null
+++ b/app/Controller/BoardPopover.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Board Popover
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class BoardPopover extends Base
+{
+ /**
+ * Change a task assignee directly from the board
+ *
+ * @access public
+ */
+ public function changeAssignee()
+ {
+ $task = $this->getTask();
+ $project = $this->project->getById($task['project_id']);
+
+ $this->response->html($this->template->render('board/popover_assignee', array(
+ 'values' => $task,
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']),
+ 'project' => $project,
+ )));
+ }
+
+ /**
+ * Validate an assignee modification
+ *
+ * @access public
+ */
+ public function updateAssignee()
+ {
+ $values = $this->request->getValues();
+
+ list($valid, ) = $this->taskValidator->validateAssigneeModification($values);
+
+ if ($valid && $this->taskModification->update($values)) {
+ $this->flash->success(t('Task updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update your task.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
+ }
+
+ /**
+ * Change a task category directly from the board
+ *
+ * @access public
+ */
+ public function changeCategory()
+ {
+ $task = $this->getTask();
+ $project = $this->project->getById($task['project_id']);
+
+ $this->response->html($this->template->render('board/popover_category', array(
+ 'values' => $task,
+ 'categories_list' => $this->category->getList($project['id']),
+ 'project' => $project,
+ )));
+ }
+
+ /**
+ * Validate a category modification
+ *
+ * @access public
+ */
+ public function updateCategory()
+ {
+ $values = $this->request->getValues();
+
+ list($valid, ) = $this->taskValidator->validateCategoryModification($values);
+
+ if ($valid && $this->taskModification->update($values)) {
+ $this->flash->success(t('Task updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update your task.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
+ }
+
+ /**
+ * Screenshot popover
+ *
+ * @access public
+ */
+ public function screenshot()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('file/screenshot', array(
+ 'task' => $task,
+ 'redirect' => 'board',
+ )));
+ }
+}
diff --git a/app/Controller/BoardTooltip.php b/app/Controller/BoardTooltip.php
new file mode 100644
index 00000000..ed58a2f2
--- /dev/null
+++ b/app/Controller/BoardTooltip.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Board Tooltip
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class BoardTooltip extends Base
+{
+ /**
+ * Get links on mouseover
+ *
+ * @access public
+ */
+ public function tasklinks()
+ {
+ $task = $this->getTask();
+ $this->response->html($this->template->render('board/tooltip_tasklinks', array(
+ 'links' => $this->taskLink->getAll($task['id']),
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Get subtasks on mouseover
+ *
+ * @access public
+ */
+ public function subtasks()
+ {
+ $task = $this->getTask();
+ $this->response->html($this->template->render('board/tooltip_subtasks', array(
+ 'subtasks' => $this->subtask->getAll($task['id']),
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Display all attachments during the task mouseover
+ *
+ * @access public
+ */
+ public function attachments()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('board/tooltip_files', array(
+ 'files' => $this->file->getAll($task['id']),
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Display comments during a task mouseover
+ *
+ * @access public
+ */
+ public function comments()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('board/tooltip_comments', array(
+ 'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting())
+ )));
+ }
+
+ /**
+ * Display task description
+ *
+ * @access public
+ */
+ public function description()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('board/tooltip_description', array(
+ 'task' => $task
+ )));
+ }
+
+ /**
+ * Get recurrence information on mouseover
+ *
+ * @access public
+ */
+ public function recurrence()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->template->render('task/recurring_info', array(
+ 'task' => $task,
+ 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
+ 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
+ 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
+ )));
+ }
+
+ /**
+ * Display swimlane description in tooltip
+ *
+ * @access public
+ */
+ public function swimlane()
+ {
+ $this->getProject();
+ $swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id'));
+ $this->response->html($this->template->render('board/tooltip_description', array('task' => $swimlane)));
+ }
+}
diff --git a/app/Controller/Config.php b/app/Controller/Config.php
index 49806144..c813c795 100644
--- a/app/Controller/Config.php
+++ b/app/Controller/Config.php
@@ -20,7 +20,7 @@ class Config extends Base
*/
private function layout($template, array $params)
{
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['values'] = $this->config->getAll();
$params['errors'] = array();
$params['config_content_for_layout'] = $this->template->render($template, $params);
diff --git a/app/Controller/Currency.php b/app/Controller/Currency.php
index 118b2c41..89e38569 100644
--- a/app/Controller/Currency.php
+++ b/app/Controller/Currency.php
@@ -20,7 +20,7 @@ class Currency extends Base
*/
private function layout($template, array $params)
{
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['config_content_for_layout'] = $this->template->render($template, $params);
return $this->template->layout('config/layout', $params);
diff --git a/app/Controller/Customfilter.php b/app/Controller/Customfilter.php
index d6863103..ef75a837 100644
--- a/app/Controller/Customfilter.php
+++ b/app/Controller/Customfilter.php
@@ -137,7 +137,7 @@ class Customfilter extends Base
{
$user_id = $this->userSession->getId();
- if ($filter['user_id'] != $user_id && (! $this->projectPermission->isManager($project['id'], $user_id) || ! $this->userSession->isAdmin())) {
+ if ($filter['user_id'] != $user_id && ($this->projectUserRole->getUserRole($project['id'], $user_id) === Role::PROJECT_MANAGER || ! $this->userSession->isAdmin())) {
$this->forbidden();
}
}
diff --git a/app/Controller/Doc.php b/app/Controller/Doc.php
index 32413048..08561aa1 100644
--- a/app/Controller/Doc.php
+++ b/app/Controller/Doc.php
@@ -53,7 +53,7 @@ class Doc extends Base
}
$this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
)));
}
}
diff --git a/app/Controller/Feed.php b/app/Controller/Feed.php
index 95b81fb8..8457c383 100644
--- a/app/Controller/Feed.php
+++ b/app/Controller/Feed.php
@@ -25,10 +25,8 @@ class Feed extends Base
$this->forbidden(true);
}
- $projects = $this->projectPermission->getActiveMemberProjects($user['id']);
-
$this->response->xml($this->template->render('feed/user', array(
- 'events' => $this->projectActivity->getProjects(array_keys($projects)),
+ 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id'])),
'user' => $user,
)));
}
diff --git a/app/Controller/Gantt.php b/app/Controller/Gantt.php
index bd3d92f7..f3954a25 100644
--- a/app/Controller/Gantt.php
+++ b/app/Controller/Gantt.php
@@ -20,13 +20,13 @@ class Gantt extends Base
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
} else {
- $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
}
$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->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
)));
}
@@ -66,7 +66,7 @@ class Gantt extends Base
}
$this->response->html($this->template->layout('gantt/project', $params + array(
- 'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
'sorting' => $sorting,
'tasks' => $filter->format(),
)));
@@ -109,7 +109,7 @@ class Gantt extends Base
'column_id' => $this->board->getFirstColumn($project['id']),
'position' => 1
),
- 'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'swimlanes_list' => $this->swimlane->getList($project['id'], false, true),
diff --git a/app/Controller/Group.php b/app/Controller/Group.php
index 4e81f6c1..22d49e61 100644
--- a/app/Controller/Group.php
+++ b/app/Controller/Group.php
@@ -25,7 +25,7 @@ class Group extends Base
->calculate();
$this->response->html($this->template->layout('group/index', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'title' => t('Groups').' ('.$paginator->getTotal().')',
'paginator' => $paginator,
)));
@@ -49,7 +49,7 @@ class Group extends Base
->calculate();
$this->response->html($this->template->layout('group/users', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($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->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'errors' => $errors,
'values' => $values,
'title' => t('New group')
@@ -105,7 +105,7 @@ class Group extends Base
}
$this->response->html($this->template->layout('group/edit', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'errors' => $errors,
'values' => $values,
'title' => t('Edit group')
@@ -149,7 +149,7 @@ class Group extends Base
}
$this->response->html($this->template->layout('group/associate', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'users' => $this->user->prepareList($this->groupMember->getNotMembers($group_id)),
'group' => $group,
'errors' => $errors,
diff --git a/app/Controller/GroupHelper.php b/app/Controller/GroupHelper.php
new file mode 100644
index 00000000..34f522a6
--- /dev/null
+++ b/app/Controller/GroupHelper.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Group Helper
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class GroupHelper extends Base
+{
+ /**
+ * Group autocompletion (Ajax)
+ *
+ * @access public
+ */
+ public function autocomplete()
+ {
+ $search = $this->request->getStringParam('term');
+ $groups = $this->groupManager->find($search);
+ $this->response->json($this->groupAutoCompleteFormatter->setGroups($groups)->format());
+ }
+}
diff --git a/app/Controller/Link.php b/app/Controller/Link.php
index c7f18230..33ec6688 100644
--- a/app/Controller/Link.php
+++ b/app/Controller/Link.php
@@ -21,7 +21,7 @@ class Link extends Base
*/
private function layout($template, array $params)
{
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['config_content_for_layout'] = $this->template->render($template, $params);
return $this->template->layout('config/layout', $params);
diff --git a/app/Controller/Oauth.php b/app/Controller/Oauth.php
index 39546148..ed901def 100644
--- a/app/Controller/Oauth.php
+++ b/app/Controller/Oauth.php
@@ -17,7 +17,7 @@ class Oauth extends Base
*/
public function google()
{
- $this->step1('google');
+ $this->step1('Google');
}
/**
@@ -27,7 +27,7 @@ class Oauth extends Base
*/
public function github()
{
- $this->step1('github');
+ $this->step1('Github');
}
/**
@@ -37,7 +37,7 @@ class Oauth extends Base
*/
public function gitlab()
{
- $this->step1('gitlab');
+ $this->step1('Gitlab');
}
/**
@@ -45,12 +45,12 @@ class Oauth extends Base
*
* @access public
*/
- public function unlink($backend = '')
+ public function unlink()
{
- $backend = $this->request->getStringParam('backend', $backend);
+ $backend = $this->request->getStringParam('backend');
$this->checkCSRFParam();
- if ($this->authentication->backend($backend)->unlink($this->userSession->getId())) {
+ if ($this->authenticationManager->getProvider($backend)->unlink($this->userSession->getId())) {
$this->flash->success(t('Your external account is not linked anymore to your profile.'));
} else {
$this->flash->failure(t('Unable to unlink your external account.'));
@@ -63,15 +63,16 @@ class Oauth extends Base
* Redirect to the provider if no code received
*
* @access private
+ * @param string $provider
*/
- private function step1($backend)
+ private function step1($provider)
{
$code = $this->request->getStringParam('code');
if (! empty($code)) {
- $this->step2($backend, $code);
+ $this->step2($provider, $code);
} else {
- $this->response->redirect($this->authentication->backend($backend)->getService()->getAuthorizationUrl());
+ $this->response->redirect($this->authenticationManager->getProvider($provider)->getService()->getAuthorizationUrl());
}
}
@@ -79,30 +80,35 @@ class Oauth extends Base
* Link or authenticate the user
*
* @access private
+ * @param string $provider
+ * @param string $code
*/
- private function step2($backend, $code)
+ private function step2($provider, $code)
{
- $profile = $this->authentication->backend($backend)->getProfile($code);
+ $this->authenticationManager->getProvider($provider)->setCode($code);
if ($this->userSession->isLogged()) {
- $this->link($backend, $profile);
+ $this->link($provider);
}
- $this->authenticate($backend, $profile);
+ $this->authenticate($provider);
}
/**
* Link the account
*
* @access private
+ * @param string $provider
*/
- private function link($backend, $profile)
+ private function link($provider)
{
- if (empty($profile)) {
+ $authProvider = $this->authenticationManager->getProvider($provider);
+
+ if (! $authProvider->authenticate()) {
$this->flash->failure(t('External authentication failed'));
} else {
+ $this->userProfile->assign($this->userSession->getId(), $authProvider->getUser());
$this->flash->success(t('Your external account is linked to your profile successfully.'));
- $this->authentication->backend($backend)->updateUser($this->userSession->getId(), $profile);
}
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
@@ -112,10 +118,11 @@ class Oauth extends Base
* Authenticate the account
*
* @access private
+ * @param string $provider
*/
- private function authenticate($backend, $profile)
+ private function authenticate($provider)
{
- if (! empty($profile) && $this->authentication->backend($backend)->authenticate($profile['id'])) {
+ if ($this->authenticationManager->oauthAuthentication($provider)) {
$this->response->redirect($this->helper->url->to('app', 'index'));
} else {
$this->response->html($this->template->layout('auth/index', array(
diff --git a/app/Controller/Project.php b/app/Controller/Project.php
index 2d9c25de..80c95aa2 100644
--- a/app/Controller/Project.php
+++ b/app/Controller/Project.php
@@ -20,7 +20,7 @@ class Project extends Base
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
} else {
- $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
}
$nb_projects = count($project_ids);
@@ -33,7 +33,7 @@ class Project extends Base
->calculate();
$this->response->html($this->template->layout('project/index', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'paginator' => $paginator,
'nb_projects' => $nb_projects,
'title' => t('Projects').' ('.$nb_projects.')'
@@ -160,11 +160,11 @@ class Project extends Base
$values = $this->request->getValues();
if (isset($values['is_private'])) {
- if (! $this->helper->user->isProjectAdministrationAllowed($project['id'])) {
+ if (! $this->helper->user->hasProjectAccess('project', 'create', $project['id'])) {
unset($values['is_private']);
}
} elseif ($project['is_private'] == 1 && ! isset($values['is_private'])) {
- if ($this->helper->user->isProjectAdministrationAllowed($project['id'])) {
+ if ($this->helper->user->hasProjectAccess('project', 'create', $project['id'])) {
$values += array('is_private' => 0);
}
}
@@ -184,120 +184,6 @@ class Project extends Base
}
/**
- * Users list for the selected project
- *
- * @access public
- */
- public function users()
- {
- $project = $this->getProject();
-
- $this->response->html($this->projectLayout('project/users', array(
- 'project' => $project,
- 'users' => $this->projectPermission->getAllUsers($project['id']),
- 'title' => t('Edit project access list')
- )));
- }
-
- /**
- * Allow everybody
- *
- * @access public
- */
- public function allowEverybody()
- {
- $project = $this->getProject();
- $values = $this->request->getValues() + array('is_everybody_allowed' => 0);
- list($valid, ) = $this->projectPermission->validateProjectModification($values);
-
- if ($valid) {
- if ($this->project->update($values)) {
- $this->flash->success(t('Project updated successfully.'));
- } else {
- $this->flash->failure(t('Unable to update this project.'));
- }
- }
-
- $this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $project['id'])));
- }
-
- /**
- * Allow a specific user (admin only)
- *
- * @access public
- */
- public function allow()
- {
- $values = $this->request->getValues();
- list($valid, ) = $this->projectPermission->validateUserModification($values);
-
- if ($valid) {
- if ($this->projectPermission->addMember($values['project_id'], $values['user_id'])) {
- $this->flash->success(t('Project updated successfully.'));
- } else {
- $this->flash->failure(t('Unable to update this project.'));
- }
- }
-
- $this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $values['project_id'])));
- }
-
- /**
- * Change the role of a project member
- *
- * @access public
- */
- public function role()
- {
- $this->checkCSRFParam();
-
- $values = array(
- 'project_id' => $this->request->getIntegerParam('project_id'),
- 'user_id' => $this->request->getIntegerParam('user_id'),
- 'is_owner' => $this->request->getIntegerParam('is_owner'),
- );
-
- list($valid, ) = $this->projectPermission->validateUserModification($values);
-
- if ($valid) {
- if ($this->projectPermission->changeRole($values['project_id'], $values['user_id'], $values['is_owner'])) {
- $this->flash->success(t('Project updated successfully.'));
- } else {
- $this->flash->failure(t('Unable to update this project.'));
- }
- }
-
- $this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $values['project_id'])));
- }
-
- /**
- * Revoke user access (admin only)
- *
- * @access public
- */
- public function revoke()
- {
- $this->checkCSRFParam();
-
- $values = array(
- 'project_id' => $this->request->getIntegerParam('project_id'),
- 'user_id' => $this->request->getIntegerParam('user_id'),
- );
-
- list($valid, ) = $this->projectPermission->validateUserModification($values);
-
- if ($valid) {
- if ($this->projectPermission->revokeMember($values['project_id'], $values['user_id'])) {
- $this->flash->success(t('Project updated successfully.'));
- } else {
- $this->flash->failure(t('Unable to update this project.'));
- }
- }
-
- $this->response->redirect($this->helper->url->to('project', 'users', array('project_id' => $values['project_id'])));
- }
-
- /**
* Remove a project
*
* @access public
@@ -413,11 +299,11 @@ class Project extends Base
*/
public function create(array $values = array(), array $errors = array())
{
- $is_private = $this->request->getIntegerParam('private', $this->userSession->isAdmin() || $this->userSession->isProjectAdmin() ? 0 : 1);
+ $is_private = isset($values['is_private']) && $values['is_private'] == 1;
$this->response->html($this->template->layout('project/new', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
- 'values' => empty($values) ? array('is_private' => $is_private) : $values,
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
+ 'values' => $values,
'errors' => $errors,
'is_private' => $is_private,
'title' => $is_private ? t('New private project') : t('New project'),
@@ -425,6 +311,17 @@ class Project extends Base
}
/**
+ * Display a form to create a private project
+ *
+ * @access public
+ */
+ public function createPrivate(array $values = array(), array $errors = array())
+ {
+ $values['is_private'] = 1;
+ $this->create($values, $errors);
+ }
+
+ /**
* Validate and save a new project
*
* @access public
diff --git a/app/Controller/ProjectPermission.php b/app/Controller/ProjectPermission.php
new file mode 100644
index 00000000..4434d017
--- /dev/null
+++ b/app/Controller/ProjectPermission.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Kanboard\Core\Security\Role;
+
+/**
+ * Project Permission
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class ProjectPermission extends Base
+{
+ /**
+ * Show all permissions
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+
+ if (empty($values)) {
+ $values['role'] = Role::PROJECT_MEMBER;
+ }
+
+ $this->response->html($this->projectLayout('project_permission/index', array(
+ 'project' => $project,
+ 'users' => $this->projectUserRole->getUsers($project['id']),
+ 'groups' => $this->projectGroupRole->getGroups($project['id']),
+ 'roles' => $this->role->getProjectRoles(),
+ 'values' => $values,
+ 'errors' => $errors,
+ 'title' => t('Project Permissions'),
+ )));
+ }
+
+ /**
+ * Allow everybody
+ *
+ * @access public
+ */
+ public function allowEverybody()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues() + array('is_everybody_allowed' => 0);
+
+ if ($this->project->update($values)) {
+ $this->flash->success(t('Project updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this project.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id'])));
+ }
+
+ /**
+ * Add user to the project
+ *
+ * @access public
+ */
+ public function addUser()
+ {
+ $values = $this->request->getValues();
+
+ if ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) {
+ $this->flash->success(t('Project updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this project.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id'])));
+ }
+
+ /**
+ * Revoke user access
+ *
+ * @access public
+ */
+ public function removeUser()
+ {
+ $this->checkCSRFParam();
+
+ $values = array(
+ 'project_id' => $this->request->getIntegerParam('project_id'),
+ 'user_id' => $this->request->getIntegerParam('user_id'),
+ );
+
+ if ($this->projectUserRole->removeUser($values['project_id'], $values['user_id'])) {
+ $this->flash->success(t('Project updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this project.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id'])));
+ }
+
+ /**
+ * Change user role
+ *
+ * @access public
+ */
+ public function changeUserRole()
+ {
+ $project_id = $this->request->getIntegerParam('project_id');
+ $values = $this->request->getJson();
+
+ if (! empty($project_id) && ! empty($values) && $this->projectUserRole->changeUserRole($project_id, $values['id'], $values['role'])) {
+ $this->response->json(array('status' => 'ok'));
+ } else {
+ $this->response->json(array('status' => 'error'));
+ }
+ }
+
+ /**
+ * Add group to the project
+ *
+ * @access public
+ */
+ public function addGroup()
+ {
+ $values = $this->request->getValues();
+
+ if (empty($values['group_id']) && ! empty($values['external_id'])) {
+ $values['group_id'] = $this->group->create($values['name'], $values['external_id']);
+ }
+
+ if ($this->projectGroupRole->addGroup($values['project_id'], $values['group_id'], $values['role'])) {
+ $this->flash->success(t('Project updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this project.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id'])));
+ }
+
+ /**
+ * Revoke group access
+ *
+ * @access public
+ */
+ public function removeGroup()
+ {
+ $this->checkCSRFParam();
+
+ $values = array(
+ 'project_id' => $this->request->getIntegerParam('project_id'),
+ 'group_id' => $this->request->getIntegerParam('group_id'),
+ );
+
+ if ($this->projectGroupRole->removeGroup($values['project_id'], $values['group_id'])) {
+ $this->flash->success(t('Project updated successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to update this project.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $values['project_id'])));
+ }
+
+ /**
+ * Change group role
+ *
+ * @access public
+ */
+ public function changeGroupRole()
+ {
+ $project_id = $this->request->getIntegerParam('project_id');
+ $values = $this->request->getJson();
+
+ if (! empty($project_id) && ! empty($values) && $this->projectGroupRole->changeGroupRole($project_id, $values['id'], $values['role'])) {
+ $this->response->json(array('status' => 'ok'));
+ } else {
+ $this->response->json(array('status' => 'error'));
+ }
+ }
+}
diff --git a/app/Controller/Projectuser.php b/app/Controller/Projectuser.php
index 18829b3c..34595764 100644
--- a/app/Controller/Projectuser.php
+++ b/app/Controller/Projectuser.php
@@ -4,6 +4,7 @@ namespace Kanboard\Controller;
use Kanboard\Model\User as UserModel;
use Kanboard\Model\Task as TaskModel;
+use Kanboard\Core\Security\Role;
/**
* Project User overview
@@ -23,7 +24,7 @@ class Projectuser extends Base
*/
private function layout($template, array $params)
{
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$params['content_for_sublayout'] = $this->template->render($template, $params);
$params['filter'] = array('user_id' => $params['user_id']);
@@ -37,17 +38,17 @@ class Projectuser extends Base
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
} else {
- $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
}
return array($user_id, $project_ids, $this->user->getList(true));
}
- private function role($is_owner, $action, $title, $title_user)
+ private function role($role, $action, $title, $title_user)
{
list($user_id, $project_ids, $users) = $this->common();
- $query = $this->projectPermission->getQueryByRole($project_ids, $is_owner)->callback(array($this->project, 'applyColumnStats'));
+ $query = $this->projectPermission->getQueryByRole($project_ids, $role)->callback(array($this->project, 'applyColumnStats'));
if ($user_id !== UserModel::EVERYBODY_ID) {
$query->eq(UserModel::TABLE.'.id', $user_id);
@@ -101,7 +102,7 @@ class Projectuser extends Base
*/
public function managers()
{
- $this->role(1, 'managers', t('People who are project managers'), 'Projects where "%s" is manager');
+ $this->role(Role::PROJECT_MANAGER, 'managers', t('People who are project managers'), 'Projects where "%s" is manager');
}
/**
@@ -110,7 +111,7 @@ class Projectuser extends Base
*/
public function members()
{
- $this->role(0, 'members', t('People who are project members'), 'Projects where "%s" is member');
+ $this->role(ROLE::PROJECT_MEMBER, 'members', t('People who are project members'), 'Projects where "%s" is member');
}
/**
diff --git a/app/Controller/Search.php b/app/Controller/Search.php
index 0aff9073..390210c0 100644
--- a/app/Controller/Search.php
+++ b/app/Controller/Search.php
@@ -12,7 +12,7 @@ class Search extends Base
{
public function index()
{
- $projects = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$search = urldecode($this->request->getStringParam('search'));
$nb_tasks = 0;
diff --git a/app/Controller/Subtask.php b/app/Controller/Subtask.php
index 30ddc375..c93b637d 100644
--- a/app/Controller/Subtask.php
+++ b/app/Controller/Subtask.php
@@ -48,7 +48,7 @@ class Subtask extends Base
$this->response->html($this->taskLayout('subtask/create', array(
'values' => $values,
'errors' => $errors,
- 'users_list' => $this->projectPermission->getMemberList($task['project_id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']),
'task' => $task,
)));
}
@@ -95,7 +95,7 @@ class Subtask extends Base
$this->response->html($this->taskLayout('subtask/edit', array(
'values' => empty($values) ? $subtask : $values,
'errors' => $errors,
- 'users_list' => $this->projectPermission->getMemberList($task['project_id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']),
'status_list' => $this->subtask->getStatusList(),
'subtask' => $subtask,
'task' => $task,
diff --git a/app/Controller/Task.php b/app/Controller/Task.php
index e71b2017..1811dcb7 100644
--- a/app/Controller/Task.php
+++ b/app/Controller/Task.php
@@ -76,7 +76,7 @@ class Task extends Base
'link_label_list' => $this->link->getList(0, false),
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->color->getList(),
- 'users_list' => $this->projectPermission->getMemberList($task['project_id'], true, false, false),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id'], true, false, false),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
'title' => $task['project_name'].' &gt; '.$task['title'],
diff --git a/app/Controller/TaskHelper.php b/app/Controller/TaskHelper.php
new file mode 100644
index 00000000..236af33e
--- /dev/null
+++ b/app/Controller/TaskHelper.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * Task Ajax Helper
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class TaskHelper extends Base
+{
+ /**
+ * Render Markdown text and reply with the HTML Code
+ *
+ * @access public
+ */
+ public function preview()
+ {
+ $payload = $this->request->getJson();
+
+ if (empty($payload['text'])) {
+ $this->response->html('<p>'.t('Nothing to preview...').'</p>');
+ }
+
+ $this->response->html($this->helper->text->markdown($payload['text']));
+ }
+
+ /**
+ * Task autocompletion (Ajax)
+ *
+ * @access public
+ */
+ public function autocomplete()
+ {
+ $search = $this->request->getStringParam('term');
+ $projects = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+
+ if (empty($projects)) {
+ $this->response->json(array());
+ }
+
+ $filter = $this->taskFilterAutoCompleteFormatter
+ ->create()
+ ->filterByProjects($projects)
+ ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
+
+ // Search by task id or by title
+ if (ctype_digit($search)) {
+ $filter->filterById($search);
+ } else {
+ $filter->filterByTitle($search);
+ }
+
+ $this->response->json($filter->format());
+ }
+}
diff --git a/app/Controller/Taskcreation.php b/app/Controller/Taskcreation.php
index cffa9d74..4d74fac6 100644
--- a/app/Controller/Taskcreation.php
+++ b/app/Controller/Taskcreation.php
@@ -36,7 +36,7 @@ class Taskcreation extends Base
'errors' => $errors,
'values' => $values + array('project_id' => $project['id']),
'columns_list' => $this->board->getColumnsList($project['id']),
- 'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'swimlanes_list' => $swimlanes_list,
diff --git a/app/Controller/Taskduplication.php b/app/Controller/Taskduplication.php
index 9cd684eb..ae8bfcbc 100644
--- a/app/Controller/Taskduplication.php
+++ b/app/Controller/Taskduplication.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Model\Project as ProjectModel;
+
/**
* Task Duplication controller
*
@@ -107,7 +109,7 @@ class Taskduplication extends Base
private function chooseDestination(array $task, $template)
{
$values = array();
- $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId());
+ $projects_list = $this->projectUserRole->getProjectsByUser($this->userSession->getId(), array(ProjectModel::ACTIVE));
unset($projects_list[$task['project_id']]);
@@ -117,7 +119,7 @@ class Taskduplication extends Base
$swimlanes_list = $this->swimlane->getList($dst_project_id, false, true);
$columns_list = $this->board->getColumnsList($dst_project_id);
$categories_list = $this->category->getList($dst_project_id);
- $users_list = $this->projectPermission->getMemberList($dst_project_id);
+ $users_list = $this->projectUserRole->getAssignableUsersList($dst_project_id);
$values = $this->taskDuplication->checkDestinationProjectValues($task);
$values['project_id'] = $dst_project_id;
diff --git a/app/Controller/Taskmodification.php b/app/Controller/Taskmodification.php
index 02b09a36..81cf430f 100644
--- a/app/Controller/Taskmodification.php
+++ b/app/Controller/Taskmodification.php
@@ -110,7 +110,7 @@ class Taskmodification extends Base
'values' => $values,
'errors' => $errors,
'task' => $task,
- 'users_list' => $this->projectPermission->getMemberList($task['project_id']),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($task['project_id']),
'date_format' => $this->config->get('application_date_format'),
diff --git a/app/Controller/Twofactor.php b/app/Controller/Twofactor.php
index a7368d6b..aeb13acc 100644
--- a/app/Controller/Twofactor.php
+++ b/app/Controller/Twofactor.php
@@ -2,10 +2,6 @@
namespace Kanboard\Controller;
-use Otp\Otp;
-use Otp\GoogleAuthenticator;
-use Base32\Base32;
-
/**
* Two Factor Auth controller
*
@@ -36,12 +32,15 @@ class Twofactor extends User
$user = $this->getUser();
$this->checkCurrentUser($user);
+ $provider = $this->authenticationManager->getPostAuthenticationProvider();
$label = $user['email'] ?: $user['username'];
+ $provider->setSecret($user['twofactor_secret']);
+
$this->response->html($this->layout('twofactor/index', array(
'user' => $user,
- 'qrcode_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getQrCodeUrl('totp', $label, $user['twofactor_secret']) : '',
- 'key_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getKeyUri('totp', $label, $user['twofactor_secret']) : '',
+ 'qrcode_url' => $user['twofactor_activated'] == 1 ? $provider->getQrCodeUrl($label) : '',
+ 'key_url' => $user['twofactor_activated'] == 1 ? $provider->getKeyUrl($label) : '',
)));
}
@@ -61,7 +60,7 @@ class Twofactor extends User
$this->user->update(array(
'id' => $user['id'],
'twofactor_activated' => 1,
- 'twofactor_secret' => GoogleAuthenticator::generateRandom(),
+ 'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(),
));
} else {
$this->user->update(array(
@@ -72,14 +71,14 @@ class Twofactor extends User
}
// Allow the user to test or disable the feature
- $this->userSession->disable2FA();
+ $this->userSession->disablePostAuthentication();
$this->flash->success(t('User updated successfully.'));
$this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id'])));
}
/**
- * Test 2FA
+ * Test code
*
* @access public
*/
@@ -88,10 +87,13 @@ class Twofactor extends User
$user = $this->getUser();
$this->checkCurrentUser($user);
- $otp = new Otp;
$values = $this->request->getValues();
- if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) {
+ $provider = $this->authenticationManager->getPostAuthenticationProvider();
+ $provider->setCode(empty($values['code']) ? '' : $values['code']);
+ $provider->setSecret($user['twofactor_secret']);
+
+ if ($provider->authenticate()) {
$this->flash->success(t('The two factor authentication code is valid.'));
} else {
$this->flash->failure(t('The two factor authentication code is not valid.'));
@@ -110,11 +112,14 @@ class Twofactor extends User
$user = $this->getUser();
$this->checkCurrentUser($user);
- $otp = new Otp;
$values = $this->request->getValues();
- if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) {
- $this->sessionStorage->postAuth['validated'] = true;
+ $provider = $this->authenticationManager->getPostAuthenticationProvider();
+ $provider->setCode(empty($values['code']) ? '' : $values['code']);
+ $provider->setSecret($user['twofactor_secret']);
+
+ if ($provider->authenticate()) {
+ $this->userSession->validatePostAuthentication();
$this->flash->success(t('The two factor authentication code is valid.'));
$this->response->redirect($this->helper->url->to('app', 'index'));
} else {
diff --git a/app/Controller/User.php b/app/Controller/User.php
index 23e19828..aa548647 100644
--- a/app/Controller/User.php
+++ b/app/Controller/User.php
@@ -3,6 +3,8 @@
namespace Kanboard\Controller;
use Kanboard\Notification\Mail as MailNotification;
+use Kanboard\Model\Project as ProjectModel;
+use Kanboard\Core\Security\Role;
/**
* User controller
@@ -24,7 +26,7 @@ class User extends Base
{
$content = $this->template->render($template, $params);
$params['user_content_for_layout'] = $content;
- $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
+ $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
if (isset($params['user'])) {
$params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')';
@@ -49,7 +51,7 @@ class User extends Base
$this->response->html(
$this->template->layout('user/index', array(
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'title' => t('Users').' ('.$paginator->getTotal().')',
'paginator' => $paginator,
)));
@@ -67,10 +69,11 @@ class User extends Base
$this->response->html($this->template->layout($is_remote ? 'user/create_remote' : 'user/create_local', array(
'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(true),
- 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'roles' => $this->role->getApplicationRoles(),
+ 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()),
'projects' => $this->project->getList(),
'errors' => $errors,
- 'values' => $values,
+ 'values' => $values + array('role' => Role::APP_USER),
'title' => t('New user')
)));
}
@@ -92,7 +95,7 @@ class User extends Base
$user_id = $this->user->create($values);
if ($user_id !== false) {
- $this->projectPermission->addMember($project_id, $user_id);
+ $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MEMBER);
if (! empty($values['notifications_enabled'])) {
$this->userNotificationType->saveSelectedTypes($user_id, array(MailNotification::TYPE));
@@ -170,7 +173,7 @@ class User extends Base
{
$user = $this->getUser();
$this->response->html($this->layout('user/sessions', array(
- 'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']),
+ 'sessions' => $this->rememberMeSession->getAll($user['id']),
'user' => $user,
)));
}
@@ -184,8 +187,8 @@ class User extends Base
{
$this->checkCSRFParam();
$user = $this->getUser();
- $this->authentication->backend('rememberMe')->remove($this->request->getIntegerParam('id'));
- $this->response->redirect($this->helper->url->to('user', 'session', array('user_id' => $user['id'])));
+ $this->rememberMeSession->remove($this->request->getIntegerParam('id'));
+ $this->response->redirect($this->helper->url->to('user', 'sessions', array('user_id' => $user['id'])));
}
/**
@@ -205,7 +208,7 @@ class User extends Base
}
$this->response->html($this->layout('user/notifications', array(
- 'projects' => $this->projectPermission->getMemberProjects($user['id']),
+ 'projects' => $this->projectUserRole->getProjectsByUser($user['id'], array(ProjectModel::ACTIVE)),
'notifications' => $this->userNotification->readSettings($user['id']),
'types' => $this->userNotificationType->getTypes(),
'filters' => $this->userNotificationFilter->getFilters(),
@@ -326,16 +329,9 @@ class User extends Base
if ($this->request->isPost()) {
$values = $this->request->getValues();
- if ($this->userSession->isAdmin()) {
- $values += array('is_admin' => 0, 'is_project_admin' => 0);
- } else {
- // Regular users can't be admin
- if (isset($values['is_admin'])) {
- unset($values['is_admin']);
- }
-
- if (isset($values['is_project_admin'])) {
- unset($values['is_project_admin']);
+ if (! $this->userSession->isAdmin()) {
+ if (isset($values['role'])) {
+ unset($values['role']);
}
}
@@ -358,6 +354,7 @@ class User extends Base
'user' => $user,
'timezones' => $this->config->getTimezones(true),
'languages' => $this->config->getLanguages(true),
+ 'roles' => $this->role->getApplicationRoles(),
)));
}
diff --git a/app/Controller/UserHelper.php b/app/Controller/UserHelper.php
new file mode 100644
index 00000000..f164d0a6
--- /dev/null
+++ b/app/Controller/UserHelper.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Kanboard\Controller;
+
+/**
+ * User Helper
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class UserHelper extends Base
+{
+ /**
+ * User autocompletion (Ajax)
+ *
+ * @access public
+ */
+ public function autocomplete()
+ {
+ $search = $this->request->getStringParam('term');
+ $users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format();
+ $this->response->json($users);
+ }
+}
diff --git a/app/Core/Base.php b/app/Core/Base.php
index d3171024..2d00e52a 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -5,29 +5,43 @@ namespace Kanboard\Core;
use Pimple\Container;
/**
- * Base class
+ * Base Class
*
* @package core
* @author Frederic Guillot
*
- * @property \Kanboard\Core\Session\SessionManager $sessionManager
- * @property \Kanboard\Core\Session\SessionStorage $sessionStorage
- * @property \Kanboard\Core\Session\FlashMessage $flash
- * @property \Kanboard\Core\Helper $helper
- * @property \Kanboard\Core\Mail\Client $emailClient
- * @property \Kanboard\Core\Paginator $paginator
+ * @property \Kanboard\Core\Cache\MemoryCache $memoryCache
+ * @property \Kanboard\Core\Group\GroupManager $groupManager
* @property \Kanboard\Core\Http\Client $httpClient
+ * @property \Kanboard\Core\Http\OAuth2 $oauth
+ * @property \Kanboard\Core\Http\RememberMeCookie $rememberMeCookie
* @property \Kanboard\Core\Http\Request $request
- * @property \Kanboard\Core\Http\Router $router
* @property \Kanboard\Core\Http\Response $response
- * @property \Kanboard\Core\Template $template
- * @property \Kanboard\Core\OAuth2 $oauth
- * @property \Kanboard\Core\Lexer $lexer
+ * @property \Kanboard\Core\Http\Router $router
+ * @property \Kanboard\Core\Mail\Client $emailClient
* @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage
- * @property \Kanboard\Core\Cache\Cache $memoryCache
* @property \Kanboard\Core\Plugin\Hook $hook
* @property \Kanboard\Core\Plugin\Loader $pluginLoader
+ * @property \Kanboard\Core\Security\AccessMap $projectAccessMap
+ * @property \Kanboard\Core\Security\AuthenticationManager $authenticationManager
+ * @property \Kanboard\Core\Security\AccessMap $applicationAccessMap
+ * @property \Kanboard\Core\Security\AccessMap $projectAccessMap
+ * @property \Kanboard\Core\Security\Authorization $applicationAuthorization
+ * @property \Kanboard\Core\Security\Authorization $projectAuthorization
+ * @property \Kanboard\Core\Security\Role $role
* @property \Kanboard\Core\Security\Token $token
+ * @property \Kanboard\Core\Session\FlashMessage $flash
+ * @property \Kanboard\Core\Session\SessionManager $sessionManager
+ * @property \Kanboard\Core\Session\SessionStorage $sessionStorage
+ * @property \Kanboard\Core\User\GroupSync $groupSync
+ * @property \Kanboard\Core\User\UserProfile $userProfile
+ * @property \Kanboard\Core\User\UserSync $userSync
+ * @property \Kanboard\Core\User\UserSession $userSession
+ * @property \Kanboard\Core\DateParser $dateParser
+ * @property \Kanboard\Core\Helper $helper
+ * @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
@@ -36,7 +50,8 @@ use Pimple\Container;
* @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter
* @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter
* @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter
- * @property \Kanboard\Model\Acl $acl
+ * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter
+ * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter
* @property \Kanboard\Model\Action $action
* @property \Kanboard\Model\Authentication $authentication
* @property \Kanboard\Model\Board $board
@@ -46,8 +61,9 @@ use Pimple\Container;
* @property \Kanboard\Model\Config $config
* @property \Kanboard\Model\Currency $currency
* @property \Kanboard\Model\CustomFilter $customFilter
- * @property \Kanboard\Model\DateParser $dateParser
* @property \Kanboard\Model\File $file
+ * @property \Kanboard\Model\Group $group
+ * @property \Kanboard\Model\GroupMember $groupMember
* @property \Kanboard\Model\LastLogin $lastLogin
* @property \Kanboard\Model\Link $link
* @property \Kanboard\Model\Notification $notification
@@ -60,8 +76,11 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
* @property \Kanboard\Model\ProjectMetadata $projectMetadata
* @property \Kanboard\Model\ProjectPermission $projectPermission
+ * @property \Kanboard\Model\ProjectUserRole $projectUserRole
+ * @property \Kanboard\Model\ProjectGroupRole $projectGroupRole
* @property \Kanboard\Model\ProjectNotification $projectNotification
* @property \Kanboard\Model\ProjectNotificationType $projectNotificationType
+ * @property \Kanboard\Model\RememberMeSession $rememberMeSession
* @property \Kanboard\Model\Subtask $subtask
* @property \Kanboard\Model\SubtaskExport $subtaskExport
* @property \Kanboard\Model\SubtaskTimeTracking $subtaskTimeTracking
@@ -84,16 +103,17 @@ use Pimple\Container;
* @property \Kanboard\Model\Transition $transition
* @property \Kanboard\Model\User $user
* @property \Kanboard\Model\UserImport $userImport
+ * @property \Kanboard\Model\UserLocking $userLocking
* @property \Kanboard\Model\UserNotification $userNotification
* @property \Kanboard\Model\UserNotificationType $userNotificationType
* @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter
* @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification
- * @property \Kanboard\Model\UserSession $userSession
* @property \Kanboard\Model\UserMetadata $userMetadata
* @property \Kanboard\Model\Webhook $webhook
* @property \Psr\Log\LoggerInterface $logger
* @property \League\HTMLToMarkdown\HtmlConverter $htmlConverter
* @property \PicoDb\Database $db
+ * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/
abstract class Base
{
diff --git a/app/Core/Cache/MemoryCache.php b/app/Core/Cache/MemoryCache.php
index c4fb7ca4..39e3947b 100644
--- a/app/Core/Cache/MemoryCache.php
+++ b/app/Core/Cache/MemoryCache.php
@@ -23,7 +23,7 @@ class MemoryCache extends Base implements CacheInterface
*
* @access public
* @param string $key
- * @param string $value
+ * @param mixed $value
*/
public function set($key, $value)
{
diff --git a/app/Core/Group/GroupBackendProviderInterface.php b/app/Core/Group/GroupBackendProviderInterface.php
new file mode 100644
index 00000000..74c5cb03
--- /dev/null
+++ b/app/Core/Group/GroupBackendProviderInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Kanboard\Core\Group;
+
+/**
+ * Group Backend Provider Interface
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+interface GroupBackendProviderInterface
+{
+ /**
+ * Find a group from a search query
+ *
+ * @access public
+ * @param string $input
+ * @return GroupProviderInterface[]
+ */
+ public function find($input);
+}
diff --git a/app/Core/Group/GroupManager.php b/app/Core/Group/GroupManager.php
new file mode 100644
index 00000000..e49ffa0f
--- /dev/null
+++ b/app/Core/Group/GroupManager.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Kanboard\Core\Group;
+
+/**
+ * Group Manager
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+class GroupManager
+{
+ /**
+ * List of backend providers
+ *
+ * @access private
+ * @var array
+ */
+ private $providers = array();
+
+ /**
+ * Register a new group backend provider
+ *
+ * @access public
+ * @param GroupBackendProviderInterface $provider
+ * @return GroupManager
+ */
+ public function register(GroupBackendProviderInterface $provider)
+ {
+ $this->providers[] = $provider;
+ return $this;
+ }
+
+ /**
+ * Find a group from a search query
+ *
+ * @access public
+ * @param string $input
+ * @return GroupProviderInterface[]
+ */
+ public function find($input)
+ {
+ $groups = array();
+
+ foreach ($this->providers as $provider) {
+ $groups = array_merge($groups, $provider->find($input));
+ }
+
+ return $this->removeDuplicates($groups);
+ }
+
+ /**
+ * Remove duplicated groups
+ *
+ * @access private
+ * @param array $groups
+ * @return GroupProviderInterface[]
+ */
+ private function removeDuplicates(array $groups)
+ {
+ $result = array();
+
+ foreach ($groups as $group) {
+ if (! isset($result[$group->getName()])) {
+ $result[$group->getName()] = $group;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Core/Group/GroupProviderInterface.php b/app/Core/Group/GroupProviderInterface.php
new file mode 100644
index 00000000..4c7c16ec
--- /dev/null
+++ b/app/Core/Group/GroupProviderInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Kanboard\Core\Group;
+
+/**
+ * Group Provider Interface
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+interface GroupProviderInterface
+{
+ /**
+ * Get internal id
+ *
+ * You must return 0 if the group come from an external backend
+ *
+ * @access public
+ * @return integer
+ */
+ public function getInternalId();
+
+ /**
+ * Get external id
+ *
+ * You must return a unique id if the group come from an external provider
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId();
+
+ /**
+ * Get group name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName();
+}
diff --git a/app/Core/OAuth2.php b/app/Core/Http/OAuth2.php
index a5bbba1a..6fa1fb0a 100644
--- a/app/Core/OAuth2.php
+++ b/app/Core/Http/OAuth2.php
@@ -1,11 +1,13 @@
<?php
-namespace Kanboard\Core;
+namespace Kanboard\Core\Http;
+
+use Kanboard\Core\Base;
/**
- * OAuth2 client
+ * OAuth2 Client
*
- * @package core
+ * @package http
* @author Frederic Guillot
*/
class OAuth2 extends Base
diff --git a/app/Core/Http/RememberMeCookie.php b/app/Core/Http/RememberMeCookie.php
new file mode 100644
index 00000000..a32b35f3
--- /dev/null
+++ b/app/Core/Http/RememberMeCookie.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Kanboard\Core\Http;
+
+use Kanboard\Core\Base;
+
+/**
+ * Remember Me Cookie
+ *
+ * @package http
+ * @author Frederic Guillot
+ */
+class RememberMeCookie extends Base
+{
+ /**
+ * Cookie name
+ *
+ * @var string
+ */
+ const COOKIE_NAME = 'KB_RM';
+
+ /**
+ * Encode the cookie
+ *
+ * @access public
+ * @param string $token Session token
+ * @param string $sequence Sequence token
+ * @return string
+ */
+ public function encode($token, $sequence)
+ {
+ return implode('|', array($token, $sequence));
+ }
+
+ /**
+ * Decode the value of a cookie
+ *
+ * @access public
+ * @param string $value Raw cookie data
+ * @return array
+ */
+ public function decode($value)
+ {
+ list($token, $sequence) = explode('|', $value);
+
+ return array(
+ 'token' => $token,
+ 'sequence' => $sequence,
+ );
+ }
+
+ /**
+ * Return true if the current user has a RememberMe cookie
+ *
+ * @access public
+ * @return bool
+ */
+ public function hasCookie()
+ {
+ return $this->request->getCookie(self::COOKIE_NAME) !== '';
+ }
+
+ /**
+ * Write and encode the cookie
+ *
+ * @access public
+ * @param string $token Session token
+ * @param string $sequence Sequence token
+ * @param string $expiration Cookie expiration
+ * @return boolean
+ */
+ public function write($token, $sequence, $expiration)
+ {
+ return setcookie(
+ self::COOKIE_NAME,
+ $this->encode($token, $sequence),
+ $expiration,
+ $this->helper->url->dir(),
+ null,
+ $this->request->isHTTPS(),
+ true
+ );
+ }
+
+ /**
+ * Read and decode the cookie
+ *
+ * @access public
+ * @return mixed
+ */
+ public function read()
+ {
+ $cookie = $this->request->getCookie(self::COOKIE_NAME);
+
+ if (empty($cookie)) {
+ return false;
+ }
+
+ return $this->decode($cookie);
+ }
+
+ /**
+ * Remove the cookie
+ *
+ * @access public
+ * @return boolean
+ */
+ public function remove()
+ {
+ return setcookie(
+ self::COOKIE_NAME,
+ '',
+ time() - 3600,
+ $this->helper->url->dir(),
+ null,
+ $this->request->isHTTPS(),
+ true
+ );
+ }
+}
diff --git a/app/Core/Http/Request.php b/app/Core/Http/Request.php
index 9f89a6e2..c626f5b2 100644
--- a/app/Core/Http/Request.php
+++ b/app/Core/Http/Request.php
@@ -2,6 +2,7 @@
namespace Kanboard\Core\Http;
+use Pimple\Container;
use Kanboard\Core\Base;
/**
@@ -13,7 +14,35 @@ use Kanboard\Core\Base;
class Request extends Base
{
/**
- * Get URL string parameter
+ * Pointer to PHP environment variables
+ *
+ * @access private
+ * @var array
+ */
+ private $server;
+ private $get;
+ private $post;
+ private $files;
+ private $cookies;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ */
+ public function __construct(Container $container, array $server = array(), array $get = array(), array $post = array(), array $files = array(), array $cookies = array())
+ {
+ parent::__construct($container);
+ $this->server = empty($server) ? $_SERVER : $server;
+ $this->get = empty($get) ? $_GET : $get;
+ $this->post = empty($post) ? $_POST : $post;
+ $this->files = empty($files) ? $_FILES : $files;
+ $this->cookies = empty($cookies) ? $_COOKIE : $cookies;
+ }
+
+ /**
+ * Get query string string parameter
*
* @access public
* @param string $name Parameter name
@@ -22,11 +51,11 @@ class Request extends Base
*/
public function getStringParam($name, $default_value = '')
{
- return isset($_GET[$name]) ? $_GET[$name] : $default_value;
+ return isset($this->get[$name]) ? $this->get[$name] : $default_value;
}
/**
- * Get URL integer parameter
+ * Get query string integer parameter
*
* @access public
* @param string $name Parameter name
@@ -35,7 +64,7 @@ class Request extends Base
*/
public function getIntegerParam($name, $default_value = 0)
{
- return isset($_GET[$name]) && ctype_digit($_GET[$name]) ? (int) $_GET[$name] : $default_value;
+ return isset($this->get[$name]) && ctype_digit($this->get[$name]) ? (int) $this->get[$name] : $default_value;
}
/**
@@ -59,9 +88,9 @@ class Request extends Base
*/
public function getValues()
{
- if (! empty($_POST) && ! empty($_POST['csrf_token']) && $this->token->validateCSRFToken($_POST['csrf_token'])) {
- unset($_POST['csrf_token']);
- return $_POST;
+ if (! empty($this->post) && ! empty($this->post['csrf_token']) && $this->token->validateCSRFToken($this->post['csrf_token'])) {
+ unset($this->post['csrf_token']);
+ return $this->post;
}
return array();
@@ -98,8 +127,8 @@ class Request extends Base
*/
public function getFileContent($name)
{
- if (isset($_FILES[$name])) {
- return file_get_contents($_FILES[$name]['tmp_name']);
+ if (isset($this->files[$name]['tmp_name'])) {
+ return file_get_contents($this->files[$name]['tmp_name']);
}
return '';
@@ -114,7 +143,7 @@ class Request extends Base
*/
public function getFilePath($name)
{
- return isset($_FILES[$name]['tmp_name']) ? $_FILES[$name]['tmp_name'] : '';
+ return isset($this->files[$name]['tmp_name']) ? $this->files[$name]['tmp_name'] : '';
}
/**
@@ -125,7 +154,7 @@ class Request extends Base
*/
public function isPost()
{
- return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST';
+ return isset($this->server['REQUEST_METHOD']) && $this->server['REQUEST_METHOD'] === 'POST';
}
/**
@@ -144,13 +173,24 @@ class Request extends Base
*
* Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS
*
- * @static
* @access public
* @return boolean
*/
- public static function isHTTPS()
+ public function isHTTPS()
+ {
+ return isset($this->server['HTTPS']) && $this->server['HTTPS'] !== '' && $this->server['HTTPS'] !== 'off';
+ }
+
+ /**
+ * Get cookie value
+ *
+ * @access public
+ * @param string $name
+ * @return string
+ */
+ public function getCookie($name)
{
- return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off';
+ return isset($this->cookies[$name]) ? $this->cookies[$name] : '';
}
/**
@@ -163,7 +203,18 @@ class Request extends Base
public function getHeader($name)
{
$name = 'HTTP_'.str_replace('-', '_', strtoupper($name));
- return isset($_SERVER[$name]) ? $_SERVER[$name] : '';
+ return isset($this->server[$name]) ? $this->server[$name] : '';
+ }
+
+ /**
+ * Get remote user
+ *
+ * @access public
+ * @return string
+ */
+ public function getRemoteUser()
+ {
+ return isset($this->server[REVERSE_PROXY_USER_HEADER]) ? $this->server[REVERSE_PROXY_USER_HEADER] : '';
}
/**
@@ -174,41 +225,38 @@ class Request extends Base
*/
public function getQueryString()
{
- return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
+ return isset($this->server['QUERY_STRING']) ? $this->server['QUERY_STRING'] : '';
}
/**
- * Returns uri
+ * Return URI
*
* @access public
* @return string
*/
public function getUri()
{
- return isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
+ return isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
}
/**
* Get the user agent
*
- * @static
* @access public
* @return string
*/
- public static function getUserAgent()
+ public function getUserAgent()
{
- return empty($_SERVER['HTTP_USER_AGENT']) ? t('Unknown') : $_SERVER['HTTP_USER_AGENT'];
+ return empty($this->server['HTTP_USER_AGENT']) ? t('Unknown') : $this->server['HTTP_USER_AGENT'];
}
/**
- * Get the real IP address of the user
+ * Get the IP address of the user
*
- * @static
* @access public
- * @param bool $only_public Return only public IP address
* @return string
*/
- public static function getIpAddress($only_public = false)
+ public function getIpAddress()
{
$keys = array(
'HTTP_CLIENT_IP',
@@ -221,23 +269,24 @@ class Request extends Base
);
foreach ($keys as $key) {
- if (isset($_SERVER[$key])) {
- foreach (explode(',', $_SERVER[$key]) as $ip_address) {
- $ip_address = trim($ip_address);
-
- if ($only_public) {
-
- // Return only public IP address
- if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
- return $ip_address;
- }
- } else {
- return $ip_address;
- }
+ if (! empty($this->server[$key])) {
+ foreach (explode(',', $this->server[$key]) as $ipAddress) {
+ return trim($ipAddress);
}
}
}
return t('Unknown');
}
+
+ /**
+ * Get start time
+ *
+ * @access public
+ * @return float
+ */
+ public function getStartTime()
+ {
+ return isset($this->server['REQUEST_TIME_FLOAT']) ? $this->server['REQUEST_TIME_FLOAT'] : 0;
+ }
}
diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php
index c5a5d3cc..fc214010 100644
--- a/app/Core/Http/Response.php
+++ b/app/Core/Http/Response.php
@@ -257,7 +257,7 @@ class Response extends Base
*/
public function hsts()
{
- if (Request::isHTTPS()) {
+ if ($this->request->isHTTPS()) {
header('Strict-Transport-Security: max-age=31536000');
}
}
diff --git a/app/Core/Ldap/Client.php b/app/Core/Ldap/Client.php
index a523428c..5d481cd3 100644
--- a/app/Core/Ldap/Client.php
+++ b/app/Core/Ldap/Client.php
@@ -2,6 +2,8 @@
namespace Kanboard\Core\Ldap;
+use LogicException;
+
/**
* LDAP Client
*
@@ -11,16 +13,60 @@ namespace Kanboard\Core\Ldap;
class Client
{
/**
+ * LDAP resource
+ *
+ * @access private
+ * @var resource
+ */
+ private $ldap;
+
+ /**
+ * Establish LDAP connection
+ *
+ * @static
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @return Client
+ */
+ public static function connect($username = null, $password = null)
+ {
+ $client = new self;
+ $client->open($client->getLdapServer());
+ $username = $username ?: $client->getLdapUsername();
+ $password = $password ?: $client->getLdapPassword();
+
+ if (empty($username) && empty($password)) {
+ $client->useAnonymousAuthentication();
+ } else {
+ $client->authenticate($username, $password);
+ }
+
+ return $client;
+ }
+
+ /**
* Get server connection
*
* @access public
+ * @return resource
+ */
+ public function getConnection()
+ {
+ return $this->ldap;
+ }
+
+ /**
+ * Establish server connection
+ *
+ * @access public
* @param string $server LDAP server hostname or IP
* @param integer $port LDAP port
* @param boolean $tls Start TLS
* @param boolean $verify Skip SSL certificate verification
- * @return resource
+ * @return Client
*/
- public function getConnection($server, $port = LDAP_PORT, $tls = LDAP_START_TLS, $verify = LDAP_SSL_VERIFY)
+ public function open($server, $port = LDAP_PORT, $tls = LDAP_START_TLS, $verify = LDAP_SSL_VERIFY)
{
if (! function_exists('ldap_connect')) {
throw new ClientException('LDAP: The PHP LDAP extension is required');
@@ -30,34 +76,33 @@ class Client
putenv('LDAPTLS_REQCERT=never');
}
- $ldap = ldap_connect($server, $port);
+ $this->ldap = ldap_connect($server, $port);
- if ($ldap === false) {
+ if ($this->ldap === false) {
throw new ClientException('LDAP: Unable to connect to the LDAP server');
}
- ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
- ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
- ldap_set_option($ldap, LDAP_OPT_NETWORK_TIMEOUT, 1);
- ldap_set_option($ldap, LDAP_OPT_TIMELIMIT, 1);
+ ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
+ ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, 0);
+ ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, 1);
+ ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, 1);
- if ($tls && ! @ldap_start_tls($ldap)) {
+ if ($tls && ! @ldap_start_tls($this->ldap)) {
throw new ClientException('LDAP: Unable to start TLS');
}
- return $ldap;
+ return $this;
}
/**
* Anonymous authentication
*
* @access public
- * @param resource $ldap
* @return boolean
*/
- public function useAnonymousAuthentication($ldap)
+ public function useAnonymousAuthentication()
{
- if (! ldap_bind($ldap)) {
+ if (! @ldap_bind($this->ldap)) {
throw new ClientException('Unable to perform anonymous binding');
}
@@ -68,17 +113,53 @@ class Client
* Authentication with username/password
*
* @access public
- * @param resource $ldap
- * @param string $username
- * @param string $password
+ * @param string $bind_rdn
+ * @param string $bind_password
* @return boolean
*/
- public function authenticate($ldap, $username, $password)
+ public function authenticate($bind_rdn, $bind_password)
{
- if (! ldap_bind($ldap, $username, $password)) {
- throw new ClientException('Unable to perform anonymous binding');
+ if (! @ldap_bind($this->ldap, $bind_rdn, $bind_password)) {
+ throw new ClientException('LDAP authentication failure for "'.$bind_rdn.'"');
}
return true;
}
+
+ /**
+ * Get LDAP server name
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapServer()
+ {
+ if (! LDAP_SERVER) {
+ throw new LogicException('LDAP server not configured, check the parameter LDAP_SERVER');
+ }
+
+ return LDAP_SERVER;
+ }
+
+ /**
+ * Get LDAP username (proxy auth)
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapUsername()
+ {
+ return LDAP_USERNAME;
+ }
+
+ /**
+ * Get LDAP password (proxy auth)
+ *
+ * @access public
+ * @return string
+ */
+ public function getLdapPassword()
+ {
+ return LDAP_PASSWORD;
+ }
}
diff --git a/app/Core/Ldap/Entries.php b/app/Core/Ldap/Entries.php
new file mode 100644
index 00000000..b0f78fa4
--- /dev/null
+++ b/app/Core/Ldap/Entries.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+/**
+ * LDAP Entries
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class Entries
+{
+ /**
+ * LDAP entries
+ *
+ * @access private
+ * @var array
+ */
+ private $entries = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $entries
+ */
+ public function __construct(array $entries)
+ {
+ $this->entries = $entries;
+ }
+
+ /**
+ * Get all entries
+ *
+ * @access public
+ * @return []Entry
+ */
+ public function getAll()
+ {
+ $entities = array();
+
+ if (! isset($this->entries['count'])) {
+ return $entities;
+ }
+
+ for ($i = 0; $i < $this->entries['count']; $i++) {
+ $entities[] = new Entry($this->entries[$i]);
+ }
+
+ return $entities;
+ }
+
+ /**
+ * Get first entry
+ *
+ * @access public
+ * @return Entry
+ */
+ public function getFirstEntry()
+ {
+ return new Entry(isset($this->entries[0]) ? $this->entries[0] : array());
+ }
+}
diff --git a/app/Core/Ldap/Entry.php b/app/Core/Ldap/Entry.php
new file mode 100644
index 00000000..e67dd625
--- /dev/null
+++ b/app/Core/Ldap/Entry.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+/**
+ * LDAP Entry
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class Entry
+{
+ /**
+ * LDAP entry
+ *
+ * @access private
+ * @var array
+ */
+ private $entry = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $entry
+ */
+ public function __construct(array $entry)
+ {
+ $this->entry = $entry;
+ }
+
+ /**
+ * Get all attribute values
+ *
+ * @access public
+ * @param string $attribute
+ * @return string[]
+ */
+ public function getAll($attribute)
+ {
+ $attributes = array();
+
+ if (! isset($this->entry[$attribute]['count'])) {
+ return $attributes;
+ }
+
+ for ($i = 0; $i < $this->entry[$attribute]['count']; $i++) {
+ $attributes[] = $this->entry[$attribute][$i];
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Get first attribute value
+ *
+ * @access public
+ * @param string $attribute
+ * @param string $default
+ * @return string
+ */
+ public function getFirstValue($attribute, $default = '')
+ {
+ return isset($this->entry[$attribute][0]) ? $this->entry[$attribute][0] : $default;
+ }
+
+ /**
+ * Get entry distinguished name
+ *
+ * @access public
+ * @return string
+ */
+ public function getDn()
+ {
+ return isset($this->entry['dn']) ? $this->entry['dn'] : '';
+ }
+
+ /**
+ * Return true if the given value exists in attribute list
+ *
+ * @access public
+ * @param string $attribute
+ * @param string $value
+ * @return boolean
+ */
+ public function hasValue($attribute, $value)
+ {
+ $attributes = $this->getAll($attribute);
+ return in_array($value, $attributes);
+ }
+}
diff --git a/app/Core/Ldap/Group.php b/app/Core/Ldap/Group.php
new file mode 100644
index 00000000..e11e8ecd
--- /dev/null
+++ b/app/Core/Ldap/Group.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Kanboard\Core\Ldap;
+
+use LogicException;
+use Kanboard\Group\LdapGroupProvider;
+
+/**
+ * LDAP Group Finder
+ *
+ * @package ldap
+ * @author Frederic Guillot
+ */
+class Group
+{
+ /**
+ * Query
+ *
+ * @access private
+ * @var Query
+ */
+ private $query;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param Query $query
+ */
+ public function __construct(Query $query)
+ {
+ $this->query = $query;
+ }
+
+ /**
+ * Get groups
+ *
+ * @static
+ * @access public
+ * @param Client $client
+ * @param string $query
+ * @return array
+ */
+ public static function getGroups(Client $client, $query)
+ {
+ $self = new self(new Query($client));
+ return $self->find($query);
+ }
+
+ /**
+ * Find groups
+ *
+ * @access public
+ * @param string $query
+ * @return array
+ */
+ public function find($query)
+ {
+ $this->query->execute($this->getBasDn(), $query, $this->getAttributes());
+ $groups = array();
+
+ if ($this->query->hasResult()) {
+ $groups = $this->build();
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Build groups list
+ *
+ * @access protected
+ * @return array
+ */
+ protected function build()
+ {
+ $groups = array();
+
+ foreach ($this->query->getEntries()->getAll() as $entry) {
+ $groups[] = new LdapGroupProvider($entry->getDn(), $entry->getFirstValue($this->getAttributeName()));
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Ge the list of attributes to fetch when reading the LDAP group entry
+ *
+ * Must returns array with index that start at 0 otherwise ldap_search returns a warning "Array initialization wrong"
+ *
+ * @access public
+ * @return array
+ */
+ public function getAttributes()
+ {
+ return array_values(array_filter(array(
+ $this->getAttributeName(),
+ )));
+ }
+
+ /**
+ * Get LDAP group name attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeName()
+ {
+ if (! LDAP_GROUP_ATTRIBUTE_NAME) {
+ throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_GROUP_ATTRIBUTE_NAME');
+ }
+
+ return LDAP_GROUP_ATTRIBUTE_NAME;
+ }
+
+ /**
+ * Get LDAP group base DN
+ *
+ * @access public
+ * @return string
+ */
+ public function getBasDn()
+ {
+ if (! LDAP_GROUP_BASE_DN) {
+ throw new LogicException('LDAP group base DN empty, check the parameter LDAP_GROUP_BASE_DN');
+ }
+
+ return LDAP_GROUP_BASE_DN;
+ }
+}
diff --git a/app/Core/Ldap/Query.php b/app/Core/Ldap/Query.php
index 1c34fa10..6ca4bc96 100644
--- a/app/Core/Ldap/Query.php
+++ b/app/Core/Ldap/Query.php
@@ -11,6 +11,14 @@ namespace Kanboard\Core\Ldap;
class Query
{
/**
+ * LDAP client
+ *
+ * @access private
+ * @var Client
+ */
+ private $client = null;
+
+ /**
* Query result
*
* @access private
@@ -22,31 +30,30 @@ class Query
* Constructor
*
* @access public
- * @param array $entries
+ * @param Client $client
*/
- public function __construct(array $entries = array())
+ public function __construct(Client $client)
{
- $this->entries = $entries;
+ $this->client = $client;
}
/**
* Execute query
*
* @access public
- * @param resource $ldap
* @param string $baseDn
* @param string $filter
* @param array $attributes
* @return Query
*/
- public function execute($ldap, $baseDn, $filter, array $attributes)
+ public function execute($baseDn, $filter, array $attributes)
{
- $sr = ldap_search($ldap, $baseDn, $filter, $attributes);
+ $sr = ldap_search($this->client->getConnection(), $baseDn, $filter, $attributes);
if ($sr === false) {
return $this;
}
- $entries = ldap_get_entries($ldap, $sr);
+ $entries = ldap_get_entries($this->client->getConnection(), $sr);
if ($entries === false || count($entries) === 0 || $entries['count'] == 0) {
return $this;
}
@@ -68,28 +75,13 @@ class Query
}
/**
- * Return subset of entries
- *
- * @access public
- * @param string $key
- * @param mixed $default
- * @return array
- */
- public function getAttribute($key, $default = null)
- {
- return isset($this->entries[0][$key]) ? $this->entries[0][$key] : $default;
- }
-
- /**
- * Return one entry from a list of entries
+ * Get LDAP Entries
*
* @access public
- * @param string $key Key
- * @param string $default Default value if key not set in entry
- * @return string
+ * @return Entities
*/
- public function getAttributeValue($key, $default = '')
+ public function getEntries()
{
- return isset($this->entries[0][$key][0]) ? $this->entries[0][$key][0] : $default;
+ return new Entries($this->entries);
}
}
diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php
index e44a4dda..ab8d7296 100644
--- a/app/Core/Ldap/User.php
+++ b/app/Core/Ldap/User.php
@@ -2,8 +2,12 @@
namespace Kanboard\Core\Ldap;
+use LogicException;
+use Kanboard\Core\Security\Role;
+use Kanboard\User\LdapUserProvider;
+
/**
- * LDAP User
+ * LDAP User Finder
*
* @package ldap
* @author Frederic Guillot
@@ -24,72 +28,70 @@ class User
* @access public
* @param Query $query
*/
- public function __construct(Query $query = null)
+ public function __construct(Query $query)
{
- $this->query = $query ?: new Query;
+ $this->query = $query;
}
/**
- * Get user profile
+ * Get user profile (helper)
*
+ * @static
* @access public
- * @param resource $ldap
- * @param string $baseDn
+ * @param Client $client
* @param string $query
* @return array
*/
- public function getProfile($ldap, $baseDn, $query)
+ public static function getUser(Client $client, $query)
{
- $this->query->execute($ldap, $baseDn, $query, $this->getAttributes());
- $profile = array();
-
- if ($this->query->hasResult()) {
- $profile = $this->prepareProfile();
- }
-
- return $profile;
+ $self = new self(new Query($client));
+ return $self->find($query);
}
/**
- * Build user profile
+ * Find user
*
- * @access private
- * @return boolean|array
+ * @access public
+ * @param string $query
+ * @return null|LdapUserProvider
*/
- private function prepareProfile()
+ public function find($query)
{
- return array(
- 'ldap_id' => $this->query->getAttribute('dn', ''),
- 'username' => $this->query->getAttributeValue($this->getAttributeUsername()),
- 'name' => $this->query->getAttributeValue($this->getAttributeName()),
- 'email' => $this->query->getAttributeValue($this->getAttributeEmail()),
- 'is_admin' => (int) $this->isMemberOf($this->query->getAttribute($this->getAttributeGroup(), array()), $this->getGroupAdminDn()),
- 'is_project_admin' => (int) $this->isMemberOf($this->query->getAttribute($this->getAttributeGroup(), array()), $this->getGroupProjectAdminDn()),
- 'is_ldap_user' => 1,
- );
+ $this->query->execute($this->getBasDn(), $query, $this->getAttributes());
+ $user = null;
+
+ if ($this->query->hasResult()) {
+ $user = $this->build();
+ }
+
+ return $user;
}
/**
- * Check group membership
+ * Build user profile
*
- * @access public
- * @param array $group_entries
- * @param string $group_dn
- * @return boolean
+ * @access protected
+ * @return LdapUserProvider
*/
- public function isMemberOf(array $group_entries, $group_dn)
+ protected function build()
{
- if (! isset($group_entries['count']) || empty($group_dn)) {
- return false;
- }
+ $entry = $this->query->getEntries()->getFirstEntry();
+ $role = Role::APP_USER;
- for ($i = 0; $i < $group_entries['count']; $i++) {
- if ($group_entries[$i] === $group_dn) {
- return true;
- }
+ if ($entry->hasValue($this->getAttributeGroup(), $this->getGroupAdminDn())) {
+ $role = Role::APP_ADMIN;
+ } elseif ($entry->hasValue($this->getAttributeGroup(), $this->getGroupManagerDn())) {
+ $role = Role::APP_MANAGER;
}
- return false;
+ return new LdapUserProvider(
+ $entry->getDn(),
+ $entry->getFirstValue($this->getAttributeUsername()),
+ $entry->getFirstValue($this->getAttributeName()),
+ $entry->getFirstValue($this->getAttributeEmail()),
+ $role,
+ $entry->getAll($this->getAttributeGroup())
+ );
}
/**
@@ -118,29 +120,41 @@ class User
*/
public function getAttributeUsername()
{
- return LDAP_ACCOUNT_ID;
+ if (! LDAP_USER_ATTRIBUTE_USERNAME) {
+ throw new LogicException('LDAP username attribute empty, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
+ }
+
+ return LDAP_USER_ATTRIBUTE_USERNAME;
}
/**
- * Get LDAP account email attribute
+ * Get LDAP user name attribute
*
* @access public
* @return string
*/
- public function getAttributeEmail()
+ public function getAttributeName()
{
- return LDAP_ACCOUNT_EMAIL;
+ if (! LDAP_USER_ATTRIBUTE_FULLNAME) {
+ throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_USER_ATTRIBUTE_FULLNAME');
+ }
+
+ return LDAP_USER_ATTRIBUTE_FULLNAME;
}
/**
- * Get LDAP account name attribute
+ * Get LDAP account email attribute
*
* @access public
* @return string
*/
- public function getAttributeName()
+ public function getAttributeEmail()
{
- return LDAP_ACCOUNT_FULLNAME;
+ if (! LDAP_USER_ATTRIBUTE_EMAIL) {
+ throw new LogicException('LDAP email attribute empty, check the parameter LDAP_USER_ATTRIBUTE_EMAIL');
+ }
+
+ return LDAP_USER_ATTRIBUTE_EMAIL;
}
/**
@@ -151,7 +165,7 @@ class User
*/
public function getAttributeGroup()
{
- return LDAP_ACCOUNT_MEMBEROF;
+ return LDAP_USER_ATTRIBUTE_GROUPS;
}
/**
@@ -166,13 +180,28 @@ class User
}
/**
- * Get LDAP project admin group DN
+ * Get LDAP application manager group DN
*
* @access public
* @return string
*/
- public function getGroupProjectAdminDn()
+ public function getGroupManagerDn()
{
- return LDAP_GROUP_PROJECT_ADMIN_DN;
+ return LDAP_GROUP_MANAGER_DN;
+ }
+
+ /**
+ * Get LDAP user base DN
+ *
+ * @access public
+ * @return string
+ */
+ public function getBasDn()
+ {
+ if (! LDAP_USER_BASE_DN) {
+ throw new LogicException('LDAP user base DN empty, check the parameter LDAP_USER_BASE_DN');
+ }
+
+ return LDAP_USER_BASE_DN;
}
}
diff --git a/app/Core/Security/AccessMap.php b/app/Core/Security/AccessMap.php
index 10a29e1f..02a4ca45 100644
--- a/app/Core/Security/AccessMap.php
+++ b/app/Core/Security/AccessMap.php
@@ -19,6 +19,14 @@ class AccessMap
private $defaultRole = '';
/**
+ * Role hierarchy
+ *
+ * @access private
+ * @var array
+ */
+ private $hierarchy = array();
+
+ /**
* Access map
*
* @access private
@@ -40,15 +48,76 @@ class AccessMap
}
/**
+ * Define role hierarchy
+ *
+ * @access public
+ * @param string $role
+ * @param array $subroles
+ * @return Acl
+ */
+ public function setRoleHierarchy($role, array $subroles)
+ {
+ foreach ($subroles as $subrole) {
+ if (isset($this->hierarchy[$subrole])) {
+ $this->hierarchy[$subrole][] = $role;
+ } else {
+ $this->hierarchy[$subrole] = array($role);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get computed role hierarchy
+ *
+ * @access public
+ * @param string $role
+ * @return array
+ */
+ public function getRoleHierarchy($role)
+ {
+ $roles = array($role);
+
+ if (isset($this->hierarchy[$role])) {
+ $roles = array_merge($roles, $this->hierarchy[$role]);
+ }
+
+ return $roles;
+ }
+
+ /**
* Add new access rules
*
* @access public
+ * @param string $controller Controller class name
+ * @param mixed $methods List of method name or just one method
+ * @param string $role Lowest role required
+ * @return Acl
+ */
+ public function add($controller, $methods, $role)
+ {
+ if (is_array($methods)) {
+ foreach ($methods as $method) {
+ $this->addRule($controller, $method, $role);
+ }
+ } else {
+ $this->addRule($controller, $methods, $role);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add new access rule
+ *
+ * @access private
* @param string $controller
* @param string $method
- * @param array $roles
+ * @param string $role
* @return Acl
*/
- public function add($controller, $method, array $roles)
+ private function addRule($controller, $method, $role)
{
$controller = strtolower($controller);
$method = strtolower($method);
@@ -57,11 +126,7 @@ class AccessMap
$this->map[$controller] = array();
}
- if (! isset($this->map[$controller][$method])) {
- $this->map[$controller][$method] = array();
- }
-
- $this->map[$controller][$method] = $roles;
+ $this->map[$controller][$method] = $role;
return $this;
}
@@ -79,14 +144,12 @@ class AccessMap
$controller = strtolower($controller);
$method = strtolower($method);
- if (isset($this->map[$controller][$method])) {
- return $this->map[$controller][$method];
- }
-
- if (isset($this->map[$controller]['*'])) {
- return $this->map[$controller]['*'];
+ foreach (array($method, '*') as $key) {
+ if (isset($this->map[$controller][$key])) {
+ return $this->getRoleHierarchy($this->map[$controller][$key]);
+ }
}
- return array($this->defaultRole);
+ return $this->getRoleHierarchy($this->defaultRole);
}
}
diff --git a/app/Core/Security/AuthenticationManager.php b/app/Core/Security/AuthenticationManager.php
new file mode 100644
index 00000000..cced58c0
--- /dev/null
+++ b/app/Core/Security/AuthenticationManager.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+use LogicException;
+use Kanboard\Core\Base;
+use Kanboard\Core\User\UserProviderInterface;
+use Kanboard\Event\AuthFailureEvent;
+use Kanboard\Event\AuthSuccessEvent;
+
+/**
+ * Authentication Manager
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+class AuthenticationManager extends Base
+{
+ /**
+ * Event names
+ *
+ * @var string
+ */
+ const EVENT_SUCCESS = 'auth.success';
+ const EVENT_FAILURE = 'auth.failure';
+
+ /**
+ * List of authentication providers
+ *
+ * @access private
+ * @var array
+ */
+ private $providers = array();
+
+ /**
+ * Register a new authentication provider
+ *
+ * @access public
+ * @param AuthenticationProviderInterface $provider
+ * @return AuthenticationManager
+ */
+ public function register(AuthenticationProviderInterface $provider)
+ {
+ $this->providers[$provider->getName()] = $provider;
+ return $this;
+ }
+
+ /**
+ * Register a new authentication provider
+ *
+ * @access public
+ * @param string $name
+ * @return AuthenticationProviderInterface|OAuthAuthenticationProviderInterface|PasswordAuthenticationProviderInterface|PreAuthenticationProviderInterface|OAuthAuthenticationProviderInterface
+ */
+ public function getProvider($name)
+ {
+ if (! isset($this->providers[$name])) {
+ throw new LogicException('Authentication provider not found: '.$name);
+ }
+
+ return $this->providers[$name];
+ }
+
+ /**
+ * Execute providers that are able to validate the current session
+ *
+ * @access public
+ * @return boolean
+ */
+ public function checkCurrentSession()
+ {
+ if ($this->userSession->isLogged() ) {
+ foreach ($this->filterProviders('SessionCheckProviderInterface') as $provider) {
+ if (! $provider->isValidSession()) {
+ unset($this->sessionStorage->user);
+ $this->preAuthentication();
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Execute pre-authentication providers
+ *
+ * @access public
+ * @return boolean
+ */
+ public function preAuthentication()
+ {
+ foreach ($this->filterProviders('PreAuthenticationProviderInterface') as $provider) {
+ if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
+ $this->dispatcher->dispatch(self::EVENT_SUCCESS, new AuthSuccessEvent($provider->getName()));
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Execute username/password authentication providers
+ *
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @param boolean $fireEvent
+ * @return boolean
+ */
+ public function passwordAuthentication($username, $password, $fireEvent = true)
+ {
+ foreach ($this->filterProviders('PasswordAuthenticationProviderInterface') as $provider) {
+ $provider->setUsername($username);
+ $provider->setPassword($password);
+
+ if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
+ if ($fireEvent) {
+ $this->dispatcher->dispatch(self::EVENT_SUCCESS, new AuthSuccessEvent($provider->getName()));
+ }
+
+ return true;
+ }
+ }
+
+ if ($fireEvent) {
+ $this->dispatcher->dispatch(self::EVENT_FAILURE, new AuthFailureEvent($username));
+ }
+
+ return false;
+ }
+
+ /**
+ * Perform OAuth2 authentication
+ *
+ * @access public
+ * @param string $name
+ * @return boolean
+ */
+ public function oauthAuthentication($name)
+ {
+ $provider = $this->getProvider($name);
+
+ if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
+ $this->dispatcher->dispatch(self::EVENT_SUCCESS, new AuthSuccessEvent($provider->getName()));
+ return true;
+ }
+
+ $this->dispatcher->dispatch(self::EVENT_FAILURE, new AuthFailureEvent);
+
+ return false;
+ }
+
+ /**
+ * Get the last Post-Authentication provider
+ *
+ * @access public
+ * @return PostAuthenticationProviderInterface
+ */
+ public function getPostAuthenticationProvider()
+ {
+ $providers = $this->filterProviders('PostAuthenticationProviderInterface');
+
+ if (empty($providers)) {
+ throw new LogicException('You must have at least one Post-Authentication Provider configured');
+ }
+
+ return array_pop($providers);
+ }
+
+ /**
+ * Filter registered providers by interface type
+ *
+ * @access private
+ * @param string $interface
+ * @return array
+ */
+ private function filterProviders($interface)
+ {
+ $interface = '\Kanboard\Core\Security\\'.$interface;
+
+ return array_filter($this->providers, function(AuthenticationProviderInterface $provider) use ($interface) {
+ return is_a($provider, $interface);
+ });
+ }
+}
diff --git a/app/Core/Security/AuthenticationProviderInterface.php b/app/Core/Security/AuthenticationProviderInterface.php
new file mode 100644
index 00000000..828e272c
--- /dev/null
+++ b/app/Core/Security/AuthenticationProviderInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Authentication Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface AuthenticationProviderInterface
+{
+ /**
+ * Get authentication provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Authenticate the user
+ *
+ * @access public
+ * @return boolean
+ */
+ public function authenticate();
+}
diff --git a/app/Core/Security/Authorization.php b/app/Core/Security/Authorization.php
index a04b3720..980db048 100644
--- a/app/Core/Security/Authorization.php
+++ b/app/Core/Security/Authorization.php
@@ -16,17 +16,17 @@ class Authorization
* @access private
* @var AccessMap
*/
- private $acl;
+ private $accessMap;
/**
* Constructor
*
* @access public
- * @param AccessMap $acl
+ * @param AccessMap $accessMap
*/
- public function __construct(AccessMap $acl)
+ public function __construct(AccessMap $accessMap)
{
- $this->acl = $acl;
+ $this->accessMap = $accessMap;
}
/**
@@ -40,7 +40,7 @@ class Authorization
*/
public function isAllowed($controller, $method, $role)
{
- $roles = $this->acl->getRoles($controller, $method);
+ $roles = $this->accessMap->getRoles($controller, $method);
return in_array($role, $roles);
}
}
diff --git a/app/Core/Security/OAuthAuthenticationProviderInterface.php b/app/Core/Security/OAuthAuthenticationProviderInterface.php
new file mode 100644
index 00000000..c32339e0
--- /dev/null
+++ b/app/Core/Security/OAuthAuthenticationProviderInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * OAuth2 Authentication Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface OAuthAuthenticationProviderInterface extends AuthenticationProviderInterface
+{
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return UserProviderInterface
+ */
+ public function getUser();
+
+ /**
+ * Unlink user
+ *
+ * @access public
+ * @param integer $userId
+ * @return bool
+ */
+ public function unlink($userId);
+
+ /**
+ * Get configured OAuth2 service
+ *
+ * @access public
+ * @return Kanboard\Core\Http\OAuth2
+ */
+ public function getService();
+
+ /**
+ * Set OAuth2 code
+ *
+ * @access public
+ * @param string $code
+ * @return OAuthAuthenticationProviderInterface
+ */
+ public function setCode($code);
+}
diff --git a/app/Core/Security/PasswordAuthenticationProviderInterface.php b/app/Core/Security/PasswordAuthenticationProviderInterface.php
new file mode 100644
index 00000000..918a4aec
--- /dev/null
+++ b/app/Core/Security/PasswordAuthenticationProviderInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Password Authentication Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface PasswordAuthenticationProviderInterface extends AuthenticationProviderInterface
+{
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return UserProviderInterface
+ */
+ public function getUser();
+
+ /**
+ * Set username
+ *
+ * @access public
+ * @param string $username
+ */
+ public function setUsername($username);
+
+ /**
+ * Set password
+ *
+ * @access public
+ * @param string $password
+ */
+ public function setPassword($password);
+}
diff --git a/app/Core/Security/PostAuthenticationProviderInterface.php b/app/Core/Security/PostAuthenticationProviderInterface.php
new file mode 100644
index 00000000..88fc2fe5
--- /dev/null
+++ b/app/Core/Security/PostAuthenticationProviderInterface.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Post Authentication Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface PostAuthenticationProviderInterface extends AuthenticationProviderInterface
+{
+ /**
+ * Set user pin-code
+ *
+ * @access public
+ * @param string $code
+ */
+ public function setCode($code);
+
+ /**
+ * Set secret token (fetched from user profile)
+ *
+ * @access public
+ * @param string $secret
+ */
+ public function setSecret($secret);
+
+ /**
+ * Get secret token (will be saved in user profile)
+ *
+ * @access public
+ * @return string
+ */
+ public function getSecret();
+
+ /**
+ * Get QR code url (empty if no QR can be provided)
+ *
+ * @access public
+ * @param string $label
+ * @return string
+ */
+ public function getQrCodeUrl($label);
+
+ /**
+ * Get key url (empty if no url can be provided)
+ *
+ * @access public
+ * @param string $label
+ * @return string
+ */
+ public function getKeyUrl($label);
+}
diff --git a/app/Core/Security/PreAuthenticationProviderInterface.php b/app/Core/Security/PreAuthenticationProviderInterface.php
new file mode 100644
index 00000000..391e8d0f
--- /dev/null
+++ b/app/Core/Security/PreAuthenticationProviderInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Pre-Authentication Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface PreAuthenticationProviderInterface extends AuthenticationProviderInterface
+{
+ /**
+ * Get user object
+ *
+ * @access public
+ * @return UserProviderInterface
+ */
+ public function getUser();
+}
diff --git a/app/Core/Security/Role.php b/app/Core/Security/Role.php
index 079ce14b..85d85743 100644
--- a/app/Core/Security/Role.php
+++ b/app/Core/Security/Role.php
@@ -18,4 +18,47 @@ class Role
const PROJECT_MANAGER = 'project-manager';
const PROJECT_MEMBER = 'project-member';
const PROJECT_VIEWER = 'project-viewer';
+
+ /**
+ * Get application roles
+ *
+ * @access public
+ * @return array
+ */
+ public function getApplicationRoles()
+ {
+ return array(
+ self::APP_ADMIN => t('Administrator'),
+ self::APP_MANAGER => t('Manager'),
+ self::APP_USER => t('User'),
+ );
+ }
+
+ /**
+ * Get project roles
+ *
+ * @access public
+ * @return array
+ */
+ public function getProjectRoles()
+ {
+ return array(
+ self::PROJECT_MANAGER => t('Project Manager'),
+ self::PROJECT_MEMBER => t('Project Member'),
+ self::PROJECT_VIEWER => t('Project Viewer'),
+ );
+ }
+
+ /**
+ * Get application roles
+ *
+ * @access public
+ * @param string $role
+ * @return string
+ */
+ public function getRoleName($role)
+ {
+ $roles = $this->getApplicationRoles() + $this->getProjectRoles();
+ return isset($roles[$role]) ? $roles[$role] : t('Unknown');
+ }
}
diff --git a/app/Core/Security/SessionCheckProviderInterface.php b/app/Core/Security/SessionCheckProviderInterface.php
new file mode 100644
index 00000000..232fe1db
--- /dev/null
+++ b/app/Core/Security/SessionCheckProviderInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Kanboard\Core\Security;
+
+/**
+ * Session Check Provider Interface
+ *
+ * @package security
+ * @author Frederic Guillot
+ */
+interface SessionCheckProviderInterface
+{
+ /**
+ * Check if the user session is valid
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isValidSession();
+}
diff --git a/app/Core/Session/SessionManager.php b/app/Core/Session/SessionManager.php
index 6153efeb..750711b0 100644
--- a/app/Core/Session/SessionManager.php
+++ b/app/Core/Session/SessionManager.php
@@ -14,6 +14,13 @@ use Kanboard\Core\Http\Request;
class SessionManager extends Base
{
/**
+ * Event names
+ *
+ * @var string
+ */
+ const EVENT_DESTROY = 'session.destroy';
+
+ /**
* Return true if the session is open
*
* @static
@@ -41,7 +48,7 @@ class SessionManager extends Base
session_name('KB_SID');
session_start();
- $this->container['sessionStorage']->setStorage($_SESSION);
+ $this->sessionStorage->setStorage($_SESSION);
}
/**
@@ -51,6 +58,8 @@ class SessionManager extends Base
*/
public function close()
{
+ $this->dispatcher->dispatch(self::EVENT_DESTROY);
+
// Destroy the session cookie
$params = session_get_cookie_params();
@@ -80,7 +89,7 @@ class SessionManager extends Base
SESSION_DURATION,
$this->helper->url->dir() ?: '/',
null,
- Request::isHTTPS(),
+ $this->request->isHTTPS(),
true
);
diff --git a/app/Core/Session/SessionStorage.php b/app/Core/Session/SessionStorage.php
index 703d2fbb..11230793 100644
--- a/app/Core/Session/SessionStorage.php
+++ b/app/Core/Session/SessionStorage.php
@@ -12,12 +12,13 @@ namespace Kanboard\Core\Session;
* @property array $user
* @property array $flash
* @property array $csrf
- * @property array $postAuth
+ * @property array $postAuthenticationValidated
* @property array $filters
* @property string $redirectAfterLogin
* @property string $captcha
* @property string $commentSorting
* @property bool $hasSubtaskInProgress
+ * @property bool $hasRememberMe
* @property bool $boardCollapsed
*/
class SessionStorage
diff --git a/app/Core/User/GroupSync.php b/app/Core/User/GroupSync.php
new file mode 100644
index 00000000..573acd47
--- /dev/null
+++ b/app/Core/User/GroupSync.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+use Kanboard\Core\Base;
+
+/**
+ * Group Synchronization
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class GroupSync extends Base
+{
+ /**
+ * Synchronize group membership
+ *
+ * @access public
+ * @param integer $userId
+ * @param array $groupIds
+ */
+ public function synchronize($userId, array $groupIds)
+ {
+ foreach ($groupIds as $groupId) {
+ $group = $this->group->getByExternalId($groupId);
+
+ if (! empty($group) && ! $this->groupMember->isMember($group['id'], $userId)) {
+ $this->groupMember->addUser($group['id'], $userId);
+ }
+ }
+ }
+}
diff --git a/app/Core/User/UserProfile.php b/app/Core/User/UserProfile.php
new file mode 100644
index 00000000..ccbc7f06
--- /dev/null
+++ b/app/Core/User/UserProfile.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+use Kanboard\Core\Base;
+
+/**
+ * User Profile
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class UserProfile extends Base
+{
+ /**
+ * Assign provider data to the local user
+ *
+ * @access public
+ * @param integer $userId
+ * @param UserProviderInterface $user
+ * @return boolean
+ */
+ public function assign($userId, UserProviderInterface $user)
+ {
+ $profile = $this->user->getById($userId);
+
+ $values = UserProperty::filterProperties($profile, UserProperty::getProperties($user));
+ $values['id'] = $userId;
+
+ if ($this->user->update($values)) {
+ $profile = array_merge($profile, $values);
+ $this->userSession->initialize($profile);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Synchronize user properties with the local database and create the user session
+ *
+ * @access public
+ * @param UserProviderInterface $user
+ * @return boolean
+ */
+ public function initialize(UserProviderInterface $user)
+ {
+ if ($user->getInternalId()) {
+ $profile = $this->user->getById($user->getInternalId());
+ } elseif ($user->getExternalIdColumn() && $user->getExternalId()) {
+ $profile = $this->userSync->synchronize($user);
+ $this->groupSync->synchronize($profile['id'], $user->getExternalGroupIds());
+ }
+
+ if (! empty($profile)) {
+ $this->userSession->initialize($profile);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/Core/User/UserProperty.php b/app/Core/User/UserProperty.php
new file mode 100644
index 00000000..f8b08a3d
--- /dev/null
+++ b/app/Core/User/UserProperty.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+/**
+ * User Property
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class UserProperty
+{
+ /**
+ * Get filtered user properties from user provider
+ *
+ * @static
+ * @access public
+ * @param UserProviderInterface $user
+ * @return array
+ */
+ public static function getProperties(UserProviderInterface $user)
+ {
+ $properties = array(
+ 'username' => $user->getUsername(),
+ 'name' => $user->getName(),
+ 'email' => $user->getEmail(),
+ 'role' => $user->getRole(),
+ $user->getExternalIdColumn() => $user->getExternalId(),
+ );
+
+ $properties = array_merge($properties, $user->getExtraAttributes());
+
+ return array_filter($properties, array(__NAMESPACE__.'\UserProperty', 'isNotEmptyValue'));
+ }
+
+ /**
+ * Filter user properties compared to existing user profile
+ *
+ * @static
+ * @access public
+ * @param array $profile
+ * @param array $properties
+ * @return array
+ */
+ public static function filterProperties(array $profile, array $properties)
+ {
+ $values = array();
+
+ foreach ($properties as $property => $value) {
+ if (array_key_exists($property, $profile) && ! self::isNotEmptyValue($profile[$property])) {
+ $values[$property] = $value;
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Check if a value is not empty
+ *
+ * @static
+ * @access public
+ * @param string $value
+ * @return boolean
+ */
+ public static function isNotEmptyValue($value)
+ {
+ return $value !== null && $value !== '';
+ }
+}
diff --git a/app/Core/User/UserProviderInterface.php b/app/Core/User/UserProviderInterface.php
new file mode 100644
index 00000000..07e01f42
--- /dev/null
+++ b/app/Core/User/UserProviderInterface.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+/**
+ * User Provider Interface
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+interface UserProviderInterface
+{
+ /**
+ * Return true to allow automatic user creation
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isUserCreationAllowed();
+
+ /**
+ * Get external id column name
+ *
+ * Example: google_id, github_id, gitlab_id...
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn();
+
+ /**
+ * Get internal id
+ *
+ * If a value is returned the user properties won't be updated in the local database
+ *
+ * @access public
+ * @return integer
+ */
+ public function getInternalId();
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId();
+
+ /**
+ * Get user role
+ *
+ * Return an empty string to not override role stored in the database
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole();
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername();
+
+ /**
+ * Get user full name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Get user email
+ *
+ * @access public
+ * @return string
+ */
+ public function getEmail();
+
+ /**
+ * Get external group ids
+ *
+ * A synchronization is done at login time,
+ * the user will be member of those groups if they exists in the database
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getExternalGroupIds();
+
+ /**
+ * Get extra user attributes
+ *
+ * Example: is_ldap_user, disable_login_form, notifications_enabled...
+ *
+ * @access public
+ * @return array
+ */
+ public function getExtraAttributes();
+}
diff --git a/app/Model/UserSession.php b/app/Core/User/UserSession.php
index a687952b..d1e0bb93 100644
--- a/app/Model/UserSession.php
+++ b/app/Core/User/UserSession.php
@@ -1,11 +1,14 @@
<?php
-namespace Kanboard\Model;
+namespace Kanboard\Core\User;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\Role;
/**
* User Session
*
- * @package model
+ * @package user
* @author Frederic Guillot
*/
class UserSession extends Base
@@ -18,76 +21,82 @@ class UserSession extends Base
*/
public function initialize(array $user)
{
- if (isset($user['password'])) {
- unset($user['password']);
- }
-
- if (isset($user['twofactor_secret'])) {
- unset($user['twofactor_secret']);
+ foreach (array('password', 'is_admin', 'is_project_admin', 'twofactor_secret') as $column) {
+ if (isset($user[$column])) {
+ unset($user[$column]);
+ }
}
$user['id'] = (int) $user['id'];
- $user['is_admin'] = isset($user['is_admin']) ? (bool) $user['is_admin'] : false;
- $user['is_project_admin'] = isset($user['is_project_admin']) ? (bool) $user['is_project_admin'] : false;
$user['is_ldap_user'] = isset($user['is_ldap_user']) ? (bool) $user['is_ldap_user'] : false;
$user['twofactor_activated'] = isset($user['twofactor_activated']) ? (bool) $user['twofactor_activated'] : false;
$this->sessionStorage->user = $user;
- $this->sessionStorage->postAuth = array('validated' => false);
+ $this->sessionStorage->postAuthenticationValidated = false;
}
/**
- * Return true if the user has validated the 2FA key
+ * Get user application role
*
* @access public
- * @return bool
+ * @return string
*/
- public function check2FA()
+ public function getRole()
{
- return isset($this->sessionStorage->postAuth['validated']) && $this->sessionStorage->postAuth['validated'] === true;
+ return $this->sessionStorage->user['role'];
}
/**
- * Return true if the user has 2FA enabled
+ * Return true if the user has validated the 2FA key
*
* @access public
* @return bool
*/
- public function has2FA()
+ public function isPostAuthenticationValidated()
{
- return isset($this->sessionStorage->user['twofactor_activated']) && $this->sessionStorage->user['twofactor_activated'] === true;
+ return isset($this->sessionStorage->postAuthenticationValidated) && $this->sessionStorage->postAuthenticationValidated === true;
}
/**
- * Disable 2FA for the current session
+ * Validate 2FA for the current session
*
* @access public
*/
- public function disable2FA()
+ public function validatePostAuthentication()
{
- $this->sessionStorage->user['twofactor_activated'] = false;
+ $this->sessionStorage->postAuthenticationValidated = true;
}
/**
- * Return true if the logged user is admin
+ * Return true if the user has 2FA enabled
*
* @access public
* @return bool
*/
- public function isAdmin()
+ public function hasPostAuthentication()
{
- return isset($this->sessionStorage->user['is_admin']) && $this->sessionStorage->user['is_admin'] === true;
+ return isset($this->sessionStorage->user['twofactor_activated']) && $this->sessionStorage->user['twofactor_activated'] === true;
}
/**
- * Return true if the logged user is project admin
+ * Disable 2FA for the current session
+ *
+ * @access public
+ */
+ public function disablePostAuthentication()
+ {
+ $this->sessionStorage->user['twofactor_activated'] = false;
+ }
+
+ /**
+ * Return true if the logged user is admin
*
* @access public
* @return bool
*/
- public function isProjectAdmin()
+ public function isAdmin()
{
- return isset($this->sessionStorage->user['is_project_admin']) && $this->sessionStorage->user['is_project_admin'] === true;
+ return isset($this->sessionStorage->user['role']) && $this->sessionStorage->user['role'] === Role::APP_ADMIN;
}
/**
diff --git a/app/Core/User/UserSync.php b/app/Core/User/UserSync.php
new file mode 100644
index 00000000..d450a0bd
--- /dev/null
+++ b/app/Core/User/UserSync.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Kanboard\Core\User;
+
+use Kanboard\Core\Base;
+
+/**
+ * User Synchronization
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class UserSync extends Base
+{
+ /**
+ * Synchronize user profile
+ *
+ * @access public
+ * @param UserProviderInterface $user
+ * @return array
+ */
+ public function synchronize(UserProviderInterface $user)
+ {
+ $profile = $this->user->getByExternalId($user->getExternalIdColumn(), $user->getExternalId());
+ $properties = UserProperty::getProperties($user);
+
+ if (! empty($profile)) {
+ $profile = $this->updateUser($profile, $properties);
+ } elseif ($user->isUserCreationAllowed()) {
+ $profile = $this->createUser($user, $properties);
+ }
+
+ return $profile;
+ }
+
+ /**
+ * Update user profile
+ *
+ * @access public
+ * @param array $profile
+ * @param array $properties
+ * @return array
+ */
+ private function updateUser(array $profile, array $properties)
+ {
+ $values = UserProperty::filterProperties($profile, $properties);
+
+ if (! empty($values)) {
+ $values['id'] = $profile['id'];
+ $result = $this->user->update($values);
+ return $result ? array_merge($profile, $properties) : $profile;
+ }
+
+ return $profile;
+ }
+
+ /**
+ * Create user
+ *
+ * @access public
+ * @param UserProviderInterface $user
+ * @param array $properties
+ * @return array
+ */
+ private function createUser(UserProviderInterface $user, array $properties)
+ {
+ $id = $this->user->create($properties);
+
+ if ($id === false) {
+ $this->logger->error('Unable to create user profile: '.$user->getExternalId());
+ return array();
+ }
+
+ return $this->user->getById($id);
+ }
+}
diff --git a/app/Event/AuthEvent.php b/app/Event/AuthEvent.php
deleted file mode 100644
index 7cbced83..00000000
--- a/app/Event/AuthEvent.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-namespace Kanboard\Event;
-
-use Symfony\Component\EventDispatcher\Event as BaseEvent;
-
-class AuthEvent extends BaseEvent
-{
- private $auth_name;
- private $user_id;
-
- public function __construct($auth_name, $user_id)
- {
- $this->auth_name = $auth_name;
- $this->user_id = $user_id;
- }
-
- public function getUserId()
- {
- return $this->user_id;
- }
-
- public function getAuthType()
- {
- return $this->auth_name;
- }
-}
diff --git a/app/Event/AuthFailureEvent.php b/app/Event/AuthFailureEvent.php
new file mode 100644
index 00000000..225ac04a
--- /dev/null
+++ b/app/Event/AuthFailureEvent.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Event;
+
+use Symfony\Component\EventDispatcher\Event as BaseEvent;
+
+/**
+ * Authentication Failure Event
+ *
+ * @package event
+ * @author Frederic Guillot
+ */
+class AuthFailureEvent extends BaseEvent
+{
+ /**
+ * Username
+ *
+ * @access private
+ * @var string
+ */
+ private $username = '';
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $username
+ */
+ public function __construct($username = '')
+ {
+ $this->username = $username;
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+}
diff --git a/app/Event/AuthSuccessEvent.php b/app/Event/AuthSuccessEvent.php
new file mode 100644
index 00000000..38323e82
--- /dev/null
+++ b/app/Event/AuthSuccessEvent.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Event;
+
+use Symfony\Component\EventDispatcher\Event as BaseEvent;
+
+/**
+ * Authentication Success Event
+ *
+ * @package event
+ * @author Frederic Guillot
+ */
+class AuthSuccessEvent extends BaseEvent
+{
+ /**
+ * Authentication provider name
+ *
+ * @access private
+ * @var string
+ */
+ private $authType;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $authType
+ */
+ public function __construct($authType)
+ {
+ $this->authType = $authType;
+ }
+
+ /**
+ * Get authentication type
+ *
+ * @return string
+ */
+ public function getAuthType()
+ {
+ return $this->authType;
+ }
+}
diff --git a/app/Formatter/GroupAutoCompleteFormatter.php b/app/Formatter/GroupAutoCompleteFormatter.php
new file mode 100644
index 00000000..7023e367
--- /dev/null
+++ b/app/Formatter/GroupAutoCompleteFormatter.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+/**
+ * Autocomplete formatter for groups
+ *
+ * @package formatter
+ * @author Frederic Guillot
+ */
+class GroupAutoCompleteFormatter implements FormatterInterface
+{
+ /**
+ * Groups found
+ *
+ * @access private
+ * @var array
+ */
+ private $groups;
+
+ /**
+ * Format groups for the ajax autocompletion
+ *
+ * @access public
+ * @param array $groups
+ * @return GroupAutoCompleteFormatter
+ */
+ public function setGroups(array $groups)
+ {
+ $this->groups = $groups;
+ return $this;
+ }
+
+ /**
+ * Format groups for the ajax autocompletion
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $result = array();
+
+ foreach ($this->groups as $group) {
+ $result[] = array(
+ 'id' => $group->getInternalId(),
+ 'external_id' => $group->getExternalId(),
+ 'value' => $group->getName(),
+ 'label' => $group->getName(),
+ );
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Formatter/ProjectGanttFormatter.php b/app/Formatter/ProjectGanttFormatter.php
index 17496088..4f73e217 100644
--- a/app/Formatter/ProjectGanttFormatter.php
+++ b/app/Formatter/ProjectGanttFormatter.php
@@ -79,7 +79,7 @@ class ProjectGanttFormatter extends Project implements FormatterInterface
'gantt_link' => $this->helper->url->href('gantt', 'project', array('project_id' => $project['id'])),
'color' => $color,
'not_defined' => empty($project['start_date']) || empty($project['end_date']),
- 'users' => $this->projectPermission->getProjectUsers($project['id']),
+ 'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']),
);
}
diff --git a/app/Formatter/UserFilterAutoCompleteFormatter.php b/app/Formatter/UserFilterAutoCompleteFormatter.php
new file mode 100644
index 00000000..b98e0d69
--- /dev/null
+++ b/app/Formatter/UserFilterAutoCompleteFormatter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Model\User;
+use Kanboard\Model\UserFilter;
+
+/**
+ * Autocomplete formatter for user filter
+ *
+ * @package formatter
+ * @author Frederic Guillot
+ */
+class UserFilterAutoCompleteFormatter extends UserFilter implements FormatterInterface
+{
+ /**
+ * Format the tasks for the ajax autocompletion
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $users = $this->query->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')->findAll();
+
+ foreach ($users as &$user) {
+ $user['value'] = $user['username'].' (#'.$user['id'].')';
+
+ if (empty($user['name'])) {
+ $user['label'] = $user['username'];
+ } else {
+ $user['label'] = $user['name'].' ('.$user['username'].')';
+ }
+ }
+
+ return $users;
+ }
+}
diff --git a/app/Group/DatabaseBackendGroupProvider.php b/app/Group/DatabaseBackendGroupProvider.php
new file mode 100644
index 00000000..a53516a0
--- /dev/null
+++ b/app/Group/DatabaseBackendGroupProvider.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Kanboard\Group;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Group\GroupBackendProviderInterface;
+
+/**
+ * Database Backend Group Provider
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+class DatabaseBackendGroupProvider extends Base implements GroupBackendProviderInterface
+{
+ /**
+ * Find a group from a search query
+ *
+ * @access public
+ * @param string $input
+ * @return []DatabaseGroupProvider
+ */
+ public function find($input)
+ {
+ $result = array();
+ $groups = $this->group->search($input);
+
+ foreach ($groups as $group) {
+ $result[] = new DatabaseGroupProvider($group);
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Group/DatabaseGroupProvider.php b/app/Group/DatabaseGroupProvider.php
new file mode 100644
index 00000000..e00f36ba
--- /dev/null
+++ b/app/Group/DatabaseGroupProvider.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Kanboard\Group;
+
+use Kanboard\Core\Group\GroupProviderInterface;
+
+/**
+ * Database Group Provider
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+class DatabaseGroupProvider implements GroupProviderInterface
+{
+ /**
+ * Group properties
+ *
+ * @access private
+ * @var array
+ */
+ private $group = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $group
+ */
+ public function __construct(array $group)
+ {
+ $this->group = $group;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return integer
+ */
+ public function getInternalId()
+ {
+ return $this->group['id'];
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get group name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->group['name'];
+ }
+}
diff --git a/app/Group/LdapBackendGroupProvider.php b/app/Group/LdapBackendGroupProvider.php
new file mode 100644
index 00000000..40273466
--- /dev/null
+++ b/app/Group/LdapBackendGroupProvider.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Kanboard\Group;
+
+use LogicException;
+use Kanboard\Core\Base;
+use Kanboard\Core\Group\GroupBackendProviderInterface;
+use Kanboard\Core\Ldap\Client as LdapClient;
+use Kanboard\Core\Ldap\ClientException as LdapException;
+use Kanboard\Core\Ldap\Group as LdapGroup;
+
+/**
+ * LDAP Backend Group Provider
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+class LdapBackendGroupProvider extends Base implements GroupBackendProviderInterface
+{
+ /**
+ * Find a group from a search query
+ *
+ * @access public
+ * @param string $input
+ * @return []LdapGroupProvider
+ */
+ public function find($input)
+ {
+ try {
+ $ldap = LdapClient::connect();
+ return LdapGroup::getGroups($ldap, $this->getLdapGroupPattern($input));
+
+ } catch (LdapException $e) {
+ $this->logger->error($e->getMessage());
+ return array();
+ }
+ }
+
+ /**
+ * Get LDAP group pattern
+ *
+ * @access public
+ * @param string $input
+ * @return string
+ */
+ public function getLdapGroupPattern($input)
+ {
+ if (empty(LDAP_GROUP_FILTER)) {
+ throw new LogicException('LDAP group filter empty, check the parameter LDAP_GROUP_FILTER');
+ }
+
+ return sprintf(LDAP_GROUP_FILTER, $input);
+ }
+}
diff --git a/app/Group/LdapGroupProvider.php b/app/Group/LdapGroupProvider.php
new file mode 100644
index 00000000..b497d485
--- /dev/null
+++ b/app/Group/LdapGroupProvider.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Kanboard\Group;
+
+use Kanboard\Core\Group\GroupProviderInterface;
+
+/**
+ * LDAP Group Provider
+ *
+ * @package group
+ * @author Frederic Guillot
+ */
+class LdapGroupProvider implements GroupProviderInterface
+{
+ /**
+ * Group DN
+ *
+ * @access private
+ * @var string
+ */
+ private $dn = '';
+
+ /**
+ * Group Name
+ *
+ * @access private
+ * @var string
+ */
+ private $name = '';
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $dn
+ * @param string $name
+ */
+ public function __construct($dn, $name)
+ {
+ $this->dn = $dn;
+ $this->name = $name;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return integer
+ */
+ public function getInternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return $this->dn;
+ }
+
+ /**
+ * Get group name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+}
diff --git a/app/Helper/Url.php b/app/Helper/Url.php
index edb26841..3658ef5f 100644
--- a/app/Helper/Url.php
+++ b/app/Helper/Url.php
@@ -125,7 +125,7 @@ class Url extends Base
return 'http://localhost/';
}
- $url = Request::isHTTPS() ? 'https://' : 'http://';
+ $url = $this->request->isHTTPS() ? 'https://' : 'http://';
$url .= $_SERVER['SERVER_NAME'];
$url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
$url .= $this->dir() ?: '/';
diff --git a/app/Helper/User.php b/app/Helper/User.php
index 9ef20b38..b242dbb4 100644
--- a/app/Helper/User.php
+++ b/app/Helper/User.php
@@ -2,6 +2,8 @@
namespace Kanboard\Helper;
+use Kanboard\Core\Security\Role;
+
/**
* User helpers
*
@@ -65,6 +67,7 @@ class User extends \Kanboard\Core\Base
array('user_id' => $this->userSession->getId())
);
}
+
/**
* Check if the given user_id is the connected user
*
@@ -88,44 +91,77 @@ class User extends \Kanboard\Core\Base
}
/**
- * Return if the logged user is project admin
+ * Get role name
*
* @access public
- * @return boolean
+ * @param string $role
+ * @return string
*/
- public function isProjectAdmin()
+ public function getRoleName($role = '')
{
- return $this->userSession->isProjectAdmin();
+ return $this->role->getRoleName($role ?: $this->userSession->getRole());
}
/**
- * Check for project administration actions access (Project Admin group)
+ * Check application access
*
- * @access public
- * @return boolean
+ * @param string $controller
+ * @param string $action
+ * @return bool
*/
- public function isProjectAdministrationAllowed($project_id)
+ public function hasAccess($controller, $action)
{
- if ($this->userSession->isAdmin()) {
- return true;
+ $key = 'app_access:'.$controller.$action;
+ $result = $this->memoryCache->get($key);
+
+ if ($result === null) {
+ $result = $this->applicationAuthorization->isAllowed($controller, $action, $this->userSession->getRole());
+ $this->memoryCache->set($key, $result);
}
- return $this->memoryCache->proxy($this->container['acl'], 'handleProjectAdminPermissions', $project_id);
+ return $result;
}
/**
- * Check for project management actions access (Regular users who are Project Managers)
+ * Check project access
*
- * @access public
- * @return boolean
+ * @param string $controller
+ * @param string $action
+ * @param integer $project_id
+ * @return bool
*/
- public function isProjectManagementAllowed($project_id)
+ public function hasProjectAccess($controller, $action, $project_id)
{
if ($this->userSession->isAdmin()) {
return true;
}
- return $this->memoryCache->proxy($this->container['acl'], 'handleProjectManagerPermissions', $project_id);
+ if (! $this->hasAccess($controller, $action)) {
+ return false;
+ }
+
+ $key = 'project_access:'.$controller.$action.$project_id;
+ $result = $this->memoryCache->get($key);
+
+ if ($result === null) {
+ $role = $this->getProjectUserRole($project_id);
+ $result = $this->projectAuthorization->isAllowed($controller, $action, $role);
+ $this->memoryCache->set($key, $result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get project role for the current user
+ *
+ * @access public
+ * @param integer $project_id
+ * @return string
+ */
+ public function getProjectUserRole($project_id)
+ {
+ return $this->memoryCache->proxy($this->projectUserRole, 'getUserRole', $project_id, $this->userSession->getId());
}
/**
diff --git a/app/Model/Acl.php b/app/Model/Acl.php
deleted file mode 100644
index 62f850cb..00000000
--- a/app/Model/Acl.php
+++ /dev/null
@@ -1,289 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Access List
- *
- * @package model
- * @author Frederic Guillot
- */
-class Acl extends Base
-{
- /**
- * Controllers and actions allowed from outside
- *
- * @access private
- * @var array
- */
- private $public_acl = array(
- 'auth' => array('login', 'check', 'captcha'),
- 'task' => array('readonly'),
- 'board' => array('readonly'),
- 'webhook' => '*',
- 'ical' => '*',
- 'feed' => '*',
- 'oauth' => array('google', 'github', 'gitlab'),
- );
-
- /**
- * Controllers and actions for project members
- *
- * @access private
- * @var array
- */
- private $project_member_acl = array(
- 'board' => '*',
- 'comment' => '*',
- 'file' => '*',
- 'project' => array('show'),
- 'listing' => '*',
- 'activity' => '*',
- 'subtask' => '*',
- 'task' => '*',
- 'taskduplication' => '*',
- 'taskcreation' => '*',
- 'taskmodification' => '*',
- 'taskstatus' => '*',
- 'tasklink' => '*',
- 'timer' => '*',
- 'customfilter' => '*',
- 'calendar' => array('show', 'project'),
- );
-
- /**
- * Controllers and actions for project managers
- *
- * @access private
- * @var array
- */
- private $project_manager_acl = array(
- 'action' => '*',
- 'analytic' => '*',
- 'category' => '*',
- 'column' => '*',
- 'export' => '*',
- 'taskimport' => '*',
- 'project' => array('edit', 'update', 'share', 'integrations', 'notifications', 'users', 'alloweverybody', 'allow', 'setowner', 'revoke', 'duplicate', 'disable', 'enable'),
- 'swimlane' => '*',
- 'gantt' => array('project', 'savetaskdate', 'task', 'savetask'),
- );
-
- /**
- * Controllers and actions for project admins
- *
- * @access private
- * @var array
- */
- private $project_admin_acl = array(
- 'project' => array('remove'),
- 'projectuser' => '*',
- 'gantt' => array('projects', 'saveprojectdate'),
- );
-
- /**
- * Controllers and actions for admins
- *
- * @access private
- * @var array
- */
- private $admin_acl = array(
- 'user' => array('index', 'create', 'save', 'remove', 'authentication'),
- 'userimport' => '*',
- 'config' => '*',
- 'link' => '*',
- 'currency' => '*',
- 'twofactor' => array('disable'),
- );
-
- /**
- * Extend ACL rules
- *
- * @access public
- * @param string $acl_name
- * @param aray $rules
- */
- public function extend($acl_name, array $rules)
- {
- $this->$acl_name = array_merge($this->$acl_name, $rules);
- }
-
- /**
- * Return true if the specified controller/action match the given acl
- *
- * @access public
- * @param array $acl Acl list
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function matchAcl(array $acl, $controller, $action)
- {
- $controller = strtolower($controller);
- $action = strtolower($action);
- return isset($acl[$controller]) && $this->hasAction($action, $acl[$controller]);
- }
-
- /**
- * Return true if the specified action is inside the list of actions
- *
- * @access public
- * @param string $action Action name
- * @param mixed $action Actions list
- * @return bool
- */
- public function hasAction($action, $actions)
- {
- if (is_array($actions)) {
- return in_array($action, $actions);
- }
-
- return $actions === '*';
- }
-
- /**
- * Return true if the given action is public
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isPublicAction($controller, $action)
- {
- return $this->matchAcl($this->public_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for admins
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isAdminAction($controller, $action)
- {
- return $this->matchAcl($this->admin_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for project managers
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isProjectManagerAction($controller, $action)
- {
- return $this->matchAcl($this->project_manager_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for application managers
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isProjectAdminAction($controller, $action)
- {
- return $this->matchAcl($this->project_admin_acl, $controller, $action);
- }
-
- /**
- * Return true if the given action is for project members
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isProjectMemberAction($controller, $action)
- {
- return $this->matchAcl($this->project_member_acl, $controller, $action);
- }
-
- /**
- * Return true if the visitor is allowed to access to the given page
- * We suppose the user already authenticated
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @param integer $project_id Project id
- * @return bool
- */
- public function isAllowed($controller, $action, $project_id = 0)
- {
- // If you are admin you have access to everything
- if ($this->userSession->isAdmin()) {
- return true;
- }
-
- // If you access to an admin action, your are not allowed
- if ($this->isAdminAction($controller, $action)) {
- return false;
- }
-
- // Check project admin permissions
- if ($this->isProjectAdminAction($controller, $action)) {
- return $this->handleProjectAdminPermissions($project_id);
- }
-
- // Check project manager permissions
- if ($this->isProjectManagerAction($controller, $action)) {
- return $this->handleProjectManagerPermissions($project_id);
- }
-
- // Check project member permissions
- if ($this->isProjectMemberAction($controller, $action)) {
- return $project_id > 0 && $this->projectPermission->isMember($project_id, $this->userSession->getId());
- }
-
- // Other applications actions are allowed
- return true;
- }
-
- /**
- * Handle permission for project manager
- *
- * @access public
- * @param integer $project_id
- * @return boolean
- */
- public function handleProjectManagerPermissions($project_id)
- {
- if ($project_id > 0) {
- if ($this->userSession->isProjectAdmin()) {
- return $this->projectPermission->isMember($project_id, $this->userSession->getId());
- }
-
- return $this->projectPermission->isManager($project_id, $this->userSession->getId());
- }
-
- return false;
- }
-
- /**
- * Handle permission for project admins
- *
- * @access public
- * @param integer $project_id
- * @return boolean
- */
- public function handleProjectAdminPermissions($project_id)
- {
- if (! $this->userSession->isProjectAdmin()) {
- return false;
- }
-
- if ($project_id > 0) {
- return $this->projectPermission->isMember($project_id, $this->userSession->getId());
- }
-
- return true;
- }
-}
diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php
index 83d85433..d10f2bf8 100644
--- a/app/Model/Authentication.php
+++ b/app/Model/Authentication.php
@@ -2,7 +2,6 @@
namespace Kanboard\Model;
-use Kanboard\Core\Http\Request;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use Gregwar\Captcha\CaptchaBuilder;
@@ -16,113 +15,6 @@ use Gregwar\Captcha\CaptchaBuilder;
class Authentication extends Base
{
/**
- * Load automatically an authentication backend
- *
- * @access public
- * @param string $name Backend class name
- * @return mixed
- */
- public function backend($name)
- {
- if (! isset($this->container[$name])) {
- $class = '\Kanboard\Auth\\'.ucfirst($name);
- $this->container[$name] = new $class($this->container);
- }
-
- return $this->container[$name];
- }
-
- /**
- * Check if the current user is authenticated
- *
- * @access public
- * @return bool
- */
- public function isAuthenticated()
- {
- // If the user is already logged it's ok
- if ($this->userSession->isLogged()) {
-
- // Check if the user session match an existing user
- $userNotFound = ! $this->user->exists($this->userSession->getId());
- $reverseProxyWrongUser = REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->getUsername() !== $this->userSession->getUsername();
-
- if ($userNotFound || $reverseProxyWrongUser) {
- $this->backend('rememberMe')->destroy($this->userSession->getId());
- $this->sessionManager->close();
- return false;
- }
-
- return true;
- }
-
- // We try first with the RememberMe cookie
- if (REMEMBER_ME_AUTH && $this->backend('rememberMe')->authenticate()) {
- return true;
- }
-
- // Then with the ReverseProxy authentication
- if (REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->authenticate()) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Authenticate a user by different methods
- *
- * @access public
- * @param string $username Username
- * @param string $password Password
- * @return boolean
- */
- public function authenticate($username, $password)
- {
- if ($this->user->isLocked($username)) {
- $this->container['logger']->error('Account locked: '.$username);
- return false;
- } elseif ($this->backend('database')->authenticate($username, $password)) {
- $this->user->resetFailedLogin($username);
- return true;
- } elseif (LDAP_AUTH && $this->backend('ldap')->authenticate($username, $password)) {
- $this->user->resetFailedLogin($username);
- return true;
- }
-
- $this->handleFailedLogin($username);
- return false;
- }
-
- /**
- * Return true if the captcha must be shown
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function hasCaptcha($username)
- {
- return $this->user->getFailedLogin($username) >= BRUTEFORCE_CAPTCHA;
- }
-
- /**
- * Handle failed login
- *
- * @access public
- * @param string $username
- */
- public function handleFailedLogin($username)
- {
- $this->user->incrementFailedLogin($username);
-
- if ($this->user->getFailedLogin($username) >= BRUTEFORCE_LOCKDOWN) {
- $this->container['logger']->critical('Locking account: '.$username);
- $this->user->lock($username, BRUTEFORCE_LOCKDOWN_DURATION);
- }
- }
-
- /**
* Validate user login form
*
* @access public
@@ -131,14 +23,14 @@ class Authentication extends Base
*/
public function validateForm(array $values)
{
- list($result, $errors) = $this->validateFormCredentials($values);
+ $result = false;
+ $errors = array();
- if ($result) {
- if ($this->validateFormCaptcha($values) && $this->authenticate($values['username'], $values['password'])) {
- $this->createRememberMeSession($values);
- } else {
- $result = false;
- $errors['login'] = t('Bad username or password');
+ foreach (array('validateFields', 'validateLocking', 'validateCaptcha', 'validateCredentials') as $method) {
+ list($result, $errors) = $this->$method($values);
+
+ if (! $result) {
+ break;
}
}
@@ -148,11 +40,11 @@ class Authentication extends Base
/**
* Validate credentials syntax
*
- * @access public
+ * @access private
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
- public function validateFormCredentials(array $values)
+ private function validateFields(array $values)
{
$v = new Validator($values, array(
new Validators\Required('username', t('The username is required')),
@@ -167,40 +59,72 @@ class Authentication extends Base
}
/**
- * Validate captcha
+ * Validate user locking
*
- * @access public
+ * @access private
* @param array $values Form values
- * @return boolean
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
- public function validateFormCaptcha(array $values)
+ private function validateLocking(array $values)
{
- if ($this->hasCaptcha($values['username'])) {
- if (! isset($this->sessionStorage->captcha)) {
- return false;
- }
+ $result = true;
+ $errors = array();
- $builder = new CaptchaBuilder;
- $builder->setPhrase($this->sessionStorage->captcha);
- return $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : '');
+ if ($this->userLocking->isLocked($values['username'])) {
+ $result = false;
+ $errors['login'] = t('Your account is locked for %d minutes', BRUTEFORCE_LOCKDOWN_DURATION);
+ $this->logger->error('Account locked: '.$values['username']);
}
- return true;
+ return array($result, $errors);
}
/**
- * Create remember me session if necessary
+ * Validate password syntax
*
* @access private
* @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
- private function createRememberMeSession(array $values)
+ private function validateCredentials(array $values)
{
- if (REMEMBER_ME_AUTH && ! empty($values['remember_me'])) {
- $credentials = $this->backend('rememberMe')
- ->create($this->userSession->getId(), Request::getIpAddress(), Request::getUserAgent());
+ $result = true;
+ $errors = array();
- $this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
+ if (! $this->authenticationManager->passwordAuthentication($values['username'], $values['password'])) {
+ $result = false;
+ $errors['login'] = t('Bad username or password');
}
+
+ return array($result, $errors);
+ }
+
+ /**
+ * Validate captcha
+ *
+ * @access private
+ * @param array $values Form values
+ * @return boolean
+ */
+ private function validateCaptcha(array $values)
+ {
+ $result = true;
+ $errors = array();
+
+ if ($this->userLocking->hasCaptcha($values['username'])) {
+ if (! isset($this->sessionStorage->captcha)) {
+ $result = false;
+ } else {
+ $builder = new CaptchaBuilder;
+ $builder->setPhrase($this->sessionStorage->captcha);
+ $result = $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : '');
+
+ if (! $result) {
+ $errors['login'] = t('Invalid captcha');
+ }
+ }
+ }
+
+ return array($result, $errors);;
}
}
diff --git a/app/Model/Group.php b/app/Model/Group.php
index 82a8887b..36171ca4 100644
--- a/app/Model/Group.php
+++ b/app/Model/Group.php
@@ -44,6 +44,18 @@ class Group extends Base
}
/**
+ * Get a specific group by external id
+ *
+ * @access public
+ * @param integer $external_id
+ * @return array
+ */
+ public function getByExternalId($external_id)
+ {
+ return $this->getQuery()->eq('external_id', $external_id)->findOne();
+ }
+
+ /**
* Get all groups
*
* @access public
@@ -55,6 +67,18 @@ class Group extends Base
}
/**
+ * Search groups by name
+ *
+ * @access public
+ * @param string $input
+ * @return array
+ */
+ public function search($input)
+ {
+ return $this->db->table(self::TABLE)->ilike('name', '%'.$input.'%')->findAll();
+ }
+
+ /**
* Remove a group
*
* @access public
diff --git a/app/Model/GroupMember.php b/app/Model/GroupMember.php
index 04e9d495..7ed5f733 100644
--- a/app/Model/GroupMember.php
+++ b/app/Model/GroupMember.php
@@ -65,8 +65,8 @@ class GroupMember extends Base
* Add user to a group
*
* @access public
- * @param integer $group_id
- * @param integer $user_id
+ * @param integer $group_id
+ * @param integer $user_id
* @return boolean
*/
public function addUser($group_id, $user_id)
@@ -81,8 +81,8 @@ class GroupMember extends Base
* Remove user from a group
*
* @access public
- * @param integer $group_id
- * @param integer $user_id
+ * @param integer $group_id
+ * @param integer $user_id
* @return boolean
*/
public function removeUser($group_id, $user_id)
@@ -92,4 +92,20 @@ class GroupMember extends Base
->eq('user_id', $user_id)
->remove();
}
+
+ /**
+ * Check if a user is member
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function isMember($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('user_id', $user_id)
+ ->exists();
+ }
}
diff --git a/app/Model/Project.php b/app/Model/Project.php
index a7f93099..8a949ba6 100644
--- a/app/Model/Project.php
+++ b/app/Model/Project.php
@@ -5,6 +5,7 @@ namespace Kanboard\Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
/**
* Project model
@@ -287,7 +288,7 @@ class Project extends Base
{
foreach ($projects as &$project) {
$this->getColumnStats($project);
- $project = array_merge($project, $this->projectPermission->getProjectUsers($project['id']));
+ $project = array_merge($project, $this->projectUserRole->getAllUsersGroupedByRole($project['id']));
}
return $projects;
@@ -365,7 +366,7 @@ class Project extends Base
}
if ($add_user && $user_id) {
- $this->projectPermission->addManager($project_id, $user_id);
+ $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MANAGER);
}
$this->category->createDefaultCategories($project_id);
diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php
index 92364c0c..e77a0368 100644
--- a/app/Model/ProjectAnalytic.php
+++ b/app/Model/ProjectAnalytic.php
@@ -56,7 +56,7 @@ class ProjectAnalytic extends Base
$metrics = array();
$total = 0;
$tasks = $this->taskFinder->getAll($project_id);
- $users = $this->projectPermission->getMemberList($project_id);
+ $users = $this->projectUserRole->getAssignableUsersList($project_id);
foreach ($tasks as $task) {
$user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0];
diff --git a/app/Model/ProjectGroupRole.php b/app/Model/ProjectGroupRole.php
new file mode 100644
index 00000000..87fdec10
--- /dev/null
+++ b/app/Model/ProjectGroupRole.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Security\Role;
+
+/**
+ * Project Group Role
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectGroupRole extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_has_groups';
+
+ /**
+ * Get the list of project visible by the given user according to groups
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $status
+ * @return array
+ */
+ public function getProjectsByUser($user_id, $status = array(Project::ACTIVE, Project::INACTIVE))
+ {
+ return $this->db
+ ->hashtable(Project::TABLE)
+ ->join(self::TABLE, 'project_id', 'id')
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE)
+ ->eq(GroupMember::TABLE.'.user_id', $user_id)
+ ->in(Project::TABLE.'.is_active', $status)
+ ->getAll(Project::TABLE.'.id', Project::TABLE.'.name');
+ }
+
+ /**
+ * For a given project get the role of the specified user
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return string
+ */
+ public function getUserRole($project_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE)
+ ->eq(GroupMember::TABLE.'.user_id', $user_id)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->findOneColumn('role');
+ }
+
+ /**
+ * Get all groups associated directly to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getGroups($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(Group::TABLE.'.id', Group::TABLE.'.name', self::TABLE.'.role')
+ ->join(Group::TABLE, 'id', 'group_id')
+ ->eq('project_id', $project_id)
+ ->asc('name')
+ ->findAll();
+ }
+
+ /**
+ * From groups get all users associated to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getUsers($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.role')
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->asc(User::TABLE.'.username')
+ ->findAll();
+ }
+
+ /**
+ * From groups get all users assignable to tasks
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAssignableUsers($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER))
+ ->asc(User::TABLE.'.username')
+ ->findAll();
+ }
+
+ /**
+ * Add a group to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $group_id
+ * @param string $role
+ * @return boolean
+ */
+ public function addGroup($project_id, $group_id, $role)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'group_id' => $group_id,
+ 'project_id' => $project_id,
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Remove a group from the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $group_id
+ * @return boolean
+ */
+ public function removeGroup($project_id, $group_id)
+ {
+ return $this->db->table(self::TABLE)->eq('group_id', $group_id)->eq('project_id', $project_id)->remove();
+ }
+
+ /**
+ * Change a group role for the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $group_id
+ * @param string $role
+ * @return boolean
+ */
+ public function changeGroupRole($project_id, $group_id, $role)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('project_id', $project_id)
+ ->update(array(
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Copy group access from a project to another one
+ *
+ * @param integer $project_src_id Project Template
+ * @return integer $project_dst_id Project that receives the copy
+ * @return boolean
+ */
+ public function duplicate($project_src_id, $project_dst_id)
+ {
+ $rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll();
+
+ foreach ($rows as $row) {
+ $result = $this->db->table(self::TABLE)->save(array(
+ 'project_id' => $project_dst_id,
+ 'group_id' => $row['group_id'],
+ 'role' => $row['role'],
+ ));
+
+ if (! $result) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php
index d9eef4db..b311c10b 100644
--- a/app/Model/ProjectPermission.php
+++ b/app/Model/ProjectPermission.php
@@ -2,11 +2,10 @@
namespace Kanboard\Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
+use Kanboard\Core\Security\Role;
/**
- * Project permission model
+ * Project Permission
*
* @package model
* @author Frederic Guillot
@@ -14,117 +13,14 @@ use SimpleValidator\Validators;
class ProjectPermission extends Base
{
/**
- * SQL table name for permissions
- *
- * @var string
- */
- const TABLE = 'project_has_users';
-
- /**
- * Get a list of people that can be assigned for tasks
- *
- * @access public
- * @param integer $project_id Project id
- * @param bool $prepend_unassigned Prepend the 'Unassigned' value
- * @param bool $prepend_everybody Prepend the 'Everbody' value
- * @param bool $allow_single_user If there is only one user return only this user
- * @return array
- */
- public function getMemberList($project_id, $prepend_unassigned = true, $prepend_everybody = false, $allow_single_user = false)
- {
- $allowed_users = $this->getMembers($project_id);
-
- if ($allow_single_user && count($allowed_users) === 1) {
- return $allowed_users;
- }
-
- if ($prepend_unassigned) {
- $allowed_users = array(t('Unassigned')) + $allowed_users;
- }
-
- if ($prepend_everybody) {
- $allowed_users = array(User::EVERYBODY_ID => t('Everybody')) + $allowed_users;
- }
-
- return $allowed_users;
- }
-
- /**
- * Get a list of members and managers with a single SQL query
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getProjectUsers($project_id)
- {
- $result = array(
- 'managers' => array(),
- 'members' => array(),
- );
-
- $users = $this->db
- ->table(self::TABLE)
- ->join(User::TABLE, 'id', 'user_id')
- ->eq('project_id', $project_id)
- ->asc('username')
- ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.is_owner')
- ->findAll();
-
- foreach ($users as $user) {
- $key = $user['is_owner'] == 1 ? 'managers' : 'members';
- $result[$key][$user['id']] = $user['name'] ?: $user['username'];
- }
-
- return $result;
- }
-
- /**
- * Get a list of allowed people for a project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getMembers($project_id)
- {
- if ($this->isEverybodyAllowed($project_id)) {
- return $this->user->getList();
- }
-
- return $this->getAssociatedUsers($project_id);
- }
-
- /**
- * Get a list of owners for a project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getManagers($project_id)
- {
- $users = $this->db
- ->table(self::TABLE)
- ->join(User::TABLE, 'id', 'user_id')
- ->eq('project_id', $project_id)
- ->eq('is_owner', 1)
- ->asc('username')
- ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
- ->findAll();
-
- return $this->user->prepareList($users);
- }
-
- /**
* Get query for project users overview
*
* @access public
* @param array $project_ids
- * @param integer $is_owner
+ * @param string $role
* @return \PicoDb\Table
*/
- public function getQueryByRole(array $project_ids, $is_owner = 0)
+ public function getQueryByRole(array $project_ids, $role)
{
if (empty($project_ids)) {
$project_ids = array(-1);
@@ -135,7 +31,7 @@ class ProjectPermission extends Base
->table(self::TABLE)
->join(User::TABLE, 'id', 'user_id')
->join(Project::TABLE, 'id', 'project_id')
- ->eq(self::TABLE.'.is_owner', $is_owner)
+ ->eq(self::TABLE.'.role', $role)
->eq(Project::TABLE.'.is_private', 0)
->in(Project::TABLE.'.id', $project_ids)
->columns(
@@ -148,172 +44,6 @@ class ProjectPermission extends Base
}
/**
- * Get a list of people associated to the project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getAssociatedUsers($project_id)
- {
- $users = $this->db
- ->table(self::TABLE)
- ->join(User::TABLE, 'id', 'user_id')
- ->eq('project_id', $project_id)
- ->asc('username')
- ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
- ->findAll();
-
- return $this->user->prepareList($users);
- }
-
- /**
- * Get allowed and not allowed users for a project
- *
- * @access public
- * @param integer $project_id Project id
- * @return array
- */
- public function getAllUsers($project_id)
- {
- $users = array(
- 'allowed' => array(),
- 'not_allowed' => array(),
- 'managers' => array(),
- );
-
- $all_users = $this->user->getList();
-
- $users['allowed'] = $this->getMembers($project_id);
- $users['managers'] = $this->getManagers($project_id);
-
- foreach ($all_users as $user_id => $username) {
- if (! isset($users['allowed'][$user_id])) {
- $users['not_allowed'][$user_id] = $username;
- }
- }
-
- return $users;
- }
-
- /**
- * Add a new project member
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function addMember($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->save(array('project_id' => $project_id, 'user_id' => $user_id));
- }
-
- /**
- * Remove a member
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function revokeMember($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->remove();
- }
-
- /**
- * Add a project manager
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function addManager($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->save(array('project_id' => $project_id, 'user_id' => $user_id, 'is_owner' => 1));
- }
-
- /**
- * Change the role of a member
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @param integer $is_owner Is user owner of the project
- * @return bool
- */
- public function changeRole($project_id, $user_id, $is_owner)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->update(array('is_owner' => (int) $is_owner));
- }
-
- /**
- * Check if a specific user is member of a project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function isMember($project_id, $user_id)
- {
- if ($this->isEverybodyAllowed($project_id)) {
- return true;
- }
-
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->exists();
- }
-
- /**
- * Check if a specific user is manager of a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function isManager($project_id, $user_id)
- {
- return $this->db
- ->table(self::TABLE)
- ->eq('project_id', $project_id)
- ->eq('user_id', $user_id)
- ->eq('is_owner', 1)
- ->exists();
- }
-
- /**
- * Check if a specific user is allowed to access to a given project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $user_id User id
- * @return bool
- */
- public function isUserAllowed($project_id, $user_id)
- {
- return $project_id === 0 || $this->user->isAdmin($user_id) || $this->isMember($project_id, $user_id);
- }
-
- /**
* Return true if everybody is allowed for the project
*
* @access public
@@ -330,172 +60,59 @@ class ProjectPermission extends Base
}
/**
- * Return a list of allowed active projects for a given user
+ * Return true if the user is allowed to access a project
*
- * @access public
- * @param integer $user_id User id
- * @return array
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
*/
- public function getAllowedProjects($user_id)
+ public function isUserAllowed($project_id, $user_id)
{
- if ($this->user->isAdmin($user_id)) {
- return $this->project->getListByStatus(Project::ACTIVE);
+ if ($this->userSession->isAdmin()) {
+ return true;
}
- return $this->getActiveMemberProjects($user_id);
- }
-
- /**
- * Return a list of projects where the user is member
- *
- * @access public
- * @param integer $user_id User id
- * @return array
- */
- public function getMemberProjects($user_id)
- {
- return $this->db
- ->hashtable(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->join(self::TABLE, 'project_id', 'id')
- ->getAll('projects.id', 'name');
- }
-
- /**
- * Return a list of project ids where the user is member
- *
- * @access public
- * @param integer $user_id User id
- * @return array
- */
- public function getMemberProjectIds($user_id)
- {
- return $this->db
- ->table(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->join(self::TABLE, 'project_id', 'id')
- ->findAllByColumn('projects.id');
+ return in_array(
+ $this->projectUserRole->getUserRole($project_id, $user_id),
+ array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER, Role::PROJECT_VIEWER)
+ );
}
/**
- * Return a list of active project ids where the user is member
+ * Return true if the user is assignable
*
* @access public
- * @param integer $user_id User id
- * @return array
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
*/
- public function getActiveMemberProjectIds($user_id)
+ public function isMember($project_id, $user_id)
{
- return $this->db
- ->table(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->eq(Project::TABLE.'.is_active', Project::ACTIVE)
- ->join(self::TABLE, 'project_id', 'id')
- ->findAllByColumn('projects.id');
+ return in_array($this->projectUserRole->getUSerRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER));
}
/**
- * Return a list of active projects where the user is member
+ * Get active project ids by user
*
* @access public
- * @param integer $user_id User id
+ * @param integer $user_id
* @return array
*/
- public function getActiveMemberProjects($user_id)
+ public function getActiveProjectIds($user_id)
{
- return $this->db
- ->hashtable(Project::TABLE)
- ->beginOr()
- ->eq(self::TABLE.'.user_id', $user_id)
- ->eq(Project::TABLE.'.is_everybody_allowed', 1)
- ->closeOr()
- ->eq(Project::TABLE.'.is_active', Project::ACTIVE)
- ->join(self::TABLE, 'project_id', 'id')
- ->getAll('projects.id', 'name');
+ return array_keys($this->projectUserRole->getProjectsByUser($user_id, array(Project::ACTIVE)));
}
/**
- * Copy user access from a project to another one
+ * Copy permissions to another project
*
- * @param integer $project_src Project Template
- * @return integer $project_dst Project that receives the copy
+ * @param integer $project_src_id Project Template
+ * @param integer $project_dst_id Project that receives the copy
* @return boolean
*/
- public function duplicate($project_src, $project_dst)
- {
- $rows = $this->db
- ->table(self::TABLE)
- ->columns('project_id', 'user_id', 'is_owner')
- ->eq('project_id', $project_src)
- ->findAll();
-
- foreach ($rows as $row) {
- $result = $this->db
- ->table(self::TABLE)
- ->save(array(
- 'project_id' => $project_dst,
- 'user_id' => $row['user_id'],
- 'is_owner' => (int) $row['is_owner'], // (int) for postgres
- ));
-
- if (! $result) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Validate allow user
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateUserModification(array $values)
+ public function duplicate($project_src_id, $project_dst_id)
{
- $v = new Validator($values, array(
- new Validators\Required('project_id', t('The project id is required')),
- new Validators\Integer('project_id', t('This value must be an integer')),
- new Validators\Required('user_id', t('The user id is required')),
- new Validators\Integer('user_id', t('This value must be an integer')),
- new Validators\Integer('is_owner', t('This value must be an integer')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
- }
-
- /**
- * Validate allow everybody
- *
- * @access public
- * @param array $values Form values
- * @return array $valid, $errors [0] = Success or not, [1] = List of errors
- */
- public function validateProjectModification(array $values)
- {
- $v = new Validator($values, array(
- new Validators\Required('id', t('The project id is required')),
- new Validators\Integer('id', t('This value must be an integer')),
- new Validators\Integer('is_everybody_allowed', t('This value must be an integer')),
- ));
-
- return array(
- $v->execute(),
- $v->getErrors()
- );
+ return $this->projectUserRole->duplicate($project_src_id, $project_dst_id) &&
+ $this->projectGroupRole->duplicate($project_src_id, $project_dst_id);
}
}
diff --git a/app/Model/ProjectUserRole.php b/app/Model/ProjectUserRole.php
new file mode 100644
index 00000000..28e6c8c6
--- /dev/null
+++ b/app/Model/ProjectUserRole.php
@@ -0,0 +1,263 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Security\Role;
+
+/**
+ * Project User Role
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectUserRole extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_has_users';
+
+ /**
+ * Get the list of project visible by the given user
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $status
+ * @return array
+ */
+ public function getProjectsByUser($user_id, $status = array(Project::ACTIVE, Project::INACTIVE))
+ {
+ $userProjects = $this->db
+ ->hashtable(Project::TABLE)
+ ->beginOr()
+ ->eq(self::TABLE.'.user_id', $user_id)
+ ->eq(Project::TABLE.'.is_everybody_allowed', 1)
+ ->closeOr()
+ ->in(Project::TABLE.'.is_active', $status)
+ ->join(self::TABLE, 'project_id', 'id')
+ ->getAll(Project::TABLE.'.id', Project::TABLE.'.name');
+
+ $groupProjects = $this->projectGroupRole->getProjectsByUser($user_id, $status);
+ $groups = $userProjects + $groupProjects;
+
+ asort($groups);
+
+ return $groups;
+ }
+
+ /**
+ * For a given project get the role of the specified user
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return string
+ */
+ public function getUserRole($project_id, $user_id)
+ {
+ if ($this->projectPermission->isEverybodyAllowed($project_id)) {
+ return Role::PROJECT_MEMBER;
+ }
+
+ $role = $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->findOneColumn('role');
+
+ if (empty($role)) {
+ $role = $this->projectGroupRole->getUserRole($project_id, $user_id);
+ }
+
+ return $role;
+ }
+
+ /**
+ * Get all users associated directly to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getUsers($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.role')
+ ->join(User::TABLE, 'id', 'user_id')
+ ->eq('project_id', $project_id)
+ ->asc(User::TABLE.'.username')
+ ->asc(User::TABLE.'.name')
+ ->findAll();
+ }
+
+ /**
+ * Get all users (fetch users from groups)
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAllUsers($project_id)
+ {
+ $userMembers = $this->getUsers($project_id);
+ $groupMembers = $this->projectGroupRole->getUsers($project_id);
+ $members = array_merge($userMembers, $groupMembers);
+
+ return $this->user->prepareList($members);
+ }
+
+ /**
+ * Get users grouped by role
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function getAllUsersGroupedByRole($project_id)
+ {
+ $users = array();
+
+ $userMembers = $this->getUsers($project_id);
+ $groupMembers = $this->projectGroupRole->getUsers($project_id);
+ $members = array_merge($userMembers, $groupMembers);
+
+ foreach ($members as $user) {
+ if (! isset($users[$user['role']])) {
+ $users[$user['role']] = array();
+ }
+
+ $users[$user['role']][$user['id']] = $user['name'] ?: $user['username'];
+ }
+
+ return $users;
+ }
+
+ /**
+ * Get list of users that can be assigned to a task (only Manager and Member)
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAssignableUsers($project_id)
+ {
+ if ($this->projectPermission->isEverybodyAllowed($project_id)) {
+ return $this->user->getList();
+ }
+
+ $userMembers = $this->db->table(self::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')
+ ->join(User::TABLE, 'id', 'user_id')
+ ->eq('project_id', $project_id)
+ ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER))
+ ->findAll();
+
+ $groupMembers = $this->projectGroupRole->getAssignableUsers($project_id);
+ $members = array_merge($userMembers, $groupMembers);
+
+ return $this->user->prepareList($members);
+ }
+
+ /**
+ * Get list of users that can be assigned to a task (only Manager and Member)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param bool $unassigned Prepend the 'Unassigned' value
+ * @param bool $everybody Prepend the 'Everbody' value
+ * @param bool $singleUser If there is only one user return only this user
+ * @return array
+ */
+ public function getAssignableUsersList($project_id, $unassigned = true, $everybody = false, $singleUser = false)
+ {
+ $users = $this->getAssignableUsers($project_id);
+
+ if ($singleUser && count($users) === 1) {
+ return $users;
+ }
+
+ if ($unassigned) {
+ $users = array(t('Unassigned')) + $users;
+ }
+
+ if ($everybody) {
+ $users = array(User::EVERYBODY_ID => t('Everybody')) + $users;
+ }
+
+ return $users;
+ }
+
+ /**
+ * Add a user to the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @param string $role
+ * @return boolean
+ */
+ public function addUser($project_id, $user_id, $role)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'user_id' => $user_id,
+ 'project_id' => $project_id,
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Remove a user from the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function removeUser($project_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->remove();
+ }
+
+ /**
+ * Change a user role for the project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $user_id
+ * @param string $role
+ * @return boolean
+ */
+ public function changeUserRole($project_id, $user_id, $role)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('user_id', $user_id)
+ ->eq('project_id', $project_id)
+ ->update(array(
+ 'role' => $role,
+ ));
+ }
+
+ /**
+ * Copy user access from a project to another one
+ *
+ * @param integer $project_src_id Project Template
+ * @return integer $project_dst_id Project that receives the copy
+ * @return boolean
+ */
+ public function duplicate($project_src_id, $project_dst_id)
+ {
+ $rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll();
+
+ foreach ($rows as $row) {
+ $result = $this->db->table(self::TABLE)->save(array(
+ 'project_id' => $project_dst_id,
+ 'user_id' => $row['user_id'],
+ 'role' => $row['role'],
+ ));
+
+ if (! $result) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/app/Model/RememberMeSession.php b/app/Model/RememberMeSession.php
new file mode 100644
index 00000000..8989a6d7
--- /dev/null
+++ b/app/Model/RememberMeSession.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Kanboard\Core\Security\Token;
+
+/**
+ * Remember Me Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class RememberMeSession extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'remember_me';
+
+ /**
+ * Expiration (60 days)
+ *
+ * @var integer
+ */
+ const EXPIRATION = 5184000;
+
+ /**
+ * Get a remember me record
+ *
+ * @access public
+ * @param $token
+ * @param $sequence
+ * @return mixed
+ */
+ public function find($token, $sequence)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('token', $token)
+ ->eq('sequence', $sequence)
+ ->gt('expiration', time())
+ ->findOne();
+ }
+
+ /**
+ * Get all sessions for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getAll($user_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('user_id', $user_id)
+ ->desc('date_creation')
+ ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
+ ->findAll();
+ }
+
+ /**
+ * Create a new RememberMe session
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param string $ip IP Address
+ * @param string $user_agent User Agent
+ * @return array
+ */
+ public function create($user_id, $ip, $user_agent)
+ {
+ $token = hash('sha256', $user_id.$user_agent.$ip.Token::getToken());
+ $sequence = Token::getToken();
+ $expiration = time() + self::EXPIRATION;
+
+ $this->cleanup($user_id);
+
+ $this
+ ->db
+ ->table(self::TABLE)
+ ->insert(array(
+ 'user_id' => $user_id,
+ 'ip' => $ip,
+ 'user_agent' => $user_agent,
+ 'token' => $token,
+ 'sequence' => $sequence,
+ 'expiration' => $expiration,
+ 'date_creation' => time(),
+ ));
+
+ return array(
+ 'token' => $token,
+ 'sequence' => $sequence,
+ 'expiration' => $expiration,
+ );
+ }
+
+ /**
+ * Remove a session record
+ *
+ * @access public
+ * @param integer $session_id Session id
+ * @return mixed
+ */
+ public function remove($session_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $session_id)
+ ->remove();
+ }
+
+ /**
+ * Remove old sessions for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return bool
+ */
+ public function cleanup($user_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('user_id', $user_id)
+ ->lt('expiration', time())
+ ->remove();
+ }
+
+ /**
+ * Return a new sequence token and update the database
+ *
+ * @access public
+ * @param string $token Session token
+ * @return string
+ */
+ public function updateSequence($token)
+ {
+ $sequence = Token::getToken();
+
+ $this
+ ->db
+ ->table(self::TABLE)
+ ->eq('token', $token)
+ ->update(array('sequence' => $sequence));
+
+ return $sequence;
+ }
+}
diff --git a/app/Model/TaskPermission.php b/app/Model/TaskPermission.php
index 4bbe6d1d..fac2153e 100644
--- a/app/Model/TaskPermission.php
+++ b/app/Model/TaskPermission.php
@@ -2,6 +2,8 @@
namespace Kanboard\Model;
+use Kanboard\Core\Security\Role;
+
/**
* Task permission model
*
@@ -20,7 +22,7 @@ class TaskPermission extends Base
*/
public function canRemoveTask(array $task)
{
- if ($this->userSession->isAdmin() || $this->projectPermission->isManager($task['project_id'], $this->userSession->getId())) {
+ if ($this->userSession->isAdmin() || $this->projectUserRole->getUserRole($task['project_id'], $this->userSession->getId()) === Role::PROJECT_MANAGER) {
return true;
} elseif (isset($task['creator_id']) && $task['creator_id'] == $this->userSession->getId()) {
return true;
diff --git a/app/Model/User.php b/app/Model/User.php
index 88361ce8..7142c258 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -7,6 +7,7 @@ use SimpleValidator\Validator;
use SimpleValidator\Validators;
use Kanboard\Core\Session\SessionManager;
use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
/**
* User model
@@ -57,8 +58,7 @@ class User extends Base
'username',
'name',
'email',
- 'is_admin',
- 'is_project_admin',
+ 'role',
'is_ldap_user',
'notifications_enabled',
'google_id',
@@ -91,7 +91,7 @@ class User extends Base
$this->db
->table(User::TABLE)
->eq('id', $user_id)
- ->eq('is_admin', 1)
+ ->eq('role', Role::APP_ADMIN)
->exists();
}
@@ -111,48 +111,17 @@ class User extends Base
* Get a specific user by the Google id
*
* @access public
- * @param string $google_id Google unique id
+ * @param string $column
+ * @param string $id
* @return array|boolean
*/
- public function getByGoogleId($google_id)
+ public function getByExternalId($column, $id)
{
- if (empty($google_id)) {
+ if (empty($id)) {
return false;
}
- return $this->db->table(self::TABLE)->eq('google_id', $google_id)->findOne();
- }
-
- /**
- * Get a specific user by the Github id
- *
- * @access public
- * @param string $github_id Github user id
- * @return array|boolean
- */
- public function getByGithubId($github_id)
- {
- if (empty($github_id)) {
- return false;
- }
-
- return $this->db->table(self::TABLE)->eq('github_id', $github_id)->findOne();
- }
-
- /**
- * Get a specific user by the Gitlab id
- *
- * @access public
- * @param string $gitlab_id Gitlab user id
- * @return array|boolean
- */
- public function getByGitlabId($gitlab_id)
- {
- if (empty($gitlab_id)) {
- return false;
- }
-
- return $this->db->table(self::TABLE)->eq('gitlab_id', $gitlab_id)->findOne();
+ return $this->db->table(self::TABLE)->eq($column, $id)->findOne();
}
/**
@@ -289,7 +258,7 @@ class User extends Base
}
$this->removeFields($values, array('confirmation', 'current_password'));
- $this->resetFields($values, array('is_admin', 'is_ldap_user', 'is_project_admin', 'disable_login_form'));
+ $this->resetFields($values, array('is_ldap_user', 'disable_login_form'));
$this->convertNullFields($values, array('gitlab_id'));
$this->convertIntegerFields($values, array('gitlab_id'));
}
@@ -355,10 +324,10 @@ class User extends Base
// All private projects are removed
$project_ids = $db->table(Project::TABLE)
- ->eq('is_private', 1)
- ->eq(ProjectPermission::TABLE.'.user_id', $user_id)
- ->join(ProjectPermission::TABLE, 'project_id', 'id')
- ->findAllByColumn(Project::TABLE.'.id');
+ ->eq('is_private', 1)
+ ->eq(ProjectUserRole::TABLE.'.user_id', $user_id)
+ ->join(ProjectUserRole::TABLE, 'project_id', 'id')
+ ->findAllByColumn(Project::TABLE.'.id');
if (! empty($project_ids)) {
$db->table(Project::TABLE)->in('id', $project_ids)->remove();
@@ -402,71 +371,6 @@ class User extends Base
}
/**
- * Get the number of failed login for the user
- *
- * @access public
- * @param string $username
- * @return integer
- */
- public function getFailedLogin($username)
- {
- return (int) $this->db->table(self::TABLE)->eq('username', $username)->findOneColumn('nb_failed_login');
- }
-
- /**
- * Reset to 0 the counter of failed login
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function resetFailedLogin($username)
- {
- return $this->db->table(self::TABLE)->eq('username', $username)->update(array('nb_failed_login' => 0, 'lock_expiration_date' => 0));
- }
-
- /**
- * Increment failed login counter
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function incrementFailedLogin($username)
- {
- return $this->db->execute('UPDATE '.self::TABLE.' SET nb_failed_login=nb_failed_login+1 WHERE username=?', array($username)) !== false;
- }
-
- /**
- * Check if the account is locked
- *
- * @access public
- * @param string $username
- * @return boolean
- */
- public function isLocked($username)
- {
- return $this->db->table(self::TABLE)
- ->eq('username', $username)
- ->neq('lock_expiration_date', 0)
- ->gte('lock_expiration_date', time())
- ->exists();
- }
-
- /**
- * Lock the account for the specified duration
- *
- * @access public
- * @param string $username Username
- * @param integer $duration Duration in minutes
- * @return boolean
- */
- public function lock($username, $duration = 15)
- {
- return $this->db->table(self::TABLE)->eq('username', $username)->update(array('lock_expiration_date' => time() + $duration * 60));
- }
-
- /**
* Common validation rules
*
* @access private
@@ -475,11 +379,10 @@ class User extends Base
private function commonValidationRules()
{
return array(
+ new Validators\MaxLength('role', t('The maximum length is %d characters', 25), 25),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'),
new Validators\Email('email', t('Email address invalid')),
- new Validators\Integer('is_admin', t('This value must be an integer')),
- new Validators\Integer('is_project_admin', t('This value must be an integer')),
new Validators\Integer('is_ldap_user', t('This value must be an integer')),
);
}
@@ -585,9 +488,7 @@ class User extends Base
$v = new Validator($values, array_merge($rules, $this->commonPasswordValidationRules()));
if ($v->execute()) {
-
- // Check password
- if ($this->authentication->authenticate($this->userSession->getUsername(), $values['current_password'])) {
+ if ($this->authenticationManager->passwordAuthentication($this->userSession->getUsername(), $values['current_password'], false)) {
return array(true, array());
} else {
return array(false, array('current_password' => array(t('Wrong password'))));
diff --git a/app/Model/UserFilter.php b/app/Model/UserFilter.php
new file mode 100644
index 00000000..ff546e96
--- /dev/null
+++ b/app/Model/UserFilter.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * User Filter
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class UserFilter extends Base
+{
+ /**
+ * Search query
+ *
+ * @access private
+ * @var string
+ */
+ private $input;
+
+ /**
+ * Query
+ *
+ * @access protected
+ * @var \PicoDb\Table
+ */
+ protected $query;
+
+ /**
+ * Initialize filter
+ *
+ * @access public
+ * @param string $input
+ * @return UserFilter
+ */
+ public function create($input)
+ {
+ $this->query = $this->db->table(User::TABLE);
+ $this->input = $input;
+ return $this;
+ }
+
+ /**
+ * Filter users by name or username
+ *
+ * @access public
+ * @return UserFilter
+ */
+ public function filterByUsernameOrByName()
+ {
+ $this->query->beginOr()
+ ->ilike('username', '%'.$this->input.'%')
+ ->ilike('name', '%'.$this->input.'%')
+ ->closeOr();
+
+ return $this;
+ }
+
+ /**
+ * Get all results of the filter
+ *
+ * @access public
+ * @return array
+ */
+ public function findAll()
+ {
+ return $this->query->findAll();
+ }
+
+ /**
+ * Get the PicoDb query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+}
diff --git a/app/Model/UserImport.php b/app/Model/UserImport.php
index 3c9e7a57..0ec4e802 100644
--- a/app/Model/UserImport.php
+++ b/app/Model/UserImport.php
@@ -4,6 +4,7 @@ namespace Kanboard\Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
+use Kanboard\Core\Security\Role;
use Kanboard\Core\Csv;
/**
@@ -36,7 +37,7 @@ class UserImport extends Base
'email' => 'Email',
'name' => 'Full Name',
'is_admin' => 'Administrator',
- 'is_project_admin' => 'Project Administrator',
+ 'is_manager' => 'Manager',
'is_ldap_user' => 'Remote User',
);
}
@@ -75,10 +76,21 @@ class UserImport extends Base
{
$row['username'] = strtolower($row['username']);
- foreach (array('is_admin', 'is_project_admin', 'is_ldap_user') as $field) {
+ foreach (array('is_admin', 'is_manager', 'is_ldap_user') as $field) {
$row[$field] = Csv::getBooleanValue($row[$field]);
}
+ if ($row['is_admin'] == 1) {
+ $row['role'] = Role::APP_ADMIN;
+ } elseif ($row['is_manager'] == 1) {
+ $row['role'] = Role::APP_MANAGER;
+ } else {
+ $row['role'] = Role::APP_USER;
+ }
+
+ unset($row['is_admin']);
+ unset($row['is_manager']);
+
$this->removeEmptyFields($row, array('password', 'email', 'name'));
return $row;
@@ -98,8 +110,6 @@ class UserImport extends Base
new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), User::TABLE, 'id'),
new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
new Validators\Email('email', t('Email address invalid')),
- new Validators\Integer('is_admin', t('This value must be an integer')),
- new Validators\Integer('is_project_admin', t('This value must be an integer')),
new Validators\Integer('is_ldap_user', t('This value must be an integer')),
));
diff --git a/app/Model/UserLocking.php b/app/Model/UserLocking.php
new file mode 100644
index 00000000..67e4c244
--- /dev/null
+++ b/app/Model/UserLocking.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * User Locking Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class UserLocking extends Base
+{
+ /**
+ * Get the number of failed login for the user
+ *
+ * @access public
+ * @param string $username
+ * @return integer
+ */
+ public function getFailedLogin($username)
+ {
+ return (int) $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->findOneColumn('nb_failed_login');
+ }
+
+ /**
+ * Reset to 0 the counter of failed login
+ *
+ * @access public
+ * @param string $username
+ * @return boolean
+ */
+ public function resetFailedLogin($username)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->update(array(
+ 'nb_failed_login' => 0,
+ 'lock_expiration_date' => 0,
+ ));
+ }
+
+ /**
+ * Increment failed login counter
+ *
+ * @access public
+ * @param string $username
+ * @return boolean
+ */
+ public function incrementFailedLogin($username)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->increment('nb_failed_login', 1);
+ }
+
+ /**
+ * Check if the account is locked
+ *
+ * @access public
+ * @param string $username
+ * @return boolean
+ */
+ public function isLocked($username)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->neq('lock_expiration_date', 0)
+ ->gte('lock_expiration_date', time())
+ ->exists();
+ }
+
+ /**
+ * Lock the account for the specified duration
+ *
+ * @access public
+ * @param string $username Username
+ * @param integer $duration Duration in minutes
+ * @return boolean
+ */
+ public function lock($username, $duration = 15)
+ {
+ return $this->db->table(User::TABLE)
+ ->eq('username', $username)
+ ->update(array(
+ 'lock_expiration_date' => time() + $duration * 60
+ ));
+ }
+
+ /**
+ * Return true if the captcha must be shown
+ *
+ * @access public
+ * @param string $username
+ * @param integer $tries
+ * @return boolean
+ */
+ public function hasCaptcha($username, $tries = BRUTEFORCE_CAPTCHA)
+ {
+ return $this->getFailedLogin($username) >= $tries;
+ }
+}
diff --git a/app/Model/UserNotification.php b/app/Model/UserNotification.php
index 3d98ebe9..e00f23c5 100644
--- a/app/Model/UserNotification.php
+++ b/app/Model/UserNotification.php
@@ -155,7 +155,7 @@ class UserNotification extends Base
private function getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id)
{
return $this->db
- ->table(ProjectPermission::TABLE)
+ ->table(ProjectUserRole::TABLE)
->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter')
->join(User::TABLE, 'id', 'user_id')
->eq('project_id', $project_id)
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 5a451c77..ac97e224 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -4,8 +4,67 @@ namespace Schema;
use PDO;
use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
-const VERSION = 95;
+const VERSION = 97;
+
+function version_97(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE `users` ADD COLUMN `role` VARCHAR(25) NOT NULL DEFAULT '".Role::APP_USER."'");
+
+ $rq = $pdo->prepare('SELECT * FROM `users`');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE `users` SET `role`=? WHERE `id`=?');
+
+ foreach ($rows as $row) {
+ $role = Role::APP_USER;
+
+ if ($row['is_admin'] == 1) {
+ $role = Role::APP_ADMIN;
+ } else if ($row['is_project_admin']) {
+ $role = Role::APP_MANAGER;
+ }
+
+ $rq->execute(array($role, $row['id']));
+ }
+
+ $pdo->exec('ALTER TABLE `users` DROP COLUMN `is_admin`');
+ $pdo->exec('ALTER TABLE `users` DROP COLUMN `is_project_admin`');
+}
+
+function version_96(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_has_groups (
+ `group_id` INT NOT NULL,
+ `project_id` INT NOT NULL,
+ `role` VARCHAR(25) NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ UNIQUE(group_id, project_id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+
+ $pdo->exec("ALTER TABLE `project_has_users` ADD COLUMN `role` VARCHAR(25) NOT NULL DEFAULT '".Role::PROJECT_VIEWER."'");
+
+ $rq = $pdo->prepare('SELECT * FROM project_has_users');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE `project_has_users` SET `role`=? WHERE `id`=?');
+
+ foreach ($rows as $row) {
+ $rq->execute(array(
+ $row['is_owner'] == 1 ? Role::PROJECT_MANAGER : Role::PROJECT_MEMBER,
+ $row['id'],
+ ));
+ }
+
+ $pdo->exec('ALTER TABLE `project_has_users` DROP COLUMN `is_owner`');
+ $pdo->exec('ALTER TABLE `project_has_users` DROP COLUMN `id`');
+}
function version_95(PDO $pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index a3887cfb..66d9acc1 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -4,8 +4,67 @@ namespace Schema;
use PDO;
use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
-const VERSION = 75;
+const VERSION = 77;
+
+function version_77(PDO $pdo)
+{
+ $pdo->exec('ALTER TABLE "users" ADD COLUMN "role" VARCHAR(25) NOT NULL DEFAULT \''.Role::APP_USER.'\'');
+
+ $rq = $pdo->prepare('SELECT * FROM "users"');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE "users" SET "role"=? WHERE "id"=?');
+
+ foreach ($rows as $row) {
+ $role = Role::APP_USER;
+
+ if ($row['is_admin'] == 1) {
+ $role = Role::APP_ADMIN;
+ } else if ($row['is_project_admin']) {
+ $role = Role::APP_MANAGER;
+ }
+
+ $rq->execute(array($role, $row['id']));
+ }
+
+ $pdo->exec('ALTER TABLE users DROP COLUMN "is_admin"');
+ $pdo->exec('ALTER TABLE users DROP COLUMN "is_project_admin"');
+}
+
+function version_76(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_has_groups (
+ group_id INTEGER NOT NULL,
+ project_id INTEGER NOT NULL,
+ role VARCHAR(25) NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ UNIQUE(group_id, project_id)
+ )
+ ");
+
+ $pdo->exec("ALTER TABLE project_has_users ADD COLUMN role VARCHAR(25) NOT NULL DEFAULT '".Role::PROJECT_VIEWER."'");
+
+ $rq = $pdo->prepare('SELECT * FROM project_has_users');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE project_has_users SET "role"=? WHERE "id"=?');
+
+ foreach ($rows as $row) {
+ $rq->execute(array(
+ $row['is_owner'] == 1 ? Role::PROJECT_MANAGER : Role::PROJECT_MEMBER,
+ $row['id'],
+ ));
+ }
+
+ $pdo->exec('ALTER TABLE project_has_users DROP COLUMN "is_owner"');
+ $pdo->exec('ALTER TABLE project_has_users DROP COLUMN "id"');
+}
function version_75(PDO $pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index f0510cff..534c3f3a 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -3,9 +3,33 @@
namespace Schema;
use Kanboard\Core\Security\Token;
+use Kanboard\Core\Security\Role;
use PDO;
-const VERSION = 89;
+const VERSION = 91;
+
+function version_91(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT '".Role::APP_USER."'");
+
+ $rq = $pdo->prepare('SELECT * FROM users');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE users SET "role"=? WHERE "id"=?');
+
+ foreach ($rows as $row) {
+ $role = Role::APP_USER;
+
+ if ($row['is_admin'] == 1) {
+ $role = Role::APP_ADMIN;
+ } else if ($row['is_project_admin']) {
+ $role = Role::APP_MANAGER;
+ }
+
+ $rq->execute(array($role, $row['id']));
+ }
+}
function version_90(PDO $pdo)
{
@@ -19,6 +43,21 @@ function version_90(PDO $pdo)
UNIQUE(group_id, project_id)
)
");
+
+ $pdo->exec("ALTER TABLE project_has_users ADD COLUMN role TEXT NOT NULL DEFAULT '".Role::PROJECT_VIEWER."'");
+
+ $rq = $pdo->prepare('SELECT * FROM project_has_users');
+ $rq->execute();
+ $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array();
+
+ $rq = $pdo->prepare('UPDATE project_has_users SET "role"=? WHERE "id"=?');
+
+ foreach ($rows as $row) {
+ $rq->execute(array(
+ $row['is_owner'] == 1 ? Role::PROJECT_MANAGER : Role::PROJECT_MEMBER,
+ $row['id'],
+ ));
+ }
}
function version_89(PDO $pdo)
@@ -1004,7 +1043,6 @@ function version_7(PDO $pdo)
{
$pdo->exec("
CREATE TABLE project_has_users (
- id INTEGER PRIMARY KEY,
project_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php
new file mode 100644
index 00000000..8600d96e
--- /dev/null
+++ b/app/ServiceProvider/AuthenticationProvider.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\Security\AuthenticationManager;
+use Kanboard\Core\Security\AccessMap;
+use Kanboard\Core\Security\Authorization;
+use Kanboard\Core\Security\Role;
+use Kanboard\Auth\RememberMeAuth;
+use Kanboard\Auth\DatabaseAuth;
+use Kanboard\Auth\LdapAuth;
+use Kanboard\Auth\GitlabAuth;
+use Kanboard\Auth\GithubAuth;
+use Kanboard\Auth\GoogleAuth;
+use Kanboard\Auth\TotpAuth;
+use Kanboard\Auth\ReverseProxyAuth;
+
+/**
+ * Authentication Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class AuthenticationProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['authenticationManager'] = new AuthenticationManager($container);
+ $container['authenticationManager']->register(new TotpAuth($container));
+ $container['authenticationManager']->register(new RememberMeAuth($container));
+ $container['authenticationManager']->register(new DatabaseAuth($container));
+
+ if (REVERSE_PROXY_AUTH) {
+ $container['authenticationManager']->register(new ReverseProxyAuth($container));
+ }
+
+ if (LDAP_AUTH) {
+ $container['authenticationManager']->register(new LdapAuth($container));
+ }
+
+ if (GITLAB_AUTH) {
+ $container['authenticationManager']->register(new GitlabAuth($container));
+ }
+
+ if (GITHUB_AUTH) {
+ $container['authenticationManager']->register(new GithubAuth($container));
+ }
+
+ if (GOOGLE_AUTH) {
+ $container['authenticationManager']->register(new GoogleAuth($container));
+ }
+
+ $container['projectAccessMap'] = $this->getProjectAccessMap();
+ $container['applicationAccessMap'] = $this->getApplicationAccessMap();
+
+ $container['projectAuthorization'] = new Authorization($container['projectAccessMap']);
+ $container['applicationAuthorization'] = new Authorization($container['applicationAccessMap']);
+
+ return $container;
+ }
+
+ /**
+ * Get ACL for projects
+ *
+ * @access public
+ * @return AccessMap
+ */
+ public function getProjectAccessMap()
+ {
+ $acl = new AccessMap;
+ $acl->setDefaultRole(Role::PROJECT_VIEWER);
+ $acl->setRoleHierarchy(Role::PROJECT_MANAGER, array(Role::PROJECT_MEMBER, Role::PROJECT_VIEWER));
+ $acl->setRoleHierarchy(Role::PROJECT_MEMBER, array(Role::PROJECT_VIEWER));
+
+ $acl->add('Action', '*', Role::PROJECT_MANAGER);
+ $acl->add('Analytic', '*', Role::PROJECT_MANAGER);
+ $acl->add('Board', 'save', Role::PROJECT_MEMBER);
+ $acl->add('BoardPopover', '*', Role::PROJECT_MEMBER);
+ $acl->add('Calendar', 'save', Role::PROJECT_MEMBER);
+ $acl->add('Category', '*', Role::PROJECT_MANAGER);
+ $acl->add('Column', '*', Role::PROJECT_MANAGER);
+ $acl->add('Comment', '*', Role::PROJECT_MEMBER);
+ $acl->add('Customfilter', '*', Role::PROJECT_MEMBER);
+ $acl->add('Export', '*', Role::PROJECT_MANAGER);
+ $acl->add('File', array('screenshot', 'create', 'save', 'remove', 'confirm'), Role::PROJECT_MEMBER);
+ $acl->add('Gantt', '*', Role::PROJECT_MANAGER);
+ $acl->add('Project', array('share', 'integrations', 'notifications', 'edit', 'update', 'duplicate', 'disable', 'enable', 'remove'), Role::PROJECT_MANAGER);
+ $acl->add('ProjectPermission', '*', Role::PROJECT_MANAGER);
+ $acl->add('Projectuser', '*', Role::PROJECT_MANAGER);
+ $acl->add('Subtask', '*', Role::PROJECT_MEMBER);
+ $acl->add('Swimlane', '*', Role::PROJECT_MANAGER);
+ $acl->add('Task', 'remove', Role::PROJECT_MEMBER);
+ $acl->add('Taskcreation', '*', Role::PROJECT_MEMBER);
+ $acl->add('Taskduplication', '*', Role::PROJECT_MEMBER);
+ $acl->add('TaskImport', '*', Role::PROJECT_MANAGER);
+ $acl->add('Tasklink', '*', Role::PROJECT_MEMBER);
+ $acl->add('Taskmodification', '*', Role::PROJECT_MEMBER);
+ $acl->add('Taskstatus', '*', Role::PROJECT_MEMBER);
+ $acl->add('Timer', '*', Role::PROJECT_MEMBER);
+
+ return $acl;
+ }
+
+ /**
+ * Get ACL for the application
+ *
+ * @access public
+ * @return AccessMap
+ */
+ public function getApplicationAccessMap()
+ {
+ $acl = new AccessMap;
+ $acl->setDefaultRole(Role::APP_USER);
+ $acl->setRoleHierarchy(Role::APP_ADMIN, array(Role::APP_MANAGER, Role::APP_USER, Role::APP_PUBLIC));
+ $acl->setRoleHierarchy(Role::APP_MANAGER, array(Role::APP_USER, Role::APP_PUBLIC));
+ $acl->setRoleHierarchy(Role::APP_USER, array(Role::APP_PUBLIC));
+
+ $acl->add('Oauth', array('google', 'github', 'gitlab'), Role::APP_PUBLIC);
+ $acl->add('Auth', array('login', 'check', 'captcha'), Role::APP_PUBLIC);
+ $acl->add('Webhook', '*', Role::APP_PUBLIC);
+ $acl->add('Task', 'readonly', Role::APP_PUBLIC);
+ $acl->add('Board', 'readonly', Role::APP_PUBLIC);
+ $acl->add('Ical', '*', Role::APP_PUBLIC);
+ $acl->add('Feed', '*', Role::APP_PUBLIC);
+
+ $acl->add('Config', '*', Role::APP_ADMIN);
+ $acl->add('Currency', '*', Role::APP_ADMIN);
+ $acl->add('Gantt', '*', Role::APP_MANAGER);
+ $acl->add('Group', '*', Role::APP_ADMIN);
+ $acl->add('Link', '*', Role::APP_ADMIN);
+ $acl->add('Project', array('users', 'allowEverybody', 'allow', 'role', 'revoke', 'create'), Role::APP_MANAGER);
+ $acl->add('ProjectPermission', '*', Role::APP_MANAGER);
+ $acl->add('Projectuser', '*', Role::APP_MANAGER);
+ $acl->add('Twofactor', 'disable', Role::APP_ADMIN);
+ $acl->add('UserImport', '*', Role::APP_ADMIN);
+ $acl->add('User', array('index', 'create', 'save', 'authentication', 'remove'), Role::APP_ADMIN);
+
+ return $acl;
+ }
+}
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index 9ec81116..76fe70f6 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -5,23 +5,17 @@ namespace Kanboard\ServiceProvider;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use League\HTMLToMarkdown\HtmlConverter;
-use Kanboard\Core\Plugin\Loader;
use Kanboard\Core\Mail\Client as EmailClient;
use Kanboard\Core\ObjectStorage\FileStorage;
use Kanboard\Core\Paginator;
-use Kanboard\Core\OAuth2;
+use Kanboard\Core\Http\OAuth2;
use Kanboard\Core\Tool;
use Kanboard\Core\Http\Client as HttpClient;
-use Kanboard\Model\UserNotificationType;
-use Kanboard\Model\ProjectNotificationType;
-use Kanboard\Notification\Mail as MailNotification;
-use Kanboard\Notification\Web as WebNotification;
class ClassProvider implements ServiceProviderInterface
{
private $classes = array(
'Model' => array(
- 'Acl',
'Action',
'Authentication',
'Board',
@@ -47,6 +41,9 @@ class ClassProvider implements ServiceProviderInterface
'ProjectPermission',
'ProjectNotification',
'ProjectMetadata',
+ 'ProjectGroupRole',
+ 'ProjectUserRole',
+ 'RememberMeSession',
'Subtask',
'SubtaskExport',
'SubtaskTimeTracking',
@@ -69,7 +66,7 @@ class ClassProvider implements ServiceProviderInterface
'Transition',
'User',
'UserImport',
- 'UserSession',
+ 'UserLocking',
'UserNotification',
'UserNotificationType',
'UserNotificationFilter',
@@ -82,6 +79,8 @@ class ClassProvider implements ServiceProviderInterface
'TaskFilterCalendarFormatter',
'TaskFilterICalendarFormatter',
'ProjectGanttFormatter',
+ 'UserFilterAutoCompleteFormatter',
+ 'GroupAutoCompleteFormatter',
),
'Core' => array(
'DateParser',
@@ -92,7 +91,7 @@ class ClassProvider implements ServiceProviderInterface
'Core\Http' => array(
'Request',
'Response',
- 'Router',
+ 'RememberMeCookie',
),
'Core\Cache' => array(
'MemoryCache',
@@ -102,6 +101,13 @@ class ClassProvider implements ServiceProviderInterface
),
'Core\Security' => array(
'Token',
+ 'Role',
+ ),
+ 'Core\User' => array(
+ 'GroupSync',
+ 'UserSync',
+ 'UserSession',
+ 'UserProfile',
),
'Integration' => array(
'BitbucketWebhook',
@@ -142,22 +148,6 @@ class ClassProvider implements ServiceProviderInterface
return $mailer;
};
- $container['userNotificationType'] = function ($container) {
- $type = new UserNotificationType($container);
- $type->setType(MailNotification::TYPE, t('Email'), '\Kanboard\Notification\Mail');
- $type->setType(WebNotification::TYPE, t('Web'), '\Kanboard\Notification\Web');
- return $type;
- };
-
- $container['projectNotificationType'] = function ($container) {
- $type = new ProjectNotificationType($container);
- $type->setType('webhook', 'Webhook', '\Kanboard\Notification\Webhook', true);
- $type->setType('activity_stream', 'ActivityStream', '\Kanboard\Notification\ActivityStream', true);
- return $type;
- };
-
- $container['pluginLoader'] = new Loader($container);
-
$container['cspRules'] = array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '* data:');
return $container;
diff --git a/app/ServiceProvider/GroupProvider.php b/app/ServiceProvider/GroupProvider.php
new file mode 100644
index 00000000..dff4b23a
--- /dev/null
+++ b/app/ServiceProvider/GroupProvider.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\Group\GroupManager;
+use Kanboard\Group\DatabaseBackendGroupProvider;
+use Kanboard\Group\LdapBackendGroupProvider;
+
+/**
+ * Group Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class GroupProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['groupManager'] = new GroupManager;
+ $container['groupManager']->register(new DatabaseBackendGroupProvider($container));
+
+ if (LDAP_AUTH && LDAP_GROUP_PROVIDER) {
+ $container['groupManager']->register(new LdapBackendGroupProvider($container));
+ }
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/NotificationProvider.php b/app/ServiceProvider/NotificationProvider.php
new file mode 100644
index 00000000..83daf65d
--- /dev/null
+++ b/app/ServiceProvider/NotificationProvider.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Model\UserNotificationType;
+use Kanboard\Model\ProjectNotificationType;
+use Kanboard\Notification\Mail as MailNotification;
+use Kanboard\Notification\Web as WebNotification;
+
+/**
+ * Notification Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class NotificationProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['userNotificationType'] = function ($container) {
+ $type = new UserNotificationType($container);
+ $type->setType(MailNotification::TYPE, t('Email'), '\Kanboard\Notification\Mail');
+ $type->setType(WebNotification::TYPE, t('Web'), '\Kanboard\Notification\Web');
+ return $type;
+ };
+
+ $container['projectNotificationType'] = function ($container) {
+ $type = new ProjectNotificationType($container);
+ $type->setType('webhook', 'Webhook', '\Kanboard\Notification\Webhook', true);
+ $type->setType('activity_stream', 'ActivityStream', '\Kanboard\Notification\ActivityStream', true);
+ return $type;
+ };
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/PluginProvider.php b/app/ServiceProvider/PluginProvider.php
new file mode 100644
index 00000000..d2f1666b
--- /dev/null
+++ b/app/ServiceProvider/PluginProvider.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\Plugin\Loader;
+
+/**
+ * Plugin Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class PluginProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['pluginLoader'] = new Loader($container);
+ $container['pluginLoader']->scan();
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php
new file mode 100644
index 00000000..60ed161c
--- /dev/null
+++ b/app/ServiceProvider/RouteProvider.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\Http\Router;
+
+/**
+ * Route Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class RouteProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['router'] = new Router($container);
+
+ if (ENABLE_URL_REWRITE) {
+ // Dashboard
+ $container['router']->addRoute('dashboard', 'app', 'index');
+ $container['router']->addRoute('dashboard/:user_id', 'app', 'index', array('user_id'));
+ $container['router']->addRoute('dashboard/:user_id/projects', 'app', 'projects', array('user_id'));
+ $container['router']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks', array('user_id'));
+ $container['router']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks', array('user_id'));
+ $container['router']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar', array('user_id'));
+ $container['router']->addRoute('dashboard/:user_id/activity', 'app', 'activity', array('user_id'));
+
+ // Search routes
+ $container['router']->addRoute('search', 'search', 'index');
+ $container['router']->addRoute('search/:search', 'search', 'index', array('search'));
+
+ // Project routes
+ $container['router']->addRoute('projects', 'project', 'index');
+ $container['router']->addRoute('project/create', 'project', 'create');
+ $container['router']->addRoute('project/create/:private', 'project', 'create', array('private'));
+ $container['router']->addRoute('project/:project_id', 'project', 'show', array('project_id'));
+ $container['router']->addRoute('p/:project_id', 'project', 'show', array('project_id'));
+ $container['router']->addRoute('project/:project_id/customer-filter', 'customfilter', 'index', array('project_id'));
+ $container['router']->addRoute('project/:project_id/share', 'project', 'share', array('project_id'));
+ $container['router']->addRoute('project/:project_id/notifications', 'project', 'notifications', array('project_id'));
+ $container['router']->addRoute('project/:project_id/edit', 'project', 'edit', array('project_id'));
+ $container['router']->addRoute('project/:project_id/integrations', 'project', 'integrations', array('project_id'));
+ $container['router']->addRoute('project/:project_id/duplicate', 'project', 'duplicate', array('project_id'));
+ $container['router']->addRoute('project/:project_id/remove', 'project', 'remove', array('project_id'));
+ $container['router']->addRoute('project/:project_id/disable', 'project', 'disable', array('project_id'));
+ $container['router']->addRoute('project/:project_id/enable', 'project', 'enable', array('project_id'));
+ $container['router']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index', array('project_id'));
+ $container['router']->addRoute('project/:project_id/import', 'taskImport', 'step1', array('project_id'));
+
+ // Action routes
+ $container['router']->addRoute('project/:project_id/actions', 'action', 'index', array('project_id'));
+ $container['router']->addRoute('project/:project_id/action/:action_id/confirm', 'action', 'confirm', array('project_id', 'action_id'));
+
+ // Column routes
+ $container['router']->addRoute('project/:project_id/columns', 'column', 'index', array('project_id'));
+ $container['router']->addRoute('project/:project_id/column/:column_id/edit', 'column', 'edit', array('project_id', 'column_id'));
+ $container['router']->addRoute('project/:project_id/column/:column_id/confirm', 'column', 'confirm', array('project_id', 'column_id'));
+ $container['router']->addRoute('project/:project_id/column/:column_id/move/:direction', 'column', 'move', array('project_id', 'column_id', 'direction'));
+
+ // Swimlane routes
+ $container['router']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index', array('project_id'));
+ $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/edit', 'swimlane', 'edit', array('project_id', 'swimlane_id'));
+ $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/confirm', 'swimlane', 'confirm', array('project_id', 'swimlane_id'));
+ $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/disable', 'swimlane', 'disable', array('project_id', 'swimlane_id'));
+ $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/enable', 'swimlane', 'enable', array('project_id', 'swimlane_id'));
+ $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/up', 'swimlane', 'moveup', array('project_id', 'swimlane_id'));
+ $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/down', 'swimlane', 'movedown', array('project_id', 'swimlane_id'));
+
+ // Category routes
+ $container['router']->addRoute('project/:project_id/categories', 'category', 'index', array('project_id'));
+ $container['router']->addRoute('project/:project_id/category/:category_id/edit', 'category', 'edit', array('project_id', 'category_id'));
+ $container['router']->addRoute('project/:project_id/category/:category_id/confirm', 'category', 'confirm', array('project_id', 'category_id'));
+
+ // Task routes
+ $container['router']->addRoute('project/:project_id/task/:task_id', 'task', 'show', array('project_id', 'task_id'));
+ $container['router']->addRoute('t/:task_id', 'task', 'show', array('task_id'));
+ $container['router']->addRoute('public/task/:task_id/:token', 'task', 'readonly', array('task_id', 'token'));
+
+ $container['router']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task', array('project_id', 'task_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot', array('project_id', 'task_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create', array('project_id', 'task_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create', array('project_id', 'task_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create', array('project_id', 'task_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions', array('project_id', 'task_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics', array('project_id', 'task_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove', array('project_id', 'task_id'));
+
+ $container['router']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit', array('project_id', 'task_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description', array('project_id', 'task_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence', array('project_id', 'task_id'));
+
+ $container['router']->addRoute('project/:project_id/task/:task_id/close', 'taskstatus', 'close', array('task_id', 'project_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/open', 'taskstatus', 'open', array('task_id', 'project_id'));
+
+ $container['router']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate', array('task_id', 'project_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy', array('task_id', 'project_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy', array('task_id', 'project_id', 'dst_project_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move', array('task_id', 'project_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move', array('task_id', 'project_id', 'dst_project_id'));
+
+ // Board routes
+ $container['router']->addRoute('board/:project_id', 'board', 'show', array('project_id'));
+ $container['router']->addRoute('b/:project_id', 'board', 'show', array('project_id'));
+ $container['router']->addRoute('public/board/:token', 'board', 'readonly', array('token'));
+
+ // Calendar routes
+ $container['router']->addRoute('calendar/:project_id', 'calendar', 'show', array('project_id'));
+ $container['router']->addRoute('c/:project_id', 'calendar', 'show', array('project_id'));
+
+ // Listing routes
+ $container['router']->addRoute('list/:project_id', 'listing', 'show', array('project_id'));
+ $container['router']->addRoute('l/:project_id', 'listing', 'show', array('project_id'));
+
+ // Gantt routes
+ $container['router']->addRoute('gantt/:project_id', 'gantt', 'project', array('project_id'));
+ $container['router']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project', array('project_id', 'sorting'));
+
+ // Subtask routes
+ $container['router']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create', array('project_id', 'task_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm', array('project_id', 'task_id', 'subtask_id'));
+ $container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/edit', 'subtask', 'edit', array('project_id', 'task_id', 'subtask_id'));
+
+ // Feed routes
+ $container['router']->addRoute('feed/project/:token', 'feed', 'project', array('token'));
+ $container['router']->addRoute('feed/user/:token', 'feed', 'user', array('token'));
+
+ // Ical routes
+ $container['router']->addRoute('ical/project/:token', 'ical', 'project', array('token'));
+ $container['router']->addRoute('ical/user/:token', 'ical', 'user', array('token'));
+
+ // Auth routes
+ $container['router']->addRoute('oauth/google', 'oauth', 'google');
+ $container['router']->addRoute('oauth/github', 'oauth', 'github');
+ $container['router']->addRoute('oauth/gitlab', 'oauth', 'gitlab');
+ $container['router']->addRoute('login', 'auth', 'login');
+ $container['router']->addRoute('logout', 'auth', 'logout');
+ }
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/SessionProvider.php b/app/ServiceProvider/SessionProvider.php
index 414d9578..0999d531 100644
--- a/app/ServiceProvider/SessionProvider.php
+++ b/app/ServiceProvider/SessionProvider.php
@@ -8,8 +8,21 @@ use Kanboard\Core\Session\SessionManager;
use Kanboard\Core\Session\SessionStorage;
use Kanboard\Core\Session\FlashMessage;
+/**
+ * Session Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
class SessionProvider implements ServiceProviderInterface
{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
public function register(Container $container)
{
$container['sessionStorage'] = function() {
diff --git a/app/Subscriber/AuthSubscriber.php b/app/Subscriber/AuthSubscriber.php
index 77a39942..a0e0be63 100644
--- a/app/Subscriber/AuthSubscriber.php
+++ b/app/Subscriber/AuthSubscriber.php
@@ -2,26 +2,100 @@
namespace Kanboard\Subscriber;
-use Kanboard\Core\Http\Request;
-use Kanboard\Event\AuthEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Kanboard\Core\Base;
+use Kanboard\Core\Security\AuthenticationManager;
+use Kanboard\Core\Session\SessionManager;
+use Kanboard\Event\AuthSuccessEvent;
+use Kanboard\Event\AuthFailureEvent;
-class AuthSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface
+/**
+ * Authentication Subscriber
+ *
+ * @package subscriber
+ * @author Frederic Guillot
+ */
+class AuthSubscriber extends Base implements EventSubscriberInterface
{
+ /**
+ * Get event listeners
+ *
+ * @static
+ * @access public
+ * @return array
+ */
public static function getSubscribedEvents()
{
return array(
- 'auth.success' => array('onSuccess', 0),
+ AuthenticationManager::EVENT_SUCCESS => 'afterLogin',
+ AuthenticationManager::EVENT_FAILURE => 'onLoginFailure',
+ SessionManager::EVENT_DESTROY => 'afterLogout',
);
}
- public function onSuccess(AuthEvent $event)
+ /**
+ * After Login callback
+ *
+ * @access public
+ * @param AuthSuccessEvent $event
+ */
+ public function afterLogin(AuthSuccessEvent $event)
{
+ $userAgent = $this->request->getUserAgent();
+ $ipAddress = $this->request->getIpAddress();
+
+ $this->userLocking->resetFailedLogin($this->userSession->getUsername());
+
$this->lastLogin->create(
$event->getAuthType(),
- $event->getUserId(),
- Request::getIpAddress(),
- Request::getUserAgent()
+ $this->userSession->getId(),
+ $ipAddress,
+ $userAgent
);
+
+ $this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
+
+ if (isset($this->sessionStorage->hasRememberMe) && $this->sessionStorage->hasRememberMe) {
+ $session = $this->rememberMeSession->create($this->userSession->getId(), $ipAddress, $userAgent);
+ $this->rememberMeCookie->write($session['token'], $session['sequence'], $session['expiration']);
+ }
+ }
+
+ /**
+ * Destroy RememberMe session on logout
+ *
+ * @access public
+ */
+ public function afterLogout()
+ {
+ $credentials = $this->rememberMeCookie->read();
+
+ if ($credentials !== false) {
+ $session = $this->rememberMeSession->find($credentials['token'], $credentials['sequence']);
+
+ if (! empty($session)) {
+ $this->rememberMeSession->remove($session['id']);
+ }
+
+ $this->rememberMeCookie->remove();
+ }
+ }
+
+ /**
+ * Increment failed login counter
+ *
+ * @access public
+ */
+ public function onLoginFailure(AuthFailureEvent $event)
+ {
+ $username = $event->getUsername();
+
+ if (! empty($username)) {
+ $this->userLocking->incrementFailedLogin($username);
+
+ if ($this->userLocking->getFailedLogin($username) > BRUTEFORCE_LOCKDOWN) {
+ $this->userLocking->lock($username, BRUTEFORCE_LOCKDOWN_DURATION);
+ }
+ }
}
}
diff --git a/app/Subscriber/BootstrapSubscriber.php b/app/Subscriber/BootstrapSubscriber.php
index 25b919f7..cc0bc06d 100644
--- a/app/Subscriber/BootstrapSubscriber.php
+++ b/app/Subscriber/BootstrapSubscriber.php
@@ -9,9 +9,7 @@ class BootstrapSubscriber extends \Kanboard\Core\Base implements EventSubscriber
public static function getSubscribedEvents()
{
return array(
- 'session.bootstrap' => array('setup', 0),
- 'api.bootstrap' => array('setup', 0),
- 'console.bootstrap' => array('setup', 0),
+ 'app.bootstrap' => array('setup', 0),
);
}
@@ -20,4 +18,18 @@ class BootstrapSubscriber extends \Kanboard\Core\Base implements EventSubscriber
$this->config->setupTranslations();
$this->config->setupTimezone();
}
+
+ public function __destruct()
+ {
+ if (DEBUG) {
+ foreach ($this->db->getLogMessages() as $message) {
+ $this->logger->debug($message);
+ }
+
+ $this->logger->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries));
+ $this->logger->debug('RENDERING={time}', array('time' => microtime(true) - $this->request->getStartTime()));
+ $this->logger->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage()));
+ $this->logger->debug('URI='.$this->request->getUri());
+ }
+ }
}
diff --git a/app/Template/activity/project.php b/app/Template/activity/project.php
index bc585212..34be06f5 100644
--- a/app/Template/activity/project.php
+++ b/app/Template/activity/project.php
@@ -19,7 +19,7 @@
<i class="fa fa-calendar fa-fw"></i>
<?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
</li>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('project', 'edit', $project['id'])): ?>
<li>
<i class="fa fa-cog fa-fw"></i>
<?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
diff --git a/app/Template/analytic/layout.php b/app/Template/analytic/layout.php
index fd2090ae..3bb6ff6e 100644
--- a/app/Template/analytic/layout.php
+++ b/app/Template/analytic/layout.php
@@ -19,7 +19,7 @@
<i class="fa fa-calendar fa-fw"></i>
<?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
</li>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('project', 'edit', $project['id'])): ?>
<li>
<i class="fa fa-cog fa-fw"></i>
<?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
diff --git a/app/Template/app/layout.php b/app/Template/app/layout.php
index 4f82121e..ad1d5a9e 100644
--- a/app/Template/app/layout.php
+++ b/app/Template/app/layout.php
@@ -1,7 +1,7 @@
<section id="main">
<div class="page-header page-header-mobile">
<ul>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('project', 'create')): ?>
<li>
<i class="fa fa-plus fa-fw"></i>
<?= $this->url->link(t('New project'), 'project', 'create') ?>
@@ -9,7 +9,7 @@
<?php endif ?>
<li>
<i class="fa fa-lock fa-fw"></i>
- <?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?>
+ <?= $this->url->link(t('New private project'), 'project', 'createPrivate') ?>
</li>
<li>
<i class="fa fa-search fa-fw"></i>
@@ -19,7 +19,7 @@
<i class="fa fa-folder fa-fw"></i>
<?= $this->url->link(t('Project management'), 'project', 'index') ?>
</li>
- <?php if ($this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('user', 'index')): ?>
<li>
<i class="fa fa-user fa-fw"></i>
<?= $this->url->link(t('User management'), 'user', 'index') ?>
diff --git a/app/Template/app/projects.php b/app/Template/app/projects.php
index cf22707b..f9267e39 100644
--- a/app/Template/app/projects.php
+++ b/app/Template/app/projects.php
@@ -22,7 +22,7 @@
<?php endif ?>
</td>
<td>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('project', 'edit', $project['id'])): ?>
<?= $this->url->link('<i class="fa fa-sliders fa-fw"></i>', 'gantt', 'project', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Gantt chart')) ?>
<?php endif ?>
diff --git a/app/Template/board/popover_assignee.php b/app/Template/board/popover_assignee.php
index 4af19cf7..f395113c 100644
--- a/app/Template/board/popover_assignee.php
+++ b/app/Template/board/popover_assignee.php
@@ -1,7 +1,7 @@
<section id="main">
<section>
<h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3>
- <form method="post" action="<?= $this->url->href('board', 'updateAssignee', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>">
+ <form method="post" action="<?= $this->url->href('BoardPopover', 'updateAssignee', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>">
<?= $this->form->csrf() ?>
diff --git a/app/Template/board/popover_category.php b/app/Template/board/popover_category.php
index f391f492..8c2a273d 100644
--- a/app/Template/board/popover_category.php
+++ b/app/Template/board/popover_category.php
@@ -1,7 +1,7 @@
<section id="main">
<section>
<h3><?= t('Change category for the task "%s"', $values['title']) ?></h3>
- <form method="post" action="<?= $this->url->href('board', 'updateCategory', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>">
+ <form method="post" action="<?= $this->url->href('BoardPopover', 'updateCategory', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>">
<?= $this->form->csrf() ?>
diff --git a/app/Template/board/table_column.php b/app/Template/board/table_column.php
index b6a38872..10bcfa08 100644
--- a/app/Template/board/table_column.php
+++ b/app/Template/board/table_column.php
@@ -12,7 +12,7 @@
<!-- column in expanded mode -->
<div class="board-column-expanded">
- <?php if (! $not_editable): ?>
+ <?php if (! $not_editable && $this->user->hasProjectAccess('taskcreation', 'create', $column['project_id'])): ?>
<div class="board-add-icon">
<?= $this->url->link('+', 'taskcreation', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover', t('Add a new task')) ?>
</div>
diff --git a/app/Template/board/table_swimlane.php b/app/Template/board/table_swimlane.php
index dd38fc97..44607859 100644
--- a/app/Template/board/table_swimlane.php
+++ b/app/Template/board/table_swimlane.php
@@ -14,7 +14,7 @@
<span
title="<?= t('Description') ?>"
class="tooltip"
- data-href="<?= $this->url->href('board', 'swimlane', array('swimlane_id' => $swimlane['id'], 'project_id' => $project['id'])) ?>">
+ data-href="<?= $this->url->href('BoardTooltip', 'swimlane', array('swimlane_id' => $swimlane['id'], 'project_id' => $project['id'])) ?>">
<i class="fa fa-info-circle"></i>
</span>
<?php endif ?>
diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php
index d486b638..e29384dc 100644
--- a/app/Template/board/task_footer.php
+++ b/app/Template/board/task_footer.php
@@ -27,31 +27,31 @@
<?php endif ?>
<?php if ($task['recurrence_status'] == \Kanboard\Model\Task::RECURRING_STATUS_PENDING): ?>
- <span title="<?= t('Recurrence') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-refresh fa-rotate-90"></i></span>
+ <span title="<?= t('Recurrence') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-refresh fa-rotate-90"></i></span>
<?php endif ?>
<?php if ($task['recurrence_status'] == \Kanboard\Model\Task::RECURRING_STATUS_PROCESSED): ?>
- <span title="<?= t('Recurrence') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-refresh fa-rotate-90 fa-inverse"></i></span>
+ <span title="<?= t('Recurrence') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-refresh fa-rotate-90 fa-inverse"></i></span>
<?php endif ?>
<?php if (! empty($task['nb_links'])): ?>
- <span title="<?= t('Links') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-code-fork"></i>&nbsp;<?= $task['nb_links'] ?></span>
+ <span title="<?= t('Links') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-code-fork"></i>&nbsp;<?= $task['nb_links'] ?></span>
<?php endif ?>
<?php if (! empty($task['nb_subtasks'])): ?>
- <span title="<?= t('Sub-Tasks') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'subtasks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-bars"></i>&nbsp;<?= round($task['nb_completed_subtasks']/$task['nb_subtasks']*100, 0).'%' ?></span>
+ <span title="<?= t('Sub-Tasks') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'subtasks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-bars"></i>&nbsp;<?= round($task['nb_completed_subtasks']/$task['nb_subtasks']*100, 0).'%' ?></span>
<?php endif ?>
<?php if (! empty($task['nb_files'])): ?>
- <span title="<?= t('Attachments') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'attachments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-paperclip"></i>&nbsp;<?= $task['nb_files'] ?></span>
+ <span title="<?= t('Attachments') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'attachments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-paperclip"></i>&nbsp;<?= $task['nb_files'] ?></span>
<?php endif ?>
<?php if (! empty($task['nb_comments'])): ?>
- <span title="<?= $task['nb_comments'] == 1 ? t('%d comment', $task['nb_comments']) : t('%d comments', $task['nb_comments']) ?>" class="tooltip" data-href="<?= $this->url->href('board', 'comments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-comment-o"></i>&nbsp;<?= $task['nb_comments'] ?></span>
+ <span title="<?= $task['nb_comments'] == 1 ? t('%d comment', $task['nb_comments']) : t('%d comments', $task['nb_comments']) ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'comments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-comment-o"></i>&nbsp;<?= $task['nb_comments'] ?></span>
<?php endif ?>
<?php if (! empty($task['description'])): ?>
- <span title="<?= t('Description') ?>" class="tooltip" data-href="<?= $this->url->href('board', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
+ <span title="<?= t('Description') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
<i class="fa fa-file-text-o"></i>
</span>
<?php endif ?>
diff --git a/app/Template/board/task_menu.php b/app/Template/board/task_menu.php
index 3eb35705..b5ed125d 100644
--- a/app/Template/board/task_menu.php
+++ b/app/Template/board/task_menu.php
@@ -1,13 +1,13 @@
<span class="dropdown">
<a href="#" class="dropdown-menu"><?= '#'.$task['id'] ?></a>
<ul>
- <li><i class="fa fa-user fa-fw"></i>&nbsp;<?= $this->url->link(t('Change assignee'), 'board', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
- <li><i class="fa fa-tag fa-fw"></i>&nbsp;<?= $this->url->link(t('Change category'), 'board', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
+ <li><i class="fa fa-user fa-fw"></i>&nbsp;<?= $this->url->link(t('Change assignee'), 'BoardPopover', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
+ <li><i class="fa fa-tag fa-fw"></i>&nbsp;<?= $this->url->link(t('Change category'), 'BoardPopover', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<li><i class="fa fa-align-left fa-fw"></i>&nbsp;<?= $this->url->link(t('Change description'), 'taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<li><i class="fa fa-pencil-square-o fa-fw"></i>&nbsp;<?= $this->url->link(t('Edit this task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<li><i class="fa fa-comment-o fa-fw"></i>&nbsp;<?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<li><i class="fa fa-code-fork fa-fw"></i>&nbsp;<?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
- <li><i class="fa fa-camera fa-fw"></i>&nbsp;<?= $this->url->link(t('Add a screenshot'), 'board', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
+ <li><i class="fa fa-camera fa-fw"></i>&nbsp;<?= $this->url->link(t('Add a screenshot'), 'BoardPopover', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<?php if ($task['is_active'] == 1): ?>
<li><i class="fa fa-close fa-fw"></i>&nbsp;<?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'popover') ?></li>
<?php else: ?>
diff --git a/app/Template/board/task_private.php b/app/Template/board/task_private.php
index da993fdd..a5d05e49 100644
--- a/app/Template/board/task_private.php
+++ b/app/Template/board/task_private.php
@@ -1,6 +1,6 @@
<div class="
task-board
- <?= $task['is_active'] == 1 ? 'draggable-item task-board-status-open '.($task['date_modification'] > (time() - $board_highlight_period) ? 'task-board-recent' : '') : 'task-board-status-closed' ?>
+ <?= $task['is_active'] == 1 ? ($this->user->hasProjectAccess('board', 'save', $task['project_id']) ? 'draggable-item ' : '').'task-board-status-open '.($task['date_modification'] > (time() - $board_highlight_period) ? 'task-board-recent' : '') : 'task-board-status-closed' ?>
color-<?= $task['color_id'] ?>"
data-task-id="<?= $task['id'] ?>"
data-owner-id="<?= $task['owner_id'] ?>"
@@ -12,7 +12,11 @@
<?php if ($this->board->isCollapsed($task['project_id'])): ?>
<div class="task-board-collapsed">
- <?= $this->render('board/task_menu', array('task' => $task)) ?>
+ <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
+ <?= $this->render('board/task_menu', array('task' => $task)) ?>
+ <?php else: ?>
+ <strong><?= '#'.$task['id'] ?></strong>
+ <?php endif ?>
<?php if (! empty($task['assignee_username'])): ?>
<span title="<?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>">
@@ -23,7 +27,11 @@
</div>
<?php else: ?>
<div class="task-board-expanded">
- <?= $this->render('board/task_menu', array('task' => $task)) ?>
+ <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
+ <?= $this->render('board/task_menu', array('task' => $task)) ?>
+ <?php else: ?>
+ <strong><?= '#'.$task['id'] ?></strong>
+ <?php endif ?>
<?php if ($task['reference']): ?>
<span class="task-board-reference" title="<?= t('Reference') ?>">
diff --git a/app/Template/calendar/show.php b/app/Template/calendar/show.php
index 0406414c..d74e945e 100644
--- a/app/Template/calendar/show.php
+++ b/app/Template/calendar/show.php
@@ -5,7 +5,7 @@
)) ?>
<div id="calendar"
- data-save-url="<?= $this->url->href('calendar', 'save') ?>"
+ data-save-url="<?= $this->url->href('calendar', 'save', array('project_id' => $project['id'])) ?>"
data-check-url="<?= $this->url->href('calendar', 'project', array('project_id' => $project['id'])) ?>"
data-check-interval="<?= $check_interval ?>"
>
diff --git a/app/Template/custom_filter/add.php b/app/Template/custom_filter/add.php
index 61df148c..b0778b8e 100644
--- a/app/Template/custom_filter/add.php
+++ b/app/Template/custom_filter/add.php
@@ -12,7 +12,7 @@
<?= $this->form->label(t('Filter'), 'filter') ?>
<?= $this->form->text('filter', $values, $errors, array('required', 'maxlength="100"')) ?>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('project', 'edit', $project['id'])): ?>
<?= $this->form->checkbox('is_shared', t('Share with all project members'), 1) ?>
<?php endif ?>
diff --git a/app/Template/custom_filter/edit.php b/app/Template/custom_filter/edit.php
index 9d296b84..683d2802 100644
--- a/app/Template/custom_filter/edit.php
+++ b/app/Template/custom_filter/edit.php
@@ -16,7 +16,7 @@
<?= $this->form->label(t('Filter'), 'filter') ?>
<?= $this->form->text('filter', $values, $errors, array('required', 'maxlength="100"')) ?>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('project', 'edit', $project['id'])): ?>
<?= $this->form->checkbox('is_shared', t('Share with all project members'), 1, $values['is_shared'] == 1) ?>
<?php else: ?>
<?= $this->form->hidden('is_shared', $values) ?>
diff --git a/app/Template/custom_filter/index.php b/app/Template/custom_filter/index.php
index c857e206..507e091b 100644
--- a/app/Template/custom_filter/index.php
+++ b/app/Template/custom_filter/index.php
@@ -32,7 +32,7 @@
</td>
<td><?= $this->e($filter['owner_name'] ?: $filter['owner_username']) ?></td>
<td>
- <?php if ($filter['user_id'] == $this->user->getId() || $this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($filter['user_id'] == $this->user->getId() || $this->user->hasProjectAccess('customfilter', 'edit', $project['id'])): ?>
<ul>
<li><?= $this->url->link(t('Remove'), 'customfilter', 'remove', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id']), true) ?></li>
<li><?= $this->url->link(t('Edit'), 'customfilter', 'edit', array('project_id' => $filter['project_id'], 'filter_id' => $filter['id'])) ?></li>
diff --git a/app/Template/gantt/projects.php b/app/Template/gantt/projects.php
index 50e244a5..46d2af91 100644
--- a/app/Template/gantt/projects.php
+++ b/app/Template/gantt/projects.php
@@ -1,7 +1,7 @@
<section id="main">
<div class="page-header">
<ul>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('project', 'create')): ?>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li>
<?php endif ?>
<li>
@@ -10,7 +10,7 @@
<li>
<i class="fa fa-folder fa-fw"></i><?= $this->url->link(t('Projects list'), 'project', 'index') ?>
</li>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('projectuser', 'managers')): ?>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('Users overview'), 'projectuser', 'managers') ?></li>
<?php endif ?>
</ul>
diff --git a/app/Template/group/dissociate.php b/app/Template/group/dissociate.php
index 2b0b1af4..e1c60764 100644
--- a/app/Template/group/dissociate.php
+++ b/app/Template/group/dissociate.php
@@ -1,11 +1,9 @@
<section id="main">
<div class="page-header">
- <?php if ($this->user->isAdmin()): ?>
<ul>
<li><i class="fa fa-users fa-fw"></i><?= $this->url->link(t('View all groups'), 'group', 'index') ?></li>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('View group members'), 'group', 'users', array('group_id' => $group['id'])) ?></li>
</ul>
- <?php endif ?>
</div>
<div class="confirm">
<p class="alert alert-info"><?= t('Do you really want to remove the user "%s" from the group "%s"?', $user['name'] ?: $user['username'], $group['name']) ?></p>
diff --git a/app/Template/group/index.php b/app/Template/group/index.php
index 24de02a0..4aea0873 100644
--- a/app/Template/group/index.php
+++ b/app/Template/group/index.php
@@ -1,11 +1,9 @@
<section id="main">
<div class="page-header">
- <?php if ($this->user->isAdmin()): ?>
<ul>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li>
<li><i class="fa fa-user-plus fa-fw"></i><?= $this->url->link(t('New group'), 'group', 'create') ?></li>
</ul>
- <?php endif ?>
</div>
<?php if ($paginator->isEmpty()): ?>
<p class="alert"><?= t('There is no group.') ?></p>
@@ -31,7 +29,7 @@
<td>
<ul>
<li><?= $this->url->link(t('Add group member'), 'group', 'associate', array('group_id' => $group['id'])) ?></li>
- <li><?= $this->url->link(t('Users'), 'group', 'users', array('group_id' => $group['id'])) ?></li>
+ <li><?= $this->url->link(t('Members'), 'group', 'users', array('group_id' => $group['id'])) ?></li>
<li><?= $this->url->link(t('Edit'), 'group', 'edit', array('group_id' => $group['id'])) ?></li>
<li><?= $this->url->link(t('Remove'), 'group', 'confirm', array('group_id' => $group['id'])) ?></li>
</ul>
diff --git a/app/Template/group/remove.php b/app/Template/group/remove.php
index 48da91d5..1cb007b1 100644
--- a/app/Template/group/remove.php
+++ b/app/Template/group/remove.php
@@ -1,11 +1,9 @@
<section id="main">
<div class="page-header">
- <?php if ($this->user->isAdmin()): ?>
<ul>
<li><i class="fa fa-users fa-fw"></i><?= $this->url->link(t('View all groups'), 'group', 'index') ?></li>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('View group members'), 'group', 'users', array('group_id' => $group['id'])) ?></li>
</ul>
- <?php endif ?>
</div>
<div class="confirm">
<p class="alert alert-info"><?= t('Do you really want to remove this group: "%s"?', $group['name']) ?></p>
diff --git a/app/Template/group/users.php b/app/Template/group/users.php
index 56ad82cf..f79cb9ad 100644
--- a/app/Template/group/users.php
+++ b/app/Template/group/users.php
@@ -1,11 +1,9 @@
<section id="main">
<div class="page-header">
- <?php if ($this->user->isAdmin()): ?>
<ul>
<li><i class="fa fa-users fa-fw"></i><?= $this->url->link(t('View all groups'), 'group', 'index') ?></li>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('Add group member'), 'group', 'associate', array('group_id' => $group['id'])) ?></li>
</ul>
- <?php endif ?>
</div>
<?php if ($paginator->isEmpty()): ?>
<p class="alert"><?= t('There is no user in this group.') ?></p>
diff --git a/app/Template/layout.php b/app/Template/layout.php
index 20582952..0c81aac2 100644
--- a/app/Template/layout.php
+++ b/app/Template/layout.php
@@ -36,7 +36,7 @@
</head>
<body data-status-url="<?= $this->url->href('app', 'status') ?>"
data-login-url="<?= $this->url->href('auth', 'login') ?>"
- data-markdown-preview-url="<?= $this->url->href('app', 'preview') ?>"
+ data-markdown-preview-url="<?= $this->url->href('TaskHelper', 'preview') ?>"
data-timezone="<?= $this->app->getTimezone() ?>"
data-js-lang="<?= $this->app->jsLang() ?>">
diff --git a/app/Template/project/dropdown.php b/app/Template/project/dropdown.php
index 1eb87b0e..9ef7cfb4 100644
--- a/app/Template/project/dropdown.php
+++ b/app/Template/project/dropdown.php
@@ -2,10 +2,13 @@
<i class="fa fa-dashboard fa-fw"></i>&nbsp;
<?= $this->url->link(t('Activity'), 'activity', 'project', array('project_id' => $project['id'])) ?>
</li>
+
+<?php if ($this->user->hasProjectAccess('customfilter', 'index', $project['id'])): ?>
<li>
<i class="fa fa-filter fa-fw"></i>&nbsp;
<?= $this->url->link(t('Custom filters'), 'customfilter', 'index', array('project_id' => $project['id'])) ?>
</li>
+<?php endif ?>
<?php if ($project['is_public']): ?>
<li>
@@ -15,15 +18,21 @@
<?= $this->hook->render('template:project:dropdown', array('project' => $project)) ?>
-<?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+<?php if ($this->user->hasProjectAccess('analytic', 'tasks', $project['id'])): ?>
<li>
<i class="fa fa-line-chart fa-fw"></i>&nbsp;
<?= $this->url->link(t('Analytics'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?>
</li>
+<?php endif ?>
+
+<?php if ($this->user->hasProjectAccess('export', 'tasks', $project['id'])): ?>
<li>
<i class="fa fa-download fa-fw"></i>&nbsp;
<?= $this->url->link(t('Exports'), 'export', 'tasks', array('project_id' => $project['id'])) ?>
</li>
+<?php endif ?>
+
+<?php if ($this->user->hasProjectAccess('project', 'edit', $project['id'])): ?>
<li>
<i class="fa fa-cog fa-fw"></i>&nbsp;
<?= $this->url->link(t('Settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
diff --git a/app/Template/project/edit.php b/app/Template/project/edit.php
index 8dcbb88f..188107d1 100644
--- a/app/Template/project/edit.php
+++ b/app/Template/project/edit.php
@@ -19,7 +19,7 @@
<?= $this->form->label(t('End date'), 'end_date') ?>
<?= $this->form->text('end_date', $values, $errors, array('maxlength="10"'), 'form-date') ?>
- <?php if ($this->user->isAdmin() || $this->user->isProjectAdministrationAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('project', 'create', $project['id'])): ?>
<?= $this->form->checkbox('is_private', t('Private project'), 1, $project['is_private'] == 1) ?>
<?php endif ?>
diff --git a/app/Template/project/filters.php b/app/Template/project/filters.php
index 9e126291..0dbb52c9 100644
--- a/app/Template/project/filters.php
+++ b/app/Template/project/filters.php
@@ -48,7 +48,7 @@
<i class="fa fa-list fa-fw"></i>
<?= $this->url->link(t('List'), 'listing', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-listing', t('Keyboard shortcut: "%s"', 'v l')) ?>
</li>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('gantt', 'project', $project['id'])): ?>
<li <?= $filters['controller'] === 'gantt' ? 'class="active"' : '' ?>>
<i class="fa fa-sliders fa-fw"></i>
<?= $this->url->link(t('Gantt'), 'gantt', 'project', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-gantt', t('Keyboard shortcut: "%s"', 'v g')) ?>
diff --git a/app/Template/project/index.php b/app/Template/project/index.php
index 4b62a27f..c7d74f8b 100644
--- a/app/Template/project/index.php
+++ b/app/Template/project/index.php
@@ -1,12 +1,14 @@
<section id="main">
<div class="page-header">
<ul>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('project', 'create')): ?>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li>
<?php endif ?>
- <li><i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?></li>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <li><i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'project', 'createPrivate') ?></li>
+ <?php if ($this->user->hasAccess('projectuser', 'managers')): ?>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('Users overview'), 'projectuser', 'managers') ?></li>
+ <?php endif ?>
+ <?php if ($this->user->hasAccess('gantt', 'projects')): ?>
<li><i class="fa fa-sliders fa-fw"></i><?= $this->url->link(t('Projects Gantt chart'), 'gantt', 'projects') ?></li>
<?php endif ?>
</ul>
@@ -21,7 +23,7 @@
<th class="column-15"><?= $paginator->order(t('Project'), 'name') ?></th>
<th class="column-8"><?= $paginator->order(t('Start date'), 'start_date') ?></th>
<th class="column-8"><?= $paginator->order(t('End date'), 'end_date') ?></th>
- <?php if ($this->user->isAdmin() || $this->user->isProjectAdmin()): ?>
+ <?php if ($this->user->hasAccess('projectuser', 'managers')): ?>
<th class="column-12"><?= t('Managers') ?></th>
<th class="column-12"><?= t('Members') ?></th>
<?php endif ?>
@@ -64,25 +66,17 @@
<td>
<?= $project['end_date'] ?>
</td>
- <?php if ($this->user->isAdmin() || $this->user->isProjectAdmin()): ?>
- <td>
- <ul class="no-bullet">
- <?php foreach ($project['managers'] as $user_id => $user_name): ?>
- <li><?= $this->url->link($this->e($user_name), 'projectuser', 'opens', array('user_id' => $user_id)) ?></li>
- <?php endforeach ?>
- </ul>
- </td>
- <td>
- <?php if ($project['is_everybody_allowed'] == 1): ?>
- <?= t('Everybody') ?>
- <?php else: ?>
- <ul class="no-bullet">
- <?php foreach ($project['members'] as $user_id => $user_name): ?>
- <li><?= $this->url->link($this->e($user_name), 'projectuser', 'opens', array('user_id' => $user_id)) ?></li>
- <?php endforeach ?>
- </ul>
- <?php endif ?>
- </td>
+ <?php if ($this->user->hasAccess('projectuser', 'managers')): ?>
+ <td>
+ <?= $this->render('project/roles', array('roles' => $project, 'role' => \Kanboard\Core\Security\Role::PROJECT_MANAGER)) ?>
+ </td>
+ <td>
+ <?php if ($project['is_everybody_allowed'] == 1): ?>
+ <?= t('Everybody') ?>
+ <?php else: ?>
+ <?= $this->render('project/roles', array('roles' => $project, 'role' => \Kanboard\Core\Security\Role::PROJECT_MEMBER)) ?>
+ <?php endif ?>
+ </td>
<?php endif ?>
<td class="dashboard-project-stats">
<?php foreach ($project['columns'] as $column): ?>
diff --git a/app/Template/project/roles.php b/app/Template/project/roles.php
new file mode 100644
index 00000000..d4cd43cb
--- /dev/null
+++ b/app/Template/project/roles.php
@@ -0,0 +1,7 @@
+<?php if (! empty($roles[$role])): ?>
+ <ul class="no-bullet">
+ <?php foreach ($roles[$role] as $user_id => $user_name): ?>
+ <li><?= $this->url->link($this->e($user_name), 'projectuser', 'opens', array('user_id' => $user_id)) ?></li>
+ <?php endforeach ?>
+ </ul>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/project/sidebar.php b/app/Template/project/sidebar.php
index fb5dd3bd..b436c9e8 100644
--- a/app/Template/project/sidebar.php
+++ b/app/Template/project/sidebar.php
@@ -8,7 +8,7 @@
<?= $this->url->link(t('Custom filters'), 'customfilter', 'index', array('project_id' => $project['id'])) ?>
</li>
- <?php if ($this->user->isProjectManagementAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('project', 'edit', $project['id'])): ?>
<li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'share' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Public access'), 'project', 'share', array('project_id' => $project['id'])) ?>
</li>
@@ -30,9 +30,9 @@
<li <?= $this->app->getRouterController() === 'category' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Categories'), 'category', 'index', array('project_id' => $project['id'])) ?>
</li>
- <?php if ($this->user->isAdmin() || $project['is_private'] == 0): ?>
- <li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'users' ? 'class="active"' : '' ?>>
- <?= $this->url->link(t('Users'), 'project', 'users', array('project_id' => $project['id'])) ?>
+ <?php if ($project['is_private'] == 0): ?>
+ <li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'permissions' ? 'class="active"' : '' ?>>
+ <?= $this->url->link(t('Permissions'), 'ProjectPermission', 'index', array('project_id' => $project['id'])) ?>
</li>
<?php endif ?>
<li <?= $this->app->getRouterController() === 'action' ? 'class="active"' : '' ?>>
@@ -51,7 +51,7 @@
<li <?= $this->app->getRouterController() === 'taskImport' && $this->app->getRouterAction() === 'step1' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Import'), 'taskImport', 'step1', array('project_id' => $project['id'])) ?>
</li>
- <?php if ($this->user->isProjectAdministrationAllowed($project['id'])): ?>
+ <?php if ($this->user->hasProjectAccess('project', 'remove', $project['id'])): ?>
<li <?= $this->app->getRouterController() === 'project' && $this->app->getRouterAction() === 'remove' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Remove'), 'project', 'remove', array('project_id' => $project['id'])) ?>
</li>
diff --git a/app/Template/project/users.php b/app/Template/project/users.php
deleted file mode 100644
index 8863a1e4..00000000
--- a/app/Template/project/users.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<div class="page-header">
- <h2><?= t('List of authorized users') ?></h2>
-</div>
-
-<?php if ($project['is_everybody_allowed']): ?>
- <div class="alert"><?= t('Everybody have access to this project.') ?></div>
-<?php else: ?>
-
- <?php if (empty($users['allowed'])): ?>
- <div class="alert alert-error"><?= t('Nobody have access to this project.') ?></div>
- <?php else: ?>
- <table>
- <tr>
- <th><?= t('User') ?></th>
- <th><?= t('Role for this project') ?></th>
- <?php if ($project['is_private'] == 0): ?>
- <th><?= t('Actions') ?></th>
- <?php endif ?>
- </tr>
- <?php foreach ($users['allowed'] as $user_id => $username): ?>
- <tr>
- <td><?= $this->e($username) ?></td>
- <td><?= isset($users['managers'][$user_id]) ? t('Project manager') : t('Project member') ?></td>
- <?php if ($project['is_private'] == 0): ?>
- <td>
- <ul>
- <li><?= $this->url->link(t('Revoke'), 'project', 'revoke', array('project_id' => $project['id'], 'user_id' => $user_id), true) ?></li>
- <li>
- <?php if (isset($users['managers'][$user_id])): ?>
- <?= $this->url->link(t('Set project member'), 'project', 'role', array('project_id' => $project['id'], 'user_id' => $user_id, 'is_owner' => 0), true) ?>
- <?php else: ?>
- <?= $this->url->link(t('Set project manager'), 'project', 'role', array('project_id' => $project['id'], 'user_id' => $user_id, 'is_owner' => 1), true) ?>
- <?php endif ?>
- </li>
- </ul>
- </td>
- <?php endif ?>
- </tr>
- <?php endforeach ?>
- </table>
- <?php endif ?>
-
- <?php if ($project['is_private'] == 0 && ! empty($users['not_allowed'])): ?>
- <hr/>
- <form method="post" action="<?= $this->url->href('project', 'allow', array('project_id' => $project['id'])) ?>" autocomplete="off">
-
- <?= $this->form->csrf() ?>
-
- <?= $this->form->hidden('project_id', array('project_id' => $project['id'])) ?>
-
- <?= $this->form->label(t('User'), 'user_id') ?>
- <?= $this->form->select('user_id', $users['not_allowed'], array(), array(), array('data-notfound="'.t('No results match:').'"'), 'chosen-select') ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Allow this user') ?>" class="btn btn-blue"/>
- </div>
- </form>
- <?php endif ?>
-
-<?php endif ?>
-
-<?php if ($project['is_private'] == 0): ?>
-<hr/>
-<form method="post" action="<?= $this->url->href('project', 'allowEverybody', array('project_id' => $project['id'])) ?>">
- <?= $this->form->csrf() ?>
-
- <?= $this->form->hidden('id', array('id' => $project['id'])) ?>
- <?= $this->form->checkbox('is_everybody_allowed', t('Allow everybody to access to this project'), 1, $project['is_everybody_allowed']) ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form>
-<?php endif ?>
-
-<div class="alert alert-info">
- <ul>
- <li><?= t('A project manager can change the settings of the project and have more privileges than a standard user.') ?></li>
- <li><?= t('Don\'t forget that administrators have access to everything.') ?></li>
- <li><?= $this->url->doc(t('Help with project permissions'), 'project-permissions') ?></li>
- </ul>
-</div>
diff --git a/app/Template/project_permission/index.php b/app/Template/project_permission/index.php
new file mode 100644
index 00000000..5f0edc2b
--- /dev/null
+++ b/app/Template/project_permission/index.php
@@ -0,0 +1,141 @@
+<div class="page-header">
+ <h2><?= t('Allowed Users') ?></h2>
+</div>
+
+<?php if ($project['is_everybody_allowed']): ?>
+ <div class="alert"><?= t('Everybody have access to this project.') ?></div>
+<?php else: ?>
+
+ <?php if (empty($users)): ?>
+ <div class="alert"><?= t('No user have been allowed specifically.') ?></div>
+ <?php else: ?>
+ <table>
+ <tr>
+ <th class="column-50"><?= t('User') ?></th>
+ <th><?= t('Role') ?></th>
+ <?php if ($project['is_private'] == 0): ?>
+ <th class="column-15"><?= t('Actions') ?></th>
+ <?php endif ?>
+ </tr>
+ <?php foreach ($users as $user): ?>
+ <tr>
+ <td><?= $this->e($user['name'] ?: $user['username']) ?></td>
+ <td>
+ <?= $this->form->select(
+ 'role-'.$user['id'],
+ $roles,
+ array('role-'.$user['id'] => $user['role']),
+ array(),
+ array('data-url="'.$this->url->href('ProjectPermission', 'changeUserRole', array('project_id' => $project['id'])).'"', 'data-id="'.$user['id'].'"'),
+ 'project-change-role'
+ ) ?>
+ </td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'ProjectPermission', 'removeUser', array('project_id' => $project['id'], 'user_id' => $user['id']), true) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+
+ <?php if ($project['is_private'] == 0): ?>
+ <div class="listing">
+ <form method="post" action="<?= $this->url->href('ProjectPermission', 'addUser', array('project_id' => $project['id'])) ?>" autocomplete="off" class="form-inline">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('project_id', array('project_id' => $project['id'])) ?>
+ <?= $this->form->hidden('user_id', $values) ?>
+
+ <?= $this->form->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors, array(
+ 'required',
+ 'placeholder="'.t('Enter user name...').'"',
+ 'title="'.t('Enter user name...').'"',
+ 'data-dst-field="user_id"',
+ 'data-search-url="'.$this->url->href('UserHelper', 'autocomplete').'"',
+ ),
+ 'autocomplete') ?>
+
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
+
+ <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/>
+ </form>
+ </div>
+ <?php endif ?>
+
+ <div class="page-header">
+ <h2><?= t('Allowed Groups') ?></h2>
+ </div>
+
+ <?php if (empty($groups)): ?>
+ <div class="alert"><?= t('No group have been allowed specifically.') ?></div>
+ <?php else: ?>
+ <table>
+ <tr>
+ <th class="column-50"><?= t('Group') ?></th>
+ <th><?= t('Role') ?></th>
+ <?php if ($project['is_private'] == 0): ?>
+ <th class="column-15"><?= t('Actions') ?></th>
+ <?php endif ?>
+ </tr>
+ <?php foreach ($groups as $group): ?>
+ <tr>
+ <td><?= $this->e($group['name']) ?></td>
+ <td>
+ <?= $this->form->select(
+ 'role-'.$group['id'],
+ $roles,
+ array('role-'.$group['id'] => $group['role']),
+ array(),
+ array('data-url="'.$this->url->href('ProjectPermission', 'changeGroupRole', array('project_id' => $project['id'])).'"', 'data-id="'.$group['id'].'"'),
+ 'project-change-role'
+ ) ?>
+ </td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'ProjectPermission', 'removeGroup', array('project_id' => $project['id'], 'group_id' => $group['id']), true) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+
+ <?php if ($project['is_private'] == 0): ?>
+ <div class="listing">
+ <form method="post" action="<?= $this->url->href('ProjectPermission', 'addGroup', array('project_id' => $project['id'])) ?>" autocomplete="off" class="form-inline">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('project_id', array('project_id' => $project['id'])) ?>
+ <?= $this->form->hidden('group_id', $values) ?>
+ <?= $this->form->hidden('external_id', $values) ?>
+
+ <?= $this->form->label(t('Group Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors, array(
+ 'required',
+ 'placeholder="'.t('Enter group name...').'"',
+ 'title="'.t('Enter group name...').'"',
+ 'data-dst-field="group_id"',
+ 'data-dst-extra-field="external_id"',
+ 'data-search-url="'.$this->url->href('GroupHelper', 'autocomplete').'"',
+ ),
+ 'autocomplete') ?>
+
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
+
+ <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/>
+ </form>
+ </div>
+ <?php endif ?>
+
+<?php endif ?>
+
+<?php if ($project['is_private'] == 0): ?>
+<hr/>
+<form method="post" action="<?= $this->url->href('ProjectPermission', 'allowEverybody', array('project_id' => $project['id'])) ?>">
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', array('id' => $project['id'])) ?>
+ <?= $this->form->checkbox('is_everybody_allowed', t('Allow everybody to access to this project'), 1, $project['is_everybody_allowed']) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+<?php endif ?>
diff --git a/app/Template/project_user/layout.php b/app/Template/project_user/layout.php
index 4cf732d6..3a569da4 100644
--- a/app/Template/project_user/layout.php
+++ b/app/Template/project_user/layout.php
@@ -1,7 +1,7 @@
<section id="main">
<div class="page-header">
<ul>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('project', 'create')): ?>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li>
<?php endif ?>
<li>
@@ -12,7 +12,7 @@
<i class="fa fa-folder fa-fw"></i>
<?= $this->url->link(t('Projects list'), 'project', 'index') ?>
</li>
- <?php if ($this->user->isProjectAdmin() || $this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('gantt', 'projects')): ?>
<li>
<i class="fa fa-sliders fa-fw"></i>
<?= $this->url->link(t('Projects Gantt chart'), 'gantt', 'projects') ?>
diff --git a/app/Template/subtask/show.php b/app/Template/subtask/show.php
index dc851642..f48484cc 100644
--- a/app/Template/subtask/show.php
+++ b/app/Template/subtask/show.php
@@ -1,10 +1,11 @@
<div id="subtasks" class="task-show-section">
- <div class="page-header">
- <h2><?= t('Sub-Tasks') ?></h2>
- </div>
<?php if (! empty($subtasks)): ?>
+ <div class="page-header">
+ <h2><?= t('Sub-Tasks') ?></h2>
+ </div>
+
<?php $first_position = $subtasks[0]['position']; ?>
<?php $last_position = $subtasks[count($subtasks) - 1]['position']; ?>
<table class="subtasks-table">
@@ -86,7 +87,13 @@
</table>
<?php endif ?>
- <?php if (! isset($not_editable)): ?>
+ <?php if (! isset($not_editable) && $this->user->hasProjectAccess('subtask', 'save', $task['project_id'])): ?>
+
+ <?php if (empty($subtasks)): ?>
+ <div class="page-header">
+ <h2><?= t('Sub-Tasks') ?></h2>
+ </div>
+ <?php endif ?>
<form method="post" action="<?= $this->url->href('subtask', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?>
diff --git a/app/Template/task/layout.php b/app/Template/task/layout.php
index 6b6e827a..0ceb9706 100644
--- a/app/Template/task/layout.php
+++ b/app/Template/task/layout.php
@@ -9,7 +9,7 @@
<i class="fa fa-calendar fa-fw"></i>
<?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $task['project_id'])) ?>
</li>
- <?php if ($this->user->isProjectManagementAllowed($task['project_id'])): ?>
+ <?php if ($this->user->hasProjectAccess('project', 'edit', $task['project_id'])): ?>
<li>
<i class="fa fa-cog fa-fw"></i>
<?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $task['project_id'])) ?>
diff --git a/app/Template/task/show.php b/app/Template/task/show.php
index 68d63c58..713c2b3a 100644
--- a/app/Template/task/show.php
+++ b/app/Template/task/show.php
@@ -6,7 +6,10 @@
'recurrence_basedate_list' => $this->task->recurrenceBasedates(),
)) ?>
-<?= $this->render('task_modification/edit_time', array('task' => $task, 'values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?>
+<?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $project['id'])): ?>
+ <?= $this->render('task_modification/edit_time', array('task' => $task, 'values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?>
+<?php endif ?>
+
<?= $this->render('task/description', array('task' => $task)) ?>
<?= $this->render('tasklink/show', array('task' => $task, 'links' => $links, 'link_label_list' => $link_label_list)) ?>
<?= $this->render('subtask/show', array('task' => $task, 'subtasks' => $subtasks, 'project' => $project, 'users_list' => isset($users_list) ? $users_list : array())) ?>
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index 9ee1e7df..d994aad3 100644
--- a/app/Template/task/sidebar.php
+++ b/app/Template/task/sidebar.php
@@ -21,6 +21,7 @@
<?= $this->hook->render('template:task:sidebar:information') ?>
</ul>
+ <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
<h2><?= t('Actions') ?></h2>
<ul>
<li <?= $this->app->getRouterController() === 'taskmodification' && $this->app->getRouterAction() === 'edit' ? 'class="active"' : '' ?>>
@@ -71,6 +72,7 @@
<?= $this->hook->render('template:task:sidebar:actions') ?>
</ul>
+ <?php endif ?>
<div class="sidebar-collapse"><a href="#" title="<?= t('Hide sidebar') ?>"><i class="fa fa-chevron-left"></i></a></div>
<div class="sidebar-expand" style="display: none"><a href="#" title="<?= t('Expand sidebar') ?>"><i class="fa fa-chevron-right"></i></a></div>
</div>
diff --git a/app/Template/tasklink/create.php b/app/Template/tasklink/create.php
index 749f2968..2832bdc7 100644
--- a/app/Template/tasklink/create.php
+++ b/app/Template/tasklink/create.php
@@ -21,9 +21,9 @@
'placeholder="'.t('Start to type task title...').'"',
'title="'.t('Start to type task title...').'"',
'data-dst-field="opposite_task_id"',
- 'data-search-url="'.$this->url->href('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
+ 'data-search-url="'.$this->url->href('TaskHelper', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
),
- 'task-autocomplete') ?>
+ 'autocomplete') ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
diff --git a/app/Template/tasklink/edit.php b/app/Template/tasklink/edit.php
index 73b43277..896f84c0 100644
--- a/app/Template/tasklink/edit.php
+++ b/app/Template/tasklink/edit.php
@@ -22,9 +22,9 @@
'placeholder="'.t('Start to type task title...').'"',
'title="'.t('Start to type task title...').'"',
'data-dst-field="opposite_task_id"',
- 'data-search-url="'.$this->url->href('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
+ 'data-search-url="'.$this->url->href('TaskHelper', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
),
- 'task-autocomplete') ?>
+ 'autocomplete') ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
diff --git a/app/Template/tasklink/show.php b/app/Template/tasklink/show.php
index 97a3a767..b66ec087 100644
--- a/app/Template/tasklink/show.php
+++ b/app/Template/tasklink/show.php
@@ -95,9 +95,9 @@
'placeholder="'.t('Start to type task title...').'"',
'title="'.t('Start to type task title...').'"',
'data-dst-field="opposite_task_id"',
- 'data-search-url="'.$this->url->href('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
+ 'data-search-url="'.$this->url->href('TaskHelper', 'autocomplete', array('exclude_task_id' => $task['id'])).'"',
),
- 'task-autocomplete') ?>
+ 'autocomplete') ?>
<input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/>
</form>
diff --git a/app/Template/twofactor/index.php b/app/Template/twofactor/index.php
index 36b92653..4c4ca088 100644
--- a/app/Template/twofactor/index.php
+++ b/app/Template/twofactor/index.php
@@ -15,10 +15,16 @@
<?php if ($user['twofactor_activated'] == 1): ?>
<div class="listing">
<p><?= t('Secret key: ') ?><strong><?= $this->e($user['twofactor_secret']) ?></strong> (base32)</p>
- <p><br/><img src="<?= $qrcode_url ?>"/><br/><br/></p>
+
+ <?php if (! empty($qrcode_url)): ?>
+ <p><br/><img src="<?= $qrcode_url ?>"/><br/><br/></p>
+ <?php endif ?>
+
<p>
- <?= t('This QR code contains the key URI: ') ?><strong><?= $this->e($key_url) ?></strong>
- <br/><br/>
+ <?php if (! empty($key_url)): ?>
+ <?= t('This QR code contains the key URI: ') ?><strong><?= $this->e($key_url) ?></strong>
+ <br/><br/>
+ <?php endif ?>
<?= t('Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).') ?>
</p>
</div>
diff --git a/app/Template/user/create_local.php b/app/Template/user/create_local.php
index 6e6ca6ac..38bd7836 100644
--- a/app/Template/user/create_local.php
+++ b/app/Template/user/create_local.php
@@ -12,34 +12,35 @@
<div class="form-column">
<?= $this->form->label(t('Username'), 'username') ?>
- <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?><br/>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
<?= $this->form->label(t('Name'), 'name') ?>
- <?= $this->form->text('name', $values, $errors) ?><br/>
+ <?= $this->form->text('name', $values, $errors) ?>
<?= $this->form->label(t('Email'), 'email') ?>
- <?= $this->form->email('email', $values, $errors) ?><br/>
+ <?= $this->form->email('email', $values, $errors) ?>
<?= $this->form->label(t('Password'), 'password') ?>
- <?= $this->form->password('password', $values, $errors, array('required')) ?><br/>
+ <?= $this->form->password('password', $values, $errors, array('required')) ?>
<?= $this->form->label(t('Confirmation'), 'confirmation') ?>
- <?= $this->form->password('confirmation', $values, $errors, array('required')) ?><br/>
+ <?= $this->form->password('confirmation', $values, $errors, array('required')) ?>
</div>
<div class="form-column">
<?= $this->form->label(t('Add project member'), 'project_id') ?>
- <?= $this->form->select('project_id', $projects, $values, $errors) ?><br/>
+ <?= $this->form->select('project_id', $projects, $values, $errors) ?>
<?= $this->form->label(t('Timezone'), 'timezone') ?>
- <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/>
+ <?= $this->form->select('timezone', $timezones, $values, $errors) ?>
<?= $this->form->label(t('Language'), 'language') ?>
- <?= $this->form->select('language', $languages, $values, $errors) ?><br/>
+ <?= $this->form->select('language', $languages, $values, $errors) ?>
+
+ <?= $this->form->label(t('Role'), 'role') ?>
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
<?= $this->form->checkbox('notifications_enabled', t('Enable email notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?>
- <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?>
- <?= $this->form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1 ? true : false) ?>
</div>
<div class="form-actions">
diff --git a/app/Template/user/create_remote.php b/app/Template/user/create_remote.php
index 49d1548c..1cc560cd 100644
--- a/app/Template/user/create_remote.php
+++ b/app/Template/user/create_remote.php
@@ -12,37 +12,38 @@
<div class="form-column">
<?= $this->form->label(t('Username'), 'username') ?>
- <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?><br/>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
<?= $this->form->label(t('Name'), 'name') ?>
- <?= $this->form->text('name', $values, $errors) ?><br/>
+ <?= $this->form->text('name', $values, $errors) ?>
<?= $this->form->label(t('Email'), 'email') ?>
- <?= $this->form->email('email', $values, $errors) ?><br/>
+ <?= $this->form->email('email', $values, $errors) ?>
<?= $this->form->label(t('Google Id'), 'google_id') ?>
- <?= $this->form->text('google_id', $values, $errors) ?><br/>
+ <?= $this->form->text('google_id', $values, $errors) ?>
<?= $this->form->label(t('Github Id'), 'github_id') ?>
- <?= $this->form->text('github_id', $values, $errors) ?><br/>
+ <?= $this->form->text('github_id', $values, $errors) ?>
<?= $this->form->label(t('Gitlab Id'), 'gitlab_id') ?>
- <?= $this->form->text('gitlab_id', $values, $errors) ?><br/>
+ <?= $this->form->text('gitlab_id', $values, $errors) ?>
</div>
<div class="form-column">
<?= $this->form->label(t('Add project member'), 'project_id') ?>
- <?= $this->form->select('project_id', $projects, $values, $errors) ?><br/>
+ <?= $this->form->select('project_id', $projects, $values, $errors) ?>
<?= $this->form->label(t('Timezone'), 'timezone') ?>
- <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/>
+ <?= $this->form->select('timezone', $timezones, $values, $errors) ?>
<?= $this->form->label(t('Language'), 'language') ?>
- <?= $this->form->select('language', $languages, $values, $errors) ?><br/>
+ <?= $this->form->select('language', $languages, $values, $errors) ?>
+
+ <?= $this->form->label(t('Role'), 'role') ?>
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
<?= $this->form->checkbox('notifications_enabled', t('Enable email notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?>
- <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?>
- <?= $this->form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1 ? true : false) ?>
<?= $this->form->checkbox('disable_login_form', t('Disallow login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?>
</div>
diff --git a/app/Template/user/edit.php b/app/Template/user/edit.php
index cd10b2ab..1a7fb430 100644
--- a/app/Template/user/edit.php
+++ b/app/Template/user/edit.php
@@ -8,23 +8,23 @@
<?= $this->form->hidden('id', $values) ?>
<?= $this->form->label(t('Username'), 'username') ?>
- <?= $this->form->text('username', $values, $errors, array('required', $values['is_ldap_user'] == 1 ? 'readonly' : '', 'maxlength="50"')) ?><br/>
+ <?= $this->form->text('username', $values, $errors, array('required', $values['is_ldap_user'] == 1 ? 'readonly' : '', 'maxlength="50"')) ?>
<?= $this->form->label(t('Name'), 'name') ?>
- <?= $this->form->text('name', $values, $errors) ?><br/>
+ <?= $this->form->text('name', $values, $errors) ?>
<?= $this->form->label(t('Email'), 'email') ?>
- <?= $this->form->email('email', $values, $errors) ?><br/>
+ <?= $this->form->email('email', $values, $errors) ?>
<?= $this->form->label(t('Timezone'), 'timezone') ?>
- <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/>
+ <?= $this->form->select('timezone', $timezones, $values, $errors) ?>
<?= $this->form->label(t('Language'), 'language') ?>
- <?= $this->form->select('language', $languages, $values, $errors) ?><br/>
+ <?= $this->form->select('language', $languages, $values, $errors) ?>
<?php if ($this->user->isAdmin()): ?>
- <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1) ?>
- <?= $this->form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1) ?>
+ <?= $this->form->label(t('Role'), 'role') ?>
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
<?php endif ?>
<div class="form-actions">
diff --git a/app/Template/user/external.php b/app/Template/user/external.php
index 7a42f38e..8b1d3c46 100644
--- a/app/Template/user/external.php
+++ b/app/Template/user/external.php
@@ -10,7 +10,7 @@
<?php if (empty($user['google_id'])): ?>
<?= $this->url->link(t('Link my Google Account'), 'oauth', 'google', array(), true) ?>
<?php else: ?>
- <?= $this->url->link(t('Unlink my Google Account'), 'oauth', 'unlink', array('backend' => 'google'), true) ?>
+ <?= $this->url->link(t('Unlink my Google Account'), 'oauth', 'unlink', array('backend' => 'Google'), true) ?>
<?php endif ?>
<?php else: ?>
<?= empty($user['google_id']) ? t('No account linked.') : t('Account linked.') ?>
@@ -26,7 +26,7 @@
<?php if (empty($user['github_id'])): ?>
<?= $this->url->link(t('Link my Github Account'), 'oauth', 'github', array(), true) ?>
<?php else: ?>
- <?= $this->url->link(t('Unlink my Github Account'), 'oauth', 'unlink', array('backend' => 'github'), true) ?>
+ <?= $this->url->link(t('Unlink my Github Account'), 'oauth', 'unlink', array('backend' => 'Github'), true) ?>
<?php endif ?>
<?php else: ?>
<?= empty($user['github_id']) ? t('No account linked.') : t('Account linked.') ?>
@@ -42,7 +42,7 @@
<?php if (empty($user['gitlab_id'])): ?>
<?= $this->url->link(t('Link my Gitlab Account'), 'oauth', 'gitlab', array(), true) ?>
<?php else: ?>
- <?= $this->url->link(t('Unlink my Gitlab Account'), 'oauth', 'unlink', array('backend' => 'gitlab'), true) ?>
+ <?= $this->url->link(t('Unlink my Gitlab Account'), 'oauth', 'unlink', array('backend' => 'Gitlab'), true) ?>
<?php endif ?>
<?php else: ?>
<?= empty($user['gitlab_id']) ? t('No account linked.') : t('Account linked.') ?>
diff --git a/app/Template/user/index.php b/app/Template/user/index.php
index 7c6ecc1e..cb7416d6 100644
--- a/app/Template/user/index.php
+++ b/app/Template/user/index.php
@@ -1,6 +1,6 @@
<section id="main">
<div class="page-header">
- <?php if ($this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('user', 'create')): ?>
<ul>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New local user'), 'user', 'create') ?></li>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New remote user'), 'user', 'create', array('remote' => 1)) ?></li>
@@ -18,8 +18,7 @@
<th><?= $paginator->order(t('Username'), 'username') ?></th>
<th><?= $paginator->order(t('Name'), 'name') ?></th>
<th><?= $paginator->order(t('Email'), 'email') ?></th>
- <th><?= $paginator->order(t('Administrator'), 'is_admin') ?></th>
- <th><?= $paginator->order(t('Project Administrator'), 'is_project_admin') ?></th>
+ <th><?= $paginator->order(t('Role'), 'role') ?></th>
<th><?= $paginator->order(t('Two factor authentication'), 'twofactor_activated') ?></th>
<th><?= $paginator->order(t('Notifications'), 'notifications_enabled') ?></th>
<th><?= $paginator->order(t('Account type'), 'is_ldap_user') ?></th>
@@ -39,10 +38,7 @@
<a href="mailto:<?= $this->e($user['email']) ?>"><?= $this->e($user['email']) ?></a>
</td>
<td>
- <?= $user['is_admin'] ? t('Yes') : t('No') ?>
- </td>
- <td>
- <?= $user['is_project_admin'] ? t('Yes') : t('No') ?>
+ <?= $this->user->getRoleName($user['role']) ?>
</td>
<td>
<?= $user['twofactor_activated'] ? t('Yes') : t('No') ?>
diff --git a/app/Template/user/layout.php b/app/Template/user/layout.php
index a27f359b..1e456348 100644
--- a/app/Template/user/layout.php
+++ b/app/Template/user/layout.php
@@ -1,6 +1,6 @@
<section id="main">
<div class="page-header">
- <?php if ($this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('user', 'create')): ?>
<ul>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New local user'), 'user', 'create') ?></li>
diff --git a/app/Template/user/sessions.php b/app/Template/user/sessions.php
index eabf3672..7a66c5ad 100644
--- a/app/Template/user/sessions.php
+++ b/app/Template/user/sessions.php
@@ -19,7 +19,7 @@
<td><?= dt('%B %e, %Y at %k:%M %p', $session['expiration']) ?></td>
<td><?= $this->e($session['ip']) ?></td>
<td><?= $this->e($session['user_agent']) ?></td>
- <td><?= $this->url->link(t('Remove'), 'user', 'removeSession', array('user_id' => $user['id'], 'id' => $session['id']), true) ?></td>
+ <td><?= $this->url->link(t('Remove'), 'User', 'removeSession', array('user_id' => $user['id'], 'id' => $session['id']), true) ?></td>
</tr>
<?php endforeach ?>
</table>
diff --git a/app/Template/user/show.php b/app/Template/user/show.php
index 220ad87e..89c6b36b 100644
--- a/app/Template/user/show.php
+++ b/app/Template/user/show.php
@@ -11,7 +11,7 @@
<h2><?= t('Security') ?></h2>
</div>
<ul class="listing">
- <li><?= t('Group:') ?> <strong><?= $user['is_admin'] ? t('Administrator') : ($user['is_project_admin'] ? t('Project Administrator') : t('Regular user')) ?></strong></li>
+ <li><?= t('Role:') ?> <strong><?= $this->user->getRoleName($user['role']) ?></strong></li>
<li><?= t('Account type:') ?> <strong><?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?></strong></li>
<li><?= $user['twofactor_activated'] == 1 ? t('Two factor authentication enabled') : t('Two factor authentication disabled') ?></li>
</ul>
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php
index 167c8054..011994b9 100644
--- a/app/Template/user/sidebar.php
+++ b/app/Template/user/sidebar.php
@@ -41,7 +41,7 @@
<li <?= $this->app->getRouterController() === 'twofactor' && $this->app->getRouterAction() === 'index' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Two factor authentication'), 'twofactor', 'index', array('user_id' => $user['id'])) ?>
</li>
- <?php elseif ($this->user->isAdmin() && $user['twofactor_activated'] == 1): ?>
+ <?php elseif ($this->user->hasAccess('twofactor', 'disable') && $user['twofactor_activated'] == 1): ?>
<li <?= $this->app->getRouterController() === 'twofactor' && $this->app->getRouterAction() === 'disable' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Two factor authentication'), 'twofactor', 'disable', array('user_id' => $user['id'])) ?>
</li>
@@ -61,7 +61,7 @@
</li>
<?php endif ?>
- <?php if ($this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('user', 'authentication')): ?>
<li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'authentication' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Edit Authentication'), 'user', 'authentication', array('user_id' => $user['id'])) ?>
</li>
@@ -69,7 +69,7 @@
<?= $this->hook->render('template:user:sidebar:actions', array('user' => $user)) ?>
- <?php if ($this->user->isAdmin() && ! $this->user->isCurrentUser($user['id'])): ?>
+ <?php if ($this->user->hasAccess('user', 'remove') && ! $this->user->isCurrentUser($user['id'])): ?>
<li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'remove' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Remove'), 'user', 'remove', array('user_id' => $user['id'])) ?>
</li>
diff --git a/app/Template/user_import/step1.php b/app/Template/user_import/step1.php
index 7256bfa6..69643d6d 100644
--- a/app/Template/user_import/step1.php
+++ b/app/Template/user_import/step1.php
@@ -1,6 +1,6 @@
<section id="main">
<div class="page-header">
- <?php if ($this->user->isAdmin()): ?>
+ <?php if ($this->user->hasAccess('user', 'create')): ?>
<ul>
<li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li>
<li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New local user'), 'user', 'create') ?></li>
diff --git a/app/User/DatabaseUserProvider.php b/app/User/DatabaseUserProvider.php
new file mode 100644
index 00000000..b6d41186
--- /dev/null
+++ b/app/User/DatabaseUserProvider.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace Kanboard\User;
+
+use Kanboard\Core\User\UserProviderInterface;
+use Kanboard\Core\Security\Role;
+
+/**
+ * Database User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class DatabaseUserProvider implements UserProviderInterface
+{
+ /**
+ * User properties
+ *
+ * @access private
+ * @var array
+ */
+ private $user = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $user
+ */
+ public function __construct(array $user)
+ {
+ $this->user = $user;
+ }
+
+ /**
+ * Return true to allow automatic user creation
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isUserCreationAllowed()
+ {
+ return false;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return string
+ */
+ public function getInternalId()
+ {
+ return $this->user['id'];
+ }
+
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn()
+ {
+ return '';
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get user role
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole()
+ {
+ return '';
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return '';
+ }
+
+ /**
+ * Get full name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return '';
+ }
+
+ /**
+ * Get user email
+ *
+ * @access public
+ * @return string
+ */
+ public function getEmail()
+ {
+ return '';
+ }
+
+ /**
+ * Get external group ids
+ *
+ * @access public
+ * @return array
+ */
+ public function getExternalGroupIds()
+ {
+ return array();
+ }
+
+ /**
+ * Get extra user attributes
+ *
+ * @access public
+ * @return array
+ */
+ public function getExtraAttributes()
+ {
+ return array();
+ }
+}
diff --git a/app/User/GithubUserProvider.php b/app/User/GithubUserProvider.php
new file mode 100644
index 00000000..ae3d7477
--- /dev/null
+++ b/app/User/GithubUserProvider.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Kanboard\User;
+
+/**
+ * Github OAuth User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class GithubUserProvider extends OAuthUserProvider
+{
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn()
+ {
+ return 'github_id';
+ }
+}
diff --git a/app/User/GitlabUserProvider.php b/app/User/GitlabUserProvider.php
new file mode 100644
index 00000000..a73472c8
--- /dev/null
+++ b/app/User/GitlabUserProvider.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Kanboard\User;
+
+/**
+ * Gitlab OAuth User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class GitlabUserProvider extends OAuthUserProvider
+{
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn()
+ {
+ return 'gitlab_id';
+ }
+}
diff --git a/app/User/GoogleUserProvider.php b/app/User/GoogleUserProvider.php
new file mode 100644
index 00000000..baa55e03
--- /dev/null
+++ b/app/User/GoogleUserProvider.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Kanboard\User;
+
+/**
+ * Google OAuth User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class GoogleUserProvider extends OAuthUserProvider
+{
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn()
+ {
+ return 'google_id';
+ }
+}
diff --git a/app/User/LdapUserProvider.php b/app/User/LdapUserProvider.php
new file mode 100644
index 00000000..9dfb2380
--- /dev/null
+++ b/app/User/LdapUserProvider.php
@@ -0,0 +1,206 @@
+<?php
+
+namespace Kanboard\User;
+
+use Kanboard\Core\User\UserProviderInterface;
+
+/**
+ * LDAP User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class LdapUserProvider implements UserProviderInterface
+{
+ /**
+ * LDAP DN
+ *
+ * @access private
+ * @var string
+ */
+ private $dn;
+
+ /**
+ * LDAP username
+ *
+ * @access private
+ * @var string
+ */
+ private $username;
+
+ /**
+ * User name
+ *
+ * @access private
+ * @var string
+ */
+ private $name;
+
+ /**
+ * Email
+ *
+ * @access private
+ * @var string
+ */
+ private $email;
+
+ /**
+ * User role
+ *
+ * @access private
+ * @var string
+ */
+ private $role;
+
+ /**
+ * Group LDAP DNs
+ *
+ * @access private
+ * @var string[]
+ */
+ private $groupIds;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $dn
+ * @param string $username
+ * @param string $name
+ * @param string $email
+ * @param string $role
+ * @param string[]
+ */
+ public function __construct($dn, $username, $name, $email, $role, array $groupIds)
+ {
+ $this->dn = $dn;
+ $this->username = $username;
+ $this->name = $name;
+ $this->email = $email;
+ $this->role = $role;
+ $this->groupIds = $groupIds;
+ }
+
+ /**
+ * Return true to allow automatic user creation
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isUserCreationAllowed()
+ {
+ return LDAP_USER_CREATION;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return string
+ */
+ public function getInternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn()
+ {
+ return 'username';
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return $this->getUsername();
+ }
+
+ /**
+ * Get user role
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole()
+ {
+ return $this->role;
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return LDAP_USERNAME_CASE_SENSITIVE ? $this->username : strtolower($this->username);
+ }
+
+ /**
+ * Get full name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get user email
+ *
+ * @access public
+ * @return string
+ */
+ public function getEmail()
+ {
+ return $this->email;
+ }
+
+ /**
+ * Get groups
+ *
+ * @access public
+ * @return array
+ */
+ public function getExternalGroupIds()
+ {
+ return $this->groupIds;
+ }
+
+ /**
+ * Get extra user attributes
+ *
+ * @access public
+ * @return array
+ */
+ public function getExtraAttributes()
+ {
+ return array(
+ 'is_ldap_user' => 1,
+ );
+ }
+
+ /**
+ * Get User DN
+ *
+ * @access public
+ * @return string
+ */
+ public function getDn()
+ {
+ return $this->dn;
+ }
+}
diff --git a/app/User/OAuthUserProvider.php b/app/User/OAuthUserProvider.php
new file mode 100644
index 00000000..3879fa76
--- /dev/null
+++ b/app/User/OAuthUserProvider.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Kanboard\User;
+
+use Kanboard\Core\User\UserProviderInterface;
+use Kanboard\Core\Security\Role;
+
+/**
+ * OAuth User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+abstract class OAuthUserProvider implements UserProviderInterface
+{
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ abstract public function getExternalIdColumn();
+
+ /**
+ * User properties
+ *
+ * @access private
+ * @var array
+ */
+ private $user = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param array $user
+ */
+ public function __construct(array $user)
+ {
+ $this->user = $user;
+ }
+
+ /**
+ * Return true to allow automatic user creation
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isUserCreationAllowed()
+ {
+ return false;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return string
+ */
+ public function getInternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return $this->user['id'];
+ }
+
+ /**
+ * Get user role
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole()
+ {
+ return '';
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return '';
+ }
+
+ /**
+ * Get full name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->user['name'];
+ }
+
+ /**
+ * Get user email
+ *
+ * @access public
+ * @return string
+ */
+ public function getEmail()
+ {
+ return $this->user['email'];
+ }
+
+ /**
+ * Get external group ids
+ *
+ * @access public
+ * @return array
+ */
+ public function getExternalGroupIds()
+ {
+ return array();
+ }
+
+ /**
+ * Get extra user attributes
+ *
+ * @access public
+ * @return array
+ */
+ public function getExtraAttributes()
+ {
+ return array();
+ }
+}
diff --git a/app/User/ReverseProxyUserProvider.php b/app/User/ReverseProxyUserProvider.php
new file mode 100644
index 00000000..071330df
--- /dev/null
+++ b/app/User/ReverseProxyUserProvider.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Kanboard\User;
+
+use Kanboard\Core\User\UserProviderInterface;
+use Kanboard\Core\Security\Role;
+
+/**
+ * Reverse Proxy User Provider
+ *
+ * @package user
+ * @author Frederic Guillot
+ */
+class ReverseProxyUserProvider implements UserProviderInterface
+{
+ /**
+ * Username
+ *
+ * @access private
+ * @var string
+ */
+ private $username = '';
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $username
+ */
+ public function __construct($username)
+ {
+ $this->username = $username;
+ }
+
+ /**
+ * Return true to allow automatic user creation
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isUserCreationAllowed()
+ {
+ return true;
+ }
+
+ /**
+ * Get internal id
+ *
+ * @access public
+ * @return string
+ */
+ public function getInternalId()
+ {
+ return '';
+ }
+
+ /**
+ * Get external id column name
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalIdColumn()
+ {
+ return 'username';
+ }
+
+ /**
+ * Get external id
+ *
+ * @access public
+ * @return string
+ */
+ public function getExternalId()
+ {
+ return $this->username;
+ }
+
+ /**
+ * Get user role
+ *
+ * @access public
+ * @return string
+ */
+ public function getRole()
+ {
+ return REVERSE_PROXY_DEFAULT_ADMIN === $this->username ? Role::APP_ADMIN : Role::APP_USER;
+ }
+
+ /**
+ * Get username
+ *
+ * @access public
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+
+ /**
+ * Get full name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return '';
+ }
+
+ /**
+ * Get user email
+ *
+ * @access public
+ * @return string
+ */
+ public function getEmail()
+ {
+ return REVERSE_PROXY_DEFAULT_DOMAIN !== '' ? $this->username.'@'.REVERSE_PROXY_DEFAULT_DOMAIN : '';
+ }
+
+ /**
+ * Get external group ids
+ *
+ * @access public
+ * @return array
+ */
+ public function getExternalGroupIds()
+ {
+ return array();
+ }
+
+ /**
+ * Get extra user attributes
+ *
+ * @access public
+ * @return array
+ */
+ public function getExtraAttributes()
+ {
+ return array(
+ 'is_ldap_user' => 1,
+ 'disable_login_form' => 1,
+ );
+ }
+}
diff --git a/app/common.php b/app/common.php
index 56f3c70f..fe5a7e69 100644
--- a/app/common.php
+++ b/app/common.php
@@ -1,6 +1,6 @@
<?php
-require dirname(__DIR__) . '/vendor/autoload.php';
+require __DIR__.'/../vendor/autoload.php';
// Automatically parse environment configuration (Heroku)
if (getenv('DATABASE_URL')) {
@@ -14,7 +14,6 @@ if (getenv('DATABASE_URL')) {
define('DB_NAME', ltrim($dbopts["path"], '/'));
}
-// Include custom config file
if (file_exists('config.php')) {
require 'config.php';
}
@@ -26,11 +25,10 @@ $container = new Pimple\Container;
$container->register(new Kanboard\ServiceProvider\SessionProvider);
$container->register(new Kanboard\ServiceProvider\LoggingProvider);
$container->register(new Kanboard\ServiceProvider\DatabaseProvider);
+$container->register(new Kanboard\ServiceProvider\AuthenticationProvider);
+$container->register(new Kanboard\ServiceProvider\NotificationProvider);
$container->register(new Kanboard\ServiceProvider\ClassProvider);
$container->register(new Kanboard\ServiceProvider\EventDispatcherProvider);
-
-if (ENABLE_URL_REWRITE) {
- require __DIR__.'/routes.php';
-}
-
-$container['pluginLoader']->scan();
+$container->register(new Kanboard\ServiceProvider\GroupProvider);
+$container->register(new Kanboard\ServiceProvider\RouteProvider);
+$container->register(new Kanboard\ServiceProvider\PluginProvider);
diff --git a/app/constants.php b/app/constants.php
index 4c22f760..da3de840 100644
--- a/app/constants.php
+++ b/app/constants.php
@@ -27,21 +27,29 @@ defined('DB_PORT') or define('DB_PORT', null);
defined('LDAP_AUTH') or define('LDAP_AUTH', false);
defined('LDAP_SERVER') or define('LDAP_SERVER', '');
defined('LDAP_PORT') or define('LDAP_PORT', 389);
-defined('LDAP_START_TLS') or define('LDAP_START_TLS', false);
defined('LDAP_SSL_VERIFY') or define('LDAP_SSL_VERIFY', true);
+defined('LDAP_START_TLS') or define('LDAP_START_TLS', false);
+defined('LDAP_USERNAME_CASE_SENSITIVE') or define('LDAP_USERNAME_CASE_SENSITIVE', false);
+
defined('LDAP_BIND_TYPE') or define('LDAP_BIND_TYPE', 'anonymous');
defined('LDAP_USERNAME') or define('LDAP_USERNAME', null);
defined('LDAP_PASSWORD') or define('LDAP_PASSWORD', null);
-defined('LDAP_ACCOUNT_BASE') or define('LDAP_ACCOUNT_BASE', '');
-defined('LDAP_USER_PATTERN') or define('LDAP_USER_PATTERN', '');
-defined('LDAP_ACCOUNT_FULLNAME') or define('LDAP_ACCOUNT_FULLNAME', 'displayname');
-defined('LDAP_ACCOUNT_EMAIL') or define('LDAP_ACCOUNT_EMAIL', 'mail');
-defined('LDAP_ACCOUNT_ID') or define('LDAP_ACCOUNT_ID', '');
-defined('LDAP_ACCOUNT_MEMBEROF') or define('LDAP_ACCOUNT_MEMBEROF', 'memberof');
-defined('LDAP_ACCOUNT_CREATION') or define('LDAP_ACCOUNT_CREATION', true);
+
+defined('LDAP_USER_BASE_DN') or define('LDAP_USER_BASE_DN', '');
+defined('LDAP_USER_FILTER') or define('LDAP_USER_FILTER', '');
+defined('LDAP_USER_ATTRIBUTE_USERNAME') or define('LDAP_USER_ATTRIBUTE_USERNAME', 'uid');
+defined('LDAP_USER_ATTRIBUTE_FULLNAME') or define('LDAP_USER_ATTRIBUTE_FULLNAME', 'cn');
+defined('LDAP_USER_ATTRIBUTE_EMAIL') or define('LDAP_USER_ATTRIBUTE_EMAIL', 'mail');
+defined('LDAP_USER_ATTRIBUTE_GROUPS') or define('LDAP_USER_ATTRIBUTE_GROUPS', 'memberof');
+defined('LDAP_USER_CREATION') or define('LDAP_USER_CREATION', true);
+
defined('LDAP_GROUP_ADMIN_DN') or define('LDAP_GROUP_ADMIN_DN', '');
-defined('LDAP_GROUP_PROJECT_ADMIN_DN') or define('LDAP_GROUP_PROJECT_ADMIN_DN', '');
-defined('LDAP_USERNAME_CASE_SENSITIVE') or define('LDAP_USERNAME_CASE_SENSITIVE', false);
+defined('LDAP_GROUP_MANAGER_DN') or define('LDAP_GROUP_MANAGER_DN', '');
+
+defined('LDAP_GROUP_PROVIDER') or define('LDAP_GROUP_PROVIDER', false);
+defined('LDAP_GROUP_BASE_DN') or define('LDAP_GROUP_BASE_DN', '');
+defined('LDAP_GROUP_FILTER') or define('LDAP_GROUP_FILTER', '');
+defined('LDAP_GROUP_ATTRIBUTE_NAME') or define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn');
// Google authentication
defined('GOOGLE_AUTH') or define('GOOGLE_AUTH', false);
diff --git a/app/routes.php b/app/routes.php
deleted file mode 100644
index 159e8f6e..00000000
--- a/app/routes.php
+++ /dev/null
@@ -1,117 +0,0 @@
-<?php
-
-// Dashboard
-$container['router']->addRoute('dashboard', 'app', 'index');
-$container['router']->addRoute('dashboard/:user_id', 'app', 'index', array('user_id'));
-$container['router']->addRoute('dashboard/:user_id/projects', 'app', 'projects', array('user_id'));
-$container['router']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks', array('user_id'));
-$container['router']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks', array('user_id'));
-$container['router']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar', array('user_id'));
-$container['router']->addRoute('dashboard/:user_id/activity', 'app', 'activity', array('user_id'));
-
-// Search routes
-$container['router']->addRoute('search', 'search', 'index');
-$container['router']->addRoute('search/:search', 'search', 'index', array('search'));
-
-// Project routes
-$container['router']->addRoute('projects', 'project', 'index');
-$container['router']->addRoute('project/create', 'project', 'create');
-$container['router']->addRoute('project/create/:private', 'project', 'create', array('private'));
-$container['router']->addRoute('project/:project_id', 'project', 'show', array('project_id'));
-$container['router']->addRoute('p/:project_id', 'project', 'show', array('project_id'));
-$container['router']->addRoute('project/:project_id/share', 'project', 'share', array('project_id'));
-$container['router']->addRoute('project/:project_id/edit', 'project', 'edit', array('project_id'));
-$container['router']->addRoute('project/:project_id/integration', 'project', 'integration', array('project_id'));
-$container['router']->addRoute('project/:project_id/users', 'project', 'users', array('project_id'));
-$container['router']->addRoute('project/:project_id/duplicate', 'project', 'duplicate', array('project_id'));
-$container['router']->addRoute('project/:project_id/remove', 'project', 'remove', array('project_id'));
-$container['router']->addRoute('project/:project_id/disable', 'project', 'disable', array('project_id'));
-$container['router']->addRoute('project/:project_id/enable', 'project', 'enable', array('project_id'));
-
-// Action routes
-$container['router']->addRoute('project/:project_id/actions', 'action', 'index', array('project_id'));
-$container['router']->addRoute('project/:project_id/action/:action_id/confirm', 'action', 'confirm', array('project_id', 'action_id'));
-
-// Column routes
-$container['router']->addRoute('project/:project_id/columns', 'column', 'index', array('project_id'));
-$container['router']->addRoute('project/:project_id/column/:column_id/edit', 'column', 'edit', array('project_id', 'column_id'));
-$container['router']->addRoute('project/:project_id/column/:column_id/confirm', 'column', 'confirm', array('project_id', 'column_id'));
-$container['router']->addRoute('project/:project_id/column/:column_id/move/:direction', 'column', 'move', array('project_id', 'column_id', 'direction'));
-
-// Swimlane routes
-$container['router']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index', array('project_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/edit', 'swimlane', 'edit', array('project_id', 'swimlane_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/confirm', 'swimlane', 'confirm', array('project_id', 'swimlane_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/disable', 'swimlane', 'disable', array('project_id', 'swimlane_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/enable', 'swimlane', 'enable', array('project_id', 'swimlane_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/up', 'swimlane', 'moveup', array('project_id', 'swimlane_id'));
-$container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/down', 'swimlane', 'movedown', array('project_id', 'swimlane_id'));
-
-// Category routes
-$container['router']->addRoute('project/:project_id/categories', 'category', 'index', array('project_id'));
-$container['router']->addRoute('project/:project_id/category/:category_id/edit', 'category', 'edit', array('project_id', 'category_id'));
-$container['router']->addRoute('project/:project_id/category/:category_id/confirm', 'category', 'confirm', array('project_id', 'category_id'));
-
-// Task routes
-$container['router']->addRoute('project/:project_id/task/:task_id', 'task', 'show', array('project_id', 'task_id'));
-$container['router']->addRoute('t/:task_id', 'task', 'show', array('task_id'));
-$container['router']->addRoute('public/task/:task_id/:token', 'task', 'readonly', array('task_id', 'token'));
-
-$container['router']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove', array('project_id', 'task_id'));
-
-$container['router']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence', array('project_id', 'task_id'));
-
-$container['router']->addRoute('project/:project_id/task/:task_id/close', 'taskstatus', 'close', array('task_id', 'project_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/open', 'taskstatus', 'open', array('task_id', 'project_id'));
-
-$container['router']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate', array('task_id', 'project_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy', array('task_id', 'project_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy', array('task_id', 'project_id', 'dst_project_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move', array('task_id', 'project_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move', array('task_id', 'project_id', 'dst_project_id'));
-
-// Board routes
-$container['router']->addRoute('board/:project_id', 'board', 'show', array('project_id'));
-$container['router']->addRoute('b/:project_id', 'board', 'show', array('project_id'));
-$container['router']->addRoute('public/board/:token', 'board', 'readonly', array('token'));
-
-// Calendar routes
-$container['router']->addRoute('calendar/:project_id', 'calendar', 'show', array('project_id'));
-$container['router']->addRoute('c/:project_id', 'calendar', 'show', array('project_id'));
-
-// Listing routes
-$container['router']->addRoute('list/:project_id', 'listing', 'show', array('project_id'));
-$container['router']->addRoute('l/:project_id', 'listing', 'show', array('project_id'));
-
-// Gantt routes
-$container['router']->addRoute('gantt/:project_id', 'gantt', 'project', array('project_id'));
-$container['router']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project', array('project_id', 'sorting'));
-
-// Subtask routes
-$container['router']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create', array('project_id', 'task_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm', array('project_id', 'task_id', 'subtask_id'));
-$container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/edit', 'subtask', 'edit', array('project_id', 'task_id', 'subtask_id'));
-
-// Feed routes
-$container['router']->addRoute('feed/project/:token', 'feed', 'project', array('token'));
-$container['router']->addRoute('feed/user/:token', 'feed', 'user', array('token'));
-
-// Ical routes
-$container['router']->addRoute('ical/project/:token', 'ical', 'project', array('token'));
-$container['router']->addRoute('ical/user/:token', 'ical', 'user', array('token'));
-
-// Auth routes
-$container['router']->addRoute('oauth/google', 'oauth', 'google');
-$container['router']->addRoute('oauth/github', 'oauth', 'github');
-$container['router']->addRoute('oauth/gitlab', 'oauth', 'gitlab');
-$container['router']->addRoute('login', 'auth', 'login');
-$container['router']->addRoute('logout', 'auth', 'logout');
diff --git a/assets/js/app.js b/assets/js/app.js
index d942b644..308615a4 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -51,4 +51,4 @@ c,a,e),l[d.key][c?"unshift":"push"]({callback:b,modifiers:d.modifiers,action:d.a
unbind:function(a,b){return m.bind(a,function(){},b)},trigger:function(a,b){if(q[a+":"+b])q[a+":"+b]({},a);return this},reset:function(){l={};q={};return this},stopCallback:function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable},handleKey:function(a,b,d){var c=C(a,b,d),e;b={};var f=0,g=!1;for(e=0;e<c.length;++e)c[e].seq&&(f=Math.max(f,c[e].level));for(e=0;e<c.length;++e)c[e].seq?c[e].level==f&&(g=!0,
b[c[e].seq]=1,x(c[e].callback,d,c[e].combo,c[e].seq)):g||x(c[e].callback,d,c[e].combo);c="keypress"==d.type&&I;d.type!=u||w(a)||c||t(b);I=g&&"keydown"==d.type}};J.Mousetrap=m;"function"===typeof define&&define.amd&&define(m)})(window,document);
Mousetrap=function(a){var d={},e=a.stopCallback;a.stopCallback=function(b,c,a){return d[a]?!1:e(b,c,a)};a.bindGlobal=function(b,c,e){a.bind(b,c,e);if(b instanceof Array)for(c=0;c<b.length;c++)d[b[c]]=!0;else d[b]=!0};return a}(Mousetrap);
-!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return a>1&&5>a&&1!==~~(a/10)}function d(a,b,d,e){var f=a+" ";switch(d){case"s":return b||e?"pár sekund":"pár sekundami";case"m":return b?"minuta":e?"minutu":"minutou";case"mm":return b||e?f+(c(a)?"minuty":"minut"):f+"minutami";case"h":return b?"hodina":e?"hodinu":"hodinou";case"hh":return b||e?f+(c(a)?"hodiny":"hodin"):f+"hodinami";case"d":return b||e?"den":"dnem";case"dd":return b||e?f+(c(a)?"dny":"dní"):f+"dny";case"M":return b||e?"měsíc":"měsícem";case"MM":return b||e?f+(c(a)?"měsíce":"měsíců"):f+"měsíci";case"y":return b||e?"rok":"rokem";case"yy":return b||e?f+(c(a)?"roky":"let"):f+"lety"}}var e="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),f="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_");(b.defineLocale||b.lang).call(b,"cs",{months:e,monthsShort:f,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(e,f),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd D. MMMM YYYY LT"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:d,m:d,mm:d,h:d,hh:d,d:d,dd:d,M:d,MM:d,y:d,yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("cs","cs",{closeText:"Zavřít",prevText:"&#x3C;Dříve",nextText:"Později&#x3E;",currentText:"Nyní",monthNames:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"],monthNamesShort:["led","úno","bře","dub","kvě","čer","čvc","srp","zář","říj","lis","pro"],dayNames:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"],dayNamesShort:["ne","po","út","st","čt","pá","so"],dayNamesMin:["ne","po","út","st","čt","pá","so"],weekHeader:"Týd",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("cs",{buttonText:{month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},allDayText:"Celý den",eventLimitText:function(a){return"+další: "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd [d.] D. MMMM YYYY LT"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("da","da",{closeText:"Luk",prevText:"&#x3C;Forrige",nextText:"Næste&#x3E;",currentText:"Idag",monthNames:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNames:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],dayNamesShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayNamesMin:["Sø","Ma","Ti","On","To","Fr","Lø"],weekHeader:"Uge",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("da",{buttonText:{month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"flere"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}(b.defineLocale||b.lang).call(b,"de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Heute um] LT [Uhr]",sameElse:"L",nextDay:"[Morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[Gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:c,mm:"%d Minuten",h:c,hh:"%d Stunden",d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("de","de",{closeText:"Schließen",prevText:"&#x3C;Zurück",nextText:"Vor&#x3E;",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),d="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");(b.defineLocale||b.lang).call(b,"es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mi_Ju_Vi_Sá".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("es","es",{closeText:"Cerrar",prevText:"&#x3C;Ant",nextText:"Sig&#x3E;",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("es",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo<br/>el día",eventLimitText:"más"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,e){var f="";switch(c){case"s":return e?"muutaman sekunnin":"muutama sekunti";case"m":return e?"minuutin":"minuutti";case"mm":f=e?"minuutin":"minuuttia";break;case"h":return e?"tunnin":"tunti";case"hh":f=e?"tunnin":"tuntia";break;case"d":return e?"päivän":"päivä";case"dd":f=e?"päivän":"päivää";break;case"M":return e?"kuukauden":"kuukausi";case"MM":f=e?"kuukauden":"kuukautta";break;case"y":return e?"vuoden":"vuosi";case"yy":f=e?"vuoden":"vuotta"}return f=d(a,e)+" "+f}function d(a,b){return 10>a?b?f[a]:e[a]:a}var e="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),f=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",e[7],e[8],e[9]];(b.defineLocale||b.lang).call(b,"fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] LT",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] LT",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] LT",llll:"ddd, Do MMM YYYY, [klo] LT"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fi","fi",{closeText:"Sulje",prevText:"&#xAB;Edellinen",nextText:"Seuraava&#xBB;",currentText:"Tänään",monthNames:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],monthNamesShort:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"],dayNamesShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayNames:["Sunnuntai","Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai"],dayNamesMin:["Su","Ma","Ti","Ke","To","Pe","La"],weekHeader:"Vk",dateFormat:"d.m.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fi",{buttonText:{month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},allDayText:"Koko päivä",eventLimitText:"lisää"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fr","fr",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr",{buttonText:{month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la<br/>journée",eventLimitText:"en plus"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e=a;switch(c){case"s":return d||b?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(d||b?" perc":" perce");case"mm":return e+(d||b?" perc":" perce");case"h":return"egy"+(d||b?" óra":" órája");case"hh":return e+(d||b?" óra":" órája");case"d":return"egy"+(d||b?" nap":" napja");case"dd":return e+(d||b?" nap":" napja");case"M":return"egy"+(d||b?" hónap":" hónapja");case"MM":return e+(d||b?" hónap":" hónapja");case"y":return"egy"+(d||b?" év":" éve");case"yy":return e+(d||b?" év":" éve")}return""}function d(a){return(a?"":"[múlt] ")+"["+e[this.day()]+"] LT[-kor]"}var e="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" ");(b.defineLocale||b.lang).call(b,"hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D., LT",LLLL:"YYYY. MMMM D., dddd LT"},meridiemParse:/de|du/i,isPM:function(a){return"u"===a.charAt(1).toLowerCase()},meridiem:function(a,b,c){return 12>a?c===!0?"de":"DE":c===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return d.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return d.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("hu","hu",{closeText:"bezár",prevText:"vissza",nextText:"előre",currentText:"ma",monthNames:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],monthNamesShort:["Jan","Feb","Már","Ápr","Máj","Jún","Júl","Aug","Szep","Okt","Nov","Dec"],dayNames:["Vasárnap","Hétfő","Kedd","Szerda","Csütörtök","Péntek","Szombat"],dayNamesShort:["Vas","Hét","Ked","Sze","Csü","Pén","Szo"],dayNamesMin:["V","H","K","Sze","Cs","P","Szo"],weekHeader:"Hét",dateFormat:"yy.mm.dd.",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("hu",{buttonText:{month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},allDayText:"Egész nap",eventLimitText:"további"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"LT.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] LT",LLLL:"dddd, D MMMM YYYY [pukul] LT"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"siang"===b?a>=11?a:a+12:"sore"===b||"malam"===b?a+12:void 0},meridiem:function(a,b,c){return 11>a?"pagi":15>a?"siang":19>a?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("id","id",{closeText:"Tutup",prevText:"&#x3C;mundur",nextText:"maju&#x3E;",currentText:"hari ini",monthNames:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","Nopember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agus","Sep","Okt","Nop","Des"],dayNames:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],dayNamesShort:["Min","Sen","Sel","Rab","kam","Jum","Sab"],dayNamesMin:["Mg","Sn","Sl","Rb","Km","jm","Sb"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("id",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayHtml:"Sehari<br/>penuh",eventLimitText:"lebih"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"D_L_Ma_Me_G_V_S".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(a){return(/^[0-9].+$/.test(a)?"tra":"in")+" "+a},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("it","it",{closeText:"Chiudi",prevText:"&#x3C;Prec",nextText:"Succ&#x3E;",currentText:"Oggi",monthNames:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthNamesShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],dayNames:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],dayNamesShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayNamesMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("it",{buttonText:{month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},allDayHtml:"Tutto il<br/>giorno",eventLimitText:function(a){return"+altri "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"LTs秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日LT",LLLL:"YYYY年M月D日LT dddd"},meridiemParse:/午前|午後/i,isPM:function(a){return"午後"===a},meridiem:function(a,b,c){return 12>a?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}}),a.fullCalendar.datepickerLang("ja","ja",{closeText:"閉じる",prevText:"&#x3C;前",nextText:"次&#x3E;",currentText:"今日",monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],dayNamesShort:["日","月","火","水","木","金","土"],dayNamesMin:["日","月","火","水","木","金","土"],weekHeader:"週",dateFormat:"yy/mm/dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("ja",{buttonText:{month:"月",week:"週",day:"日",list:"予定リスト"},allDayText:"終日",eventLimitText:function(a){return"他 "+a+" 件"}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),d="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_");(b.defineLocale||b.lang).call(b,"nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nl","nl",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nl",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tirs_ons_tors_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"H.mm",LTS:"LT.ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"for %s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nb","nb",{closeText:"Lukk",prevText:"&#xAB;Forrige",nextText:"Neste&#xBB;",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["søn","man","tir","ons","tor","fre","lør"],dayNames:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],dayNamesMin:["sø","ma","ti","on","to","fr","lø"],weekHeader:"Uke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nb",{buttonText:{month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"til"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return 5>a%10&&a%10>1&&~~(a/10)%10!==1}function d(a,b,d){var e=a+" ";switch(d){case"m":return b?"minuta":"minutę";case"mm":return e+(c(a)?"minuty":"minut");case"h":return b?"godzina":"godzinę";case"hh":return e+(c(a)?"godziny":"godzin");case"MM":return e+(c(a)?"miesiące":"miesięcy");case"yy":return e+(c(a)?"lata":"lat")}}var e="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),f="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_");(b.defineLocale||b.lang).call(b,"pl",{months:function(a,b){return/D MMMM/.test(b)?f[a.month()]:e[a.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"nie_pon_wt_śr_czw_pt_sb".split("_"),weekdaysMin:"N_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:d,mm:d,h:d,hh:d,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:d,y:"rok",yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pl","pl",{closeText:"Zamknij",prevText:"&#x3C;Poprzedni",nextText:"Następny&#x3E;",currentText:"Dziś",monthNames:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthNamesShort:["Sty","Lu","Mar","Kw","Maj","Cze","Lip","Sie","Wrz","Pa","Lis","Gru"],dayNames:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],dayNamesShort:["Nie","Pn","Wt","Śr","Czw","Pt","So"],dayNamesMin:["N","Pn","Wt","Śr","Cz","Pt","So"],weekHeader:"Tydz",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pl",{buttonText:{month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},allDayText:"Cały dzień",eventLimitText:"więcej"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pt","pt",{closeText:"Fechar",prevText:"Anterior",nextText:"Seguinte",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sem",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},allDayText:"Todo o dia",eventLimitText:"mais"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt-br",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] LT",LLLL:"dddd, D [de] MMMM [de] YYYY [às] LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"}),a.fullCalendar.datepickerLang("pt-br","pt-BR",{closeText:"Fechar",prevText:"&#x3C;Anterior",nextText:"Próximo&#x3E;",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt-br",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},allDayText:"dia inteiro",eventLimitText:function(a){return"mais +"+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function d(a,b,d){var e={mm:b?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===d?b?"минута":"минуту":a+" "+c(e[d],+a)}function e(a,b){var c={nominative:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_"),accusative:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function f(a,b){var c={nominative:"янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек".split("_"),accusative:"янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function g(a,b){var c={nominative:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),accusative:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_")},d=/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/.test(b)?"accusative":"nominative";return c[d][a.day()]}(b.defineLocale||b.lang).call(b,"ru",{months:e,monthsShort:f,weekdays:g,weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[й|я]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., LT",LLLL:"dddd, D MMMM YYYY г., LT"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(){return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT"},lastWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:d,mm:d,h:"час",hh:d,d:"день",dd:d,M:"месяц",MM:d,y:"год",yy:d},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(a){return/^(дня|вечера)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночи":12>a?"утра":17>a?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":return a+"-й";case"D":return a+"-го";case"w":case"W":return a+"-я";default:return a}},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("ru","ru",{closeText:"Закрыть",prevText:"&#x3C;Пред",nextText:"След&#x3E;",currentText:"Сегодня",monthNames:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthNamesShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],dayNames:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],dayNamesShort:["вск","пнд","втр","срд","чтв","птн","сбт"],dayNamesMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Нед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ru",{buttonText:{month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},allDayText:"Весь день",eventLimitText:function(a){return"+ ещё "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"dddd LT",lastWeek:"[Förra] dddd[en] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"e":1===b?"a":2===b?"a":"e";return a+c},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("sv","sv",{closeText:"Stäng",prevText:"&#xAB;Förra",nextText:"Nästa&#xBB;",currentText:"Idag",monthNames:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNamesShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayNames:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],dayNamesMin:["Sö","Må","Ti","On","To","Fr","Lö"],weekHeader:"Ve",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sv",{buttonText:{month:"Månad",week:"Vecka",day:"Dag",list:"Program"},allDayText:"Heldag",eventLimitText:"till"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(a,b,d){var e=c.words[d];return 1===d.length?b?e[0]:e[1]:a+" "+c.correctGrammaticalCase(a,e)}};(b.defineLocale||b.lang).call(b,"sr",{months:["januar","februar","mart","april","maj","jun","jul","avgust","septembar","oktobar","novembar","decembar"],monthsShort:["jan.","feb.","mar.","apr.","maj","jun","jul","avg.","sep.","okt.","nov.","dec."],weekdays:["nedelja","ponedeljak","utorak","sreda","četvrtak","petak","subota"],weekdaysShort:["ned.","pon.","uto.","sre.","čet.","pet.","sub."],weekdaysMin:["ne","po","ut","sr","če","pe","su"],longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var a=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:c.translate,mm:c.translate,h:c.translate,hh:c.translate,d:"dan",dd:c.translate,M:"mesec",MM:c.translate,y:"godinu",yy:c.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("sr","sr",{closeText:"Затвори",prevText:"&#x3C;",nextText:"&#x3E;",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"),weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),longDateFormat:{LT:"H นาฬิกา m นาที",LTS:"LT s วินาที",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา LT",LLLL:"วันddddที่ D MMMM YYYY เวลา LT"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(a){return"หลังเที่ยง"===a},meridiem:function(a,b,c){return 12>a?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",y:"1 ปี",yy:"%d ปี"}}),a.fullCalendar.datepickerLang("th","th",{closeText:"ปิด",prevText:"&#xAB;&#xA0;ย้อน",nextText:"ถัดไป&#xA0;&#xBB;",currentText:"วันนี้",monthNames:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthNamesShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],dayNames:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"],dayNamesShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayNamesMin:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("th",{buttonText:{month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"};(b.defineLocale||b.lang).call(b,"tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(a){if(0===a)return a+"'ıncı";var b=a%10,d=a%100-b,e=a>=100?100:null;return a+(c[b]||c[d]||c[e])},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("tr","tr",{closeText:"kapat",prevText:"&#x3C;geri",nextText:"ileri&#x3e",currentText:"bugün",monthNames:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthNamesShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],dayNames:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],dayNamesShort:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],dayNamesMin:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],weekHeader:"Hf",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("tr",{buttonText:{next:"ileri",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},allDayText:"Tüm gün",eventLimitText:"daha fazla"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日LT",LLLL:"YYYY年MMMD日ddddLT",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日LT",llll:"YYYY年MMMD日ddddLT"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(a,b){return 12===a&&(a=0),"凌晨"===b||"早上"===b||"上午"===b?a:"下午"===b||"晚上"===b?a+12:a>=11?a:a+12},meridiem:function(a,b,c){var d=100*a+b;return 600>d?"凌晨":900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()-a.unix()>=604800?"[下]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},lastWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()<a.unix()?"[上]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},sameElse:"LL"},ordinalParse:/\d{1,2}(日|月|周)/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";case"M":return a+"月";case"w":case"W":return a+"周";default:return a}},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1分钟",mm:"%d分钟",h:"1小时",hh:"%d小时",d:"1天",dd:"%d天",M:"1个月",MM:"%d个月",y:"1年",yy:"%d年"},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("zh-cn","zh-CN",{closeText:"关闭",prevText:"&#x3C;上月",nextText:"下月&#x3E;",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("zh-cn",{buttonText:{month:"月",week:"周",day:"日",list:"日程"},allDayText:"全天",eventLimitText:function(a){return"另外 "+a+" 个"}})});(function(){function r(v){this.app=v;this.router=new t();this.router.addRoute("screenshot-zone",e)}r.prototype.isOpen=function(){return $("#popover-container").size()>0};r.prototype.open=function(w){var v=this;v.app.dropdown.close();$.get(w,function(x){$("body").append('<div id="popover-container"><div id="popover-content">'+x+"</div></div>");v.app.refresh();v.router.dispatch(this.app);v.afterOpen()})};r.prototype.close=function(v){if(this.isOpen()){if(v){v.preventDefault()}$("#popover-container").remove()}};r.prototype.onClick=function(w){w.preventDefault();w.stopPropagation();var v=w.target.getAttribute("href");if(!v){v=w.target.getAttribute("data-href")}if(v){this.open(v)}};r.prototype.listen=function(){$(document).on("click",".popover",this.onClick.bind(this));$(document).on("click",".close-popover",this.close.bind(this));$(document).on("click","#popover-container",this.close.bind(this));$(document).on("click","#popover-content",function(v){v.stopPropagation()})};r.prototype.afterOpen=function(){var v=this;var w=$("#task-form");if(w){w.on("submit",function(x){x.preventDefault();$.ajax({type:"POST",url:w.attr("action"),data:w.serialize(),success:function(z,A,y){if(y.getResponseHeader("X-Ajax-Redirect")){window.location=y.getResponseHeader("X-Ajax-Redirect")}else{$("#popover-content").html(z);v.afterOpen()}}})})}};function p(){}p.prototype.listen=function(){var v=this;$(document).on("click",function(){v.close()});$(document).on("click",".dropdown-menu",function(z){z.preventDefault();z.stopImmediatePropagation();v.close();var x=$(this).next("ul");var y=240;var A=$(this).offset();var w=$(this).height();$("body").append(jQuery("<div>",{id:"dropdown"}));x.clone().appendTo("#dropdown");var B=$("#dropdown ul");B.css("left",A.left);if(A.top+y-$(window).scrollTop()>$(window).height()){B.css("top",A.top-y-w)}else{B.css("top",A.top+w)}B.addClass("dropdown-submenu-open")});$(document).on("click",".dropdown-submenu-open li",function(w){if($(w.target).is("li")){$(this).find("a:visible")[0].click()}})};p.prototype.close=function(){$("#dropdown").remove()};function o(v){this.app=v}o.prototype.listen=function(){var v=this;$(".tooltip").tooltip({track:false,show:false,hide:false,position:{my:"left-20 top",at:"center bottom+9",using:function(w,x){$(this).css(w);var y=x.target.left+x.target.width/2-x.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(x.vertical).addClass(y<1?"align-left":"align-right").appendTo(this)}},content:function(){var y=this;var w=$(this).attr("data-href");if(!w){return'<div class="markdown">'+$(this).attr("title")+"</div>"}$.get(w,function x(B){var A=$(".ui-tooltip:visible");$(".ui-tooltip-content:visible").html(B);A.css({top:"",left:""});A.children(".tooltip-arrow").remove();var z=$(y).tooltip("option","position");z.of=$(y);A.position(z);$("#tooltip-subtasks a").not(".popover").click(function(C){C.preventDefault();C.stopPropagation();if($(this).hasClass("popover-subtask-restriction")){v.app.popover.open($(this).attr("href"));$(y).tooltip("close")}else{$.get($(this).attr("href"),x)}})});return'<i class="fa fa-spinner fa-spin"></i>'}}).on("mouseenter",function(){var w=this;$(this).tooltip("open");$(".ui-tooltip").on("mouseleave",function(){$(w).tooltip("close")})}).on("mouseleave focusout",function(w){w.stopImmediatePropagation();var x=this;setTimeout(function(){if(!$(".ui-tooltip:hover").length){$(x).tooltip("close")}},100)})};function k(){}k.prototype.showPreview=function(z){z.preventDefault();var w=$(".write-area");var y=$(".preview-area");var v=$("textarea");$("#markdown-write").parent().removeClass("form-tab-selected");$("#markdown-preview").parent().addClass("form-tab-selected");var x=$.ajax({url:$("body").data("markdown-preview-url"),contentType:"application/json",type:"POST",processData:false,dataType:"html",data:JSON.stringify({text:v.val()})});x.done(function(A){y.find(".markdown").html(A);y.css("height",v.css("height"));y.css("width",v.css("width"));w.hide();y.show()})};k.prototype.showWriter=function(v){v.preventDefault();$("#markdown-write").parent().addClass("form-tab-selected");$("#markdown-preview").parent().removeClass("form-tab-selected");$(".write-area").show();$(".preview-area").hide()};k.prototype.listen=function(){$(document).on("click","#markdown-preview",this.showPreview.bind(this));$(document).on("click","#markdown-write",this.showWriter.bind(this))};function b(){}b.prototype.expand=function(v){v.preventDefault();$(".sidebar-container").removeClass("sidebar-collapsed");$(".sidebar-collapse").show();$(".sidebar h2").show();$(".sidebar ul").show();$(".sidebar-expand").hide()};b.prototype.collapse=function(v){v.preventDefault();$(".sidebar-container").addClass("sidebar-collapsed");$(".sidebar-expand").show();$(".sidebar h2").hide();$(".sidebar ul").hide();$(".sidebar-collapse").hide()};b.prototype.listen=function(){$(document).on("click",".sidebar-collapse",this.collapse);$(document).on("click",".sidebar-expand",this.expand)};function f(v){this.app=v;this.keyboardShortcuts()}f.prototype.focus=function(){$(document).on("focus","#form-search",function(){if($("#form-search")[0].setSelectionRange){$("#form-search")[0].setSelectionRange($("#form-search").val().length,$("#form-search").val().length)}})};f.prototype.listen=function(){var v=this;$(document).on("click",".filter-helper",function(y){y.preventDefault();var x=$(this).data("filter");var w=$(this).data("append-filter");if(w){x=$("#form-search").val()+" "+w}$("#form-search").val(x);if($("#board").length){v.app.board.reloadFilters(x)}else{$("form.search").submit()}})};f.prototype.keyboardShortcuts=function(){var v=this;Mousetrap.bind("v b",function(x){var w=$(".view-board");if(w.length){window.location=w.attr("href")}});Mousetrap.bind("v c",function(x){var w=$(".view-calendar");if(w.length){window.location=w.attr("href")}});Mousetrap.bind("v l",function(x){var w=$(".view-listing");if(w.length){window.location=w.attr("href")}});Mousetrap.bind("v g",function(x){var w=$(".view-gantt");if(w.length){window.location=w.attr("href")}});Mousetrap.bind("f",function(x){x.preventDefault();var w=document.getElementById("form-search");if(w){w.focus()}});Mousetrap.bind("r",function(x){x.preventDefault();var w=$(".filter-reset").data("filter");$("#form-search").val(w);if($("#board").length){v.app.board.reloadFilters(w)}else{$("form.search").submit()}})};function l(){this.board=new j(this);this.markdown=new k();this.sidebar=new b();this.search=new f(this);this.swimlane=new g();this.dropdown=new p();this.tooltip=new o(this);this.popover=new r(this);this.task=new a();this.keyboardShortcuts();this.chosen();this.poll();$(".alert-fade-out").delay(4000).fadeOut(800,function(){$(this).remove()});var v=false;$("select.task-reload-project-destination").change(function(){if(!v){$(".loading-icon").show();v=true;window.location=$(this).data("redirect").replace(/PROJECT_ID/g,$(this).val())}})}l.prototype.listen=function(){this.popover.listen();this.markdown.listen();this.sidebar.listen();this.tooltip.listen();this.dropdown.listen();this.search.listen();this.task.listen();this.swimlane.listen();this.search.focus();this.taskAutoComplete();this.datePicker();this.focus()};l.prototype.refresh=function(){$(document).off();this.listen()};l.prototype.focus=function(){$("[autofocus]").each(function(v,w){$(this).focus()});$(document).on("focus",".auto-select",function(){$(this).select()});$(document).on("mouseup",".auto-select",function(v){v.preventDefault()})};l.prototype.poll=function(){window.setInterval(this.checkSession,60000)};l.prototype.keyboardShortcuts=function(){var v=this;Mousetrap.bindGlobal("mod+enter",function(){$("form").submit()});Mousetrap.bind("b",function(w){w.preventDefault();$("#board-selector").trigger("chosen:open")});Mousetrap.bindGlobal("esc",function(){v.popover.close();v.dropdown.close()})};l.prototype.checkSession=function(){if(!$(".form-login").length){$.ajax({cache:false,url:$("body").data("status-url"),statusCode:{401:function(){window.location=$("body").data("login-url")}}})}};l.prototype.datePicker=function(){$.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]);$(".form-date").datepicker({showOtherMonths:true,selectOtherMonths:true,dateFormat:"yy-mm-dd",constrainInput:false});$(".form-datetime").datetimepicker({controlType:"select",oneLine:true,dateFormat:"yy-mm-dd",constrainInput:false})};l.prototype.taskAutoComplete=function(){if($(".task-autocomplete").length){if($(".opposite_task_id").val()==""){$(".task-autocomplete").parent().find("input[type=submit]").attr("disabled","disabled")}$(".task-autocomplete").autocomplete({source:$(".task-autocomplete").data("search-url"),minLength:1,select:function(v,w){var x=$(".task-autocomplete").data("dst-field");$("input[name="+x+"]").val(w.item.id);$(".task-autocomplete").parent().find("input[type=submit]").removeAttr("disabled")}})}};l.prototype.chosen=function(){$(".chosen-select").chosen({width:"180px",no_results_text:$(".chosen-select").data("notfound"),disable_search_threshold:10});$(".select-auto-redirect").change(function(){var v=new RegExp($(this).data("redirect-regex"),"g");window.location=$(this).data("redirect-url").replace(v,$(this).val())})};l.prototype.showLoadingIcon=function(){$("body").append('<span id="app-loading-icon">&nbsp;<i class="fa fa-spinner fa-spin"></i></span>')};l.prototype.hideLoadingIcon=function(){$("#app-loading-icon").remove()};l.prototype.isVisible=function(){var v="";if(typeof document.hidden!=="undefined"){v="visibilityState"}else{if(typeof document.mozHidden!=="undefined"){v="mozVisibilityState"}else{if(typeof document.msHidden!=="undefined"){v="msVisibilityState"}else{if(typeof document.webkitHidden!=="undefined"){v="webkitVisibilityState"}}}}if(v!=""){return document[v]=="visible"}return true};l.prototype.formatDuration=function(v){if(v>=86400){return Math.round(v/86400)+"d"}else{if(v>=3600){return Math.round(v/3600)+"h"}else{if(v>=60){return Math.round(v/60)+"m"}}}return v+"s"};function e(){this.pasteCatcher=null}e.prototype.execute=function(){this.initialize()};e.prototype.initialize=function(){this.destroy();if(!window.Clipboard){this.pasteCatcher=document.createElement("div");this.pasteCatcher.id="screenshot-pastezone";this.pasteCatcher.contentEditable="true";this.pasteCatcher.style.opacity=0;this.pasteCatcher.style.position="fixed";this.pasteCatcher.style.top=0;this.pasteCatcher.style.right=0;this.pasteCatcher.style.width=0;document.body.insertBefore(this.pasteCatcher,document.body.firstChild);this.pasteCatcher.focus();document.addEventListener("click",this.setFocus.bind(this));document.getElementById("screenshot-zone").addEventListener("click",this.setFocus.bind(this))}window.addEventListener("paste",this.pasteHandler.bind(this))};e.prototype.destroy=function(){if(this.pasteCatcher!=null){document.body.removeChild(this.pasteCatcher)}else{if(document.getElementById("screenshot-pastezone")){document.body.removeChild(document.getElementById("screenshot-pastezone"))}}document.removeEventListener("click",this.setFocus.bind(this));this.pasteCatcher=null};e.prototype.setFocus=function(){if(this.pasteCatcher!==null){this.pasteCatcher.focus()}};e.prototype.pasteHandler=function(A){if(A.clipboardData&&A.clipboardData.items){var y=A.clipboardData.items;if(y){for(var z=0;z<y.length;z++){if(y[z].type.indexOf("image")!==-1){var x=y[z].getAsFile();var v=new FileReader();var w=this;v.onload=function(B){w.createImage(B.target.result)};v.readAsDataURL(x)}}}}else{setTimeout(this.checkInput.bind(this),100)}};e.prototype.checkInput=function(){var v=this.pasteCatcher.childNodes[0];if(v){if(v.tagName==="IMG"){this.createImage(v.src)}}this.pasteCatcher.innerHTML=""};e.prototype.createImage=function(x){var w=new Image();w.src=x;w.onload=function(){var y=x.split("base64,");var z=y[1];$("input[name=screenshot]").val(z)};var v=document.getElementById("screenshot-zone");v.innerHTML="";v.className="screenshot-pasted";v.appendChild(w);this.destroy();this.initialize()};function i(){}i.prototype.execute=function(){var v=$("#calendar");v.fullCalendar({lang:$("body").data("js-lang"),editable:true,eventLimit:true,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},eventDrop:function(w){$.ajax({cache:false,url:v.data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:w.id,date_due:w.start.format()})})},viewRender:function(){var w=v.data("check-url");var y={start:v.fullCalendar("getView").start.format(),end:v.fullCalendar("getView").end.format()};for(var x in y){w+="&"+x+"="+y[x]}$.getJSON(w,function(z){v.fullCalendar("removeEvents");v.fullCalendar("addEventSource",z);v.fullCalendar("rerenderEvents")})}})};function j(v){this.app=v;this.checkInterval=null}j.prototype.execute=function(){this.app.swimlane.refresh();this.restoreColumnViewMode();this.compactView();this.columnScrolling();this.poll();this.keyboardShortcuts();this.listen();this.dragAndDrop();$(window).resize(this.columnScrolling)};j.prototype.poll=function(){var v=parseInt($("#board").attr("data-check-interval"));if(v>0){this.checkInterval=window.setInterval(this.check.bind(this),v*1000)}};j.prototype.reloadFilters=function(v){this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("reload-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({search:v}),success:this.refresh.bind(this),error:this.app.hideLoadingIcon.bind(this)})};j.prototype.check=function(){if(this.app.isVisible()){var v=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("check-url"),statusCode:{200:function(w){v.refresh(w)},304:function(){v.app.hideLoadingIcon()}}})}};j.prototype.save=function(x,y,v,w){this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:x,column_id:y,swimlane_id:w,position:v}),success:this.refresh.bind(this),error:this.app.hideLoadingIcon.bind(this)})};j.prototype.refresh=function(v){$("#board-container").replaceWith(v);this.app.refresh();this.app.swimlane.refresh();this.columnScrolling();this.app.hideLoadingIcon();this.listen();this.dragAndDrop();this.compactView();this.restoreColumnViewMode()};j.prototype.dragAndDrop=function(){var v=this;var w={forcePlaceholderSize:true,tolerance:"pointer",connectWith:".board-task-list",placeholder:"draggable-placeholder",items:".draggable-item",stop:function(x,y){y.item.removeClass("draggable-item-selected");v.save(y.item.attr("data-task-id"),y.item.parent().attr("data-column-id"),y.item.index()+1,y.item.parent().attr("data-swimlane-id"))},start:function(x,y){y.item.addClass("draggable-item-selected");y.placeholder.height(y.item.height())}};if($.support.touch){$(".task-board-sort-handle").css("display","inline");w.handle=".task-board-sort-handle"}$(".board-task-list").sortable(w)};j.prototype.listen=function(){var v=this;$(document).on("click",".task-board",function(w){if(w.target.tagName!="A"){window.location=$(this).data("task-url")}});$(document).on("click",".filter-toggle-scrolling",function(w){w.preventDefault();v.toggleCompactView()});$(document).on("click",".filter-toggle-height",function(w){w.preventDefault();v.toggleColumnScrolling()});$(document).on("click",".board-column-title",function(){v.toggleColumnViewMode($(this).data("column-id"))})};j.prototype.toggleColumnScrolling=function(){var v=localStorage.getItem("column_scroll")||1;localStorage.setItem("column_scroll",v==0?1:0);this.columnScrolling()};j.prototype.columnScrolling=function(){if(localStorage.getItem("column_scroll")==0){$(".filter-max-height").show();$(".filter-min-height").hide();$(".board-task-list").each(function(){$(this).css("min-height",80);$(this).css("height","");$(".board-rotation-wrapper").css("min-height","")})}else{$(".filter-max-height").hide();$(".filter-min-height").show();if($(".board-swimlane").length>1){$(".board-task-list").each(function(){if($(this).height()>500){$(this).css("height",500)}else{$(this).css("min-height",320);$(".board-rotation-wrapper").css("min-height",320)}})}else{var v=$(window).height()-145;$(".board-task-list").css("height",v);$(".board-rotation-wrapper").css("min-height",v)}}};j.prototype.toggleCompactView=function(){var v=localStorage.getItem("horizontal_scroll")||1;localStorage.setItem("horizontal_scroll",v==0?1:0);this.compactView()};j.prototype.compactView=function(){if(localStorage.getItem("horizontal_scroll")==0){$(".filter-wide").show();$(".filter-compact").hide();$("#board-container").addClass("board-container-compact");$("#board th:not(.board-column-header-collapsed)").addClass("board-column-compact")}else{$(".filter-wide").hide();$(".filter-compact").show();$("#board-container").removeClass("board-container-compact");$("#board th").removeClass("board-column-compact")}};j.prototype.toggleCollapsedMode=function(){var v=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$('.filter-display-mode:not([style="display: none;"]) a').attr("href"),success:function(w){$(".filter-display-mode").toggle();v.refresh(w)}})};j.prototype.restoreColumnViewMode=function(){var v=this;$(".board-column-header").each(function(){var w=$(this).data("column-id");if(localStorage.getItem("hidden_column_"+w)){v.hideColumn(w)}})};j.prototype.toggleColumnViewMode=function(v){if(localStorage.getItem("hidden_column_"+v)){this.showColumn(v)}else{this.hideColumn(v)}};j.prototype.hideColumn=function(v){$(".board-column-"+v+" .board-column-expanded").hide();$(".board-column-"+v+" .board-column-collapsed").show();$(".board-column-header-"+v+" .board-column-expanded").hide();$(".board-column-header-"+v+" .board-column-collapsed").show();$(".board-column-header-"+v).each(function(){$(this).removeClass("board-column-compact");$(this).addClass("board-column-header-collapsed")});$(".board-column-"+v).each(function(){$(this).addClass("board-column-task-collapsed")});$(".board-column-"+v+" .board-rotation").each(function(){$(this).css("width",$(".board-column-"+v+"").height())});localStorage.setItem("hidden_column_"+v,1)};j.prototype.showColumn=function(v){$(".board-column-"+v+" .board-column-expanded").show();$(".board-column-"+v+" .board-column-collapsed").hide();$(".board-column-header-"+v+" .board-column-expanded").show();$(".board-column-header-"+v+" .board-column-collapsed").hide();$(".board-column-header-"+v).removeClass("board-column-header-collapsed");$(".board-column-"+v).removeClass("board-column-task-collapsed");if(localStorage.getItem("horizontal_scroll")==0){$(".board-column-header-"+v).addClass("board-column-compact")}localStorage.removeItem("hidden_column_"+v)};j.prototype.keyboardShortcuts=function(){var v=this;Mousetrap.bind("c",function(){v.toggleCompactView()});Mousetrap.bind("s",function(){v.toggleCollapsedMode()});Mousetrap.bind("n",function(){v.app.popover.open($("#board").data("task-creation-url"))})};function g(){}g.prototype.getStorageKey=function(){return"hidden_swimlanes_"+$("#board").data("project-id")};g.prototype.expand=function(w){var x=this.getAllCollapsed();var v=x.indexOf(w);if(v>-1){x.splice(v,1)}localStorage.setItem(this.getStorageKey(),JSON.stringify(x));$(".board-swimlane-columns-"+w).css("display","table-row");$(".board-swimlane-tasks-"+w).css("display","table-row");$(".hide-icon-swimlane-"+w).css("display","inline");$(".show-icon-swimlane-"+w).css("display","none")};g.prototype.collapse=function(v){var w=this.getAllCollapsed();if(w.indexOf(v)<0){w.push(v);localStorage.setItem(this.getStorageKey(),JSON.stringify(w))}$(".board-swimlane-columns-"+v+":not(:first-child)").css("display","none");$(".board-swimlane-tasks-"+v).css("display","none");$(".hide-icon-swimlane-"+v).css("display","none");$(".show-icon-swimlane-"+v).css("display","inline")};g.prototype.isCollapsed=function(v){return this.getAllCollapsed().indexOf(v)>-1};g.prototype.getAllCollapsed=function(){return JSON.parse(localStorage.getItem(this.getStorageKey()))||[]};g.prototype.refresh=function(){var w=this.getAllCollapsed();for(var v=0;v<w.length;v++){this.collapse(w[v])}};g.prototype.listen=function(){var v=this;$(document).on("click",".board-swimlane-toggle",function(x){x.preventDefault();var w=$(this).data("swimlane-id");if(v.isCollapsed(w)){v.expand(w)}else{v.collapse(w)}})};function c(v){this.app=v;this.data=[];this.options={container:"#gantt-chart",showWeekends:true,allowMoves:true,allowResizes:true,cellWidth:21,cellHeight:31,slideWidth:1000,vHeaderWidth:200}}c.prototype.saveRecord=function(v){this.app.showLoadingIcon();$.ajax({cache:false,url:$(this.options.container).data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify(v),complete:this.app.hideLoadingIcon.bind(this)})};c.prototype.execute=function(){this.data=this.prepareData($(this.options.container).data("records"));var y=Math.floor((this.options.slideWidth/this.options.cellWidth)+5);var x=this.getDateRange(y);var v=x[0];var A=x[1];var w=$(this.options.container);var z=jQuery("<div>",{"class":"ganttview"});z.append(this.renderVerticalHeader());z.append(this.renderSlider(v,A));w.append(z);jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child",w).addClass("last");jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child",w).addClass("last");jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child",w).addClass("last");if(!$(this.options.container).data("readonly")){this.listenForBlockResize(v);this.listenForBlockMove(v)}else{this.options.allowResizes=false;this.options.allowMoves=false}};c.prototype.renderVerticalHeader=function(){var z=jQuery("<div>",{"class":"ganttview-vtheader"});var w=jQuery("<div>",{"class":"ganttview-vtheader-item"});var y=jQuery("<div>",{"class":"ganttview-vtheader-series"});for(var v=0;v<this.data.length;v++){var x=jQuery("<span>").append(jQuery("<i>",{"class":"fa fa-info-circle tooltip",title:this.getVerticalHeaderTooltip(this.data[v])})).append("&nbsp;");if(this.data[v].type=="task"){x.append(jQuery("<a>",{href:this.data[v].link,target:"_blank",title:this.data[v].title}).append(this.data[v].title))}else{x.append(jQuery("<a>",{href:this.data[v].board_link,target:"_blank",title:$(this.options.container).data("label-board-link")}).append('<i class="fa fa-th"></i>')).append("&nbsp;").append(jQuery("<a>",{href:this.data[v].gantt_link,target:"_blank",title:$(this.options.container).data("label-gantt-link")}).append('<i class="fa fa-sliders"></i>')).append("&nbsp;").append(jQuery("<a>",{href:this.data[v].link,target:"_blank"}).append(this.data[v].title))}y.append(jQuery("<div>",{"class":"ganttview-vtheader-series-name"}).append(x))}w.append(y);z.append(w);return z};c.prototype.renderSlider=function(w,y){var v=jQuery("<div>",{"class":"ganttview-slide-container"});var x=this.getDates(w,y);v.append(this.renderHorizontalHeader(x));v.append(this.renderGrid(x));v.append(this.addBlockContainers());this.addBlocks(v,w);return v};c.prototype.renderHorizontalHeader=function(v){var D=jQuery("<div>",{"class":"ganttview-hzheader"});var B=jQuery("<div>",{"class":"ganttview-hzheader-months"});var A=jQuery("<div>",{"class":"ganttview-hzheader-days"});var z=0;for(var E in v){for(var x in v[E]){var F=v[E][x].length*this.options.cellWidth;z=z+F;B.append(jQuery("<div>",{"class":"ganttview-hzheader-month",css:{width:(F-1)+"px"}}).append($.datepicker.regional[$("body").data("js-lang")].monthNames[x]+" "+E));for(var C in v[E][x]){A.append(jQuery("<div>",{"class":"ganttview-hzheader-day"}).append(v[E][x][C].getDate()))}}}B.css("width",z+"px");A.css("width",z+"px");D.append(B).append(A);return D};c.prototype.renderGrid=function(v){var F=jQuery("<div>",{"class":"ganttview-grid"});var A=jQuery("<div>",{"class":"ganttview-grid-row"});for(var D in v){for(var x in v[D]){for(var C in v[D][x]){var z=jQuery("<div>",{"class":"ganttview-grid-row-cell"});if(this.options.showWeekends&&this.isWeekend(v[D][x][C])){z.addClass("ganttview-weekend")}A.append(z)}}}var E=jQuery("div.ganttview-grid-row-cell",A).length*this.options.cellWidth;A.css("width",E+"px");F.css("width",E+"px");for(var B=0;B<this.data.length;B++){F.append(A.clone())}return F};c.prototype.addBlockContainers=function(){var w=jQuery("<div>",{"class":"ganttview-blocks"});for(var v=0;v<this.data.length;v++){w.append(jQuery("<div>",{"class":"ganttview-block-container"}))}return w};c.prototype.addBlocks=function(w,v){var D=jQuery("div.ganttview-blocks div.ganttview-block-container",w);var x=0;for(var A=0;A<this.data.length;A++){var B=this.data[A];var E=this.daysBetween(B.start,B.end)+1;var z=this.daysBetween(v,B.start);var C=jQuery("<div>",{"class":"ganttview-block-text"});var y=jQuery("<div>",{"class":"ganttview-block tooltip"+(this.options.allowMoves?" ganttview-block-movable":""),title:this.getBarTooltip(this.data[A]),css:{width:((E*this.options.cellWidth)-9)+"px","margin-left":(z*this.options.cellWidth)+"px"}}).append(C);if(E>=2){C.append(this.data[A].progress)}y.data("record",this.data[A]);this.setBarColor(y,this.data[A]);y.append(jQuery("<div>",{css:{"z-index":0,position:"absolute",top:0,bottom:0,"background-color":B.color.border,width:B.progress,opacity:0.4}}));jQuery(D[x]).append(y);x=x+1}};c.prototype.getVerticalHeaderTooltip=function(w){var B="";if(w.type=="task"){B="<strong>"+w.column_title+"</strong> ("+w.progress+")<br/>"+w.title}else{var y=["managers","members"];for(var x in y){var z=y[x];if(!jQuery.isEmptyObject(w.users[z])){var A=jQuery("<ul>");for(var v in w.users[z]){A.append(jQuery("<li>").append(w.users[z][v]))}B+="<p><strong>"+$(this.options.container).data("label-"+z)+"</strong></p>"+A[0].outerHTML}}}return B};c.prototype.getBarTooltip=function(v){var w="";if(v.not_defined){w=$(this.options.container).data("label-not-defined")}else{if(v.type=="task"){w="<strong>"+v.progress+"</strong><br/>"+$(this.options.container).data("label-assignee")+" "+(v.assignee?v.assignee:"")+"<br/>"}w+=$(this.options.container).data("label-start-date")+" "+$.datepicker.formatDate("yy-mm-dd",v.start)+"<br/>";w+=$(this.options.container).data("label-end-date")+" "+$.datepicker.formatDate("yy-mm-dd",v.end)}return w};c.prototype.setBarColor=function(w,v){if(v.not_defined){w.addClass("ganttview-block-not-defined")}else{w.css("background-color",v.color.background);w.css("border-color",v.color.border)}};c.prototype.listenForBlockResize=function(v){var w=this;jQuery("div.ganttview-block",this.options.container).resizable({grid:this.options.cellWidth,handles:"e,w",delay:300,stop:function(){var x=jQuery(this);w.updateDataAndPosition(x,v);w.saveRecord(x.data("record"))}})};c.prototype.listenForBlockMove=function(v){var w=this;jQuery("div.ganttview-block",this.options.container).draggable({axis:"x",delay:300,grid:[this.options.cellWidth,this.options.cellWidth],stop:function(){var x=jQuery(this);w.updateDataAndPosition(x,v);w.saveRecord(x.data("record"))}})};c.prototype.updateDataAndPosition=function(A,y){var v=jQuery("div.ganttview-slide-container",this.options.container);var E=v.scrollLeft();var B=A.offset().left-v.offset().left-1+E;var D=A.data("record");D.not_defined=false;this.setBarColor(A,D);var x=Math.round(B/this.options.cellWidth);var C=this.addDays(this.cloneDate(y),x);D.start=C;var w=A.outerWidth();var z=Math.round(w/this.options.cellWidth)-1;D.end=this.addDays(this.cloneDate(C),z);if(D.type==="task"&&z>0){jQuery("div.ganttview-block-text",A).text(D.progress)}A.attr("title",this.getBarTooltip(D));A.data("record",D);A.css("top","").css("left","").css("position","relative").css("margin-left",B+"px")};c.prototype.getDates=function(z,v){var y=[];y[z.getFullYear()]=[];y[z.getFullYear()][z.getMonth()]=[z];var x=z;while(this.compareDate(x,v)==-1){var w=this.addDays(this.cloneDate(x),1);if(!y[w.getFullYear()]){y[w.getFullYear()]=[]}if(!y[w.getFullYear()][w.getMonth()]){y[w.getFullYear()][w.getMonth()]=[]}y[w.getFullYear()][w.getMonth()].push(w);x=w}return y};c.prototype.prepareData=function(x){for(var w=0;w<x.length;w++){var y=new Date(x[w].start[0],x[w].start[1]-1,x[w].start[2],0,0,0,0);x[w].start=y;var v=new Date(x[w].end[0],x[w].end[1]-1,x[w].end[2],0,0,0,0);x[w].end=v}return x};c.prototype.getDateRange=function(x){var A=new Date();var w=new Date();for(var y=0;y<this.data.length;y++){var z=new Date();z.setTime(Date.parse(this.data[y].start));var v=new Date();v.setTime(Date.parse(this.data[y].end));if(y==0){A=z;w=v}if(this.compareDate(A,z)==1){A=z}if(this.compareDate(w,v)==-1){w=v}}if(this.daysBetween(A,w)<x){w=this.addDays(this.cloneDate(A),x)}A.setDate(A.getDate()-1);return[A,w]};c.prototype.daysBetween=function(y,v){if(!y||!v){return 0}var x=0,w=this.cloneDate(y);while(this.compareDate(w,v)==-1){x=x+1;this.addDays(w,1)}return x};c.prototype.isWeekend=function(v){return v.getDay()%6==0};c.prototype.cloneDate=function(v){return new Date(v.getTime())};c.prototype.addDays=function(v,w){v.setDate(v.getDate()+w*1);return v};c.prototype.compareDate=function(w,v){if(isNaN(w)||isNaN(v)){throw new Error(w+" - "+v)}else{if(w instanceof Date&&v instanceof Date){return(w<v)?-1:(w>v)?1:0}else{throw new TypeError(w+" - "+v)}}};function a(){}a.prototype.listen=function(){$(document).on("click",".color-square",function(){$(".color-square-selected").removeClass("color-square-selected");$(this).addClass("color-square-selected");$("#form-color_id").val($(this).data("color-id"))})};function q(){}q.prototype.execute=function(){var x=$("#chart").data("metrics");var w=[];for(var v=0;v<x.length;v++){w.push([x[v].column_title,x[v].nb_tasks])}c3.generate({data:{columns:w,type:"donut"}})};function n(){}n.prototype.execute=function(){var x=$("#chart").data("metrics");var w=[];for(var v=0;v<x.length;v++){w.push([x[v].user,x[v].nb_tasks])}c3.generate({data:{columns:w,type:"donut"}})};function d(){}d.prototype.execute=function(){var B=$("#chart").data("metrics");var A=[];var v=[];var w=[];var y=d3.time.format("%Y-%m-%d");var C=d3.time.format($("#chart").data("date-format"));for(var z=0;z<B.length;z++){for(var x=0;x<B[z].length;x++){if(z==0){A.push([B[z][x]]);if(x>0){v.push(B[z][x])}}else{A[x].push(B[z][x]);if(x==0){w.push(C(y.parse(B[z][x])))}}}}c3.generate({data:{columns:A,type:"area-spline",groups:[v]},axis:{x:{type:"category",categories:w}}})};function m(){}m.prototype.execute=function(){var A=$("#chart").data("metrics");var z=[[$("#chart").data("label-total")]];var v=[];var x=d3.time.format("%Y-%m-%d");var B=d3.time.format($("#chart").data("date-format"));for(var y=0;y<A.length;y++){for(var w=0;w<A[y].length;w++){if(y==0){z.push([A[y][w]])}else{z[w+1].push(A[y][w]);if(w>0){if(z[0][y]==undefined){z[0].push(0)}z[0][y]+=A[y][w]}if(w==0){v.push(B(x.parse(A[y][w])))}}}}c3.generate({data:{columns:z},axis:{x:{type:"category",categories:v}}})};function h(v){this.app=v}h.prototype.execute=function(){var x=$("#chart").data("metrics");var y=[$("#chart").data("label")];var v=[];for(var w in x){y.push(x[w].average);v.push(x[w].title)}c3.generate({data:{columns:[y],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:v},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};function u(v){this.app=v}u.prototype.execute=function(){var x=$("#chart").data("metrics");var y=[$("#chart").data("label")];var v=[];for(var w=0;w<x.length;w++){y.push(x[w].time_spent);v.push(x[w].title)}c3.generate({data:{columns:[y],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:v},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};function s(v){this.app=v}s.prototype.execute=function(){var B=$("#chart").data("metrics");var A=[$("#chart").data("label-cycle")];var x=[$("#chart").data("label-lead")];var w=[];var z={};z[$("#chart").data("label-cycle")]="area";z[$("#chart").data("label-lead")]="area-spline";var v={};v[$("#chart").data("label-lead")]="#afb42b";v[$("#chart").data("label-cycle")]="#4e342e";for(var y=0;y<B.length;y++){A.push(parseInt(B[y].avg_cycle_time));x.push(parseInt(B[y].avg_lead_time));w.push(B[y].day)}c3.generate({data:{columns:[x,A],types:z,colors:v},axis:{x:{type:"category",categories:w},y:{tick:{format:this.app.formatDuration}}}})};function t(){this.routes={}}t.prototype.addRoute=function(w,v){this.routes[w]=v};t.prototype.dispatch=function(w){for(var x in this.routes){if(document.getElementById(x)){var v=Object.create(this.routes[x].prototype);this.routes[x].apply(v,[w]);v.execute();break}}};jQuery(document).ready(function(){var w=new l();var v=new t();v.addRoute("board",j);v.addRoute("calendar",i);v.addRoute("screenshot-zone",e);v.addRoute("analytic-task-repartition",q);v.addRoute("analytic-user-repartition",n);v.addRoute("analytic-cfd",d);v.addRoute("analytic-burndown",m);v.addRoute("analytic-avg-time-column",h);v.addRoute("analytic-task-time-column",u);v.addRoute("analytic-lead-cycle-time",s);v.addRoute("gantt-chart",c);v.dispatch(w);w.listen()})})(); \ No newline at end of file
+!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return a>1&&5>a&&1!==~~(a/10)}function d(a,b,d,e){var f=a+" ";switch(d){case"s":return b||e?"pár sekund":"pár sekundami";case"m":return b?"minuta":e?"minutu":"minutou";case"mm":return b||e?f+(c(a)?"minuty":"minut"):f+"minutami";case"h":return b?"hodina":e?"hodinu":"hodinou";case"hh":return b||e?f+(c(a)?"hodiny":"hodin"):f+"hodinami";case"d":return b||e?"den":"dnem";case"dd":return b||e?f+(c(a)?"dny":"dní"):f+"dny";case"M":return b||e?"měsíc":"měsícem";case"MM":return b||e?f+(c(a)?"měsíce":"měsíců"):f+"měsíci";case"y":return b||e?"rok":"rokem";case"yy":return b||e?f+(c(a)?"roky":"let"):f+"lety"}}var e="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),f="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_");(b.defineLocale||b.lang).call(b,"cs",{months:e,monthsShort:f,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(e,f),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd D. MMMM YYYY LT"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:d,m:d,mm:d,h:d,hh:d,d:d,dd:d,M:d,MM:d,y:d,yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("cs","cs",{closeText:"Zavřít",prevText:"&#x3C;Dříve",nextText:"Později&#x3E;",currentText:"Nyní",monthNames:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"],monthNamesShort:["led","úno","bře","dub","kvě","čer","čvc","srp","zář","říj","lis","pro"],dayNames:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"],dayNamesShort:["ne","po","út","st","čt","pá","so"],dayNamesMin:["ne","po","út","st","čt","pá","so"],weekHeader:"Týd",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("cs",{buttonText:{month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},allDayText:"Celý den",eventLimitText:function(a){return"+další: "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd [d.] D. MMMM YYYY LT"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("da","da",{closeText:"Luk",prevText:"&#x3C;Forrige",nextText:"Næste&#x3E;",currentText:"Idag",monthNames:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNames:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],dayNamesShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayNamesMin:["Sø","Ma","Ti","On","To","Fr","Lø"],weekHeader:"Uge",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("da",{buttonText:{month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"flere"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}(b.defineLocale||b.lang).call(b,"de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Heute um] LT [Uhr]",sameElse:"L",nextDay:"[Morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[Gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:c,mm:"%d Minuten",h:c,hh:"%d Stunden",d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("de","de",{closeText:"Schließen",prevText:"&#x3C;Zurück",nextText:"Vor&#x3E;",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),d="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");(b.defineLocale||b.lang).call(b,"es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mi_Ju_Vi_Sá".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("es","es",{closeText:"Cerrar",prevText:"&#x3C;Ant",nextText:"Sig&#x3E;",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("es",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo<br/>el día",eventLimitText:"más"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,e){var f="";switch(c){case"s":return e?"muutaman sekunnin":"muutama sekunti";case"m":return e?"minuutin":"minuutti";case"mm":f=e?"minuutin":"minuuttia";break;case"h":return e?"tunnin":"tunti";case"hh":f=e?"tunnin":"tuntia";break;case"d":return e?"päivän":"päivä";case"dd":f=e?"päivän":"päivää";break;case"M":return e?"kuukauden":"kuukausi";case"MM":f=e?"kuukauden":"kuukautta";break;case"y":return e?"vuoden":"vuosi";case"yy":f=e?"vuoden":"vuotta"}return f=d(a,e)+" "+f}function d(a,b){return 10>a?b?f[a]:e[a]:a}var e="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),f=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",e[7],e[8],e[9]];(b.defineLocale||b.lang).call(b,"fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] LT",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] LT",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] LT",llll:"ddd, Do MMM YYYY, [klo] LT"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fi","fi",{closeText:"Sulje",prevText:"&#xAB;Edellinen",nextText:"Seuraava&#xBB;",currentText:"Tänään",monthNames:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],monthNamesShort:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"],dayNamesShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayNames:["Sunnuntai","Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai"],dayNamesMin:["Su","Ma","Ti","Ke","To","Pe","La"],weekHeader:"Vk",dateFormat:"d.m.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fi",{buttonText:{month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},allDayText:"Koko päivä",eventLimitText:"lisää"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fr","fr",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr",{buttonText:{month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la<br/>journée",eventLimitText:"en plus"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b,c,d){var e=a;switch(c){case"s":return d||b?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(d||b?" perc":" perce");case"mm":return e+(d||b?" perc":" perce");case"h":return"egy"+(d||b?" óra":" órája");case"hh":return e+(d||b?" óra":" órája");case"d":return"egy"+(d||b?" nap":" napja");case"dd":return e+(d||b?" nap":" napja");case"M":return"egy"+(d||b?" hónap":" hónapja");case"MM":return e+(d||b?" hónap":" hónapja");case"y":return"egy"+(d||b?" év":" éve");case"yy":return e+(d||b?" év":" éve")}return""}function d(a){return(a?"":"[múlt] ")+"["+e[this.day()]+"] LT[-kor]"}var e="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" ");(b.defineLocale||b.lang).call(b,"hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D., LT",LLLL:"YYYY. MMMM D., dddd LT"},meridiemParse:/de|du/i,isPM:function(a){return"u"===a.charAt(1).toLowerCase()},meridiem:function(a,b,c){return 12>a?c===!0?"de":"DE":c===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return d.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return d.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("hu","hu",{closeText:"bezár",prevText:"vissza",nextText:"előre",currentText:"ma",monthNames:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],monthNamesShort:["Jan","Feb","Már","Ápr","Máj","Jún","Júl","Aug","Szep","Okt","Nov","Dec"],dayNames:["Vasárnap","Hétfő","Kedd","Szerda","Csütörtök","Péntek","Szombat"],dayNamesShort:["Vas","Hét","Ked","Sze","Csü","Pén","Szo"],dayNamesMin:["V","H","K","Sze","Cs","P","Szo"],weekHeader:"Hét",dateFormat:"yy.mm.dd.",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("hu",{buttonText:{month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},allDayText:"Egész nap",eventLimitText:"további"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"LT.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] LT",LLLL:"dddd, D MMMM YYYY [pukul] LT"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"siang"===b?a>=11?a:a+12:"sore"===b||"malam"===b?a+12:void 0},meridiem:function(a,b,c){return 11>a?"pagi":15>a?"siang":19>a?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("id","id",{closeText:"Tutup",prevText:"&#x3C;mundur",nextText:"maju&#x3E;",currentText:"hari ini",monthNames:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","Nopember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agus","Sep","Okt","Nop","Des"],dayNames:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],dayNamesShort:["Min","Sen","Sel","Rab","kam","Jum","Sab"],dayNamesMin:["Mg","Sn","Sl","Rb","Km","jm","Sb"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("id",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayHtml:"Sehari<br/>penuh",eventLimitText:"lebih"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"D_L_Ma_Me_G_V_S".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(a){return(/^[0-9].+$/.test(a)?"tra":"in")+" "+a},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("it","it",{closeText:"Chiudi",prevText:"&#x3C;Prec",nextText:"Succ&#x3E;",currentText:"Oggi",monthNames:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthNamesShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],dayNames:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],dayNamesShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayNamesMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("it",{buttonText:{month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},allDayHtml:"Tutto il<br/>giorno",eventLimitText:function(a){return"+altri "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"LTs秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日LT",LLLL:"YYYY年M月D日LT dddd"},meridiemParse:/午前|午後/i,isPM:function(a){return"午後"===a},meridiem:function(a,b,c){return 12>a?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}}),a.fullCalendar.datepickerLang("ja","ja",{closeText:"閉じる",prevText:"&#x3C;前",nextText:"次&#x3E;",currentText:"今日",monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],dayNamesShort:["日","月","火","水","木","金","土"],dayNamesMin:["日","月","火","水","木","金","土"],weekHeader:"週",dateFormat:"yy/mm/dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("ja",{buttonText:{month:"月",week:"週",day:"日",list:"予定リスト"},allDayText:"終日",eventLimitText:function(a){return"他 "+a+" 件"}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),d="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_");(b.defineLocale||b.lang).call(b,"nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nl","nl",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nl",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tirs_ons_tors_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"H.mm",LTS:"LT.ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"for %s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nb","nb",{closeText:"Lukk",prevText:"&#xAB;Forrige",nextText:"Neste&#xBB;",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["søn","man","tir","ons","tor","fre","lør"],dayNames:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],dayNamesMin:["sø","ma","ti","on","to","fr","lø"],weekHeader:"Uke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nb",{buttonText:{month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"til"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a){return 5>a%10&&a%10>1&&~~(a/10)%10!==1}function d(a,b,d){var e=a+" ";switch(d){case"m":return b?"minuta":"minutę";case"mm":return e+(c(a)?"minuty":"minut");case"h":return b?"godzina":"godzinę";case"hh":return e+(c(a)?"godziny":"godzin");case"MM":return e+(c(a)?"miesiące":"miesięcy");case"yy":return e+(c(a)?"lata":"lat")}}var e="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),f="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_");(b.defineLocale||b.lang).call(b,"pl",{months:function(a,b){return/D MMMM/.test(b)?f[a.month()]:e[a.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"nie_pon_wt_śr_czw_pt_sb".split("_"),weekdaysMin:"N_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:d,mm:d,h:d,hh:d,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:d,y:"rok",yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pl","pl",{closeText:"Zamknij",prevText:"&#x3C;Poprzedni",nextText:"Następny&#x3E;",currentText:"Dziś",monthNames:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthNamesShort:["Sty","Lu","Mar","Kw","Maj","Cze","Lip","Sie","Wrz","Pa","Lis","Gru"],dayNames:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],dayNamesShort:["Nie","Pn","Wt","Śr","Czw","Pt","So"],dayNamesMin:["N","Pn","Wt","Śr","Cz","Pt","So"],weekHeader:"Tydz",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pl",{buttonText:{month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},allDayText:"Cały dzień",eventLimitText:"więcej"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pt","pt",{closeText:"Fechar",prevText:"Anterior",nextText:"Seguinte",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sem",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},allDayText:"Todo o dia",eventLimitText:"mais"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"pt-br",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] LT",LLLL:"dddd, D [de] MMMM [de] YYYY [às] LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"}),a.fullCalendar.datepickerLang("pt-br","pt-BR",{closeText:"Fechar",prevText:"&#x3C;Anterior",nextText:"Próximo&#x3E;",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt-br",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},allDayText:"dia inteiro",eventLimitText:function(a){return"mais +"+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){function c(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function d(a,b,d){var e={mm:b?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===d?b?"минута":"минуту":a+" "+c(e[d],+a)}function e(a,b){var c={nominative:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_"),accusative:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function f(a,b){var c={nominative:"янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек".split("_"),accusative:"янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function g(a,b){var c={nominative:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),accusative:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_")},d=/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/.test(b)?"accusative":"nominative";return c[d][a.day()]}(b.defineLocale||b.lang).call(b,"ru",{months:e,monthsShort:f,weekdays:g,weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[й|я]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., LT",LLLL:"dddd, D MMMM YYYY г., LT"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(){return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT"},lastWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:d,mm:d,h:"час",hh:d,d:"день",dd:d,M:"месяц",MM:d,y:"год",yy:d},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(a){return/^(дня|вечера)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночи":12>a?"утра":17>a?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":return a+"-й";case"D":return a+"-го";case"w":case"W":return a+"-я";default:return a}},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("ru","ru",{closeText:"Закрыть",prevText:"&#x3C;Пред",nextText:"След&#x3E;",currentText:"Сегодня",monthNames:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthNamesShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],dayNames:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],dayNamesShort:["вск","пнд","втр","срд","чтв","птн","сбт"],dayNamesMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Нед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ru",{buttonText:{month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},allDayText:"Весь день",eventLimitText:function(a){return"+ ещё "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"dddd LT",lastWeek:"[Förra] dddd[en] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"e":1===b?"a":2===b?"a":"e";return a+c},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("sv","sv",{closeText:"Stäng",prevText:"&#xAB;Förra",nextText:"Nästa&#xBB;",currentText:"Idag",monthNames:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNamesShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayNames:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],dayNamesMin:["Sö","Må","Ti","On","To","Fr","Lö"],weekHeader:"Ve",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sv",{buttonText:{month:"Månad",week:"Vecka",day:"Dag",list:"Program"},allDayText:"Heldag",eventLimitText:"till"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(a,b,d){var e=c.words[d];return 1===d.length?b?e[0]:e[1]:a+" "+c.correctGrammaticalCase(a,e)}};(b.defineLocale||b.lang).call(b,"sr",{months:["januar","februar","mart","april","maj","jun","jul","avgust","septembar","oktobar","novembar","decembar"],monthsShort:["jan.","feb.","mar.","apr.","maj","jun","jul","avg.","sep.","okt.","nov.","dec."],weekdays:["nedelja","ponedeljak","utorak","sreda","četvrtak","petak","subota"],weekdaysShort:["ned.","pon.","uto.","sre.","čet.","pet.","sub."],weekdaysMin:["ne","po","ut","sr","če","pe","su"],longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var a=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:c.translate,mm:c.translate,h:c.translate,hh:c.translate,d:"dan",dd:c.translate,M:"mesec",MM:c.translate,y:"godinu",yy:c.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("sr","sr",{closeText:"Затвори",prevText:"&#x3C;",nextText:"&#x3E;",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"),weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),longDateFormat:{LT:"H นาฬิกา m นาที",LTS:"LT s วินาที",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา LT",LLLL:"วันddddที่ D MMMM YYYY เวลา LT"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(a){return"หลังเที่ยง"===a},meridiem:function(a,b,c){return 12>a?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",y:"1 ปี",yy:"%d ปี"}}),a.fullCalendar.datepickerLang("th","th",{closeText:"ปิด",prevText:"&#xAB;&#xA0;ย้อน",nextText:"ถัดไป&#xA0;&#xBB;",currentText:"วันนี้",monthNames:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthNamesShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],dayNames:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"],dayNamesShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayNamesMin:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("th",{buttonText:{month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){var c={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"};(b.defineLocale||b.lang).call(b,"tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(a){if(0===a)return a+"'ıncı";var b=a%10,d=a%100-b,e=a>=100?100:null;return a+(c[b]||c[d]||c[e])},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("tr","tr",{closeText:"kapat",prevText:"&#x3C;geri",nextText:"ileri&#x3e",currentText:"bugün",monthNames:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthNamesShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],dayNames:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],dayNamesShort:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],dayNamesMin:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],weekHeader:"Hf",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("tr",{buttonText:{next:"ileri",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},allDayText:"Tüm gün",eventLimitText:"daha fazla"})});!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){(b.defineLocale||b.lang).call(b,"zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日LT",LLLL:"YYYY年MMMD日ddddLT",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日LT",llll:"YYYY年MMMD日ddddLT"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(a,b){return 12===a&&(a=0),"凌晨"===b||"早上"===b||"上午"===b?a:"下午"===b||"晚上"===b?a+12:a>=11?a:a+12},meridiem:function(a,b,c){var d=100*a+b;return 600>d?"凌晨":900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()-a.unix()>=604800?"[下]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},lastWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()<a.unix()?"[上]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},sameElse:"LL"},ordinalParse:/\d{1,2}(日|月|周)/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";case"M":return a+"月";case"w":case"W":return a+"周";default:return a}},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1分钟",mm:"%d分钟",h:"1小时",hh:"%d小时",d:"1天",dd:"%d天",M:"1个月",MM:"%d个月",y:"1年",yy:"%d年"},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("zh-cn","zh-CN",{closeText:"关闭",prevText:"&#x3C;上月",nextText:"下月&#x3E;",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("zh-cn",{buttonText:{month:"月",week:"周",day:"日",list:"日程"},allDayText:"全天",eventLimitText:function(a){return"另外 "+a+" 个"}})});(function(){function s(w){this.app=w;this.router=new u();this.router.addRoute("screenshot-zone",e)}s.prototype.isOpen=function(){return $("#popover-container").size()>0};s.prototype.open=function(x){var w=this;w.app.dropdown.close();$.get(x,function(y){$("body").append('<div id="popover-container"><div id="popover-content">'+y+"</div></div>");w.app.refresh();w.router.dispatch(this.app);w.afterOpen()})};s.prototype.close=function(w){if(this.isOpen()){if(w){w.preventDefault()}$("#popover-container").remove()}};s.prototype.onClick=function(x){x.preventDefault();x.stopPropagation();var w=x.target.getAttribute("href");if(!w){w=x.target.getAttribute("data-href")}if(w){this.open(w)}};s.prototype.listen=function(){$(document).on("click",".popover",this.onClick.bind(this));$(document).on("click",".close-popover",this.close.bind(this));$(document).on("click","#popover-container",this.close.bind(this));$(document).on("click","#popover-content",function(w){w.stopPropagation()})};s.prototype.afterOpen=function(){var w=this;var x=$("#task-form");if(x){x.on("submit",function(y){y.preventDefault();$.ajax({type:"POST",url:x.attr("action"),data:x.serialize(),success:function(A,B,z){if(z.getResponseHeader("X-Ajax-Redirect")){window.location=z.getResponseHeader("X-Ajax-Redirect")}else{$("#popover-content").html(A);w.afterOpen()}}})})}};function q(){}q.prototype.listen=function(){var w=this;$(document).on("click",function(){w.close()});$(document).on("click",".dropdown-menu",function(A){A.preventDefault();A.stopImmediatePropagation();w.close();var y=$(this).next("ul");var z=240;var B=$(this).offset();var x=$(this).height();$("body").append(jQuery("<div>",{id:"dropdown"}));y.clone().appendTo("#dropdown");var C=$("#dropdown ul");C.css("left",B.left);if(B.top+z-$(window).scrollTop()>$(window).height()){C.css("top",B.top-z-x)}else{C.css("top",B.top+x)}C.addClass("dropdown-submenu-open")});$(document).on("click",".dropdown-submenu-open li",function(x){if($(x.target).is("li")){$(this).find("a:visible")[0].click()}})};q.prototype.close=function(){$("#dropdown").remove()};function p(w){this.app=w}p.prototype.listen=function(){var w=this;$(".tooltip").tooltip({track:false,show:false,hide:false,position:{my:"left-20 top",at:"center bottom+9",using:function(x,y){$(this).css(x);var z=y.target.left+y.target.width/2-y.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(y.vertical).addClass(z<1?"align-left":"align-right").appendTo(this)}},content:function(){var z=this;var x=$(this).attr("data-href");if(!x){return'<div class="markdown">'+$(this).attr("title")+"</div>"}$.get(x,function y(C){var B=$(".ui-tooltip:visible");$(".ui-tooltip-content:visible").html(C);B.css({top:"",left:""});B.children(".tooltip-arrow").remove();var A=$(z).tooltip("option","position");A.of=$(z);B.position(A);$("#tooltip-subtasks a").not(".popover").click(function(D){D.preventDefault();D.stopPropagation();if($(this).hasClass("popover-subtask-restriction")){w.app.popover.open($(this).attr("href"));$(z).tooltip("close")}else{$.get($(this).attr("href"),y)}})});return'<i class="fa fa-spinner fa-spin"></i>'}}).on("mouseenter",function(){var x=this;$(this).tooltip("open");$(".ui-tooltip").on("mouseleave",function(){$(x).tooltip("close")})}).on("mouseleave focusout",function(x){x.stopImmediatePropagation();var y=this;setTimeout(function(){if(!$(".ui-tooltip:hover").length){$(y).tooltip("close")}},100)})};function k(){}k.prototype.showPreview=function(A){A.preventDefault();var x=$(".write-area");var z=$(".preview-area");var w=$("textarea");$("#markdown-write").parent().removeClass("form-tab-selected");$("#markdown-preview").parent().addClass("form-tab-selected");var y=$.ajax({url:$("body").data("markdown-preview-url"),contentType:"application/json",type:"POST",processData:false,dataType:"html",data:JSON.stringify({text:w.val()})});y.done(function(B){z.find(".markdown").html(B);z.css("height",w.css("height"));z.css("width",w.css("width"));x.hide();z.show()})};k.prototype.showWriter=function(w){w.preventDefault();$("#markdown-write").parent().addClass("form-tab-selected");$("#markdown-preview").parent().removeClass("form-tab-selected");$(".write-area").show();$(".preview-area").hide()};k.prototype.listen=function(){$(document).on("click","#markdown-preview",this.showPreview.bind(this));$(document).on("click","#markdown-write",this.showWriter.bind(this))};function b(){}b.prototype.expand=function(w){w.preventDefault();$(".sidebar-container").removeClass("sidebar-collapsed");$(".sidebar-collapse").show();$(".sidebar h2").show();$(".sidebar ul").show();$(".sidebar-expand").hide()};b.prototype.collapse=function(w){w.preventDefault();$(".sidebar-container").addClass("sidebar-collapsed");$(".sidebar-expand").show();$(".sidebar h2").hide();$(".sidebar ul").hide();$(".sidebar-collapse").hide()};b.prototype.listen=function(){$(document).on("click",".sidebar-collapse",this.collapse);$(document).on("click",".sidebar-expand",this.expand)};function f(w){this.app=w;this.keyboardShortcuts()}f.prototype.focus=function(){$(document).on("focus","#form-search",function(){if($("#form-search")[0].setSelectionRange){$("#form-search")[0].setSelectionRange($("#form-search").val().length,$("#form-search").val().length)}})};f.prototype.listen=function(){var w=this;$(document).on("click",".filter-helper",function(z){z.preventDefault();var y=$(this).data("filter");var x=$(this).data("append-filter");if(x){y=$("#form-search").val()+" "+x}$("#form-search").val(y);if($("#board").length){w.app.board.reloadFilters(y)}else{$("form.search").submit()}})};f.prototype.keyboardShortcuts=function(){var w=this;Mousetrap.bind("v b",function(y){var x=$(".view-board");if(x.length){window.location=x.attr("href")}});Mousetrap.bind("v c",function(y){var x=$(".view-calendar");if(x.length){window.location=x.attr("href")}});Mousetrap.bind("v l",function(y){var x=$(".view-listing");if(x.length){window.location=x.attr("href")}});Mousetrap.bind("v g",function(y){var x=$(".view-gantt");if(x.length){window.location=x.attr("href")}});Mousetrap.bind("f",function(y){y.preventDefault();var x=document.getElementById("form-search");if(x){x.focus()}});Mousetrap.bind("r",function(y){y.preventDefault();var x=$(".filter-reset").data("filter");$("#form-search").val(x);if($("#board").length){w.app.board.reloadFilters(x)}else{$("form.search").submit()}})};function l(){this.board=new j(this);this.markdown=new k();this.sidebar=new b();this.search=new f(this);this.swimlane=new g();this.dropdown=new q();this.tooltip=new p(this);this.popover=new s(this);this.task=new a();this.project=new m();this.keyboardShortcuts();this.chosen();this.poll();$(".alert-fade-out").delay(4000).fadeOut(800,function(){$(this).remove()});var w=false;$("select.task-reload-project-destination").change(function(){if(!w){$(".loading-icon").show();w=true;window.location=$(this).data("redirect").replace(/PROJECT_ID/g,$(this).val())}})}l.prototype.listen=function(){this.project.listen();this.popover.listen();this.markdown.listen();this.sidebar.listen();this.tooltip.listen();this.dropdown.listen();this.search.listen();this.task.listen();this.swimlane.listen();this.search.focus();this.autoComplete();this.datePicker();this.focus()};l.prototype.refresh=function(){$(document).off();this.listen()};l.prototype.focus=function(){$("[autofocus]").each(function(w,x){$(this).focus()});$(document).on("focus",".auto-select",function(){$(this).select()});$(document).on("mouseup",".auto-select",function(w){w.preventDefault()})};l.prototype.poll=function(){window.setInterval(this.checkSession,60000)};l.prototype.keyboardShortcuts=function(){var w=this;Mousetrap.bindGlobal("mod+enter",function(){$("form").submit()});Mousetrap.bind("b",function(x){x.preventDefault();$("#board-selector").trigger("chosen:open")});Mousetrap.bindGlobal("esc",function(){w.popover.close();w.dropdown.close()})};l.prototype.checkSession=function(){if(!$(".form-login").length){$.ajax({cache:false,url:$("body").data("status-url"),statusCode:{401:function(){window.location=$("body").data("login-url")}}})}};l.prototype.datePicker=function(){$.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]);$(".form-date").datepicker({showOtherMonths:true,selectOtherMonths:true,dateFormat:"yy-mm-dd",constrainInput:false});$(".form-datetime").datetimepicker({controlType:"select",oneLine:true,dateFormat:"yy-mm-dd",constrainInput:false})};l.prototype.autoComplete=function(){$(".autocomplete").each(function(){var x=$(this);var y=x.data("dst-field");var w=x.data("dst-extra-field");if($("#form-"+y).val()==""){x.parent().find("input[type=submit]").attr("disabled","disabled")}x.autocomplete({source:x.data("search-url"),minLength:1,select:function(z,A){$("input[name="+y+"]").val(A.item.id);if(w){$("input[name="+w+"]").val(A.item[w])}x.parent().find("input[type=submit]").removeAttr("disabled")}})})};l.prototype.chosen=function(){$(".chosen-select").chosen({width:"180px",no_results_text:$(".chosen-select").data("notfound"),disable_search_threshold:10});$(".select-auto-redirect").change(function(){var w=new RegExp($(this).data("redirect-regex"),"g");window.location=$(this).data("redirect-url").replace(w,$(this).val())})};l.prototype.showLoadingIcon=function(){$("body").append('<span id="app-loading-icon">&nbsp;<i class="fa fa-spinner fa-spin"></i></span>')};l.prototype.hideLoadingIcon=function(){$("#app-loading-icon").remove()};l.prototype.isVisible=function(){var w="";if(typeof document.hidden!=="undefined"){w="visibilityState"}else{if(typeof document.mozHidden!=="undefined"){w="mozVisibilityState"}else{if(typeof document.msHidden!=="undefined"){w="msVisibilityState"}else{if(typeof document.webkitHidden!=="undefined"){w="webkitVisibilityState"}}}}if(w!=""){return document[w]=="visible"}return true};l.prototype.formatDuration=function(w){if(w>=86400){return Math.round(w/86400)+"d"}else{if(w>=3600){return Math.round(w/3600)+"h"}else{if(w>=60){return Math.round(w/60)+"m"}}}return w+"s"};function e(){this.pasteCatcher=null}e.prototype.execute=function(){this.initialize()};e.prototype.initialize=function(){this.destroy();if(!window.Clipboard){this.pasteCatcher=document.createElement("div");this.pasteCatcher.id="screenshot-pastezone";this.pasteCatcher.contentEditable="true";this.pasteCatcher.style.opacity=0;this.pasteCatcher.style.position="fixed";this.pasteCatcher.style.top=0;this.pasteCatcher.style.right=0;this.pasteCatcher.style.width=0;document.body.insertBefore(this.pasteCatcher,document.body.firstChild);this.pasteCatcher.focus();document.addEventListener("click",this.setFocus.bind(this));document.getElementById("screenshot-zone").addEventListener("click",this.setFocus.bind(this))}window.addEventListener("paste",this.pasteHandler.bind(this))};e.prototype.destroy=function(){if(this.pasteCatcher!=null){document.body.removeChild(this.pasteCatcher)}else{if(document.getElementById("screenshot-pastezone")){document.body.removeChild(document.getElementById("screenshot-pastezone"))}}document.removeEventListener("click",this.setFocus.bind(this));this.pasteCatcher=null};e.prototype.setFocus=function(){if(this.pasteCatcher!==null){this.pasteCatcher.focus()}};e.prototype.pasteHandler=function(B){if(B.clipboardData&&B.clipboardData.items){var z=B.clipboardData.items;if(z){for(var A=0;A<z.length;A++){if(z[A].type.indexOf("image")!==-1){var y=z[A].getAsFile();var w=new FileReader();var x=this;w.onload=function(C){x.createImage(C.target.result)};w.readAsDataURL(y)}}}}else{setTimeout(this.checkInput.bind(this),100)}};e.prototype.checkInput=function(){var w=this.pasteCatcher.childNodes[0];if(w){if(w.tagName==="IMG"){this.createImage(w.src)}}this.pasteCatcher.innerHTML=""};e.prototype.createImage=function(y){var x=new Image();x.src=y;x.onload=function(){var z=y.split("base64,");var A=z[1];$("input[name=screenshot]").val(A)};var w=document.getElementById("screenshot-zone");w.innerHTML="";w.className="screenshot-pasted";w.appendChild(x);this.destroy();this.initialize()};function i(){}i.prototype.execute=function(){var w=$("#calendar");w.fullCalendar({lang:$("body").data("js-lang"),editable:true,eventLimit:true,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},eventDrop:function(x){$.ajax({cache:false,url:w.data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:x.id,date_due:x.start.format()})})},viewRender:function(){var x=w.data("check-url");var z={start:w.fullCalendar("getView").start.format(),end:w.fullCalendar("getView").end.format()};for(var y in z){x+="&"+y+"="+z[y]}$.getJSON(x,function(A){w.fullCalendar("removeEvents");w.fullCalendar("addEventSource",A);w.fullCalendar("rerenderEvents")})}})};function j(w){this.app=w;this.checkInterval=null}j.prototype.execute=function(){this.app.swimlane.refresh();this.restoreColumnViewMode();this.compactView();this.columnScrolling();this.poll();this.keyboardShortcuts();this.listen();this.dragAndDrop();$(window).resize(this.columnScrolling)};j.prototype.poll=function(){var w=parseInt($("#board").attr("data-check-interval"));if(w>0){this.checkInterval=window.setInterval(this.check.bind(this),w*1000)}};j.prototype.reloadFilters=function(w){this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("reload-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({search:w}),success:this.refresh.bind(this),error:this.app.hideLoadingIcon.bind(this)})};j.prototype.check=function(){if(this.app.isVisible()){var w=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("check-url"),statusCode:{200:function(x){w.refresh(x)},304:function(){w.app.hideLoadingIcon()}}})}};j.prototype.save=function(y,z,w,x){this.app.showLoadingIcon();$.ajax({cache:false,url:$("#board").data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({task_id:y,column_id:z,swimlane_id:x,position:w}),success:this.refresh.bind(this),error:this.app.hideLoadingIcon.bind(this)})};j.prototype.refresh=function(w){$("#board-container").replaceWith(w);this.app.refresh();this.app.swimlane.refresh();this.columnScrolling();this.app.hideLoadingIcon();this.listen();this.dragAndDrop();this.compactView();this.restoreColumnViewMode()};j.prototype.dragAndDrop=function(){var w=this;var x={forcePlaceholderSize:true,tolerance:"pointer",connectWith:".board-task-list",placeholder:"draggable-placeholder",items:".draggable-item",stop:function(y,z){z.item.removeClass("draggable-item-selected");w.save(z.item.attr("data-task-id"),z.item.parent().attr("data-column-id"),z.item.index()+1,z.item.parent().attr("data-swimlane-id"))},start:function(y,z){z.item.addClass("draggable-item-selected");z.placeholder.height(z.item.height())}};if($.support.touch){$(".task-board-sort-handle").css("display","inline");x.handle=".task-board-sort-handle"}$(".board-task-list").sortable(x)};j.prototype.listen=function(){var w=this;$(document).on("click",".task-board",function(x){if(x.target.tagName!="A"){window.location=$(this).data("task-url")}});$(document).on("click",".filter-toggle-scrolling",function(x){x.preventDefault();w.toggleCompactView()});$(document).on("click",".filter-toggle-height",function(x){x.preventDefault();w.toggleColumnScrolling()});$(document).on("click",".board-column-title",function(){w.toggleColumnViewMode($(this).data("column-id"))})};j.prototype.toggleColumnScrolling=function(){var w=localStorage.getItem("column_scroll")||1;localStorage.setItem("column_scroll",w==0?1:0);this.columnScrolling()};j.prototype.columnScrolling=function(){if(localStorage.getItem("column_scroll")==0){$(".filter-max-height").show();$(".filter-min-height").hide();$(".board-task-list").each(function(){$(this).css("min-height",80);$(this).css("height","");$(".board-rotation-wrapper").css("min-height","")})}else{$(".filter-max-height").hide();$(".filter-min-height").show();if($(".board-swimlane").length>1){$(".board-task-list").each(function(){if($(this).height()>500){$(this).css("height",500)}else{$(this).css("min-height",320);$(".board-rotation-wrapper").css("min-height",320)}})}else{var w=$(window).height()-145;$(".board-task-list").css("height",w);$(".board-rotation-wrapper").css("min-height",w)}}};j.prototype.toggleCompactView=function(){var w=localStorage.getItem("horizontal_scroll")||1;localStorage.setItem("horizontal_scroll",w==0?1:0);this.compactView()};j.prototype.compactView=function(){if(localStorage.getItem("horizontal_scroll")==0){$(".filter-wide").show();$(".filter-compact").hide();$("#board-container").addClass("board-container-compact");$("#board th:not(.board-column-header-collapsed)").addClass("board-column-compact")}else{$(".filter-wide").hide();$(".filter-compact").show();$("#board-container").removeClass("board-container-compact");$("#board th").removeClass("board-column-compact")}};j.prototype.toggleCollapsedMode=function(){var w=this;this.app.showLoadingIcon();$.ajax({cache:false,url:$('.filter-display-mode:not([style="display: none;"]) a').attr("href"),success:function(x){$(".filter-display-mode").toggle();w.refresh(x)}})};j.prototype.restoreColumnViewMode=function(){var w=this;$(".board-column-header").each(function(){var x=$(this).data("column-id");if(localStorage.getItem("hidden_column_"+x)){w.hideColumn(x)}})};j.prototype.toggleColumnViewMode=function(w){if(localStorage.getItem("hidden_column_"+w)){this.showColumn(w)}else{this.hideColumn(w)}};j.prototype.hideColumn=function(w){$(".board-column-"+w+" .board-column-expanded").hide();$(".board-column-"+w+" .board-column-collapsed").show();$(".board-column-header-"+w+" .board-column-expanded").hide();$(".board-column-header-"+w+" .board-column-collapsed").show();$(".board-column-header-"+w).each(function(){$(this).removeClass("board-column-compact");$(this).addClass("board-column-header-collapsed")});$(".board-column-"+w).each(function(){$(this).addClass("board-column-task-collapsed")});$(".board-column-"+w+" .board-rotation").each(function(){$(this).css("width",$(".board-column-"+w+"").height())});localStorage.setItem("hidden_column_"+w,1)};j.prototype.showColumn=function(w){$(".board-column-"+w+" .board-column-expanded").show();$(".board-column-"+w+" .board-column-collapsed").hide();$(".board-column-header-"+w+" .board-column-expanded").show();$(".board-column-header-"+w+" .board-column-collapsed").hide();$(".board-column-header-"+w).removeClass("board-column-header-collapsed");$(".board-column-"+w).removeClass("board-column-task-collapsed");if(localStorage.getItem("horizontal_scroll")==0){$(".board-column-header-"+w).addClass("board-column-compact")}localStorage.removeItem("hidden_column_"+w)};j.prototype.keyboardShortcuts=function(){var w=this;Mousetrap.bind("c",function(){w.toggleCompactView()});Mousetrap.bind("s",function(){w.toggleCollapsedMode()});Mousetrap.bind("n",function(){w.app.popover.open($("#board").data("task-creation-url"))})};function g(){}g.prototype.getStorageKey=function(){return"hidden_swimlanes_"+$("#board").data("project-id")};g.prototype.expand=function(x){var y=this.getAllCollapsed();var w=y.indexOf(x);if(w>-1){y.splice(w,1)}localStorage.setItem(this.getStorageKey(),JSON.stringify(y));$(".board-swimlane-columns-"+x).css("display","table-row");$(".board-swimlane-tasks-"+x).css("display","table-row");$(".hide-icon-swimlane-"+x).css("display","inline");$(".show-icon-swimlane-"+x).css("display","none")};g.prototype.collapse=function(w){var x=this.getAllCollapsed();if(x.indexOf(w)<0){x.push(w);localStorage.setItem(this.getStorageKey(),JSON.stringify(x))}$(".board-swimlane-columns-"+w+":not(:first-child)").css("display","none");$(".board-swimlane-tasks-"+w).css("display","none");$(".hide-icon-swimlane-"+w).css("display","none");$(".show-icon-swimlane-"+w).css("display","inline")};g.prototype.isCollapsed=function(w){return this.getAllCollapsed().indexOf(w)>-1};g.prototype.getAllCollapsed=function(){return JSON.parse(localStorage.getItem(this.getStorageKey()))||[]};g.prototype.refresh=function(){var x=this.getAllCollapsed();for(var w=0;w<x.length;w++){this.collapse(x[w])}};g.prototype.listen=function(){var w=this;$(document).on("click",".board-swimlane-toggle",function(y){y.preventDefault();var x=$(this).data("swimlane-id");if(w.isCollapsed(x)){w.expand(x)}else{w.collapse(x)}})};function c(w){this.app=w;this.data=[];this.options={container:"#gantt-chart",showWeekends:true,allowMoves:true,allowResizes:true,cellWidth:21,cellHeight:31,slideWidth:1000,vHeaderWidth:200}}c.prototype.saveRecord=function(w){this.app.showLoadingIcon();$.ajax({cache:false,url:$(this.options.container).data("save-url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify(w),complete:this.app.hideLoadingIcon.bind(this)})};c.prototype.execute=function(){this.data=this.prepareData($(this.options.container).data("records"));var z=Math.floor((this.options.slideWidth/this.options.cellWidth)+5);var y=this.getDateRange(z);var w=y[0];var B=y[1];var x=$(this.options.container);var A=jQuery("<div>",{"class":"ganttview"});A.append(this.renderVerticalHeader());A.append(this.renderSlider(w,B));x.append(A);jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child",x).addClass("last");jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child",x).addClass("last");jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child",x).addClass("last");if(!$(this.options.container).data("readonly")){this.listenForBlockResize(w);this.listenForBlockMove(w)}else{this.options.allowResizes=false;this.options.allowMoves=false}};c.prototype.renderVerticalHeader=function(){var A=jQuery("<div>",{"class":"ganttview-vtheader"});var x=jQuery("<div>",{"class":"ganttview-vtheader-item"});var z=jQuery("<div>",{"class":"ganttview-vtheader-series"});for(var w=0;w<this.data.length;w++){var y=jQuery("<span>").append(jQuery("<i>",{"class":"fa fa-info-circle tooltip",title:this.getVerticalHeaderTooltip(this.data[w])})).append("&nbsp;");if(this.data[w].type=="task"){y.append(jQuery("<a>",{href:this.data[w].link,target:"_blank",title:this.data[w].title}).append(this.data[w].title))}else{y.append(jQuery("<a>",{href:this.data[w].board_link,target:"_blank",title:$(this.options.container).data("label-board-link")}).append('<i class="fa fa-th"></i>')).append("&nbsp;").append(jQuery("<a>",{href:this.data[w].gantt_link,target:"_blank",title:$(this.options.container).data("label-gantt-link")}).append('<i class="fa fa-sliders"></i>')).append("&nbsp;").append(jQuery("<a>",{href:this.data[w].link,target:"_blank"}).append(this.data[w].title))}z.append(jQuery("<div>",{"class":"ganttview-vtheader-series-name"}).append(y))}x.append(z);A.append(x);return A};c.prototype.renderSlider=function(x,z){var w=jQuery("<div>",{"class":"ganttview-slide-container"});var y=this.getDates(x,z);w.append(this.renderHorizontalHeader(y));w.append(this.renderGrid(y));w.append(this.addBlockContainers());this.addBlocks(w,x);return w};c.prototype.renderHorizontalHeader=function(x){var E=jQuery("<div>",{"class":"ganttview-hzheader"});var C=jQuery("<div>",{"class":"ganttview-hzheader-months"});var B=jQuery("<div>",{"class":"ganttview-hzheader-days"});var A=0;for(var F in x){for(var z in x[F]){var G=x[F][z].length*this.options.cellWidth;A=A+G;C.append(jQuery("<div>",{"class":"ganttview-hzheader-month",css:{width:(G-1)+"px"}}).append($.datepicker.regional[$("body").data("js-lang")].monthNames[z]+" "+F));for(var D in x[F][z]){B.append(jQuery("<div>",{"class":"ganttview-hzheader-day"}).append(x[F][z][D].getDate()))}}}C.css("width",A+"px");B.css("width",A+"px");E.append(C).append(B);return E};c.prototype.renderGrid=function(x){var G=jQuery("<div>",{"class":"ganttview-grid"});var B=jQuery("<div>",{"class":"ganttview-grid-row"});for(var E in x){for(var z in x[E]){for(var D in x[E][z]){var A=jQuery("<div>",{"class":"ganttview-grid-row-cell"});if(this.options.showWeekends&&this.isWeekend(x[E][z][D])){A.addClass("ganttview-weekend")}B.append(A)}}}var F=jQuery("div.ganttview-grid-row-cell",B).length*this.options.cellWidth;B.css("width",F+"px");G.css("width",F+"px");for(var C=0;C<this.data.length;C++){G.append(B.clone())}return G};c.prototype.addBlockContainers=function(){var x=jQuery("<div>",{"class":"ganttview-blocks"});for(var w=0;w<this.data.length;w++){x.append(jQuery("<div>",{"class":"ganttview-block-container"}))}return x};c.prototype.addBlocks=function(x,w){var E=jQuery("div.ganttview-blocks div.ganttview-block-container",x);var y=0;for(var B=0;B<this.data.length;B++){var C=this.data[B];var F=this.daysBetween(C.start,C.end)+1;var A=this.daysBetween(w,C.start);var D=jQuery("<div>",{"class":"ganttview-block-text"});var z=jQuery("<div>",{"class":"ganttview-block tooltip"+(this.options.allowMoves?" ganttview-block-movable":""),title:this.getBarTooltip(this.data[B]),css:{width:((F*this.options.cellWidth)-9)+"px","margin-left":(A*this.options.cellWidth)+"px"}}).append(D);if(F>=2){D.append(this.data[B].progress)}z.data("record",this.data[B]);this.setBarColor(z,this.data[B]);z.append(jQuery("<div>",{css:{"z-index":0,position:"absolute",top:0,bottom:0,"background-color":C.color.border,width:C.progress,opacity:0.4}}));jQuery(E[y]).append(z);y=y+1}};c.prototype.getVerticalHeaderTooltip=function(x){var C="";if(x.type=="task"){C="<strong>"+x.column_title+"</strong> ("+x.progress+")<br/>"+x.title}else{var z=["managers","members"];for(var y in z){var A=z[y];if(!jQuery.isEmptyObject(x.users[A])){var B=jQuery("<ul>");for(var w in x.users[A]){B.append(jQuery("<li>").append(x.users[A][w]))}C+="<p><strong>"+$(this.options.container).data("label-"+A)+"</strong></p>"+B[0].outerHTML}}}return C};c.prototype.getBarTooltip=function(w){var x="";if(w.not_defined){x=$(this.options.container).data("label-not-defined")}else{if(w.type=="task"){x="<strong>"+w.progress+"</strong><br/>"+$(this.options.container).data("label-assignee")+" "+(w.assignee?w.assignee:"")+"<br/>"}x+=$(this.options.container).data("label-start-date")+" "+$.datepicker.formatDate("yy-mm-dd",w.start)+"<br/>";x+=$(this.options.container).data("label-end-date")+" "+$.datepicker.formatDate("yy-mm-dd",w.end)}return x};c.prototype.setBarColor=function(x,w){if(w.not_defined){x.addClass("ganttview-block-not-defined")}else{x.css("background-color",w.color.background);x.css("border-color",w.color.border)}};c.prototype.listenForBlockResize=function(w){var x=this;jQuery("div.ganttview-block",this.options.container).resizable({grid:this.options.cellWidth,handles:"e,w",delay:300,stop:function(){var y=jQuery(this);x.updateDataAndPosition(y,w);x.saveRecord(y.data("record"))}})};c.prototype.listenForBlockMove=function(w){var x=this;jQuery("div.ganttview-block",this.options.container).draggable({axis:"x",delay:300,grid:[this.options.cellWidth,this.options.cellWidth],stop:function(){var y=jQuery(this);x.updateDataAndPosition(y,w);x.saveRecord(y.data("record"))}})};c.prototype.updateDataAndPosition=function(B,z){var w=jQuery("div.ganttview-slide-container",this.options.container);var F=w.scrollLeft();var C=B.offset().left-w.offset().left-1+F;var E=B.data("record");E.not_defined=false;this.setBarColor(B,E);var y=Math.round(C/this.options.cellWidth);var D=this.addDays(this.cloneDate(z),y);E.start=D;var x=B.outerWidth();var A=Math.round(x/this.options.cellWidth)-1;E.end=this.addDays(this.cloneDate(D),A);if(E.type==="task"&&A>0){jQuery("div.ganttview-block-text",B).text(E.progress)}B.attr("title",this.getBarTooltip(E));B.data("record",E);B.css("top","").css("left","").css("position","relative").css("margin-left",C+"px")};c.prototype.getDates=function(A,w){var z=[];z[A.getFullYear()]=[];z[A.getFullYear()][A.getMonth()]=[A];var y=A;while(this.compareDate(y,w)==-1){var x=this.addDays(this.cloneDate(y),1);if(!z[x.getFullYear()]){z[x.getFullYear()]=[]}if(!z[x.getFullYear()][x.getMonth()]){z[x.getFullYear()][x.getMonth()]=[]}z[x.getFullYear()][x.getMonth()].push(x);y=x}return z};c.prototype.prepareData=function(y){for(var x=0;x<y.length;x++){var z=new Date(y[x].start[0],y[x].start[1]-1,y[x].start[2],0,0,0,0);y[x].start=z;var w=new Date(y[x].end[0],y[x].end[1]-1,y[x].end[2],0,0,0,0);y[x].end=w}return y};c.prototype.getDateRange=function(y){var B=new Date();var x=new Date();for(var z=0;z<this.data.length;z++){var A=new Date();A.setTime(Date.parse(this.data[z].start));var w=new Date();w.setTime(Date.parse(this.data[z].end));if(z==0){B=A;x=w}if(this.compareDate(B,A)==1){B=A}if(this.compareDate(x,w)==-1){x=w}}if(this.daysBetween(B,x)<y){x=this.addDays(this.cloneDate(B),y)}B.setDate(B.getDate()-1);return[B,x]};c.prototype.daysBetween=function(z,w){if(!z||!w){return 0}var y=0,x=this.cloneDate(z);while(this.compareDate(x,w)==-1){y=y+1;this.addDays(x,1)}return y};c.prototype.isWeekend=function(w){return w.getDay()%6==0};c.prototype.cloneDate=function(w){return new Date(w.getTime())};c.prototype.addDays=function(w,x){w.setDate(w.getDate()+x*1);return w};c.prototype.compareDate=function(x,w){if(isNaN(x)||isNaN(w)){throw new Error(x+" - "+w)}else{if(x instanceof Date&&w instanceof Date){return(x<w)?-1:(x>w)?1:0}else{throw new TypeError(x+" - "+w)}}};function a(){}a.prototype.listen=function(){$(document).on("click",".color-square",function(){$(".color-square-selected").removeClass("color-square-selected");$(this).addClass("color-square-selected");$("#form-color_id").val($(this).data("color-id"))})};function m(){}m.prototype.listen=function(){$(".project-change-role").on("change",function(){$.ajax({cache:false,url:$(this).data("url"),contentType:"application/json",type:"POST",processData:false,data:JSON.stringify({id:$(this).data("id"),role:$(this).val()})})})};function r(){}r.prototype.execute=function(){var y=$("#chart").data("metrics");var x=[];for(var w=0;w<y.length;w++){x.push([y[w].column_title,y[w].nb_tasks])}c3.generate({data:{columns:x,type:"donut"}})};function o(){}o.prototype.execute=function(){var y=$("#chart").data("metrics");var x=[];for(var w=0;w<y.length;w++){x.push([y[w].user,y[w].nb_tasks])}c3.generate({data:{columns:x,type:"donut"}})};function d(){}d.prototype.execute=function(){var C=$("#chart").data("metrics");var B=[];var w=[];var x=[];var z=d3.time.format("%Y-%m-%d");var D=d3.time.format($("#chart").data("date-format"));for(var A=0;A<C.length;A++){for(var y=0;y<C[A].length;y++){if(A==0){B.push([C[A][y]]);if(y>0){w.push(C[A][y])}}else{B[y].push(C[A][y]);if(y==0){x.push(D(z.parse(C[A][y])))}}}}c3.generate({data:{columns:B,type:"area-spline",groups:[w]},axis:{x:{type:"category",categories:x}}})};function n(){}n.prototype.execute=function(){var B=$("#chart").data("metrics");var A=[[$("#chart").data("label-total")]];var w=[];var y=d3.time.format("%Y-%m-%d");var C=d3.time.format($("#chart").data("date-format"));for(var z=0;z<B.length;z++){for(var x=0;x<B[z].length;x++){if(z==0){A.push([B[z][x]])}else{A[x+1].push(B[z][x]);if(x>0){if(A[0][z]==undefined){A[0].push(0)}A[0][z]+=B[z][x]}if(x==0){w.push(C(y.parse(B[z][x])))}}}}c3.generate({data:{columns:A},axis:{x:{type:"category",categories:w}}})};function h(w){this.app=w}h.prototype.execute=function(){var y=$("#chart").data("metrics");var z=[$("#chart").data("label")];var w=[];for(var x in y){z.push(y[x].average);w.push(y[x].title)}c3.generate({data:{columns:[z],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:w},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};function v(w){this.app=w}v.prototype.execute=function(){var y=$("#chart").data("metrics");var z=[$("#chart").data("label")];var w=[];for(var x=0;x<y.length;x++){z.push(y[x].time_spent);w.push(y[x].title)}c3.generate({data:{columns:[z],type:"bar"},bar:{width:{ratio:0.5}},axis:{x:{type:"category",categories:w},y:{tick:{format:this.app.formatDuration}}},legend:{show:false}})};function t(w){this.app=w}t.prototype.execute=function(){var C=$("#chart").data("metrics");var B=[$("#chart").data("label-cycle")];var y=[$("#chart").data("label-lead")];var x=[];var A={};A[$("#chart").data("label-cycle")]="area";A[$("#chart").data("label-lead")]="area-spline";var w={};w[$("#chart").data("label-lead")]="#afb42b";w[$("#chart").data("label-cycle")]="#4e342e";for(var z=0;z<C.length;z++){B.push(parseInt(C[z].avg_cycle_time));y.push(parseInt(C[z].avg_lead_time));x.push(C[z].day)}c3.generate({data:{columns:[y,B],types:A,colors:w},axis:{x:{type:"category",categories:x},y:{tick:{format:this.app.formatDuration}}}})};function u(){this.routes={}}u.prototype.addRoute=function(x,w){this.routes[x]=w};u.prototype.dispatch=function(x){for(var y in this.routes){if(document.getElementById(y)){var w=Object.create(this.routes[y].prototype);this.routes[y].apply(w,[x]);w.execute();break}}};jQuery(document).ready(function(){var x=new l();var w=new u();w.addRoute("board",j);w.addRoute("calendar",i);w.addRoute("screenshot-zone",e);w.addRoute("analytic-task-repartition",r);w.addRoute("analytic-user-repartition",o);w.addRoute("analytic-cfd",d);w.addRoute("analytic-burndown",n);w.addRoute("analytic-avg-time-column",h);w.addRoute("analytic-task-time-column",v);w.addRoute("analytic-lead-cycle-time",t);w.addRoute("gantt-chart",c);w.dispatch(x);x.listen()})})(); \ No newline at end of file
diff --git a/assets/js/src/App.js b/assets/js/src/App.js
index ca94661d..0102ada4 100644
--- a/assets/js/src/App.js
+++ b/assets/js/src/App.js
@@ -8,6 +8,7 @@ function App() {
this.tooltip = new Tooltip(this);
this.popover = new Popover(this);
this.task = new Task();
+ this.project = new Project();
this.keyboardShortcuts();
this.chosen();
this.poll();
@@ -29,6 +30,7 @@ function App() {
}
App.prototype.listen = function() {
+ this.project.listen();
this.popover.listen();
this.markdown.listen();
this.sidebar.listen();
@@ -38,7 +40,7 @@ App.prototype.listen = function() {
this.task.listen();
this.swimlane.listen();
this.search.focus();
- this.taskAutoComplete();
+ this.autoComplete();
this.datePicker();
this.focus();
};
@@ -127,25 +129,30 @@ App.prototype.datePicker = function() {
});
};
-App.prototype.taskAutoComplete = function() {
- // Task auto-completion
- if ($(".task-autocomplete").length) {
+App.prototype.autoComplete = function() {
+ $(".autocomplete").each(function() {
+ var input = $(this);
+ var field = input.data("dst-field");
+ var extraField = input.data("dst-extra-field");
- if ($('.opposite_task_id').val() == '') {
- $(".task-autocomplete").parent().find("input[type=submit]").attr('disabled','disabled');
+ if ($('#form-' + field).val() == '') {
+ input.parent().find("input[type=submit]").attr('disabled','disabled');
}
- $(".task-autocomplete").autocomplete({
- source: $(".task-autocomplete").data("search-url"),
+ input.autocomplete({
+ source: input.data("search-url"),
minLength: 1,
select: function(event, ui) {
- var field = $(".task-autocomplete").data("dst-field");
$("input[name=" + field + "]").val(ui.item.id);
- $(".task-autocomplete").parent().find("input[type=submit]").removeAttr('disabled');
+ if (extraField) {
+ $("input[name=" + extraField + "]").val(ui.item[extraField]);
+ }
+
+ input.parent().find("input[type=submit]").removeAttr('disabled');
}
});
- }
+ });
};
App.prototype.chosen = function() {
diff --git a/assets/js/src/Project.js b/assets/js/src/Project.js
new file mode 100644
index 00000000..e2412412
--- /dev/null
+++ b/assets/js/src/Project.js
@@ -0,0 +1,18 @@
+function Project() {
+}
+
+Project.prototype.listen = function() {
+ $('.project-change-role').on('change', function() {
+ $.ajax({
+ cache: false,
+ url: $(this).data('url'),
+ contentType: "application/json",
+ type: "POST",
+ processData: false,
+ data: JSON.stringify({
+ "id": $(this).data('id'),
+ "role": $(this).val()
+ })
+ });
+ });
+};
diff --git a/composer.lock b/composer.lock
index a7abe89f..d232f879 100644
--- a/composer.lock
+++ b/composer.lock
@@ -300,12 +300,12 @@
"source": {
"type": "git",
"url": "https://github.com/fguillot/picoDb.git",
- "reference": "2db9ce62ac3aa968fbbc24ee4d418cab21b5ed3f"
+ "reference": "0b4640ebe531a89d2faa36dfabe30a194f1fe903"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/fguillot/picoDb/zipball/2db9ce62ac3aa968fbbc24ee4d418cab21b5ed3f",
- "reference": "2db9ce62ac3aa968fbbc24ee4d418cab21b5ed3f",
+ "url": "https://api.github.com/repos/fguillot/picoDb/zipball/0b4640ebe531a89d2faa36dfabe30a194f1fe903",
+ "reference": "0b4640ebe531a89d2faa36dfabe30a194f1fe903",
"shasum": ""
},
"require": {
@@ -329,7 +329,7 @@
],
"description": "Minimalist database query builder",
"homepage": "https://github.com/fguillot/picoDb",
- "time": "2015-11-26 02:33:54"
+ "time": "2015-11-30 02:57:18"
},
{
"name": "fguillot/simple-validator",
diff --git a/config.default.php b/config.default.php
index 067d9d60..d8d0ba3b 100644
--- a/config.default.php
+++ b/config.default.php
@@ -65,6 +65,10 @@ define('LDAP_SSL_VERIFY', true);
// Enable LDAP START_TLS
define('LDAP_START_TLS', false);
+// By default Kanboard lowercase the ldap username to avoid duplicate users (the database is case sensitive)
+// Set to true if you want to preserve the case
+define('LDAP_USERNAME_CASE_SENSITIVE', false);
+
// LDAP bind type: "anonymous", "user" or "proxy"
define('LDAP_BIND_TYPE', 'anonymous');
@@ -75,43 +79,56 @@ define('LDAP_USERNAME', null);
// LDAP password to use for proxy mode
define('LDAP_PASSWORD', null);
-// LDAP account base, i.e. root of all user account
-// Example: ou=People,dc=example,dc=com
-define('LDAP_ACCOUNT_BASE', '');
+// LDAP DN for users
+// Example for ActiveDirectory: CN=Users,DC=kanboard,DC=local
+// Example for OpenLDAP: ou=People,dc=example,dc=com
+define('LDAP_USER_BASE_DN', '');
-// LDAP query pattern to use when searching for a user account
+// LDAP pattern to use when searching for a user account
// Example for ActiveDirectory: '(&(objectClass=user)(sAMAccountName=%s))'
// Example for OpenLDAP: 'uid=%s'
-define('LDAP_USER_PATTERN', '');
+define('LDAP_USER_FILTER', '');
-// Name of an attribute of the user account object which should be used as the full name of the user
-define('LDAP_ACCOUNT_FULLNAME', 'displayname');
-
-// Name of an attribute of the user account object which should be used as the email of the user
-define('LDAP_ACCOUNT_EMAIL', 'mail');
-
-// Name of an attribute of the user account object which should be used as the id of the user. (optional)
+// LDAP attribute for username
// Example for ActiveDirectory: 'samaccountname'
// Example for OpenLDAP: 'uid'
-define('LDAP_ACCOUNT_ID', '');
+define('LDAP_USER_ATTRIBUTE_USERNAME', 'uid');
+
+// LDAP attribute for user full name
+// Example for ActiveDirectory: 'displayname'
+// Example for OpenLDAP: 'cn'
+define('LDAP_USER_ATTRIBUTE_FULLNAME', 'cn');
-// LDAP Attribute for group membership
-define('LDAP_ACCOUNT_MEMBEROF', 'memberof');
+// LDAP attribute for user email
+define('LDAP_USER_ATTRIBUTE_EMAIL', 'mail');
-// DN for administrators
-// Example: CN=Kanboard Admins,CN=Users,DC=kanboard,DC=local
+// LDAP attribute to find groups in user profile
+define('LDAP_USER_ATTRIBUTE_GROUPS', 'memberof');
+
+// Allow automatic LDAP user creation
+define('LDAP_USER_CREATION', true);
+
+// LDAP DN for administrators
+// Example: CN=Kanboard-Admins,CN=Users,DC=kanboard,DC=local
define('LDAP_GROUP_ADMIN_DN', '');
-// DN for project administrators
-// Example: CN=Kanboard Project Admins,CN=Users,DC=kanboard,DC=local
-define('LDAP_GROUP_PROJECT_ADMIN_DN', '');
+// LDAP DN for managers
+// Example: CN=Kanboard Managers,CN=Users,DC=kanboard,DC=local
+define('LDAP_GROUP_MANAGER_DN', '');
-// By default Kanboard lowercase the ldap username to avoid duplicate users (the database is case sensitive)
-// Set to true if you want to preserve the case
-define('LDAP_USERNAME_CASE_SENSITIVE', false);
+// Enable LDAP group provider for project permissions
+// The end-user will be able to browse LDAP groups from the user interface and allow access to specified projects
+define('LDAP_GROUP_PROVIDER', false);
+
+// LDAP Base DN for groups
+define('LDAP_GROUP_BASE_DN', '');
+
+// LDAP group filter
+// Example for ActiveDirectory: (&(objectClass=group)(sAMAccountName=%s*))
+define('LDAP_GROUP_FILTER', '');
-// Automatically create user account
-define('LDAP_ACCOUNT_CREATION', true);
+// LDAP attribute for the group name
+define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn');
// Enable/disable Google authentication
define('GOOGLE_AUTH', false);
diff --git a/doc/api-action-procedures.markdown b/doc/api-action-procedures.markdown
new file mode 100644
index 00000000..5dd88f4a
--- /dev/null
+++ b/doc/api-action-procedures.markdown
@@ -0,0 +1,245 @@
+API Automatic Actions Procedures
+================================
+
+### getAvailableActions
+
+- Purpose: **Get list of available automatic actions**
+- Parameters: none
+- Result on success: **list of actions**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAvailableActions",
+ "id": 1217735483
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1217735483,
+ "result": {
+ "TaskLogMoveAnotherColumn": "Add a comment logging moving the task between columns",
+ "TaskAssignColorUser": "Assign a color to a specific user",
+ "TaskAssignColorColumn": "Assign a color when the task is moved to a specific column",
+ "TaskAssignCategoryColor": "Assign automatically a category based on a color",
+ "TaskAssignColorCategory": "Assign automatically a color based on a category",
+ "TaskAssignSpecificUser": "Assign the task to a specific user",
+ "TaskAssignCurrentUser": "Assign the task to the person who does the action",
+ "TaskUpdateStartDate": "Automatically update the start date",
+ "TaskAssignUser": "Change the assignee based on an external username",
+ "TaskAssignCategoryLabel": "Change the category based on an external label",
+ "TaskClose": "Close a task",
+ "CommentCreation": "Create a comment from an external provider",
+ "TaskCreation": "Create a task from an external provider",
+ "TaskDuplicateAnotherProject": "Duplicate the task to another project",
+ "TaskMoveColumnAssigned": "Move the task to another column when assigned to a user",
+ "TaskMoveColumnUnAssigned": "Move the task to another column when assignee is cleared",
+ "TaskMoveAnotherProject": "Move the task to another project",
+ "TaskOpen": "Open a task"
+ }
+}
+```
+
+### getAvailableActionEvents
+
+- Purpose: **Get list of available events for actions**
+- Parameters: none
+- Result on success: **list of events**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAvailableActionEvents",
+ "id": 2116665643
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2116665643,
+ "result": {
+ "bitbucket.webhook.commit": "Bitbucket commit received",
+ "task.close": "Closing a task",
+ "github.webhook.commit": "Github commit received",
+ "github.webhook.issue.assignee": "Github issue assignee change",
+ "github.webhook.issue.closed": "Github issue closed",
+ "github.webhook.issue.commented": "Github issue comment created",
+ "github.webhook.issue.label": "Github issue label change",
+ "github.webhook.issue.opened": "Github issue opened",
+ "github.webhook.issue.reopened": "Github issue reopened",
+ "gitlab.webhook.commit": "Gitlab commit received",
+ "gitlab.webhook.issue.closed": "Gitlab issue closed",
+ "gitlab.webhook.issue.opened": "Gitlab issue opened",
+ "task.move.column": "Move a task to another column",
+ "task.open": "Open a closed task",
+ "task.assignee_change": "Task assignee change",
+ "task.create": "Task creation",
+ "task.create_update": "Task creation or modification",
+ "task.update": "Task modification"
+ }
+}
+```
+
+### getCompatibleActionEvents
+
+- Purpose: **Get list of events compatible with an action**
+- Parameters:
+ - **action_name** (string, required)
+- Result on success: **list of events**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getCompatibleActionEvents",
+ "id": 899370297,
+ "params": [
+ "TaskClose"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 899370297,
+ "result": {
+ "bitbucket.webhook.commit": "Bitbucket commit received",
+ "github.webhook.commit": "Github commit received",
+ "github.webhook.issue.closed": "Github issue closed",
+ "gitlab.webhook.commit": "Gitlab commit received",
+ "gitlab.webhook.issue.closed": "Gitlab issue closed",
+ "task.move.column": "Move a task to another column"
+ }
+}
+```
+
+### getActions
+
+- Purpose: **Get list of actions for a project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **list of actions properties**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getActions",
+ "id": 1433237746,
+ "params": [
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1433237746,
+ "result": [
+ {
+ "id" : "13",
+ "project_id" : "2",
+ "event_name" : "task.move.column",
+ "action_name" : "TaskAssignSpecificUser",
+ "params" : {
+ "column_id" : "5",
+ "user_id" : "1"
+ }
+ }
+ ]
+}
+```
+
+### createAction
+
+- Purpose: **Create an action**
+- Parameters:
+ - **project_id** (integer, required)
+ - **event_name** (string, required)
+ - **action_name** (string, required)
+ - **params** (key/value parameters, required)
+- Result on success: **action_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createAction",
+ "id": 1433237746,
+ "params": {
+ "project_id" : "2",
+ "event_name" : "task.move.column",
+ "action_name" : "TaskAssignSpecificUser",
+ "params" : {
+ "column_id" : "3",
+ "user_id" : "2"
+ }
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1433237746,
+ "result": 14
+}
+```
+
+### removeAction
+
+- Purpose: **Remove an action**
+- Parameters:
+ - **action_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeAction",
+ "id": 1510741671,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1510741671,
+ "result": true
+}
+```
diff --git a/doc/api-application-procedures.markdown b/doc/api-application-procedures.markdown
new file mode 100644
index 00000000..ab1230cc
--- /dev/null
+++ b/doc/api-application-procedures.markdown
@@ -0,0 +1,231 @@
+API Application Procedures
+==========================
+
+### getVersion
+
+- Purpose: **Get the application version**
+- Parameters: none
+- Result: **version** (Example: 1.0.12, master)
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getVersion",
+ "id": 1661138292
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1661138292,
+ "result": "1.0.13"
+}
+```
+
+### getTimezone
+
+- Purpose: **Get the application timezone**
+- Parameters: none
+- Result on success: **Timezone** (Example: UTC, Europe/Paris)
+- Result on failure: **Default timezone** (UTC)
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getTimezone",
+ "id": 1661138292
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1661138292,
+ "result": "Europe\/Paris"
+}
+```
+
+### getDefaultTaskColors
+
+- Purpose: **Get all default task colors**
+- Parameters: None
+- Result on success: **Color properties**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getDefaultTaskColors",
+ "id": 2108929212
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2108929212,
+ "result": {
+ "yellow": {
+ "name": "Yellow",
+ "background": "rgb(245, 247, 196)",
+ "border": "rgb(223, 227, 45)"
+ },
+ "blue": {
+ "name": "Blue",
+ "background": "rgb(219, 235, 255)",
+ "border": "rgb(168, 207, 255)"
+ },
+ "green": {
+ "name": "Green",
+ "background": "rgb(189, 244, 203)",
+ "border": "rgb(74, 227, 113)"
+ },
+ "purple": {
+ "name": "Purple",
+ "background": "rgb(223, 176, 255)",
+ "border": "rgb(205, 133, 254)"
+ },
+ "red": {
+ "name": "Red",
+ "background": "rgb(255, 187, 187)",
+ "border": "rgb(255, 151, 151)"
+ },
+ "orange": {
+ "name": "Orange",
+ "background": "rgb(255, 215, 179)",
+ "border": "rgb(255, 172, 98)"
+ },
+ "grey": {
+ "name": "Grey",
+ "background": "rgb(238, 238, 238)",
+ "border": "rgb(204, 204, 204)"
+ },
+ "brown": {
+ "name": "Brown",
+ "background": "#d7ccc8",
+ "border": "#4e342e"
+ },
+ "deep_orange": {
+ "name": "Deep Orange",
+ "background": "#ffab91",
+ "border": "#e64a19"
+ },
+ "dark_grey": {
+ "name": "Dark Grey",
+ "background": "#cfd8dc",
+ "border": "#455a64"
+ },
+ "pink": {
+ "name": "Pink",
+ "background": "#f48fb1",
+ "border": "#d81b60"
+ },
+ "teal": {
+ "name": "Teal",
+ "background": "#80cbc4",
+ "border": "#00695c"
+ },
+ "cyan": {
+ "name": "Cyan",
+ "background": "#b2ebf2",
+ "border": "#00bcd4"
+ },
+ "lime": {
+ "name": "Lime",
+ "background": "#e6ee9c",
+ "border": "#afb42b"
+ },
+ "light_green": {
+ "name": "Light Green",
+ "background": "#dcedc8",
+ "border": "#689f38"
+ },
+ "amber": {
+ "name": "Amber",
+ "background": "#ffe082",
+ "border": "#ffa000"
+ }
+ }
+}
+```
+
+### getDefaultTaskColor
+
+- Purpose: **Get default task color**
+- Parameters: None
+- Result on success: **color_id**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getDefaultTaskColor",
+ "id": 1144775215
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1144775215,
+ "result": "yellow"
+}
+```
+
+### getColorList
+
+- Purpose: **Get the list of task colors**
+- Parameters: none
+- Result on success: **Dictionary of color_id => color_name**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getColorList",
+ "id": 1677051386
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1677051386,
+ "result": {
+ "yellow": "Yellow",
+ "blue": "Blue",
+ "green": "Green",
+ "purple": "Purple",
+ "red": "Red",
+ "orange": "Orange",
+ "grey": "Grey",
+ "brown": "Brown",
+ "deep_orange": "Deep Orange",
+ "dark_grey": "Dark Grey",
+ "pink": "Pink",
+ "teal": "Teal",
+ "cyan": "Cyan",
+ "lime": "Lime",
+ "light_green": "Light Green",
+ "amber": "Amber"
+ }
+}
+```
diff --git a/doc/api-authentication.markdown b/doc/api-authentication.markdown
new file mode 100644
index 00000000..962e5b1b
--- /dev/null
+++ b/doc/api-authentication.markdown
@@ -0,0 +1,66 @@
+API Authentication
+==================
+
+Default method (HTTP Basic)
+---------------------------
+
+The API credentials are available on the settings page.
+
+- API end-point: `https://YOUR_SERVER/jsonrpc.php`
+
+If you want to use the "application api":
+
+- Username: `jsonrpc`
+- Password: API token on the settings page
+
+Otherwise for the "user api", just use the real username/passsword.
+
+The API use the [HTTP Basic Authentication Scheme described in the RFC2617](http://www.ietf.org/rfc/rfc2617.txt).
+If there is an authentication error, you will receive the HTTP status code `401 Not Authorized`.
+
+### Authorized User API procedures
+
+- getMe
+- getMyDashboard
+- getMyActivityStream
+- createMyPrivateProject
+- getMyProjectsList
+- getMyProjects
+- getTimezone
+- getVersion
+- getDefaultTaskColor
+- getDefaultTaskColors
+- getColorList
+- getProjectById
+- getTask
+- getTaskByReference
+- getAllTasks
+- openTask
+- closeTask
+- moveTaskPosition
+- createTask
+- updateTask
+- getBoard
+- getProjectActivity
+- getMyOverdueTasks
+
+Custom HTTP header
+------------------
+
+You can use an alternative HTTP header for the authentication if your server have a very specific configuration.
+
+- The header name can be anything you want, by example `X-API-Auth`.
+- The header value is the `username:password` encoded in Base64.
+
+Configuration:
+
+1. Define your custom header in your `config.php`: `define('API_AUTHENTICATION_HEADER', 'X-API-Auth');`
+2. Encode the credentials in Base64, example with PHP `base64_encode('jsonrpc:19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929');`
+3. Test with curl:
+
+```bash
+curl \
+-H 'X-API-Auth: anNvbnJwYzoxOWZmZDk3MDlkMDNjZTUwNjc1YzNhNDNkMWM0OWMxYWMyMDdmNGJjNDVmMDZjNWIyNzAxZmJkZjg5Mjk=' \
+-d '{"jsonrpc": "2.0", "method": "getAllProjects", "id": 1}' \
+http://localhost/kanboard/jsonrpc.php
+```
diff --git a/doc/api-board-procedures.markdown b/doc/api-board-procedures.markdown
new file mode 100644
index 00000000..8643fa75
--- /dev/null
+++ b/doc/api-board-procedures.markdown
@@ -0,0 +1,416 @@
+API Board Procedures
+====================
+
+### getBoard
+
+- Purpose: **Get all necessary information to display a board**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **board properties**
+- Result on failure: **empty list**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getBoard",
+ "id": 827046470,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 827046470,
+ "result": [
+ {
+ "id": 0,
+ "name": "Default swimlane",
+ "columns": [
+ {
+ "id": "1",
+ "title": "Backlog",
+ "position": "1",
+ "project_id": "1",
+ "task_limit": "0",
+ "description": "",
+ "tasks": [],
+ "nb_tasks": 0,
+ "score": 0
+ },
+ {
+ "id": "2",
+ "title": "Ready",
+ "position": "2",
+ "project_id": "1",
+ "task_limit": "0",
+ "description": "",
+ "tasks": [
+ {
+ "nb_comments":"0",
+ "nb_files":"0",
+ "nb_subtasks":"0",
+ "nb_completed_subtasks":"0",
+ "nb_links":"0",
+ "id":"2",
+ "reference":"",
+ "title":"Test",
+ "description":"",
+ "date_creation":"1430870507",
+ "date_modification":"1430870507",
+ "date_completed":null,
+ "date_due":"0",
+ "color_id":"yellow",
+ "project_id":"1",
+ "column_id":"2",
+ "swimlane_id":"0",
+ "owner_id":"0",
+ "creator_id":"1",
+ "position":"1",
+ "is_active":"1",
+ "score":"0",
+ "category_id":"0",
+ "date_moved":"1430870507",
+ "recurrence_status":"0",
+ "recurrence_trigger":"0",
+ "recurrence_factor":"0",
+ "recurrence_timeframe":"0",
+ "recurrence_basedate":"0",
+ "recurrence_parent":null,
+ "recurrence_child":null,
+ "assignee_username":null,
+ "assignee_name":null
+ }
+ ],
+ "nb_tasks": 1,
+ "score": 0
+ },
+ {
+ "id": "3",
+ "title": "Work in progress",
+ "position": "3",
+ "project_id": "1",
+ "task_limit": "0",
+ "description": "",
+ "tasks": [
+ {
+ "nb_comments":"0",
+ "nb_files":"0",
+ "nb_subtasks":"1",
+ "nb_completed_subtasks":"0",
+ "nb_links":"0",
+ "id":"1",
+ "reference":"",
+ "title":"Task with comment",
+ "description":"",
+ "date_creation":"1430783188",
+ "date_modification":"1430783188",
+ "date_completed":null,
+ "date_due":"0",
+ "color_id":"red",
+ "project_id":"1",
+ "column_id":"3",
+ "swimlane_id":"0",
+ "owner_id":"1",
+ "creator_id":"0",
+ "position":"1",
+ "is_active":"1",
+ "score":"0",
+ "category_id":"0",
+ "date_moved":"1430783191",
+ "recurrence_status":"0",
+ "recurrence_trigger":"0",
+ "recurrence_factor":"0",
+ "recurrence_timeframe":"0",
+ "recurrence_basedate":"0",
+ "recurrence_parent":null,
+ "recurrence_child":null,
+ "assignee_username":"admin",
+ "assignee_name":null
+ }
+ ],
+ "nb_tasks": 1,
+ "score": 0
+ },
+ {
+ "id": "4",
+ "title": "Done",
+ "position": "4",
+ "project_id": "1",
+ "task_limit": "0",
+ "description": "",
+ "tasks": [],
+ "nb_tasks": 0,
+ "score": 0
+ }
+ ],
+ "nb_columns": 4,
+ "nb_tasks": 2
+ }
+ ]
+}
+```
+
+### getColumns
+
+- Purpose: **Get all columns information for a given project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **columns properties**
+- Result on failure: **empty list**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getColumns",
+ "id": 887036325,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 887036325,
+ "result": [
+ {
+ "id": "1",
+ "title": "Backlog",
+ "position": "1",
+ "project_id": "1",
+ "task_limit": "0"
+ },
+ {
+ "id": "2",
+ "title": "Ready",
+ "position": "2",
+ "project_id": "1",
+ "task_limit": "0"
+ },
+ {
+ "id": "3",
+ "title": "Work in progress",
+ "position": "3",
+ "project_id": "1",
+ "task_limit": "0"
+ }
+ ]
+}
+```
+
+### getColumn
+
+- Purpose: **Get a single column**
+- Parameters:
+ - **column_id** (integer, required)
+- Result on success: **column properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getColumn",
+ "id": 1242049935,
+ "params": [
+ 2
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1242049935,
+ "result": {
+ "id": "2",
+ "title": "Youpi",
+ "position": "2",
+ "project_id": "1",
+ "task_limit": "5"
+ }
+}
+```
+
+### moveColumnUp
+
+- Purpose: **Move up the column position**
+- Parameters:
+ - **project_id** (integer, required)
+ - **column_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "moveColumnUp",
+ "id": 99275573,
+ "params": [
+ 1,
+ 2
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 99275573,
+ "result": true
+}
+```
+
+### moveColumnDown
+
+- Purpose: **Move down the column position**
+- Parameters:
+ - **project_id** (integer, required)
+ - **column_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "moveColumnDown",
+ "id": 957090649,
+ "params": {
+ "project_id": 1,
+ "column_id": 2
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 957090649,
+ "result": true
+}
+```
+
+### updateColumn
+
+- Purpose: **Update column properties**
+- Parameters:
+ - **column_id** (integer, required)
+ - **title** (string, required)
+ - **task_limit** (integer, optional)
+ - **description** (string, optional)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateColumn",
+ "id": 480740641,
+ "params": [
+ 2,
+ "Boo",
+ 5
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 480740641,
+ "result": true
+}
+```
+
+### addColumn
+
+- Purpose: **Add a new column**
+- Parameters:
+ - **project_id** (integer, required)
+ - **title** (string, required)
+ - **task_limit** (integer, optional)
+ - **description** (string, optional)
+- Result on success: **column_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "addColumn",
+ "id": 638544704,
+ "params": [
+ 1,
+ "Boo"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 638544704,
+ "result": 5
+}
+```
+
+### removeColumn
+
+- Purpose: **Remove a column**
+- Parameters:
+ - **column_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeColumn",
+ "id": 1433237746,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1433237746,
+ "result": true
+}
+```
diff --git a/doc/api-category-procedures.markdown b/doc/api-category-procedures.markdown
new file mode 100644
index 00000000..6a762009
--- /dev/null
+++ b/doc/api-category-procedures.markdown
@@ -0,0 +1,172 @@
+API Category Procedures
+=======================
+
+### createCategory
+
+- Purpose: **Create a new category**
+- Parameters:
+- **project_id** (integer, required)
+ - **name** (string, required, must be unique for the given project)
+- Result on success: **category_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createCategory",
+ "id": 541909890,
+ "params": {
+ "name": "Super category",
+ "project_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 541909890,
+ "result": 4
+}
+```
+
+### getCategory
+
+- Purpose: **Get category information**
+- Parameters:
+ - **category_id** (integer, required)
+- Result on success: **category properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getCategory",
+ "id": 203539163,
+ "params": {
+ "category_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+
+ "jsonrpc": "2.0",
+ "id": 203539163,
+ "result": {
+ "id": "1",
+ "name": "Super category",
+ "project_id": "1"
+ }
+}
+```
+
+### getAllCategories
+
+- Purpose: **Get all available categories**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **List of categories**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllCategories",
+ "id": 1261777968,
+ "params": {
+ "project_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1261777968,
+ "result": [
+ {
+ "id": "1",
+ "name": "Super category",
+ "project_id": "1"
+ }
+ ]
+}
+```
+
+### updateCategory
+
+- Purpose: **Update a category**
+- Parameters:
+ - **id** (integer, required)
+ - **name** (string, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateCategory",
+ "id": 570195391,
+ "params": {
+ "id": 1,
+ "name": "Renamed category"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 570195391,
+ "result": true
+}
+```
+
+### removeCategory
+
+- Purpose: **Remove a category**
+- Parameters:
+ - **category_id** (integer)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeCategory",
+ "id": 88225706,
+ "params": {
+ "category_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 88225706,
+ "result": true
+}
+```
diff --git a/doc/api-comment-procedures.markdown b/doc/api-comment-procedures.markdown
new file mode 100644
index 00000000..33a856ea
--- /dev/null
+++ b/doc/api-comment-procedures.markdown
@@ -0,0 +1,182 @@
+API Comment Procedures
+======================
+
+### createComment
+
+- Purpose: **Create a new comment**
+- Parameters:
+ - **task_id** (integer, required)
+ - **user_id** (integer, required)
+ - **content** Markdown content (string, required)
+- Result on success: **comment_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createComment",
+ "id": 1580417921,
+ "params": {
+ "task_id": 1,
+ "user_id": 1,
+ "content": "Comment #1"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1580417921,
+ "result": 11
+}
+```
+
+### getComment
+
+- Purpose: **Get comment information**
+- Parameters:
+ - **comment_id** (integer, required)
+- Result on success: **comment properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getComment",
+ "id": 867839500,
+ "params": {
+ "comment_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 867839500,
+ "result": {
+ "id": "1",
+ "task_id": "1",
+ "user_id": "1",
+ "date_creation": "1410881970",
+ "comment": "Comment #1",
+ "username": "admin",
+ "name": null
+ }
+}
+```
+
+### getAllComments
+
+- Purpose: **Get all available comments**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **List of comments**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllComments",
+ "id": 148484683,
+ "params": {
+ "task_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 148484683,
+ "result": [
+ {
+ "id": "1",
+ "date_creation": "1410882272",
+ "task_id": "1",
+ "user_id": "1",
+ "comment": "Comment #1",
+ "username": "admin",
+ "name": null
+ },
+ ...
+ ]
+}
+```
+
+### updateComment
+
+- Purpose: **Update a comment**
+- Parameters:
+ - **id** (integer, required)
+ - **content** Markdown content (string, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateComment",
+ "id": 496470023,
+ "params": {
+ "id": 1,
+ "content": "Comment #1 updated"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1493368950,
+ "result": true
+}
+```
+
+### removeComment
+
+- Purpose: **Remove a comment**
+- Parameters:
+ - **comment_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeComment",
+ "id": 328836871,
+ "params": {
+ "comment_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 328836871,
+ "result": true
+}
+```
diff --git a/doc/api-examples.markdown b/doc/api-examples.markdown
new file mode 100644
index 00000000..9b1ed12a
--- /dev/null
+++ b/doc/api-examples.markdown
@@ -0,0 +1,184 @@
+API Examples
+============
+
+Example with cURL
+-----------------
+
+From the command line:
+
+```bash
+curl \
+-u "jsonrpc:19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929" \
+-d '{"jsonrpc": "2.0", "method": "getAllProjects", "id": 1}' \
+http://localhost/kanboard/jsonrpc.php
+```
+
+Response from the server:
+
+```json
+{
+ "jsonrpc":"2.0",
+ "id":1,
+ "result":[
+ {
+ "id":"1",
+ "name":"API test",
+ "is_active":"1",
+ "token":"6bd0932fe7f4b5e6e4bc3c72800bfdef36a2c5de2f38f756dfb5bd632ebf",
+ "last_modified":"1403392631"
+ }
+ ]
+}
+```
+
+Example with Python
+-------------------
+
+Here a basic example written in Python to create a task:
+
+```python
+#!/usr/bin/env python
+
+import requests
+import json
+
+def main():
+ url = "http://demo.kanboard.net/jsonrpc.php"
+ api_key = "be4271664ca8169d32af49d8e1ec854edb0290bc3588a2e356275eab9505"
+ headers = {"content-type": "application/json"}
+
+ payload = {
+ "method": "createTask",
+ "params": {
+ "title": "Python API test",
+ "project_id": 1
+ },
+ "jsonrpc": "2.0",
+ "id": 1,
+ }
+
+ response = requests.post(
+ url,
+ data=json.dumps(payload),
+ headers=headers,
+ auth=("jsonrpc", api_key)
+ )
+
+ if response.status_code == 401:
+ print "Authentication failed"
+ else:
+ result = response.json()
+
+ assert result["result"] == True
+ assert result["jsonrpc"]
+ assert result["id"] == 1
+
+ print "Task created successfully!"
+
+if __name__ == "__main__":
+ main()
+```
+
+Run this script from your terminal:
+
+```bash
+python jsonrpc.py
+Task created successfully!
+```
+
+Example with a PHP client
+-------------------------
+
+I wrote a simple [Json-RPC Client/Server library in PHP](https://github.com/fguillot/JsonRPC), here an example:
+
+```php
+<?php
+
+$client = new JsonRPC\Client('http://localhost:8000/jsonrpc.php');
+$client->authentication('jsonrpc', '19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929');
+
+print_r($client->getAllProjects());
+
+```
+
+The response:
+
+```
+Array
+(
+ [0] => Array
+ (
+ [id] => 1
+ [name] => API test
+ [is_active] => 1
+ [token] => 6bd0932fe7f4b5e6e4bc3c72800bfdef36a2c5de2f38f756dfb5bd632ebf
+ [last_modified] => 1403392631
+ )
+
+)
+```
+
+Example with Ruby
+-----------------
+
+This example can be used with Kanboard configured with Reverse-Proxy authentication and the API configured with a custom authentication header:
+
+```ruby
+require 'faraday'
+
+conn = Faraday.new(:url => 'https://kanboard.example.com') do |faraday|
+ faraday.response :logger
+ faraday.headers['X-API-Auth'] = 'XXX' # base64_encode('jsonrpc:API_KEY')
+ faraday.basic_auth(ENV['user'], ENV['pw']) # user/pass to get through basic auth
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
+end
+
+response = conn.post do |req|
+ req.url '/jsonrpc.php'
+ req.headers['Content-Type'] = 'application/json'
+ req.body = '{ "jsonrpc": "2.0", "id": 1, "method": "getAllProjects" }'
+end
+
+puts response.body
+```
+
+
+Example with Java
+-----------------
+
+This is a basic example using Spring. For proper usage see [this link](http://spring.io/guides/gs/consuming-rest).
+
+```java
+import java.io.UnsupportedEncodingException;
+import java.util.Base64;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.web.client.RestTemplate;
+
+public class ProjectService {
+
+ public void getAllProjects() throws UnsupportedEncodingException {
+
+ RestTemplate restTemplate = new RestTemplate();
+
+ String url = "http://localhost/kanboard/jsonrpc.php";
+ String requestJson = "{\"jsonrpc\": \"2.0\", \"method\": \"getAllProjects\", \"id\": 1}";
+ String user = "jsonrpc";
+ String apiToken = "19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929";
+
+ // encode api token
+ byte[] xApiAuthTokenBytes = String.join(":", user, apiToken).getBytes("utf-8");
+ String xApiAuthToken = Base64.getEncoder().encodeToString(xApiAuthTokenBytes);
+
+ // consume request
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("X-API-Auth", xApiAuthToken);
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity<String> entity = new HttpEntity<String>(requestJson, headers);
+ String answer = restTemplate.postForObject(url, entity, String.class);
+ System.out.println(answer);
+ }
+}
+```
diff --git a/doc/api-file-procedures.markdown b/doc/api-file-procedures.markdown
new file mode 100644
index 00000000..35f633a1
--- /dev/null
+++ b/doc/api-file-procedures.markdown
@@ -0,0 +1,217 @@
+API File Procedures
+===================
+
+### createFile
+
+- Purpose: **Create and upload a new task attachment**
+- Parameters:
+ - **project_id** (integer, required)
+ - **task_id** (integer, required)
+ - **filename** (integer, required)
+ - **blob** File content encoded in base64 (string, required)
+- Result on success: **file_id**
+- Result on failure: **false**
+- Note: **The maximum file size depends of your PHP configuration, this method should not be used to upload large files**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createFile",
+ "id": 94500810,
+ "params": [
+ 1,
+ 1,
+ "My file",
+ "cGxhaW4gdGV4dCBmaWxl"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 94500810,
+ "result": 1
+}
+```
+
+### getAllFiles
+
+- Purpose: **Get all files attached to task**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **list of files**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllFiles",
+ "id": 1880662820,
+ "params": {
+ "task_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1880662820,
+ "result": [
+ {
+ "id": "1",
+ "name": "My file",
+ "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596",
+ "is_image": "0",
+ "task_id": "1",
+ "date": "1432509941",
+ "user_id": "0",
+ "size": "15",
+ "username": null,
+ "user_name": null
+ }
+ ]
+}
+```
+
+### getFile
+
+- Purpose: **Get file information**
+- Parameters:
+ - **file_id** (integer, required)
+- Result on success: **file properties**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getFile",
+ "id": 318676852,
+ "params": [
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 318676852,
+ "result": {
+ "id": "1",
+ "name": "My file",
+ "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596",
+ "is_image": "0",
+ "task_id": "1",
+ "date": "1432509941",
+ "user_id": "0",
+ "size": "15"
+ }
+}
+```
+
+### downloadFile
+
+- Purpose: **Download file contents (encoded in base64)**
+- Parameters:
+ - **file_id** (integer, required)
+- Result on success: **base64 encoded string**
+- Result on failure: **empty string**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "downloadFile",
+ "id": 235943344,
+ "params": [
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 235943344,
+ "result": "cGxhaW4gdGV4dCBmaWxl"
+}
+```
+
+### removeFile
+
+- Purpose: **Remove file**
+- Parameters:
+ - **file_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeFile",
+ "id": 447036524,
+ "params": [
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 447036524,
+ "result": true
+}
+```
+
+### removeAllFiles
+
+- Purpose: **Remove all files associated to a task**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeAllFiles",
+ "id": 593312993,
+ "params": {
+ "task_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 593312993,
+ "result": true
+}
+```
diff --git a/doc/api-json-rpc.markdown b/doc/api-json-rpc.markdown
index 359f8b05..710b0b1b 100644
--- a/doc/api-json-rpc.markdown
+++ b/doc/api-json-rpc.markdown
@@ -43,4511 +43,21 @@ You must call the API with a `POST` HTTP request.
Kanboard support batch requests, so you can make multiple API calls in a single HTTP request. It's particularly useful for mobile clients with higher network latency.
-Authentication
---------------
-
-### Default method (HTTP Basic)
-
-The API credentials are available on the settings page.
-
-- API end-point: `https://YOUR_SERVER/jsonrpc.php`
-
-If you want to use the "application api":
-
-- Username: `jsonrpc`
-- Password: API token on the settings page
-
-Otherwise for the "user api", just use the real username/passsword.
-
-The API use the [HTTP Basic Authentication Scheme described in the RFC2617](http://www.ietf.org/rfc/rfc2617.txt).
-If there is an authentication error, you will receive the HTTP status code `401 Not Authorized`.
-
-### Authorized User API procedures
-
-- getMe
-- getMyDashboard
-- getMyActivityStream
-- createMyPrivateProject
-- getMyProjectsList
-- getMyProjects
-- getTimezone
-- getVersion
-- getDefaultTaskColor
-- getDefaultTaskColors
-- getColorList
-- getProjectById
-- getTask
-- getTaskByReference
-- getAllTasks
-- openTask
-- closeTask
-- moveTaskPosition
-- createTask
-- updateTask
-- getBoard
-- getProjectActivity
-- getMyOverdueTasks
-
-### Custom HTTP header
-
-You can use an alternative HTTP header for the authentication if your server have a very specific configuration.
-
-- The header name can be anything you want, by example `X-API-Auth`.
-- The header value is the `username:password` encoded in Base64.
-
-Configuration:
-
-1. Define your custom header in your `config.php`: `define('API_AUTHENTICATION_HEADER', 'X-API-Auth');`
-2. Encode the credentials in Base64, example with PHP `base64_encode('jsonrpc:19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929');`
-3. Test with curl:
-
-```bash
-curl \
--H 'X-API-Auth: anNvbnJwYzoxOWZmZDk3MDlkMDNjZTUwNjc1YzNhNDNkMWM0OWMxYWMyMDdmNGJjNDVmMDZjNWIyNzAxZmJkZjg5Mjk=' \
--d '{"jsonrpc": "2.0", "method": "getAllProjects", "id": 1}' \
-http://localhost/kanboard/jsonrpc.php
-```
-
-Examples
---------
-
-### Example with cURL
-
-From the command line:
-
-```bash
-curl \
--u "jsonrpc:19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929" \
--d '{"jsonrpc": "2.0", "method": "getAllProjects", "id": 1}' \
-http://localhost/kanboard/jsonrpc.php
-```
-
-Response from the server:
-
-```json
-{
- "jsonrpc":"2.0",
- "id":1,
- "result":[
- {
- "id":"1",
- "name":"API test",
- "is_active":"1",
- "token":"6bd0932fe7f4b5e6e4bc3c72800bfdef36a2c5de2f38f756dfb5bd632ebf",
- "last_modified":"1403392631"
- }
- ]
-}
-```
-
-### Example with Python
-
-Here a basic example written in Python to create a task:
-
-```python
-#!/usr/bin/env python
-
-import requests
-import json
-
-def main():
- url = "http://demo.kanboard.net/jsonrpc.php"
- api_key = "be4271664ca8169d32af49d8e1ec854edb0290bc3588a2e356275eab9505"
- headers = {"content-type": "application/json"}
-
- payload = {
- "method": "createTask",
- "params": {
- "title": "Python API test",
- "project_id": 1
- },
- "jsonrpc": "2.0",
- "id": 1,
- }
-
- response = requests.post(
- url,
- data=json.dumps(payload),
- headers=headers,
- auth=("jsonrpc", api_key)
- )
-
- if response.status_code == 401:
- print "Authentication failed"
- else:
- result = response.json()
-
- assert result["result"] == True
- assert result["jsonrpc"]
- assert result["id"] == 1
-
- print "Task created successfully!"
-
-if __name__ == "__main__":
- main()
-```
-
-Run this script from your terminal:
-
-```bash
-python jsonrpc.py
-Task created successfully!
-```
-
-### Example with a PHP client:
-
-I wrote a simple [Json-RPC Client/Server library in PHP](https://github.com/fguillot/JsonRPC), here an example:
-
-```php
-<?php
-
-$client = new JsonRPC\Client('http://localhost:8000/jsonrpc.php');
-$client->authentication('jsonrpc', '19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929');
-
-print_r($client->getAllProjects());
-
-```
-
-The response:
-
-```
-Array
-(
- [0] => Array
- (
- [id] => 1
- [name] => API test
- [is_active] => 1
- [token] => 6bd0932fe7f4b5e6e4bc3c72800bfdef36a2c5de2f38f756dfb5bd632ebf
- [last_modified] => 1403392631
- )
-
-)
-```
-
-### Example with Ruby
-
-This example can be used with Kanboard configured with Reverse-Proxy authentication and the API configured with a custom authentication header:
-
-```ruby
-require 'faraday'
-
-conn = Faraday.new(:url => 'https://kanboard.example.com') do |faraday|
- faraday.response :logger
- faraday.headers['X-API-Auth'] = 'XXX' # base64_encode('jsonrpc:API_KEY')
- faraday.basic_auth(ENV['user'], ENV['pw']) # user/pass to get through basic auth
- faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
-end
-
-response = conn.post do |req|
- req.url '/jsonrpc.php'
- req.headers['Content-Type'] = 'application/json'
- req.body = '{ "jsonrpc": "2.0", "id": 1, "method": "getAllProjects" }'
-end
-
-puts response.body
-```
-
-
-### Example with Java
-
-This is a basic example using Spring. For proper usage see [this link](http://spring.io/guides/gs/consuming-rest).
-
-```java
-import java.io.UnsupportedEncodingException;
-import java.util.Base64;
-
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestTemplate;
-
-public class ProjectService {
-
- public void getAllProjects() throws UnsupportedEncodingException {
-
- RestTemplate restTemplate = new RestTemplate();
-
- String url = "http://localhost/kanboard/jsonrpc.php";
- String requestJson = "{\"jsonrpc\": \"2.0\", \"method\": \"getAllProjects\", \"id\": 1}";
- String user = "jsonrpc";
- String apiToken = "19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929";
-
- // encode api token
- byte[] xApiAuthTokenBytes = String.join(":", user, apiToken).getBytes("utf-8");
- String xApiAuthToken = Base64.getEncoder().encodeToString(xApiAuthTokenBytes);
-
- // consume request
- HttpHeaders headers = new HttpHeaders();
- headers.add("X-API-Auth", xApiAuthToken);
- headers.setContentType(MediaType.APPLICATION_JSON);
- HttpEntity<String> entity = new HttpEntity<String>(requestJson, headers);
- String answer = restTemplate.postForObject(url, entity, String.class);
- System.out.println(answer);
- }
-}
-```
-
-Procedures
-----------
-
-### getVersion
-
-- Purpose: **Get the application version**
-- Parameters: none
-- Result: **version** (Example: 1.0.12, master)
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getVersion",
- "id": 1661138292
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1661138292,
- "result": "1.0.13"
-}
-```
-
-### getTimezone
-
-- Purpose: **Get the application timezone**
-- Parameters: none
-- Result on success: **Timezone** (Example: UTC, Europe/Paris)
-- Result on failure: **Default timezone** (UTC)
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getTimezone",
- "id": 1661138292
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1661138292,
- "result": "Europe\/Paris"
-}
-```
-
-### getDefaultTaskColors
-
-- Purpose: **Get all default task colors**
-- Parameters: None
-- Result on success: **Color properties**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getDefaultTaskColors",
- "id": 2108929212
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 2108929212,
- "result": {
- "yellow": {
- "name": "Yellow",
- "background": "rgb(245, 247, 196)",
- "border": "rgb(223, 227, 45)"
- },
- "blue": {
- "name": "Blue",
- "background": "rgb(219, 235, 255)",
- "border": "rgb(168, 207, 255)"
- },
- "green": {
- "name": "Green",
- "background": "rgb(189, 244, 203)",
- "border": "rgb(74, 227, 113)"
- },
- "purple": {
- "name": "Purple",
- "background": "rgb(223, 176, 255)",
- "border": "rgb(205, 133, 254)"
- },
- "red": {
- "name": "Red",
- "background": "rgb(255, 187, 187)",
- "border": "rgb(255, 151, 151)"
- },
- "orange": {
- "name": "Orange",
- "background": "rgb(255, 215, 179)",
- "border": "rgb(255, 172, 98)"
- },
- "grey": {
- "name": "Grey",
- "background": "rgb(238, 238, 238)",
- "border": "rgb(204, 204, 204)"
- },
- "brown": {
- "name": "Brown",
- "background": "#d7ccc8",
- "border": "#4e342e"
- },
- "deep_orange": {
- "name": "Deep Orange",
- "background": "#ffab91",
- "border": "#e64a19"
- },
- "dark_grey": {
- "name": "Dark Grey",
- "background": "#cfd8dc",
- "border": "#455a64"
- },
- "pink": {
- "name": "Pink",
- "background": "#f48fb1",
- "border": "#d81b60"
- },
- "teal": {
- "name": "Teal",
- "background": "#80cbc4",
- "border": "#00695c"
- },
- "cyan": {
- "name": "Cyan",
- "background": "#b2ebf2",
- "border": "#00bcd4"
- },
- "lime": {
- "name": "Lime",
- "background": "#e6ee9c",
- "border": "#afb42b"
- },
- "light_green": {
- "name": "Light Green",
- "background": "#dcedc8",
- "border": "#689f38"
- },
- "amber": {
- "name": "Amber",
- "background": "#ffe082",
- "border": "#ffa000"
- }
- }
-}
-```
-
-### getDefaultTaskColor
-
-- Purpose: **Get default task color**
-- Parameters: None
-- Result on success: **color_id**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getDefaultTaskColor",
- "id": 1144775215
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1144775215,
- "result": "yellow"
-}
-```
-
-### getColorList
-
-- Purpose: **Get the list of task colors**
-- Parameters: none
-- Result on success: **Dictionary of color_id => color_name**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getColorList",
- "id": 1677051386
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1677051386,
- "result": {
- "yellow": "Yellow",
- "blue": "Blue",
- "green": "Green",
- "purple": "Purple",
- "red": "Red",
- "orange": "Orange",
- "grey": "Grey",
- "brown": "Brown",
- "deep_orange": "Deep Orange",
- "dark_grey": "Dark Grey",
- "pink": "Pink",
- "teal": "Teal",
- "cyan": "Cyan",
- "lime": "Lime",
- "light_green": "Light Green",
- "amber": "Amber"
- }
-}
-```
-
-### createProject
-
-- Purpose: **Create a new project**
-- Parameters:
- - **name** (string, required)
- - **description** (string, optional)
-- Result on success: **project_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createProject",
- "id": 1797076613,
- "params": {
- "name": "PHP client"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1797076613,
- "result": 2
-}
-```
-
-### getProjectById
-
-- Purpose: **Get project information**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **project properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getProjectById",
- "id": 226760253,
- "params": {
- "project_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 226760253,
- "result": {
- "id": "1",
- "name": "API test",
- "is_active": "1",
- "token": "",
- "last_modified": "1436119135",
- "is_public": "0",
- "is_private": "0",
- "is_everybody_allowed": "0",
- "default_swimlane": "Default swimlane",
- "show_default_swimlane": "1",
- "description": "test",
- "identifier": "",
- "url": {
- "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
- "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
- "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
- }
- }
-}
-```
-
-### getProjectByName
-
-- Purpose: **Get project information**
-- Parameters:
- - **name** (string, required)
-- Result on success: **project properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getProjectByName",
- "id": 1620253806,
- "params": {
- "name": "Test"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1620253806,
- "result": {
- "id": "1",
- "name": "Test",
- "is_active": "1",
- "token": "",
- "last_modified": "1436119135",
- "is_public": "0",
- "is_private": "0",
- "is_everybody_allowed": "0",
- "default_swimlane": "Default swimlane",
- "show_default_swimlane": "1",
- "description": "test",
- "identifier": "",
- "url": {
- "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
- "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
- "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
- }
- }
-}
-```
-
-### getAllProjects
-
-- Purpose: **Get all available projects**
-- Parameters:
- - **none**
-- Result on success: **List of projects**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllProjects",
- "id": 2134420212
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 2134420212,
- "result": [
- {
- "id": "1",
- "name": "API test",
- "is_active": "1",
- "token": "",
- "last_modified": "1436119570",
- "is_public": "0",
- "is_private": "0",
- "is_everybody_allowed": "0",
- "default_swimlane": "Default swimlane",
- "show_default_swimlane": "1",
- "description": null,
- "identifier": "",
- "url": {
- "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
- "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
- "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
- }
- }
- ]
-}
-```
-
-### updateProject
-
-- Purpose: **Update a project**
-- Parameters:
- - **id** (integer, required)
- - **name** (string, required)
- - **description** (string, optional)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateProject",
- "id": 1853996288,
- "params": {
- "id": 1,
- "name": "PHP client update"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1853996288,
- "result": true
-}
-```
-
-### removeProject
-
-- Purpose: **Remove a project**
-- Parameters:
- **project_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeProject",
- "id": 46285125,
- "params": {
- "project_id": "2"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 46285125,
- "result": true
-}
-```
-
-### enableProject
-
-- Purpose: **Enable a project**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "enableProject",
- "id": 1775494839,
- "params": [
- "1"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1775494839,
- "result": true
-}
-```
-
-### disableProject
-
-- Purpose: **Disable a project**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "disableProject",
- "id": 1734202312,
- "params": [
- "1"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1734202312,
- "result": true
-}
-```
-
-### enableProjectPublicAccess
-
-- Purpose: **Enable public access for a given project**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "enableProjectPublicAccess",
- "id": 103792571,
- "params": [
- "1"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 103792571,
- "result": true
-}
-```
-
-### disableProjectPublicAccess
-
-- Purpose: **Disable public access for a given project**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "disableProjectPublicAccess",
- "id": 942472945,
- "params": [
- "1"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 942472945,
- "result": true
-}
-```
-
-### getProjectActivity
-
-- Purpose: **Get activity stream for a project**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **List of events**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getProjectActivity",
- "id": 942472945,
- "params": [
- "project_id": 1
- ]
-}
-```
-
-### getProjectActivities
-
-- Purpose: **Get Activityfeed for Project(s)**
-- Parameters:
- - **project_ids** (integer array, required)
-- Result on success: **List of events**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getProjectActivities",
- "id": 942472945,
- "params": [
- "project_ids": [1,2]
- ]
-}
-```
-
-### getMembers
-
-- Purpose: **Get members of a project**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: Key/value pair of user_id and username
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getMembers",
- "id": 1944388643,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1944388643,
- "result": {
- "1": "user1",
- "2": "user2",
- "3": "user3"
- }
-}
-```
-
-### revokeUser
-
-- Purpose: **Revoke user access for a given project**
-- Parameters:
- - **project_id** (integer, required)
- - **user_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "revokeUser",
- "id": 251218350,
- "params": [
- 1,
- 2
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 251218350,
- "result": true
-}
-```
-
-### allowUser
-
-- Purpose: **Grant user access for a given project**
-- Parameters:
- - **project_id** (integer, required)
- - **user_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "allowUser",
- "id": 2111451404,
- "params": [
- 1,
- 2
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 2111451404,
- "result": true
-}
-```
-
-
-### getBoard
-
-- Purpose: **Get all necessary information to display a board**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **board properties**
-- Result on failure: **empty list**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getBoard",
- "id": 827046470,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 827046470,
- "result": [
- {
- "id": 0,
- "name": "Default swimlane",
- "columns": [
- {
- "id": "1",
- "title": "Backlog",
- "position": "1",
- "project_id": "1",
- "task_limit": "0",
- "description": "",
- "tasks": [],
- "nb_tasks": 0,
- "score": 0
- },
- {
- "id": "2",
- "title": "Ready",
- "position": "2",
- "project_id": "1",
- "task_limit": "0",
- "description": "",
- "tasks": [
- {
- "nb_comments":"0",
- "nb_files":"0",
- "nb_subtasks":"0",
- "nb_completed_subtasks":"0",
- "nb_links":"0",
- "id":"2",
- "reference":"",
- "title":"Test",
- "description":"",
- "date_creation":"1430870507",
- "date_modification":"1430870507",
- "date_completed":null,
- "date_due":"0",
- "color_id":"yellow",
- "project_id":"1",
- "column_id":"2",
- "swimlane_id":"0",
- "owner_id":"0",
- "creator_id":"1",
- "position":"1",
- "is_active":"1",
- "score":"0",
- "category_id":"0",
- "date_moved":"1430870507",
- "recurrence_status":"0",
- "recurrence_trigger":"0",
- "recurrence_factor":"0",
- "recurrence_timeframe":"0",
- "recurrence_basedate":"0",
- "recurrence_parent":null,
- "recurrence_child":null,
- "assignee_username":null,
- "assignee_name":null
- }
- ],
- "nb_tasks": 1,
- "score": 0
- },
- {
- "id": "3",
- "title": "Work in progress",
- "position": "3",
- "project_id": "1",
- "task_limit": "0",
- "description": "",
- "tasks": [
- {
- "nb_comments":"0",
- "nb_files":"0",
- "nb_subtasks":"1",
- "nb_completed_subtasks":"0",
- "nb_links":"0",
- "id":"1",
- "reference":"",
- "title":"Task with comment",
- "description":"",
- "date_creation":"1430783188",
- "date_modification":"1430783188",
- "date_completed":null,
- "date_due":"0",
- "color_id":"red",
- "project_id":"1",
- "column_id":"3",
- "swimlane_id":"0",
- "owner_id":"1",
- "creator_id":"0",
- "position":"1",
- "is_active":"1",
- "score":"0",
- "category_id":"0",
- "date_moved":"1430783191",
- "recurrence_status":"0",
- "recurrence_trigger":"0",
- "recurrence_factor":"0",
- "recurrence_timeframe":"0",
- "recurrence_basedate":"0",
- "recurrence_parent":null,
- "recurrence_child":null,
- "assignee_username":"admin",
- "assignee_name":null
- }
- ],
- "nb_tasks": 1,
- "score": 0
- },
- {
- "id": "4",
- "title": "Done",
- "position": "4",
- "project_id": "1",
- "task_limit": "0",
- "description": "",
- "tasks": [],
- "nb_tasks": 0,
- "score": 0
- }
- ],
- "nb_columns": 4,
- "nb_tasks": 2
- }
- ]
-}
-```
-
-### getColumns
-
-- Purpose: **Get all columns information for a given project**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **columns properties**
-- Result on failure: **empty list**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getColumns",
- "id": 887036325,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 887036325,
- "result": [
- {
- "id": "1",
- "title": "Backlog",
- "position": "1",
- "project_id": "1",
- "task_limit": "0"
- },
- {
- "id": "2",
- "title": "Ready",
- "position": "2",
- "project_id": "1",
- "task_limit": "0"
- },
- {
- "id": "3",
- "title": "Work in progress",
- "position": "3",
- "project_id": "1",
- "task_limit": "0"
- }
- ]
-}
-```
-
-### getColumn
-
-- Purpose: **Get a single column**
-- Parameters:
- - **column_id** (integer, required)
-- Result on success: **column properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getColumn",
- "id": 1242049935,
- "params": [
- 2
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1242049935,
- "result": {
- "id": "2",
- "title": "Youpi",
- "position": "2",
- "project_id": "1",
- "task_limit": "5"
- }
-}
-```
-
-### moveColumnUp
-
-- Purpose: **Move up the column position**
-- Parameters:
- - **project_id** (integer, required)
- - **column_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "moveColumnUp",
- "id": 99275573,
- "params": [
- 1,
- 2
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 99275573,
- "result": true
-}
-```
-
-### moveColumnDown
-
-- Purpose: **Move down the column position**
-- Parameters:
- - **project_id** (integer, required)
- - **column_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "moveColumnDown",
- "id": 957090649,
- "params": {
- "project_id": 1,
- "column_id": 2
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 957090649,
- "result": true
-}
-```
-
-### updateColumn
-
-- Purpose: **Update column properties**
-- Parameters:
- - **column_id** (integer, required)
- - **title** (string, required)
- - **task_limit** (integer, optional)
- - **description** (string, optional)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateColumn",
- "id": 480740641,
- "params": [
- 2,
- "Boo",
- 5
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 480740641,
- "result": true
-}
-```
-
-### addColumn
-
-- Purpose: **Add a new column**
-- Parameters:
- - **project_id** (integer, required)
- - **title** (string, required)
- - **task_limit** (integer, optional)
- - **description** (string, optional)
-- Result on success: **column_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "addColumn",
- "id": 638544704,
- "params": [
- 1,
- "Boo"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 638544704,
- "result": 5
-}
-```
-
-### removeColumn
-
-- Purpose: **Remove a column**
-- Parameters:
- - **column_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeColumn",
- "id": 1433237746,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1433237746,
- "result": true
-}
-```
-
-### getDefaultSwimlane
-
-- Purpose: **Get the default swimlane for a project**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getDefaultSwimlane",
- "id": 898774713,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 898774713,
- "result": {
- "id": "1",
- "default_swimlane": "Default swimlane",
- "show_default_swimlane": "1"
- }
-}
-```
-
-### getActiveSwimlanes
-
-- Purpose: **Get the list of enabled swimlanes of a project (include default swimlane if enabled)**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **List of swimlanes**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getActiveSwimlanes",
- "id": 934789422,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 934789422,
- "result": [
- {
- "id": 0,
- "name": "Default swimlane"
- },
- {
- "id": "2",
- "name": "Swimlane A"
- }
- ]
-}
-```
-
-### getAllSwimlanes
-
-- Purpose: **Get the list of all swimlanes of a project (enabled or disabled) and sorted by position**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **List of swimlanes**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllSwimlanes",
- "id": 509791576,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 509791576,
- "result": [
- {
- "id": "1",
- "name": "Another swimlane",
- "position": "1",
- "is_active": "1",
- "project_id": "1"
- },
- {
- "id": "2",
- "name": "Swimlane A",
- "position": "2",
- "is_active": "1",
- "project_id": "1"
- }
- ]
-}
-```
-
-### getSwimlane
-
-- Purpose: **Get the a swimlane by id**
-- Parameters:
- - **swimlane_id** (integer, required)
-- Result on success: **swimlane properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getSwimlane",
- "id": 131071870,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 131071870,
- "result": {
- "id": "1",
- "name": "Swimlane 1",
- "position": "1",
- "is_active": "1",
- "project_id": "1"
- }
-}
-```
-
-### getSwimlaneById
-
-- Purpose: **Get the a swimlane by id**
-- Parameters:
- - **swimlane_id** (integer, required)
-- Result on success: **swimlane properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getSwimlaneById",
- "id": 131071870,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 131071870,
- "result": {
- "id": "1",
- "name": "Swimlane 1",
- "position": "1",
- "is_active": "1",
- "project_id": "1"
- }
-}
-```
-
-### getSwimlaneByName
-
-- Purpose: **Get the a swimlane by name**
-- Parameters:
- - **project_id** (integer, required)
- - **name** (string, required)
-- Result on success: **swimlane properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getSwimlaneByName",
- "id": 824623567,
- "params": [
- 1,
- "Swimlane 1"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 824623567,
- "result": {
- "id": "1",
- "name": "Swimlane 1",
- "position": "1",
- "is_active": "1",
- "project_id": "1"
- }
-}
-```
-
-### moveSwimlaneUp
-
-- Purpose: **Move up the swimlane position**
-- Parameters:
- - **project_id** (integer, required)
- - **swimlane_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "moveSwimlaneUp",
- "id": 99275573,
- "params": [
- 1,
- 2
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 99275573,
- "result": true
-}
-```
-
-### moveSwimlaneDown
-
-- Purpose: **Move down the swimlane position**
-- Parameters:
- - **project_id** (integer, required)
- - **swimlane_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "moveSwimlaneDown",
- "id": 957090649,
- "params": {
- "project_id": 1,
- "swimlane_id": 2
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 957090649,
- "result": true
-}
-```
-
-### updateSwimlane
-
-- Purpose: **Update swimlane properties**
-- Parameters:
- - **swimlane_id** (integer, required)
- - **name** (string, required)
- - **description** (string, optional)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateSwimlane",
- "id": 87102426,
- "params": [
- "1",
- "Another swimlane"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 87102426,
- "result": true
-}
-```
-
-### addSwimlane
-
-- Purpose: **Add a new swimlane**
-- Parameters:
- - **project_id** (integer, required)
- - **name** (string, required)
- - **description** (string, optional)
-- Result on success: **swimlane_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "addSwimlane",
- "id": 849940086,
- "params": [
- 1,
- "Swimlane 1"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 849940086,
- "result": 1
-}
-```
-
-### removeSwimlane
-
-- Purpose: **Remove a swimlane**
-- Parameters:
- - **project_id** (integer, required)
- - **swimlane_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeSwimlane",
- "id": 1433237746,
- "params": [
- 2,
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1433237746,
- "result": true
-}
-```
-
-### disableSwimlane
-
-- Purpose: **Enable a swimlane**
-- Parameters:
- - **project_id** (integer, required)
- - **swimlane_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "disableSwimlane",
- "id": 1433237746,
- "params": [
- 2,
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1433237746,
- "result": true
-}
-```
-
-### enableSwimlane
-
-- Purpose: **Enable a swimlane**
-- Parameters:
- - **project_id** (integer, required)
- - **swimlane_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "enableSwimlane",
- "id": 1433237746,
- "params": [
- 2,
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1433237746,
- "result": true
-}
-```
-
-### getAvailableActions
-
-- Purpose: **Get list of available automatic actions**
-- Parameters: none
-- Result on success: **list of actions**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAvailableActions",
- "id": 1217735483
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1217735483,
- "result": {
- "TaskLogMoveAnotherColumn": "Add a comment logging moving the task between columns",
- "TaskAssignColorUser": "Assign a color to a specific user",
- "TaskAssignColorColumn": "Assign a color when the task is moved to a specific column",
- "TaskAssignCategoryColor": "Assign automatically a category based on a color",
- "TaskAssignColorCategory": "Assign automatically a color based on a category",
- "TaskAssignSpecificUser": "Assign the task to a specific user",
- "TaskAssignCurrentUser": "Assign the task to the person who does the action",
- "TaskUpdateStartDate": "Automatically update the start date",
- "TaskAssignUser": "Change the assignee based on an external username",
- "TaskAssignCategoryLabel": "Change the category based on an external label",
- "TaskClose": "Close a task",
- "CommentCreation": "Create a comment from an external provider",
- "TaskCreation": "Create a task from an external provider",
- "TaskDuplicateAnotherProject": "Duplicate the task to another project",
- "TaskMoveColumnAssigned": "Move the task to another column when assigned to a user",
- "TaskMoveColumnUnAssigned": "Move the task to another column when assignee is cleared",
- "TaskMoveAnotherProject": "Move the task to another project",
- "TaskOpen": "Open a task"
- }
-}
-```
-
-### getAvailableActionEvents
-
-- Purpose: **Get list of available events for actions**
-- Parameters: none
-- Result on success: **list of events**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAvailableActionEvents",
- "id": 2116665643
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 2116665643,
- "result": {
- "bitbucket.webhook.commit": "Bitbucket commit received",
- "task.close": "Closing a task",
- "github.webhook.commit": "Github commit received",
- "github.webhook.issue.assignee": "Github issue assignee change",
- "github.webhook.issue.closed": "Github issue closed",
- "github.webhook.issue.commented": "Github issue comment created",
- "github.webhook.issue.label": "Github issue label change",
- "github.webhook.issue.opened": "Github issue opened",
- "github.webhook.issue.reopened": "Github issue reopened",
- "gitlab.webhook.commit": "Gitlab commit received",
- "gitlab.webhook.issue.closed": "Gitlab issue closed",
- "gitlab.webhook.issue.opened": "Gitlab issue opened",
- "task.move.column": "Move a task to another column",
- "task.open": "Open a closed task",
- "task.assignee_change": "Task assignee change",
- "task.create": "Task creation",
- "task.create_update": "Task creation or modification",
- "task.update": "Task modification"
- }
-}
-```
-
-### getCompatibleActionEvents
-
-- Purpose: **Get list of events compatible with an action**
-- Parameters:
- - **action_name** (string, required)
-- Result on success: **list of events**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getCompatibleActionEvents",
- "id": 899370297,
- "params": [
- "TaskClose"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 899370297,
- "result": {
- "bitbucket.webhook.commit": "Bitbucket commit received",
- "github.webhook.commit": "Github commit received",
- "github.webhook.issue.closed": "Github issue closed",
- "gitlab.webhook.commit": "Gitlab commit received",
- "gitlab.webhook.issue.closed": "Gitlab issue closed",
- "task.move.column": "Move a task to another column"
- }
-}
-```
-
-### getActions
-
-- Purpose: **Get list of actions for a project**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **list of actions properties**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getActions",
- "id": 1433237746,
- "params": [
- "1"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1433237746,
- "result": [
- {
- "id" : "13",
- "project_id" : "2",
- "event_name" : "task.move.column",
- "action_name" : "TaskAssignSpecificUser",
- "params" : {
- "column_id" : "5",
- "user_id" : "1"
- }
- }
- ]
-}
-```
-
-### createAction
-
-- Purpose: **Create an action**
-- Parameters:
- - **project_id** (integer, required)
- - **event_name** (string, required)
- - **action_name** (string, required)
- - **params** (key/value parameters, required)
-- Result on success: **action_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createAction",
- "id": 1433237746,
- "params": {
- "project_id" : "2",
- "event_name" : "task.move.column",
- "action_name" : "TaskAssignSpecificUser",
- "params" : {
- "column_id" : "3",
- "user_id" : "2"
- }
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1433237746,
- "result": 14
-}
-```
-
-### removeAction
-
-- Purpose: **Remove an action**
-- Parameters:
- - **action_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeAction",
- "id": 1510741671,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1510741671,
- "result": true
-}
-```
-
-### createTask
-
-- Purpose: **Create a new task**
-- Parameters:
- - **title** (string, required)
- - **project_id** (integer, required)
- - **color_id** (string, optional)
- - **column_id** (integer, optional)
- - **owner_id** (integer, optional)
- - **creator_id** (integer, optional)
- - **date_due**: ISO8601 format (string, optional)
- - **description** Markdown content (string, optional)
- - **category_id** (integer, optional)
- - **score** (integer, optional)
- - **swimlane_id** (integer, optional)
- - **recurrence_status** (integer, optional)
- - **recurrence_trigger** (integer, optional)
- - **recurrence_factor** (integer, optional)
- - **recurrence_timeframe** (integer, optional)
- - **recurrence_basedate** (integer, optional)
-- Result on success: **task_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createTask",
- "id": 1176509098,
- "params": {
- "owner_id": 1,
- "creator_id": 0,
- "date_due": "",
- "description": "",
- "category_id": 0,
- "score": 0,
- "title": "Test",
- "project_id": 1,
- "color_id": "green",
- "column_id": 2,
- "recurrence_status": 0,
- "recurrence_trigger": 0,
- "recurrence_factor": 0,
- "recurrence_timeframe": 0,
- "recurrence_basedate": 0
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1176509098,
- "result": 3
-}
-```
-
-### getTask
-
-- Purpose: **Get task by the unique id**
-- Parameters:
- - **task_id** (integer, required)
-- Result on success: **task properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getTask",
- "id": 700738119,
- "params": {
- "task_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 700738119,
- "result": {
- "id": "1",
- "title": "Task #1",
- "description": "",
- "date_creation": "1409963206",
- "color_id": "blue",
- "project_id": "1",
- "column_id": "2",
- "owner_id": "1",
- "position": "1",
- "is_active": "1",
- "date_completed": null,
- "score": "0",
- "date_due": "0",
- "category_id": "0",
- "creator_id": "0",
- "date_modification": "1409963206",
- "reference": "",
- "date_started": null,
- "time_spent": "0",
- "time_estimated": "0",
- "swimlane_id": "0",
- "date_moved": "1430875287",
- "recurrence_status": "0",
- "recurrence_trigger": "0",
- "recurrence_factor": "0",
- "recurrence_timeframe": "0",
- "recurrence_basedate": "0",
- "recurrence_parent": null,
- "recurrence_child": null,
- "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=1&project_id=1",
- "color": {
- "name": "Yellow",
- "background": "rgb(245, 247, 196)",
- "border": "rgb(223, 227, 45)"
- }
- }
-}
-```
-
-### getTaskByReference
-
-- Purpose: **Get task by the external reference**
-- Parameters:
- - **project_id** (integer, required)
- - **reference** (string, required)
-- Result on success: **task properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getTaskByReference",
- "id": 1992081213,
- "params": {
- "project_id": 1,
- "reference": "TICKET-1234"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1992081213,
- "result": {
- "id": "5",
- "title": "Task with external ticket number",
- "description": "[Link to my ticket](http:\/\/my-ticketing-system\/1234)",
- "date_creation": "1434227446",
- "color_id": "yellow",
- "project_id": "1",
- "column_id": "1",
- "owner_id": "0",
- "position": "4",
- "is_active": "1",
- "date_completed": null,
- "score": "0",
- "date_due": "0",
- "category_id": "0",
- "creator_id": "0",
- "date_modification": "1434227446",
- "reference": "TICKET-1234",
- "date_started": null,
- "time_spent": "0",
- "time_estimated": "0",
- "swimlane_id": "0",
- "date_moved": "1434227446",
- "recurrence_status": "0",
- "recurrence_trigger": "0",
- "recurrence_factor": "0",
- "recurrence_timeframe": "0",
- "recurrence_basedate": "0",
- "recurrence_parent": null,
- "recurrence_child": null,
- "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=5&project_id=1"
- }
-}
-```
-
-### getAllTasks
-
-- Purpose: **Get all available tasks**
-- Parameters:
- - **project_id** (integer, required)
- - **status_id**: The value 1 for active tasks and 0 for inactive (integer, required)
-- Result on success: **List of tasks**
-- Result on failure: **false**
-
-Request example to fetch all tasks on the board:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllTasks",
- "id": 133280317,
- "params": {
- "project_id": 1,
- "status_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 133280317,
- "result": [
- {
- "id": "1",
- "title": "Task #1",
- "description": "",
- "date_creation": "1409961789",
- "color_id": "blue",
- "project_id": "1",
- "column_id": "2",
- "owner_id": "1",
- "position": "1",
- "is_active": "1",
- "date_completed": null,
- "score": "0",
- "date_due": "0",
- "category_id": "0",
- "creator_id": "0",
- "date_modification": "1409961789",
- "reference": "",
- "date_started": null,
- "time_spent": "0",
- "time_estimated": "0",
- "swimlane_id": "0",
- "date_moved": "1430783191",
- "recurrence_status": "0",
- "recurrence_trigger": "0",
- "recurrence_factor": "0",
- "recurrence_timeframe": "0",
- "recurrence_basedate": "0",
- "recurrence_parent": null,
- "recurrence_child": null,
- "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=1&project_id=1"
- },
- {
- "id": "2",
- "title": "Test",
- "description": "",
- "date_creation": "1409962115",
- "color_id": "green",
- "project_id": "1",
- "column_id": "2",
- "owner_id": "1",
- "position": "2",
- "is_active": "1",
- "date_completed": null,
- "score": "0",
- "date_due": "0",
- "category_id": "0",
- "creator_id": "0",
- "date_modification": "1409962115",
- "reference": "",
- "date_started": null,
- "time_spent": "0",
- "time_estimated": "0",
- "swimlane_id": "0",
- "date_moved": "1430783191",
- "recurrence_status": "0",
- "recurrence_trigger": "0",
- "recurrence_factor": "0",
- "recurrence_timeframe": "0",
- "recurrence_basedate": "0",
- "recurrence_parent": null,
- "recurrence_child": null,
- "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=2&project_id=1"
- },
- ...
- ]
-}
-```
-
-### getOverdueTasks
-
-- Purpose: **Get all overdue tasks**
-- Result on success: **List of tasks**
-- Result on failure: **false**
-
-Request example to fetch all tasks on the board:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getOverdueTasks",
- "id": 133280317
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 133280317,
- "result": [
- {
- "id": "1",
- "title": "Task #1",
- "date_due": "1409961789",
- "project_id": "1",
- "project_name": "Test",
- "assignee_username":"admin",
- "assignee_name": null
- },
- {
- "id": "2",
- "title": "Test",
- "date_due": "1409962115",
- "project_id": "1",
- "project_name": "Test",
- "assignee_username":"admin",
- "assignee_name": null
- },
- ...
- ]
-}
-```
-
-### getOverdueTasksByProject
-
-- Purpose: **Get all overdue tasks for a special project**
-- Result on success: **List of tasks**
-- Result on failure: **false**
-
-Request example to fetch all tasks on the board:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getOverdueTasksByProject",
- "id": 133280317,
- "params": {
- "project_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 133280317,
- "result": [
- {
- "id": "1",
- "title": "Task #1",
- "date_due": "1409961789",
- "project_id": "1",
- "project_name": "Test",
- "assignee_username":"admin",
- "assignee_name": null
- },
- {
- "id": "2",
- "title": "Test",
- "date_due": "1409962115",
- "project_id": "1",
- "project_name": "Test",
- "assignee_username":"admin",
- "assignee_name": null
- },
- ...
- ]
-}
-```
-
-### updateTask
-
-- Purpose: **Update a task**
-- Parameters:
- - **id** (integer, required)
- - **title** (string, optional)
- - **project_id** (integer, optional)
- - **color_id** (string, optional)
- - **owner_id** (integer, optional)
- - **creator_id** (integer, optional)
- - **date_due**: ISO8601 format (string, optional)
- - **description** Markdown content (string, optional)
- - **category_id** (integer, optional)
- - **score** (integer, optional)
- - **recurrence_status** (integer, optional)
- - **recurrence_trigger** (integer, optional)
- - **recurrence_factor** (integer, optional)
- - **recurrence_timeframe** (integer, optional)
- - **recurrence_basedate** (integer, optional)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example to change the task color:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateTask",
- "id": 1406803059,
- "params": {
- "id": 1,
- "color_id": "blue"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1406803059,
- "result": true
-}
-```
-
-### openTask
-
-- Purpose: **Set a task to the status open**
-- Parameters:
- - **task_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "openTask",
- "id": 1888531925,
- "params": {
- "task_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1888531925,
- "result": true
-}
-```
-
-### closeTask
-
-- Purpose: **Set a task to the status close**
-- Parameters:
- - **task_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "closeTask",
- "id": 1654396960,
- "params": {
- "task_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1654396960,
- "result": true
-}
-```
-
-### removeTask
-
-- Purpose: **Remove a task**
-- Parameters:
- - **task_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeTask",
- "id": 1423501287,
- "params": {
- "task_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1423501287,
- "result": true
-}
-```
-
-### moveTaskPosition
-
-- Purpose: **Move a task to another column or another position**
-- Parameters:
- - **project_id** (integer, required)
- - **task_id** (integer, required)
- - **column_id** (integer, required)
- - **position** (integer, required)
- - **swimlane_id** (integer, optional, default=0)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "moveTaskPosition",
- "id": 117211800,
- "params": {
- "project_id": 1,
- "task_id": 1,
- "column_id": 2,
- "position": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 117211800,
- "result": true
-}
-```
-
-### createUser
-
-- Purpose: **Create a new user**
-- Parameters:
- - **username** Must be unique (string, required)
- - **password** Must have at least 6 characters (string, required)
- - **name** (string, optional)
- - **email** (string, optional)
- - **is_admin** Set the value 1 for admins or 0 for regular users (integer, optional)
- - **is_project_admin** Set the value 1 for project admins or 0 for regular users (integer, optional)
-- Result on success: **user_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createUser",
- "id": 1518863034,
- "params": {
- "username": "biloute",
- "password": "123456"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1518863034,
- "result": 22
-}
-```
-
-### createLdapUser
-
-- Purpose: **Create a new user authentified by LDAP**
-- Parameters:
- - **username** (string, optional if email is set)
- - **email** (string, optional if username is set)
- - **is_admin** Set the value 1 for admins or 0 for regular users (integer, optional)
- - **is_project_admin** Set the value 1 for project admins or 0 for regular users (integer, optional)
-- Result on success: **user_id**
-- Result on failure: **false**
-
-The user will only be created if a matching is found on the LDAP server.
-Username or email (or both) must be provided.
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createLdapUser",
- "id": 1518863034,
- "params": {
- "username": "biloute",
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1518863034,
- "result": 22
-}
-```
-
-### getUser
-
-- Purpose: **Get user information**
-- Parameters:
- - **user_id** (integer, required)
-- Result on success: **user properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getUser",
- "id": 1769674781,
- "params": {
- "user_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1769674781,
- "result": {
- "id": "1",
- "username": "biloute",
- "password": "$2y$10$dRs6pPoBu935RpmsrhmbjevJH5MgZ7Kr9QrnVINwwyZ3.MOwqg.0m",
- "is_admin": "0",
- "is_ldap_user": "0",
- "name": "",
- "email": "",
- "google_id": null,
- "github_id": null,
- "notifications_enabled": "0"
- }
-}
-```
-
-### getAllUsers
-
-- Purpose: **Get all available users**
-- Parameters:
- - **none**
-- Result on success: **List of users**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllUsers",
- "id": 1438712131
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1438712131,
- "result": [
- {
- "id": "1",
- "username": "biloute",
- "name": "",
- "email": "",
- "is_admin": "0",
- "is_ldap_user": "0",
- "notifications_enabled": "0",
- "google_id": null,
- "github_id": null
- },
- ...
- ]
-}
-```
-
-### updateUser
-
-- Purpose: **Update a user**
-- Parameters:
- - **id** (integer)
- - **username** (string, optional)
- - **name** (string, optional)
- - **email** (string, optional)
- - **is_admin** (integer, optional)
- - **is_project_admin** Set the value 1 for project admins or 0 for regular users (integer, optional)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateUser",
- "id": 322123657,
- "params": {
- "id": 1,
- "is_admin": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 322123657,
- "result": true
-}
-```
-
-### removeUser
-
-- Purpose: **Remove a user**
-- Parameters:
- - **user_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeUser",
- "id": 2094191872,
- "params": {
- "user_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 2094191872,
- "result": true
-}
-```
-
-
-### createCategory
-
-- Purpose: **Create a new category**
-- Parameters:
-- **project_id** (integer, required)
- - **name** (string, required, must be unique for the given project)
-- Result on success: **category_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createCategory",
- "id": 541909890,
- "params": {
- "name": "Super category",
- "project_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 541909890,
- "result": 4
-}
-```
-
-### getCategory
-
-- Purpose: **Get category information**
-- Parameters:
- - **category_id** (integer, required)
-- Result on success: **category properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getCategory",
- "id": 203539163,
- "params": {
- "category_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
-
- "jsonrpc": "2.0",
- "id": 203539163,
- "result": {
- "id": "1",
- "name": "Super category",
- "project_id": "1"
- }
-}
-```
-
-### getAllCategories
-
-- Purpose: **Get all available categories**
-- Parameters:
- - **project_id** (integer, required)
-- Result on success: **List of categories**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllCategories",
- "id": 1261777968,
- "params": {
- "project_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1261777968,
- "result": [
- {
- "id": "1",
- "name": "Super category",
- "project_id": "1"
- }
- ]
-}
-```
-
-### updateCategory
-
-- Purpose: **Update a category**
-- Parameters:
- - **id** (integer, required)
- - **name** (string, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateCategory",
- "id": 570195391,
- "params": {
- "id": 1,
- "name": "Renamed category"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 570195391,
- "result": true
-}
-```
-
-### removeCategory
-
-- Purpose: **Remove a category**
-- Parameters:
- - **category_id** (integer)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeCategory",
- "id": 88225706,
- "params": {
- "category_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 88225706,
- "result": true
-}
-```
-
-
-### createComment
-
-- Purpose: **Create a new comment**
-- Parameters:
- - **task_id** (integer, required)
- - **user_id** (integer, required)
- - **content** Markdown content (string, required)
-- Result on success: **comment_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createComment",
- "id": 1580417921,
- "params": {
- "task_id": 1,
- "user_id": 1,
- "content": "Comment #1"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1580417921,
- "result": 11
-}
-```
-
-### getComment
-
-- Purpose: **Get comment information**
-- Parameters:
- - **comment_id** (integer, required)
-- Result on success: **comment properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getComment",
- "id": 867839500,
- "params": {
- "comment_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 867839500,
- "result": {
- "id": "1",
- "task_id": "1",
- "user_id": "1",
- "date_creation": "1410881970",
- "comment": "Comment #1",
- "username": "admin",
- "name": null
- }
-}
-```
-
-### getAllComments
-
-- Purpose: **Get all available comments**
-- Parameters:
- - **task_id** (integer, required)
-- Result on success: **List of comments**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllComments",
- "id": 148484683,
- "params": {
- "task_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 148484683,
- "result": [
- {
- "id": "1",
- "date_creation": "1410882272",
- "task_id": "1",
- "user_id": "1",
- "comment": "Comment #1",
- "username": "admin",
- "name": null
- },
- ...
- ]
-}
-```
-
-### updateComment
-
-- Purpose: **Update a comment**
-- Parameters:
- - **id** (integer, required)
- - **content** Markdown content (string, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateComment",
- "id": 496470023,
- "params": {
- "id": 1,
- "content": "Comment #1 updated"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1493368950,
- "result": true
-}
-```
-
-### removeComment
-
-- Purpose: **Remove a comment**
-- Parameters:
- - **comment_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeComment",
- "id": 328836871,
- "params": {
- "comment_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 328836871,
- "result": true
-}
-```
-
-### createSubtask
-
-- Purpose: **Create a new subtask**
-- Parameters:
- - **task_id** (integer, required)
- - **title** (integer, required)
- - **user_id** (int, optional)
- - **time_estimated** (int, optional)
- - **time_spent** (int, optional)
- - **status** (int, optional)
-- Result on success: **subtask_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createSubtask",
- "id": 2041554661,
- "params": {
- "task_id": 1,
- "title": "Subtask #1"
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 2041554661,
- "result": 45
-}
-```
-
-### getSubtask
-
-- Purpose: **Get subtask information**
-- Parameters:
- - **subtask_id** (integer)
-- Result on success: **subtask properties**
-- Result on failure: **null**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getSubtask",
- "id": 133184525,
- "params": {
- "subtask_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 133184525,
- "result": {
- "id": "1",
- "title": "Subtask #1",
- "status": "0",
- "time_estimated": "0",
- "time_spent": "0",
- "task_id": "1",
- "user_id": "0"
- }
-}
-```
-
-### getAllSubtasks
-
-- Purpose: **Get all available subtasks**
-- Parameters:
- - **task_id** (integer, required)
-- Result on success: **List of subtasks**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllSubtasks",
- "id": 2087700490,
- "params": {
- "task_id": 1
- }
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 2087700490,
- "result": [
- {
- "id": "1",
- "title": "Subtask #1",
- "status": "0",
- "time_estimated": "0",
- "time_spent": "0",
- "task_id": "1",
- "user_id": "0",
- "username": null,
- "name": null,
- "status_name": "Todo"
- },
- ...
- ]
-}
-```
-
-### updateSubtask
-
-- Purpose: **Update a subtask**
-- Parameters:
- - **id** (integer, required)
- - **task_id** (integer, required)
- - **title** (integer, optional)
- - **user_id** (integer, optional)
- - **time_estimated** (integer, optional)
- - **time_spent** (integer, optional)
- - **status** (integer, optional)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateSubtask",
- "id": 191749979,
- "params": {
- "id": 1,
- "task_id": 1,
- "status": 1,
- "time_spent": 5,
- "user_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 191749979,
- "result": true
-}
-```
-
-### removeSubtask
-
-- Purpose: **Remove a subtask**
-- Parameters:
- - **subtask_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeSubtask",
- "id": 1382487306,
- "params": {
- "subtask_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1382487306,
- "result": true
-}
-```
-
-### getAllLinks
-
-- Purpose: **Get the list of possible relations between tasks**
-- Parameters: none
-- Result on success: **List of links**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllLinks",
- "id": 113057196
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 113057196,
- "result": [
- {
- "id": "1",
- "label": "relates to",
- "opposite_id": "0"
- },
- {
- "id": "2",
- "label": "blocks",
- "opposite_id": "3"
- },
- {
- "id": "3",
- "label": "is blocked by",
- "opposite_id": "2"
- },
- {
- "id": "4",
- "label": "duplicates",
- "opposite_id": "5"
- },
- {
- "id": "5",
- "label": "is duplicated by",
- "opposite_id": "4"
- },
- {
- "id": "6",
- "label": "is a child of",
- "opposite_id": "7"
- },
- {
- "id": "7",
- "label": "is a parent of",
- "opposite_id": "6"
- },
- {
- "id": "8",
- "label": "targets milestone",
- "opposite_id": "9"
- },
- {
- "id": "9",
- "label": "is a milestone of",
- "opposite_id": "8"
- },
- {
- "id": "10",
- "label": "fixes",
- "opposite_id": "11"
- },
- {
- "id": "11",
- "label": "is fixed by",
- "opposite_id": "10"
- }
- ]
-}
-```
-
-### getOppositeLinkId
-
-- Purpose: **Get the opposite link id of a task link**
-- Parameters:
- - **link_id** (integer, required)
-- Result on success: **link_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getOppositeLinkId",
- "id": 407062448,
- "params": [
- 2
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 407062448,
- "result": "3"
-}
-```
-
-### getLinkByLabel
-
-- Purpose: **Get a link by label**
-- Parameters:
- - **label** (integer, required)
-- Result on success: **link properties**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getLinkByLabel",
- "id": 1796123316,
- "params": [
- "blocks"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1796123316,
- "result": {
- "id": "2",
- "label": "blocks",
- "opposite_id": "3"
- }
-}
-```
-
-### getLinkById
-
-- Purpose: **Get a link by id**
-- Parameters:
- - **link_id** (integer, required)
-- Result on success: **link properties**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getLinkById",
- "id": 1190238402,
- "params": [
- 4
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1190238402,
- "result": {
- "id": "4",
- "label": "duplicates",
- "opposite_id": "5"
- }
-}
-```
-
-### createLink
-
-- Purpose: **Create a new task relation**
-- Parameters:
- - **label** (integer, required)
- - **opposite_label** (integer, optional)
-- Result on success: **link_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createLink",
- "id": 1040237496,
- "params": [
- "foo",
- "bar"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1040237496,
- "result": 13
-}
-```
-
-### updateLink
-
-- Purpose: **Update a link**
-- Parameters:
- - **link_id** (integer, required)
- - **opposite_link_id** (integer, required)
- - **label** (string, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateLink",
- "id": 2110446926,
- "params": [
- "14",
- "12",
- "boo"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 2110446926,
- "result": true
-}
-```
-
-### removeLink
-
-- Purpose: **Remove a link**
-- Parameters:
- - **link_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeLink",
- "id": 2136522739,
- "params": [
- "14"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 2136522739,
- "result": true
-}
-```
-
-### createTaskLink
-
-- Purpose: **Create a link between two tasks**
-- Parameters:
- - **task_id** (integer, required)
- - **opposite_task_id** (integer, required)
- - **link_id** (integer, required)
-- Result on success: **task_link_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createTaskLink",
- "id": 509742912,
- "params": [
- 2,
- 3,
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 509742912,
- "result": 1
-}
-```
-
-### updateTaskLink
-
-- Purpose: **Update task link**
-- Parameters:
- - **task_link_id** (integer, required)
- - **task_id** (integer, required)
- - **opposite_task_id** (integer, required)
- - **link_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "updateTaskLink",
- "id": 669037109,
- "params": [
- 1,
- 2,
- 4,
- 2
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 669037109,
- "result": true
-}
-```
-
-### getTaskLinkById
-
-- Purpose: **Get a task link**
-- Parameters:
- - **task_link_id** (integer, required)
-- Result on success: **task link properties**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getTaskLinkById",
- "id": 809885202,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 809885202,
- "result": {
- "id": "1",
- "link_id": "1",
- "task_id": "2",
- "opposite_task_id": "3"
- }
-}
-```
-
-### getAllTaskLinks
-
-- Purpose: **Get all links related to a task**
-- Parameters:
- - **task_id** (integer, required)
-- Result on success: **list of task link**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllTaskLinks",
- "id": 810848359,
- "params": [
- 2
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 810848359,
- "result": [
- {
- "id": "1",
- "task_id": "3",
- "label": "relates to",
- "title": "B",
- "is_active": "1",
- "project_id": "1",
- "task_time_spent": "0",
- "task_time_estimated": "0",
- "task_assignee_id": "0",
- "task_assignee_username": null,
- "task_assignee_name": null,
- "column_title": "Backlog"
- }
- ]
-}
-```
-
-### removeTaskLink
-
-- Purpose: **Remove a link between two tasks**
-- Parameters:
- - **task_link_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeTaskLink",
- "id": 473028226,
- "params": [
- 1
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 473028226,
- "result": true
-}
-```
-
-### createFile
-
-- Purpose: **Create and upload a new task attachment**
-- Parameters:
- - **project_id** (integer, required)
- - **task_id** (integer, required)
- - **filename** (integer, required)
- - **blob** File content encoded in base64 (string, required)
-- Result on success: **file_id**
-- Result on failure: **false**
-- Note: **The maximum file size depends of your PHP configuration, this method should not be used to upload large files**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createFile",
- "id": 94500810,
- "params": [
- 1,
- 1,
- "My file",
- "cGxhaW4gdGV4dCBmaWxl"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 94500810,
- "result": 1
-}
-```
-
-### getAllFiles
-
-- Purpose: **Get all files attached to task**
-- Parameters:
- - **task_id** (integer, required)
-- Result on success: **list of files**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getAllFiles",
- "id": 1880662820,
- "params": {
- "task_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1880662820,
- "result": [
- {
- "id": "1",
- "name": "My file",
- "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596",
- "is_image": "0",
- "task_id": "1",
- "date": "1432509941",
- "user_id": "0",
- "size": "15",
- "username": null,
- "user_name": null
- }
- ]
-}
-```
-
-### getFile
-
-- Purpose: **Get file information**
-- Parameters:
- - **file_id** (integer, required)
-- Result on success: **file properties**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getFile",
- "id": 318676852,
- "params": [
- "1"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 318676852,
- "result": {
- "id": "1",
- "name": "My file",
- "path": "1\/1\/0db4d0a897a4c852f6e12f0239d4805f7b4ab596",
- "is_image": "0",
- "task_id": "1",
- "date": "1432509941",
- "user_id": "0",
- "size": "15"
- }
-}
-```
-
-### downloadFile
-
-- Purpose: **Download file contents (encoded in base64)**
-- Parameters:
- - **file_id** (integer, required)
-- Result on success: **base64 encoded string**
-- Result on failure: **empty string**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "downloadFile",
- "id": 235943344,
- "params": [
- "1"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 235943344,
- "result": "cGxhaW4gdGV4dCBmaWxl"
-}
-```
-
-### removeFile
-
-- Purpose: **Remove file**
-- Parameters:
- - **file_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeFile",
- "id": 447036524,
- "params": [
- "1"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 447036524,
- "result": true
-}
-```
-
-### removeAllFiles
-
-- Purpose: **Remove all files associated to a task**
-- Parameters:
- - **task_id** (integer, required)
-- Result on success: **true**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "removeAllFiles",
- "id": 593312993,
- "params": {
- "task_id": 1
- }
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 593312993,
- "result": true
-}
-```
-
-### getMe
-
-- Purpose: **Get logged user session**
-- Parameters: None
-- Result on success: **user session data**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getMe",
- "id": 1718627783
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1718627783,
- "result": {
- "id": 2,
- "username": "user",
- "is_admin": false,
- "is_ldap_user": false,
- "name": "",
- "email": "",
- "google_id": null,
- "github_id": null,
- "notifications_enabled": "0",
- "timezone": null,
- "language": null,
- "disable_login_form": "0",
- "twofactor_activated": false,
- "twofactor_secret": null,
- "token": "",
- "notifications_filter": "4"
- }
-}
-```
-
-### getMyDashboard
-
-- Purpose: **Get the dashboard of the logged user without pagination**
-- Parameters: None
-- Result on success: **Dashboard information**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getMyDashboard",
- "id": 447898718
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1563664593,
- "result": {
- "projects": [
- {
- "id": "2",
- "name": "my project",
- "is_active": "1",
- "token": "",
- "last_modified": "1438205337",
- "is_public": "0",
- "is_private": "1",
- "is_everybody_allowed": "0",
- "default_swimlane": "Default swimlane",
- "show_default_swimlane": "1",
- "description": null,
- "identifier": "",
- "columns": [
- {
- "id": "5",
- "title": "Backlog",
- "position": "1",
- "project_id": "2",
- "task_limit": "0",
- "description": "",
- "nb_tasks": 0
- },
- {
- "id": "6",
- "title": "Ready",
- "position": "2",
- "project_id": "2",
- "task_limit": "0",
- "description": "",
- "nb_tasks": 0
- },
- {
- "id": "7",
- "title": "Work in progress",
- "position": "3",
- "project_id": "2",
- "task_limit": "0",
- "description": "",
- "nb_tasks": 0
- },
- {
- "id": "8",
- "title": "Done",
- "position": "4",
- "project_id": "2",
- "task_limit": "0",
- "description": "",
- "nb_tasks": 0
- }
- ],
- "url": {
- "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=2",
- "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=2",
- "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=2"
- }
- }
- ],
- "tasks": [
- {
- "id": "1",
- "title": "new title",
- "date_due": "0",
- "date_creation": "1438205336",
- "project_id": "2",
- "color_id": "yellow",
- "time_spent": "0",
- "time_estimated": "0",
- "project_name": "my project",
- "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=1&project_id=2"
- }
- ],
- "subtasks": []
- }
-}
-```
-
-### getMyActivityStream
-
-- Purpose: **Get the last 100 events for the logged user**
-- Parameters: None
-- Result on success: **List of events**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getMyActivityStream",
- "id": 1132562181
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1132562181,
- "result": [
- {
- "id": "1",
- "date_creation": "1438205054",
- "event_name": "task.create",
- "creator_id": "2",
- "project_id": "2",
- "task_id": "1",
- "author_username": "user",
- "author_name": "",
- "email": "",
- "task": {
- "id": "1",
- "reference": "",
- "title": "my user title",
- "description": "",
- "date_creation": "1438205054",
- "date_completed": null,
- "date_modification": "1438205054",
- "date_due": "0",
- "date_started": null,
- "time_estimated": "0",
- "time_spent": "0",
- "color_id": "yellow",
- "project_id": "2",
- "column_id": "5",
- "owner_id": "0",
- "creator_id": "2",
- "position": "1",
- "is_active": "1",
- "score": "0",
- "category_id": "0",
- "swimlane_id": "0",
- "date_moved": "1438205054",
- "recurrence_status": "0",
- "recurrence_trigger": "0",
- "recurrence_factor": "0",
- "recurrence_timeframe": "0",
- "recurrence_basedate": "0",
- "recurrence_parent": null,
- "recurrence_child": null,
- "category_name": null,
- "swimlane_name": null,
- "project_name": "my project",
- "default_swimlane": "Default swimlane",
- "column_title": "Backlog",
- "assignee_username": null,
- "assignee_name": null,
- "creator_username": "user",
- "creator_name": ""
- },
- "changes": [],
- "author": "user",
- "event_title": "user created the task #1",
- "event_content": "\n<p class=\"activity-title\">\n user created the task <a href=\"\/?controller=task&amp;action=show&amp;task_id=1&amp;project_id=2\" class=\"\" title=\"\" >#1<\/a><\/p>\n<p class=\"activity-description\">\n <em>my user title<\/em>\n<\/p>"
- }
- ]
-}
-```
-
-### createMyPrivateProject
-
-- Purpose: **Create a private project for the logged user**
-- Parameters:
- - **name** (string, required)
- - **description** (string, optional)
-- Result on success: **project_id**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "createMyPrivateProject",
- "id": 1271580569,
- "params": [
- "my project"
- ]
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 1271580569,
- "result": 2
-}
-```
-
-### getMyProjectsList
-
-- Purpose: **Get projects of the connected user**
-- Parameters: None
-- Result on success: **dictionary of project_id => project_name**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getMyProjectsList",
- "id": 987834805
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 987834805,
- "result": {
- "2": "my project"
- }
-}
-```
-### getMyOverdueTasks
-
-- Purpose: **Get my overdue tasks**
-- Result on success: **List of tasks**
-- Result on failure: **false**
-
-Request example to fetch all tasks on the board:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getMyOverdueTasks",
- "id": 133280317
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 133280317,
- "result": [
- {
- "id": "1",
- "title": "Task #1",
- "date_due": "1409961789",
- "project_id": "1",
- "project_name": "Test",
- "assignee_username":"admin",
- "assignee_name": null
- },
- {
- "id": "2",
- "title": "Test",
- "date_due": "1409962115",
- "project_id": "1",
- "project_name": "Test",
- "assignee_username":"admin",
- "assignee_name": null
- },
- ...
- ]
-}
-```
-
-### getMyProjects
-
-- Purpose: **Get projects of connected user with full details**
-- Parameters:
- - **none**
-- Result on success: **List of projects with details**
-- Result on failure: **false**
-
-Request example:
-
-```json
-{
- "jsonrpc": "2.0",
- "method": "getmyProjects",
- "id": 2134420212
-}
-```
-
-Response example:
-
-```json
-{
- "jsonrpc": "2.0",
- "id": 2134420212,
- "result": [
- {
- "id": "1",
- "name": "API test",
- "is_active": "1",
- "token": "",
- "last_modified": "1436119570",
- "is_public": "0",
- "is_private": "0",
- "is_everybody_allowed": "0",
- "default_swimlane": "Default swimlane",
- "show_default_swimlane": "1",
- "description": null,
- "identifier": "",
- "url": {
- "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
- "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
- "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
- }
- }
- ]
-}
-```
+Usage
+-----
+
+- [Authentication](api-authentication.markdown)
+- [Examples](api-examples.markdown)
+- [Application Procedures](api-application-procedures.markdown)
+- [Project Procedures](api-project-procedures.markdown)
+- [Board Procedures](api-board-procedures.markdown)
+- [Swimlane Procedures](api-swimlane-procedures.markdown)
+- [Category Procedures](api-category-procedures.markdown)
+- [Automatic Action Procedures](api-action-procedures.markdown)
+- [Task Procedures](api-task-procedures.markdown)
+- [Subtask Procedures](api-subtask-procedures.markdown)
+- [File Procedures](api-file-procedures.markdown)
+- [Link Procedures](api-link-procedures.markdown)
+- [Comment Procedures](api-comment-procedures.markdown)
+- [User Procedures](api-user-procedures.markdown)
+- [User API Access Procedures](api-me-procedures.markdown)
diff --git a/doc/api-link-procedures.markdown b/doc/api-link-procedures.markdown
new file mode 100644
index 00000000..1eb5fd6f
--- /dev/null
+++ b/doc/api-link-procedures.markdown
@@ -0,0 +1,470 @@
+API Link Procedures
+===================
+
+### getAllLinks
+
+- Purpose: **Get the list of possible relations between tasks**
+- Parameters: none
+- Result on success: **List of links**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllLinks",
+ "id": 113057196
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 113057196,
+ "result": [
+ {
+ "id": "1",
+ "label": "relates to",
+ "opposite_id": "0"
+ },
+ {
+ "id": "2",
+ "label": "blocks",
+ "opposite_id": "3"
+ },
+ {
+ "id": "3",
+ "label": "is blocked by",
+ "opposite_id": "2"
+ },
+ {
+ "id": "4",
+ "label": "duplicates",
+ "opposite_id": "5"
+ },
+ {
+ "id": "5",
+ "label": "is duplicated by",
+ "opposite_id": "4"
+ },
+ {
+ "id": "6",
+ "label": "is a child of",
+ "opposite_id": "7"
+ },
+ {
+ "id": "7",
+ "label": "is a parent of",
+ "opposite_id": "6"
+ },
+ {
+ "id": "8",
+ "label": "targets milestone",
+ "opposite_id": "9"
+ },
+ {
+ "id": "9",
+ "label": "is a milestone of",
+ "opposite_id": "8"
+ },
+ {
+ "id": "10",
+ "label": "fixes",
+ "opposite_id": "11"
+ },
+ {
+ "id": "11",
+ "label": "is fixed by",
+ "opposite_id": "10"
+ }
+ ]
+}
+```
+
+### getOppositeLinkId
+
+- Purpose: **Get the opposite link id of a task link**
+- Parameters:
+ - **link_id** (integer, required)
+- Result on success: **link_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getOppositeLinkId",
+ "id": 407062448,
+ "params": [
+ 2
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 407062448,
+ "result": "3"
+}
+```
+
+### getLinkByLabel
+
+- Purpose: **Get a link by label**
+- Parameters:
+ - **label** (integer, required)
+- Result on success: **link properties**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getLinkByLabel",
+ "id": 1796123316,
+ "params": [
+ "blocks"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1796123316,
+ "result": {
+ "id": "2",
+ "label": "blocks",
+ "opposite_id": "3"
+ }
+}
+```
+
+### getLinkById
+
+- Purpose: **Get a link by id**
+- Parameters:
+ - **link_id** (integer, required)
+- Result on success: **link properties**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getLinkById",
+ "id": 1190238402,
+ "params": [
+ 4
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1190238402,
+ "result": {
+ "id": "4",
+ "label": "duplicates",
+ "opposite_id": "5"
+ }
+}
+```
+
+### createLink
+
+- Purpose: **Create a new task relation**
+- Parameters:
+ - **label** (integer, required)
+ - **opposite_label** (integer, optional)
+- Result on success: **link_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createLink",
+ "id": 1040237496,
+ "params": [
+ "foo",
+ "bar"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1040237496,
+ "result": 13
+}
+```
+
+### updateLink
+
+- Purpose: **Update a link**
+- Parameters:
+ - **link_id** (integer, required)
+ - **opposite_link_id** (integer, required)
+ - **label** (string, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateLink",
+ "id": 2110446926,
+ "params": [
+ "14",
+ "12",
+ "boo"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2110446926,
+ "result": true
+}
+```
+
+### removeLink
+
+- Purpose: **Remove a link**
+- Parameters:
+ - **link_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeLink",
+ "id": 2136522739,
+ "params": [
+ "14"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2136522739,
+ "result": true
+}
+```
+
+### createTaskLink
+
+- Purpose: **Create a link between two tasks**
+- Parameters:
+ - **task_id** (integer, required)
+ - **opposite_task_id** (integer, required)
+ - **link_id** (integer, required)
+- Result on success: **task_link_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createTaskLink",
+ "id": 509742912,
+ "params": [
+ 2,
+ 3,
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 509742912,
+ "result": 1
+}
+```
+
+### updateTaskLink
+
+- Purpose: **Update task link**
+- Parameters:
+ - **task_link_id** (integer, required)
+ - **task_id** (integer, required)
+ - **opposite_task_id** (integer, required)
+ - **link_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateTaskLink",
+ "id": 669037109,
+ "params": [
+ 1,
+ 2,
+ 4,
+ 2
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 669037109,
+ "result": true
+}
+```
+
+### getTaskLinkById
+
+- Purpose: **Get a task link**
+- Parameters:
+ - **task_link_id** (integer, required)
+- Result on success: **task link properties**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getTaskLinkById",
+ "id": 809885202,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 809885202,
+ "result": {
+ "id": "1",
+ "link_id": "1",
+ "task_id": "2",
+ "opposite_task_id": "3"
+ }
+}
+```
+
+### getAllTaskLinks
+
+- Purpose: **Get all links related to a task**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **list of task link**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllTaskLinks",
+ "id": 810848359,
+ "params": [
+ 2
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 810848359,
+ "result": [
+ {
+ "id": "1",
+ "task_id": "3",
+ "label": "relates to",
+ "title": "B",
+ "is_active": "1",
+ "project_id": "1",
+ "task_time_spent": "0",
+ "task_time_estimated": "0",
+ "task_assignee_id": "0",
+ "task_assignee_username": null,
+ "task_assignee_name": null,
+ "column_title": "Backlog"
+ }
+ ]
+}
+```
+
+### removeTaskLink
+
+- Purpose: **Remove a link between two tasks**
+- Parameters:
+ - **task_link_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeTaskLink",
+ "id": 473028226,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 473028226,
+ "result": true
+}
+```
diff --git a/doc/api-me-procedures.markdown b/doc/api-me-procedures.markdown
new file mode 100644
index 00000000..43afb0ef
--- /dev/null
+++ b/doc/api-me-procedures.markdown
@@ -0,0 +1,385 @@
+User API Specific Procedures
+============================
+
+### getMe
+
+- Purpose: **Get logged user session**
+- Parameters: None
+- Result on success: **user session data**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getMe",
+ "id": 1718627783
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1718627783,
+ "result": {
+ "id": 2,
+ "username": "user",
+ "role": "app-user",
+ "is_ldap_user": false,
+ "name": "",
+ "email": "",
+ "google_id": null,
+ "github_id": null,
+ "notifications_enabled": "0",
+ "timezone": null,
+ "language": null,
+ "disable_login_form": "0",
+ "twofactor_activated": false,
+ "twofactor_secret": null,
+ "token": "",
+ "notifications_filter": "4"
+ }
+}
+```
+
+### getMyDashboard
+
+- Purpose: **Get the dashboard of the logged user without pagination**
+- Parameters: None
+- Result on success: **Dashboard information**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getMyDashboard",
+ "id": 447898718
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1563664593,
+ "result": {
+ "projects": [
+ {
+ "id": "2",
+ "name": "my project",
+ "is_active": "1",
+ "token": "",
+ "last_modified": "1438205337",
+ "is_public": "0",
+ "is_private": "1",
+ "is_everybody_allowed": "0",
+ "default_swimlane": "Default swimlane",
+ "show_default_swimlane": "1",
+ "description": null,
+ "identifier": "",
+ "columns": [
+ {
+ "id": "5",
+ "title": "Backlog",
+ "position": "1",
+ "project_id": "2",
+ "task_limit": "0",
+ "description": "",
+ "nb_tasks": 0
+ },
+ {
+ "id": "6",
+ "title": "Ready",
+ "position": "2",
+ "project_id": "2",
+ "task_limit": "0",
+ "description": "",
+ "nb_tasks": 0
+ },
+ {
+ "id": "7",
+ "title": "Work in progress",
+ "position": "3",
+ "project_id": "2",
+ "task_limit": "0",
+ "description": "",
+ "nb_tasks": 0
+ },
+ {
+ "id": "8",
+ "title": "Done",
+ "position": "4",
+ "project_id": "2",
+ "task_limit": "0",
+ "description": "",
+ "nb_tasks": 0
+ }
+ ],
+ "url": {
+ "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=2",
+ "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=2",
+ "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=2"
+ }
+ }
+ ],
+ "tasks": [
+ {
+ "id": "1",
+ "title": "new title",
+ "date_due": "0",
+ "date_creation": "1438205336",
+ "project_id": "2",
+ "color_id": "yellow",
+ "time_spent": "0",
+ "time_estimated": "0",
+ "project_name": "my project",
+ "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=1&project_id=2"
+ }
+ ],
+ "subtasks": []
+ }
+}
+```
+
+### getMyActivityStream
+
+- Purpose: **Get the last 100 events for the logged user**
+- Parameters: None
+- Result on success: **List of events**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getMyActivityStream",
+ "id": 1132562181
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1132562181,
+ "result": [
+ {
+ "id": "1",
+ "date_creation": "1438205054",
+ "event_name": "task.create",
+ "creator_id": "2",
+ "project_id": "2",
+ "task_id": "1",
+ "author_username": "user",
+ "author_name": "",
+ "email": "",
+ "task": {
+ "id": "1",
+ "reference": "",
+ "title": "my user title",
+ "description": "",
+ "date_creation": "1438205054",
+ "date_completed": null,
+ "date_modification": "1438205054",
+ "date_due": "0",
+ "date_started": null,
+ "time_estimated": "0",
+ "time_spent": "0",
+ "color_id": "yellow",
+ "project_id": "2",
+ "column_id": "5",
+ "owner_id": "0",
+ "creator_id": "2",
+ "position": "1",
+ "is_active": "1",
+ "score": "0",
+ "category_id": "0",
+ "swimlane_id": "0",
+ "date_moved": "1438205054",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0",
+ "recurrence_factor": "0",
+ "recurrence_timeframe": "0",
+ "recurrence_basedate": "0",
+ "recurrence_parent": null,
+ "recurrence_child": null,
+ "category_name": null,
+ "swimlane_name": null,
+ "project_name": "my project",
+ "default_swimlane": "Default swimlane",
+ "column_title": "Backlog",
+ "assignee_username": null,
+ "assignee_name": null,
+ "creator_username": "user",
+ "creator_name": ""
+ },
+ "changes": [],
+ "author": "user",
+ "event_title": "user created the task #1",
+ "event_content": "\n<p class=\"activity-title\">\n user created the task <a href=\"\/?controller=task&amp;action=show&amp;task_id=1&amp;project_id=2\" class=\"\" title=\"\" >#1<\/a><\/p>\n<p class=\"activity-description\">\n <em>my user title<\/em>\n<\/p>"
+ }
+ ]
+}
+```
+
+### createMyPrivateProject
+
+- Purpose: **Create a private project for the logged user**
+- Parameters:
+ - **name** (string, required)
+ - **description** (string, optional)
+- Result on success: **project_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createMyPrivateProject",
+ "id": 1271580569,
+ "params": [
+ "my project"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1271580569,
+ "result": 2
+}
+```
+
+### getMyProjectsList
+
+- Purpose: **Get projects of the connected user**
+- Parameters: None
+- Result on success: **dictionary of project_id => project_name**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getMyProjectsList",
+ "id": 987834805
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 987834805,
+ "result": {
+ "2": "my project"
+ }
+}
+```
+### getMyOverdueTasks
+
+- Purpose: **Get my overdue tasks**
+- Result on success: **List of tasks**
+- Result on failure: **false**
+
+Request example to fetch all tasks on the board:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getMyOverdueTasks",
+ "id": 133280317
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 133280317,
+ "result": [
+ {
+ "id": "1",
+ "title": "Task #1",
+ "date_due": "1409961789",
+ "project_id": "1",
+ "project_name": "Test",
+ "assignee_username":"admin",
+ "assignee_name": null
+ },
+ {
+ "id": "2",
+ "title": "Test",
+ "date_due": "1409962115",
+ "project_id": "1",
+ "project_name": "Test",
+ "assignee_username":"admin",
+ "assignee_name": null
+ },
+ ...
+ ]
+}
+```
+
+### getMyProjects
+
+- Purpose: **Get projects of connected user with full details**
+- Parameters:
+ - **none**
+- Result on success: **List of projects with details**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getmyProjects",
+ "id": 2134420212
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2134420212,
+ "result": [
+ {
+ "id": "1",
+ "name": "API test",
+ "is_active": "1",
+ "token": "",
+ "last_modified": "1436119570",
+ "is_public": "0",
+ "is_private": "0",
+ "is_everybody_allowed": "0",
+ "default_swimlane": "Default swimlane",
+ "show_default_swimlane": "1",
+ "description": null,
+ "identifier": "",
+ "url": {
+ "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
+ "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
+ "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
+ }
+ }
+ ]
+}
+```
diff --git a/doc/api-project-procedures.markdown b/doc/api-project-procedures.markdown
new file mode 100644
index 00000000..acb4297e
--- /dev/null
+++ b/doc/api-project-procedures.markdown
@@ -0,0 +1,512 @@
+API Project Procedures
+======================
+
+### createProject
+
+- Purpose: **Create a new project**
+- Parameters:
+ - **name** (string, required)
+ - **description** (string, optional)
+- Result on success: **project_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createProject",
+ "id": 1797076613,
+ "params": {
+ "name": "PHP client"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1797076613,
+ "result": 2
+}
+```
+
+### getProjectById
+
+- Purpose: **Get project information**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **project properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getProjectById",
+ "id": 226760253,
+ "params": {
+ "project_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 226760253,
+ "result": {
+ "id": "1",
+ "name": "API test",
+ "is_active": "1",
+ "token": "",
+ "last_modified": "1436119135",
+ "is_public": "0",
+ "is_private": "0",
+ "is_everybody_allowed": "0",
+ "default_swimlane": "Default swimlane",
+ "show_default_swimlane": "1",
+ "description": "test",
+ "identifier": "",
+ "url": {
+ "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
+ "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
+ "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
+ }
+ }
+}
+```
+
+### getProjectByName
+
+- Purpose: **Get project information**
+- Parameters:
+ - **name** (string, required)
+- Result on success: **project properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getProjectByName",
+ "id": 1620253806,
+ "params": {
+ "name": "Test"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1620253806,
+ "result": {
+ "id": "1",
+ "name": "Test",
+ "is_active": "1",
+ "token": "",
+ "last_modified": "1436119135",
+ "is_public": "0",
+ "is_private": "0",
+ "is_everybody_allowed": "0",
+ "default_swimlane": "Default swimlane",
+ "show_default_swimlane": "1",
+ "description": "test",
+ "identifier": "",
+ "url": {
+ "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
+ "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
+ "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
+ }
+ }
+}
+```
+
+### getAllProjects
+
+- Purpose: **Get all available projects**
+- Parameters:
+ - **none**
+- Result on success: **List of projects**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllProjects",
+ "id": 2134420212
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2134420212,
+ "result": [
+ {
+ "id": "1",
+ "name": "API test",
+ "is_active": "1",
+ "token": "",
+ "last_modified": "1436119570",
+ "is_public": "0",
+ "is_private": "0",
+ "is_everybody_allowed": "0",
+ "default_swimlane": "Default swimlane",
+ "show_default_swimlane": "1",
+ "description": null,
+ "identifier": "",
+ "url": {
+ "board": "http:\/\/127.0.0.1:8000\/?controller=board&action=show&project_id=1",
+ "calendar": "http:\/\/127.0.0.1:8000\/?controller=calendar&action=show&project_id=1",
+ "list": "http:\/\/127.0.0.1:8000\/?controller=listing&action=show&project_id=1"
+ }
+ }
+ ]
+}
+```
+
+### updateProject
+
+- Purpose: **Update a project**
+- Parameters:
+ - **id** (integer, required)
+ - **name** (string, required)
+ - **description** (string, optional)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateProject",
+ "id": 1853996288,
+ "params": {
+ "id": 1,
+ "name": "PHP client update"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1853996288,
+ "result": true
+}
+```
+
+### removeProject
+
+- Purpose: **Remove a project**
+- Parameters:
+ **project_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeProject",
+ "id": 46285125,
+ "params": {
+ "project_id": "2"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 46285125,
+ "result": true
+}
+```
+
+### enableProject
+
+- Purpose: **Enable a project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "enableProject",
+ "id": 1775494839,
+ "params": [
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1775494839,
+ "result": true
+}
+```
+
+### disableProject
+
+- Purpose: **Disable a project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "disableProject",
+ "id": 1734202312,
+ "params": [
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1734202312,
+ "result": true
+}
+```
+
+### enableProjectPublicAccess
+
+- Purpose: **Enable public access for a given project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "enableProjectPublicAccess",
+ "id": 103792571,
+ "params": [
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 103792571,
+ "result": true
+}
+```
+
+### disableProjectPublicAccess
+
+- Purpose: **Disable public access for a given project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "disableProjectPublicAccess",
+ "id": 942472945,
+ "params": [
+ "1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 942472945,
+ "result": true
+}
+```
+
+### getProjectActivity
+
+- Purpose: **Get activity stream for a project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **List of events**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getProjectActivity",
+ "id": 942472945,
+ "params": [
+ "project_id": 1
+ ]
+}
+```
+
+### getProjectActivities
+
+- Purpose: **Get Activityfeed for Project(s)**
+- Parameters:
+ - **project_ids** (integer array, required)
+- Result on success: **List of events**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getProjectActivities",
+ "id": 942472945,
+ "params": [
+ "project_ids": [1,2]
+ ]
+}
+```
+
+### getMembers
+
+- Purpose: **Get members of a project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: Key/value pair of user_id and username
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getMembers",
+ "id": 1944388643,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1944388643,
+ "result": {
+ "1": "user1",
+ "2": "user2",
+ "3": "user3"
+ }
+}
+```
+
+### revokeUser
+
+- Purpose: **Revoke user access for a given project**
+- Parameters:
+ - **project_id** (integer, required)
+ - **user_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "revokeUser",
+ "id": 251218350,
+ "params": [
+ 1,
+ 2
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 251218350,
+ "result": true
+}
+```
+
+### allowUser
+
+- Purpose: **Grant user access for a given project**
+- Parameters:
+ - **project_id** (integer, required)
+ - **user_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "allowUser",
+ "id": 2111451404,
+ "params": [
+ 1,
+ 2
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2111451404,
+ "result": true
+}
+```
diff --git a/doc/api-subtask-procedures.markdown b/doc/api-subtask-procedures.markdown
new file mode 100644
index 00000000..9ec9a807
--- /dev/null
+++ b/doc/api-subtask-procedures.markdown
@@ -0,0 +1,194 @@
+API Subtask procedures
+======================
+
+### createSubtask
+
+- Purpose: **Create a new subtask**
+- Parameters:
+ - **task_id** (integer, required)
+ - **title** (integer, required)
+ - **user_id** (int, optional)
+ - **time_estimated** (int, optional)
+ - **time_spent** (int, optional)
+ - **status** (int, optional)
+- Result on success: **subtask_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createSubtask",
+ "id": 2041554661,
+ "params": {
+ "task_id": 1,
+ "title": "Subtask #1"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2041554661,
+ "result": 45
+}
+```
+
+### getSubtask
+
+- Purpose: **Get subtask information**
+- Parameters:
+ - **subtask_id** (integer)
+- Result on success: **subtask properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getSubtask",
+ "id": 133184525,
+ "params": {
+ "subtask_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 133184525,
+ "result": {
+ "id": "1",
+ "title": "Subtask #1",
+ "status": "0",
+ "time_estimated": "0",
+ "time_spent": "0",
+ "task_id": "1",
+ "user_id": "0"
+ }
+}
+```
+
+### getAllSubtasks
+
+- Purpose: **Get all available subtasks**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **List of subtasks**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllSubtasks",
+ "id": 2087700490,
+ "params": {
+ "task_id": 1
+ }
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2087700490,
+ "result": [
+ {
+ "id": "1",
+ "title": "Subtask #1",
+ "status": "0",
+ "time_estimated": "0",
+ "time_spent": "0",
+ "task_id": "1",
+ "user_id": "0",
+ "username": null,
+ "name": null,
+ "status_name": "Todo"
+ },
+ ...
+ ]
+}
+```
+
+### updateSubtask
+
+- Purpose: **Update a subtask**
+- Parameters:
+ - **id** (integer, required)
+ - **task_id** (integer, required)
+ - **title** (integer, optional)
+ - **user_id** (integer, optional)
+ - **time_estimated** (integer, optional)
+ - **time_spent** (integer, optional)
+ - **status** (integer, optional)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateSubtask",
+ "id": 191749979,
+ "params": {
+ "id": 1,
+ "task_id": 1,
+ "status": 1,
+ "time_spent": 5,
+ "user_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 191749979,
+ "result": true
+}
+```
+
+### removeSubtask
+
+- Purpose: **Remove a subtask**
+- Parameters:
+ - **subtask_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeSubtask",
+ "id": 1382487306,
+ "params": {
+ "subtask_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1382487306,
+ "result": true
+}
+```
diff --git a/doc/api-swimlane-procedures.markdown b/doc/api-swimlane-procedures.markdown
new file mode 100644
index 00000000..fe917042
--- /dev/null
+++ b/doc/api-swimlane-procedures.markdown
@@ -0,0 +1,469 @@
+API Swimlane Procedures
+=======================
+
+### getDefaultSwimlane
+
+- Purpose: **Get the default swimlane for a project**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getDefaultSwimlane",
+ "id": 898774713,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 898774713,
+ "result": {
+ "id": "1",
+ "default_swimlane": "Default swimlane",
+ "show_default_swimlane": "1"
+ }
+}
+```
+
+### getActiveSwimlanes
+
+- Purpose: **Get the list of enabled swimlanes of a project (include default swimlane if enabled)**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **List of swimlanes**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getActiveSwimlanes",
+ "id": 934789422,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 934789422,
+ "result": [
+ {
+ "id": 0,
+ "name": "Default swimlane"
+ },
+ {
+ "id": "2",
+ "name": "Swimlane A"
+ }
+ ]
+}
+```
+
+### getAllSwimlanes
+
+- Purpose: **Get the list of all swimlanes of a project (enabled or disabled) and sorted by position**
+- Parameters:
+ - **project_id** (integer, required)
+- Result on success: **List of swimlanes**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllSwimlanes",
+ "id": 509791576,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 509791576,
+ "result": [
+ {
+ "id": "1",
+ "name": "Another swimlane",
+ "position": "1",
+ "is_active": "1",
+ "project_id": "1"
+ },
+ {
+ "id": "2",
+ "name": "Swimlane A",
+ "position": "2",
+ "is_active": "1",
+ "project_id": "1"
+ }
+ ]
+}
+```
+
+### getSwimlane
+
+- Purpose: **Get the a swimlane by id**
+- Parameters:
+ - **swimlane_id** (integer, required)
+- Result on success: **swimlane properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getSwimlane",
+ "id": 131071870,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 131071870,
+ "result": {
+ "id": "1",
+ "name": "Swimlane 1",
+ "position": "1",
+ "is_active": "1",
+ "project_id": "1"
+ }
+}
+```
+
+### getSwimlaneById
+
+- Purpose: **Get the a swimlane by id**
+- Parameters:
+ - **swimlane_id** (integer, required)
+- Result on success: **swimlane properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getSwimlaneById",
+ "id": 131071870,
+ "params": [
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 131071870,
+ "result": {
+ "id": "1",
+ "name": "Swimlane 1",
+ "position": "1",
+ "is_active": "1",
+ "project_id": "1"
+ }
+}
+```
+
+### getSwimlaneByName
+
+- Purpose: **Get the a swimlane by name**
+- Parameters:
+ - **project_id** (integer, required)
+ - **name** (string, required)
+- Result on success: **swimlane properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getSwimlaneByName",
+ "id": 824623567,
+ "params": [
+ 1,
+ "Swimlane 1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 824623567,
+ "result": {
+ "id": "1",
+ "name": "Swimlane 1",
+ "position": "1",
+ "is_active": "1",
+ "project_id": "1"
+ }
+}
+```
+
+### moveSwimlaneUp
+
+- Purpose: **Move up the swimlane position**
+- Parameters:
+ - **project_id** (integer, required)
+ - **swimlane_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "moveSwimlaneUp",
+ "id": 99275573,
+ "params": [
+ 1,
+ 2
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 99275573,
+ "result": true
+}
+```
+
+### moveSwimlaneDown
+
+- Purpose: **Move down the swimlane position**
+- Parameters:
+ - **project_id** (integer, required)
+ - **swimlane_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "moveSwimlaneDown",
+ "id": 957090649,
+ "params": {
+ "project_id": 1,
+ "swimlane_id": 2
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 957090649,
+ "result": true
+}
+```
+
+### updateSwimlane
+
+- Purpose: **Update swimlane properties**
+- Parameters:
+ - **swimlane_id** (integer, required)
+ - **name** (string, required)
+ - **description** (string, optional)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateSwimlane",
+ "id": 87102426,
+ "params": [
+ "1",
+ "Another swimlane"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 87102426,
+ "result": true
+}
+```
+
+### addSwimlane
+
+- Purpose: **Add a new swimlane**
+- Parameters:
+ - **project_id** (integer, required)
+ - **name** (string, required)
+ - **description** (string, optional)
+- Result on success: **swimlane_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "addSwimlane",
+ "id": 849940086,
+ "params": [
+ 1,
+ "Swimlane 1"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 849940086,
+ "result": 1
+}
+```
+
+### removeSwimlane
+
+- Purpose: **Remove a swimlane**
+- Parameters:
+ - **project_id** (integer, required)
+ - **swimlane_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeSwimlane",
+ "id": 1433237746,
+ "params": [
+ 2,
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1433237746,
+ "result": true
+}
+```
+
+### disableSwimlane
+
+- Purpose: **Enable a swimlane**
+- Parameters:
+ - **project_id** (integer, required)
+ - **swimlane_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "disableSwimlane",
+ "id": 1433237746,
+ "params": [
+ 2,
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1433237746,
+ "result": true
+}
+```
+
+### enableSwimlane
+
+- Purpose: **Enable a swimlane**
+- Parameters:
+ - **project_id** (integer, required)
+ - **swimlane_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "enableSwimlane",
+ "id": 1433237746,
+ "params": [
+ 2,
+ 1
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1433237746,
+ "result": true
+}
+```
diff --git a/doc/api-task-procedures.markdown b/doc/api-task-procedures.markdown
new file mode 100644
index 00000000..71d5bd23
--- /dev/null
+++ b/doc/api-task-procedures.markdown
@@ -0,0 +1,564 @@
+API Task Procedures
+===================
+
+### createTask
+
+- Purpose: **Create a new task**
+- Parameters:
+ - **title** (string, required)
+ - **project_id** (integer, required)
+ - **color_id** (string, optional)
+ - **column_id** (integer, optional)
+ - **owner_id** (integer, optional)
+ - **creator_id** (integer, optional)
+ - **date_due**: ISO8601 format (string, optional)
+ - **description** Markdown content (string, optional)
+ - **category_id** (integer, optional)
+ - **score** (integer, optional)
+ - **swimlane_id** (integer, optional)
+ - **recurrence_status** (integer, optional)
+ - **recurrence_trigger** (integer, optional)
+ - **recurrence_factor** (integer, optional)
+ - **recurrence_timeframe** (integer, optional)
+ - **recurrence_basedate** (integer, optional)
+- Result on success: **task_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createTask",
+ "id": 1176509098,
+ "params": {
+ "owner_id": 1,
+ "creator_id": 0,
+ "date_due": "",
+ "description": "",
+ "category_id": 0,
+ "score": 0,
+ "title": "Test",
+ "project_id": 1,
+ "color_id": "green",
+ "column_id": 2,
+ "recurrence_status": 0,
+ "recurrence_trigger": 0,
+ "recurrence_factor": 0,
+ "recurrence_timeframe": 0,
+ "recurrence_basedate": 0
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1176509098,
+ "result": 3
+}
+```
+
+### getTask
+
+- Purpose: **Get task by the unique id**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **task properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getTask",
+ "id": 700738119,
+ "params": {
+ "task_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 700738119,
+ "result": {
+ "id": "1",
+ "title": "Task #1",
+ "description": "",
+ "date_creation": "1409963206",
+ "color_id": "blue",
+ "project_id": "1",
+ "column_id": "2",
+ "owner_id": "1",
+ "position": "1",
+ "is_active": "1",
+ "date_completed": null,
+ "score": "0",
+ "date_due": "0",
+ "category_id": "0",
+ "creator_id": "0",
+ "date_modification": "1409963206",
+ "reference": "",
+ "date_started": null,
+ "time_spent": "0",
+ "time_estimated": "0",
+ "swimlane_id": "0",
+ "date_moved": "1430875287",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0",
+ "recurrence_factor": "0",
+ "recurrence_timeframe": "0",
+ "recurrence_basedate": "0",
+ "recurrence_parent": null,
+ "recurrence_child": null,
+ "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=1&project_id=1",
+ "color": {
+ "name": "Yellow",
+ "background": "rgb(245, 247, 196)",
+ "border": "rgb(223, 227, 45)"
+ }
+ }
+}
+```
+
+### getTaskByReference
+
+- Purpose: **Get task by the external reference**
+- Parameters:
+ - **project_id** (integer, required)
+ - **reference** (string, required)
+- Result on success: **task properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getTaskByReference",
+ "id": 1992081213,
+ "params": {
+ "project_id": 1,
+ "reference": "TICKET-1234"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1992081213,
+ "result": {
+ "id": "5",
+ "title": "Task with external ticket number",
+ "description": "[Link to my ticket](http:\/\/my-ticketing-system\/1234)",
+ "date_creation": "1434227446",
+ "color_id": "yellow",
+ "project_id": "1",
+ "column_id": "1",
+ "owner_id": "0",
+ "position": "4",
+ "is_active": "1",
+ "date_completed": null,
+ "score": "0",
+ "date_due": "0",
+ "category_id": "0",
+ "creator_id": "0",
+ "date_modification": "1434227446",
+ "reference": "TICKET-1234",
+ "date_started": null,
+ "time_spent": "0",
+ "time_estimated": "0",
+ "swimlane_id": "0",
+ "date_moved": "1434227446",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0",
+ "recurrence_factor": "0",
+ "recurrence_timeframe": "0",
+ "recurrence_basedate": "0",
+ "recurrence_parent": null,
+ "recurrence_child": null,
+ "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=5&project_id=1"
+ }
+}
+```
+
+### getAllTasks
+
+- Purpose: **Get all available tasks**
+- Parameters:
+ - **project_id** (integer, required)
+ - **status_id**: The value 1 for active tasks and 0 for inactive (integer, required)
+- Result on success: **List of tasks**
+- Result on failure: **false**
+
+Request example to fetch all tasks on the board:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllTasks",
+ "id": 133280317,
+ "params": {
+ "project_id": 1,
+ "status_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 133280317,
+ "result": [
+ {
+ "id": "1",
+ "title": "Task #1",
+ "description": "",
+ "date_creation": "1409961789",
+ "color_id": "blue",
+ "project_id": "1",
+ "column_id": "2",
+ "owner_id": "1",
+ "position": "1",
+ "is_active": "1",
+ "date_completed": null,
+ "score": "0",
+ "date_due": "0",
+ "category_id": "0",
+ "creator_id": "0",
+ "date_modification": "1409961789",
+ "reference": "",
+ "date_started": null,
+ "time_spent": "0",
+ "time_estimated": "0",
+ "swimlane_id": "0",
+ "date_moved": "1430783191",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0",
+ "recurrence_factor": "0",
+ "recurrence_timeframe": "0",
+ "recurrence_basedate": "0",
+ "recurrence_parent": null,
+ "recurrence_child": null,
+ "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=1&project_id=1"
+ },
+ {
+ "id": "2",
+ "title": "Test",
+ "description": "",
+ "date_creation": "1409962115",
+ "color_id": "green",
+ "project_id": "1",
+ "column_id": "2",
+ "owner_id": "1",
+ "position": "2",
+ "is_active": "1",
+ "date_completed": null,
+ "score": "0",
+ "date_due": "0",
+ "category_id": "0",
+ "creator_id": "0",
+ "date_modification": "1409962115",
+ "reference": "",
+ "date_started": null,
+ "time_spent": "0",
+ "time_estimated": "0",
+ "swimlane_id": "0",
+ "date_moved": "1430783191",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0",
+ "recurrence_factor": "0",
+ "recurrence_timeframe": "0",
+ "recurrence_basedate": "0",
+ "recurrence_parent": null,
+ "recurrence_child": null,
+ "url": "http:\/\/127.0.0.1:8000\/?controller=task&action=show&task_id=2&project_id=1"
+ },
+ ...
+ ]
+}
+```
+
+### getOverdueTasks
+
+- Purpose: **Get all overdue tasks**
+- Result on success: **List of tasks**
+- Result on failure: **false**
+
+Request example to fetch all tasks on the board:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getOverdueTasks",
+ "id": 133280317
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 133280317,
+ "result": [
+ {
+ "id": "1",
+ "title": "Task #1",
+ "date_due": "1409961789",
+ "project_id": "1",
+ "project_name": "Test",
+ "assignee_username":"admin",
+ "assignee_name": null
+ },
+ {
+ "id": "2",
+ "title": "Test",
+ "date_due": "1409962115",
+ "project_id": "1",
+ "project_name": "Test",
+ "assignee_username":"admin",
+ "assignee_name": null
+ },
+ ...
+ ]
+}
+```
+
+### getOverdueTasksByProject
+
+- Purpose: **Get all overdue tasks for a special project**
+- Result on success: **List of tasks**
+- Result on failure: **false**
+
+Request example to fetch all tasks on the board:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getOverdueTasksByProject",
+ "id": 133280317,
+ "params": {
+ "project_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 133280317,
+ "result": [
+ {
+ "id": "1",
+ "title": "Task #1",
+ "date_due": "1409961789",
+ "project_id": "1",
+ "project_name": "Test",
+ "assignee_username":"admin",
+ "assignee_name": null
+ },
+ {
+ "id": "2",
+ "title": "Test",
+ "date_due": "1409962115",
+ "project_id": "1",
+ "project_name": "Test",
+ "assignee_username":"admin",
+ "assignee_name": null
+ },
+ ...
+ ]
+}
+```
+
+### updateTask
+
+- Purpose: **Update a task**
+- Parameters:
+ - **id** (integer, required)
+ - **title** (string, optional)
+ - **project_id** (integer, optional)
+ - **color_id** (string, optional)
+ - **owner_id** (integer, optional)
+ - **creator_id** (integer, optional)
+ - **date_due**: ISO8601 format (string, optional)
+ - **description** Markdown content (string, optional)
+ - **category_id** (integer, optional)
+ - **score** (integer, optional)
+ - **recurrence_status** (integer, optional)
+ - **recurrence_trigger** (integer, optional)
+ - **recurrence_factor** (integer, optional)
+ - **recurrence_timeframe** (integer, optional)
+ - **recurrence_basedate** (integer, optional)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example to change the task color:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateTask",
+ "id": 1406803059,
+ "params": {
+ "id": 1,
+ "color_id": "blue"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1406803059,
+ "result": true
+}
+```
+
+### openTask
+
+- Purpose: **Set a task to the status open**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "openTask",
+ "id": 1888531925,
+ "params": {
+ "task_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1888531925,
+ "result": true
+}
+```
+
+### closeTask
+
+- Purpose: **Set a task to the status close**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "closeTask",
+ "id": 1654396960,
+ "params": {
+ "task_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1654396960,
+ "result": true
+}
+```
+
+### removeTask
+
+- Purpose: **Remove a task**
+- Parameters:
+ - **task_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeTask",
+ "id": 1423501287,
+ "params": {
+ "task_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1423501287,
+ "result": true
+}
+```
+
+### moveTaskPosition
+
+- Purpose: **Move a task to another column or another position**
+- Parameters:
+ - **project_id** (integer, required)
+ - **task_id** (integer, required)
+ - **column_id** (integer, required)
+ - **position** (integer, required)
+ - **swimlane_id** (integer, optional, default=0)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "moveTaskPosition",
+ "id": 117211800,
+ "params": {
+ "project_id": 1,
+ "task_id": 1,
+ "column_id": 2,
+ "position": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 117211800,
+ "result": true
+}
+```
diff --git a/doc/api-user-procedures.markdown b/doc/api-user-procedures.markdown
new file mode 100644
index 00000000..ccf4a6a6
--- /dev/null
+++ b/doc/api-user-procedures.markdown
@@ -0,0 +1,222 @@
+API User Procedures
+===================
+
+### createUser
+
+- Purpose: **Create a new user**
+- Parameters:
+ - **username** Must be unique (string, required)
+ - **password** Must have at least 6 characters (string, required)
+ - **name** (string, optional)
+ - **email** (string, optional)
+ - **role** (string, optional, example: app-admin, app-manager, app-user)
+- Result on success: **user_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createUser",
+ "id": 1518863034,
+ "params": {
+ "username": "biloute",
+ "password": "123456"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1518863034,
+ "result": 22
+}
+```
+
+### createLdapUser
+
+- Purpose: **Create a new user authentified by LDAP**
+- Parameters:
+ - **username** (string, required)
+- Result on success: **user_id**
+- Result on failure: **false**
+
+The user will only be created if he is found on the LDAP server.
+This method works only with LDAP authentication configured in proxy or anonymous mode.
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createLdapUser",
+ "id": 1518863034,
+ "params": {
+ "username": "my_ldap_user",
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1518863034,
+ "result": 22
+}
+```
+
+### getUser
+
+- Purpose: **Get user information**
+- Parameters:
+ - **user_id** (integer, required)
+- Result on success: **user properties**
+- Result on failure: **null**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getUser",
+ "id": 1769674781,
+ "params": {
+ "user_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1769674781,
+ "result": {
+ "id": "1",
+ "username": "biloute",
+ "password": "$2y$10$dRs6pPoBu935RpmsrhmbjevJH5MgZ7Kr9QrnVINwwyZ3.MOwqg.0m",
+ "role": "app-user",
+ "is_ldap_user": "0",
+ "name": "",
+ "email": "",
+ "google_id": null,
+ "github_id": null,
+ "notifications_enabled": "0"
+ }
+}
+```
+
+### getAllUsers
+
+- Purpose: **Get all available users**
+- Parameters:
+ - **none**
+- Result on success: **List of users**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getAllUsers",
+ "id": 1438712131
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1438712131,
+ "result": [
+ {
+ "id": "1",
+ "username": "biloute",
+ "name": "",
+ "email": "",
+ "role": "app-user",
+ "is_ldap_user": "0",
+ "notifications_enabled": "0",
+ "google_id": null,
+ "github_id": null
+ },
+ ...
+ ]
+}
+```
+
+### updateUser
+
+- Purpose: **Update a user**
+- Parameters:
+ - **id** (integer)
+ - **username** (string, optional)
+ - **name** (string, optional)
+ - **email** (string, optional)
+ - **role** (string, optional, example: app-admin, app-manager, app-user)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "updateUser",
+ "id": 322123657,
+ "params": {
+ "id": 1,
+ "role": "app-manager"
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 322123657,
+ "result": true
+}
+```
+
+### removeUser
+
+- Purpose: **Remove a user**
+- Parameters:
+ - **user_id** (integer, required)
+- Result on success: **true**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "removeUser",
+ "id": 2094191872,
+ "params": {
+ "user_id": 1
+ }
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2094191872,
+ "result": true
+}
+```
diff --git a/kanboard b/kanboard
index 2911a506..73dab4e9 100755
--- a/kanboard
+++ b/kanboard
@@ -14,7 +14,7 @@ use Kanboard\Console\TransitionExport;
use Kanboard\Console\LocaleSync;
use Kanboard\Console\LocaleComparator;
-$container['dispatcher']->dispatch('console.bootstrap', new Event);
+$container['dispatcher']->dispatch('app.bootstrap', new Event);
$application = new Application('Kanboard', APP_VERSION);
$application->add(new TaskOverdueNotification($container));
diff --git a/tests/functionals/ApiTest.php b/tests/functionals/ApiTest.php
index 7576f031..9be22023 100644
--- a/tests/functionals/ApiTest.php
+++ b/tests/functionals/ApiTest.php
@@ -504,6 +504,21 @@ class Api extends PHPUnit_Framework_TestCase
$this->assertTrue($user_id > 0);
}
+ public function testCreateManagerUser()
+ {
+ $user = array(
+ 'username' => 'manager',
+ 'name' => 'Manager',
+ 'password' => '123456',
+ 'role' => 'app-manager'
+ );
+
+ $user_id = $this->client->execute('createUser', $user);
+ $this->assertNotFalse($user_id);
+ $this->assertInternalType('int', $user_id);
+ $this->assertTrue($user_id > 0);
+ }
+
/**
* @expectedException InvalidArgumentException
*/
@@ -524,6 +539,10 @@ class Api extends PHPUnit_Framework_TestCase
$this->assertTrue(is_array($user));
$this->assertEquals('toto', $user['username']);
+ $user = $this->client->getUser(3);
+ $this->assertNotEmpty($user);
+ $this->assertEquals('app-manager', $user['role']);
+
$this->assertNull($this->client->getUser(2222));
}
diff --git a/tests/units/Action/TaskAssignCurrentUserTest.php b/tests/units/Action/TaskAssignCurrentUserTest.php
index 08176b1c..e2d1c11a 100644
--- a/tests/units/Action/TaskAssignCurrentUserTest.php
+++ b/tests/units/Action/TaskAssignCurrentUserTest.php
@@ -7,7 +7,7 @@ use Kanboard\Model\Task;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Project;
-use Kanboard\Model\UserSession;
+use Kanboard\Core\User\UserSession;
use Kanboard\Action\TaskAssignCurrentUser;
class TaskAssignCurrentUserTest extends Base
diff --git a/tests/units/Auth/DatabaseAuthTest.php b/tests/units/Auth/DatabaseAuthTest.php
new file mode 100644
index 00000000..a13b7fee
--- /dev/null
+++ b/tests/units/Auth/DatabaseAuthTest.php
@@ -0,0 +1,52 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Auth\DatabaseAuth;
+
+class DatabaseAuthTest extends Base
+{
+ public function testGetName()
+ {
+ $provider = new DatabaseAuth($this->container);
+ $this->assertEquals('Database', $provider->getName());
+ }
+
+ public function testAuthenticate()
+ {
+ $provider = new DatabaseAuth($this->container);
+
+ $provider->setUsername('admin');
+ $provider->setPassword('admin');
+ $this->assertTrue($provider->authenticate());
+
+ $provider->setUsername('admin');
+ $provider->setPassword('test');
+ $this->assertFalse($provider->authenticate());
+ }
+
+ public function testGetUser()
+ {
+ $provider = new DatabaseAuth($this->container);
+ $this->assertEquals(null, $provider->getUser());
+
+ $provider = new DatabaseAuth($this->container);
+ $provider->setUsername('admin');
+ $provider->setPassword('admin');
+
+ $this->assertTrue($provider->authenticate());
+ $this->assertInstanceOf('Kanboard\User\DatabaseUserProvider', $provider->getUser());
+ }
+
+ public function testIsvalidSession()
+ {
+ $provider = new DatabaseAuth($this->container);
+ $this->assertFalse($provider->isValidSession());
+
+ $this->container['sessionStorage']->user = array('id' => 1);
+ $this->assertTrue($provider->isValidSession());
+
+ $this->container['sessionStorage']->user = array('id' => 2);
+ $this->assertFalse($provider->isValidSession());
+ }
+}
diff --git a/tests/units/Auth/LdapTest.php b/tests/units/Auth/LdapTest.php
deleted file mode 100644
index bf2cec35..00000000
--- a/tests/units/Auth/LdapTest.php
+++ /dev/null
@@ -1,697 +0,0 @@
-<?php
-
-namespace Kanboard\Auth;
-
-require_once __DIR__.'/../Base.php';
-
-function ldap_connect($hostname, $port)
-{
- return LdapTest::$functions->ldap_connect($hostname, $port);
-}
-
-function ldap_set_option()
-{
-}
-
-function ldap_bind($link_identifier, $bind_rdn, $bind_password)
-{
- return LdapTest::$functions->ldap_bind($link_identifier, $bind_rdn, $bind_password);
-}
-
-function ldap_search($link_identifier, $base_dn, $filter, array $attributes)
-{
- return LdapTest::$functions->ldap_search($link_identifier, $base_dn, $filter, $attributes);
-}
-
-function ldap_get_entries($link_identifier, $result_identifier)
-{
- return LdapTest::$functions->ldap_get_entries($link_identifier, $result_identifier);
-}
-
-class LdapTest extends \Base
-{
- public static $functions;
- private $ldap;
-
- public function setUp()
- {
- parent::setup();
-
- self::$functions = $this
- ->getMockBuilder('stdClass')
- ->setMethods(array(
- 'ldap_connect',
- 'ldap_set_option',
- 'ldap_bind',
- 'ldap_search',
- 'ldap_get_entries',
- ))
- ->getMock();
- }
-
- public function tearDown()
- {
- parent::tearDown();
- self::$functions = null;
- }
-
- public function testGetAttributes()
- {
- $ldap = new Ldap($this->container);
- $this->assertCount(3, $ldap->getProfileAttributes());
- $this->assertContains(LDAP_ACCOUNT_FULLNAME, $ldap->getProfileAttributes());
- $this->assertContains(LDAP_ACCOUNT_EMAIL, $ldap->getProfileAttributes());
- $this->assertContains(LDAP_ACCOUNT_MEMBEROF, $ldap->getProfileAttributes());
- }
-
- public function testConnectSuccess()
- {
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getLdapServer'))
- ->getMock();
-
- $ldap
- ->expects($this->once())
- ->method('getLdapServer')
- ->will($this->returnValue('my_ldap_server'));
-
- self::$functions
- ->expects($this->once())
- ->method('ldap_connect')
- ->with(
- $this->equalTo('my_ldap_server'),
- $this->equalTo($ldap->getLdapPort())
- )
- ->will($this->returnValue('my_ldap_resource'));
-
- $this->assertNotFalse($ldap->connect());
- }
-
- public function testConnectFailure()
- {
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getLdapServer'))
- ->getMock();
-
- $ldap
- ->expects($this->once())
- ->method('getLdapServer')
- ->will($this->returnValue('my_ldap_server'));
-
- self::$functions
- ->expects($this->once())
- ->method('ldap_connect')
- ->with(
- $this->equalTo('my_ldap_server'),
- $this->equalTo($ldap->getLdapPort())
- )
- ->will($this->returnValue(false));
-
- $this->assertFalse($ldap->connect());
- }
-
- public function testBindAnonymous()
- {
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getLdapBindType'))
- ->getMock();
-
- $ldap
- ->expects($this->any())
- ->method('getLdapBindType')
- ->will($this->returnValue('anonymous'));
-
- self::$functions
- ->expects($this->once())
- ->method('ldap_bind')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo(null),
- $this->equalTo(null)
- )
- ->will($this->returnValue(true));
-
- $this->assertTrue($ldap->bind('my_ldap_connection', 'my_user', 'my_password'));
- }
-
- public function testBindUser()
- {
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getLdapUsername', 'getLdapBindType'))
- ->getMock();
-
- $ldap
- ->expects($this->once())
- ->method('getLdapUsername')
- ->will($this->returnValue('uid=my_user'));
-
- $ldap
- ->expects($this->any())
- ->method('getLdapBindType')
- ->will($this->returnValue('user'));
-
- self::$functions
- ->expects($this->once())
- ->method('ldap_bind')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('uid=my_user'),
- $this->equalTo('my_password')
- )
- ->will($this->returnValue(true));
-
- $this->assertTrue($ldap->bind('my_ldap_connection', 'my_user', 'my_password'));
- }
-
- public function testBindProxy()
- {
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getLdapUsername', 'getLdapPassword', 'getLdapBindType'))
- ->getMock();
-
- $ldap
- ->expects($this->once())
- ->method('getLdapUsername')
- ->will($this->returnValue('someone'));
-
- $ldap
- ->expects($this->once())
- ->method('getLdapPassword')
- ->will($this->returnValue('something'));
-
- $ldap
- ->expects($this->any())
- ->method('getLdapBindType')
- ->will($this->returnValue('proxy'));
-
- self::$functions
- ->expects($this->once())
- ->method('ldap_bind')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('someone'),
- $this->equalTo('something')
- )
- ->will($this->returnValue(true));
-
- $this->assertTrue($ldap->bind('my_ldap_connection', 'my_user', 'my_password'));
- }
-
- public function testSearchSuccess()
- {
- $entries = array(
- 'count' => 1,
- 0 => array(
- 'count' => 2,
- 'dn' => 'uid=my_user,ou=People,dc=kanboard,dc=local',
- 'displayname' => array(
- 'count' => 1,
- 0 => 'My user',
- ),
- 'mail' => array(
- 'count' => 2,
- 0 => 'user1@localhost',
- 1 => 'user2@localhost',
- ),
- 0 => 'displayname',
- 1 => 'mail',
- )
- );
-
- $expected = array(
- 'username' => 'my_user',
- 'name' => 'My user',
- 'email' => 'user1@localhost',
- 'is_admin' => 0,
- 'is_project_admin' => 0,
- 'is_ldap_user' => 1,
- );
-
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getLdapUserPattern', 'getLdapBaseDn'))
- ->getMock();
-
- $ldap
- ->expects($this->once())
- ->method('getLdapUserPattern')
- ->will($this->returnValue('uid=my_user'));
-
- $ldap
- ->expects($this->once())
- ->method('getLdapBaseDn')
- ->will($this->returnValue('ou=People,dc=kanboard,dc=local'));
-
- self::$functions
- ->expects($this->at(0))
- ->method('ldap_search')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('ou=People,dc=kanboard,dc=local'),
- $this->equalTo('uid=my_user'),
- $this->equalTo($ldap->getProfileAttributes())
- )
- ->will($this->returnValue('my_result_identifier'));
-
- self::$functions
- ->expects($this->at(1))
- ->method('ldap_get_entries')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('my_result_identifier')
- )
- ->will($this->returnValue($entries));
-
- self::$functions
- ->expects($this->at(2))
- ->method('ldap_bind')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('uid=my_user,ou=People,dc=kanboard,dc=local'),
- $this->equalTo('my_password')
- )
- ->will($this->returnValue(true));
-
- $this->assertEquals($expected, $ldap->getProfile('my_ldap_connection', 'my_user', 'my_password'));
- }
-
- public function testSearchWithBadPassword()
- {
- $entries = array(
- 'count' => 1,
- 0 => array(
- 'count' => 2,
- 'dn' => 'uid=my_user,ou=People,dc=kanboard,dc=local',
- 'displayname' => array(
- 'count' => 1,
- 0 => 'My user',
- ),
- 'mail' => array(
- 'count' => 2,
- 0 => 'user1@localhost',
- 1 => 'user2@localhost',
- ),
- 0 => 'displayname',
- 1 => 'mail',
- )
- );
-
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getLdapUserPattern', 'getLdapBaseDn'))
- ->getMock();
-
- $ldap
- ->expects($this->once())
- ->method('getLdapUserPattern')
- ->will($this->returnValue('uid=my_user'));
-
- $ldap
- ->expects($this->once())
- ->method('getLdapBaseDn')
- ->will($this->returnValue('ou=People,dc=kanboard,dc=local'));
-
- self::$functions
- ->expects($this->at(0))
- ->method('ldap_search')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('ou=People,dc=kanboard,dc=local'),
- $this->equalTo('uid=my_user'),
- $this->equalTo($ldap->getProfileAttributes())
- )
- ->will($this->returnValue('my_result_identifier'));
-
- self::$functions
- ->expects($this->at(1))
- ->method('ldap_get_entries')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('my_result_identifier')
- )
- ->will($this->returnValue($entries));
-
- self::$functions
- ->expects($this->at(2))
- ->method('ldap_bind')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('uid=my_user,ou=People,dc=kanboard,dc=local'),
- $this->equalTo('my_password')
- )
- ->will($this->returnValue(false));
-
- $this->assertFalse($ldap->getProfile('my_ldap_connection', 'my_user', 'my_password'));
- }
-
- public function testSearchWithUserNotFound()
- {
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getLdapUserPattern', 'getLdapBaseDn'))
- ->getMock();
-
- $ldap
- ->expects($this->once())
- ->method('getLdapUserPattern')
- ->will($this->returnValue('uid=my_user'));
-
- $ldap
- ->expects($this->once())
- ->method('getLdapBaseDn')
- ->will($this->returnValue('ou=People,dc=kanboard,dc=local'));
-
- self::$functions
- ->expects($this->at(0))
- ->method('ldap_search')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('ou=People,dc=kanboard,dc=local'),
- $this->equalTo('uid=my_user'),
- $this->equalTo($ldap->getProfileAttributes())
- )
- ->will($this->returnValue('my_result_identifier'));
-
- self::$functions
- ->expects($this->at(1))
- ->method('ldap_get_entries')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('my_result_identifier')
- )
- ->will($this->returnValue(array()));
-
- $this->assertFalse($ldap->getProfile('my_ldap_connection', 'my_user', 'my_password'));
- }
-
- public function testSuccessfulAuthentication()
- {
- $this->container['userSession'] = $this
- ->getMockBuilder('\Kanboard\Model\UserSession')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('initialize'))
- ->getMock();
-
- $this->container['user'] = $this
- ->getMockBuilder('\Kanboard\Model\User')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getByUsername'))
- ->getMock();
-
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('findUser'))
- ->getMock();
-
- $ldap
- ->expects($this->once())
- ->method('findUser')
- ->with(
- $this->equalTo('user'),
- $this->equalTo('password')
- )
- ->will($this->returnValue(array('username' => 'user', 'name' => 'My user', 'email' => 'user@here')));
-
- $this->container['user']
- ->expects($this->once())
- ->method('getByUsername')
- ->with(
- $this->equalTo('user')
- )
- ->will($this->returnValue(array('id' => 2, 'username' => 'user', 'is_ldap_user' => 1)));
-
- $this->container['userSession']
- ->expects($this->once())
- ->method('initialize');
-
- $this->assertTrue($ldap->authenticate('user', 'password'));
- }
-
- public function testAuthenticationWithExistingLocalUser()
- {
- $this->container['userSession'] = $this
- ->getMockBuilder('\Kanboard\Model\UserSession')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('initialize'))
- ->getMock();
-
- $this->container['user'] = $this
- ->getMockBuilder('\Kanboard\Model\User')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getByUsername'))
- ->getMock();
-
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('findUser'))
- ->getMock();
-
- $ldap
- ->expects($this->once())
- ->method('findUser')
- ->with(
- $this->equalTo('user'),
- $this->equalTo('password')
- )
- ->will($this->returnValue(array('username' => 'user', 'name' => 'My user', 'email' => 'user@here')));
-
- $this->container['user']
- ->expects($this->once())
- ->method('getByUsername')
- ->with(
- $this->equalTo('user')
- )
- ->will($this->returnValue(array('id' => 2, 'username' => 'user', 'is_ldap_user' => 0)));
-
- $this->container['userSession']
- ->expects($this->never())
- ->method('initialize');
-
- $this->assertFalse($ldap->authenticate('user', 'password'));
- }
-
- public function testAuthenticationWithAutomaticAccountCreation()
- {
- $ldap_profile = array('username' => 'user', 'name' => 'My user', 'email' => 'user@here');
-
- $this->container['userSession'] = $this
- ->getMockBuilder('\Kanboard\Model\UserSession')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('initialize'))
- ->getMock();
-
- $this->container['user'] = $this
- ->getMockBuilder('\Kanboard\Model\User')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getByUsername', 'create'))
- ->getMock();
-
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('findUser'))
- ->getMock();
-
- $ldap
- ->expects($this->at(0))
- ->method('findUser')
- ->with(
- $this->equalTo('user'),
- $this->equalTo('password')
- )
- ->will($this->returnValue($ldap_profile));
-
- $this->container['user']
- ->expects($this->at(0))
- ->method('getByUsername')
- ->with(
- $this->equalTo('user')
- )
- ->will($this->returnValue(null));
-
- $this->container['user']
- ->expects($this->at(1))
- ->method('create')
- ->with(
- $this->equalTo($ldap_profile)
- )
- ->will($this->returnValue(true));
-
- $this->container['user']
- ->expects($this->at(2))
- ->method('getByUsername')
- ->with(
- $this->equalTo('user')
- )
- ->will($this->returnValue(array('id' => 2, 'username' => 'user', 'is_ldap_user' => 1)));
-
- $this->container['userSession']
- ->expects($this->once())
- ->method('initialize');
-
- $this->assertTrue($ldap->authenticate('user', 'password'));
- }
-
- public function testAuthenticationWithAutomaticAccountCreationFailed()
- {
- $ldap_profile = array('username' => 'user', 'name' => 'My user', 'email' => 'user@here');
-
- $this->container['userSession'] = $this
- ->getMockBuilder('\Kanboard\Model\UserSession')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('initialize'))
- ->getMock();
-
- $this->container['user'] = $this
- ->getMockBuilder('\Kanboard\Model\User')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('getByUsername', 'create'))
- ->getMock();
-
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('findUser'))
- ->getMock();
-
- $ldap
- ->expects($this->at(0))
- ->method('findUser')
- ->with(
- $this->equalTo('user'),
- $this->equalTo('password')
- )
- ->will($this->returnValue($ldap_profile));
-
- $this->container['user']
- ->expects($this->at(0))
- ->method('getByUsername')
- ->with(
- $this->equalTo('user')
- )
- ->will($this->returnValue(null));
-
- $this->container['user']
- ->expects($this->at(1))
- ->method('create')
- ->with(
- $this->equalTo($ldap_profile)
- )
- ->will($this->returnValue(false));
-
- $this->container['userSession']
- ->expects($this->never())
- ->method('initialize');
-
- $this->assertFalse($ldap->authenticate('user', 'password'));
- }
-
- public function testLookup()
- {
- $entries = array(
- 'count' => 1,
- 0 => array(
- 'count' => 2,
- 'dn' => 'uid=my_user,ou=People,dc=kanboard,dc=local',
- 'displayname' => array(
- 'count' => 1,
- 0 => 'My LDAP user',
- ),
- 'mail' => array(
- 'count' => 2,
- 0 => 'user1@localhost',
- 1 => 'user2@localhost',
- ),
- 'samaccountname' => array(
- 'count' => 1,
- 0 => 'my_ldap_user',
- ),
- 0 => 'displayname',
- 1 => 'mail',
- 2 => 'samaccountname',
- )
- );
-
- $expected = array(
- 'username' => 'my_ldap_user',
- 'name' => 'My LDAP user',
- 'email' => 'user1@localhost',
- 'is_admin' => 0,
- 'is_project_admin' => 0,
- 'is_ldap_user' => 1,
- );
-
- $ldap = $this
- ->getMockBuilder('\Kanboard\Auth\Ldap')
- ->setConstructorArgs(array($this->container))
- ->setMethods(array('connect', 'getLdapUserPattern', 'getLdapBaseDn', 'getLdapAccountId'))
- ->getMock();
-
- $ldap
- ->expects($this->once())
- ->method('connect')
- ->will($this->returnValue('my_ldap_connection'));
-
- $ldap
- ->expects($this->once())
- ->method('getLdapUserPattern')
- ->will($this->returnValue('sAMAccountName=my_user'));
-
- $ldap
- ->expects($this->any())
- ->method('getLdapAccountId')
- ->will($this->returnValue('samaccountname'));
-
- $ldap
- ->expects($this->once())
- ->method('getLdapBaseDn')
- ->will($this->returnValue('ou=People,dc=kanboard,dc=local'));
-
- self::$functions
- ->expects($this->at(0))
- ->method('ldap_bind')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo(null),
- $this->equalTo(null)
- )
- ->will($this->returnValue(true));
-
- self::$functions
- ->expects($this->at(1))
- ->method('ldap_search')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('ou=People,dc=kanboard,dc=local'),
- $this->equalTo('(&(sAMAccountName=my_user)(mail=user@localhost))'),
- $this->equalTo($ldap->getProfileAttributes())
- )
- ->will($this->returnValue('my_result_identifier'));
-
- self::$functions
- ->expects($this->at(2))
- ->method('ldap_get_entries')
- ->with(
- $this->equalTo('my_ldap_connection'),
- $this->equalTo('my_result_identifier')
- )
- ->will($this->returnValue($entries));
-
- $this->assertEquals($expected, $ldap->lookup('my_user', 'user@localhost'));
- }
-}
diff --git a/tests/units/Auth/ReverseProxyTest.php b/tests/units/Auth/ReverseProxyTest.php
deleted file mode 100644
index 6aaa5a67..00000000
--- a/tests/units/Auth/ReverseProxyTest.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Auth\ReverseProxy;
-use Kanboard\Model\User;
-
-class ReverseProxyTest extends Base
-{
- public function setUp()
- {
- parent::setup();
- $_SERVER = array();
- }
-
- public function testFailedAuthentication()
- {
- $auth = new ReverseProxy($this->container);
- $this->assertFalse($auth->authenticate());
- }
-
- public function testSuccessfulAuthentication()
- {
- $_SERVER[REVERSE_PROXY_USER_HEADER] = 'my_user';
-
- $a = new ReverseProxy($this->container);
- $u = new User($this->container);
-
- $this->assertTrue($a->authenticate());
-
- $user = $u->getByUsername('my_user');
- $this->assertNotEmpty($user);
- $this->assertEquals(0, $user['is_admin']);
- $this->assertEquals(1, $user['is_ldap_user']);
- $this->assertEquals(1, $user['disable_login_form']);
- }
-}
diff --git a/tests/units/Auth/TotpAuthTest.php b/tests/units/Auth/TotpAuthTest.php
new file mode 100644
index 00000000..fcb7ea31
--- /dev/null
+++ b/tests/units/Auth/TotpAuthTest.php
@@ -0,0 +1,63 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Auth\TotpAuth;
+
+class TotpAuthTest extends Base
+{
+ public function testGetName()
+ {
+ $provider = new TotpAuth($this->container);
+ $this->assertEquals('Time-based One-time Password Algorithm', $provider->getName());
+ }
+
+ public function testGetSecret()
+ {
+ $provider = new TotpAuth($this->container);
+ $secret = $provider->getSecret();
+
+ $this->assertNotEmpty($secret);
+ $this->assertEquals($secret, $provider->getSecret());
+ $this->assertEquals($secret, $provider->getSecret());
+ }
+
+ public function testSetSecret()
+ {
+ $provider = new TotpAuth($this->container);
+ $provider->setSecret('mySecret');
+ $this->assertEquals('mySecret', $provider->getSecret());
+ }
+
+ public function testGetUrl()
+ {
+ $provider = new TotpAuth($this->container);
+ $this->assertEmpty($provider->getQrCodeUrl('me'));
+ $this->assertEmpty($provider->getKeyUrl('me'));
+
+ $provider->setSecret('mySecret');
+ $this->assertEquals(
+ 'https://chart.googleapis.com/chart?chs=200x200&cht=qr&chld=M|0&chl=otpauth%3A%2F%2Ftotp%2Fme%3Fsecret%3DmySecret',
+ $provider->getQrCodeUrl('me')
+ );
+
+ $this->assertEquals('otpauth://totp/me?secret=mySecret', $provider->getKeyUrl('me'));
+ }
+
+ public function testAuthentication()
+ {
+ $provider = new TotpAuth($this->container);
+
+ $secret = $provider->getSecret();
+ $this->assertNotEmpty($secret);
+
+ $provider->setCode('1234');
+ $this->assertFalse($provider->authenticate());
+
+ if (!!`which oathtool`) {
+ $code = shell_exec('oathtool --totp -b '.$secret);
+ $provider->setCode(trim($code));
+ $this->assertTrue($provider->authenticate());
+ }
+ }
+}
diff --git a/tests/units/Base.php b/tests/units/Base.php
index 6d7a97a3..4b54cdb0 100644
--- a/tests/units/Base.php
+++ b/tests/units/Base.php
@@ -75,8 +75,11 @@ abstract class Base extends PHPUnit_Framework_TestCase
}
$this->container = new Pimple\Container;
+ $this->container->register(new Kanboard\ServiceProvider\AuthenticationProvider);
$this->container->register(new Kanboard\ServiceProvider\DatabaseProvider);
$this->container->register(new Kanboard\ServiceProvider\ClassProvider);
+ $this->container->register(new Kanboard\ServiceProvider\NotificationProvider);
+ $this->container->register(new Kanboard\ServiceProvider\RouteProvider);
$this->container['dispatcher'] = new TraceableEventDispatcher(
new EventDispatcher,
diff --git a/tests/units/Core/OAuth2Test.php b/tests/units/Core/Http/OAuth2Test.php
index d5713608..d703dd7a 100644
--- a/tests/units/Core/OAuth2Test.php
+++ b/tests/units/Core/Http/OAuth2Test.php
@@ -1,8 +1,8 @@
<?php
-require_once __DIR__.'/../Base.php';
+require_once __DIR__.'/../../Base.php';
-use Kanboard\Core\OAuth2;
+use Kanboard\Core\Http\OAuth2;
class OAuth2Test extends Base
{
diff --git a/tests/units/Core/Http/RememberMeCookieTest.php b/tests/units/Core/Http/RememberMeCookieTest.php
new file mode 100644
index 00000000..ae5606ac
--- /dev/null
+++ b/tests/units/Core/Http/RememberMeCookieTest.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Kanboard\Core\Http;
+
+require_once __DIR__.'/../../Base.php';
+
+function setcookie($name, $value = "", $expire = 0, $path = "", $domain = "", $secure = false, $httponly = false)
+{
+ return RememberMeCookieTest::$functions->setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
+}
+
+class RememberMeCookieTest extends \Base
+{
+ public static $functions;
+
+ public function setUp()
+ {
+ parent::setup();
+
+ self::$functions = $this
+ ->getMockBuilder('stdClass')
+ ->setMethods(array(
+ 'setcookie',
+ ))
+ ->getMock();
+ }
+
+ public function tearDown()
+ {
+ parent::tearDown();
+ self::$functions = null;
+ }
+
+ public function testEncode()
+ {
+ $cookie = new RememberMeCookie($this->container);
+ $this->assertEquals('a|b', $cookie->encode('a', 'b'));
+ }
+
+ public function testDecode()
+ {
+ $cookie = new RememberMeCookie($this->container);
+ $this->assertEquals(array('token' => 'a', 'sequence' => 'b'), $cookie->decode('a|b'));
+ }
+
+ public function testHasCookie()
+ {
+ $this->container['request'] = new Request($this->container, array(), array(), array(), array(), array());
+
+ $cookie = new RememberMeCookie($this->container);
+ $this->assertFalse($cookie->hasCookie());
+
+ $this->container['request'] = new Request($this->container, array(), array(), array(), array(), array(RememberMeCookie::COOKIE_NAME => 'miam'));
+ $this->assertTrue($cookie->hasCookie());
+ }
+
+ public function testWrite()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('setcookie')
+ ->with(
+ RememberMeCookie::COOKIE_NAME,
+ 'myToken|mySequence',
+ 1234,
+ '',
+ '',
+ false,
+ true
+ )
+ ->will($this->returnValue(true));
+
+ $cookie = new RememberMeCookie($this->container);
+ $this->assertTrue($cookie->write('myToken', 'mySequence', 1234));
+ }
+
+ public function testRead()
+ {
+ $this->container['request'] = new Request($this->container, array(), array(), array(), array(), array());
+
+ $cookie = new RememberMeCookie($this->container);
+ $this->assertFalse($cookie->read());
+
+ $this->container['request'] = new Request($this->container, array(), array(), array(), array(), array(RememberMeCookie::COOKIE_NAME => 'T|S'));
+
+ $this->assertEquals(array('token' => 'T', 'sequence' => 'S'), $cookie->read());
+ }
+
+ public function testRemove()
+ {
+ self::$functions
+ ->expects($this->once())
+ ->method('setcookie')
+ ->with(
+ RememberMeCookie::COOKIE_NAME,
+ '',
+ time() - 3600,
+ '',
+ '',
+ false,
+ true
+ )
+ ->will($this->returnValue(true));
+
+ $cookie = new RememberMeCookie($this->container);
+ $this->assertTrue($cookie->remove());
+ }
+}
diff --git a/tests/units/Core/Http/RequestTest.php b/tests/units/Core/Http/RequestTest.php
new file mode 100644
index 00000000..217698f9
--- /dev/null
+++ b/tests/units/Core/Http/RequestTest.php
@@ -0,0 +1,175 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Http\Request;
+
+class RequestTest extends Base
+{
+ public function testGetStringParam()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEquals('', $request->getStringParam('myvar'));
+
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEquals('default', $request->getStringParam('myvar', 'default'));
+
+ $request = new Request($this->container, array(), array('myvar' => 'myvalue'), array(), array(), array());
+ $this->assertEquals('myvalue', $request->getStringParam('myvar'));
+ }
+
+ public function testGetIntegerParam()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEquals(0, $request->getIntegerParam('myvar'));
+
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEquals(5, $request->getIntegerParam('myvar', 5));
+
+ $request = new Request($this->container, array(), array('myvar' => 'myvalue'), array(), array(), array());
+ $this->assertEquals(0, $request->getIntegerParam('myvar'));
+
+ $request = new Request($this->container, array(), array('myvar' => '123'), array(), array(), array());
+ $this->assertEquals(123, $request->getIntegerParam('myvar'));
+ }
+
+ public function testGetValues()
+ {
+ $request = new Request($this->container, array(), array(), array('myvar' => 'myvalue'), array(), array());
+ $this->assertEmpty($request->getValue('myvar'));
+
+ $request = new Request($this->container, array(), array(), array('myvar' => 'myvalue', 'csrf_token' => $this->container['token']->getCSRFToken()), array(), array());
+ $this->assertEquals('myvalue', $request->getValue('myvar'));
+
+ $request = new Request($this->container, array(), array(), array('myvar' => 'myvalue', 'csrf_token' => $this->container['token']->getCSRFToken()), array(), array());
+ $this->assertEquals(array('myvar' => 'myvalue'), $request->getValues());
+ }
+
+ public function testGetFileContent()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEmpty($request->getFileContent('myfile'));
+
+ $filename = tempnam(sys_get_temp_dir(), 'UnitTest');
+ file_put_contents($filename, 'something');
+
+ $request = new Request($this->container, array(), array(), array(), array('myfile' => array('tmp_name' => $filename)), array());
+ $this->assertEquals('something', $request->getFileContent('myfile'));
+
+ unlink($filename);
+ }
+
+ public function testGetFilePath()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEmpty($request->getFilePath('myfile'));
+
+ $request = new Request($this->container, array(), array(), array(), array('myfile' => array('tmp_name' => 'somewhere')), array());
+ $this->assertEquals('somewhere', $request->getFilePath('myfile'));
+ }
+
+ public function testIsPost()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertFalse($request->isPost());
+
+ $request = new Request($this->container, array('REQUEST_METHOD' => 'POST'), array(), array(), array(), array());
+ $this->assertTrue($request->isPost());
+ }
+
+ public function testIsAjax()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertFalse($request->isAjax());
+
+ $request = new Request($this->container, array('HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'), array(), array(), array(), array());
+ $this->assertTrue($request->isAjax());
+ }
+
+ public function testIsHTTPS()
+ {
+ $request = new Request($this->container, array(), array(), array(), array());
+ $this->assertFalse($request->isHTTPS());
+
+ $request = new Request($this->container, array('HTTPS' => ''), array(), array(), array(), array());
+ $this->assertFalse($request->isHTTPS());
+
+ $request = new Request($this->container, array('HTTPS' => 'off'), array(), array(), array(), array());
+ $this->assertFalse($request->isHTTPS());
+
+ $request = new Request($this->container, array('HTTPS' => 'on'), array(), array(), array(), array());
+ $this->assertTrue($request->isHTTPS());
+
+ $request = new Request($this->container, array('HTTPS' => '1'), array(), array(), array(), array());
+ $this->assertTrue($request->isHTTPS());
+ }
+
+ public function testGetCookie()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEmpty($request->getCookie('mycookie'));
+
+ $request = new Request($this->container, array(), array(), array(), array(), array('mycookie' => 'miam'));
+ $this->assertEquals('miam', $request->getCookie('mycookie'));
+ }
+
+ public function testGetHeader()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEmpty($request->getHeader('X-Forwarded-For'));
+
+ $request = new Request($this->container, array('HTTP_X_FORWARDED_FOR' => 'test'), array(), array(), array(), array());
+ $this->assertEquals('test', $request->getHeader('X-Forwarded-For'));
+ }
+
+ public function testGetRemoteUser()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEmpty($request->getRemoteUser());
+
+ $request = new Request($this->container, array(REVERSE_PROXY_USER_HEADER => 'test'), array(), array(), array(), array());
+ $this->assertEquals('test', $request->getRemoteUser());
+ }
+
+ public function testGetQueryString()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEmpty($request->getQueryString());
+
+ $request = new Request($this->container, array('QUERY_STRING' => 'k=v'), array(), array(), array(), array());
+ $this->assertEquals('k=v', $request->getQueryString());
+ }
+
+ public function testGetUri()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEmpty($request->getUri());
+
+ $request = new Request($this->container, array('REQUEST_URI' => '/blah'), array(), array(), array(), array());
+ $this->assertEquals('/blah', $request->getUri());
+ }
+
+ public function testGetUserAgent()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEquals('Unknown', $request->getUserAgent());
+
+ $request = new Request($this->container, array('HTTP_USER_AGENT' => 'My browser'), array(), array(), array(), array());
+ $this->assertEquals('My browser', $request->getUserAgent());
+ }
+
+ public function testGetIpAddress()
+ {
+ $request = new Request($this->container, array(), array(), array(), array(), array());
+ $this->assertEquals('Unknown', $request->getIpAddress());
+
+ $request = new Request($this->container, array('HTTP_X_FORWARDED_FOR' => '192.168.0.1,127.0.0.1'), array(), array(), array(), array());
+ $this->assertEquals('192.168.0.1', $request->getIpAddress());
+
+ $request = new Request($this->container, array('REMOTE_ADDR' => '192.168.0.1'), array(), array(), array(), array());
+ $this->assertEquals('192.168.0.1', $request->getIpAddress());
+
+ $request = new Request($this->container, array('REMOTE_ADDR' => ''), array(), array(), array(), array());
+ $this->assertEquals('Unknown', $request->getIpAddress());
+ }
+}
diff --git a/tests/units/Core/Ldap/ClientTest.php b/tests/units/Core/Ldap/ClientTest.php
index 7b6e983d..d149500e 100644
--- a/tests/units/Core/Ldap/ClientTest.php
+++ b/tests/units/Core/Ldap/ClientTest.php
@@ -49,6 +49,13 @@ class ClientTest extends \Base
self::$functions = null;
}
+ public function testGetLdapServerNotConfigured()
+ {
+ $this->setExpectedException('\LogicException');
+ $ldap = new Client;
+ $ldap->getLdapServer();
+ }
+
public function testConnectSuccess()
{
self::$functions
@@ -61,7 +68,8 @@ class ClientTest extends \Base
->will($this->returnValue('my_ldap_resource'));
$ldap = new Client;
- $this->assertEquals('my_ldap_resource', $ldap->getConnection('my_ldap_server'));
+ $ldap->open('my_ldap_server');
+ $this->assertEquals('my_ldap_resource', $ldap->getConnection());
}
public function testConnectFailure()
@@ -78,7 +86,8 @@ class ClientTest extends \Base
$this->setExpectedException('\Kanboard\Core\Ldap\ClientException');
$ldap = new Client;
- $this->assertNotEquals('my_ldap_resource', $ldap->getConnection('my_ldap_server'));
+ $ldap->open('my_ldap_server');
+ $this->assertNotEquals('my_ldap_resource', $ldap->getConnection());
}
public function testConnectSuccessWithTLS()
@@ -101,7 +110,8 @@ class ClientTest extends \Base
->will($this->returnValue(true));
$ldap = new Client;
- $this->assertEquals('my_ldap_resource', $ldap->getConnection('my_ldap_server', 389, true));
+ $ldap->open('my_ldap_server', 389, true);
+ $this->assertEquals('my_ldap_resource', $ldap->getConnection());
}
public function testConnectFailureWithTLS()
@@ -126,7 +136,8 @@ class ClientTest extends \Base
$this->setExpectedException('\Kanboard\Core\Ldap\ClientException');
$ldap = new Client;
- $this->assertNotEquals('my_ldap_resource', $ldap->getConnection('my_ldap_server', 389, true));
+ $ldap->open('my_ldap_server', 389, true);
+ $this->assertNotEquals('my_ldap_resource', $ldap->getConnection());
}
public function testAnonymousAuthenticationSuccess()
@@ -134,13 +145,10 @@ class ClientTest extends \Base
self::$functions
->expects($this->once())
->method('ldap_bind')
- ->with(
- $this->equalTo('my_ldap_resource')
- )
->will($this->returnValue(true));
$ldap = new Client;
- $this->assertTrue($ldap->useAnonymousAuthentication('my_ldap_resource'));
+ $this->assertTrue($ldap->useAnonymousAuthentication());
}
public function testAnonymousAuthenticationFailure()
@@ -148,21 +156,27 @@ class ClientTest extends \Base
self::$functions
->expects($this->once())
->method('ldap_bind')
- ->with(
- $this->equalTo('my_ldap_resource')
- )
->will($this->returnValue(false));
$this->setExpectedException('\Kanboard\Core\Ldap\ClientException');
$ldap = new Client;
- $ldap->useAnonymousAuthentication('my_ldap_resource');
+ $ldap->useAnonymousAuthentication();
}
public function testUserAuthenticationSuccess()
{
self::$functions
->expects($this->once())
+ ->method('ldap_connect')
+ ->with(
+ $this->equalTo('my_ldap_server'),
+ $this->equalTo(389)
+ )
+ ->will($this->returnValue('my_ldap_resource'));
+
+ self::$functions
+ ->expects($this->once())
->method('ldap_bind')
->with(
$this->equalTo('my_ldap_resource'),
@@ -172,13 +186,23 @@ class ClientTest extends \Base
->will($this->returnValue(true));
$ldap = new Client;
- $this->assertTrue($ldap->authenticate('my_ldap_resource', 'my_ldap_user', 'my_ldap_password'));
+ $ldap->open('my_ldap_server');
+ $this->assertTrue($ldap->authenticate('my_ldap_user', 'my_ldap_password'));
}
public function testUserAuthenticationFailure()
{
self::$functions
->expects($this->once())
+ ->method('ldap_connect')
+ ->with(
+ $this->equalTo('my_ldap_server'),
+ $this->equalTo(389)
+ )
+ ->will($this->returnValue('my_ldap_resource'));
+
+ self::$functions
+ ->expects($this->once())
->method('ldap_bind')
->with(
$this->equalTo('my_ldap_resource'),
@@ -190,6 +214,7 @@ class ClientTest extends \Base
$this->setExpectedException('\Kanboard\Core\Ldap\ClientException');
$ldap = new Client;
- $ldap->authenticate('my_ldap_resource', 'my_ldap_user', 'my_ldap_password');
+ $ldap->open('my_ldap_server');
+ $ldap->authenticate('my_ldap_user', 'my_ldap_password');
}
}
diff --git a/tests/units/Core/Ldap/EntriesTest.php b/tests/units/Core/Ldap/EntriesTest.php
new file mode 100644
index 00000000..65025b6e
--- /dev/null
+++ b/tests/units/Core/Ldap/EntriesTest.php
@@ -0,0 +1,55 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Ldap\Entries;
+
+class EntriesTest extends Base
+{
+ private $entries = array(
+ 'count' => 2,
+ 0 => array(
+ 'cn' => array(
+ 'count' => 1,
+ 0 => 'Kanboard Other Group',
+ ),
+ 0 => 'cn',
+ 'count' => 1,
+ 'dn' => 'CN=Kanboard Other Group,CN=Users,DC=kanboard,DC=local',
+ ),
+ 1 => array(
+ 'cn' => array(
+ 'count' => 1,
+ 0 => 'Kanboard Users',
+ ),
+ 0 => 'cn',
+ 'count' => 1,
+ 'dn' => 'CN=Kanboard Users,CN=Users,DC=kanboard,DC=local',
+ ),
+ );
+
+ public function testGetAll()
+ {
+ $entries = new Entries(array());
+ $this->assertEmpty($entries->getAll());
+
+ $entries = new Entries($this->entries);
+ $result = $entries->getAll();
+ $this->assertCount(2, $result);
+ $this->assertInstanceOf('Kanboard\Core\Ldap\Entry', $result[0]);
+ $this->assertEquals('CN=Kanboard Users,CN=Users,DC=kanboard,DC=local', $result[1]->getDn());
+ $this->assertEquals('Kanboard Users', $result[1]->getFirstValue('cn'));
+ }
+
+ public function testGetFirst()
+ {
+ $entries = new Entries(array());
+ $this->assertEquals('', $entries->getFirstEntry()->getDn());
+
+ $entries = new Entries($this->entries);
+ $result = $entries->getFirstEntry();
+ $this->assertInstanceOf('Kanboard\Core\Ldap\Entry', $result);
+ $this->assertEquals('CN=Kanboard Other Group,CN=Users,DC=kanboard,DC=local', $result->getDn());
+ $this->assertEquals('Kanboard Other Group', $result->getFirstValue('cn'));
+ }
+}
diff --git a/tests/units/Core/Ldap/EntryTest.php b/tests/units/Core/Ldap/EntryTest.php
new file mode 100644
index 00000000..45585e77
--- /dev/null
+++ b/tests/units/Core/Ldap/EntryTest.php
@@ -0,0 +1,71 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Ldap\Entry;
+
+class EntryTest extends Base
+{
+ private $entry = array(
+ 'count' => 2,
+ 'dn' => 'uid=my_user,ou=People,dc=kanboard,dc=local',
+ 'displayname' => array(
+ 'count' => 1,
+ 0 => 'My LDAP user',
+ ),
+ 'broken' => array(
+ ),
+ 'mail' => array(
+ 'count' => 2,
+ 0 => 'user1@localhost',
+ 1 => 'user2@localhost',
+ ),
+ 'samaccountname' => array(
+ 'count' => 1,
+ 0 => 'my_ldap_user',
+ ),
+ 0 => 'displayname',
+ 1 => 'mail',
+ 2 => 'samaccountname',
+ );
+
+ public function testGetAll()
+ {
+ $expected = array(
+ 'user1@localhost',
+ 'user2@localhost',
+ );
+
+ $entry = new Entry($this->entry);
+ $this->assertEquals($expected, $entry->getAll('mail'));
+ $this->assertEmpty($entry->getAll('not found'));
+ $this->assertEmpty($entry->getAll('broken'));
+ }
+
+ public function testGetFirst()
+ {
+ $entry = new Entry($this->entry);
+ $this->assertEquals('user1@localhost', $entry->getFirstValue('mail'));
+ $this->assertEquals('', $entry->getFirstValue('not found'));
+ $this->assertEquals('default', $entry->getFirstValue('not found', 'default'));
+ $this->assertEquals('default', $entry->getFirstValue('broken', 'default'));
+ }
+
+ public function testGetDn()
+ {
+ $entry = new Entry($this->entry);
+ $this->assertEquals('uid=my_user,ou=People,dc=kanboard,dc=local', $entry->getDn());
+
+ $entry = new Entry(array());
+ $this->assertEquals('', $entry->getDn());
+ }
+
+ public function testHasValue()
+ {
+ $entry = new Entry($this->entry);
+ $this->assertTrue($entry->hasValue('mail', 'user2@localhost'));
+ $this->assertFalse($entry->hasValue('mail', 'user3@localhost'));
+ $this->assertTrue($entry->hasValue('displayname', 'My LDAP user'));
+ $this->assertFalse($entry->hasValue('displayname', 'Something else'));
+ }
+}
diff --git a/tests/units/Core/Ldap/LdapGroupTest.php b/tests/units/Core/Ldap/LdapGroupTest.php
new file mode 100644
index 00000000..3f538249
--- /dev/null
+++ b/tests/units/Core/Ldap/LdapGroupTest.php
@@ -0,0 +1,160 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Ldap\Group;
+use Kanboard\Core\Ldap\Entries;
+use Kanboard\Core\Security\Role;
+
+class LdapGroupTest extends Base
+{
+ private $query;
+ private $client;
+ private $group;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->client = $this
+ ->getMockBuilder('\Kanboard\Core\Ldap\Client')
+ ->setMethods(array(
+ 'getConnection',
+ ))
+ ->getMock();
+
+ $this->query = $this
+ ->getMockBuilder('\Kanboard\Core\Ldap\Query')
+ ->setConstructorArgs(array($this->client))
+ ->setMethods(array(
+ 'execute',
+ 'hasResult',
+ 'getEntries',
+ ))
+ ->getMock();
+
+ $this->group = $this
+ ->getMockBuilder('\Kanboard\Core\Ldap\Group')
+ ->setConstructorArgs(array($this->query))
+ ->setMethods(array(
+ 'getAttributeName',
+ 'getBasDn',
+ ))
+ ->getMock();
+ }
+
+ public function testGetGroups()
+ {
+ $entries = new Entries(array(
+ 'count' => 2,
+ 0 => array(
+ 'cn' => array(
+ 'count' => 1,
+ 0 => 'Kanboard Other Group',
+ ),
+ 0 => 'cn',
+ 'count' => 1,
+ 'dn' => 'CN=Kanboard Other Group,CN=Users,DC=kanboard,DC=local',
+ ),
+ 1 => array(
+ 'cn' => array(
+ 'count' => 1,
+ 0 => 'Kanboard Users',
+ ),
+ 0 => 'cn',
+ 'count' => 1,
+ 'dn' => 'CN=Kanboard Users,CN=Users,DC=kanboard,DC=local',
+ ),
+ ));
+
+ $this->client
+ ->expects($this->any())
+ ->method('getConnection')
+ ->will($this->returnValue('my_ldap_resource'));
+
+ $this->query
+ ->expects($this->once())
+ ->method('execute')
+ ->with(
+ $this->equalTo('CN=Users,DC=kanboard,DC=local'),
+ $this->equalTo('(&(objectClass=group)(sAMAccountName=Kanboard*))')
+ );
+
+ $this->query
+ ->expects($this->once())
+ ->method('hasResult')
+ ->will($this->returnValue(true));
+
+ $this->query
+ ->expects($this->once())
+ ->method('getEntries')
+ ->will($this->returnValue($entries));
+
+ $this->group
+ ->expects($this->any())
+ ->method('getAttributeName')
+ ->will($this->returnValue('cn'));
+
+ $this->group
+ ->expects($this->any())
+ ->method('getBasDn')
+ ->will($this->returnValue('CN=Users,DC=kanboard,DC=local'));
+
+ $groups = $this->group->find('(&(objectClass=group)(sAMAccountName=Kanboard*))');
+ $this->assertCount(2, $groups);
+ $this->assertInstanceOf('Kanboard\Group\LdapGroupProvider', $groups[0]);
+ $this->assertInstanceOf('Kanboard\Group\LdapGroupProvider', $groups[1]);
+ $this->assertEquals('Kanboard Other Group', $groups[0]->getName());
+ $this->assertEquals('Kanboard Users', $groups[1]->getName());
+ $this->assertEquals('CN=Kanboard Other Group,CN=Users,DC=kanboard,DC=local', $groups[0]->getExternalId());
+ $this->assertEquals('CN=Kanboard Users,CN=Users,DC=kanboard,DC=local', $groups[1]->getExternalId());
+ }
+
+ public function testGetGroupsWithNoResult()
+ {
+ $entries = new Entries(array());
+
+ $this->client
+ ->expects($this->any())
+ ->method('getConnection')
+ ->will($this->returnValue('my_ldap_resource'));
+
+ $this->query
+ ->expects($this->once())
+ ->method('execute')
+ ->with(
+ $this->equalTo('CN=Users,DC=kanboard,DC=local'),
+ $this->equalTo('(&(objectClass=group)(sAMAccountName=Kanboard*))')
+ );
+
+ $this->query
+ ->expects($this->once())
+ ->method('hasResult')
+ ->will($this->returnValue(false));
+
+ $this->query
+ ->expects($this->never())
+ ->method('getEntries');
+
+ $this->group
+ ->expects($this->any())
+ ->method('getAttributeName')
+ ->will($this->returnValue('cn'));
+
+ $this->group
+ ->expects($this->any())
+ ->method('getBasDn')
+ ->will($this->returnValue('CN=Users,DC=kanboard,DC=local'));
+
+ $groups = $this->group->find('(&(objectClass=group)(sAMAccountName=Kanboard*))');
+ $this->assertCount(0, $groups);
+ }
+
+ public function testGetBaseDnNotConfigured()
+ {
+ $this->setExpectedException('\LogicException');
+
+ $group = new Group($this->query);
+ $group->getBasDn();
+ }
+}
diff --git a/tests/units/Core/Ldap/LdapUserTest.php b/tests/units/Core/Ldap/LdapUserTest.php
new file mode 100644
index 00000000..2b3db1e5
--- /dev/null
+++ b/tests/units/Core/Ldap/LdapUserTest.php
@@ -0,0 +1,379 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Ldap\User;
+use Kanboard\Core\Ldap\Entries;
+use Kanboard\Core\Security\Role;
+
+class LdapUserTest extends Base
+{
+ private $query;
+ private $client;
+ private $user;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->client = $this
+ ->getMockBuilder('\Kanboard\Core\Ldap\Client')
+ ->setMethods(array(
+ 'getConnection',
+ ))
+ ->getMock();
+
+ $this->query = $this
+ ->getMockBuilder('\Kanboard\Core\Ldap\Query')
+ ->setConstructorArgs(array($this->client))
+ ->setMethods(array(
+ 'execute',
+ 'hasResult',
+ 'getEntries',
+ ))
+ ->getMock();
+
+ $this->user = $this
+ ->getMockBuilder('\Kanboard\Core\Ldap\User')
+ ->setConstructorArgs(array($this->query))
+ ->setMethods(array(
+ 'getAttributeUsername',
+ 'getAttributeEmail',
+ 'getAttributeName',
+ 'getAttributeGroup',
+ 'getGroupAdminDn',
+ 'getGroupManagerDn',
+ 'getBasDn',
+ ))
+ ->getMock();
+ }
+
+ public function testGetUser()
+ {
+ $entries = new Entries(array(
+ 'count' => 1,
+ 0 => array(
+ 'count' => 2,
+ 'dn' => 'uid=my_ldap_user,ou=People,dc=kanboard,dc=local',
+ 'displayname' => array(
+ 'count' => 1,
+ 0 => 'My LDAP user',
+ ),
+ 'mail' => array(
+ 'count' => 2,
+ 0 => 'user1@localhost',
+ 1 => 'user2@localhost',
+ ),
+ 'samaccountname' => array(
+ 'count' => 1,
+ 0 => 'my_ldap_user',
+ ),
+ 0 => 'displayname',
+ 1 => 'mail',
+ 2 => 'samaccountname',
+ )
+ ));
+
+ $this->client
+ ->expects($this->any())
+ ->method('getConnection')
+ ->will($this->returnValue('my_ldap_resource'));
+
+ $this->query
+ ->expects($this->once())
+ ->method('execute')
+ ->with(
+ $this->equalTo('ou=People,dc=kanboard,dc=local'),
+ $this->equalTo('(uid=my_ldap_user)')
+ );
+
+ $this->query
+ ->expects($this->once())
+ ->method('hasResult')
+ ->will($this->returnValue(true));
+
+ $this->query
+ ->expects($this->once())
+ ->method('getEntries')
+ ->will($this->returnValue($entries));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeUsername')
+ ->will($this->returnValue('samaccountname'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeName')
+ ->will($this->returnValue('displayname'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeEmail')
+ ->will($this->returnValue('mail'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getBasDn')
+ ->will($this->returnValue('ou=People,dc=kanboard,dc=local'));
+
+ $user = $this->user->find('(uid=my_ldap_user)');
+ $this->assertInstanceOf('Kanboard\User\LdapUserProvider', $user);
+ $this->assertEquals('uid=my_ldap_user,ou=People,dc=kanboard,dc=local', $user->getDn());
+ $this->assertEquals('my_ldap_user', $user->getUsername());
+ $this->assertEquals('My LDAP user', $user->getName());
+ $this->assertEquals('user1@localhost', $user->getEmail());
+ $this->assertEquals(Role::APP_USER, $user->getRole());
+ $this->assertEquals(array(), $user->getExternalGroupIds());
+ $this->assertEquals(array('is_ldap_user' => 1), $user->getExtraAttributes());
+ }
+
+ public function testGetUserWithAdminRole()
+ {
+ $entries = new Entries(array(
+ 'count' => 1,
+ 0 => array(
+ 'count' => 2,
+ 'dn' => 'uid=my_ldap_user,ou=People,dc=kanboard,dc=local',
+ 'displayname' => array(
+ 'count' => 1,
+ 0 => 'My LDAP user',
+ ),
+ 'mail' => array(
+ 'count' => 2,
+ 0 => 'user1@localhost',
+ 1 => 'user2@localhost',
+ ),
+ 'samaccountname' => array(
+ 'count' => 1,
+ 0 => 'my_ldap_user',
+ ),
+ 'memberof' => array(
+ 'count' => 1,
+ 0 => 'CN=Kanboard-Admins,CN=Users,DC=kanboard,DC=local',
+ ),
+ 0 => 'displayname',
+ 1 => 'mail',
+ 2 => 'samaccountname',
+ 3 => 'memberof',
+ )
+ ));
+
+ $this->client
+ ->expects($this->any())
+ ->method('getConnection')
+ ->will($this->returnValue('my_ldap_resource'));
+
+ $this->query
+ ->expects($this->once())
+ ->method('execute')
+ ->with(
+ $this->equalTo('ou=People,dc=kanboard,dc=local'),
+ $this->equalTo('(uid=my_ldap_user)')
+ );
+
+ $this->query
+ ->expects($this->once())
+ ->method('hasResult')
+ ->will($this->returnValue(true));
+
+ $this->query
+ ->expects($this->once())
+ ->method('getEntries')
+ ->will($this->returnValue($entries));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeUsername')
+ ->will($this->returnValue('samaccountname'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeName')
+ ->will($this->returnValue('displayname'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeEmail')
+ ->will($this->returnValue('mail'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeGroup')
+ ->will($this->returnValue('memberof'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getGroupAdminDn')
+ ->will($this->returnValue('CN=Kanboard-Admins,CN=Users,DC=kanboard,DC=local'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getBasDn')
+ ->will($this->returnValue('ou=People,dc=kanboard,dc=local'));
+
+ $user = $this->user->find('(uid=my_ldap_user)');
+ $this->assertInstanceOf('Kanboard\User\LdapUserProvider', $user);
+ $this->assertEquals('uid=my_ldap_user,ou=People,dc=kanboard,dc=local', $user->getDn());
+ $this->assertEquals('my_ldap_user', $user->getUsername());
+ $this->assertEquals('My LDAP user', $user->getName());
+ $this->assertEquals('user1@localhost', $user->getEmail());
+ $this->assertEquals(Role::APP_ADMIN, $user->getRole());
+ $this->assertEquals(array('CN=Kanboard-Admins,CN=Users,DC=kanboard,DC=local'), $user->getExternalGroupIds());
+ $this->assertEquals(array('is_ldap_user' => 1), $user->getExtraAttributes());
+ }
+
+ public function testGetUserWithManagerRole()
+ {
+ $entries = new Entries(array(
+ 'count' => 1,
+ 0 => array(
+ 'count' => 2,
+ 'dn' => 'uid=my_ldap_user,ou=People,dc=kanboard,dc=local',
+ 'displayname' => array(
+ 'count' => 1,
+ 0 => 'My LDAP user',
+ ),
+ 'mail' => array(
+ 'count' => 2,
+ 0 => 'user1@localhost',
+ 1 => 'user2@localhost',
+ ),
+ 'samaccountname' => array(
+ 'count' => 1,
+ 0 => 'my_ldap_user',
+ ),
+ 'memberof' => array(
+ 'count' => 2,
+ 0 => 'CN=Kanboard-Users,CN=Users,DC=kanboard,DC=local',
+ 1 => 'CN=Kanboard-Managers,CN=Users,DC=kanboard,DC=local',
+ ),
+ 0 => 'displayname',
+ 1 => 'mail',
+ 2 => 'samaccountname',
+ 3 => 'memberof',
+ )
+ ));
+
+ $this->client
+ ->expects($this->any())
+ ->method('getConnection')
+ ->will($this->returnValue('my_ldap_resource'));
+
+ $this->query
+ ->expects($this->once())
+ ->method('execute')
+ ->with(
+ $this->equalTo('ou=People,dc=kanboard,dc=local'),
+ $this->equalTo('(uid=my_ldap_user)')
+ );
+
+ $this->query
+ ->expects($this->once())
+ ->method('hasResult')
+ ->will($this->returnValue(true));
+
+ $this->query
+ ->expects($this->once())
+ ->method('getEntries')
+ ->will($this->returnValue($entries));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeUsername')
+ ->will($this->returnValue('samaccountname'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeName')
+ ->will($this->returnValue('displayname'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeEmail')
+ ->will($this->returnValue('mail'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeGroup')
+ ->will($this->returnValue('memberof'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getGroupManagerDn')
+ ->will($this->returnValue('CN=Kanboard-Managers,CN=Users,DC=kanboard,DC=local'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getBasDn')
+ ->will($this->returnValue('ou=People,dc=kanboard,dc=local'));
+
+ $user = $this->user->find('(uid=my_ldap_user)');
+ $this->assertInstanceOf('Kanboard\User\LdapUserProvider', $user);
+ $this->assertEquals('uid=my_ldap_user,ou=People,dc=kanboard,dc=local', $user->getDn());
+ $this->assertEquals('my_ldap_user', $user->getUsername());
+ $this->assertEquals('My LDAP user', $user->getName());
+ $this->assertEquals('user1@localhost', $user->getEmail());
+ $this->assertEquals(Role::APP_MANAGER, $user->getRole());
+ $this->assertEquals(array('CN=Kanboard-Users,CN=Users,DC=kanboard,DC=local', 'CN=Kanboard-Managers,CN=Users,DC=kanboard,DC=local'), $user->getExternalGroupIds());
+ $this->assertEquals(array('is_ldap_user' => 1), $user->getExtraAttributes());
+ }
+
+ public function testGetUserNotFound()
+ {
+ $entries = new Entries(array());
+
+ $this->client
+ ->expects($this->any())
+ ->method('getConnection')
+ ->will($this->returnValue('my_ldap_resource'));
+
+ $this->query
+ ->expects($this->once())
+ ->method('execute')
+ ->with(
+ $this->equalTo('ou=People,dc=kanboard,dc=local'),
+ $this->equalTo('(uid=my_ldap_user)')
+ );
+
+ $this->query
+ ->expects($this->once())
+ ->method('hasResult')
+ ->will($this->returnValue(false));
+
+ $this->query
+ ->expects($this->never())
+ ->method('getEntries');
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeUsername')
+ ->will($this->returnValue('samaccountname'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeName')
+ ->will($this->returnValue('displayname'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getAttributeEmail')
+ ->will($this->returnValue('mail'));
+
+ $this->user
+ ->expects($this->any())
+ ->method('getBasDn')
+ ->will($this->returnValue('ou=People,dc=kanboard,dc=local'));
+
+ $user = $this->user->find('(uid=my_ldap_user)');
+ $this->assertEquals(null, $user);
+ }
+
+ public function testGetBaseDnNotConfigured()
+ {
+ $this->setExpectedException('\LogicException');
+
+ $user = new User($this->query);
+ $user->getBasDn();
+ }
+}
diff --git a/tests/units/Core/Ldap/QueryTest.php b/tests/units/Core/Ldap/QueryTest.php
index 2eb3940f..b3987df0 100644
--- a/tests/units/Core/Ldap/QueryTest.php
+++ b/tests/units/Core/Ldap/QueryTest.php
@@ -17,6 +17,7 @@ function ldap_get_entries($link_identifier, $result_identifier)
class QueryTest extends \Base
{
public static $functions;
+ private $client;
public function setUp()
{
@@ -29,6 +30,13 @@ class QueryTest extends \Base
'ldap_get_entries',
))
->getMock();
+
+ $this->client = $this
+ ->getMockBuilder('\Kanboard\Core\Ldap\Client')
+ ->setMethods(array(
+ 'getConnection',
+ ))
+ ->getMock();
}
public function tearDown()
@@ -58,6 +66,11 @@ class QueryTest extends \Base
)
);
+ $this->client
+ ->expects($this->any())
+ ->method('getConnection')
+ ->will($this->returnValue('my_ldap_resource'));
+
self::$functions
->expects($this->once())
->method('ldap_search')
@@ -78,20 +91,25 @@ class QueryTest extends \Base
)
->will($this->returnValue($entries));
- $query = new Query;
- $query->execute('my_ldap_resource', 'ou=People,dc=kanboard,dc=local', 'uid=my_user', array('displayname'));
+ $query = new Query($this->client);
+ $query->execute('ou=People,dc=kanboard,dc=local', 'uid=my_user', array('displayname'));
$this->assertTrue($query->hasResult());
- $this->assertEquals('My user', $query->getAttributeValue('displayname'));
- $this->assertEquals('user1@localhost', $query->getAttributeValue('mail'));
- $this->assertEquals('', $query->getAttributeValue('not_found'));
+ $this->assertEquals('My user', $query->getEntries()->getFirstEntry()->getFirstValue('displayname'));
+ $this->assertEquals('user1@localhost', $query->getEntries()->getFirstEntry()->getFirstValue('mail'));
+ $this->assertEquals('', $query->getEntries()->getFirstEntry()->getFirstValue('not_found'));
- $this->assertEquals('uid=my_user,ou=People,dc=kanboard,dc=local', $query->getAttribute('dn'));
- $this->assertEquals(null, $query->getAttribute('missing'));
+ $this->assertEquals('uid=my_user,ou=People,dc=kanboard,dc=local', $query->getEntries()->getFirstEntry()->getDn());
+ $this->assertEquals('', $query->getEntries()->getFirstEntry()->getFirstValue('missing'));
}
public function testExecuteQueryNotFound()
{
+ $this->client
+ ->expects($this->any())
+ ->method('getConnection')
+ ->will($this->returnValue('my_ldap_resource'));
+
self::$functions
->expects($this->once())
->method('ldap_search')
@@ -112,13 +130,18 @@ class QueryTest extends \Base
)
->will($this->returnValue(array()));
- $query = new Query;
- $query->execute('my_ldap_resource', 'ou=People,dc=kanboard,dc=local', 'uid=my_user', array('displayname'));
+ $query = new Query($this->client);
+ $query->execute('ou=People,dc=kanboard,dc=local', 'uid=my_user', array('displayname'));
$this->assertFalse($query->hasResult());
}
public function testExecuteQueryFailed()
{
+ $this->client
+ ->expects($this->once())
+ ->method('getConnection')
+ ->will($this->returnValue('my_ldap_resource'));
+
self::$functions
->expects($this->once())
->method('ldap_search')
@@ -130,8 +153,8 @@ class QueryTest extends \Base
)
->will($this->returnValue(false));
- $query = new Query;
- $query->execute('my_ldap_resource', 'ou=People,dc=kanboard,dc=local', 'uid=my_user', array('displayname'));
+ $query = new Query($this->client);
+ $query->execute('ou=People,dc=kanboard,dc=local', 'uid=my_user', array('displayname'));
$this->assertFalse($query->hasResult());
}
}
diff --git a/tests/units/Core/Ldap/UserTest.php b/tests/units/Core/Ldap/UserTest.php
deleted file mode 100644
index 56cc588c..00000000
--- a/tests/units/Core/Ldap/UserTest.php
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-
-namespace Kanboard\Core\Ldap;
-
-require_once __DIR__.'/../../Base.php';
-
-class UserTest extends \Base
-{
- public function testGetProfile()
- {
- $entries = array(
- 'count' => 1,
- 0 => array(
- 'count' => 2,
- 'dn' => 'uid=my_user,ou=People,dc=kanboard,dc=local',
- 'displayname' => array(
- 'count' => 1,
- 0 => 'My LDAP user',
- ),
- 'mail' => array(
- 'count' => 2,
- 0 => 'user1@localhost',
- 1 => 'user2@localhost',
- ),
- 'samaccountname' => array(
- 'count' => 1,
- 0 => 'my_ldap_user',
- ),
- 0 => 'displayname',
- 1 => 'mail',
- 2 => 'samaccountname',
- )
- );
-
- $expected = array(
- 'ldap_id' => 'uid=my_user,ou=People,dc=kanboard,dc=local',
- 'username' => 'my_ldap_user',
- 'name' => 'My LDAP user',
- 'email' => 'user1@localhost',
- 'is_admin' => 0,
- 'is_project_admin' => 0,
- 'is_ldap_user' => 1,
- );
-
- $query = $this
- ->getMockBuilder('\Kanboard\Core\Ldap\Query')
- ->setConstructorArgs(array($entries))
- ->setMethods(array(
- 'execute',
- 'hasResult',
- ))
- ->getMock();
-
- $query
- ->expects($this->once())
- ->method('execute')
- ->with(
- $this->equalTo('my_ldap_resource'),
- $this->equalTo('ou=People,dc=kanboard,dc=local'),
- $this->equalTo('(uid=my_user)')
- );
-
- $query
- ->expects($this->once())
- ->method('hasResult')
- ->will($this->returnValue(true));
-
- $user = $this
- ->getMockBuilder('\Kanboard\Core\Ldap\User')
- ->setConstructorArgs(array($query))
- ->setMethods(array(
- 'getAttributeUsername',
- 'getAttributeEmail',
- 'getAttributeName',
- ))
- ->getMock();
-
- $user
- ->expects($this->any())
- ->method('getAttributeUsername')
- ->will($this->returnValue('samaccountname'));
-
- $user
- ->expects($this->any())
- ->method('getAttributeName')
- ->will($this->returnValue('displayname'));
-
- $user
- ->expects($this->any())
- ->method('getAttributeEmail')
- ->will($this->returnValue('mail'));
-
- $this->assertEquals($expected, $user->getProfile('my_ldap_resource', 'ou=People,dc=kanboard,dc=local', '(uid=my_user)'));
- }
-}
diff --git a/tests/units/Core/Security/AccessMapTest.php b/tests/units/Core/Security/AccessMapTest.php
index ab74e036..61693ce8 100644
--- a/tests/units/Core/Security/AccessMapTest.php
+++ b/tests/units/Core/Security/AccessMapTest.php
@@ -6,17 +6,34 @@ use Kanboard\Core\Security\AccessMap;
class AccessMapTest extends Base
{
- public function testGetRoles()
+ public function testRoleHierarchy()
+ {
+ $acl = new AccessMap;
+ $acl->setRoleHierarchy('admin', array('manager', 'user'));
+ $acl->setRoleHierarchy('manager', array('user'));
+
+ $this->assertEquals(array('admin'), $acl->getRoleHierarchy('admin'));
+ $this->assertEquals(array('manager', 'admin'), $acl->getRoleHierarchy('manager'));
+ $this->assertEquals(array('user', 'admin', 'manager'), $acl->getRoleHierarchy('user'));
+ }
+
+ public function testAddRulesAndGetRoles()
{
$acl = new AccessMap;
$acl->setDefaultRole('role3');
- $acl->add('MyController', 'myAction1', array('role1', 'role2'));
- $acl->add('MyController', 'myAction2', array('role1'));
- $acl->add('MyAdminController', '*', array('role2'));
+ $acl->setRoleHierarchy('role2', array('role1'));
+
+ $acl->add('MyController', 'myAction1', 'role2');
+ $acl->add('MyController', 'myAction2', 'role1');
+ $acl->add('MyAdminController', '*', 'role2');
+ $acl->add('SomethingElse', array('actionA', 'actionB'), 'role2');
- $this->assertEquals(array('role1', 'role2'), $acl->getRoles('mycontroller', 'MyAction1'));
- $this->assertEquals(array('role1'), $acl->getRoles('mycontroller', 'MyAction2'));
+ $this->assertEquals(array('role2'), $acl->getRoles('mycontroller', 'MyAction1'));
+ $this->assertEquals(array('role1', 'role2'), $acl->getRoles('mycontroller', 'MyAction2'));
$this->assertEquals(array('role2'), $acl->getRoles('Myadmincontroller', 'MyAction'));
$this->assertEquals(array('role3'), $acl->getRoles('AnotherController', 'ActionNotFound'));
+ $this->assertEquals(array('role2'), $acl->getRoles('somethingelse', 'actiona'));
+ $this->assertEquals(array('role2'), $acl->getRoles('somethingelse', 'actionb'));
+ $this->assertEquals(array('role3'), $acl->getRoles('somethingelse', 'actionc'));
}
}
diff --git a/tests/units/Core/Security/AuthenticationManagerTest.php b/tests/units/Core/Security/AuthenticationManagerTest.php
new file mode 100644
index 00000000..c2369626
--- /dev/null
+++ b/tests/units/Core/Security/AuthenticationManagerTest.php
@@ -0,0 +1,150 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Http\Request;
+use Kanboard\Core\Security\AuthenticationManager;
+use Kanboard\Auth\DatabaseAuth;
+use Kanboard\Auth\TotpAuth;
+use Kanboard\Auth\ReverseProxyAuth;
+
+class AuthenticationManagerTest extends Base
+{
+ public function testRegister()
+ {
+ $authManager = new AuthenticationManager($this->container);
+ $authManager->register(new DatabaseAuth($this->container));
+ $provider = $authManager->getProvider('Database');
+
+ $this->assertInstanceOf('Kanboard\Core\Security\AuthenticationProviderInterface', $provider);
+ }
+
+ public function testGetProviderNotFound()
+ {
+ $authManager = new AuthenticationManager($this->container);
+ $this->setExpectedException('LogicException');
+ $authManager->getProvider('Dababase');
+ }
+
+ public function testGetPostProviderNotFound()
+ {
+ $authManager = new AuthenticationManager($this->container);
+ $this->setExpectedException('LogicException');
+ $authManager->getPostAuthenticationProvider();
+ }
+
+ public function testGetPostProvider()
+ {
+ $authManager = new AuthenticationManager($this->container);
+ $authManager->register(new TotpAuth($this->container));
+ $provider = $authManager->getPostAuthenticationProvider();
+
+ $this->assertInstanceOf('Kanboard\Core\Security\PostAuthenticationProviderInterface', $provider);
+ }
+
+ public function testCheckSessionWhenNobodyIsLogged()
+ {
+ $authManager = new AuthenticationManager($this->container);
+ $authManager->register(new DatabaseAuth($this->container));
+
+ $this->assertFalse($this->container['userSession']->isLogged());
+ $this->assertTrue($authManager->checkCurrentSession());
+ }
+
+ public function testCheckSessionWhenSomeoneIsLogged()
+ {
+ $authManager = new AuthenticationManager($this->container);
+ $authManager->register(new DatabaseAuth($this->container));
+
+ $this->container['sessionStorage']->user = array('id' => 1);
+
+ $this->assertTrue($this->container['userSession']->isLogged());
+ $this->assertTrue($authManager->checkCurrentSession());
+ }
+
+ public function testCheckSessionWhenNotValid()
+ {
+ $authManager = new AuthenticationManager($this->container);
+ $authManager->register(new DatabaseAuth($this->container));
+
+ $this->container['sessionStorage']->user = array('id' => 2);
+
+ $this->assertTrue($this->container['userSession']->isLogged());
+ $this->assertFalse($authManager->checkCurrentSession());
+ $this->assertFalse($this->container['userSession']->isLogged());
+ }
+
+ public function testPreAuthenticationSuccessful()
+ {
+ $this->container['request'] = new Request($this->container, array(REVERSE_PROXY_USER_HEADER => 'admin'));
+ $this->container['dispatcher']->addListener(AuthenticationManager::EVENT_SUCCESS, array($this, 'onSuccess'));
+ $this->container['dispatcher']->addListener(AuthenticationManager::EVENT_FAILURE, array($this, 'onFailure'));
+
+ $authManager = new AuthenticationManager($this->container);
+ $authManager->register(new ReverseProxyAuth($this->container));
+
+ $this->assertTrue($authManager->preAuthentication());
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayHasKey(AuthenticationManager::EVENT_SUCCESS.'.AuthenticationManagerTest::onSuccess', $called);
+ $this->assertArrayNotHasKey(AuthenticationManager::EVENT_FAILURE.'.AuthenticationManagerTest::onFailure', $called);
+ }
+
+ public function testPreAuthenticationFailed()
+ {
+ $this->container['request'] = new Request($this->container, array(REVERSE_PROXY_USER_HEADER => ''));
+ $this->container['dispatcher']->addListener(AuthenticationManager::EVENT_SUCCESS, array($this, 'onSuccess'));
+ $this->container['dispatcher']->addListener(AuthenticationManager::EVENT_FAILURE, array($this, 'onFailure'));
+
+ $authManager = new AuthenticationManager($this->container);
+ $authManager->register(new ReverseProxyAuth($this->container));
+
+ $this->assertFalse($authManager->preAuthentication());
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayNotHasKey(AuthenticationManager::EVENT_SUCCESS.'.AuthenticationManagerTest::onSuccess', $called);
+ $this->assertArrayNotHasKey(AuthenticationManager::EVENT_FAILURE.'.AuthenticationManagerTest::onFailure', $called);
+ }
+
+ public function testPasswordAuthenticationSuccessful()
+ {
+ $this->container['dispatcher']->addListener(AuthenticationManager::EVENT_SUCCESS, array($this, 'onSuccess'));
+ $this->container['dispatcher']->addListener(AuthenticationManager::EVENT_FAILURE, array($this, 'onFailure'));
+
+ $authManager = new AuthenticationManager($this->container);
+ $authManager->register(new DatabaseAuth($this->container));
+
+ $this->assertTrue($authManager->passwordAuthentication('admin', 'admin'));
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayHasKey(AuthenticationManager::EVENT_SUCCESS.'.AuthenticationManagerTest::onSuccess', $called);
+ $this->assertArrayNotHasKey(AuthenticationManager::EVENT_FAILURE.'.AuthenticationManagerTest::onFailure', $called);
+ }
+
+ public function testPasswordAuthenticationFailed()
+ {
+ $this->container['dispatcher']->addListener(AuthenticationManager::EVENT_SUCCESS, array($this, 'onSuccess'));
+ $this->container['dispatcher']->addListener(AuthenticationManager::EVENT_FAILURE, array($this, 'onFailure'));
+
+ $authManager = new AuthenticationManager($this->container);
+ $authManager->register(new DatabaseAuth($this->container));
+
+ $this->assertFalse($authManager->passwordAuthentication('admin', 'wrong password'));
+
+ $called = $this->container['dispatcher']->getCalledListeners();
+ $this->assertArrayNotHasKey(AuthenticationManager::EVENT_SUCCESS.'.AuthenticationManagerTest::onSuccess', $called);
+ $this->assertArrayHasKey(AuthenticationManager::EVENT_FAILURE.'.AuthenticationManagerTest::onFailure', $called);
+ }
+
+ public function onSuccess($event)
+ {
+ $this->assertInstanceOf('Kanboard\Event\AuthSuccessEvent', $event);
+ $this->assertTrue(in_array($event->getAuthType(), array('Database', 'ReverseProxy')));
+ }
+
+ public function onFailure($event)
+ {
+ $this->assertInstanceOf('Kanboard\Event\AuthFailureEvent', $event);
+ $this->assertEquals('admin', $event->getUsername());
+ }
+}
diff --git a/tests/units/Core/Security/AuthorizationTest.php b/tests/units/Core/Security/AuthorizationTest.php
index ffeb3741..70561ad8 100644
--- a/tests/units/Core/Security/AuthorizationTest.php
+++ b/tests/units/Core/Security/AuthorizationTest.php
@@ -12,17 +12,28 @@ class AuthorizationTest extends Base
{
$acl = new AccessMap;
$acl->setDefaultRole(Role::APP_USER);
- $acl->add('MyController', 'myAction1', array(Role::APP_ADMIN, Role::APP_MANAGER));
- $acl->add('MyController', 'myAction2', array(Role::APP_ADMIN));
- $acl->add('MyAdminController', '*', array(Role::APP_MANAGER));
+ $acl->setRoleHierarchy(Role::APP_ADMIN, array(Role::APP_MANAGER, Role::APP_USER));
+ $acl->setRoleHierarchy(Role::APP_MANAGER, array(Role::APP_USER));
+
+ $acl->add('MyController', 'myAction1', Role::APP_MANAGER);
+ $acl->add('MyController', 'myAction2', Role::APP_ADMIN);
+ $acl->add('MyManagerController', '*', Role::APP_MANAGER);
$authorization = new Authorization($acl);
+
$this->assertTrue($authorization->isAllowed('myController', 'myAction1', Role::APP_ADMIN));
$this->assertTrue($authorization->isAllowed('myController', 'myAction1', Role::APP_MANAGER));
$this->assertFalse($authorization->isAllowed('myController', 'myAction1', Role::APP_USER));
- $this->assertTrue($authorization->isAllowed('anotherController', 'anotherAction', Role::APP_USER));
- $this->assertTrue($authorization->isAllowed('MyAdminController', 'myAction', Role::APP_MANAGER));
- $this->assertFalse($authorization->isAllowed('MyAdminController', 'myAction', Role::APP_ADMIN));
- $this->assertFalse($authorization->isAllowed('MyAdminController', 'myAction', 'something else'));
+ $this->assertFalse($authorization->isAllowed('myController', 'myAction1', 'something else'));
+
+ $this->assertTrue($authorization->isAllowed('MyManagerController', 'myAction', Role::APP_ADMIN));
+ $this->assertTrue($authorization->isAllowed('MyManagerController', 'myAction', Role::APP_MANAGER));
+ $this->assertFalse($authorization->isAllowed('MyManagerController', 'myAction', Role::APP_USER));
+ $this->assertFalse($authorization->isAllowed('MyManagerController', 'myAction', 'something else'));
+
+ $this->assertTrue($authorization->isAllowed('MyUserController', 'myAction', Role::APP_ADMIN));
+ $this->assertTrue($authorization->isAllowed('MyUserController', 'myAction', Role::APP_MANAGER));
+ $this->assertTrue($authorization->isAllowed('MyUserController', 'myAction', Role::APP_USER));
+ $this->assertFalse($authorization->isAllowed('MyUserController', 'myAction', 'something else'));
}
}
diff --git a/tests/units/Core/User/GroupSyncTest.php b/tests/units/Core/User/GroupSyncTest.php
new file mode 100644
index 00000000..e22b86d4
--- /dev/null
+++ b/tests/units/Core/User/GroupSyncTest.php
@@ -0,0 +1,30 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\User\GroupSync;
+use Kanboard\Model\Group;
+use Kanboard\Model\GroupMember;
+
+class GroupSyncTest extends Base
+{
+ public function testSynchronize()
+ {
+ $group = new Group($this->container);
+ $groupMember = new GroupMember($this->container);
+ $groupSync = new GroupSync($this->container);
+
+ $this->assertEquals(1, $group->create('My Group 1', 'externalId1'));
+ $this->assertEquals(2, $group->create('My Group 2', 'externalId2'));
+
+ $this->assertTrue($groupMember->addUser(1, 1));
+
+ $this->assertTrue($groupMember->isMember(1, 1));
+ $this->assertFalse($groupMember->isMember(2, 1));
+
+ $groupSync->synchronize(1, array('externalId1', 'externalId2', 'externalId3'));
+
+ $this->assertTrue($groupMember->isMember(1, 1));
+ $this->assertTrue($groupMember->isMember(2, 1));
+ }
+}
diff --git a/tests/units/Core/User/UserProfileTest.php b/tests/units/Core/User/UserProfileTest.php
new file mode 100644
index 00000000..4886a945
--- /dev/null
+++ b/tests/units/Core/User/UserProfileTest.php
@@ -0,0 +1,63 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Security\Role;
+use Kanboard\Core\User\UserProfile;
+use Kanboard\User\LdapUserProvider;
+use Kanboard\User\DatabaseUserProvider;
+
+class UserProfileTest extends Base
+{
+ public function testInitializeLocalUser()
+ {
+ $userProfile = new UserProfile($this->container);
+ $user = new DatabaseUserProvider(array('id' => 1));
+
+ $this->assertTrue($userProfile->initialize($user));
+ $this->assertNotEmpty($this->container['sessionStorage']->user);
+ $this->assertEquals('admin', $this->container['sessionStorage']->user['username']);
+ }
+
+ public function testInitializeLocalUserNotFound()
+ {
+ $userProfile = new UserProfile($this->container);
+ $user = new DatabaseUserProvider(array('id' => 2));
+
+ $this->assertFalse($userProfile->initialize($user));
+ $this->assertFalse(isset($this->container['sessionStorage']->user));
+ }
+
+ public function testInitializeRemoteUser()
+ {
+ $userProfile = new UserProfile($this->container);
+ $user = new LdapUserProvider('ldapId', 'bob', 'Bob', '', Role::APP_MANAGER, array());
+
+ $this->assertTrue($userProfile->initialize($user));
+ $this->assertNotEmpty($this->container['sessionStorage']->user);
+ $this->assertEquals(2, $this->container['sessionStorage']->user['id']);
+ $this->assertEquals('bob', $this->container['sessionStorage']->user['username']);
+ $this->assertEquals(Role::APP_MANAGER, $this->container['sessionStorage']->user['role']);
+
+ $user = new LdapUserProvider('ldapId', 'bob', 'Bob', '', Role::APP_MANAGER, array());
+
+ $this->assertTrue($userProfile->initialize($user));
+ $this->assertNotEmpty($this->container['sessionStorage']->user);
+ $this->assertEquals(2, $this->container['sessionStorage']->user['id']);
+ $this->assertEquals('bob', $this->container['sessionStorage']->user['username']);
+ }
+
+ public function testAssignRemoteUser()
+ {
+ $userProfile = new UserProfile($this->container);
+ $user = new LdapUserProvider('ldapId', 'bob', 'Bob', '', Role::APP_MANAGER, array());
+
+ $this->assertTrue($userProfile->assign(1, $user));
+ $this->assertNotEmpty($this->container['sessionStorage']->user);
+ $this->assertEquals(1, $this->container['sessionStorage']->user['id']);
+ $this->assertEquals('admin', $this->container['sessionStorage']->user['username']);
+ $this->assertEquals('Bob', $this->container['sessionStorage']->user['name']);
+ $this->assertEquals('', $this->container['sessionStorage']->user['email']);
+ $this->assertEquals(Role::APP_ADMIN, $this->container['sessionStorage']->user['role']);
+ }
+}
diff --git a/tests/units/Core/User/UserPropertyTest.php b/tests/units/Core/User/UserPropertyTest.php
new file mode 100644
index 00000000..170eab4c
--- /dev/null
+++ b/tests/units/Core/User/UserPropertyTest.php
@@ -0,0 +1,60 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Security\Role;
+use Kanboard\Core\User\UserProperty;
+use Kanboard\User\LdapUserProvider;
+
+class UserPropertyTest extends Base
+{
+ public function testGetProperties()
+ {
+ $user = new LdapUserProvider('ldapId', 'bob', 'Bob', '', Role::APP_USER, array());
+
+ $expected = array(
+ 'username' => 'bob',
+ 'name' => 'Bob',
+ 'role' => Role::APP_USER,
+ 'is_ldap_user' => 1,
+ );
+
+ $this->assertEquals($expected, UserProperty::getProperties($user));
+
+ $user = new LdapUserProvider('ldapId', 'bob', '', '', '', array());
+
+ $expected = array(
+ 'username' => 'bob',
+ 'is_ldap_user' => 1,
+ );
+
+ $this->assertEquals($expected, UserProperty::getProperties($user));
+ }
+
+ public function testFilterProperties()
+ {
+ $profile = array(
+ 'id' => 123,
+ 'username' => 'bob',
+ 'name' => null,
+ 'email' => '',
+ 'other_column' => 'myvalue',
+ 'role' => Role::APP_ADMIN,
+ );
+
+ $properties = array(
+ 'external_id' => '456',
+ 'username' => 'bobby',
+ 'name' => 'Bobby',
+ 'email' => 'admin@localhost',
+ 'role' => '',
+ );
+
+ $expected = array(
+ 'name' => 'Bobby',
+ 'email' => 'admin@localhost',
+ );
+
+ $this->assertEquals($expected, UserProperty::filterProperties($profile, $properties));
+ }
+}
diff --git a/tests/units/Model/UserSessionTest.php b/tests/units/Core/User/UserSessionTest.php
index ba1f8aac..64413f98 100644
--- a/tests/units/Model/UserSessionTest.php
+++ b/tests/units/Core/User/UserSessionTest.php
@@ -1,8 +1,9 @@
<?php
-require_once __DIR__.'/../Base.php';
+require_once __DIR__.'/../../Base.php';
-use Kanboard\Model\UserSession;
+use Kanboard\Core\User\UserSession;
+use Kanboard\Core\Security\Role;
class UserSessionTest extends Base
{
@@ -19,6 +20,7 @@ class UserSessionTest extends Base
'is_project_admin' => '0',
'is_ldap_user' => '0',
'twofactor_activated' => '0',
+ 'role' => Role::APP_MANAGER,
);
$us->initialize($user);
@@ -28,12 +30,13 @@ class UserSessionTest extends Base
$this->assertNotEmpty($session);
$this->assertEquals(123, $session['user']['id']);
$this->assertEquals('john', $session['user']['username']);
- $this->assertTrue($session['user']['is_admin']);
- $this->assertFalse($session['user']['is_project_admin']);
+ $this->assertEquals(Role::APP_MANAGER, $session['user']['role']);
$this->assertFalse($session['user']['is_ldap_user']);
$this->assertFalse($session['user']['twofactor_activated']);
$this->assertArrayNotHasKey('password', $session['user']);
$this->assertArrayNotHasKey('twofactor_secret', $session['user']);
+ $this->assertArrayNotHasKey('is_admin', $session['user']);
+ $this->assertArrayNotHasKey('is_project_admin', $session['user']);
$this->assertEquals('john', $us->getUsername());
}
@@ -70,30 +73,14 @@ class UserSessionTest extends Base
$this->assertFalse($us->isAdmin());
- $this->container['sessionStorage']->user = array('is_admin' => '1');
- $this->assertFalse($us->isAdmin());
+ $this->container['sessionStorage']->user = array('role' => Role::APP_ADMIN);
+ $this->assertTrue($us->isAdmin());
- $this->container['sessionStorage']->user = array('is_admin' => '2');
+ $this->container['sessionStorage']->user = array('role' => Role::APP_USER);
$this->assertFalse($us->isAdmin());
- $this->container['sessionStorage']->user = array('is_admin' => false);
+ $this->container['sessionStorage']->user = array('role' => '');
$this->assertFalse($us->isAdmin());
-
- $this->container['sessionStorage']->user = array('is_admin' => true);
- $this->assertTrue($us->isAdmin());
- }
-
- public function testIsProjectAdmin()
- {
- $us = new UserSession($this->container);
-
- $this->assertFalse($us->isProjectAdmin());
-
- $this->container['sessionStorage']->user = array('is_project_admin' => false);
- $this->assertFalse($us->isProjectAdmin());
-
- $this->container['sessionStorage']->user = array('is_project_admin' => true);
- $this->assertTrue($us->isProjectAdmin());
}
public function testCommentSorting()
@@ -131,28 +118,27 @@ class UserSessionTest extends Base
$this->assertEquals('assignee:bob', $us->getFilters(2));
}
- public function test2FA()
+ public function testPostAuthentication()
{
$us = new UserSession($this->container);
+ $this->assertFalse($us->isPostAuthenticationValidated());
- $this->assertFalse($us->check2FA());
-
- $this->container['sessionStorage']->postAuth = array('validated' => false);
- $this->assertFalse($us->check2FA());
+ $this->container['sessionStorage']->postAuthenticationValidated = false;
+ $this->assertFalse($us->isPostAuthenticationValidated());
- $this->container['sessionStorage']->postAuth = array('validated' => true);
- $this->assertTrue($us->check2FA());
+ $us->validatePostAuthentication();
+ $this->assertTrue($us->isPostAuthenticationValidated());
$this->container['sessionStorage']->user = array();
- $this->assertFalse($us->has2FA());
+ $this->assertFalse($us->hasPostAuthentication());
$this->container['sessionStorage']->user = array('twofactor_activated' => false);
- $this->assertFalse($us->has2FA());
+ $this->assertFalse($us->hasPostAuthentication());
$this->container['sessionStorage']->user = array('twofactor_activated' => true);
- $this->assertTrue($us->has2FA());
+ $this->assertTrue($us->hasPostAuthentication());
- $us->disable2FA();
- $this->assertFalse($us->has2FA());
+ $us->disablePostAuthentication();
+ $this->assertFalse($us->hasPostAuthentication());
}
}
diff --git a/tests/units/Core/User/UserSyncTest.php b/tests/units/Core/User/UserSyncTest.php
new file mode 100644
index 00000000..e7ce42b2
--- /dev/null
+++ b/tests/units/Core/User/UserSyncTest.php
@@ -0,0 +1,55 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Security\Role;
+use Kanboard\Core\User\UserSync;
+use Kanboard\User\LdapUserProvider;
+
+class UserSyncTest extends Base
+{
+ public function testSynchronizeNewUser()
+ {
+ $user = new LdapUserProvider('ldapId', 'bob', 'Bob', '', Role::APP_MANAGER, array());
+ $userSync = new UserSync($this->container);
+
+ $profile = array(
+ 'id' => 2,
+ 'username' => 'bob',
+ 'name' => 'Bob',
+ 'email' => '',
+ 'role' => Role::APP_MANAGER,
+ 'is_ldap_user' => 1,
+ );
+
+ $this->assertArraySubset($profile, $userSync->synchronize($user));
+ }
+
+ public function testSynchronizeExistingUser()
+ {
+ $userSync = new UserSync($this->container);
+ $user = new LdapUserProvider('ldapId', 'admin', 'Admin', 'email@localhost', Role::APP_MANAGER, array());
+
+ $profile = array(
+ 'id' => 1,
+ 'username' => 'admin',
+ 'name' => 'Admin',
+ 'email' => 'email@localhost',
+ 'role' => Role::APP_MANAGER,
+ );
+
+ $this->assertArraySubset($profile, $userSync->synchronize($user));
+
+ $user = new LdapUserProvider('ldapId', 'admin', '', '', Role::APP_ADMIN, array());
+
+ $profile = array(
+ 'id' => 1,
+ 'username' => 'admin',
+ 'name' => 'Admin',
+ 'email' => 'email@localhost',
+ 'role' => Role::APP_ADMIN,
+ );
+
+ $this->assertArraySubset($profile, $userSync->synchronize($user));
+ }
+}
diff --git a/tests/units/Helper/UserHelperTest.php b/tests/units/Helper/UserHelperTest.php
index eba977ee..2b503157 100644
--- a/tests/units/Helper/UserHelperTest.php
+++ b/tests/units/Helper/UserHelperTest.php
@@ -4,172 +4,227 @@ require_once __DIR__.'/../Base.php';
use Kanboard\Helper\User;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
+use Kanboard\Model\ProjectUserRole;
use Kanboard\Model\User as UserModel;
+use Kanboard\Core\Security\Role;
class UserHelperTest extends Base
{
public function testInitials()
{
- $h = new User($this->container);
+ $helper = new User($this->container);
- $this->assertEquals('CN', $h->getInitials('chuck norris'));
- $this->assertEquals('A', $h->getInitials('admin'));
+ $this->assertEquals('CN', $helper->getInitials('chuck norris'));
+ $this->assertEquals('A', $helper->getInitials('admin'));
}
- public function testIsProjectAdministrationAllowedForProjectAdmin()
+ public function testGetRoleName()
{
- $h = new User($this->container);
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new UserModel($this->container);
-
- // We create our user
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
+ $helper = new User($this->container);
+ $this->assertEquals('Administrator', $helper->getRoleName(Role::APP_ADMIN));
+ $this->assertEquals('Manager', $helper->getRoleName(Role::APP_MANAGER));
+ $this->assertEquals('Project Viewer', $helper->getRoleName(Role::PROJECT_VIEWER));
+ }
- // We create a project and set our user as project manager
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->isMember(1, 2));
- $this->assertFalse($pp->isManager(1, 2));
+ public function testHasAccessForAdmins()
+ {
+ $helper = new User($this->container);
- // We fake a session for him
$this->container['sessionStorage']->user = array(
'id' => 2,
- 'is_admin' => false,
- 'is_project_admin' => true,
+ 'role' => Role::APP_ADMIN,
);
- $this->assertTrue($h->isProjectAdministrationAllowed(1));
+ $this->assertTrue($helper->hasAccess('user', 'create'));
+ $this->assertTrue($helper->hasAccess('project', 'create'));
+ $this->assertTrue($helper->hasAccess('project', 'createPrivate'));
}
- public function testIsProjectAdministrationAllowedForProjectMember()
+ public function testHasAccessForManagers()
{
- $h = new User($this->container);
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new UserModel($this->container);
-
- // We create our user
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
-
- // We create a project and set our user as project member
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->isMember(1, 2));
- $this->assertFalse($pp->isManager(1, 2));
+ $helper = new User($this->container);
- // We fake a session for him
$this->container['sessionStorage']->user = array(
'id' => 2,
- 'is_admin' => false,
- 'is_project_admin' => false,
+ 'role' => Role::APP_MANAGER,
);
- $this->assertFalse($h->isProjectAdministrationAllowed(1));
+ $this->assertFalse($helper->hasAccess('user', 'create'));
+ $this->assertTrue($helper->hasAccess('project', 'create'));
+ $this->assertTrue($helper->hasAccess('project', 'createPrivate'));
}
- public function testIsProjectAdministrationAllowedForProjectManager()
+ public function testHasAccessForUsers()
{
- $h = new User($this->container);
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new UserModel($this->container);
+ $helper = new User($this->container);
- // We create our user
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
+ $this->container['sessionStorage']->user = array(
+ 'id' => 2,
+ 'role' => Role::APP_USER,
+ );
+
+ $this->assertFalse($helper->hasAccess('user', 'create'));
+ $this->assertFalse($helper->hasAccess('project', 'create'));
+ $this->assertTrue($helper->hasAccess('project', 'createPrivate'));
+ }
- // We create a project and set our user as project member
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($pp->addManager(1, 2));
- $this->assertTrue($pp->isMember(1, 2));
- $this->assertTrue($pp->isManager(1, 2));
+ public function testHasProjectAccessForAdmins()
+ {
+ $helper = new User($this->container);
+ $project = new Project($this->container);
- // We fake a session for him
$this->container['sessionStorage']->user = array(
'id' => 2,
- 'is_admin' => false,
- 'is_project_admin' => false,
+ 'role' => Role::APP_ADMIN,
);
- $this->assertFalse($h->isProjectAdministrationAllowed(1));
+ $this->assertEquals(1, $project->create(array('name' => 'My project')));
+
+ $this->assertTrue($helper->hasProjectAccess('project', 'edit', 1));
+ $this->assertTrue($helper->hasProjectAccess('board', 'show', 1));
}
- public function testIsProjectManagementAllowedForProjectAdmin()
+ public function testHasProjectAccessForManagers()
{
- $h = new User($this->container);
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new UserModel($this->container);
+ $helper = new User($this->container);
+ $project = new Project($this->container);
+
+ $this->container['sessionStorage']->user = array(
+ 'id' => 2,
+ 'role' => Role::APP_MANAGER,
+ );
- // We create our user
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
+ $this->assertEquals(1, $project->create(array('name' => 'My project')));
- // We create a project and set our user as project manager
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->isMember(1, 2));
- $this->assertFalse($pp->isManager(1, 2));
+ $this->assertFalse($helper->hasProjectAccess('project', 'edit', 1));
+ $this->assertFalse($helper->hasProjectAccess('board', 'show', 1));
+ }
+
+ public function testHasProjectAccessForUsers()
+ {
+ $helper = new User($this->container);
+ $project = new Project($this->container);
- // We fake a session for him
$this->container['sessionStorage']->user = array(
'id' => 2,
- 'is_admin' => false,
- 'is_project_admin' => true,
+ 'role' => Role::APP_USER,
);
- $this->assertTrue($h->isProjectManagementAllowed(1));
+ $this->assertEquals(1, $project->create(array('name' => 'My project')));
+
+ $this->assertFalse($helper->hasProjectAccess('project', 'edit', 1));
+ $this->assertFalse($helper->hasProjectAccess('board', 'show', 1));
}
- public function testIsProjectManagementAllowedForProjectMember()
+ public function testHasProjectAccessForAppManagerAndProjectManagers()
{
- $h = new User($this->container);
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new UserModel($this->container);
+ $helper = new User($this->container);
+ $user = new UserModel($this->container);
+ $project = new Project($this->container);
+ $projectUserRole = new ProjectUserRole($this->container);
- // We create our user
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
+ $this->container['sessionStorage']->user = array(
+ 'id' => 2,
+ 'role' => Role::APP_MANAGER,
+ );
+
+ $this->assertEquals(1, $project->create(array('name' => 'My project')));
+ $this->assertEquals(2, $project->create(array('name' => 'My project')));
+ $this->assertEquals(2, $user->create(array('username' => 'user')));
+ $this->assertTrue($projectUserRole->addUser(1, 2, Role::PROJECT_MANAGER));
+
+ $this->assertTrue($helper->hasProjectAccess('project', 'edit', 1));
+ $this->assertTrue($helper->hasProjectAccess('board', 'show', 1));
+ $this->assertTrue($helper->hasProjectAccess('task', 'show', 1));
+ $this->assertTrue($helper->hasProjectAccess('taskcreation', 'save', 1));
- // We create a project and set our user as project member
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->isMember(1, 2));
- $this->assertFalse($pp->isManager(1, 2));
+ $this->assertFalse($helper->hasProjectAccess('project', 'edit', 2));
+ $this->assertFalse($helper->hasProjectAccess('board', 'show', 2));
+ $this->assertFalse($helper->hasProjectAccess('task', 'show', 2));
+ $this->assertFalse($helper->hasProjectAccess('taskcreation', 'save', 2));
+ }
+
+ public function testHasProjectAccessForProjectManagers()
+ {
+ $helper = new User($this->container);
+ $user = new UserModel($this->container);
+ $project = new Project($this->container);
+ $projectUserRole = new ProjectUserRole($this->container);
- // We fake a session for him
$this->container['sessionStorage']->user = array(
'id' => 2,
- 'is_admin' => false,
- 'is_project_admin' => false,
+ 'role' => Role::APP_USER,
);
- $this->assertFalse($h->isProjectManagementAllowed(1));
+ $this->assertEquals(1, $project->create(array('name' => 'My project')));
+ $this->assertEquals(2, $project->create(array('name' => 'My project')));
+ $this->assertEquals(2, $user->create(array('username' => 'user')));
+ $this->assertTrue($projectUserRole->addUser(1, 2, Role::PROJECT_MANAGER));
+
+ $this->assertTrue($helper->hasProjectAccess('project', 'edit', 1));
+ $this->assertTrue($helper->hasProjectAccess('board', 'show', 1));
+ $this->assertTrue($helper->hasProjectAccess('task', 'show', 1));
+ $this->assertTrue($helper->hasProjectAccess('taskcreation', 'save', 1));
+
+ $this->assertFalse($helper->hasProjectAccess('project', 'edit', 2));
+ $this->assertFalse($helper->hasProjectAccess('board', 'show', 2));
+ $this->assertFalse($helper->hasProjectAccess('task', 'show', 2));
+ $this->assertFalse($helper->hasProjectAccess('taskcreation', 'save', 2));
}
- public function testIsProjectManagementAllowedForProjectManager()
+ public function testHasProjectAccessForProjectMembers()
{
- $h = new User($this->container);
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new UserModel($this->container);
+ $helper = new User($this->container);
+ $user = new UserModel($this->container);
+ $project = new Project($this->container);
+ $projectUserRole = new ProjectUserRole($this->container);
- // We create our user
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
+ $this->container['sessionStorage']->user = array(
+ 'id' => 2,
+ 'role' => Role::APP_USER,
+ );
+
+ $this->assertEquals(1, $project->create(array('name' => 'My project')));
+ $this->assertEquals(2, $project->create(array('name' => 'My project')));
+ $this->assertEquals(2, $user->create(array('username' => 'user')));
+ $this->assertTrue($projectUserRole->addUser(1, 2, Role::PROJECT_MEMBER));
+
+ $this->assertFalse($helper->hasProjectAccess('project', 'edit', 1));
+ $this->assertTrue($helper->hasProjectAccess('board', 'show', 1));
+ $this->assertTrue($helper->hasProjectAccess('task', 'show', 1));
+ $this->assertTrue($helper->hasProjectAccess('taskcreation', 'save', 1));
- // We create a project and set our user as project member
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($pp->addManager(1, 2));
- $this->assertTrue($pp->isMember(1, 2));
- $this->assertTrue($pp->isManager(1, 2));
+ $this->assertFalse($helper->hasProjectAccess('project', 'edit', 2));
+ $this->assertFalse($helper->hasProjectAccess('board', 'show', 2));
+ $this->assertFalse($helper->hasProjectAccess('task', 'show', 2));
+ $this->assertFalse($helper->hasProjectAccess('taskcreation', 'save', 2));
+ }
+
+ public function testHasProjectAccessForProjectViewers()
+ {
+ $helper = new User($this->container);
+ $user = new UserModel($this->container);
+ $project = new Project($this->container);
+ $projectUserRole = new ProjectUserRole($this->container);
- // We fake a session for him
$this->container['sessionStorage']->user = array(
'id' => 2,
- 'is_admin' => false,
- 'is_project_admin' => false,
+ 'role' => Role::APP_USER,
);
- $this->assertTrue($h->isProjectManagementAllowed(1));
+ $this->assertEquals(1, $project->create(array('name' => 'My project')));
+ $this->assertEquals(2, $project->create(array('name' => 'My project')));
+ $this->assertEquals(2, $user->create(array('username' => 'user')));
+ $this->assertTrue($projectUserRole->addUser(1, 2, Role::PROJECT_VIEWER));
+
+ $this->assertFalse($helper->hasProjectAccess('project', 'edit', 1));
+ $this->assertTrue($helper->hasProjectAccess('board', 'show', 1));
+ $this->assertTrue($helper->hasProjectAccess('task', 'show', 1));
+ $this->assertFalse($helper->hasProjectAccess('taskcreation', 'save', 1));
+
+ $this->assertFalse($helper->hasProjectAccess('project', 'edit', 2));
+ $this->assertFalse($helper->hasProjectAccess('board', 'show', 2));
+ $this->assertFalse($helper->hasProjectAccess('task', 'show', 2));
+ $this->assertFalse($helper->hasProjectAccess('taskcreation', 'save', 2));
}
}
diff --git a/tests/units/Integration/BitbucketWebhookTest.php b/tests/units/Integration/BitbucketWebhookTest.php
index 1a3b005b..40dbde2e 100644
--- a/tests/units/Integration/BitbucketWebhookTest.php
+++ b/tests/units/Integration/BitbucketWebhookTest.php
@@ -6,8 +6,9 @@ use Kanboard\Integration\BitbucketWebhook;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
+use Kanboard\Model\ProjectUserRole;
use Kanboard\Model\User;
+use Kanboard\Core\Security\Role;
class BitbucketWebhookTest extends Base
{
@@ -108,8 +109,8 @@ class BitbucketWebhookTest extends Base
$u = new User($this->container);
$this->assertEquals(2, $u->create(array('username' => 'minicoders')));
- $pp = new ProjectPermission($this->container);
- $this->assertTrue($pp->addMember(1, 2));
+ $pp = new ProjectUserRole($this->container);
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
$g = new BitbucketWebhook($this->container);
$g->setProjectId(1);
@@ -232,8 +233,8 @@ class BitbucketWebhookTest extends Base
$u = new User($this->container);
$this->assertEquals(2, $u->create(array('username' => 'minicoders')));
- $pp = new ProjectPermission($this->container);
- $this->assertTrue($pp->addMember(1, 2));
+ $pp = new ProjectUserRole($this->container);
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
$g = new BitbucketWebhook($this->container);
$g->setProjectId(1);
diff --git a/tests/units/Integration/GithubWebhookTest.php b/tests/units/Integration/GithubWebhookTest.php
index d64e783e..f00e5dde 100644
--- a/tests/units/Integration/GithubWebhookTest.php
+++ b/tests/units/Integration/GithubWebhookTest.php
@@ -6,8 +6,9 @@ use Kanboard\Integration\GithubWebhook;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
+use Kanboard\Model\ProjectUserRole;
use Kanboard\Model\User;
+use Kanboard\Core\Security\Role;
class GithubWebhookTest extends Base
{
@@ -40,8 +41,8 @@ class GithubWebhookTest extends Base
$u = new User($this->container);
$this->assertEquals(2, $u->create(array('username' => 'fguillot')));
- $pp = new ProjectPermission($this->container);
- $this->assertTrue($pp->addMember(1, 2));
+ $pp = new ProjectUserRole($this->container);
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
$g = new GithubWebhook($this->container);
$g->setProjectId(1);
@@ -111,8 +112,8 @@ class GithubWebhookTest extends Base
$u = new User($this->container);
$this->assertEquals(2, $u->create(array('username' => 'fguillot')));
- $pp = new ProjectPermission($this->container);
- $this->assertTrue($pp->addMember(1, 2));
+ $pp = new ProjectUserRole($this->container);
+ $this->assertTrue($pp->addUser(1, 2, ROLE::PROJECT_MANAGER));
$g = new GithubWebhook($this->container);
$g->setProjectId(1);
@@ -323,8 +324,8 @@ class GithubWebhookTest extends Base
$u = new User($this->container);
$this->assertEquals(2, $u->create(array('username' => 'fguillot')));
- $pp = new ProjectPermission($this->container);
- $this->assertTrue($pp->addMember(1, 2));
+ $pp = new ProjectUserRole($this->container);
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
$g = new GithubWebhook($this->container);
$g->setProjectId(1);
diff --git a/tests/units/Integration/GitlabWebhookTest.php b/tests/units/Integration/GitlabWebhookTest.php
index fa05f292..3e0e6d2c 100644
--- a/tests/units/Integration/GitlabWebhookTest.php
+++ b/tests/units/Integration/GitlabWebhookTest.php
@@ -6,8 +6,9 @@ use Kanboard\Integration\GitlabWebhook;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
+use Kanboard\Model\ProjectUserRole;
use Kanboard\Model\User;
+use Kanboard\Core\Security\Role;
class GitlabWebhookTest extends Base
{
@@ -179,8 +180,8 @@ class GitlabWebhookTest extends Base
$u = new User($this->container);
$this->assertEquals(2, $u->create(array('username' => 'minicoders')));
- $pp = new ProjectPermission($this->container);
- $this->assertTrue($pp->addMember(1, 2));
+ $pp = new ProjectUserRole($this->container);
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
$g = new GitlabWebhook($this->container);
$g->setProjectId(1);
diff --git a/tests/units/Model/AclTest.php b/tests/units/Model/AclTest.php
deleted file mode 100644
index afda446b..00000000
--- a/tests/units/Model/AclTest.php
+++ /dev/null
@@ -1,296 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Model\Acl;
-use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
-use Kanboard\Model\User;
-
-class AclTest extends Base
-{
- public function testMatchAcl()
- {
- $acl_rules = array(
- 'controller1' => array('action1', 'action3'),
- 'controller3' => '*',
- 'controller5' => '-',
- 'controller6' => array(),
- 'controllera' => '*',
- );
-
- $acl = new Acl($this->container);
- $this->assertTrue($acl->matchAcl($acl_rules, 'controller1', 'aCtiOn1'));
- $this->assertTrue($acl->matchAcl($acl_rules, 'controller1', 'action1'));
- $this->assertTrue($acl->matchAcl($acl_rules, 'controller1', 'action3'));
- $this->assertFalse($acl->matchAcl($acl_rules, 'controller1', 'action2'));
- $this->assertFalse($acl->matchAcl($acl_rules, 'controller2', 'action2'));
- $this->assertFalse($acl->matchAcl($acl_rules, 'controller2', 'action3'));
- $this->assertTrue($acl->matchAcl($acl_rules, 'controller3', 'anything'));
- $this->assertFalse($acl->matchAcl($acl_rules, 'controller4', 'anything'));
- $this->assertFalse($acl->matchAcl($acl_rules, 'controller5', 'anything'));
- $this->assertFalse($acl->matchAcl($acl_rules, 'controller6', 'anything'));
- $this->assertTrue($acl->matchAcl($acl_rules, 'ControllerA', 'anything'));
- $this->assertTrue($acl->matchAcl($acl_rules, 'controllera', 'anything'));
- }
-
- public function testPublicActions()
- {
- $acl = new Acl($this->container);
- $this->assertTrue($acl->isPublicAction('task', 'readonly'));
- $this->assertTrue($acl->isPublicAction('board', 'readonly'));
- $this->assertFalse($acl->isPublicAction('board', 'show'));
- $this->assertTrue($acl->isPublicAction('feed', 'project'));
- $this->assertTrue($acl->isPublicAction('feed', 'user'));
- $this->assertTrue($acl->isPublicAction('ical', 'project'));
- $this->assertTrue($acl->isPublicAction('ical', 'user'));
- $this->assertTrue($acl->isPublicAction('oauth', 'github'));
- $this->assertTrue($acl->isPublicAction('oauth', 'google'));
- $this->assertTrue($acl->isPublicAction('auth', 'login'));
- $this->assertTrue($acl->isPublicAction('auth', 'check'));
- $this->assertTrue($acl->isPublicAction('auth', 'captcha'));
- }
-
- public function testAdminActions()
- {
- $acl = new Acl($this->container);
- $this->assertFalse($acl->isAdminAction('board', 'show'));
- $this->assertFalse($acl->isAdminAction('task', 'show'));
- $this->assertTrue($acl->isAdminAction('config', 'api'));
- $this->assertTrue($acl->isAdminAction('config', 'anything'));
- $this->assertTrue($acl->isAdminAction('config', 'anything'));
- $this->assertTrue($acl->isAdminAction('user', 'save'));
- }
-
- public function testProjectAdminActions()
- {
- $acl = new Acl($this->container);
- $this->assertFalse($acl->isProjectAdminAction('config', 'save'));
- $this->assertFalse($acl->isProjectAdminAction('user', 'index'));
- $this->assertTrue($acl->isProjectAdminAction('project', 'remove'));
- }
-
- public function testProjectManagerActions()
- {
- $acl = new Acl($this->container);
- $this->assertFalse($acl->isProjectManagerAction('board', 'readonly'));
- $this->assertFalse($acl->isProjectManagerAction('project', 'remove'));
- $this->assertFalse($acl->isProjectManagerAction('project', 'show'));
- $this->assertTrue($acl->isProjectManagerAction('project', 'disable'));
- $this->assertTrue($acl->isProjectManagerAction('category', 'index'));
- $this->assertTrue($acl->isProjectManagerAction('project', 'users'));
- $this->assertFalse($acl->isProjectManagerAction('app', 'index'));
- }
-
- public function testPageAccessNoSession()
- {
- $acl = new Acl($this->container);
-
- $this->assertFalse($acl->isAllowed('board', 'readonly'));
- $this->assertFalse($acl->isAllowed('task', 'show'));
- $this->assertFalse($acl->isAllowed('config', 'application'));
- $this->assertFalse($acl->isAllowed('project', 'users'));
- $this->assertFalse($acl->isAllowed('task', 'remove'));
- $this->assertTrue($acl->isAllowed('app', 'index'));
- }
-
- public function testPageAccessEmptySession()
- {
- $acl = new Acl($this->container);
- $this->container['sessionStorage']->user = array();
-
- $this->assertFalse($acl->isAllowed('board', 'readonly'));
- $this->assertFalse($acl->isAllowed('task', 'show'));
- $this->assertFalse($acl->isAllowed('config', 'application'));
- $this->assertFalse($acl->isAllowed('project', 'users'));
- $this->assertFalse($acl->isAllowed('task', 'remove'));
- $this->assertTrue($acl->isAllowed('app', 'index'));
- }
-
- public function testPageAccessAdminUser()
- {
- $acl = new Acl($this->container);
- $this->container['sessionStorage']->user = array(
- 'is_admin' => true,
- );
-
- $this->assertTrue($acl->isAllowed('board', 'readonly'));
- $this->assertTrue($acl->isAllowed('task', 'readonly'));
- $this->assertTrue($acl->isAllowed('webhook', 'github'));
- $this->assertTrue($acl->isAllowed('task', 'show'));
- $this->assertTrue($acl->isAllowed('task', 'update'));
- $this->assertTrue($acl->isAllowed('config', 'application'));
- $this->assertTrue($acl->isAllowed('project', 'show'));
- $this->assertTrue($acl->isAllowed('project', 'users'));
- $this->assertTrue($acl->isAllowed('project', 'remove'));
- $this->assertTrue($acl->isAllowed('category', 'edit'));
- $this->assertTrue($acl->isAllowed('task', 'remove'));
- $this->assertTrue($acl->isAllowed('app', 'index'));
- }
-
- public function testPageAccessProjectAdmin()
- {
- $acl = new Acl($this->container);
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new User($this->container);
-
- // We create our user
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
-
- // We create a project and set our user as project manager
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->isMember(1, 2));
- $this->assertFalse($pp->isManager(1, 2));
-
- // We fake a session for him
- $this->container['sessionStorage']->user = array(
- 'id' => 2,
- 'is_admin' => false,
- 'is_project_admin' => true,
- );
-
- $this->assertTrue($acl->isAllowed('board', 'readonly', 1));
- $this->assertTrue($acl->isAllowed('task', 'readonly', 1));
- $this->assertTrue($acl->isAllowed('webhook', 'github', 1));
- $this->assertTrue($acl->isAllowed('task', 'show', 1));
- $this->assertFalse($acl->isAllowed('task', 'show', 2));
- $this->assertTrue($acl->isAllowed('task', 'update', 1));
- $this->assertTrue($acl->isAllowed('project', 'show', 1));
- $this->assertFalse($acl->isAllowed('config', 'application', 1));
-
- $this->assertTrue($acl->isAllowed('project', 'users', 1));
- $this->assertFalse($acl->isAllowed('project', 'users', 2));
-
- $this->assertTrue($acl->isAllowed('project', 'remove', 1));
- $this->assertFalse($acl->isAllowed('project', 'remove', 2));
-
- $this->assertTrue($acl->isAllowed('category', 'edit', 1));
- $this->assertTrue($acl->isAllowed('task', 'remove', 1));
- $this->assertTrue($acl->isAllowed('app', 'index', 1));
- }
-
- public function testPageAccessProjectManager()
- {
- $acl = new Acl($this->container);
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new User($this->container);
-
- // We create our user
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
-
- // We create a project and set our user as project manager
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest'), 2, true));
- $this->assertTrue($pp->isMember(1, 2));
- $this->assertTrue($pp->isManager(1, 2));
-
- // We fake a session for him
- $this->container['sessionStorage']->user = array(
- 'id' => 2,
- 'is_admin' => false,
- );
-
- $this->assertTrue($acl->isAllowed('board', 'readonly', 1));
- $this->assertTrue($acl->isAllowed('task', 'readonly', 1));
- $this->assertTrue($acl->isAllowed('webhook', 'github', 1));
- $this->assertTrue($acl->isAllowed('task', 'show', 1));
- $this->assertFalse($acl->isAllowed('task', 'show', 2));
- $this->assertTrue($acl->isAllowed('task', 'update', 1));
- $this->assertTrue($acl->isAllowed('project', 'show', 1));
- $this->assertFalse($acl->isAllowed('config', 'application', 1));
-
- $this->assertTrue($acl->isAllowed('project', 'users', 1));
- $this->assertFalse($acl->isAllowed('project', 'users', 2));
-
- $this->assertFalse($acl->isAllowed('project', 'remove', 1));
- $this->assertFalse($acl->isAllowed('project', 'remove', 2));
-
- $this->assertTrue($acl->isAllowed('category', 'edit', 1));
- $this->assertTrue($acl->isAllowed('task', 'remove', 1));
- $this->assertTrue($acl->isAllowed('app', 'index', 1));
- }
-
- public function testPageAccessMember()
- {
- $acl = new Acl($this->container);
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new User($this->container);
-
- // We create our user
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
-
- // We create a project and set our user as member
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest1')));
- $this->assertEquals(2, $p->create(array('name' => 'UnitTest2')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->isMember(1, 2));
- $this->assertFalse($pp->isManager(1, 2));
-
- $this->container['sessionStorage']->user = array(
- 'id' => 2,
- 'is_admin' => false,
- );
-
- $this->assertTrue($acl->isAllowed('board', 'readonly', 1));
- $this->assertTrue($acl->isAllowed('task', 'readonly', 1));
- $this->assertTrue($acl->isAllowed('webhook', 'github', 1));
- $this->assertFalse($acl->isAllowed('board', 'show', 2));
- $this->assertTrue($acl->isAllowed('board', 'show', 1));
- $this->assertFalse($acl->isAllowed('task', 'show', 2));
- $this->assertTrue($acl->isAllowed('task', 'show', 1));
- $this->assertTrue($acl->isAllowed('task', 'update', 1));
- $this->assertTrue($acl->isAllowed('project', 'show', 1));
- $this->assertFalse($acl->isAllowed('config', 'application', 1));
- $this->assertFalse($acl->isAllowed('project', 'users', 1));
- $this->assertTrue($acl->isAllowed('task', 'remove', 1));
- $this->assertFalse($acl->isAllowed('task', 'remove', 2));
- $this->assertTrue($acl->isAllowed('app', 'index', 1));
- }
-
- public function testPageAccessNotMember()
- {
- $acl = new Acl($this->container);
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new User($this->container);
-
- // We create our user
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
-
- // We create a project and set our user as member
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest1')));
- $this->assertEquals(2, $p->create(array('name' => 'UnitTest2')));
- $this->assertFalse($pp->isMember(1, 2));
- $this->assertFalse($pp->isManager(1, 2));
-
- $this->container['sessionStorage']->user = array(
- 'id' => 2,
- 'is_admin' => false,
- );
-
- $this->assertFalse($acl->isAllowed('board', 'show', 2));
- $this->assertFalse($acl->isAllowed('board', 'show', 1));
- $this->assertFalse($acl->isAllowed('task', 'show', 1));
- $this->assertFalse($acl->isAllowed('task', 'update', 1));
- $this->assertFalse($acl->isAllowed('project', 'show', 1));
- $this->assertFalse($acl->isAllowed('config', 'application', 1));
- $this->assertFalse($acl->isAllowed('project', 'users', 1));
- $this->assertFalse($acl->isAllowed('task', 'remove', 1));
- $this->assertTrue($acl->isAllowed('app', 'index', 1));
- }
-
- public function testExtend()
- {
- $acl = new Acl($this->container);
-
- $this->assertFalse($acl->isProjectManagerAction('plop', 'show'));
-
- $acl->extend('project_manager_acl', array('plop' => '*'));
-
- $this->assertTrue($acl->isProjectManagerAction('plop', 'show'));
- $this->assertTrue($acl->isProjectManagerAction('swimlane', 'index'));
- }
-}
diff --git a/tests/units/Model/ActionTest.php b/tests/units/Model/ActionTest.php
index 30f6b22c..4a338707 100644
--- a/tests/units/Model/ActionTest.php
+++ b/tests/units/Model/ActionTest.php
@@ -11,9 +11,10 @@ use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Category;
use Kanboard\Model\User;
-use Kanboard\Model\ProjectPermission;
+use Kanboard\Model\ProjectUserRole;
use Kanboard\Integration\GithubWebhook;
use Kanboard\Integration\BitbucketWebhook;
+use Kanboard\Core\Security\Role;
class ActionTest extends Base
{
@@ -62,7 +63,7 @@ class ActionTest extends Base
public function testResolveDuplicatedParameters()
{
$p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
$a = new Action($this->container);
$c = new Category($this->container);
$u = new User($this->container);
@@ -78,9 +79,9 @@ class ActionTest extends Base
$this->assertEquals(2, $u->create(array('username' => 'unittest1')));
$this->assertEquals(3, $u->create(array('username' => 'unittest2')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->addMember(1, 3));
- $this->assertTrue($pp->addMember(2, 3));
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(1, 3, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(2, 3, Role::PROJECT_MEMBER));
// anything
$this->assertEquals('blah', $a->resolveParameters(array('name' => 'foobar', 'value' => 'blah'), 2));
@@ -113,7 +114,7 @@ class ActionTest extends Base
public function testDuplicateSuccess()
{
$p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
$a = new Action($this->container);
$u = new User($this->container);
@@ -123,9 +124,9 @@ class ActionTest extends Base
$this->assertEquals(2, $u->create(array('username' => 'unittest1')));
$this->assertEquals(3, $u->create(array('username' => 'unittest2')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->addMember(1, 3));
- $this->assertTrue($pp->addMember(2, 3));
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(1, 3, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(2, 3, Role::PROJECT_MEMBER));
$this->assertEquals(1, $a->create(array(
'project_id' => 1,
@@ -159,7 +160,7 @@ class ActionTest extends Base
public function testDuplicateUnableToResolveParams()
{
$p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
$a = new Action($this->container);
$u = new User($this->container);
@@ -168,7 +169,7 @@ class ActionTest extends Base
$this->assertEquals(2, $u->create(array('username' => 'unittest1')));
- $this->assertTrue($pp->addMember(1, 2));
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
$this->assertEquals(1, $a->create(array(
'project_id' => 1,
@@ -196,7 +197,7 @@ class ActionTest extends Base
public function testDuplicateMixedResults()
{
$p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
$a = new Action($this->container);
$u = new User($this->container);
$c = new Category($this->container);
@@ -210,7 +211,7 @@ class ActionTest extends Base
$this->assertEquals(2, $u->create(array('username' => 'unittest1')));
- $this->assertTrue($pp->addMember(1, 2));
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
$this->assertEquals(1, $a->create(array(
'project_id' => 1,
diff --git a/tests/units/Model/AuthenticationTest.php b/tests/units/Model/AuthenticationTest.php
deleted file mode 100644
index 6b48affe..00000000
--- a/tests/units/Model/AuthenticationTest.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Model\User;
-use Kanboard\Model\Authentication;
-
-class AuthenticationTest extends Base
-{
- public function testHasCaptcha()
- {
- $u = new User($this->container);
- $a = new Authentication($this->container);
-
- $this->assertFalse($a->hasCaptcha('not_found'));
- $this->assertFalse($a->hasCaptcha('admin'));
-
- $this->assertTrue($u->incrementFailedLogin('admin'));
- $this->assertTrue($u->incrementFailedLogin('admin'));
- $this->assertTrue($u->incrementFailedLogin('admin'));
-
- $this->assertFalse($a->hasCaptcha('not_found'));
- $this->assertTrue($a->hasCaptcha('admin'));
- }
-
- public function testHandleFailedLogin()
- {
- $u = new User($this->container);
- $a = new Authentication($this->container);
-
- $this->assertFalse($u->isLocked('admin'));
-
- for ($i = 0; $i <= 6; $i++) {
- $a->handleFailedLogin('admin');
- }
-
- $this->assertTrue($u->isLocked('admin'));
- }
-}
diff --git a/tests/units/Model/ProjectDuplicationTest.php b/tests/units/Model/ProjectDuplicationTest.php
index e3234dfe..8b74675b 100644
--- a/tests/units/Model/ProjectDuplicationTest.php
+++ b/tests/units/Model/ProjectDuplicationTest.php
@@ -5,13 +5,14 @@ require_once __DIR__.'/../Base.php';
use Kanboard\Model\Action;
use Kanboard\Model\Project;
use Kanboard\Model\Category;
-use Kanboard\Model\ProjectPermission;
+use Kanboard\Model\ProjectUserRole;
use Kanboard\Model\ProjectDuplication;
use Kanboard\Model\User;
use Kanboard\Model\Swimlane;
use Kanboard\Model\Task;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
+use Kanboard\Core\Security\Role;
class ProjectDuplicationTest extends Base
{
@@ -72,10 +73,10 @@ class ProjectDuplicationTest extends Base
$this->assertEquals(0, $project['is_public']);
$this->assertEmpty($project['token']);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
- $this->assertEquals(array(1 => 'admin'), $pp->getMembers(1));
- $this->assertEquals(array(1 => 'admin'), $pp->getMembers(2));
+ $this->assertEquals(array(1 => 'admin'), $pp->getAssignableUsers(1));
+ $this->assertEquals(array(1 => 'admin'), $pp->getAssignableUsers(2));
}
public function testCloneProjectWithCategories()
@@ -114,7 +115,7 @@ class ProjectDuplicationTest extends Base
{
$p = new Project($this->container);
$c = new Category($this->container);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
$u = new User($this->container);
$pd = new ProjectDuplication($this->container);
@@ -123,15 +124,12 @@ class ProjectDuplicationTest extends Base
$this->assertEquals(4, $u->create(array('username' => 'unittest3', 'password' => 'unittest')));
$this->assertEquals(1, $p->create(array('name' => 'P1')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->addMember(1, 4));
- $this->assertTrue($pp->addManager(1, 3));
- $this->assertTrue($pp->isMember(1, 2));
- $this->assertTrue($pp->isMember(1, 3));
- $this->assertTrue($pp->isMember(1, 4));
- $this->assertFalse($pp->isManager(1, 2));
- $this->assertTrue($pp->isManager(1, 3));
- $this->assertFalse($pp->isManager(1, 4));
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(1, 3, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(1, 4, Role::PROJECT_MANAGER));
+ $this->assertEquals(Role::PROJECT_MEMBER, $pp->getUserRole(1, 2));
+ $this->assertEquals(Role::PROJECT_MEMBER, $pp->getUserRole(1, 3));
+ $this->assertEquals(Role::PROJECT_MANAGER, $pp->getUserRole(1, 4));
$this->assertEquals(2, $pd->duplicate(1));
@@ -139,13 +137,10 @@ class ProjectDuplicationTest extends Base
$this->assertNotEmpty($project);
$this->assertEquals('P1 (Clone)', $project['name']);
- $this->assertEquals(3, count($pp->getMembers(2)));
- $this->assertTrue($pp->isMember(2, 2));
- $this->assertTrue($pp->isMember(2, 3));
- $this->assertTrue($pp->isMember(2, 4));
- $this->assertFalse($pp->isManager(2, 2));
- $this->assertTrue($pp->isManager(2, 3));
- $this->assertFalse($pp->isManager(2, 4));
+ $this->assertEquals(3, count($pp->getUsers(2)));
+ $this->assertEquals(Role::PROJECT_MEMBER, $pp->getUserRole(2, 2));
+ $this->assertEquals(Role::PROJECT_MEMBER, $pp->getUserRole(2, 3));
+ $this->assertEquals(Role::PROJECT_MANAGER, $pp->getUserRole(2, 4));
}
public function testCloneProjectWithActionTaskAssignCurrentUser()
diff --git a/tests/units/Model/ProjectGroupRoleTest.php b/tests/units/Model/ProjectGroupRoleTest.php
new file mode 100644
index 00000000..52b83082
--- /dev/null
+++ b/tests/units/Model/ProjectGroupRoleTest.php
@@ -0,0 +1,340 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Model\Project;
+use Kanboard\Model\User;
+use Kanboard\Model\Group;
+use Kanboard\Model\GroupMember;
+use Kanboard\Model\ProjectGroupRole;
+use Kanboard\Core\Security\Role;
+
+class ProjectGroupRoleTest extends Base
+{
+ public function testGetUserRole()
+ {
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $groupModel->create('Group A'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 1));
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+
+ $this->assertEquals(Role::PROJECT_VIEWER, $groupRoleModel->getUserRole(1, 1));
+ $this->assertEquals('', $groupRoleModel->getUserRole(1, 2));
+ }
+
+ public function testAddGroup()
+ {
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $groupModel->create('Test'));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertFalse($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+
+ $groups = $groupRoleModel->getGroups(1);
+ $this->assertCount(1, $groups);
+ $this->assertEquals(1, $groups[0]['id']);
+ $this->assertEquals('Test', $groups[0]['name']);
+ $this->assertEquals(Role::PROJECT_VIEWER, $groups[0]['role']);
+ }
+
+ public function testRemoveGroup()
+ {
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $groupModel->create('Test'));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_MEMBER));
+ $this->assertTrue($groupRoleModel->removeGroup(1, 1));
+ $this->assertFalse($groupRoleModel->removeGroup(1, 1));
+
+ $this->assertEmpty($groupRoleModel->getGroups(1));
+ }
+
+ public function testChangeRole()
+ {
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $groupModel->create('Test'));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->changeGroupRole(1, 1, Role::PROJECT_MANAGER));
+
+ $groups = $groupRoleModel->getGroups(1);
+ $this->assertCount(1, $groups);
+ $this->assertEquals(1, $groups[0]['id']);
+ $this->assertEquals('Test', $groups[0]['name']);
+ $this->assertEquals(Role::PROJECT_MANAGER, $groups[0]['role']);
+ }
+
+ public function testGetGroups()
+ {
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $groupModel->create('Group C'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+ $this->assertEquals(3, $groupModel->create('Group A'));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER));
+
+ $groups = $groupRoleModel->getGroups(1);
+ $this->assertCount(3, $groups);
+
+ $this->assertEquals(3, $groups[0]['id']);
+ $this->assertEquals('Group A', $groups[0]['name']);
+ $this->assertEquals(Role::PROJECT_MANAGER, $groups[0]['role']);
+
+ $this->assertEquals(2, $groups[1]['id']);
+ $this->assertEquals('Group B', $groups[1]['name']);
+ $this->assertEquals(Role::PROJECT_MEMBER, $groups[1]['role']);
+
+ $this->assertEquals(1, $groups[2]['id']);
+ $this->assertEquals('Group C', $groups[2]['name']);
+ $this->assertEquals(Role::PROJECT_VIEWER, $groups[2]['role']);
+ }
+
+ public function testGetUsers()
+ {
+ $userModel = new User($this->container);
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project 2')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user 1', 'name' => 'User #1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user 2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user 3')));
+
+ $this->assertEquals(1, $groupModel->create('Group A'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+ $this->assertEquals(3, $groupModel->create('Group C'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 4));
+ $this->assertTrue($groupMemberModel->addUser(2, 3));
+ $this->assertTrue($groupMemberModel->addUser(3, 2));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER));
+
+ $users = $groupRoleModel->getUsers(2);
+ $this->assertCount(0, $users);
+
+ $users = $groupRoleModel->getUsers(1);
+ $this->assertCount(3, $users);
+
+ $this->assertEquals(2, $users[0]['id']);
+ $this->assertEquals('user 1', $users[0]['username']);
+ $this->assertEquals('User #1', $users[0]['name']);
+ $this->assertEquals(Role::PROJECT_MANAGER, $users[0]['role']);
+
+ $this->assertEquals(3, $users[1]['id']);
+ $this->assertEquals('user 2', $users[1]['username']);
+ $this->assertEquals('', $users[1]['name']);
+ $this->assertEquals(Role::PROJECT_MEMBER, $users[1]['role']);
+
+ $this->assertEquals(4, $users[2]['id']);
+ $this->assertEquals('user 3', $users[2]['username']);
+ $this->assertEquals('', $users[2]['name']);
+ $this->assertEquals(Role::PROJECT_VIEWER, $users[2]['role']);
+ }
+
+ public function testGetAssignableUsers()
+ {
+ $userModel = new User($this->container);
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project 2')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user 1', 'name' => 'User #1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user 2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user 3')));
+
+ $this->assertEquals(1, $groupModel->create('Group A'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+ $this->assertEquals(3, $groupModel->create('Group C'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 4));
+ $this->assertTrue($groupMemberModel->addUser(2, 3));
+ $this->assertTrue($groupMemberModel->addUser(3, 2));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER));
+
+ $users = $groupRoleModel->getAssignableUsers(2);
+ $this->assertCount(0, $users);
+
+ $users = $groupRoleModel->getAssignableUsers(1);
+ $this->assertCount(2, $users);
+
+ $this->assertEquals(2, $users[0]['id']);
+ $this->assertEquals('user 1', $users[0]['username']);
+ $this->assertEquals('User #1', $users[0]['name']);
+
+ $this->assertEquals(3, $users[1]['id']);
+ $this->assertEquals('user 2', $users[1]['username']);
+ $this->assertEquals('', $users[1]['name']);
+ }
+
+ public function testGetProjectsByUser()
+ {
+ $userModel = new User($this->container);
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project 2')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user 1', 'name' => 'User #1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user 2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user 3')));
+ $this->assertEquals(5, $userModel->create(array('username' => 'user 4')));
+
+ $this->assertEquals(1, $groupModel->create('Group C'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+ $this->assertEquals(3, $groupModel->create('Group A'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 4));
+ $this->assertTrue($groupMemberModel->addUser(2, 5));
+ $this->assertTrue($groupMemberModel->addUser(3, 3));
+ $this->assertTrue($groupMemberModel->addUser(3, 2));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(2, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER));
+
+ $projects = $groupRoleModel->getProjectsByUser(2);
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $groupRoleModel->getProjectsByUser(3);
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $groupRoleModel->getProjectsByUser(4);
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $groupRoleModel->getProjectsByUser(5);
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 2', $projects[2]);
+ }
+
+ public function testGetActiveProjectsByUser()
+ {
+ $userModel = new User($this->container);
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1', 'is_active' => 0)));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project 2')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user 1', 'name' => 'User #1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user 2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user 3')));
+ $this->assertEquals(5, $userModel->create(array('username' => 'user 4')));
+
+ $this->assertEquals(1, $groupModel->create('Group C'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+ $this->assertEquals(3, $groupModel->create('Group A'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 4));
+ $this->assertTrue($groupMemberModel->addUser(2, 5));
+ $this->assertTrue($groupMemberModel->addUser(3, 3));
+ $this->assertTrue($groupMemberModel->addUser(3, 2));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(2, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER));
+
+ $projects = $groupRoleModel->getProjectsByUser(2, array(Project::ACTIVE));
+ $this->assertCount(0, $projects);
+
+ $projects = $groupRoleModel->getProjectsByUser(3, array(Project::ACTIVE));
+ $this->assertCount(0, $projects);
+
+ $projects = $groupRoleModel->getProjectsByUser(4, array(Project::ACTIVE));
+ $this->assertCount(0, $projects);
+
+ $projects = $groupRoleModel->getProjectsByUser(5, array(Project::ACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 2', $projects[2]);
+ }
+
+ public function testGetInactiveProjectsByUser()
+ {
+ $userModel = new User($this->container);
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1', 'is_active' => 0)));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project 2')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user 1', 'name' => 'User #1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user 2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user 3')));
+ $this->assertEquals(5, $userModel->create(array('username' => 'user 4')));
+
+ $this->assertEquals(1, $groupModel->create('Group C'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+ $this->assertEquals(3, $groupModel->create('Group A'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 4));
+ $this->assertTrue($groupMemberModel->addUser(2, 5));
+ $this->assertTrue($groupMemberModel->addUser(3, 3));
+ $this->assertTrue($groupMemberModel->addUser(3, 2));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(2, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER));
+
+ $projects = $groupRoleModel->getProjectsByUser(2, array(Project::INACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $groupRoleModel->getProjectsByUser(3, array(Project::INACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $groupRoleModel->getProjectsByUser(4, array(Project::INACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $groupRoleModel->getProjectsByUser(5, array(Project::INACTIVE));
+ $this->assertCount(0, $projects);
+ }
+}
diff --git a/tests/units/Model/ProjectPermissionTest.php b/tests/units/Model/ProjectPermissionTest.php
index 1ee63a76..8f118cd9 100644
--- a/tests/units/Model/ProjectPermissionTest.php
+++ b/tests/units/Model/ProjectPermissionTest.php
@@ -2,286 +2,54 @@
require_once __DIR__.'/../Base.php';
-use Kanboard\Model\Project;
use Kanboard\Model\ProjectPermission;
+use Kanboard\Model\Project;
use Kanboard\Model\User;
+use Kanboard\Model\Group;
+use Kanboard\Model\GroupMember;
+use Kanboard\Model\ProjectGroupRole;
+use Kanboard\Model\ProjectUserRole;
+use Kanboard\Core\Security\Role;
class ProjectPermissionTest extends Base
{
- public function testAllowEverybody()
- {
- $user = new User($this->container);
- $this->assertNotFalse($user->create(array('username' => 'unittest#1', 'password' => 'unittest')));
- $this->assertNotFalse($user->create(array('username' => 'unittest#2', 'password' => 'unittest')));
-
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
- $this->assertFalse($pp->isEverybodyAllowed(1));
- $this->assertTrue($pp->isUserAllowed(1, 1));
- $this->assertFalse($pp->isUserAllowed(1, 2));
- $this->assertFalse($pp->isUserAllowed(1, 3));
- $this->assertEquals(array(), $pp->getMembers(1));
- $this->assertEquals(array('Unassigned'), $pp->getMemberList(1));
-
- $this->assertEmpty($pp->getMemberProjects(1));
- $this->assertEmpty($pp->getMemberProjects(2));
- $this->assertEmpty($pp->getMemberProjects(3));
-
- $this->assertEmpty($pp->getMemberProjectIds(1));
- $this->assertEmpty($pp->getMemberProjectIds(2));
- $this->assertEmpty($pp->getMemberProjectIds(3));
-
- $this->assertEmpty($pp->getActiveMemberProjectIds(1));
- $this->assertEmpty($pp->getActiveMemberProjectIds(2));
- $this->assertEmpty($pp->getActiveMemberProjectIds(3));
-
- $this->assertEmpty($pp->getActiveMemberProjects(1));
- $this->assertEmpty($pp->getActiveMemberProjects(2));
- $this->assertEmpty($pp->getActiveMemberProjects(3));
-
- $this->assertTrue($p->update(array('id' => 1, 'is_everybody_allowed' => 1)));
- $this->assertTrue($pp->isEverybodyAllowed(1));
- $this->assertTrue($pp->isUserAllowed(1, 1));
- $this->assertTrue($pp->isUserAllowed(1, 2));
- $this->assertTrue($pp->isUserAllowed(1, 3));
- $this->assertEquals(array('1' => 'admin', '2' => 'unittest#1', '3' => 'unittest#2'), $pp->getMembers(1));
- $this->assertEquals(array('Unassigned', '1' => 'admin', '2' => 'unittest#1', '3' => 'unittest#2'), $pp->getMemberList(1));
-
- $this->assertNotEmpty($pp->getMemberProjects(1));
- $this->assertNotEmpty($pp->getMemberProjects(2));
- $this->assertNotEmpty($pp->getMemberProjects(3));
-
- $this->assertNotEmpty($pp->getMemberProjectIds(1));
- $this->assertNotEmpty($pp->getMemberProjectIds(2));
- $this->assertNotEmpty($pp->getMemberProjectIds(3));
-
- $this->assertNotEmpty($pp->getActiveMemberProjectIds(1));
- $this->assertNotEmpty($pp->getActiveMemberProjectIds(2));
- $this->assertNotEmpty($pp->getActiveMemberProjectIds(3));
-
- $this->assertNotEmpty($pp->getActiveMemberProjects(1));
- $this->assertNotEmpty($pp->getActiveMemberProjects(2));
- $this->assertNotEmpty($pp->getActiveMemberProjects(3));
-
- $this->assertTrue($p->disable(1));
-
- $this->assertEmpty($pp->getActiveMemberProjectIds(1));
- $this->assertEmpty($pp->getActiveMemberProjectIds(2));
- $this->assertEmpty($pp->getActiveMemberProjectIds(3));
-
- $this->assertEmpty($pp->getActiveMemberProjects(1));
- $this->assertEmpty($pp->getActiveMemberProjects(2));
- $this->assertEmpty($pp->getActiveMemberProjects(3));
- }
-
- public function testDisallowEverybody()
- {
- // We create a regular user
- $user = new User($this->container);
- $user->create(array('username' => 'unittest', 'password' => 'unittest'));
-
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
-
- $this->assertEmpty($pp->getMembers(1)); // Nobody is specified for the given project
- $this->assertTrue($pp->isUserAllowed(1, 1)); // Admin should be allowed
- $this->assertFalse($pp->isUserAllowed(1, 2)); // Regular user should be denied
- }
-
- public function testAllowUser()
- {
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $user = new User($this->container);
-
- $this->assertNotFalse($user->create(array('username' => 'unittest', 'password' => 'unittest')));
-
- // We create a project
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
-
- $this->assertEmpty($pp->getMemberProjects(1));
- $this->assertEmpty($pp->getMemberProjects(2));
-
- $this->assertEmpty($pp->getMemberProjectIds(1));
- $this->assertEmpty($pp->getMemberProjectIds(2));
-
- $this->assertEmpty($pp->getActiveMemberProjectIds(1));
- $this->assertEmpty($pp->getActiveMemberProjectIds(2));
-
- $this->assertEmpty($pp->getActiveMemberProjects(1));
- $this->assertEmpty($pp->getActiveMemberProjects(2));
-
- // We allow the admin user
- $this->assertTrue($pp->addMember(1, 1));
- $this->assertTrue($pp->addMember(1, 2));
-
- // Non-existant project
- $this->assertFalse($pp->addMember(50, 1));
-
- // Non-existant user
- $this->assertFalse($pp->addMember(1, 50));
-
- // Both users should be allowed
- $this->assertEquals(array('1' => 'admin', '2' => 'unittest'), $pp->getMembers(1));
- $this->assertTrue($pp->isUserAllowed(1, 1));
- $this->assertTrue($pp->isUserAllowed(1, 2));
-
- $this->assertNotEmpty($pp->getMemberProjects(1));
- $this->assertNotEmpty($pp->getMemberProjects(2));
-
- $this->assertNotEmpty($pp->getMemberProjectIds(1));
- $this->assertNotEmpty($pp->getMemberProjectIds(2));
-
- $this->assertNotEmpty($pp->getActiveMemberProjectIds(1));
- $this->assertNotEmpty($pp->getActiveMemberProjectIds(2));
-
- $this->assertNotEmpty($pp->getActiveMemberProjects(1));
- $this->assertNotEmpty($pp->getActiveMemberProjects(2));
- }
-
- public function testRevokeUser()
- {
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $user = new User($this->container);
-
- $user->create(array('username' => 'unittest', 'password' => 'unittest'));
-
- // We create a project
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
-
- // We revoke our admin user (not existing row)
- $this->assertFalse($pp->revokeMember(1, 1));
-
- // We should have nobody in the users list
- $this->assertEmpty($pp->getMembers(1));
-
- // Only admin is allowed
- $this->assertTrue($pp->isUserAllowed(1, 1));
- $this->assertFalse($pp->isUserAllowed(1, 2));
-
- // We allow only the regular user
- $this->assertTrue($pp->addMember(1, 2));
-
- // All users should be allowed (admin and regular)
- $this->assertTrue($pp->isUserAllowed(1, 1));
- $this->assertTrue($pp->isUserAllowed(1, 2));
-
- // However, we should have only our regular user in the list
- $this->assertEquals(array('2' => 'unittest'), $pp->getMembers(1));
-
- // We allow our admin, we should have both in the list
- $this->assertTrue($pp->addMember(1, 1));
- $this->assertEquals(array('1' => 'admin', '2' => 'unittest'), $pp->getMembers(1));
- $this->assertTrue($pp->isUserAllowed(1, 1));
- $this->assertTrue($pp->isUserAllowed(1, 2));
-
- // We revoke the regular user
- $this->assertTrue($pp->revokeMember(1, 2));
-
- // Only admin should be allowed
- $this->assertTrue($pp->isUserAllowed(1, 1));
- $this->assertFalse($pp->isUserAllowed(1, 2));
-
- // We should have only admin in the list
- $this->assertEquals(array('1' => 'admin'), $pp->getMembers(1));
-
- // We revoke the admin user
- $this->assertTrue($pp->revokeMember(1, 1));
- $this->assertEmpty($pp->getMembers(1));
-
- // Only admin should be allowed again
- $this->assertTrue($pp->isUserAllowed(1, 1));
- $this->assertFalse($pp->isUserAllowed(1, 2));
- }
-
- public function testManager()
- {
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
- $u = new User($this->container);
-
- $this->assertEquals(2, $u->create(array('username' => 'unittest', 'password' => 'unittest')));
-
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest1')));
- $this->assertFalse($pp->isMember(1, 2));
- $this->assertFalse($pp->isManager(1, 2));
-
- $this->assertEquals(2, $p->create(array('name' => 'UnitTest2'), 1, true));
- $this->assertFalse($pp->isMember(2, 2));
- $this->assertFalse($pp->isManager(2, 2));
-
- $this->assertEquals(3, $p->create(array('name' => 'UnitTest3'), 2, true));
- $this->assertTrue($pp->isMember(3, 2));
- $this->assertTrue($pp->isManager(3, 2));
-
- $this->assertEquals(4, $p->create(array('name' => 'UnitTest4')));
-
- $this->assertTrue($pp->addManager(4, 2));
- $this->assertTrue($pp->isMember(4, 2));
- $this->assertTrue($pp->isManager(4, 2));
-
- $this->assertEquals(5, $p->create(array('name' => 'UnitTest5')));
- $this->assertTrue($pp->addMember(5, 2));
- $this->assertTrue($pp->changeRole(5, 2, 1));
- $this->assertTrue($pp->isMember(5, 2));
- $this->assertTrue($pp->isManager(5, 2));
- $this->assertTrue($pp->changeRole(5, 2, 0));
- $this->assertTrue($pp->isMember(5, 2));
- $this->assertFalse($pp->isManager(5, 2));
- }
-
- public function testUsersList()
+ public function testDuplicate()
{
- $p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
-
- $user = new User($this->container);
- $this->assertNotFalse($user->create(array('username' => 'unittest', 'password' => 'unittest')));
-
- // We create project
- $this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
-
- // No restriction, we should have no body
- $this->assertEquals(
- array('Unassigned'),
- $pp->getMemberList(1)
- );
-
- // We allow only the regular user
- $this->assertTrue($pp->addMember(1, 2));
-
- $this->assertEquals(
- array(0 => 'Unassigned', 2 => 'unittest'),
- $pp->getMemberList(1)
- );
-
- // We allow the admin user
- $this->assertTrue($pp->addMember(1, 1));
-
- $this->assertEquals(
- array(0 => 'Unassigned', 1 => 'admin', 2 => 'unittest'),
- $pp->getMemberList(1)
- );
-
- // We revoke only the regular user
- $this->assertTrue($pp->revokeMember(1, 2));
-
- $this->assertEquals(
- array(0 => 'Unassigned', 1 => 'admin'),
- $pp->getMemberList(1)
- );
-
- // We revoke only the admin user, we should have everybody
- $this->assertTrue($pp->revokeMember(1, 1));
-
- $this->assertEquals(
- array(0 => 'Unassigned'),
- $pp->getMemberList(1)
- );
+ $userModel = new User($this->container);
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+ $projectPermission = new ProjectPermission($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project 2')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user 1', 'name' => 'User #1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user 2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user 3')));
+ $this->assertEquals(5, $userModel->create(array('username' => 'user 4')));
+ $this->assertEquals(6, $userModel->create(array('username' => 'user 5', 'name' => 'User #5')));
+
+ $this->assertEquals(1, $groupModel->create('Group C'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+ $this->assertEquals(3, $groupModel->create('Group A'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 4));
+ $this->assertTrue($groupMemberModel->addUser(2, 5));
+ $this->assertTrue($groupMemberModel->addUser(3, 3));
+ $this->assertTrue($groupMemberModel->addUser(3, 2));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER));
+
+ $this->assertTrue($userRoleModel->addUser(1, 5, Role::PROJECT_MANAGER));
+ $this->assertTrue($userRoleModel->addUser(1, 6, Role::PROJECT_MEMBER));
+
+ $this->assertTrue($projectPermission->duplicate(1, 2));
+
+ $this->assertCount(2, $userRoleModel->getUsers(2));
+ $this->assertCount(3, $groupRoleModel->getUsers(2));
}
}
diff --git a/tests/units/Model/ProjectTest.php b/tests/units/Model/ProjectTest.php
index 56791700..4d8e6fc7 100644
--- a/tests/units/Model/ProjectTest.php
+++ b/tests/units/Model/ProjectTest.php
@@ -5,7 +5,6 @@ require_once __DIR__.'/../Base.php';
use Kanboard\Core\Translator;
use Kanboard\Subscriber\ProjectModificationDateSubscriber;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
use Kanboard\Model\User;
use Kanboard\Model\Task;
use Kanboard\Model\TaskCreation;
diff --git a/tests/units/Model/ProjectUserRoleTest.php b/tests/units/Model/ProjectUserRoleTest.php
new file mode 100644
index 00000000..c6b4eb7c
--- /dev/null
+++ b/tests/units/Model/ProjectUserRoleTest.php
@@ -0,0 +1,400 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Model\Project;
+use Kanboard\Model\User;
+use Kanboard\Model\Group;
+use Kanboard\Model\GroupMember;
+use Kanboard\Model\ProjectGroupRole;
+use Kanboard\Model\ProjectUserRole;
+use Kanboard\Core\Security\Role;
+
+class ProjectUserRoleTest extends Base
+{
+ public function testAddUser()
+ {
+ $projectModel = new Project($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+
+ $this->assertTrue($userRoleModel->addUser(1, 1, Role::PROJECT_VIEWER));
+ $this->assertFalse($userRoleModel->addUser(1, 1, Role::PROJECT_VIEWER));
+
+ $users = $userRoleModel->getUsers(1);
+ $this->assertCount(1, $users);
+ $this->assertEquals(1, $users[0]['id']);
+ $this->assertEquals('admin', $users[0]['username']);
+ $this->assertEquals('', $users[0]['name']);
+ $this->assertEquals(Role::PROJECT_VIEWER, $users[0]['role']);
+ }
+
+ public function testRemoveUser()
+ {
+ $projectModel = new Project($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+
+ $this->assertTrue($userRoleModel->addUser(1, 1, Role::PROJECT_MEMBER));
+ $this->assertTrue($userRoleModel->removeUser(1, 1));
+ $this->assertFalse($userRoleModel->removeUser(1, 1));
+
+ $this->assertEmpty($userRoleModel->getUsers(1));
+ }
+
+ public function testChangeRole()
+ {
+ $projectModel = new Project($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+
+ $this->assertTrue($userRoleModel->addUser(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($userRoleModel->changeUserRole(1, 1, Role::PROJECT_MANAGER));
+
+ $users = $userRoleModel->getUsers(1);
+ $this->assertCount(1, $users);
+ $this->assertEquals(1, $users[0]['id']);
+ $this->assertEquals('admin', $users[0]['username']);
+ $this->assertEquals('', $users[0]['name']);
+ $this->assertEquals(Role::PROJECT_MANAGER, $users[0]['role']);
+ }
+
+ public function testGetRole()
+ {
+ $projectModel = new Project($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEmpty($userRoleModel->getUserRole(1, 1));
+
+ $this->assertTrue($userRoleModel->addUser(1, 1, Role::PROJECT_VIEWER));
+ $this->assertEquals(Role::PROJECT_VIEWER, $userRoleModel->getUserRole(1, 1));
+
+ $this->assertTrue($userRoleModel->changeUserRole(1, 1, Role::PROJECT_MEMBER));
+ $this->assertEquals(Role::PROJECT_MEMBER, $userRoleModel->getUserRole(1, 1));
+
+ $this->assertTrue($userRoleModel->changeUserRole(1, 1, Role::PROJECT_MANAGER));
+ $this->assertEquals(Role::PROJECT_MANAGER, $userRoleModel->getUserRole(1, 1));
+
+ $this->assertEquals('', $userRoleModel->getUserRole(1, 2));
+ }
+
+ public function testGetRoleWithGroups()
+ {
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $groupModel->create('Group A'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 1));
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+
+ $this->assertEquals(Role::PROJECT_VIEWER, $userRoleModel->getUserRole(1, 1));
+ $this->assertEquals('', $userRoleModel->getUserRole(1, 2));
+ }
+
+ public function testGetAssignableUsersWithoutGroups()
+ {
+ $projectModel = new Project($this->container);
+ $userModel = new User($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(2, $userModel->create(array('username' => 'user1', 'name' => 'User1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user2', 'name' => 'User2')));
+
+ $this->assertTrue($userRoleModel->addUser(1, 1, Role::PROJECT_MEMBER));
+ $this->assertTrue($userRoleModel->addUser(1, 2, Role::PROJECT_MANAGER));
+ $this->assertTrue($userRoleModel->addUser(1, 3, Role::PROJECT_VIEWER));
+
+ $users = $userRoleModel->getAssignableUsers(1);
+ $this->assertCount(2, $users);
+
+ $this->assertEquals('admin', $users[1]);
+ $this->assertEquals('User1', $users[2]);
+ }
+
+ public function testGetAssignableUsersWithGroups()
+ {
+ $projectModel = new Project($this->container);
+ $userModel = new User($this->container);
+ $groupModel = new Group($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user1', 'name' => 'User1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user2', 'name' => 'User2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user3', 'name' => 'User3')));
+ $this->assertEquals(5, $userModel->create(array('username' => 'user4', 'name' => 'User4')));
+
+ $this->assertTrue($userRoleModel->addUser(1, 1, Role::PROJECT_MEMBER));
+ $this->assertTrue($userRoleModel->addUser(1, 2, Role::PROJECT_MANAGER));
+ $this->assertTrue($userRoleModel->addUser(1, 3, Role::PROJECT_VIEWER));
+
+ $this->assertEquals(1, $groupModel->create('Group A'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 4));
+ $this->assertTrue($groupMemberModel->addUser(2, 5));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 2, Role::PROJECT_MEMBER));
+
+ $users = $userRoleModel->getAssignableUsers(1);
+ $this->assertCount(3, $users);
+
+ $this->assertEquals('admin', $users[1]);
+ $this->assertEquals('User1', $users[2]);
+ $this->assertEquals('User4', $users[5]);
+ }
+
+ public function testGetAssignableUsersList()
+ {
+ $projectModel = new Project($this->container);
+ $userModel = new User($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Test2')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user1', 'name' => 'User1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user2', 'name' => 'User2')));
+
+ $this->assertTrue($userRoleModel->addUser(2, 1, Role::PROJECT_MEMBER));
+ $this->assertTrue($userRoleModel->addUser(1, 1, Role::PROJECT_MEMBER));
+ $this->assertTrue($userRoleModel->addUser(1, 2, Role::PROJECT_MANAGER));
+ $this->assertTrue($userRoleModel->addUser(1, 3, Role::PROJECT_VIEWER));
+
+ $users = $userRoleModel->getAssignableUsersList(1);
+ $this->assertCount(3, $users);
+
+ $this->assertEquals('Unassigned', $users[0]);
+ $this->assertEquals('admin', $users[1]);
+ $this->assertEquals('User1', $users[2]);
+
+ $users = $userRoleModel->getAssignableUsersList(1, true, true, true);
+ $this->assertCount(4, $users);
+
+ $this->assertEquals('Unassigned', $users[0]);
+ $this->assertEquals('Everybody', $users[-1]);
+ $this->assertEquals('admin', $users[1]);
+ $this->assertEquals('User1', $users[2]);
+
+ $users = $userRoleModel->getAssignableUsersList(2, true, true, true);
+ $this->assertCount(1, $users);
+
+ $this->assertEquals('admin', $users[1]);
+ }
+
+ public function testGetAssignableUsersWithEverybodyAllowed()
+ {
+ $projectModel = new Project($this->container);
+ $userModel = new User($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test', 'is_everybody_allowed' => 1)));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user1', 'name' => 'User1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user2', 'name' => 'User2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user3', 'name' => 'User3')));
+ $this->assertEquals(5, $userModel->create(array('username' => 'user4', 'name' => 'User4')));
+
+ $users = $userRoleModel->getAssignableUsers(1);
+ $this->assertCount(5, $users);
+
+ $this->assertEquals('admin', $users[1]);
+ $this->assertEquals('User1', $users[2]);
+ $this->assertEquals('User2', $users[3]);
+ $this->assertEquals('User3', $users[4]);
+ $this->assertEquals('User4', $users[5]);
+ }
+
+ public function testGetProjectsByUser()
+ {
+ $userModel = new User($this->container);
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project 2')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user 1', 'name' => 'User #1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user 2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user 3')));
+ $this->assertEquals(5, $userModel->create(array('username' => 'user 4')));
+ $this->assertEquals(6, $userModel->create(array('username' => 'user 5', 'name' => 'User #5')));
+ $this->assertEquals(7, $userModel->create(array('username' => 'user 6')));
+
+ $this->assertEquals(1, $groupModel->create('Group C'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+ $this->assertEquals(3, $groupModel->create('Group A'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 4));
+ $this->assertTrue($groupMemberModel->addUser(2, 5));
+ $this->assertTrue($groupMemberModel->addUser(3, 3));
+ $this->assertTrue($groupMemberModel->addUser(3, 2));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(2, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER));
+
+ $this->assertTrue($userRoleModel->addUser(1, 6, Role::PROJECT_MANAGER));
+ $this->assertTrue($userRoleModel->addUser(2, 6, Role::PROJECT_MEMBER));
+ $this->assertTrue($userRoleModel->addUser(2, 7, Role::PROJECT_MEMBER));
+
+ $projects = $userRoleModel->getProjectsByUser(2);
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $userRoleModel->getProjectsByUser(3);
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $userRoleModel->getProjectsByUser(4);
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $userRoleModel->getProjectsByUser(5);
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 2', $projects[2]);
+
+ $projects = $userRoleModel->getProjectsByUser(6);
+ $this->assertCount(2, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+ $this->assertEquals('Project 2', $projects[2]);
+
+ $projects = $userRoleModel->getProjectsByUser(7);
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 2', $projects[2]);
+ }
+
+ public function testGetActiveProjectsByUser()
+ {
+ $userModel = new User($this->container);
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1', 'is_active' => 0)));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project 2')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user 1', 'name' => 'User #1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user 2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user 3')));
+ $this->assertEquals(5, $userModel->create(array('username' => 'user 4')));
+ $this->assertEquals(6, $userModel->create(array('username' => 'user 5', 'name' => 'User #5')));
+ $this->assertEquals(7, $userModel->create(array('username' => 'user 6')));
+
+ $this->assertEquals(1, $groupModel->create('Group C'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+ $this->assertEquals(3, $groupModel->create('Group A'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 4));
+ $this->assertTrue($groupMemberModel->addUser(2, 5));
+ $this->assertTrue($groupMemberModel->addUser(3, 3));
+ $this->assertTrue($groupMemberModel->addUser(3, 2));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(2, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER));
+
+ $this->assertTrue($userRoleModel->addUser(1, 6, Role::PROJECT_MANAGER));
+ $this->assertTrue($userRoleModel->addUser(2, 6, Role::PROJECT_MEMBER));
+ $this->assertTrue($userRoleModel->addUser(2, 7, Role::PROJECT_MEMBER));
+
+ $projects = $userRoleModel->getProjectsByUser(2, array(Project::ACTIVE));
+ $this->assertCount(0, $projects);
+
+ $projects = $userRoleModel->getProjectsByUser(3, array(Project::ACTIVE));
+ $this->assertCount(0, $projects);
+
+ $projects = $userRoleModel->getProjectsByUser(4, array(Project::ACTIVE));
+ $this->assertCount(0, $projects);
+
+ $projects = $userRoleModel->getProjectsByUser(5, array(Project::ACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 2', $projects[2]);
+
+ $projects = $userRoleModel->getProjectsByUser(6, array(Project::ACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 2', $projects[2]);
+
+ $projects = $userRoleModel->getProjectsByUser(7, array(Project::ACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 2', $projects[2]);
+ }
+
+ public function testGetInactiveProjectsByUser()
+ {
+ $userModel = new User($this->container);
+ $projectModel = new Project($this->container);
+ $groupModel = new Group($this->container);
+ $groupMemberModel = new GroupMember($this->container);
+ $groupRoleModel = new ProjectGroupRole($this->container);
+ $userRoleModel = new ProjectUserRole($this->container);
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project 1', 'is_active' => 0)));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project 2')));
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'user 1', 'name' => 'User #1')));
+ $this->assertEquals(3, $userModel->create(array('username' => 'user 2')));
+ $this->assertEquals(4, $userModel->create(array('username' => 'user 3')));
+ $this->assertEquals(5, $userModel->create(array('username' => 'user 4')));
+ $this->assertEquals(6, $userModel->create(array('username' => 'user 5', 'name' => 'User #5')));
+ $this->assertEquals(7, $userModel->create(array('username' => 'user 6')));
+
+ $this->assertEquals(1, $groupModel->create('Group C'));
+ $this->assertEquals(2, $groupModel->create('Group B'));
+ $this->assertEquals(3, $groupModel->create('Group A'));
+
+ $this->assertTrue($groupMemberModel->addUser(1, 4));
+ $this->assertTrue($groupMemberModel->addUser(2, 5));
+ $this->assertTrue($groupMemberModel->addUser(3, 3));
+ $this->assertTrue($groupMemberModel->addUser(3, 2));
+
+ $this->assertTrue($groupRoleModel->addGroup(1, 1, Role::PROJECT_VIEWER));
+ $this->assertTrue($groupRoleModel->addGroup(2, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($groupRoleModel->addGroup(1, 3, Role::PROJECT_MANAGER));
+
+ $this->assertTrue($userRoleModel->addUser(1, 6, Role::PROJECT_MANAGER));
+ $this->assertTrue($userRoleModel->addUser(2, 6, Role::PROJECT_MEMBER));
+ $this->assertTrue($userRoleModel->addUser(2, 7, Role::PROJECT_MEMBER));
+
+ $projects = $userRoleModel->getProjectsByUser(2, array(Project::INACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $userRoleModel->getProjectsByUser(3, array(Project::INACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $userRoleModel->getProjectsByUser(4, array(Project::INACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $userRoleModel->getProjectsByUser(5, array(Project::INACTIVE));
+ $this->assertCount(0, $projects);
+
+ $projects = $userRoleModel->getProjectsByUser(6, array(Project::INACTIVE));
+ $this->assertCount(1, $projects);
+ $this->assertEquals('Project 1', $projects[1]);
+
+ $projects = $userRoleModel->getProjectsByUser(7, array(Project::INACTIVE));
+ $this->assertCount(0, $projects);
+ }
+}
diff --git a/tests/units/Model/SubtaskTest.php b/tests/units/Model/SubtaskTest.php
index e446e104..9be2dff4 100644
--- a/tests/units/Model/SubtaskTest.php
+++ b/tests/units/Model/SubtaskTest.php
@@ -8,7 +8,7 @@ use Kanboard\Model\Subtask;
use Kanboard\Model\Project;
use Kanboard\Model\Category;
use Kanboard\Model\User;
-use Kanboard\Model\UserSession;
+use Kanboard\Core\User\UserSession;
class SubtaskTest extends Base
{
diff --git a/tests/units/Model/TaskCreationTest.php b/tests/units/Model/TaskCreationTest.php
index 5de0a5cc..e35f1bc7 100644
--- a/tests/units/Model/TaskCreationTest.php
+++ b/tests/units/Model/TaskCreationTest.php
@@ -8,7 +8,6 @@ use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\TaskStatus;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
class TaskCreationTest extends Base
{
diff --git a/tests/units/Model/TaskDuplicationTest.php b/tests/units/Model/TaskDuplicationTest.php
index d65e8f28..798b3835 100644
--- a/tests/units/Model/TaskDuplicationTest.php
+++ b/tests/units/Model/TaskDuplicationTest.php
@@ -8,10 +8,11 @@ use Kanboard\Model\TaskDuplication;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\TaskStatus;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
+use Kanboard\Model\ProjectUserRole;
use Kanboard\Model\Category;
use Kanboard\Model\User;
use Kanboard\Model\Swimlane;
+use Kanboard\Core\Security\Role;
class TaskDuplicationTest extends Base
{
@@ -127,7 +128,7 @@ class TaskDuplicationTest extends Base
// Check the values of the duplicated task
$task = $tf->getById(2);
$this->assertNotEmpty($task);
- $this->assertEquals(1, $task['owner_id']);
+ $this->assertEquals(0, $task['owner_id']);
$this->assertEquals(0, $task['category_id']);
$this->assertEquals(0, $task['swimlane_id']);
$this->assertEquals(6, $task['column_id']);
@@ -333,7 +334,7 @@ class TaskDuplicationTest extends Base
$tc = new TaskCreation($this->container);
$tf = new TaskFinder($this->container);
$p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
// We create 2 projects
$this->assertEquals(1, $p->create(array('name' => 'test1')));
@@ -357,10 +358,8 @@ class TaskDuplicationTest extends Base
// We create a new user for our project
$user = new User($this->container);
$this->assertNotFalse($user->create(array('username' => 'unittest#1', 'password' => 'unittest')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->addMember(2, 2));
- $this->assertTrue($pp->isUserAllowed(1, 2));
- $this->assertTrue($pp->isUserAllowed(2, 2));
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(2, 2, Role::PROJECT_MEMBER));
// We duplicate our task to the 2nd project
$this->assertEquals(3, $td->duplicateToProject(1, 2));
@@ -391,7 +390,7 @@ class TaskDuplicationTest extends Base
$tc = new TaskCreation($this->container);
$tf = new TaskFinder($this->container);
$p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
+ $pr = new ProjectUserRole($this->container);
// We create 2 projects
$this->assertEquals(1, $p->create(array('name' => 'test1')));
@@ -399,6 +398,7 @@ class TaskDuplicationTest extends Base
// We create a task
$this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2)));
+ $this->assertTrue($pr->addUser(2, 1, Role::PROJECT_MEMBER));
// We duplicate our task to the 2nd project
$this->assertEquals(2, $td->duplicateToProject(1, 2, null, null, null, 1));
@@ -425,7 +425,6 @@ class TaskDuplicationTest extends Base
$tc = new TaskCreation($this->container);
$tf = new TaskFinder($this->container);
$p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
$user = new User($this->container);
// We create 2 projects
@@ -446,7 +445,7 @@ class TaskDuplicationTest extends Base
// Check the values of the moved task
$task = $tf->getById(1);
$this->assertNotEmpty($task);
- $this->assertEquals(1, $task['owner_id']);
+ $this->assertEquals(0, $task['owner_id']);
$this->assertEquals(0, $task['category_id']);
$this->assertEquals(0, $task['swimlane_id']);
$this->assertEquals(2, $task['project_id']);
@@ -496,7 +495,7 @@ class TaskDuplicationTest extends Base
$tc = new TaskCreation($this->container);
$tf = new TaskFinder($this->container);
$p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
$user = new User($this->container);
// We create 2 projects
@@ -505,10 +504,8 @@ class TaskDuplicationTest extends Base
// We create a new user for our project
$this->assertNotFalse($user->create(array('username' => 'unittest#1', 'password' => 'unittest')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->addMember(2, 2));
- $this->assertTrue($pp->isUserAllowed(1, 2));
- $this->assertTrue($pp->isUserAllowed(2, 2));
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(2, 2, Role::PROJECT_MEMBER));
// We create a task
$this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 2)));
@@ -531,7 +528,7 @@ class TaskDuplicationTest extends Base
$tc = new TaskCreation($this->container);
$tf = new TaskFinder($this->container);
$p = new Project($this->container);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
$user = new User($this->container);
// We create 2 projects
@@ -540,10 +537,8 @@ class TaskDuplicationTest extends Base
// We create a new user for our project
$this->assertNotFalse($user->create(array('username' => 'unittest#1', 'password' => 'unittest')));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->addMember(2, 2));
- $this->assertTrue($pp->isUserAllowed(1, 2));
- $this->assertTrue($pp->isUserAllowed(2, 2));
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(2, 2, Role::PROJECT_MEMBER));
// We create a task
$this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 3)));
diff --git a/tests/units/Model/TaskFinderTest.php b/tests/units/Model/TaskFinderTest.php
index e22f14e1..b21a4ea3 100644
--- a/tests/units/Model/TaskFinderTest.php
+++ b/tests/units/Model/TaskFinderTest.php
@@ -6,7 +6,6 @@ use Kanboard\Model\Task;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
use Kanboard\Model\Category;
use Kanboard\Model\User;
diff --git a/tests/units/Model/TaskModificationTest.php b/tests/units/Model/TaskModificationTest.php
index 49b51f9b..25a4952e 100644
--- a/tests/units/Model/TaskModificationTest.php
+++ b/tests/units/Model/TaskModificationTest.php
@@ -8,7 +8,6 @@ use Kanboard\Model\TaskModification;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\TaskStatus;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
class TaskModificationTest extends Base
{
diff --git a/tests/units/Model/TaskPermissionTest.php b/tests/units/Model/TaskPermissionTest.php
index 56886ee7..0b093bbb 100644
--- a/tests/units/Model/TaskPermissionTest.php
+++ b/tests/units/Model/TaskPermissionTest.php
@@ -9,7 +9,7 @@ use Kanboard\Model\TaskPermission;
use Kanboard\Model\Project;
use Kanboard\Model\Category;
use Kanboard\Model\User;
-use Kanboard\Model\UserSession;
+use Kanboard\Core\User\UserSession;
class TaskPermissionTest extends Base
{
diff --git a/tests/units/Model/TaskStatusTest.php b/tests/units/Model/TaskStatusTest.php
index de08ffb3..612afd23 100644
--- a/tests/units/Model/TaskStatusTest.php
+++ b/tests/units/Model/TaskStatusTest.php
@@ -8,7 +8,6 @@ use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\TaskStatus;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
class TaskStatusTest extends Base
{
diff --git a/tests/units/Model/TaskTest.php b/tests/units/Model/TaskTest.php
index 192dc098..60e752e5 100644
--- a/tests/units/Model/TaskTest.php
+++ b/tests/units/Model/TaskTest.php
@@ -7,7 +7,6 @@ use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\TaskStatus;
use Kanboard\Model\Project;
-use Kanboard\Model\ProjectPermission;
use Kanboard\Model\Category;
use Kanboard\Model\User;
diff --git a/tests/units/Model/UserLockingTest.php b/tests/units/Model/UserLockingTest.php
new file mode 100644
index 00000000..c743f8eb
--- /dev/null
+++ b/tests/units/Model/UserLockingTest.php
@@ -0,0 +1,43 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\Model\UserLocking;
+
+class UserLockingTest extends Base
+{
+ public function testFailedLogin()
+ {
+ $u = new UserLocking($this->container);
+
+ $this->assertEquals(0, $u->getFailedLogin('admin'));
+ $this->assertEquals(0, $u->getFailedLogin('not_found'));
+
+ $this->assertTrue($u->incrementFailedLogin('admin'));
+ $this->assertTrue($u->incrementFailedLogin('admin'));
+
+ $this->assertEquals(2, $u->getFailedLogin('admin'));
+ $this->assertTrue($u->resetFailedLogin('admin'));
+ $this->assertEquals(0, $u->getFailedLogin('admin'));
+ }
+
+ public function testLocking()
+ {
+ $u = new UserLocking($this->container);
+
+ $this->assertFalse($u->isLocked('admin'));
+ $this->assertFalse($u->isLocked('not_found'));
+ $this->assertTrue($u->lock('admin', 1));
+ $this->assertTrue($u->isLocked('admin'));
+ }
+
+ public function testCaptcha()
+ {
+ $u = new UserLocking($this->container);
+ $this->assertTrue($u->incrementFailedLogin('admin'));
+ $this->assertFalse($u->hasCaptcha('admin', 2));
+
+ $this->assertTrue($u->incrementFailedLogin('admin'));
+ $this->assertTrue($u->hasCaptcha('admin', 2));
+ }
+}
diff --git a/tests/units/Model/UserNotificationTest.php b/tests/units/Model/UserNotificationTest.php
index 729667de..bf3b1b3f 100644
--- a/tests/units/Model/UserNotificationTest.php
+++ b/tests/units/Model/UserNotificationTest.php
@@ -9,12 +9,14 @@ use Kanboard\Model\Comment;
use Kanboard\Model\User;
use Kanboard\Model\File;
use Kanboard\Model\Project;
-use Kanboard\Model\Task;
use Kanboard\Model\ProjectPermission;
+use Kanboard\Model\Task;
+use Kanboard\Model\ProjectUserRole;
use Kanboard\Model\UserNotification;
use Kanboard\Model\UserNotificationFilter;
use Kanboard\Model\UserNotificationType;
use Kanboard\Subscriber\UserNotificationSubscriber;
+use Kanboard\Core\Security\Role;
class UserNotificationTest extends Base
{
@@ -23,11 +25,11 @@ class UserNotificationTest extends Base
$u = new User($this->container);
$p = new Project($this->container);
$n = new UserNotification($this->container);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
$this->assertEquals(1, $p->create(array('name' => 'UnitTest1')));
$this->assertEquals(2, $u->create(array('username' => 'user1')));
- $this->assertTrue($pp->addMember(1, 2));
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
$this->assertEmpty($n->getUsersWithNotificationEnabled(1));
$n->enableNotification(2);
@@ -101,7 +103,7 @@ class UserNotificationTest extends Base
$u = new User($this->container);
$p = new Project($this->container);
$n = new UserNotification($this->container);
- $pp = new ProjectPermission($this->container);
+ $pp = new ProjectUserRole($this->container);
$this->assertEquals(1, $p->create(array('name' => 'UnitTest1')));
@@ -118,16 +120,16 @@ class UserNotificationTest extends Base
$this->assertNotFalse($u->create(array('username' => 'user4')));
// Nobody is member of any projects
- $this->assertEmpty($pp->getMembers(1));
+ $this->assertEmpty($pp->getUsers(1));
$this->assertEmpty($n->getUsersWithNotificationEnabled(1));
// We allow all users to be member of our projects
- $this->assertTrue($pp->addMember(1, 1));
- $this->assertTrue($pp->addMember(1, 2));
- $this->assertTrue($pp->addMember(1, 3));
- $this->assertTrue($pp->addMember(1, 4));
+ $this->assertTrue($pp->addUser(1, 1, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(1, 2, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(1, 3, Role::PROJECT_MEMBER));
+ $this->assertTrue($pp->addUser(1, 4, Role::PROJECT_MEMBER));
- $this->assertNotEmpty($pp->getMembers(1));
+ $this->assertNotEmpty($pp->getUsers(1));
$users = $n->getUsersWithNotificationEnabled(1);
$this->assertNotEmpty($users);
diff --git a/tests/units/Model/UserTest.php b/tests/units/Model/UserTest.php
index 90a80954..1d381006 100644
--- a/tests/units/Model/UserTest.php
+++ b/tests/units/Model/UserTest.php
@@ -9,34 +9,10 @@ use Kanboard\Model\Task;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Project;
+use Kanboard\Core\Security\Role;
class UserTest extends Base
{
- public function testFailedLogin()
- {
- $u = new User($this->container);
-
- $this->assertEquals(0, $u->getFailedLogin('admin'));
- $this->assertEquals(0, $u->getFailedLogin('not_found'));
-
- $this->assertTrue($u->incrementFailedLogin('admin'));
- $this->assertTrue($u->incrementFailedLogin('admin'));
-
- $this->assertEquals(2, $u->getFailedLogin('admin'));
- $this->assertTrue($u->resetFailedLogin('admin'));
- $this->assertEquals(0, $u->getFailedLogin('admin'));
- }
-
- public function testLocking()
- {
- $u = new User($this->container);
-
- $this->assertFalse($u->isLocked('admin'));
- $this->assertFalse($u->isLocked('not_found'));
- $this->assertTrue($u->lock('admin', 1));
- $this->assertTrue($u->isLocked('admin'));
- }
-
public function testGetByEmail()
{
$u = new User($this->container);
@@ -47,33 +23,27 @@ class UserTest extends Base
$this->assertEmpty($u->getByEmail(''));
}
- public function testGetByGitlabId()
+ public function testGetByExternalId()
{
$u = new User($this->container);
$this->assertNotFalse($u->create(array('username' => 'user1', 'password' => '123456', 'gitlab_id' => '1234')));
- $this->assertNotEmpty($u->getByGitlabId('1234'));
- $this->assertEmpty($u->getByGitlabId(''));
- }
+ $this->assertNotEmpty($u->getByExternalId('gitlab_id', '1234'));
+ $this->assertEmpty($u->getByExternalId('gitlab_id', ''));
- public function testGetByGithubId()
- {
$u = new User($this->container);
- $this->assertNotFalse($u->create(array('username' => 'user1', 'password' => '123456', 'github_id' => 'plop')));
- $this->assertNotFalse($u->create(array('username' => 'user2', 'password' => '123456', 'github_id' => '')));
+ $this->assertNotFalse($u->create(array('username' => 'user2', 'password' => '123456', 'github_id' => 'plop')));
+ $this->assertNotFalse($u->create(array('username' => 'user3', 'password' => '123456', 'github_id' => '')));
- $this->assertNotEmpty($u->getByGithubId('plop'));
- $this->assertEmpty($u->getByGithubId(''));
- }
+ $this->assertNotEmpty($u->getByExternalId('github_id', 'plop'));
+ $this->assertEmpty($u->getByExternalId('github_id', ''));
- public function testGetByGoogleId()
- {
$u = new User($this->container);
- $this->assertNotFalse($u->create(array('username' => 'user1', 'password' => '123456', 'google_id' => '1234')));
- $this->assertNotFalse($u->create(array('username' => 'user2', 'password' => '123456', 'google_id' => '')));
+ $this->assertNotFalse($u->create(array('username' => 'user4', 'password' => '123456', 'google_id' => '1234')));
+ $this->assertNotFalse($u->create(array('username' => 'user5', 'password' => '123456', 'google_id' => '')));
- $this->assertNotEmpty($u->getByGoogleId('1234'));
- $this->assertEmpty($u->getByGoogleId(''));
+ $this->assertNotEmpty($u->getByExternalId('google_id', '1234'));
+ $this->assertEmpty($u->getByExternalId('google_id', ''));
}
public function testGetByToken()
@@ -197,7 +167,7 @@ class UserTest extends Base
'password' => '1234',
'confirmation' => '1234',
'name' => 'me',
- 'is_admin' => '',
+ 'role' => Role::APP_ADMIN,
);
$u->prepare($input);
@@ -207,9 +177,6 @@ class UserTest extends Base
$this->assertNotEquals('1234', $input['password']);
$this->assertNotEmpty($input['password']);
- $this->assertArrayHasKey('is_admin', $input);
- $this->assertInternalType('integer', $input['is_admin']);
-
$input = array(
'username' => 'user1',
'password' => '1234',
@@ -273,8 +240,8 @@ class UserTest extends Base
$u = new User($this->container);
$this->assertEquals(2, $u->create(array('username' => 'user #1', 'password' => '123456', 'name' => 'User')));
$this->assertEquals(3, $u->create(array('username' => 'user #2', 'is_ldap_user' => 1)));
- $this->assertEquals(4, $u->create(array('username' => 'user #3', 'is_project_admin' => 1)));
- $this->assertEquals(5, $u->create(array('username' => 'user #4', 'gitlab_id' => '')));
+ $this->assertEquals(4, $u->create(array('username' => 'user #3', 'role' => Role::APP_MANAGER)));
+ $this->assertEquals(5, $u->create(array('username' => 'user #4', 'gitlab_id' => '', 'role' => Role::APP_ADMIN)));
$this->assertEquals(6, $u->create(array('username' => 'user #5', 'gitlab_id' => '1234')));
$this->assertFalse($u->create(array('username' => 'user #1')));
@@ -283,7 +250,7 @@ class UserTest extends Base
$this->assertTrue(is_array($user));
$this->assertEquals('admin', $user['username']);
$this->assertEquals('', $user['name']);
- $this->assertEquals(1, $user['is_admin']);
+ $this->assertEquals(Role::APP_ADMIN, $user['role']);
$this->assertEquals(0, $user['is_ldap_user']);
$user = $u->getById(2);
@@ -291,7 +258,7 @@ class UserTest extends Base
$this->assertTrue(is_array($user));
$this->assertEquals('user #1', $user['username']);
$this->assertEquals('User', $user['name']);
- $this->assertEquals(0, $user['is_admin']);
+ $this->assertEquals(Role::APP_USER, $user['role']);
$this->assertEquals(0, $user['is_ldap_user']);
$user = $u->getById(3);
@@ -299,27 +266,28 @@ class UserTest extends Base
$this->assertTrue(is_array($user));
$this->assertEquals('user #2', $user['username']);
$this->assertEquals('', $user['name']);
- $this->assertEquals(0, $user['is_admin']);
+ $this->assertEquals(Role::APP_USER, $user['role']);
$this->assertEquals(1, $user['is_ldap_user']);
$user = $u->getById(4);
$this->assertNotFalse($user);
$this->assertTrue(is_array($user));
$this->assertEquals('user #3', $user['username']);
- $this->assertEquals(0, $user['is_admin']);
- $this->assertEquals(1, $user['is_project_admin']);
+ $this->assertEquals(Role::APP_MANAGER, $user['role']);
$user = $u->getById(5);
$this->assertNotFalse($user);
$this->assertTrue(is_array($user));
$this->assertEquals('user #4', $user['username']);
$this->assertEquals('', $user['gitlab_id']);
+ $this->assertEquals(Role::APP_ADMIN, $user['role']);
$user = $u->getById(6);
$this->assertNotFalse($user);
$this->assertTrue(is_array($user));
$this->assertEquals('user #5', $user['username']);
$this->assertEquals('1234', $user['gitlab_id']);
+ $this->assertEquals(Role::APP_USER, $user['role']);
}
public function testUpdate()
@@ -336,7 +304,7 @@ class UserTest extends Base
$this->assertTrue(is_array($user));
$this->assertEquals('biloute', $user['username']);
$this->assertEquals('Toto', $user['name']);
- $this->assertEquals(0, $user['is_admin']);
+ $this->assertEquals(Role::APP_USER, $user['role']);
$this->assertEquals(0, $user['is_ldap_user']);
$user = $u->getById(3);
@@ -423,4 +391,36 @@ class UserTest extends Base
$this->assertEquals('toto', $user['username']);
$this->assertEmpty($user['token']);
}
+
+ public function testValidatePasswordModification()
+ {
+ $u = new User($this->container);
+
+ $this->container['sessionStorage']->user = array(
+ 'id' => 1,
+ 'role' => Role::APP_ADMIN,
+ 'username' => 'admin',
+ );
+
+ $result = $u->validatePasswordModification(array());
+ $this->assertFalse($result[0]);
+
+ $result = $u->validatePasswordModification(array('id' => 1));
+ $this->assertFalse($result[0]);
+
+ $result = $u->validatePasswordModification(array('id' => 1, 'password' => '123456'));
+ $this->assertFalse($result[0]);
+
+ $result = $u->validatePasswordModification(array('id' => 1, 'password' => '123456', 'confirmation' => 'wrong'));
+ $this->assertFalse($result[0]);
+
+ $result = $u->validatePasswordModification(array('id' => 1, 'password' => '123456', 'confirmation' => '123456'));
+ $this->assertFalse($result[0]);
+
+ $result = $u->validatePasswordModification(array('id' => 1, 'password' => '123456', 'confirmation' => '123456', 'current_password' => 'wrong'));
+ $this->assertFalse($result[0]);
+
+ $result = $u->validatePasswordModification(array('id' => 1, 'password' => '123456', 'confirmation' => '123456', 'current_password' => 'admin'));
+ $this->assertTrue($result[0]);
+ }
}
diff --git a/tests/units/Notification/MailTest.php b/tests/units/Notification/MailTest.php
index 3aa1a39c..8f343ba3 100644
--- a/tests/units/Notification/MailTest.php
+++ b/tests/units/Notification/MailTest.php
@@ -10,7 +10,6 @@ use Kanboard\Model\User;
use Kanboard\Model\File;
use Kanboard\Model\Project;
use Kanboard\Model\Task;
-use Kanboard\Model\ProjectPermission;
use Kanboard\Notification\Mail;
use Kanboard\Subscriber\NotificationSubscriber;
diff --git a/tests/units/User/DatabaseUserProviderTest.php b/tests/units/User/DatabaseUserProviderTest.php
new file mode 100644
index 00000000..96c03667
--- /dev/null
+++ b/tests/units/User/DatabaseUserProviderTest.php
@@ -0,0 +1,14 @@
+<?php
+
+require_once __DIR__.'/../Base.php';
+
+use Kanboard\User\DatabaseUserProvider;
+
+class DatabaseUserProviderTest extends Base
+{
+ public function testGetInternalId()
+ {
+ $provider = new DatabaseUserProvider(array('id' => 123));
+ $this->assertEquals(123, $provider->getInternalId());
+ }
+}