diff options
255 files changed, 14066 insertions, 9772 deletions
@@ -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: @@ -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'].' > '.$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'].' > '.$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'].' > '.$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> <?= $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> <?= $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> <?= 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> <?= 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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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> <?= $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:"<Dříve",nextText:"Později>",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:"<Forrige",nextText:"Næste>",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:"<Zurück",nextText:"Vor>",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:"<Ant",nextText:"Sig>",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:"«Edellinen",nextText:"Seuraava»",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:"<mundur",nextText:"maju>",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:"<Prec",nextText:"Succ>",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:"<前",nextText:"次>",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:"«Forrige",nextText:"Neste»",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:"<Poprzedni",nextText:"Następny>",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:"<Anterior",nextText:"Próximo>",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:"<Пред",nextText:"След>",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:"«Förra",nextText:"Nästa»",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:"<",nextText:">",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:"« ย้อน",nextText:"ถัดไป »",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:"<geri",nextText:"ileri>",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:"<上月",nextText:"下月>",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"> <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(" ");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(" ").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(" ").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:"<Dříve",nextText:"Později>",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:"<Forrige",nextText:"Næste>",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:"<Zurück",nextText:"Vor>",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:"<Ant",nextText:"Sig>",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:"«Edellinen",nextText:"Seuraava»",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:"<mundur",nextText:"maju>",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:"<Prec",nextText:"Succ>",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:"<前",nextText:"次>",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:"«Forrige",nextText:"Neste»",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:"<Poprzedni",nextText:"Następny>",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:"<Anterior",nextText:"Próximo>",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:"<Пред",nextText:"След>",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:"«Förra",nextText:"Nästa»",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:"<",nextText:">",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:"« ย้อน",nextText:"ถัดไป »",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:"<geri",nextText:"ileri>",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:"<上月",nextText:"下月>",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"> <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(" ");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(" ").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(" ").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&action=show&task_id=1&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&action=show&task_id=1&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 +} +``` @@ -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()); + } +} |