diff --git a/README.markdown b/README.markdown index 72d0772..d42a67a 100644 --- a/README.markdown +++ b/README.markdown @@ -33,7 +33,8 @@ From command line: Infos ----- -Kanboard v1.0.21 +Kanboard v1.0.24 +Reverse-Proxy Authentication with LDAP user provider plugin v1.0.0 https://github.com/kanboard/plugin-reverse-proxy-ldap Yunohost forum thread: diff --git a/conf/config.php b/conf/config.php index 32a11a9..fd637cb 100644 --- a/conf/config.php +++ b/conf/config.php @@ -28,19 +28,6 @@ define('MAIL_SMTP_ENCRYPTION', null); // Valid values are "null", "ssl" or "tls" // Sendmail command to use when the transport is "sendmail" define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'); -// Postmark API token (used to send emails through their API) -define('POSTMARK_API_TOKEN', ''); - -// Mailgun API key (used to send emails through their API) -define('MAILGUN_API_TOKEN', ''); - -// Mailgun domain name -define('MAILGUN_DOMAIN', ''); - -// Sendgrid API configuration -define('SENDGRID_API_USER', ''); -define('SENDGRID_API_KEY', ''); - // Database driver: sqlite, mysql or postgres (sqlite by default) define('DB_DRIVER', 'mysql'); @@ -63,7 +50,7 @@ define('DB_PORT', null); define('LDAP_AUTH', false); // LDAP server hostname -define('LDAP_SERVER', ''); +define('LDAP_SERVER', 'localhost'); // LDAP server port (389 by default) define('LDAP_PORT', 389); @@ -74,6 +61,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'); @@ -84,43 +75,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', 'ou=users,dc=yunohost,dc=org'); -// 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', 'uid=%s'); -// 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 group membership -define('LDAP_ACCOUNT_MEMBEROF', 'memberof'); +// LDAP attribute for user full name +// Example for ActiveDirectory: 'displayname' +// Example for OpenLDAP: 'cn' +define('LDAP_USER_ATTRIBUTE_FULLNAME', 'displayname'); -// DN for administrators -// Example: CN=Kanboard Admins,CN=Users,DC=kanboard,DC=local +// LDAP attribute for user email +define('LDAP_USER_ATTRIBUTE_EMAIL', 'mail'); + +// 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); -// Automatically create user account -define('LDAP_ACCOUNT_CREATION', true); +// LDAP Base DN for groups +define('LDAP_GROUP_BASE_DN', ''); + +// LDAP group filter +// Example for ActiveDirectory: (&(objectClass=group)(sAMAccountName=%s*)) +define('LDAP_GROUP_FILTER', ''); + +// LDAP attribute for the group name +define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); // Enable/disable Google authentication define('GOOGLE_AUTH', false); @@ -168,7 +172,7 @@ define('GITLAB_OAUTH_TOKEN_URL', 'https://gitlab.com/oauth/token'); define('GITLAB_API_URL', 'https://gitlab.com/api/v3/'); // Enable/disable the reverse proxy authentication -define('REVERSE_PROXY_AUTH', true); +define('REVERSE_PROXY_AUTH', false); // Header name to use for the username define('REVERSE_PROXY_USER_HEADER', 'REMOTE_USER'); diff --git a/scripts/upgrade b/scripts/upgrade index 01135cf..aef4bc7 100644 --- a/scripts/upgrade +++ b/scripts/upgrade @@ -10,12 +10,19 @@ admin=$(sudo yunohost app setting $app adminusername) email=$(sudo yunohost user info $admin | grep mail: | sed "s/mail: //g") db_pwd=$(sudo yunohost app setting $app mysqlpwd) +# flush php sessions before upgrade +sudo rm -rf /var/lib/php5/session/* + final_path=/var/www/$app sudo mv $final_path $final_path.old sudo mkdir -p $final_path sudo cp -a ../sources/. $final_path -sudo cp -a $final_path.old/data $final_path/data +# restore data +sudo cp -a $final_path.old/data $final_path +# restore plugins +sudo cp -a $final_path.old/plugins $final_path +# delete temp directory sudo rm -Rf $final_path.old # Copy and edit config.php diff --git a/sources/ChangeLog b/sources/ChangeLog index 56bbcdf..5ae4fb7 100644 --- a/sources/ChangeLog +++ b/sources/ChangeLog @@ -1,11 +1,118 @@ +Version 1.0.24 +-------------- + +New features: + +* Forgot Password +* Add drop-down menu on each board column title to close all tasks +* Add Malay language +* Add new API procedures for groups, roles, project permissions and to move/duplicate tasks to another project + +Improvements: + +* Avoid to send XHR request when a task has not moved after a drag and drop +* Set maximum dropzone height when the individual column scrolling is disabled +* Always show the search box in board selector +* Replace logout link by a drop-down menu +* Handle notification for group members attached to a project +* Return the highest role for a project when a user is member of multiple groups +* Show in user interface the saving state of the task +* Add drop-down menu for subtasks, categories, swimlanes, columns, custom filters, task links and groups +* Add new template hooks +* Application settings are not cached anymore in the session +* Do not check board status during task move +* Move validators to a separate namespace +* Improve and write unit tests for reports +* Reduce the number of SQL queries for project daily column stats +* Remove event subscriber to update date_moved field +* Make sure that some event subscribers are not executed multiple times +* Show rendering time of individual templates when debug mode is enabled +* Make sure that no events are fired if nothing has been modified in the task +* Make dashboard section title clickable +* Add unit tests for LastLogin + +Bug fixes: + +* Automatic action listeners were using the same instance +* Fix wrong link for category in task footer +* Unable to set currency rate with Postgres database +* Avoid automatic actions that change the color to fire subsequent events +* Unable to unassign a task from the API +* Revert back previous optimizations of TaskPosition (incompatibility with some environment) + +Version 1.0.23 +-------------- + +Breaking changes: + +* Plugin API changes for Automatic Actions +* Automatic Action to close a task doesn't have the column parameter anymore (use the action "Close a task in a specific column") +* Action name stored in the database is now the absolute class name +* Core functionalities moved to external plugins: + - Github Webhook: https://github.com/kanboard/plugin-github-webhook + - Gitlab Webhook: https://github.com/kanboard/plugin-gitlab-webhook + - Bitbucket Webhook: https://github.com/kanboard/plugin-bitbucket-webhook + +New features: + +* Added support of user mentions (@username) +* Added report to compare working hours between open and closed tasks +* Added the possibility to define custom routes from plugins +* Added new method to remove metadata + +Improvements: + +* Improve Two-Factor activation and plugin API +* Improving performance during task position change (SQL queries are 3 times faster than before) +* Do not show window scrollbars when individual column scrolling is enabled +* Automatic Actions code improvements and unit tests +* Increase action name column length in actions table + +Bug fixes: + +* Fix compatibility issue with FreeBSD for session.hash_function parameter +* Fix wrong constant name that causes a PHP error in project management section +* Fix pagination in group members listing +* Avoid PHP error when enabling LDAP group provider with PHP < 5.5 + +Version 1.0.22 +-------------- + +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: replaced column "is_owner" with column "role" + - Sqlite does not support alter table, old columns still there but unused +* API procedure changes: + - createUser + - createLdapUser + - updateUser + - updateTask +* Event removed: "session.bootstrap", use "app.boostrap" instead + +New features: + +* Add pluggable authentication and authorization system (complete rewrite) +* Add groups (teams/organization) +* Add LDAP groups synchronization +* Add project group permissions +* Add new project role Viewer +* Add generic LDAP client library +* Add search query attribute for task link +* Add the possibility to define API token in config file +* Add capability to reopen Gitlab issues +* Try to load config.php from /data if not available + 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 +* Projects with duplicate names are now allowed: + - For Postgres and Mysql the unique constraint is removed by database migration + - However Sqlite does not support alter table, only new databases will have the unique constraint removed New features: @@ -14,7 +121,7 @@ New features: Improvements: -* Dropdown menu entry are now clickable outside of the html link +* Dropdown menu entries are now clickable outside of the html link * Improve error handling of plugins * Use PHP7 function random_bytes() to generate tokens if available * CSV task export show the assignee name in addition to the assignee username @@ -62,7 +169,7 @@ Improvements: Bug fixes: * People should not see any tasks during a search when they are not associated to a project -* Avoid to disable the default swimlane during renaming when there is no other activated swimlane +* Avoid disabling the default swimlane during renaming when there is no other activated swimlane Version 1.0.19 -------------- @@ -94,15 +201,15 @@ Improvements: * Offer alternative method to create Mysql and Postgres databases (import sql dump) * Make sure there is always a trailing slash for application_url * Do not show the checkbox "Show default swimlane" when there is no active swimlanes -* Append filters instead of replacing value for users and categories dropdowns +* Append filters instead of replacing value for users and categories drop-downs * Do not show empty swimlanes in public view * Change swimlane layout to save space on the screen * Add the possibility to set/unset max column height (column scrolling) -* Show "Open this task" in dropdown menu for closed tasks +* Show "Open this task" in drop-down menu for closed tasks * Show assignee on card only when someone is assigned (hide nobody text) -* Highlight selected item in dropdown menus +* Highlight selected item in drop-down menus * Gantt chart: change bar color according to task progress -* Replace color dropdown by color picker in task forms +* Replace color drop-down by color picker in task forms * Creating another task stay in the popover (no full page refresh anymore) * Avoid scrollbar in Gantt chart for row title on Windows platform * Remove unnecessary margin for calendar header @@ -114,14 +221,14 @@ Improvements: Others: -* Data directory permissions are not checked anymore +* Data directory permission are not checked anymore * Data directory is not mandatory anymore for people that use a remote database and remote object storage Bug fixes: -* Fix typo in template that prevent the Gitlab OAuth link to be displayed +* Fix typo in template that prevents Gitlab OAuth link to be displayed * Fix Markdown preview links focus -* Avoid dropdown menu to be truncated inside a column with scrolling +* Avoid drop-down menu to be truncated inside a column with scrolling * Deleting subtask doesn't update task time tracking * Fix Mysql error about gitlab_id when creating remote user * Fix subtask timer bug (event called recursively) @@ -139,7 +246,7 @@ New features: * Add hide/show columns * Add Gantt chart for projects and tasks * Add new role "Project Administrator" -* Add login bruteforce protection with captcha and account lockdown +* Add login brute force protection with captcha and account lockdown * Add new api procedures: getDefaultTaskColor(), getDefaultTaskColors() and getColorList() * Add user api access * Add config parameter to define session duration @@ -147,7 +254,7 @@ New features: * Add start/end date for projects * Add new automated action to change task color based on the task link * Add milestone marker in board task -* Add search in task title when using an integer only input +* Add search for task title when using an integer only input * Add Portuguese (European) translation * Add Norwegian translation @@ -164,16 +271,16 @@ Improvements: * Improve sidebar menus * Add no referrer policy in meta tags * Run automated unit tests with Sqlite/Mysql/Postgres on Travis-ci -* Add Makefile and remove the scripts directory +* Add Makefile and remove the "scripts" directory Bug fixes: * Wrong template name for subtasks tooltip due to previous refactoring * Fix broken url for closed tasks in project view * Fix permission issue when changing the url manually -* Fix bug task estimate is reseted when using subtask timer +* Fix bug task estimate is reset when using subtask timer * Fix screenshot feature with Firefox 40 -* Fix bug when uploading files with cyrilic characters +* Fix bug when uploading files with Cyrilic characters Version 1.0.17 -------------- @@ -187,14 +294,14 @@ New features: * Added new dashboard layout * Added new layout for board/calendar/list views * Added filters helper for search forms -* Added settings option to disable subtask timer -* Added settings option to include or exclude closed tasks into CFD -* Added settings option to define the default task color +* Added setting option to disable subtask timer +* Added setting option to include or exclude closed tasks into CFD +* Added setting option to define the default task color * Added new config option to disable automatic creation of LDAP accounts * Added loading icon on board view * Prompt user when moving or duplicate a task to another project * Added current values when moving/duplicate a task to another project and add a loading icon -* Added memory consumption in debug log +* Added memory consumption to debug log * Added form to create remote user * Added edit form for user authentication * Added config option to hide login form @@ -205,7 +312,7 @@ New features: * Added new report: Lead and cycle time for projects * Added new report: Average time spent into each column * Added task analytics -* Added icon to set automatically the start date +* Added icon to set the start date automatically * Added datetime picker for start date Improvements: @@ -214,8 +321,8 @@ Improvements: * Display user initials when tasks are in collapsed mode * Show title in tooltip for collapsed tasks * Improve alert box fadeout to avoid an empty space -* Set focus on the dropdown for category popover -* Make escape keyboard shorcut global +* Set focus on the drop-down for category popover +* Make escape keyboard shortcut global * Check the box remember me by default * Store redirect login url in session instead of using url parameter * Update Gitlab webhook @@ -242,7 +349,7 @@ Translations: Bug fixes: -* Screenshot dropdown: unexpected scroll down on the board view and focus lost when clicking on the drop zone +* Screenshot drop-down: unexpected scroll down on the board view and focus lost when clicking on the drop zone * No creator when duplicating a task * Avoid the creation of multiple subtask timer for the same task and user diff --git a/sources/LICENSE b/sources/LICENSE index dbe5032..c1efa20 100644 --- a/sources/LICENSE +++ b/sources/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014-2015 Frédéric Guillot +Copyright (c) 2014-2016 Frédéric Guillot Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/sources/app/Action/Base.php b/sources/app/Action/Base.php index 81e2ccc..efc52f0 100644 --- a/sources/app/Action/Base.php +++ b/sources/app/Action/Base.php @@ -3,7 +3,6 @@ namespace Kanboard\Action; use Kanboard\Event\GenericEvent; -use Pimple\Container; /** * Base class for automatic actions @@ -13,6 +12,14 @@ use Pimple\Container; */ abstract class Base extends \Kanboard\Core\Base { + /** + * Extended events + * + * @access private + * @var array + */ + private $compatibleEvents = array(); + /** * Flag for called listener * @@ -27,7 +34,7 @@ abstract class Base extends \Kanboard\Core\Base * @access private * @var integer */ - private $project_id = 0; + private $projectId = 0; /** * User parameters @@ -38,20 +45,25 @@ abstract class Base extends \Kanboard\Core\Base private $params = array(); /** - * Attached event name + * Get automatic action name * - * @access protected - * @var string + * @final + * @access public + * @return string */ - protected $event_name = ''; + final public function getName() + { + return '\\'.get_called_class(); + } /** - * Container instance + * Get automatic action description * - * @access protected - * @var \Pimple\Container + * @abstract + * @access public + * @return string */ - protected $container; + abstract public function getDescription(); /** * Execute the action @@ -99,22 +111,6 @@ abstract class Base extends \Kanboard\Core\Base */ abstract public function hasRequiredCondition(array $data); - /** - * Constructor - * - * @access public - * @param \Pimple\Container $container Container - * @param integer $project_id Project id - * @param string $event_name Attached event name - */ - public function __construct(Container $container, $project_id, $event_name) - { - $this->container = $container; - $this->project_id = $project_id; - $this->event_name = $event_name; - $this->called = false; - } - /** * Return class information * @@ -123,7 +119,25 @@ abstract class Base extends \Kanboard\Core\Base */ public function __toString() { - return get_called_class(); + $params = array(); + + foreach ($this->params as $key => $value) { + $params[] = $key.'='.var_export($value, true); + } + + return $this->getName().'('.implode('|', $params).'])'; + } + + /** + * Set project id + * + * @access public + * @return Base + */ + public function setProjectId($project_id) + { + $this->projectId = $project_id; + return $this; } /** @@ -134,7 +148,7 @@ abstract class Base extends \Kanboard\Core\Base */ public function getProjectId() { - return $this->project_id; + return $this->projectId; } /** @@ -143,10 +157,12 @@ abstract class Base extends \Kanboard\Core\Base * @access public * @param string $name Parameter name * @param mixed $value Value + * @param Base */ public function setParam($name, $value) { $this->params[$name] = $value; + return $this; } /** @@ -154,24 +170,25 @@ abstract class Base extends \Kanboard\Core\Base * * @access public * @param string $name Parameter name - * @param mixed $default_value Default value + * @param mixed $default Default value * @return mixed */ - public function getParam($name, $default_value = null) + public function getParam($name, $default = null) { - return isset($this->params[$name]) ? $this->params[$name] : $default_value; + return isset($this->params[$name]) ? $this->params[$name] : $default; } /** * Check if an action is executable (right project and required parameters) * * @access public - * @param array $data Event data dictionary - * @return bool True if the action is executable + * @param array $data + * @param string $eventName + * @return bool */ - public function isExecutable(array $data) + public function isExecutable(array $data, $eventName) { - return $this->hasCompatibleEvent() && + return $this->hasCompatibleEvent($eventName) && $this->hasRequiredProject($data) && $this->hasRequiredParameters($data) && $this->hasRequiredCondition($data); @@ -181,11 +198,12 @@ abstract class Base extends \Kanboard\Core\Base * Check if the event is compatible with the action * * @access public + * @param string $eventName * @return bool */ - public function hasCompatibleEvent() + public function hasCompatibleEvent($eventName) { - return in_array($this->event_name, $this->getCompatibleEvents()); + return in_array($eventName, $this->getEvents()); } /** @@ -197,7 +215,7 @@ abstract class Base extends \Kanboard\Core\Base */ public function hasRequiredProject(array $data) { - return isset($data['project_id']) && $data['project_id'] == $this->project_id; + return isset($data['project_id']) && $data['project_id'] == $this->getProjectId(); } /** @@ -222,10 +240,11 @@ abstract class Base extends \Kanboard\Core\Base * Execute the action * * @access public - * @param \Event\GenericEvent $event Event data dictionary - * @return bool True if the action was executed or false when not executed + * @param \Kanboard\Event\GenericEvent $event + * @param string $eventName + * @return bool */ - public function execute(GenericEvent $event) + public function execute(GenericEvent $event, $eventName) { // Avoid infinite loop, a listener instance can be called only one time if ($this->called) { @@ -233,17 +252,44 @@ abstract class Base extends \Kanboard\Core\Base } $data = $event->getAll(); - $result = false; + $executable = $this->isExecutable($data, $eventName); + $executed = false; - if ($this->isExecutable($data)) { + if ($executable) { $this->called = true; - $result = $this->doAction($data); + $executed = $this->doAction($data); } - if (DEBUG) { - $this->logger->debug(get_called_class().' => '.($result ? 'true' : 'false')); + $this->logger->debug($this.' ['.$eventName.'] => executable='.var_export($executable, true).' exec_success='.var_export($executed, true)); + + return $executed; + } + + /** + * Register a new event for the automatic action + * + * @access public + * @param string $event + * @param string $description + */ + public function addEvent($event, $description = '') + { + if ($description !== '') { + $this->eventManager->register($event, $description); } - return $result; + $this->compatibleEvents[] = $event; + return $this; + } + + /** + * Get all compatible events of an automatic action + * + * @access public + * @return array + */ + public function getEvents() + { + return array_unique(array_merge($this->getCompatibleEvents(), $this->compatibleEvents)); } } diff --git a/sources/app/Action/CommentCreation.php b/sources/app/Action/CommentCreation.php index 73fedc3..b91e39e 100644 --- a/sources/app/Action/CommentCreation.php +++ b/sources/app/Action/CommentCreation.php @@ -2,10 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\BitbucketWebhook; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\GitlabWebhook; - /** * Create automatically a comment from a webhook * @@ -14,6 +10,17 @@ use Kanboard\Integration\GitlabWebhook; */ class CommentCreation extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Create a comment from an external provider'); + } + /** * Get the list of compatible events * @@ -22,14 +29,7 @@ class CommentCreation extends Base */ public function getCompatibleEvents() { - return array( - GithubWebhook::EVENT_ISSUE_COMMENT, - GithubWebhook::EVENT_COMMIT, - BitbucketWebhook::EVENT_ISSUE_COMMENT, - BitbucketWebhook::EVENT_COMMIT, - GitlabWebhook::EVENT_COMMIT, - GitlabWebhook::EVENT_ISSUE_COMMENT, - ); + return array(); } /** @@ -67,9 +67,9 @@ class CommentCreation extends Base { return (bool) $this->comment->create(array( 'reference' => isset($data['reference']) ? $data['reference'] : '', - 'comment' => empty($data['comment']) ? $data['commit_comment'] : $data['comment'], + 'comment' => $data['comment'], 'task_id' => $data['task_id'], - 'user_id' => empty($data['user_id']) ? 0 : $data['user_id'], + 'user_id' => isset($data['user_id']) && $this->projectPermission->isAssignable($this->getProjectId(), $data['user_id']) ? $data['user_id'] : 0, )); } @@ -82,6 +82,6 @@ class CommentCreation extends Base */ public function hasRequiredCondition(array $data) { - return ! empty($data['comment']) || ! empty($data['commit_comment']); + return ! empty($data['comment']); } } diff --git a/sources/app/Action/TaskLogMoveAnotherColumn.php b/sources/app/Action/CommentCreationMoveTaskColumn.php similarity index 83% rename from sources/app/Action/TaskLogMoveAnotherColumn.php rename to sources/app/Action/CommentCreationMoveTaskColumn.php index a699c4a..4473cf9 100644 --- a/sources/app/Action/TaskLogMoveAnotherColumn.php +++ b/sources/app/Action/CommentCreationMoveTaskColumn.php @@ -5,13 +5,24 @@ namespace Kanboard\Action; use Kanboard\Model\Task; /** - * Add a log of the triggering event to the task description. + * Add a comment of the triggering event to the task description. * * @package action * @author Oren Ben-Kiki */ -class TaskLogMoveAnotherColumn extends Base +class CommentCreationMoveTaskColumn extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Add a comment log when moving the task between columns'); + } + /** * Get the list of compatible events * diff --git a/sources/app/Action/TaskAssignCategoryColor.php b/sources/app/Action/TaskAssignCategoryColor.php index ffa1ac2..f5085cb 100644 --- a/sources/app/Action/TaskAssignCategoryColor.php +++ b/sources/app/Action/TaskAssignCategoryColor.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignCategoryColor extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign automatically a category based on a color'); + } + /** * Get the list of compatible events * diff --git a/sources/app/Action/TaskAssignCategoryLabel.php b/sources/app/Action/TaskAssignCategoryLabel.php index 0ef474b..95fa116 100644 --- a/sources/app/Action/TaskAssignCategoryLabel.php +++ b/sources/app/Action/TaskAssignCategoryLabel.php @@ -2,8 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\GithubWebhook; - /** * Set a category automatically according to a label * @@ -12,6 +10,17 @@ use Kanboard\Integration\GithubWebhook; */ class TaskAssignCategoryLabel extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Change the category based on an external label'); + } + /** * Get the list of compatible events * @@ -20,9 +29,7 @@ class TaskAssignCategoryLabel extends Base */ public function getCompatibleEvents() { - return array( - GithubWebhook::EVENT_ISSUE_LABEL_CHANGE, - ); + return array(); } /** @@ -64,7 +71,7 @@ class TaskAssignCategoryLabel extends Base { $values = array( 'id' => $data['task_id'], - 'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'), + 'category_id' => $this->getParam('category_id'), ); return $this->taskModification->update($values); @@ -79,6 +86,6 @@ class TaskAssignCategoryLabel extends Base */ public function hasRequiredCondition(array $data) { - return $data['label'] == $this->getParam('label'); + return $data['label'] == $this->getParam('label') && empty($data['category_id']); } } diff --git a/sources/app/Action/TaskAssignCategoryLink.php b/sources/app/Action/TaskAssignCategoryLink.php index 3d00e8d..b39e41b 100644 --- a/sources/app/Action/TaskAssignCategoryLink.php +++ b/sources/app/Action/TaskAssignCategoryLink.php @@ -13,6 +13,17 @@ use Kanboard\Model\TaskLink; */ class TaskAssignCategoryLink extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign automatically a category based on a link'); + } + /** * Get the list of compatible events * @@ -65,7 +76,7 @@ class TaskAssignCategoryLink extends Base { $values = array( 'id' => $data['task_id'], - 'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'), + 'category_id' => $this->getParam('category_id'), ); return $this->taskModification->update($values); diff --git a/sources/app/Action/TaskAssignColorCategory.php b/sources/app/Action/TaskAssignColorCategory.php index a2332f7..139c24c 100644 --- a/sources/app/Action/TaskAssignColorCategory.php +++ b/sources/app/Action/TaskAssignColorCategory.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignColorCategory extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign automatically a color based on a category'); + } + /** * Get the list of compatible events * @@ -67,7 +78,7 @@ class TaskAssignColorCategory extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignColorColumn.php b/sources/app/Action/TaskAssignColorColumn.php index 5314073..9241273 100644 --- a/sources/app/Action/TaskAssignColorColumn.php +++ b/sources/app/Action/TaskAssignColorColumn.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignColorColumn extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign a color when the task is moved to a specific column'); + } + /** * Get the list of compatible events * @@ -68,7 +79,7 @@ class TaskAssignColorColumn extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignColorLink.php b/sources/app/Action/TaskAssignColorLink.php index 67b2ef6..12ceabb 100644 --- a/sources/app/Action/TaskAssignColorLink.php +++ b/sources/app/Action/TaskAssignColorLink.php @@ -12,6 +12,17 @@ use Kanboard\Model\TaskLink; */ class TaskAssignColorLink extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Change task color when using a specific task link'); + } + /** * Get the list of compatible events * @@ -67,7 +78,7 @@ class TaskAssignColorLink extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignColorUser.php b/sources/app/Action/TaskAssignColorUser.php index 6bf02c3..6ec8ce9 100644 --- a/sources/app/Action/TaskAssignColorUser.php +++ b/sources/app/Action/TaskAssignColorUser.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignColorUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign a color to a specific user'); + } + /** * Get the list of compatible events * @@ -68,7 +79,7 @@ class TaskAssignColorUser extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignCurrentUser.php b/sources/app/Action/TaskAssignCurrentUser.php index f34c4f3..192a120 100644 --- a/sources/app/Action/TaskAssignCurrentUser.php +++ b/sources/app/Action/TaskAssignCurrentUser.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignCurrentUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign the task to the person who does the action'); + } + /** * Get the list of compatible events * @@ -22,7 +33,6 @@ class TaskAssignCurrentUser extends Base { return array( Task::EVENT_CREATE, - Task::EVENT_MOVE_COLUMN, ); } @@ -34,9 +44,7 @@ class TaskAssignCurrentUser extends Base */ public function getActionRequiredParameters() { - return array( - 'column_id' => t('Column'), - ); + return array(); } /** @@ -49,7 +57,6 @@ class TaskAssignCurrentUser extends Base { return array( 'task_id', - 'column_id', ); } @@ -83,6 +90,6 @@ class TaskAssignCurrentUser extends Base */ public function hasRequiredCondition(array $data) { - return $data['column_id'] == $this->getParam('column_id'); + return true; } } diff --git a/sources/app/Action/TaskAssignCurrentUserColumn.php b/sources/app/Action/TaskAssignCurrentUserColumn.php new file mode 100644 index 0000000..05d08dd --- /dev/null +++ b/sources/app/Action/TaskAssignCurrentUserColumn.php @@ -0,0 +1,98 @@ + t('Column'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if (! $this->userSession->isLogged()) { + return false; + } + + $values = array( + 'id' => $data['task_id'], + 'owner_id' => $this->userSession->getId(), + ); + + return $this->taskModification->update($values); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/sources/app/Action/TaskAssignSpecificUser.php b/sources/app/Action/TaskAssignSpecificUser.php index dfcb281..2dc3e96 100644 --- a/sources/app/Action/TaskAssignSpecificUser.php +++ b/sources/app/Action/TaskAssignSpecificUser.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskAssignSpecificUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Assign the task to a specific user'); + } + /** * Get the list of compatible events * diff --git a/sources/app/Action/TaskAssignUser.php b/sources/app/Action/TaskAssignUser.php index a582172..da54d18 100644 --- a/sources/app/Action/TaskAssignUser.php +++ b/sources/app/Action/TaskAssignUser.php @@ -2,9 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\BitbucketWebhook; - /** * Assign a task to someone * @@ -13,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook; */ class TaskAssignUser extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Change the assignee based on an external username'); + } + /** * Get the list of compatible events * @@ -21,10 +29,7 @@ class TaskAssignUser extends Base */ public function getCompatibleEvents() { - return array( - GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE, - BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE, - ); + return array(); } /** @@ -78,6 +83,6 @@ class TaskAssignUser extends Base */ public function hasRequiredCondition(array $data) { - return true; + return $this->projectPermission->isAssignable($this->getProjectId(), $data['owner_id']); } } diff --git a/sources/app/Action/TaskClose.php b/sources/app/Action/TaskClose.php index d80bd02..cf91e83 100644 --- a/sources/app/Action/TaskClose.php +++ b/sources/app/Action/TaskClose.php @@ -2,11 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\GitlabWebhook; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\BitbucketWebhook; -use Kanboard\Model\Task; - /** * Close automatically a task * @@ -15,6 +10,17 @@ use Kanboard\Model\Task; */ class TaskClose extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Close a task'); + } + /** * Get the list of compatible events * @@ -23,15 +29,7 @@ class TaskClose extends Base */ public function getCompatibleEvents() { - return array( - Task::EVENT_MOVE_COLUMN, - GithubWebhook::EVENT_COMMIT, - GithubWebhook::EVENT_ISSUE_CLOSED, - GitlabWebhook::EVENT_COMMIT, - GitlabWebhook::EVENT_ISSUE_CLOSED, - BitbucketWebhook::EVENT_COMMIT, - BitbucketWebhook::EVENT_ISSUE_CLOSED, - ); + return array(); } /** @@ -42,17 +40,7 @@ class TaskClose extends Base */ public function getActionRequiredParameters() { - switch ($this->event_name) { - case GithubWebhook::EVENT_COMMIT: - case GithubWebhook::EVENT_ISSUE_CLOSED: - case GitlabWebhook::EVENT_COMMIT: - case GitlabWebhook::EVENT_ISSUE_CLOSED: - case BitbucketWebhook::EVENT_COMMIT: - case BitbucketWebhook::EVENT_ISSUE_CLOSED: - return array(); - default: - return array('column_id' => t('Column')); - } + return array(); } /** @@ -63,17 +51,7 @@ class TaskClose extends Base */ public function getEventRequiredParameters() { - switch ($this->event_name) { - case GithubWebhook::EVENT_COMMIT: - case GithubWebhook::EVENT_ISSUE_CLOSED: - case GitlabWebhook::EVENT_COMMIT: - case GitlabWebhook::EVENT_ISSUE_CLOSED: - case BitbucketWebhook::EVENT_COMMIT: - case BitbucketWebhook::EVENT_ISSUE_CLOSED: - return array('task_id'); - default: - return array('task_id', 'column_id'); - } + return array('task_id'); } /** @@ -97,16 +75,6 @@ class TaskClose extends Base */ public function hasRequiredCondition(array $data) { - switch ($this->event_name) { - case GithubWebhook::EVENT_COMMIT: - case GithubWebhook::EVENT_ISSUE_CLOSED: - case GitlabWebhook::EVENT_COMMIT: - case GitlabWebhook::EVENT_ISSUE_CLOSED: - case BitbucketWebhook::EVENT_COMMIT: - case BitbucketWebhook::EVENT_ISSUE_CLOSED: - return true; - default: - return $data['column_id'] == $this->getParam('column_id'); - } + return true; } } diff --git a/sources/app/Action/TaskCloseColumn.php b/sources/app/Action/TaskCloseColumn.php new file mode 100644 index 0000000..09af3b9 --- /dev/null +++ b/sources/app/Action/TaskCloseColumn.php @@ -0,0 +1,84 @@ + t('Column')); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array('task_id', 'column_id'); + } + + /** + * Execute the action (close the task) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + return $this->taskStatus->close($data['task_id']); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/sources/app/Action/TaskCreation.php b/sources/app/Action/TaskCreation.php index af1403f..290c31e 100644 --- a/sources/app/Action/TaskCreation.php +++ b/sources/app/Action/TaskCreation.php @@ -2,10 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\GitlabWebhook; -use Kanboard\Integration\BitbucketWebhook; - /** * Create automatically a task from a webhook * @@ -14,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook; */ class TaskCreation extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Create a task from an external provider'); + } + /** * Get the list of compatible events * @@ -22,11 +29,7 @@ class TaskCreation extends Base */ public function getCompatibleEvents() { - return array( - GithubWebhook::EVENT_ISSUE_OPENED, - GitlabWebhook::EVENT_ISSUE_OPENED, - BitbucketWebhook::EVENT_ISSUE_OPENED, - ); + return array(); } /** diff --git a/sources/app/Action/TaskDuplicateAnotherProject.php b/sources/app/Action/TaskDuplicateAnotherProject.php index 1f6684d..5bcdce0 100644 --- a/sources/app/Action/TaskDuplicateAnotherProject.php +++ b/sources/app/Action/TaskDuplicateAnotherProject.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskDuplicateAnotherProject extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Duplicate the task to another project'); + } + /** * Get the list of compatible events * @@ -51,7 +62,6 @@ class TaskDuplicateAnotherProject extends Base return array( 'task_id', 'column_id', - 'project_id', ); } @@ -65,7 +75,6 @@ class TaskDuplicateAnotherProject extends Base public function doAction(array $data) { $destination_column_id = $this->board->getFirstColumn($this->getParam('project_id')); - return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id); } diff --git a/sources/app/Action/TaskEmail.php b/sources/app/Action/TaskEmail.php index 7fb76c4..4e0e06a 100644 --- a/sources/app/Action/TaskEmail.php +++ b/sources/app/Action/TaskEmail.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskEmail extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Send a task by email to someone'); + } + /** * Get the list of compatible events * diff --git a/sources/app/Action/TaskMoveAnotherProject.php b/sources/app/Action/TaskMoveAnotherProject.php index 476e203..fdff0d8 100644 --- a/sources/app/Action/TaskMoveAnotherProject.php +++ b/sources/app/Action/TaskMoveAnotherProject.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveAnotherProject extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another project'); + } + /** * Get the list of compatible events * diff --git a/sources/app/Action/TaskMoveColumnAssigned.php b/sources/app/Action/TaskMoveColumnAssigned.php index 16622ee..1b23a59 100644 --- a/sources/app/Action/TaskMoveColumnAssigned.php +++ b/sources/app/Action/TaskMoveColumnAssigned.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveColumnAssigned extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another column when assigned to a user'); + } + /** * Get the list of compatible events * @@ -51,7 +62,6 @@ class TaskMoveColumnAssigned extends Base return array( 'task_id', 'column_id', - 'project_id', 'owner_id' ); } diff --git a/sources/app/Action/TaskMoveColumnCategoryChange.php b/sources/app/Action/TaskMoveColumnCategoryChange.php index 1e12be4..0f591ed 100644 --- a/sources/app/Action/TaskMoveColumnCategoryChange.php +++ b/sources/app/Action/TaskMoveColumnCategoryChange.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveColumnCategoryChange extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another column when the category is changed'); + } + /** * Get the list of compatible events * @@ -50,7 +61,6 @@ class TaskMoveColumnCategoryChange extends Base return array( 'task_id', 'column_id', - 'project_id', 'category_id', ); } @@ -71,7 +81,8 @@ class TaskMoveColumnCategoryChange extends Base $data['task_id'], $this->getParam('dest_column_id'), $original_task['position'], - $original_task['swimlane_id'] + $original_task['swimlane_id'], + false ); } diff --git a/sources/app/Action/TaskMoveColumnUnAssigned.php b/sources/app/Action/TaskMoveColumnUnAssigned.php index 617c75a..99ef935 100644 --- a/sources/app/Action/TaskMoveColumnUnAssigned.php +++ b/sources/app/Action/TaskMoveColumnUnAssigned.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskMoveColumnUnAssigned extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Move the task to another column when assignee is cleared'); + } + /** * Get the list of compatible events * @@ -51,7 +62,6 @@ class TaskMoveColumnUnAssigned extends Base return array( 'task_id', 'column_id', - 'project_id', 'owner_id' ); } diff --git a/sources/app/Action/TaskOpen.php b/sources/app/Action/TaskOpen.php index 2e53efa..ec0f96f 100644 --- a/sources/app/Action/TaskOpen.php +++ b/sources/app/Action/TaskOpen.php @@ -2,9 +2,6 @@ namespace Kanboard\Action; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\BitbucketWebhook; - /** * Open automatically a task * @@ -13,6 +10,17 @@ use Kanboard\Integration\BitbucketWebhook; */ class TaskOpen extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Open a task'); + } + /** * Get the list of compatible events * @@ -21,10 +29,7 @@ class TaskOpen extends Base */ public function getCompatibleEvents() { - return array( - GithubWebhook::EVENT_ISSUE_REOPENED, - BitbucketWebhook::EVENT_ISSUE_REOPENED, - ); + return array(); } /** diff --git a/sources/app/Action/TaskUpdateStartDate.php b/sources/app/Action/TaskUpdateStartDate.php index 4cd548a..e5cea01 100644 --- a/sources/app/Action/TaskUpdateStartDate.php +++ b/sources/app/Action/TaskUpdateStartDate.php @@ -12,6 +12,17 @@ use Kanboard\Model\Task; */ class TaskUpdateStartDate extends Base { + /** + * Get automatic action description + * + * @access public + * @return string + */ + public function getDescription() + { + return t('Automatically update the start date'); + } + /** * Get the list of compatible events * @@ -66,7 +77,7 @@ class TaskUpdateStartDate extends Base 'date_started' => time(), ); - return $this->taskModification->update($values); + return $this->taskModification->update($values, false); } /** diff --git a/sources/app/Analytic/AverageLeadCycleTimeAnalytic.php b/sources/app/Analytic/AverageLeadCycleTimeAnalytic.php new file mode 100644 index 0000000..fd85f86 --- /dev/null +++ b/sources/app/Analytic/AverageLeadCycleTimeAnalytic.php @@ -0,0 +1,114 @@ + 0, + 'total_lead_time' => 0, + 'total_cycle_time' => 0, + 'avg_lead_time' => 0, + 'avg_cycle_time' => 0, + ); + + $tasks = $this->getTasks($project_id); + + foreach ($tasks as &$task) { + $stats['count']++; + $stats['total_lead_time'] += $this->calculateLeadTime($task); + $stats['total_cycle_time'] += $this->calculateCycleTime($task); + } + + $stats['avg_lead_time'] = $this->calculateAverage($stats, 'total_lead_time'); + $stats['avg_cycle_time'] = $this->calculateAverage($stats, 'total_cycle_time'); + + return $stats; + } + + /** + * Calculate average + * + * @access private + * @param array &$stats + * @param string $field + * @return float + */ + private function calculateAverage(array &$stats, $field) + { + if ($stats['count'] > 0) { + return (int) ($stats[$field] / $stats['count']); + } + + return 0; + } + + /** + * Calculate lead time + * + * @access private + * @param array &$task + * @return integer + */ + private function calculateLeadTime(array &$task) + { + $end = $task['date_completed'] ?: time(); + $start = $task['date_creation']; + + return $end - $start; + } + + /** + * Calculate cycle time + * + * @access private + * @param array &$task + * @return integer + */ + private function calculateCycleTime(array &$task) + { + if (empty($task['date_started'])) { + return 0; + } + + $end = $task['date_completed'] ?: time(); + $start = $task['date_started']; + + return $end - $start; + } + + /** + * Get the 1000 last created tasks + * + * @access private + * @return array + */ + private function getTasks($project_id) + { + return $this->db + ->table(Task::TABLE) + ->columns('date_completed', 'date_creation', 'date_started') + ->eq('project_id', $project_id) + ->desc('id') + ->limit(1000) + ->findAll(); + } +} diff --git a/sources/app/Analytic/AverageTimeSpentColumnAnalytic.php b/sources/app/Analytic/AverageTimeSpentColumnAnalytic.php new file mode 100644 index 0000000..c3cff54 --- /dev/null +++ b/sources/app/Analytic/AverageTimeSpentColumnAnalytic.php @@ -0,0 +1,153 @@ +initialize($project_id); + + $this->processTasks($stats, $project_id); + $this->calculateAverage($stats); + + return $stats; + } + + /** + * Initialize default values for each column + * + * @access private + * @param integer $project_id + * @return array + */ + private function initialize($project_id) + { + $stats = array(); + $columns = $this->board->getColumnsList($project_id); + + foreach ($columns as $column_id => $column_title) { + $stats[$column_id] = array( + 'count' => 0, + 'time_spent' => 0, + 'average' => 0, + 'title' => $column_title, + ); + } + + return $stats; + } + + /** + * Calculate time spent for each tasks for each columns + * + * @access private + * @param array $stats + * @param integer $project_id + */ + private function processTasks(array &$stats, $project_id) + { + $tasks = $this->getTasks($project_id); + + foreach ($tasks as &$task) { + foreach ($this->getTaskTimeByColumns($task) as $column_id => $time_spent) { + if (isset($stats[$column_id])) { + $stats[$column_id]['count']++; + $stats[$column_id]['time_spent'] += $time_spent; + } + } + } + } + + /** + * Calculate averages + * + * @access private + * @param array $stats + */ + private function calculateAverage(array &$stats) + { + foreach ($stats as &$column) { + $this->calculateColumnAverage($column); + } + } + + /** + * Calculate column average + * + * @access private + * @param array $column + */ + private function calculateColumnAverage(array &$column) + { + if ($column['count'] > 0) { + $column['average'] = (int) ($column['time_spent'] / $column['count']); + } + } + + /** + * Get time spent for each column for a given task + * + * @access private + * @param array $task + * @return array + */ + private function getTaskTimeByColumns(array &$task) + { + $columns = $this->transition->getTimeSpentByTask($task['id']); + + if (! isset($columns[$task['column_id']])) { + $columns[$task['column_id']] = 0; + } + + $columns[$task['column_id']] += $this->getTaskTimeSpentInCurrentColumn($task); + + return $columns; + } + + /** + * Calculate time spent of a task in the current column + * + * @access private + * @param array $task + */ + private function getTaskTimeSpentInCurrentColumn(array &$task) + { + $end = $task['date_completed'] ?: time(); + return $end - $task['date_moved']; + } + + /** + * Fetch the last 1000 tasks + * + * @access private + * @param integer $project_id + * @return array + */ + private function getTasks($project_id) + { + return $this->db + ->table(Task::TABLE) + ->columns('id', 'date_completed', 'date_moved', 'column_id') + ->eq('project_id', $project_id) + ->desc('id') + ->limit(1000) + ->findAll(); + } +} diff --git a/sources/app/Analytic/EstimatedTimeComparisonAnalytic.php b/sources/app/Analytic/EstimatedTimeComparisonAnalytic.php new file mode 100644 index 0000000..490bcd5 --- /dev/null +++ b/sources/app/Analytic/EstimatedTimeComparisonAnalytic.php @@ -0,0 +1,50 @@ +db->table(Task::TABLE) + ->columns('SUM(time_estimated) AS time_estimated', 'SUM(time_spent) AS time_spent', 'is_active') + ->eq('project_id', $project_id) + ->groupBy('is_active') + ->findAll(); + + $metrics = array( + 'open' => array( + 'time_spent' => 0, + 'time_estimated' => 0, + ), + 'closed' => array( + 'time_spent' => 0, + 'time_estimated' => 0, + ), + ); + + foreach ($rows as $row) { + $key = $row['is_active'] == Task::STATUS_OPEN ? 'open' : 'closed'; + $metrics[$key]['time_spent'] = $row['time_spent']; + $metrics[$key]['time_estimated'] = $row['time_estimated']; + } + + return $metrics; + } +} diff --git a/sources/app/Analytic/TaskDistributionAnalytic.php b/sources/app/Analytic/TaskDistributionAnalytic.php new file mode 100644 index 0000000..614c5f7 --- /dev/null +++ b/sources/app/Analytic/TaskDistributionAnalytic.php @@ -0,0 +1,48 @@ +board->getColumns($project_id); + + foreach ($columns as $column) { + $nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']); + $total += $nb_tasks; + + $metrics[] = array( + 'column_title' => $column['title'], + 'nb_tasks' => $nb_tasks, + ); + } + + if ($total === 0) { + return array(); + } + + foreach ($metrics as &$metric) { + $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); + } + + return $metrics; + } +} diff --git a/sources/app/Analytic/UserDistributionAnalytic.php b/sources/app/Analytic/UserDistributionAnalytic.php new file mode 100644 index 0000000..e1815f9 --- /dev/null +++ b/sources/app/Analytic/UserDistributionAnalytic.php @@ -0,0 +1,56 @@ +taskFinder->getAll($project_id); + $users = $this->projectUserRole->getAssignableUsersList($project_id); + + foreach ($tasks as $task) { + $user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0]; + $total++; + + if (! isset($metrics[$user])) { + $metrics[$user] = array( + 'nb_tasks' => 0, + 'percentage' => 0, + 'user' => $user, + ); + } + + $metrics[$user]['nb_tasks']++; + } + + if ($total === 0) { + return array(); + } + + foreach ($metrics as &$metric) { + $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); + } + + ksort($metrics); + + return array_values($metrics); + } +} diff --git a/sources/app/Api/Action.php b/sources/app/Api/Action.php index 0ae91f1..9e3b86f 100644 --- a/sources/app/Api/Action.php +++ b/sources/app/Api/Action.php @@ -12,17 +12,17 @@ class Action extends \Kanboard\Core\Base { public function getAvailableActions() { - return $this->action->getAvailableActions(); + return $this->actionManager->getAvailableActions(); } public function getAvailableActionEvents() { - return $this->action->getAvailableEvents(); + return $this->eventManager->getAll(); } public function getCompatibleActionEvents($action_name) { - return $this->action->getCompatibleEvents($action_name); + return $this->actionManager->getCompatibleEvents($action_name); } public function removeAction($action_id) @@ -32,22 +32,10 @@ class Action extends \Kanboard\Core\Base public function getActions($project_id) { - $actions = $this->action->getAllByProject($project_id); - - foreach ($actions as $index => $action) { - $params = array(); - - foreach ($action['params'] as $param) { - $params[$param['name']] = $param['value']; - } - - $actions[$index]['params'] = $params; - } - - return $actions; + return $this->action->getAllByProject($project_id); } - public function createAction($project_id, $event_name, $action_name, $params) + public function createAction($project_id, $event_name, $action_name, array $params) { $values = array( 'project_id' => $project_id, @@ -56,23 +44,23 @@ class Action extends \Kanboard\Core\Base 'params' => $params, ); - list($valid, ) = $this->action->validateCreation($values); + list($valid, ) = $this->actionValidator->validateCreation($values); if (! $valid) { return false; } // Check if the action exists - $actions = $this->action->getAvailableActions(); + $actions = $this->actionManager->getAvailableActions(); if (! isset($actions[$action_name])) { return false; } // Check the event - $action = $this->action->load($action_name, $project_id, $event_name); + $action = $this->actionManager->getAction($action_name); - if (! in_array($event_name, $action->getCompatibleEvents())) { + if (! in_array($event_name, $action->getEvents())) { return false; } diff --git a/sources/app/Api/App.php b/sources/app/Api/App.php index d082bcf..635f1ce 100644 --- a/sources/app/Api/App.php +++ b/sources/app/Api/App.php @@ -34,4 +34,14 @@ class App extends \Kanboard\Core\Base { return $this->color->getList(); } + + public function getApplicationRoles() + { + return $this->role->getApplicationRoles(); + } + + public function getProjectRoles() + { + return $this->role->getProjectRoles(); + } } diff --git a/sources/app/Api/Auth.php b/sources/app/Api/Auth.php index a084d6e..c7c5298 100644 --- a/sources/app/Api/Auth.php +++ b/sources/app/Api/Auth.php @@ -3,7 +3,6 @@ namespace Kanboard\Api; use JsonRPC\AuthenticationFailure; -use Symfony\Component\EventDispatcher\Event; /** * Base class @@ -24,15 +23,58 @@ class Auth extends Base */ public function checkCredentials($username, $password, $class, $method) { - $this->container['dispatcher']->dispatch('api.bootstrap', new Event); + $this->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->getApiToken(); + } + + /** + * Get API Token + * + * @access private + * @return string + */ + private function getApiToken() + { + if (defined('API_AUTHENTICATION_TOKEN')) { + return API_AUTHENTICATION_TOKEN; + } + + return $this->config->get('api_token'); + } } diff --git a/sources/app/Api/Category.php b/sources/app/Api/Category.php index 458eaef..fbd61c5 100644 --- a/sources/app/Api/Category.php +++ b/sources/app/Api/Category.php @@ -32,7 +32,7 @@ class Category extends \Kanboard\Core\Base 'name' => $name, ); - list($valid, ) = $this->category->validateCreation($values); + list($valid, ) = $this->categoryValidator->validateCreation($values); return $valid ? $this->category->create($values) : false; } @@ -43,7 +43,7 @@ class Category extends \Kanboard\Core\Base 'name' => $name, ); - list($valid, ) = $this->category->validateModification($values); + list($valid, ) = $this->categoryValidator->validateModification($values); return $valid && $this->category->update($values); } } diff --git a/sources/app/Api/Comment.php b/sources/app/Api/Comment.php index 26b632e..1fc1c70 100644 --- a/sources/app/Api/Comment.php +++ b/sources/app/Api/Comment.php @@ -25,15 +25,16 @@ class Comment extends \Kanboard\Core\Base return $this->comment->remove($comment_id); } - public function createComment($task_id, $user_id, $content) + public function createComment($task_id, $user_id, $content, $reference = '') { $values = array( 'task_id' => $task_id, 'user_id' => $user_id, 'comment' => $content, + 'reference' => $reference, ); - list($valid, ) = $this->comment->validateCreation($values); + list($valid, ) = $this->commentValidator->validateCreation($values); return $valid ? $this->comment->create($values) : false; } @@ -45,7 +46,7 @@ class Comment extends \Kanboard\Core\Base 'comment' => $content, ); - list($valid, ) = $this->comment->validateModification($values); + list($valid, ) = $this->commentValidator->validateModification($values); return $valid && $this->comment->update($values); } } diff --git a/sources/app/Api/File.php b/sources/app/Api/File.php index be415ec..269803e 100644 --- a/sources/app/Api/File.php +++ b/sources/app/Api/File.php @@ -32,14 +32,18 @@ class File extends \Kanboard\Core\Base } } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); + return ''; } - - return ''; } public function createFile($project_id, $task_id, $filename, $blob) { - return $this->file->uploadContent($project_id, $task_id, $filename, $blob); + try { + return $this->file->uploadContent($project_id, $task_id, $filename, $blob); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + return false; + } } public function removeFile($file_id) diff --git a/sources/app/Api/Group.php b/sources/app/Api/Group.php new file mode 100644 index 0000000..a1e0a73 --- /dev/null +++ b/sources/app/Api/Group.php @@ -0,0 +1,49 @@ +group->create($name, $external_id); + } + + public function updateGroup($group_id, $name = null, $external_id = null) + { + $values = array( + 'id' => $group_id, + 'name' => $name, + 'external_id' => $external_id, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + return $this->group->update($values); + } + + public function removeGroup($group_id) + { + return $this->group->remove($group_id); + } + + public function getGroup($group_id) + { + return $this->group->getById($group_id); + } + + public function getAllGroups() + { + return $this->group->getAll(); + } +} diff --git a/sources/app/Api/GroupMember.php b/sources/app/Api/GroupMember.php new file mode 100644 index 0000000..de62f0c --- /dev/null +++ b/sources/app/Api/GroupMember.php @@ -0,0 +1,32 @@ +groupMember->getMembers($group_id); + } + + public function addGroupMember($group_id, $user_id) + { + return $this->groupMember->addUser($group_id, $user_id); + } + + public function removeGroupMember($group_id, $user_id) + { + return $this->groupMember->removeUser($group_id, $user_id); + } + + public function isGroupMember($group_id, $user_id) + { + return $this->groupMember->isMember($group_id, $user_id); + } +} diff --git a/sources/app/Api/Link.php b/sources/app/Api/Link.php index d4df18f..23a9916 100644 --- a/sources/app/Api/Link.php +++ b/sources/app/Api/Link.php @@ -72,7 +72,7 @@ class Link extends \Kanboard\Core\Base 'opposite_label' => $opposite_label, ); - list($valid, ) = $this->link->validateCreation($values); + list($valid, ) = $this->linkValidator->validateCreation($values); return $valid ? $this->link->create($label, $opposite_label) : false; } @@ -93,7 +93,7 @@ class Link extends \Kanboard\Core\Base 'label' => $label, ); - list($valid, ) = $this->link->validateModification($values); + list($valid, ) = $this->linkValidator->validateModification($values); return $valid && $this->link->update($values); } diff --git a/sources/app/Api/Me.php b/sources/app/Api/Me.php index 2c4161f..df8ec07 100644 --- a/sources/app/Api/Me.php +++ b/sources/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); } @@ -44,13 +44,13 @@ class Me extends Base 'is_private' => 1, ); - list($valid, ) = $this->project->validateCreation($values); + list($valid, ) = $this->projectValidator->validateCreation($values); return $valid ? $this->project->create($values, $this->userSession->getId(), true) : false; } 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/sources/app/Api/Project.php b/sources/app/Api/Project.php index f934432..8e311f7 100644 --- a/sources/app/Api/Project.php +++ b/sources/app/Api/Project.php @@ -69,7 +69,7 @@ class Project extends Base 'description' => $description ); - list($valid, ) = $this->project->validateCreation($values); + list($valid, ) = $this->projectValidator->validateCreation($values); return $valid ? $this->project->create($values) : false; } @@ -81,7 +81,7 @@ class Project extends Base 'description' => $description ); - list($valid, ) = $this->project->validateModification($values); + list($valid, ) = $this->projectValidator->validateModification($values); return $valid && $this->project->update($values); } } diff --git a/sources/app/Api/ProjectPermission.php b/sources/app/Api/ProjectPermission.php index 8032339..11e92af 100644 --- a/sources/app/Api/ProjectPermission.php +++ b/sources/app/Api/ProjectPermission.php @@ -2,26 +2,71 @@ namespace Kanboard\Api; +use Kanboard\Core\Security\Role; + /** - * ProjectPermission API controller + * Project Permission API controller * * @package api * @author Frederic Guillot */ class ProjectPermission extends \Kanboard\Core\Base { + public function getProjectUsers($project_id) + { + return $this->projectUserRole->getAllUsers($project_id); + } + + public function getAssignableUsers($project_id, $prepend_unassigned = false) + { + return $this->projectUserRole->getAssignableUsersList($project_id, $prepend_unassigned); + } + + public function addProjectUser($project_id, $user_id, $role = Role::PROJECT_MEMBER) + { + return $this->projectUserRole->addUser($project_id, $user_id, $role); + } + + public function addProjectGroup($project_id, $group_id, $role = Role::PROJECT_MEMBER) + { + return $this->projectGroupRole->addGroup($project_id, $group_id, $role); + } + + public function removeProjectUser($project_id, $user_id) + { + return $this->projectUserRole->removeUser($project_id, $user_id); + } + + public function removeProjectGroup($project_id, $group_id) + { + return $this->projectGroupRole->removeGroup($project_id, $group_id); + } + + public function changeProjectUserRole($project_id, $user_id, $role) + { + return $this->projectUserRole->changeUserRole($project_id, $user_id, $role); + } + + public function changeProjectGroupRole($project_id, $group_id, $role) + { + return $this->projectGroupRole->changeGroupRole($project_id, $group_id, $role); + } + + // Deprecated public function getMembers($project_id) { - return $this->projectPermission->getMembers($project_id); + return $this->getProjectUsers($project_id); } + // Deprecated public function revokeUser($project_id, $user_id) { - return $this->projectPermission->revokeMember($project_id, $user_id); + return $this->removeProjectUser($project_id, $user_id); } + // Deprecated public function allowUser($project_id, $user_id) { - return $this->projectPermission->addMember($project_id, $user_id); + return $this->addProjectUser($project_id, $user_id); } } diff --git a/sources/app/Api/Subtask.php b/sources/app/Api/Subtask.php index 7baee3d..782fdb0 100644 --- a/sources/app/Api/Subtask.php +++ b/sources/app/Api/Subtask.php @@ -36,7 +36,7 @@ class Subtask extends \Kanboard\Core\Base 'status' => $status, ); - list($valid, ) = $this->subtask->validateCreation($values); + list($valid, ) = $this->subtaskValidator->validateCreation($values); return $valid ? $this->subtask->create($values) : false; } @@ -58,7 +58,7 @@ class Subtask extends \Kanboard\Core\Base } } - list($valid, ) = $this->subtask->validateApiModification($values); + list($valid, ) = $this->subtaskValidator->validateApiModification($values); return $valid && $this->subtask->update($values); } } diff --git a/sources/app/Api/Task.php b/sources/app/Api/Task.php index 0dceb20..f132bcd 100644 --- a/sources/app/Api/Task.php +++ b/sources/app/Api/Task.php @@ -64,6 +64,16 @@ class Task extends Base return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id); } + public function moveTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) + { + return $this->taskDuplication->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); + } + + public function duplicateTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) + { + return $this->taskDuplication->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); + } + public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0, $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0, $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0, @@ -71,6 +81,14 @@ class Task extends Base { $this->checkProjectPermission($project_id); + if ($owner_id !== 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) { + return false; + } + + if ($this->userSession->isLogged()) { + $creator_id = $this->userSession->getId(); + } + $values = array( 'title' => $title, 'project_id' => $project_id, @@ -96,20 +114,28 @@ class Task extends Base return $valid ? $this->taskCreation->create($values) : false; } - public function updateTask($id, $title = null, $project_id = null, $color_id = null, $owner_id = null, - $creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null, + public function updateTask($id, $title = null, $color_id = null, $owner_id = null, + $date_due = null, $description = null, $category_id = null, $score = null, $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null, $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null) { $this->checkTaskPermission($id); + $project_id = $this->taskFinder->getProjectId($id); + + if ($project_id === 0) { + return false; + } + + if ($owner_id !== null && $owner_id != 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) { + return false; + } + $values = array( 'id' => $id, 'title' => $title, - 'project_id' => $project_id, 'color_id' => $color_id, 'owner_id' => $owner_id, - 'creator_id' => $creator_id, 'date_due' => $date_due, 'description' => $description, 'category_id' => $category_id, diff --git a/sources/app/Api/User.php b/sources/app/Api/User.php index 105723d..63c222f 100644 --- a/sources/app/Api/User.php +++ b/sources/app/Api/User.php @@ -2,7 +2,11 @@ namespace Kanboard\Api; -use Kanboard\Auth\Ldap; +use LogicException; +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); + list($valid, ) = $this->userValidator->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) { + $ldap = LdapClient::connect(); + $user = LdapUser::getUser($ldap, sprintf(LDAP_USER_FILTER, $username)); + + 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'); + } + + $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; } - - $values = array( - 'username' => $user['username'], - 'name' => $user['name'], - 'email' => $user['email'], - 'is_ldap_user' => 1, - 'is_admin' => $is_admin, - 'is_project_admin' => $is_project_admin, - ); - - return $this->user->create($values); } - public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $is_project_admin = null) + 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) { @@ -81,7 +94,7 @@ class User extends \Kanboard\Core\Base } } - list($valid, ) = $this->user->validateApiModification($values); + list($valid, ) = $this->userValidator->validateApiModification($values); return $valid && $this->user->update($values); } } diff --git a/sources/app/Auth/Database.php b/sources/app/Auth/Database.php deleted file mode 100644 index c2041d4..0000000 --- a/sources/app/Auth/Database.php +++ /dev/null @@ -1,49 +0,0 @@ -db - ->table(User::TABLE) - ->eq('username', $username) - ->eq('disable_login_form', 0) - ->eq('is_ldap_user', 0) - ->findOne(); - - if (is_array($user) && password_verify($password, $user['password'])) { - $this->userSession->initialize($user); - $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id'])); - return true; - } - - return false; - } -} diff --git a/sources/app/Auth/DatabaseAuth.php b/sources/app/Auth/DatabaseAuth.php new file mode 100644 index 0000000..5a8ee64 --- /dev/null +++ b/sources/app/Auth/DatabaseAuth.php @@ -0,0 +1,125 @@ +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 \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/sources/app/Auth/Github.php b/sources/app/Auth/Github.php deleted file mode 100644 index 4777152..0000000 --- a/sources/app/Auth/Github.php +++ /dev/null @@ -1,123 +0,0 @@ -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/sources/app/Auth/GithubAuth.php b/sources/app/Auth/GithubAuth.php new file mode 100644 index 0000000..8369958 --- /dev/null +++ b/sources/app/Auth/GithubAuth.php @@ -0,0 +1,143 @@ +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 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 public + * @return array + */ + public 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/sources/app/Auth/Gitlab.php b/sources/app/Auth/Gitlab.php deleted file mode 100644 index 698b59c..0000000 --- a/sources/app/Auth/Gitlab.php +++ /dev/null @@ -1,123 +0,0 @@ -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/sources/app/Auth/GitlabAuth.php b/sources/app/Auth/GitlabAuth.php new file mode 100644 index 0000000..c0a2cf9 --- /dev/null +++ b/sources/app/Auth/GitlabAuth.php @@ -0,0 +1,143 @@ +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 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 public + * @return array + */ + public 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/sources/app/Auth/Google.php b/sources/app/Auth/Google.php deleted file mode 100644 index 6c1bc3c..0000000 --- a/sources/app/Auth/Google.php +++ /dev/null @@ -1,124 +0,0 @@ -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/sources/app/Auth/GoogleAuth.php b/sources/app/Auth/GoogleAuth.php new file mode 100644 index 0000000..6eacf0b --- /dev/null +++ b/sources/app/Auth/GoogleAuth.php @@ -0,0 +1,143 @@ +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 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 public + * @return array + */ + public 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/sources/app/Auth/Ldap.php b/sources/app/Auth/Ldap.php deleted file mode 100644 index 3d361aa..0000000 --- a/sources/app/Auth/Ldap.php +++ /dev/null @@ -1,521 +0,0 @@ -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/sources/app/Auth/LdapAuth.php b/sources/app/Auth/LdapAuth.php new file mode 100644 index 0000000..b4efbb5 --- /dev/null +++ b/sources/app/Auth/LdapAuth.php @@ -0,0 +1,172 @@ +getLdapUsername(), $this->getLdapPassword()); + $user = LdapUser::getUser($client, $this->username); + + 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 ($client->authenticate($user->getDn(), $this->password)) { + $this->userInfo = $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->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; + } + + /** + * 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/sources/app/Auth/RememberMe.php b/sources/app/Auth/RememberMe.php deleted file mode 100644 index 0a567cb..0000000 --- a/sources/app/Auth/RememberMe.php +++ /dev/null @@ -1,323 +0,0 @@ -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/sources/app/Auth/RememberMeAuth.php b/sources/app/Auth/RememberMeAuth.php new file mode 100644 index 0000000..509a511 --- /dev/null +++ b/sources/app/Auth/RememberMeAuth.php @@ -0,0 +1,79 @@ +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 DatabaseUserProvider + */ + public function getUser() + { + if (empty($this->userInfo)) { + return null; + } + + return new DatabaseUserProvider($this->userInfo); + } +} diff --git a/sources/app/Auth/ReverseProxy.php b/sources/app/Auth/ReverseProxy.php deleted file mode 100644 index d119ca9..0000000 --- a/sources/app/Auth/ReverseProxy.php +++ /dev/null @@ -1,83 +0,0 @@ -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/sources/app/Auth/ReverseProxyAuth.php b/sources/app/Auth/ReverseProxyAuth.php new file mode 100644 index 0000000..b9730c5 --- /dev/null +++ b/sources/app/Auth/ReverseProxyAuth.php @@ -0,0 +1,76 @@ +request->getRemoteUser(); + + if (! empty($username)) { + $this->userInfo = 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 ReverseProxyUserProvider + */ + public function getUser() + { + return $this->userInfo; + } +} diff --git a/sources/app/Auth/TotpAuth.php b/sources/app/Auth/TotpAuth.php new file mode 100644 index 0000000..f430493 --- /dev/null +++ b/sources/app/Auth/TotpAuth.php @@ -0,0 +1,144 @@ +checkTotp(Base32::decode($this->secret), $this->code); + } + + /** + * Called before to prompt the user + * + * @access public + */ + public function beforeCode() + { + + } + + /** + * Set validation code + * + * @access public + * @param string $code + */ + public function setCode($code) + { + $this->code = $code; + } + + /** + * Generate secret + * + * @access public + * @return string + */ + public function generateSecret() + { + $this->secret = GoogleAuthenticator::generateRandom(); + return $this->secret; + } + + /** + * 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() + { + 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/sources/app/Controller/Action.php b/sources/app/Controller/Action.php index ad13606..645b53b 100644 --- a/sources/app/Controller/Action.php +++ b/sources/app/Controller/Action.php @@ -18,17 +18,18 @@ class Action extends Base public function index() { $project = $this->getProject(); + $actions = $this->action->getAllByProject($project['id']); $this->response->html($this->projectLayout('action/index', array( 'values' => array('project_id' => $project['id']), 'project' => $project, - 'actions' => $this->action->getAllByProject($project['id']), - 'available_actions' => $this->action->getAvailableActions(), - 'available_events' => $this->action->getAvailableEvents(), - 'available_params' => $this->action->getAllActionParameters(), + 'actions' => $actions, + 'available_actions' => $this->actionManager->getAvailableActions(), + 'available_events' => $this->eventManager->getAll(), + 'available_params' => $this->actionManager->getAvailableParameters($actions), 'columns_list' => $this->board->getColumnsList($project['id']), - 'users_list' => $this->projectPermission->getMemberList($project['id']), - 'projects_list' => $this->project->getList(false), + 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), + 'projects_list' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), 'links_list' => $this->link->getList(0, false), @@ -53,7 +54,7 @@ class Action extends Base $this->response->html($this->projectLayout('action/event', array( 'values' => $values, 'project' => $project, - 'events' => $this->action->getCompatibleEvents($values['action_name']), + 'events' => $this->actionManager->getCompatibleEvents($values['action_name']), 'title' => t('Automatic actions') ))); } @@ -72,21 +73,21 @@ class Action extends Base $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); } - $action = $this->action->load($values['action_name'], $values['project_id'], $values['event_name']); + $action = $this->actionManager->getAction($values['action_name']); $action_params = $action->getActionRequiredParameters(); if (empty($action_params)) { $this->doCreation($project, $values + array('params' => array())); } - $projects_list = $this->project->getList(false); + $projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); unset($projects_list[$project['id']]); $this->response->html($this->projectLayout('action/params', array( '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']), @@ -115,7 +116,7 @@ class Action extends Base */ private function doCreation(array $project, array $values) { - list($valid, ) = $this->action->validateCreation($values); + list($valid, ) = $this->actionValidator->validateCreation($values); if ($valid) { if ($this->action->create($values) !== false) { @@ -139,8 +140,8 @@ class Action extends Base $this->response->html($this->projectLayout('action/remove', array( 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), - 'available_events' => $this->action->getAvailableEvents(), - 'available_actions' => $this->action->getAvailableActions(), + 'available_events' => $this->eventManager->getAll(), + 'available_actions' => $this->actionManager->getAvailableActions(), 'project' => $project, 'title' => t('Remove an action') ))); diff --git a/sources/app/Controller/Activity.php b/sources/app/Controller/Activity.php index 24327c2..3865834 100644 --- a/sources/app/Controller/Activity.php +++ b/sources/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->getActiveProjectsByUser($this->userSession->getId()), 'events' => $this->projectActivity->getProject($project['id']), 'project' => $project, 'title' => t('%s\'s activity', $project['name']) diff --git a/sources/app/Controller/Analytic.php b/sources/app/Controller/Analytic.php index 1082b46..d203fb8 100644 --- a/sources/app/Controller/Analytic.php +++ b/sources/app/Controller/Analytic.php @@ -1,6 +1,7 @@ projectPermission->getAllowedProjects($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); $params['content_for_sublayout'] = $this->template->render($template, $params); return $this->template->layout('analytic/layout', $params); @@ -34,17 +35,7 @@ class Analytic extends Base public function leadAndCycleTime() { $project = $this->getProject(); - $values = $this->request->getValues(); - - $this->projectDailyStats->updateTotals($project['id'], date('Y-m-d')); - - $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); - $to = $this->request->getStringParam('to', date('Y-m-d')); - - if (! empty($values)) { - $from = $values['from']; - $to = $values['to']; - } + list($from, $to) = $this->getDates(); $this->response->html($this->layout('analytic/lead_cycle_time', array( 'values' => array( @@ -52,7 +43,7 @@ class Analytic extends Base 'to' => $to, ), 'project' => $project, - 'average' => $this->projectAnalytic->getAverageLeadAndCycleTime($project['id']), + 'average' => $this->averageLeadCycleTimeAnalytic->build($project['id']), 'metrics' => $this->projectDailyStats->getRawMetrics($project['id'], $from, $to), 'date_format' => $this->config->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats(), @@ -60,6 +51,32 @@ class Analytic extends Base ))); } + /** + * Show comparison between actual and estimated hours chart + * + * @access public + */ + public function compareHours() + { + $project = $this->getProject(); + $params = $this->getProjectFilters('analytic', 'compareHours'); + $query = $this->taskFilter->create()->filterByProject($params['project']['id'])->getQuery(); + + $paginator = $this->paginator + ->setUrl('analytic', 'compareHours', array('project_id' => $project['id'])) + ->setMax(30) + ->setOrder(TaskModel::TABLE.'.id') + ->setQuery($query) + ->calculate(); + + $this->response->html($this->layout('analytic/compare_hours', array( + 'project' => $project, + 'paginator' => $paginator, + 'metrics' => $this->estimatedTimeComparisonAnalytic->build($project['id']), + 'title' => t('Compare hours for "%s"', $project['name']), + ))); + } + /** * Show average time spent by column * @@ -71,7 +88,7 @@ class Analytic extends Base $this->response->html($this->layout('analytic/avg_time_columns', array( 'project' => $project, - 'metrics' => $this->projectAnalytic->getAverageTimeSpentByColumn($project['id']), + 'metrics' => $this->averageTimeSpentColumnAnalytic->build($project['id']), 'title' => t('Average time spent into each column for "%s"', $project['name']), ))); } @@ -87,7 +104,7 @@ class Analytic extends Base $this->response->html($this->layout('analytic/tasks', array( 'project' => $project, - 'metrics' => $this->projectAnalytic->getTaskRepartition($project['id']), + 'metrics' => $this->taskDistributionAnalytic->build($project['id']), 'title' => t('Task repartition for "%s"', $project['name']), ))); } @@ -103,7 +120,7 @@ class Analytic extends Base $this->response->html($this->layout('analytic/users', array( 'project' => $project, - 'metrics' => $this->projectAnalytic->getUserRepartition($project['id']), + 'metrics' => $this->userDistributionAnalytic->build($project['id']), 'title' => t('User repartition for "%s"', $project['name']), ))); } @@ -132,21 +149,14 @@ 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) { $project = $this->getProject(); - $values = $this->request->getValues(); - - $this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d')); - - $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); - $to = $this->request->getStringParam('to', date('Y-m-d')); - - if (! empty($values)) { - $from = $values['from']; - $to = $values['to']; - } + list($from, $to) = $this->getDates(); $display_graph = $this->projectDailyColumnStats->countDays($project['id'], $from, $to) >= 2; @@ -163,4 +173,19 @@ class Analytic extends Base 'title' => t($title, $project['name']), ))); } + + private function getDates() + { + $values = $this->request->getValues(); + + $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); + $to = $this->request->getStringParam('to', date('Y-m-d')); + + if (! empty($values)) { + $from = $values['from']; + $to = $values['to']; + } + + return array($from, $to); + } } diff --git a/sources/app/Controller/App.php b/sources/app/Controller/App.php index 2fae004..bdd7fbc 100644 --- a/sources/app/Controller/App.php +++ b/sources/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->getActiveProjectsByUser($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('

'.t('Nothing to preview...').'

'); - } - - $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/sources/app/Controller/Auth.php b/sources/app/Controller/Auth.php index b90e756..5284e12 100644 --- a/sources/app/Controller/Auth.php +++ b/sources/app/Controller/Auth.php @@ -2,8 +2,6 @@ namespace Kanboard\Controller; -use Gregwar\Captcha\CaptchaBuilder; - /** * Authentication controller * @@ -24,7 +22,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 +38,11 @@ class Auth extends Base public function check() { $values = $this->request->getValues(); - list($valid, $errors) = $this->authentication->validateForm($values); + $this->sessionStorage->hasRememberMe = ! empty($values['remember_me']); + list($valid, $errors) = $this->authValidator->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,23 +55,23 @@ 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')); } /** - * Display captcha image + * Redirect the user after the authentication * - * @access public + * @access private */ - public function captcha() + private function redirectAfterLogin() { - $this->response->contentType('image/jpeg'); + 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); + } - $builder = new CaptchaBuilder; - $builder->build(); - $this->sessionStorage->captcha = $builder->getPhrase(); - $builder->output(); + $this->response->redirect($this->helper->url->to('app', 'index')); } } diff --git a/sources/app/Controller/Base.php b/sources/app/Controller/Base.php index 8630f00..66a9e84 100644 --- a/sources/app/Controller/Base.php +++ b/sources/app/Controller/Base.php @@ -2,8 +2,7 @@ namespace Kanboard\Controller; -use Pimple\Container; -use Symfony\Component\EventDispatcher\Event; +use Kanboard\Core\Security\Role; /** * Base controller @@ -14,36 +13,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() { - if (DEBUG) { - foreach ($this->db->getLogMessages() as $message) { - $this->logger->debug($message); - } + $this->sessionManager->open(); + $this->dispatcher->dispatch('app.bootstrap'); + $this->sendHeaders(); + $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($this->router->getController(), $this->router->getAction(), Role::APP_PUBLIC)) { + $this->handleAuthentication(); + $this->handlePostAuthentication(); + $this->checkApplicationAuthorization(); + $this->checkProjectAuthorization(); } } @@ -52,7 +37,7 @@ abstract class Base extends \Kanboard\Core\Base * * @access private */ - private function sendHeaders($action) + private function sendHeaders() { // HTTP secure headers $this->response->csp($this->container['cspRules']); @@ -60,7 +45,7 @@ abstract class Base extends \Kanboard\Core\Base $this->response->xss(); // Allow the public board iframe inclusion - if (ENABLE_XFRAME && $action !== 'readonly') { + if (ENABLE_XFRAME && $this->router->getAction() !== 'readonly') { $this->response->xframe(); } @@ -69,34 +54,14 @@ 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 +72,17 @@ 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 = strtolower($this->router->getController()); + $action = strtolower($this->router->getAction()); $ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout'); - if ($ignore === false && $this->userSession->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 +92,23 @@ abstract class Base extends \Kanboard\Core\Base } /** - * Check page access and authorization + * Check application authorization * - * @access public + * @access private */ - public function handleAuthorization($controller, $action) + private function checkApplicationAuthorization() + { + if (! $this->helper->user->hasAccess($this->router->getController(), $this->router->getAction())) { + $this->forbidden(); + } + } + + /** + * Check project authorization + * + * @access private + */ + private function checkProjectAuthorization() { $project_id = $this->request->getIntegerParam('project_id'); $task_id = $this->request->getIntegerParam('task_id'); @@ -139,7 +118,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($this->router->getController(), $this->router->getAction(), $project_id)) { $this->forbidden(); } } @@ -147,10 +126,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 +140,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('Access Forbidden', 403); + } + $this->response->html($this->template->layout('app/forbidden', array( 'title' => t('Access Forbidden'), 'no_layout' => $no_layout, @@ -209,7 +192,7 @@ abstract class Base extends \Kanboard\Core\Base $content = $this->template->render($template, $params); $params['task_content_for_layout'] = $content; $params['title'] = $params['task']['project_name'].' > '.$params['task']['title']; - $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); return $this->template->layout('task/layout', $params); } @@ -227,7 +210,7 @@ abstract class Base extends \Kanboard\Core\Base $content = $this->template->render($template, $params); $params['project_content_for_layout'] = $content; $params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' > '.$params['title']; - $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); $params['sidebar_template'] = $sidebar_template; return $this->template->layout('project/layout', $params); @@ -300,12 +283,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->getActiveProjectsByUser($this->userSession->getId()); unset($board_selector[$project['id']]); $filters = array( diff --git a/sources/app/Controller/Board.php b/sources/app/Controller/Board.php index 7442ff2..06736cc 100644 --- a/sources/app/Controller/Board.php +++ b/sources/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'], @@ -73,10 +73,6 @@ class Board extends Base return $this->response->status(403); } - if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { - $this->response->text('Forbidden', 403); - } - $values = $this->request->getJson(); $result =$this->taskPosition->movePosition( @@ -101,22 +97,18 @@ class Board extends Base */ public function check() { - if (! $this->request->isAjax()) { - return $this->response->status(403); - } - $project_id = $this->request->getIntegerParam('project_id'); $timestamp = $this->request->getIntegerParam('timestamp'); - if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { - $this->response->text('Forbidden', 403); + if (! $project_id || ! $this->request->isAjax()) { + return $this->response->status(403); } if (! $this->project->isModifiedSince($project_id, $timestamp)) { return $this->response->status(304); } - $this->response->html($this->renderBoard($project_id)); + return $this->response->html($this->renderBoard($project_id)); } /** @@ -126,14 +118,10 @@ class Board extends Base */ public function reload() { - if (! $this->request->isAjax()) { - return $this->response->status(403); - } - $project_id = $this->request->getIntegerParam('project_id'); - if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { - $this->response->text('Forbidden', 403); + if (! $project_id || ! $this->request->isAjax()) { + return $this->response->status(403); } $values = $this->request->getJson(); @@ -142,195 +130,6 @@ class Board extends Base $this->response->html($this->renderBoard($project_id)); } - /** - * 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 * @@ -355,6 +154,7 @@ class Board extends Base * Change display mode * * @access private + * @param boolean $mode */ private function changeDisplayMode($mode) { @@ -372,6 +172,7 @@ class Board extends Base * Render board * * @access private + * @param integer $project_id */ private function renderBoard($project_id) { diff --git a/sources/app/Controller/BoardPopover.php b/sources/app/Controller/BoardPopover.php new file mode 100644 index 0000000..a214439 --- /dev/null +++ b/sources/app/Controller/BoardPopover.php @@ -0,0 +1,136 @@ +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', + ))); + } + + /** + * Confirmation before to close all column tasks + * + * @access public + */ + public function confirmCloseColumnTasks() + { + $project = $this->getProject(); + $column_id = $this->request->getIntegerParam('column_id'); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + $this->response->html($this->template->render('board/popover_close_all_tasks_column', array( + 'project' => $project, + 'nb_tasks' => $this->taskFinder->countByColumnAndSwimlaneId($project['id'], $column_id, $swimlane_id), + 'column' => $this->board->getColumnTitleById($column_id), + 'swimlane' => $this->swimlane->getNameById($swimlane_id) ?: t($project['default_swimlane']), + 'values' => array('column_id' => $column_id, 'swimlane_id' => $swimlane_id), + ))); + } + + /** + * Close all column tasks + * + * @access public + */ + public function closeColumnTasks() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + $this->taskStatus->closeTasksBySwimlaneAndColumn($values['swimlane_id'], $values['column_id']); + $this->flash->success(t('All tasks of the column "%s" and the swimlane "%s" have been closed successfully.', $this->board->getColumnTitleById($values['column_id']), $this->swimlane->getNameById($values['swimlane_id']) ?: t($project['default_swimlane']))); + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id']))); + } +} diff --git a/sources/app/Controller/BoardTooltip.php b/sources/app/Controller/BoardTooltip.php new file mode 100644 index 0000000..ed58a2f --- /dev/null +++ b/sources/app/Controller/BoardTooltip.php @@ -0,0 +1,112 @@ +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/sources/app/Controller/Captcha.php b/sources/app/Controller/Captcha.php new file mode 100644 index 0000000..fcf081e --- /dev/null +++ b/sources/app/Controller/Captcha.php @@ -0,0 +1,29 @@ +response->contentType('image/jpeg'); + + $builder = new CaptchaBuilder; + $builder->build(); + $this->sessionStorage->captcha = $builder->getPhrase(); + $builder->output(); + } +} diff --git a/sources/app/Controller/Category.php b/sources/app/Controller/Category.php index 9864348..a0af413 100644 --- a/sources/app/Controller/Category.php +++ b/sources/app/Controller/Category.php @@ -57,7 +57,7 @@ class Category extends Base $project = $this->getProject(); $values = $this->request->getValues(); - list($valid, $errors) = $this->category->validateCreation($values); + list($valid, $errors) = $this->categoryValidator->validateCreation($values); if ($valid) { if ($this->category->create($values)) { @@ -99,7 +99,7 @@ class Category extends Base $project = $this->getProject(); $values = $this->request->getValues(); - list($valid, $errors) = $this->category->validateModification($values); + list($valid, $errors) = $this->categoryValidator->validateModification($values); if ($valid) { if ($this->category->update($values)) { diff --git a/sources/app/Controller/Column.php b/sources/app/Controller/Column.php index b484fe1..1ce575d 100644 --- a/sources/app/Controller/Column.php +++ b/sources/app/Controller/Column.php @@ -51,7 +51,7 @@ class Column extends Base $values['title['.$column_id.']'] = $column_title; } - list($valid, $errors) = $this->board->validateCreation($data); + list($valid, $errors) = $this->columnValidator->validateCreation($data); if ($valid) { if ($this->board->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) { @@ -94,7 +94,7 @@ class Column extends Base $project = $this->getProject(); $values = $this->request->getValues(); - list($valid, $errors) = $this->board->validateModification($values); + list($valid, $errors) = $this->columnValidator->validateModification($values); if ($valid) { if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) { diff --git a/sources/app/Controller/Comment.php b/sources/app/Controller/Comment.php index 54339e4..a608dd1 100644 --- a/sources/app/Controller/Comment.php +++ b/sources/app/Controller/Comment.php @@ -78,7 +78,7 @@ class Comment extends Base $values = $this->request->getValues(); $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); - list($valid, $errors) = $this->comment->validateCreation($values); + list($valid, $errors) = $this->commentValidator->validateCreation($values); if ($valid) { if ($this->comment->create($values)) { @@ -127,7 +127,7 @@ class Comment extends Base $comment = $this->getComment(); $values = $this->request->getValues(); - list($valid, $errors) = $this->comment->validateModification($values); + list($valid, $errors) = $this->commentValidator->validateModification($values); if ($valid) { if ($this->comment->update($values)) { diff --git a/sources/app/Controller/Config.php b/sources/app/Controller/Config.php index 4980614..4aee855 100644 --- a/sources/app/Controller/Config.php +++ b/sources/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->getActiveProjectsByUser($this->userSession->getId()); $params['values'] = $this->config->getAll(); $params['errors'] = array(); $params['config_content_for_layout'] = $this->template->render($template, $params); @@ -40,6 +40,9 @@ class Config extends Base $values = $this->request->getValues(); switch ($redirect) { + case 'application': + $values += array('password_reset' => 0); + break; case 'project': $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'cfd_include_closed_tasks' => 0); break; diff --git a/sources/app/Controller/Currency.php b/sources/app/Controller/Currency.php index 118b2c4..42e404f 100644 --- a/sources/app/Controller/Currency.php +++ b/sources/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->getActiveProjectsByUser($this->userSession->getId()); $params['config_content_for_layout'] = $this->template->render($template, $params); return $this->template->layout('config/layout', $params); @@ -38,7 +38,7 @@ class Currency extends Base 'values' => $values, 'errors' => $errors, 'rates' => $this->currency->getAll(), - 'currencies' => $this->config->getCurrencies(), + 'currencies' => $this->currency->getCurrencies(), 'title' => t('Settings').' > '.t('Currency rates'), ))); } @@ -51,7 +51,7 @@ class Currency extends Base public function create() { $values = $this->request->getValues(); - list($valid, $errors) = $this->currency->validate($values); + list($valid, $errors) = $this->currencyValidator->validateCreation($values); if ($valid) { if ($this->currency->create($values['currency'], $values['rate'])) { diff --git a/sources/app/Controller/Customfilter.php b/sources/app/Controller/Customfilter.php index d686310..1b43f1d 100644 --- a/sources/app/Controller/Customfilter.php +++ b/sources/app/Controller/Customfilter.php @@ -2,6 +2,8 @@ namespace Kanboard\Controller; +use Kanboard\Core\Security\Role; + /** * Custom Filter management * @@ -40,7 +42,7 @@ class Customfilter extends Base $values = $this->request->getValues(); $values['user_id'] = $this->userSession->getId(); - list($valid, $errors) = $this->customFilter->validateCreation($values); + list($valid, $errors) = $this->customFilterValidator->validateCreation($values); if ($valid) { if ($this->customFilter->create($values)) { @@ -119,7 +121,7 @@ class Customfilter extends Base $values += array('append' => 0); } - list($valid, $errors) = $this->customFilter->validateModification($values); + list($valid, $errors) = $this->customFilterValidator->validateModification($values); if ($valid) { if ($this->customFilter->update($values)) { @@ -137,7 +139,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/sources/app/Controller/Doc.php b/sources/app/Controller/Doc.php index 3241304..a233b12 100644 --- a/sources/app/Controller/Doc.php +++ b/sources/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->getActiveProjectsByUser($this->userSession->getId()), ))); } } diff --git a/sources/app/Controller/Feed.php b/sources/app/Controller/Feed.php index 95b81fb..8457c38 100644 --- a/sources/app/Controller/Feed.php +++ b/sources/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/sources/app/Controller/Gantt.php b/sources/app/Controller/Gantt.php index bd3d92f..ac0e6fa 100644 --- a/sources/app/Controller/Gantt.php +++ b/sources/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->getActiveProjectsByUser($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/sources/app/Controller/Group.php b/sources/app/Controller/Group.php new file mode 100644 index 0000000..e952c0e --- /dev/null +++ b/sources/app/Controller/Group.php @@ -0,0 +1,255 @@ +paginator + ->setUrl('group', 'index') + ->setMax(30) + ->setOrder('name') + ->setQuery($this->group->getQuery()) + ->calculate(); + + $this->response->html($this->template->layout('group/index', array( + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + 'title' => t('Groups').' ('.$paginator->getTotal().')', + 'paginator' => $paginator, + ))); + } + + /** + * List all users + * + * @access public + */ + public function users() + { + $group_id = $this->request->getIntegerParam('group_id'); + $group = $this->group->getById($group_id); + + $paginator = $this->paginator + ->setUrl('group', 'users', array('group_id' => $group_id)) + ->setMax(30) + ->setOrder('username') + ->setQuery($this->groupMember->getQuery($group_id)) + ->calculate(); + + $this->response->html($this->template->layout('group/users', array( + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + 'title' => t('Members of %s', $group['name']).' ('.$paginator->getTotal().')', + 'paginator' => $paginator, + 'group' => $group, + ))); + } + + /** + * Display a form to create a new group + * + * @access public + */ + public function create(array $values = array(), array $errors = array()) + { + $this->response->html($this->template->layout('group/create', array( + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + 'errors' => $errors, + 'values' => $values, + 'title' => t('New group') + ))); + } + + /** + * Validate and save a new group + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->groupValidator->validateCreation($values); + + if ($valid) { + if ($this->group->create($values['name']) !== false) { + $this->flash->success(t('Group created successfully.')); + $this->response->redirect($this->helper->url->to('group', 'index')); + } else { + $this->flash->failure(t('Unable to create your group.')); + } + } + + $this->create($values, $errors); + } + + /** + * Display a form to update a group + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + if (empty($values)) { + $values = $this->group->getById($this->request->getIntegerParam('group_id')); + } + + $this->response->html($this->template->layout('group/edit', array( + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + 'errors' => $errors, + 'values' => $values, + 'title' => t('Edit group') + ))); + } + + /** + * Validate and save a group + * + * @access public + */ + public function update() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->groupValidator->validateModification($values); + + if ($valid) { + if ($this->group->update($values) !== false) { + $this->flash->success(t('Group updated successfully.')); + $this->response->redirect($this->helper->url->to('group', 'index')); + } else { + $this->flash->failure(t('Unable to update your group.')); + } + } + + $this->edit($values, $errors); + } + + /** + * Form to associate a user to a group + * + * @access public + */ + public function associate(array $values = array(), array $errors = array()) + { + $group_id = $this->request->getIntegerParam('group_id'); + $group = $this->group->getbyId($group_id); + + if (empty($values)) { + $values['group_id'] = $group_id; + } + + $this->response->html($this->template->layout('group/associate', array( + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + 'users' => $this->user->prepareList($this->groupMember->getNotMembers($group_id)), + 'group' => $group, + 'errors' => $errors, + 'values' => $values, + 'title' => t('Add group member to "%s"', $group['name']), + ))); + } + + /** + * Add user to a group + * + * @access public + */ + public function addUser() + { + $values = $this->request->getValues(); + + if (isset($values['group_id']) && isset($values['user_id'])) { + if ($this->groupMember->addUser($values['group_id'], $values['user_id'])) { + $this->flash->success(t('Group member added successfully.')); + $this->response->redirect($this->helper->url->to('group', 'users', array('group_id' => $values['group_id']))); + } else { + $this->flash->failure(t('Unable to add group member.')); + } + } + + $this->associate($values); + } + + /** + * Confirmation dialog to remove a user from a group + * + * @access public + */ + public function dissociate() + { + $group_id = $this->request->getIntegerParam('group_id'); + $user_id = $this->request->getIntegerParam('user_id'); + $group = $this->group->getById($group_id); + $user = $this->user->getById($user_id); + + $this->response->html($this->template->layout('group/dissociate', array( + 'group' => $group, + 'user' => $user, + 'title' => t('Remove user from group "%s"', $group['name']), + ))); + } + + /** + * Remove a user from a group + * + * @access public + */ + public function removeUser() + { + $this->checkCSRFParam(); + $group_id = $this->request->getIntegerParam('group_id'); + $user_id = $this->request->getIntegerParam('user_id'); + + if ($this->groupMember->removeUser($group_id, $user_id)) { + $this->flash->success(t('User removed successfully from this group.')); + } else { + $this->flash->failure(t('Unable to remove this user from the group.')); + } + + $this->response->redirect($this->helper->url->to('group', 'users', array('group_id' => $group_id))); + } + + /** + * Confirmation dialog to remove a group + * + * @access public + */ + public function confirm() + { + $group_id = $this->request->getIntegerParam('group_id'); + $group = $this->group->getById($group_id); + + $this->response->html($this->template->layout('group/remove', array( + 'group' => $group, + 'title' => t('Remove group'), + ))); + } + + /** + * Remove a group + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $group_id = $this->request->getIntegerParam('group_id'); + + if ($this->group->remove($group_id)) { + $this->flash->success(t('Group removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this group.')); + } + + $this->response->redirect($this->helper->url->to('group', 'index')); + } +} diff --git a/sources/app/Controller/GroupHelper.php b/sources/app/Controller/GroupHelper.php new file mode 100644 index 0000000..34f522a --- /dev/null +++ b/sources/app/Controller/GroupHelper.php @@ -0,0 +1,24 @@ +request->getStringParam('term'); + $groups = $this->groupManager->find($search); + $this->response->json($this->groupAutoCompleteFormatter->setGroups($groups)->format()); + } +} diff --git a/sources/app/Controller/Link.php b/sources/app/Controller/Link.php index c7f1823..d52d1f9 100644 --- a/sources/app/Controller/Link.php +++ b/sources/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->getActiveProjectsByUser($this->userSession->getId()); $params['config_content_for_layout'] = $this->template->render($template, $params); return $this->template->layout('config/layout', $params); @@ -67,7 +67,7 @@ class Link extends Base public function save() { $values = $this->request->getValues(); - list($valid, $errors) = $this->link->validateCreation($values); + list($valid, $errors) = $this->linkValidator->validateCreation($values); if ($valid) { if ($this->link->create($values['label'], $values['opposite_label']) !== false) { @@ -108,7 +108,7 @@ class Link extends Base public function update() { $values = $this->request->getValues(); - list($valid, $errors) = $this->link->validateModification($values); + list($valid, $errors) = $this->linkValidator->validateModification($values); if ($valid) { if ($this->link->update($values)) { diff --git a/sources/app/Controller/Oauth.php b/sources/app/Controller/Oauth.php index 3954614..ed901de 100644 --- a/sources/app/Controller/Oauth.php +++ b/sources/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/sources/app/Controller/PasswordReset.php b/sources/app/Controller/PasswordReset.php new file mode 100644 index 0000000..23567c9 --- /dev/null +++ b/sources/app/Controller/PasswordReset.php @@ -0,0 +1,120 @@ +checkActivation(); + + $this->response->html($this->template->layout('password_reset/create', array( + 'errors' => $errors, + 'values' => $values, + 'no_layout' => true, + ))); + } + + /** + * Validate and send the email + */ + public function save() + { + $this->checkActivation(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->passwordResetValidator->validateCreation($values); + + if ($valid) { + $this->sendEmail($values['username']); + $this->response->redirect($this->helper->url->to('auth', 'login')); + } + + $this->create($values, $errors); + } + + /** + * Show the form to set a new password + */ + public function change(array $values = array(), array $errors = array()) + { + $this->checkActivation(); + + $token = $this->request->getStringParam('token'); + $user_id = $this->passwordReset->getUserIdByToken($token); + + if ($user_id !== false) { + $this->response->html($this->template->layout('password_reset/change', array( + 'token' => $token, + 'errors' => $errors, + 'values' => $values, + 'no_layout' => true, + ))); + } + + $this->response->redirect($this->helper->url->to('auth', 'login')); + } + + /** + * Set the new password + */ + public function update() + { + $this->checkActivation(); + + $token = $this->request->getStringParam('token'); + $values = $this->request->getValues(); + list($valid, $errors) = $this->passwordResetValidator->validateModification($values); + + if ($valid) { + $user_id = $this->passwordReset->getUserIdByToken($token); + + if ($user_id !== false) { + $this->user->update(array('id' => $user_id, 'password' => $values['password'])); + $this->passwordReset->disable($user_id); + } + + $this->response->redirect($this->helper->url->to('auth', 'login')); + } + + $this->change($values, $errors); + } + + /** + * Send the email + */ + private function sendEmail($username) + { + $token = $this->passwordReset->create($username); + + if ($token !== false) { + $user = $this->user->getByUsername($username); + + $this->emailClient->send( + $user['email'], + $user['name'] ?: $user['username'], + t('Password Reset for Kanboard'), + $this->template->render('password_reset/email', array('token' => $token)) + ); + } + } + + /** + * Check feature availability + */ + private function checkActivation() + { + if ($this->config->get('password_reset', 0) == 0) { + $this->response->redirect($this->helper->url->to('auth', 'login')); + } + } +} diff --git a/sources/app/Controller/Project.php b/sources/app/Controller/Project.php index 2d9c25d..27c827d 100644 --- a/sources/app/Controller/Project.php +++ b/sources/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->getActiveProjectsByUser($this->userSession->getId()), 'paginator' => $paginator, 'nb_projects' => $nb_projects, 'title' => t('Projects').' ('.$nb_projects.')' @@ -160,16 +160,16 @@ 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); } } - list($valid, $errors) = $this->project->validateModification($values); + list($valid, $errors) = $this->projectValidator->validateModification($values); if ($valid) { if ($this->project->update($values)) { @@ -183,120 +183,6 @@ class Project extends Base $this->edit($values, $errors); } - /** - * 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 * @@ -413,17 +299,28 @@ 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->getActiveProjectsByUser($this->userSession->getId()), + 'values' => $values, 'errors' => $errors, 'is_private' => $is_private, 'title' => $is_private ? t('New private project') : t('New project'), ))); } + /** + * 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 * @@ -432,7 +329,7 @@ class Project extends Base public function save() { $values = $this->request->getValues(); - list($valid, $errors) = $this->project->validateCreation($values); + list($valid, $errors) = $this->projectValidator->validateCreation($values); if ($valid) { $project_id = $this->project->create($values, $this->userSession->getId(), true); diff --git a/sources/app/Controller/ProjectPermission.php b/sources/app/Controller/ProjectPermission.php new file mode 100644 index 0000000..4434d01 --- /dev/null +++ b/sources/app/Controller/ProjectPermission.php @@ -0,0 +1,177 @@ +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/sources/app/Controller/Projectuser.php b/sources/app/Controller/Projectuser.php index 18829b3..806ede7 100644 --- a/sources/app/Controller/Projectuser.php +++ b/sources/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->getActiveProjectsByUser($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/sources/app/Controller/Search.php b/sources/app/Controller/Search.php index 0aff907..390210c 100644 --- a/sources/app/Controller/Search.php +++ b/sources/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/sources/app/Controller/Subtask.php b/sources/app/Controller/Subtask.php index 30ddc37..caaaa85 100644 --- a/sources/app/Controller/Subtask.php +++ b/sources/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, ))); } @@ -63,7 +63,7 @@ class Subtask extends Base $task = $this->getTask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->subtask->validateCreation($values); + list($valid, $errors) = $this->subtaskValidator->validateCreation($values); if ($valid) { if ($this->subtask->create($values)) { @@ -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, @@ -113,7 +113,7 @@ class Subtask extends Base $this->getSubtask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->subtask->validateModification($values); + list($valid, $errors) = $this->subtaskValidator->validateModification($values); if ($valid) { if ($this->subtask->update($values)) { diff --git a/sources/app/Controller/Swimlane.php b/sources/app/Controller/Swimlane.php index 5229621..6641088 100644 --- a/sources/app/Controller/Swimlane.php +++ b/sources/app/Controller/Swimlane.php @@ -60,7 +60,7 @@ class Swimlane extends Base { $project = $this->getProject(); $values = $this->request->getValues(); - list($valid, $errors) = $this->swimlane->validateCreation($values); + list($valid, $errors) = $this->swimlaneValidator->validateCreation($values); if ($valid) { if ($this->swimlane->create($values)) { @@ -84,7 +84,7 @@ class Swimlane extends Base $project = $this->getProject(); $values = $this->request->getValues() + array('show_default_swimlane' => 0); - list($valid, ) = $this->swimlane->validateDefaultModification($values); + list($valid, ) = $this->swimlaneValidator->validateDefaultModification($values); if ($valid) { if ($this->swimlane->updateDefault($values)) { @@ -126,7 +126,7 @@ class Swimlane extends Base $project = $this->getProject(); $values = $this->request->getValues(); - list($valid, $errors) = $this->swimlane->validateModification($values); + list($valid, $errors) = $this->swimlaneValidator->validateModification($values); if ($valid) { if ($this->swimlane->update($values)) { diff --git a/sources/app/Controller/Task.php b/sources/app/Controller/Task.php index e71b201..1811dcb 100644 --- a/sources/app/Controller/Task.php +++ b/sources/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/sources/app/Controller/TaskHelper.php b/sources/app/Controller/TaskHelper.php new file mode 100644 index 0000000..236af33 --- /dev/null +++ b/sources/app/Controller/TaskHelper.php @@ -0,0 +1,57 @@ +request->getJson(); + + if (empty($payload['text'])) { + $this->response->html('

'.t('Nothing to preview...').'

'); + } + + $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/sources/app/Controller/Taskcreation.php b/sources/app/Controller/Taskcreation.php index cffa9d7..4d74fac 100644 --- a/sources/app/Controller/Taskcreation.php +++ b/sources/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/sources/app/Controller/Taskduplication.php b/sources/app/Controller/Taskduplication.php index 9cd684e..ae8bfcb 100644 --- a/sources/app/Controller/Taskduplication.php +++ b/sources/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/sources/app/Controller/Tasklink.php b/sources/app/Controller/Tasklink.php index 068bf16..a81d3ee 100644 --- a/sources/app/Controller/Tasklink.php +++ b/sources/app/Controller/Tasklink.php @@ -69,7 +69,7 @@ class Tasklink extends Base $values = $this->request->getValues(); $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); - list($valid, $errors) = $this->taskLink->validateCreation($values); + list($valid, $errors) = $this->taskLinkValidator->validateCreation($values); if ($valid) { if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) { @@ -125,7 +125,7 @@ class Tasklink extends Base $task = $this->getTask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->taskLink->validateModification($values); + list($valid, $errors) = $this->taskLinkValidator->validateModification($values); if ($valid) { if ($this->taskLink->update($values['id'], $values['task_id'], $values['opposite_task_id'], $values['link_id'])) { diff --git a/sources/app/Controller/Taskmodification.php b/sources/app/Controller/Taskmodification.php index 02b09a3..81cf430 100644 --- a/sources/app/Controller/Taskmodification.php +++ b/sources/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/sources/app/Controller/Twofactor.php b/sources/app/Controller/Twofactor.php index a7368d6..8dbfcf6 100644 --- a/sources/app/Controller/Twofactor.php +++ b/sources/app/Controller/Twofactor.php @@ -2,10 +2,6 @@ namespace Kanboard\Controller; -use Otp\Otp; -use Otp\GoogleAuthenticator; -use Base32\Base32; - /** * Two Factor Auth controller * @@ -27,7 +23,7 @@ class Twofactor extends User } /** - * Index + * Show form to disable/enable 2FA * * @access public */ @@ -35,51 +31,45 @@ class Twofactor extends User { $user = $this->getUser(); $this->checkCurrentUser($user); - - $label = $user['email'] ?: $user['username']; + unset($this->sessionStorage->twoFactorSecret); $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']) : '', + 'provider' => $this->authenticationManager->getPostAuthenticationProvider()->getName(), ))); } /** - * Enable/disable 2FA + * Show page with secret and test form * * @access public */ - public function save() + public function show() { $user = $this->getUser(); $this->checkCurrentUser($user); - $values = $this->request->getValues(); + $label = $user['email'] ?: $user['username']; + $provider = $this->authenticationManager->getPostAuthenticationProvider(); - if (isset($values['twofactor_activated']) && $values['twofactor_activated'] == 1) { - $this->user->update(array( - 'id' => $user['id'], - 'twofactor_activated' => 1, - 'twofactor_secret' => GoogleAuthenticator::generateRandom(), - )); + if (! isset($this->sessionStorage->twoFactorSecret)) { + $provider->generateSecret(); + $provider->beforeCode(); + $this->sessionStorage->twoFactorSecret = $provider->getSecret(); } else { - $this->user->update(array( - 'id' => $user['id'], - 'twofactor_activated' => 0, - 'twofactor_secret' => '', - )); + $provider->setSecret($this->sessionStorage->twoFactorSecret); } - // Allow the user to test or disable the feature - $this->userSession->disable2FA(); - - $this->flash->success(t('User updated successfully.')); - $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); + $this->response->html($this->layout('twofactor/show', array( + 'user' => $user, + 'secret' => $this->sessionStorage->twoFactorSecret, + 'qrcode_url' => $provider->getQrCodeUrl($label), + 'key_url' => $provider->getKeyUrl($label), + ))); } /** - * Test 2FA + * Test code and save secret * * @access public */ @@ -88,15 +78,51 @@ 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($this->sessionStorage->twoFactorSecret); + + if ($provider->authenticate()) { $this->flash->success(t('The two factor authentication code is valid.')); + + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 1, + 'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(), + )); + + unset($this->sessionStorage->twoFactorSecret); + $this->userSession->disablePostAuthentication(); + + $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); } else { $this->flash->failure(t('The two factor authentication code is not valid.')); + $this->response->redirect($this->helper->url->to('twofactor', 'show', array('user_id' => $user['id']))); } + } + /** + * Disable 2FA for the current user + * + * @access public + */ + public function deactivate() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 0, + 'twofactor_secret' => '', + )); + + // Allow the user to test or disable the feature + $this->userSession->disablePostAuthentication(); + + $this->flash->success(t('User updated successfully.')); $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); } @@ -110,11 +136,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 { @@ -130,6 +159,12 @@ class Twofactor extends User */ public function code() { + if (! isset($this->sessionStorage->twoFactorBeforeCodeCalled)) { + $provider = $this->authenticationManager->getPostAuthenticationProvider(); + $provider->beforeCode(); + $this->sessionStorage->twoFactorBeforeCodeCalled = true; + } + $this->response->html($this->template->layout('twofactor/check', array( 'title' => t('Check two factor authentication code'), ))); diff --git a/sources/app/Controller/User.php b/sources/app/Controller/User.php index 23e1982..97e0155 100644 --- a/sources/app/Controller/User.php +++ b/sources/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->getActiveProjectsByUser($this->userSession->getId()); if (isset($params['user'])) { $params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')'; @@ -49,12 +51,34 @@ 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->getActiveProjectsByUser($this->userSession->getId()), 'title' => t('Users').' ('.$paginator->getTotal().')', 'paginator' => $paginator, ))); } + /** + * Public user profile + * + * @access public + */ + public function profile() + { + $user = $this->user->getById($this->request->getIntegerParam('user_id')); + + if (empty($user)) { + $this->notfound(); + } + + $this->response->html( + $this->template->layout('user/profile', array( + 'board_selector' => $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()), + 'title' => $user['name'] ?: $user['username'], + 'user' => $user, + ) + )); + } + /** * Display a form to create a new user * @@ -67,10 +91,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->getActiveProjectsByUser($this->userSession->getId()), 'projects' => $this->project->getList(), 'errors' => $errors, - 'values' => $values, + 'values' => $values + array('role' => Role::APP_USER), 'title' => t('New user') ))); } @@ -83,7 +108,7 @@ class User extends Base public function save() { $values = $this->request->getValues(); - list($valid, $errors) = $this->user->validateCreation($values); + list($valid, $errors) = $this->userValidator->validateCreation($values); if ($valid) { $project_id = empty($values['project_id']) ? 0 : $values['project_id']; @@ -92,7 +117,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)); @@ -147,6 +172,20 @@ class User extends Base ))); } + /** + * Display last password reset + * + * @access public + */ + public function passwordReset() + { + $user = $this->getUser(); + $this->response->html($this->layout('user/password_reset', array( + 'tokens' => $this->passwordReset->getAll($user['id']), + 'user' => $user, + ))); + } + /** * Display last connections * @@ -170,7 +209,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 +223,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 +244,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(), @@ -290,7 +329,7 @@ class User extends Base if ($this->request->isPost()) { $values = $this->request->getValues(); - list($valid, $errors) = $this->user->validatePasswordModification($values); + list($valid, $errors) = $this->userValidator->validatePasswordModification($values); if ($valid) { if ($this->user->update($values)) { @@ -326,20 +365,13 @@ 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']); } } - list($valid, $errors) = $this->user->validateModification($values); + list($valid, $errors) = $this->userValidator->validateModification($values); if ($valid) { if ($this->user->update($values)) { @@ -358,6 +390,7 @@ class User extends Base 'user' => $user, 'timezones' => $this->config->getTimezones(true), 'languages' => $this->config->getLanguages(true), + 'roles' => $this->role->getApplicationRoles(), ))); } @@ -376,7 +409,7 @@ class User extends Base if ($this->request->isPost()) { $values = $this->request->getValues() + array('disable_login_form' => 0, 'is_ldap_user' => 0); - list($valid, $errors) = $this->user->validateModification($values); + list($valid, $errors) = $this->userValidator->validateModification($values); if ($valid) { if ($this->user->update($values)) { diff --git a/sources/app/Controller/UserHelper.php b/sources/app/Controller/UserHelper.php new file mode 100644 index 0000000..041ed2c --- /dev/null +++ b/sources/app/Controller/UserHelper.php @@ -0,0 +1,37 @@ +request->getStringParam('term'); + $users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format(); + $this->response->json($users); + } + + /** + * User mention autocompletion (Ajax) + * + * @access public + */ + public function mention() + { + $project_id = $this->request->getStringParam('project_id'); + $query = $this->request->getStringParam('q'); + $users = $this->projectPermission->findUsernames($project_id, $query); + $this->response->json($users); + } +} diff --git a/sources/app/Controller/Webhook.php b/sources/app/Controller/Webhook.php index a7e9bde..0eafe3e 100644 --- a/sources/app/Controller/Webhook.php +++ b/sources/app/Controller/Webhook.php @@ -39,57 +39,4 @@ class Webhook extends Base $this->response->text('FAILED'); } - - /** - * Handle Github webhooks - * - * @access public - */ - public function github() - { - $this->checkWebhookToken(); - - $this->githubWebhook->setProjectId($this->request->getIntegerParam('project_id')); - - $result = $this->githubWebhook->parsePayload( - $this->request->getHeader('X-Github-Event'), - $this->request->getJson() - ); - - echo $result ? 'PARSED' : 'IGNORED'; - } - - /** - * Handle Gitlab webhooks - * - * @access public - */ - public function gitlab() - { - $this->checkWebhookToken(); - - $this->gitlabWebhook->setProjectId($this->request->getIntegerParam('project_id')); - $result = $this->gitlabWebhook->parsePayload($this->request->getJson()); - - echo $result ? 'PARSED' : 'IGNORED'; - } - - /** - * Handle Bitbucket webhooks - * - * @access public - */ - public function bitbucket() - { - $this->checkWebhookToken(); - - $this->bitbucketWebhook->setProjectId($this->request->getIntegerParam('project_id')); - - $result = $this->bitbucketWebhook->parsePayload( - $this->request->getHeader('X-Event-Key'), - $this->request->getJson() - ); - - echo $result ? 'PARSED' : 'IGNORED'; - } } diff --git a/sources/app/Core/Action/ActionManager.php b/sources/app/Core/Action/ActionManager.php new file mode 100644 index 0000000..f1ea8ab --- /dev/null +++ b/sources/app/Core/Action/ActionManager.php @@ -0,0 +1,142 @@ +actions[$action->getName()] = $action; + return $this; + } + + /** + * Get automatic action instance + * + * @access public + * @param string $name Absolute class name with namespace + * @return ActionBase + */ + public function getAction($name) + { + if (isset($this->actions[$name])) { + return $this->actions[$name]; + } + + throw new RuntimeException('Automatic Action Not Found: '.$name); + } + + /** + * Get available automatic actions + * + * @access public + * @return array + */ + public function getAvailableActions() + { + $actions = array(); + + foreach ($this->actions as $action) { + if (count($action->getEvents()) > 0) { + $actions[$action->getName()] = $action->getDescription(); + } + } + + asort($actions); + + return $actions; + } + + /** + * Get all available action parameters + * + * @access public + * @param array $actions + * @return array + */ + public function getAvailableParameters(array $actions) + { + $params = array(); + + foreach ($actions as $action) { + $currentAction = $this->getAction($action['action_name']); + $params[$currentAction->getName()] = $currentAction->getActionRequiredParameters(); + } + + return $params; + } + + /** + * Get list of compatible events for a given action + * + * @access public + * @param string $name + * @return array + */ + public function getCompatibleEvents($name) + { + $events = array(); + $actionEvents = $this->getAction($name)->getEvents(); + + foreach ($this->eventManager->getAll() as $event => $description) { + if (in_array($event, $actionEvents)) { + $events[$event] = $description; + } + } + + return $events; + } + + /** + * Bind automatic actions to events + * + * @access public + * @return ActionManager + */ + public function attachEvents() + { + if ($this->userSession->isLogged()) { + $actions = $this->action->getAllByUser($this->userSession->getId()); + } else { + $actions = $this->action->getAll(); + } + + foreach ($actions as $action) { + $listener = clone $this->getAction($action['action_name']); + $listener->setProjectId($action['project_id']); + + foreach ($action['params'] as $param_name => $param_value) { + $listener->setParam($param_name, $param_value); + } + + $this->dispatcher->addListener($action['event_name'], array($listener, 'execute')); + } + + return $this; + } +} diff --git a/sources/app/Core/Base.php b/sources/app/Core/Base.php index d317102..2821e5a 100644 --- a/sources/app/Core/Base.php +++ b/sources/app/Core/Base.php @@ -5,40 +5,60 @@ 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\Analytic\TaskDistributionAnalytic $taskDistributionAnalytic + * @property \Kanboard\Analytic\UserDistributionAnalytic $userDistributionAnalytic + * @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic + * @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic + * @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic + * @property \Kanboard\Core\Action\ActionManager $actionManager + * @property \Kanboard\Core\Cache\MemoryCache $memoryCache + * @property \Kanboard\Core\Event\EventManager $eventManager + * @property \Kanboard\Core\Group\GroupManager $groupManager * @property \Kanboard\Core\Http\Client $httpClient + * @property \Kanboard\Core\Http\OAuth2 $oauth + * @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\Http\Route $route + * @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\Integration\BitbucketWebhook $bitbucketWebhook - * @property \Kanboard\Integration\GithubWebhook $githubWebhook - * @property \Kanboard\Integration\GitlabWebhook $gitlabWebhook + * @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\Formatter\ProjectGanttFormatter $projectGanttFormatter * @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter * @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\ActionParameter $actionParameter * @property \Kanboard\Model\Board $board * @property \Kanboard\Model\Category $category * @property \Kanboard\Model\Color $color @@ -46,12 +66,14 @@ 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 * @property \Kanboard\Model\OverdueNotification $overdueNotification + * @property \Kanboard\Model\PasswordReset $passwordReset * @property \Kanboard\Model\Project $project * @property \Kanboard\Model\ProjectActivity $projectActivity * @property \Kanboard\Model\ProjectAnalytic $projectAnalytic @@ -60,8 +82,12 @@ 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\projectUserRoleFilter $projectUserRoleFilter + * @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 @@ -79,21 +105,38 @@ use Pimple\Container; * @property \Kanboard\Model\TaskPermission $taskPermission * @property \Kanboard\Model\TaskPosition $taskPosition * @property \Kanboard\Model\TaskStatus $taskStatus - * @property \Kanboard\Model\TaskValidator $taskValidator * @property \Kanboard\Model\TaskMetadata $taskMetadata * @property \Kanboard\Model\Transition $transition * @property \Kanboard\Model\User $user * @property \Kanboard\Model\UserImport $userImport + * @property \Kanboard\Model\UserLocking $userLocking + * @property \Kanboard\Model\UserMention $userMention * @property \Kanboard\Model\UserNotification $userNotification * @property \Kanboard\Model\UserNotificationType $userNotificationType * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter * @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification - * @property \Kanboard\Model\UserSession $userSession * @property \Kanboard\Model\UserMetadata $userMetadata * @property \Kanboard\Model\Webhook $webhook + * @property \Kanboard\Validator\ActionValidator $actionValidator + * @property \Kanboard\Validator\AuthValidator $authValidator + * @property \Kanboard\Validator\ColumnValidator $columnValidator + * @property \Kanboard\Validator\CategoryValidator $categoryValidator + * @property \Kanboard\Validator\ColumnValidator $columnValidator + * @property \Kanboard\Validator\CommentValidator $commentValidator + * @property \Kanboard\Validator\CurrencyValidator $currencyValidator + * @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator + * @property \Kanboard\Validator\GroupValidator $groupValidator + * @property \Kanboard\Validator\LinkValidator $linkValidator + * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator + * @property \Kanboard\Validator\ProjectValidator $projectValidator + * @property \Kanboard\Validator\SubtaskValidator $subtaskValidator + * @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator + * @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator + * @property \Kanboard\Validator\TaskValidator $taskValidator + * @property \Kanboard\Validator\UserValidator $userValidator * @property \Psr\Log\LoggerInterface $logger - * @property \League\HTMLToMarkdown\HtmlConverter $htmlConverter * @property \PicoDb\Database $db + * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */ abstract class Base { diff --git a/sources/app/Core/Cache/MemoryCache.php b/sources/app/Core/Cache/MemoryCache.php index c4fb7ca..39e3947 100644 --- a/sources/app/Core/Cache/MemoryCache.php +++ b/sources/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/sources/app/Core/Csv.php b/sources/app/Core/Csv.php index 28c1997..e45af24 100644 --- a/sources/app/Core/Csv.php +++ b/sources/app/Core/Csv.php @@ -93,7 +93,7 @@ class Csv { if (! empty($value)) { $value = trim(strtolower($value)); - return $value === '1' || $value{0} === 't' ? 1 : 0; + return $value === '1' || $value{0} === 't' || $value{0} === 'y' ? 1 : 0; } return 0; diff --git a/sources/app/Core/Event/EventManager.php b/sources/app/Core/Event/EventManager.php new file mode 100644 index 0000000..8d76bfc --- /dev/null +++ b/sources/app/Core/Event/EventManager.php @@ -0,0 +1,62 @@ +events[$event] = $description; + return $this; + } + + /** + * Get the list of events and description that can be used from the user interface + * + * @access public + * @return array + */ + public function getAll() + { + $events = array( + TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'), + Task::EVENT_MOVE_COLUMN => t('Move a task to another column'), + Task::EVENT_UPDATE => t('Task modification'), + Task::EVENT_CREATE => t('Task creation'), + Task::EVENT_OPEN => t('Reopen a task'), + Task::EVENT_CLOSE => t('Closing a task'), + Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), + Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'), + ); + + $events = array_merge($events, $this->events); + asort($events); + + return $events; + } +} diff --git a/sources/app/Core/Group/GroupBackendProviderInterface.php b/sources/app/Core/Group/GroupBackendProviderInterface.php new file mode 100644 index 0000000..74c5cb0 --- /dev/null +++ b/sources/app/Core/Group/GroupBackendProviderInterface.php @@ -0,0 +1,21 @@ +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 array_values($result); + } +} diff --git a/sources/app/Core/Group/GroupProviderInterface.php b/sources/app/Core/Group/GroupProviderInterface.php new file mode 100644 index 0000000..4c7c16e --- /dev/null +++ b/sources/app/Core/Group/GroupProviderInterface.php @@ -0,0 +1,40 @@ + $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/sources/app/Core/Http/Request.php b/sources/app/Core/Http/Request.php index 9f89a6e..1b3036d 100644 --- a/sources/app/Core/Http/Request.php +++ b/sources/app/Core/Http/Request.php @@ -2,6 +2,7 @@ namespace Kanboard\Core\Http; +use Pimple\Container; use Kanboard\Core\Base; /** @@ -13,7 +14,45 @@ 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; + } + + /** + * Set GET parameters + * + * @param array $params + */ + public function setParams(array $params) + { + $this->get = array_merge($this->get, $params); + } + + /** + * Get query string string parameter * * @access public * @param string $name Parameter name @@ -22,11 +61,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 +74,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 +98,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 +137,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 +153,30 @@ 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'] : ''; + } + + /** + * Get info of an uploaded file + * + * @access public + * @param string $name Form file name + * @return array + */ + public function getFileInfo($name) + { + return isset($this->files[$name]) ? $this->files[$name] : array(); + } + + /** + * Return HTTP method + * + * @access public + * @return bool + */ + public function getMethod() + { + return $this->getServerVariable('REQUEST_METHOD'); } /** @@ -125,7 +187,7 @@ class Request extends Base */ public function isPost() { - return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST'; + return $this->getServerVariable('REQUEST_METHOD') === 'POST'; } /** @@ -144,13 +206,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($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off'; + 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($this->cookies[$name]) ? $this->cookies[$name] : ''; } /** @@ -163,52 +236,60 @@ class Request extends Base public function getHeader($name) { $name = 'HTTP_'.str_replace('-', '_', strtoupper($name)); - return isset($_SERVER[$name]) ? $_SERVER[$name] : ''; + return $this->getServerVariable($name); } /** - * Returns current request's query string, useful for redirecting + * Get remote user + * + * @access public + * @return string + */ + public function getRemoteUser() + { + return $this->getServerVariable(REVERSE_PROXY_USER_HEADER); + } + + /** + * Returns query string * * @access public * @return string */ public function getQueryString() { - return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; + return $this->getServerVariable('QUERY_STRING'); } /** - * Returns uri + * Return URI * * @access public * @return string */ public function getUri() { - return isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; + return $this->getServerVariable('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 +302,36 @@ 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 ($this->getServerVariable($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 $this->getServerVariable('REQUEST_TIME_FLOAT') ?: 0; + } + + /** + * Get server variable + * + * @access public + * @param string $variable + * @return string + */ + public function getServerVariable($variable) + { + return isset($this->server[$variable]) ? $this->server[$variable] : ''; + } } diff --git a/sources/app/Core/Http/Response.php b/sources/app/Core/Http/Response.php index c5a5d3c..7fefdde 100644 --- a/sources/app/Core/Http/Response.php +++ b/sources/app/Core/Http/Response.php @@ -60,7 +60,7 @@ class Response extends Base public function status($status_code) { header('Status: '.$status_code); - header($_SERVER['SERVER_PROTOCOL'].' '.$status_code); + header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$status_code); } /** @@ -71,7 +71,7 @@ class Response extends Base */ public function redirect($url) { - if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { + if ($this->request->getServerVariable('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest') { header('X-Ajax-Redirect: '.$url); } else { header('Location: '.$url); @@ -220,7 +220,6 @@ class Response extends Base */ public function csp(array $policies = array()) { - $policies['default-src'] = "'self'"; $values = ''; foreach ($policies as $policy => $acl) { @@ -257,7 +256,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/sources/app/Core/Http/Route.php b/sources/app/Core/Http/Route.php new file mode 100644 index 0000000..7836146 --- /dev/null +++ b/sources/app/Core/Http/Route.php @@ -0,0 +1,187 @@ +activated = true; + return $this; + } + + /** + * Add route + * + * @access public + * @param string $path + * @param string $controller + * @param string $action + * @param string $plugin + * @return Route + */ + public function addRoute($path, $controller, $action, $plugin = '') + { + if ($this->activated) { + $path = ltrim($path, '/'); + $items = explode('/', $path); + $params = $this->findParams($items); + + $this->paths[] = array( + 'items' => $items, + 'count' => count($items), + 'controller' => $controller, + 'action' => $action, + 'plugin' => $plugin, + ); + + $this->urls[$plugin][$controller][$action][] = array( + 'path' => $path, + 'params' => $params, + 'count' => count($params), + ); + } + + return $this; + } + + /** + * Find a route according to the given path + * + * @access public + * @param string $path + * @return array + */ + public function findRoute($path) + { + $items = explode('/', ltrim($path, '/')); + $count = count($items); + + foreach ($this->paths as $route) { + if ($count === $route['count']) { + $params = array(); + + for ($i = 0; $i < $count; $i++) { + if ($route['items'][$i]{0} === ':') { + $params[substr($route['items'][$i], 1)] = $items[$i]; + } elseif ($route['items'][$i] !== $items[$i]) { + break; + } + } + + if ($i === $count) { + $this->request->setParams($params); + return array( + 'controller' => $route['controller'], + 'action' => $route['action'], + 'plugin' => $route['plugin'], + ); + } + } + } + + return array( + 'controller' => 'app', + 'action' => 'index', + 'plugin' => '', + ); + } + + /** + * Find route url + * + * @access public + * @param string $controller + * @param string $action + * @param array $params + * @param string $plugin + * @return string + */ + public function findUrl($controller, $action, array $params = array(), $plugin = '') + { + if ($plugin === '' && isset($params['plugin'])) { + $plugin = $params['plugin']; + unset($params['plugin']); + } + + if (! isset($this->urls[$plugin][$controller][$action])) { + return ''; + } + + foreach ($this->urls[$plugin][$controller][$action] as $route) { + if (array_diff_key($params, $route['params']) === array()) { + $url = $route['path']; + $i = 0; + + foreach ($params as $variable => $value) { + $url = str_replace(':'.$variable, $value, $url); + $i++; + } + + if ($i === $route['count']) { + return $url; + } + } + } + + return ''; + } + + /** + * Find url params + * + * @access public + * @param array $items + * @return array + */ + public function findParams(array $items) + { + $params = array(); + + foreach ($items as $item) { + if ($item !== '' && $item{0} === ':') { + $params[substr($item, 1)] = true; + } + } + + return $params; + } +} diff --git a/sources/app/Core/Http/Router.php b/sources/app/Core/Http/Router.php index 0080b23..0fe80ec 100644 --- a/sources/app/Core/Http/Router.php +++ b/sources/app/Core/Http/Router.php @@ -6,13 +6,21 @@ use RuntimeException; use Kanboard\Core\Base; /** - * Router class + * Route Dispatcher * * @package http * @author Frederic Guillot */ class Router extends Base { + /** + * Plugin name + * + * @access private + * @var string + */ + private $plugin = ''; + /** * Controller * @@ -30,30 +38,14 @@ class Router extends Base private $action = ''; /** - * Store routes for path lookup - * - * @access private - * @var array - */ - private $paths = array(); - - /** - * Store routes for url lookup - * - * @access private - * @var array - */ - private $urls = array(); - - /** - * Get action + * Get plugin name * * @access public * @return string */ - public function getAction() + public function getPlugin() { - return $this->action; + return $this->plugin; } /** @@ -67,23 +59,32 @@ class Router extends Base return $this->controller; } + /** + * Get action + * + * @access public + * @return string + */ + public function getAction() + { + return $this->action; + } + /** * Get the path to compare patterns * * @access public - * @param string $uri - * @param string $query_string * @return string */ - public function getPath($uri, $query_string = '') + public function getPath() { - $path = substr($uri, strlen($this->helper->url->dir())); + $path = substr($this->request->getUri(), strlen($this->helper->url->dir())); - if (! empty($query_string)) { - $path = substr($path, 0, - strlen($query_string) - 1); + if ($this->request->getQueryString() !== '') { + $path = substr($path, 0, - strlen($this->request->getQueryString()) - 1); } - if (! empty($path) && $path{0} === '/') { + if ($path !== '' && $path{0} === '/') { $path = substr($path, 1); } @@ -91,140 +92,78 @@ class Router extends Base } /** - * Add route + * Find controller/action from the route table or from get arguments * * @access public - * @param string $path - * @param string $controller - * @param string $action - * @param array $params */ - public function addRoute($path, $controller, $action, array $params = array()) + public function dispatch() { - $pattern = explode('/', $path); + $controller = $this->request->getStringParam('controller'); + $action = $this->request->getStringParam('action'); + $plugin = $this->request->getStringParam('plugin'); - $this->paths[] = array( - 'pattern' => $pattern, - 'count' => count($pattern), - 'controller' => $controller, - 'action' => $action, - ); - - $this->urls[$controller][$action][] = array( - 'path' => $path, - 'params' => array_flip($params), - 'count' => count($params), - ); - } - - /** - * Find a route according to the given path - * - * @access public - * @param string $path - * @return array - */ - public function findRoute($path) - { - $parts = explode('/', $path); - $count = count($parts); - - foreach ($this->paths as $route) { - if ($count === $route['count']) { - $params = array(); - - for ($i = 0; $i < $count; $i++) { - if ($route['pattern'][$i]{0} === ':') { - $params[substr($route['pattern'][$i], 1)] = $parts[$i]; - } elseif ($route['pattern'][$i] !== $parts[$i]) { - break; - } - } - - if ($i === $count) { - $_GET = array_merge($_GET, $params); - return array($route['controller'], $route['action']); - } - } + if ($controller === '') { + $route = $this->route->findRoute($this->getPath()); + $controller = $route['controller']; + $action = $route['action']; + $plugin = $route['plugin']; } - return array('app', 'index'); - } + $this->controller = ucfirst($this->sanitize($controller, 'app')); + $this->action = $this->sanitize($action, 'index'); + $this->plugin = ucfirst($this->sanitize($plugin)); - /** - * Find route url - * - * @access public - * @param string $controller - * @param string $action - * @param array $params - * @return string - */ - public function findUrl($controller, $action, array $params = array()) - { - if (! isset($this->urls[$controller][$action])) { - return ''; - } - - foreach ($this->urls[$controller][$action] as $pattern) { - if (array_diff_key($params, $pattern['params']) === array()) { - $url = $pattern['path']; - $i = 0; - - foreach ($params as $variable => $value) { - $url = str_replace(':'.$variable, $value, $url); - $i++; - } - - if ($i === $pattern['count']) { - return $url; - } - } - } - - return ''; + return $this->executeAction(); } /** * Check controller and action parameter * * @access public - * @param string $value Controller or action name - * @param string $default_value Default value if validation fail + * @param string $value + * @param string $default * @return string */ - public function sanitize($value, $default_value) + public function sanitize($value, $default = '') { - return ! preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $default_value : $value; + return preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $value : $default; } /** - * Find controller/action from the route table or from get arguments + * Execute controller action * - * @access public - * @param string $uri - * @param string $query_string + * @access private */ - public function dispatch($uri, $query_string = '') + private function executeAction() { - if (! empty($_GET['controller']) && ! empty($_GET['action'])) { - $this->controller = $this->sanitize($_GET['controller'], 'app'); - $this->action = $this->sanitize($_GET['action'], 'index'); - $plugin = ! empty($_GET['plugin']) ? $this->sanitize($_GET['plugin'], '') : ''; - } else { - list($this->controller, $this->action) = $this->findRoute($this->getPath($uri, $query_string)); // TODO: add plugin for routes - $plugin = ''; + $class = $this->getControllerClassName(); + + if (! class_exists($class)) { + throw new RuntimeException('Controller not found'); } - $class = '\Kanboard\\'; - $class .= empty($plugin) ? 'Controller\\'.ucfirst($this->controller) : 'Plugin\\'.ucfirst($plugin).'\Controller\\'.ucfirst($this->controller); - - if (! class_exists($class) || ! method_exists($class, $this->action)) { - throw new RuntimeException('Controller or method not found for the given url!'); + if (! method_exists($class, $this->action)) { + throw new RuntimeException('Action not implemented'); } $instance = new $class($this->container); - $instance->beforeAction($this->controller, $this->action); + $instance->beforeAction(); $instance->{$this->action}(); + return $instance; + } + + /** + * Get controller class name + * + * @access private + * @return string + */ + private function getControllerClassName() + { + if ($this->plugin !== '') { + return '\Kanboard\Plugin\\'.$this->plugin.'\Controller\\'.$this->controller; + } + + return '\Kanboard\Controller\\'.$this->controller; } } diff --git a/sources/app/Core/Ldap/Client.php b/sources/app/Core/Ldap/Client.php new file mode 100644 index 0000000..63149ae --- /dev/null +++ b/sources/app/Core/Ldap/Client.php @@ -0,0 +1,165 @@ +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 Client + */ + 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'); + } + + if (! $verify) { + putenv('LDAPTLS_REQCERT=never'); + } + + $this->ldap = ldap_connect($server, $port); + + if ($this->ldap === false) { + throw new ClientException('LDAP: Unable to connect to the LDAP server'); + } + + 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($this->ldap)) { + throw new ClientException('LDAP: Unable to start TLS'); + } + + return $this; + } + + /** + * Anonymous authentication + * + * @access public + * @return boolean + */ + public function useAnonymousAuthentication() + { + if (! @ldap_bind($this->ldap)) { + throw new ClientException('Unable to perform anonymous binding'); + } + + return true; + } + + /** + * Authentication with username/password + * + * @access public + * @param string $bind_rdn + * @param string $bind_password + * @return boolean + */ + public function authenticate($bind_rdn, $bind_password) + { + 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/sources/app/Core/Ldap/ClientException.php b/sources/app/Core/Ldap/ClientException.php new file mode 100644 index 0000000..a0f9f84 --- /dev/null +++ b/sources/app/Core/Ldap/ClientException.php @@ -0,0 +1,15 @@ +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/sources/app/Core/Ldap/Entry.php b/sources/app/Core/Ldap/Entry.php new file mode 100644 index 0000000..0b99a58 --- /dev/null +++ b/sources/app/Core/Ldap/Entry.php @@ -0,0 +1,91 @@ +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/sources/app/Core/Ldap/Group.php b/sources/app/Core/Ldap/Group.php new file mode 100644 index 0000000..634d47e --- /dev/null +++ b/sources/app/Core/Ldap/Group.php @@ -0,0 +1,131 @@ +query = $query; + } + + /** + * Get groups + * + * @static + * @access public + * @param Client $client + * @param string $query + * @return array + */ + public static function getGroups(Client $client, $query) + { + $className = get_called_class(); + $self = new $className(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/sources/app/Core/Ldap/Query.php b/sources/app/Core/Ldap/Query.php new file mode 100644 index 0000000..e03495e --- /dev/null +++ b/sources/app/Core/Ldap/Query.php @@ -0,0 +1,87 @@ +client = $client; + } + + /** + * Execute query + * + * @access public + * @param string $baseDn + * @param string $filter + * @param array $attributes + * @return Query + */ + public function execute($baseDn, $filter, array $attributes) + { + $sr = ldap_search($this->client->getConnection(), $baseDn, $filter, $attributes); + if ($sr === false) { + return $this; + } + + $entries = ldap_get_entries($this->client->getConnection(), $sr); + if ($entries === false || count($entries) === 0 || $entries['count'] == 0) { + return $this; + } + + $this->entries = $entries; + + return $this; + } + + /** + * Return true if the query returned a result + * + * @access public + * @return boolean + */ + public function hasResult() + { + return ! empty($this->entries); + } + + /** + * Get LDAP Entries + * + * @access public + * @return Entities + */ + public function getEntries() + { + return new Entries($this->entries); + } +} diff --git a/sources/app/Core/Ldap/User.php b/sources/app/Core/Ldap/User.php new file mode 100644 index 0000000..d36d6f3 --- /dev/null +++ b/sources/app/Core/Ldap/User.php @@ -0,0 +1,224 @@ +query = $query; + } + + /** + * Get user profile + * + * @static + * @access public + * @param Client $client + * @param string $username + * @return LdapUserProvider + */ + public static function getUser(Client $client, $username) + { + $className = get_called_class(); + $self = new $className(new Query($client)); + return $self->find($self->getLdapUserPattern($username)); + } + + /** + * Find user + * + * @access public + * @param string $query + * @return null|LdapUserProvider + */ + public function find($query) + { + $this->query->execute($this->getBasDn(), $query, $this->getAttributes()); + $user = null; + + if ($this->query->hasResult()) { + $user = $this->build(); + } + + return $user; + } + + /** + * Build user profile + * + * @access protected + * @return LdapUserProvider + */ + protected function build() + { + $entry = $this->query->getEntries()->getFirstEntry(); + $role = Role::APP_USER; + + if ($entry->hasValue($this->getAttributeGroup(), $this->getGroupAdminDn())) { + $role = Role::APP_ADMIN; + } elseif ($entry->hasValue($this->getAttributeGroup(), $this->getGroupManagerDn())) { + $role = Role::APP_MANAGER; + } + + return new LdapUserProvider( + $entry->getDn(), + $entry->getFirstValue($this->getAttributeUsername()), + $entry->getFirstValue($this->getAttributeName()), + $entry->getFirstValue($this->getAttributeEmail()), + $role, + $entry->getAll($this->getAttributeGroup()) + ); + } + + /** + * 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 getAttributes() + { + return array_values(array_filter(array( + $this->getAttributeUsername(), + $this->getAttributeName(), + $this->getAttributeEmail(), + $this->getAttributeGroup(), + ))); + } + + /** + * Get LDAP account id attribute + * + * @access public + * @return string + */ + public function getAttributeUsername() + { + 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 user name attribute + * + * @access public + * @return string + */ + public function getAttributeName() + { + 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 email attribute + * + * @access public + * @return string + */ + public function getAttributeEmail() + { + if (! LDAP_USER_ATTRIBUTE_EMAIL) { + throw new LogicException('LDAP email attribute empty, check the parameter LDAP_USER_ATTRIBUTE_EMAIL'); + } + + return LDAP_USER_ATTRIBUTE_EMAIL; + } + + /** + * Get LDAP account memberof attribute + * + * @access public + * @return string + */ + public function getAttributeGroup() + { + return LDAP_USER_ATTRIBUTE_GROUPS; + } + + /** + * Get LDAP admin group DN + * + * @access public + * @return string + */ + public function getGroupAdminDn() + { + return LDAP_GROUP_ADMIN_DN; + } + + /** + * Get LDAP application manager group DN + * + * @access public + * @return string + */ + public function getGroupManagerDn() + { + 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; + } + + /** + * Get LDAP user pattern + * + * @access public + * @param string $username + * @return string + */ + public function getLdapUserPattern($username) + { + if (! LDAP_USER_FILTER) { + throw new LogicException('LDAP user filter empty, check the parameter LDAP_USER_FILTER'); + } + + return sprintf(LDAP_USER_FILTER, $username); + } +} diff --git a/sources/app/Core/Lexer.php b/sources/app/Core/Lexer.php index ca2ef89..df2d90a 100644 --- a/sources/app/Core/Lexer.php +++ b/sources/app/Core/Lexer.php @@ -39,6 +39,7 @@ class Lexer "/^(swimlane:)/" => 'T_SWIMLANE', "/^(ref:)/" => 'T_REFERENCE', "/^(reference:)/" => 'T_REFERENCE', + "/^(link:)/" => 'T_LINK', "/^(\s+)/" => 'T_WHITESPACE', '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE', '/^(yesterday|tomorrow|today)/' => 'T_DATE', @@ -118,6 +119,7 @@ class Lexer case 'T_COLUMN': case 'T_PROJECT': case 'T_SWIMLANE': + case 'T_LINK': $next = next($tokens); if ($next !== false && $next['token'] === 'T_STRING') { diff --git a/sources/app/Core/Mail/Client.php b/sources/app/Core/Mail/Client.php index 7b4268b..e1f3169 100644 --- a/sources/app/Core/Mail/Client.php +++ b/sources/app/Core/Mail/Client.php @@ -45,19 +45,21 @@ class Client extends Base */ public function send($email, $name, $subject, $html) { - $this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')'); + if (! empty($email)) { + $this->logger->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')'); - $start_time = microtime(true); - $author = 'Kanboard'; + $start_time = microtime(true); + $author = 'Kanboard'; - if ($this->userSession->isLogged()) { - $author = e('%s via Kanboard', $this->helper->user->getFullname()); - } + if ($this->userSession->isLogged()) { + $author = e('%s via Kanboard', $this->helper->user->getFullname()); + } - $this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author); + $this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author); - if (DEBUG) { - $this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds'); + if (DEBUG) { + $this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds'); + } } return $this; diff --git a/sources/app/Core/Mail/Transport/Mail.php b/sources/app/Core/Mail/Transport/Mail.php index 4d833f8..aff3ee2 100644 --- a/sources/app/Core/Mail/Transport/Mail.php +++ b/sources/app/Core/Mail/Transport/Mail.php @@ -46,7 +46,7 @@ class Mail extends Base implements ClientInterface * Get SwiftMailer transport * * @access protected - * @return \Swift_Transport + * @return \Swift_Transport|\Swift_MailTransport|\Swift_SmtpTransport|\Swift_SendmailTransport */ protected function getTransport() { diff --git a/sources/app/Core/Mail/Transport/Sendmail.php b/sources/app/Core/Mail/Transport/Sendmail.php index 849e338..039be70 100644 --- a/sources/app/Core/Mail/Transport/Sendmail.php +++ b/sources/app/Core/Mail/Transport/Sendmail.php @@ -16,7 +16,7 @@ class Sendmail extends Mail * Get SwiftMailer transport * * @access protected - * @return \Swift_Transport + * @return \Swift_Transport|\Swift_MailTransport|\Swift_SmtpTransport|\Swift_SendmailTransport */ protected function getTransport() { diff --git a/sources/app/Core/Mail/Transport/Smtp.php b/sources/app/Core/Mail/Transport/Smtp.php index 757408e..66f0a3a 100644 --- a/sources/app/Core/Mail/Transport/Smtp.php +++ b/sources/app/Core/Mail/Transport/Smtp.php @@ -16,7 +16,7 @@ class Smtp extends Mail * Get SwiftMailer transport * * @access protected - * @return \Swift_Transport + * @return \Swift_Transport|\Swift_MailTransport|\Swift_SmtpTransport|\Swift_SendmailTransport */ protected function getTransport() { diff --git a/sources/app/Core/Markdown.php b/sources/app/Core/Markdown.php index f08c486..827fd0d 100644 --- a/sources/app/Core/Markdown.php +++ b/sources/app/Core/Markdown.php @@ -3,7 +3,7 @@ namespace Kanboard\Core; use Parsedown; -use Kanboard\Helper\Url; +use Pimple\Container; /** * Specific Markdown rules for Kanboard @@ -14,22 +14,51 @@ use Kanboard\Helper\Url; */ class Markdown extends Parsedown { - private $link; - private $helper; + /** + * Link params for tasks + * + * @access private + * @var array + */ + private $link = array(); - public function __construct($link, Url $helper) + /** + * Container + * + * @access private + * @var Container + */ + private $container; + + /** + * Constructor + * + * @access public + * @param Container $container + * @param array $link + */ + public function __construct(Container $container, array $link) { $this->link = $link; - $this->helper = $helper; + $this->container = $container; $this->InlineTypes['#'][] = 'TaskLink'; - $this->inlineMarkerList .= '#'; + $this->InlineTypes['@'][] = 'UserLink'; + $this->inlineMarkerList .= '#@'; } - protected function inlineTaskLink($Excerpt) + /** + * Handle Task Links + * + * Replace "#123" by a link to the task + * + * @access public + * @param array $Excerpt + * @return array + */ + protected function inlineTaskLink(array $Excerpt) { - // Replace task #123 by a link to the task if (! empty($this->link) && preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) { - $url = $this->helper->href( + $url = $this->container['helper']->url->href( $this->link['controller'], $this->link['action'], $this->link['params'] + array('task_id' => $matches[1]) @@ -40,7 +69,38 @@ class Markdown extends Parsedown 'element' => array( 'name' => 'a', 'text' => $matches[0], - 'attributes' => array('href' => $url))); + 'attributes' => array('href' => $url) + ), + ); + } + } + + /** + * Handle User Mentions + * + * Replace "@username" by a link to the user + * + * @access public + * @param array $Excerpt + * @return array + */ + protected function inlineUserLink(array $Excerpt) + { + if (preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) { + $user_id = $this->container['user']->getIdByUsername($matches[1]); + + if (! empty($user_id)) { + $url = $this->container['helper']->url->href('user', 'profile', array('user_id' => $user_id)); + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[0], + 'attributes' => array('href' => $url, 'class' => 'user-mention-link'), + ), + ); + } } } } diff --git a/sources/app/Core/Security/AccessMap.php b/sources/app/Core/Security/AccessMap.php new file mode 100644 index 0000000..f34c4b0 --- /dev/null +++ b/sources/app/Core/Security/AccessMap.php @@ -0,0 +1,175 @@ +defaultRole = $role; + return $this; + } + + /** + * 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; + } + + /** + * Get the highest role from a list + * + * @access public + * @param array $roles + * @return string + */ + public function getHighestRole(array $roles) + { + $rank = array(); + + foreach ($roles as $role) { + $rank[$role] = count($this->getRoleHierarchy($role)); + } + + asort($rank); + + return key($rank); + } + + /** + * Add new access rules + * + * @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 string $role + * @return Acl + */ + private function addRule($controller, $method, $role) + { + $controller = strtolower($controller); + $method = strtolower($method); + + if (! isset($this->map[$controller])) { + $this->map[$controller] = array(); + } + + $this->map[$controller][$method] = $role; + + return $this; + } + + /** + * Get roles that match the given controller/method + * + * @access public + * @param string $controller + * @param string $method + * @return boolean + */ + public function getRoles($controller, $method) + { + $controller = strtolower($controller); + $method = strtolower($method); + + foreach (array($method, '*') as $key) { + if (isset($this->map[$controller][$key])) { + return $this->getRoleHierarchy($this->map[$controller][$key]); + } + } + + return $this->getRoleHierarchy($this->defaultRole); + } +} diff --git a/sources/app/Core/Security/AuthenticationManager.php b/sources/app/Core/Security/AuthenticationManager.php new file mode 100644 index 0000000..b1ba76c --- /dev/null +++ b/sources/app/Core/Security/AuthenticationManager.php @@ -0,0 +1,187 @@ +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()) { + $this->logger->debug('Invalidate session for '.$this->userSession->getUsername()); + $this->sessionStorage->flush(); + $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/sources/app/Core/Security/AuthenticationProviderInterface.php b/sources/app/Core/Security/AuthenticationProviderInterface.php new file mode 100644 index 0000000..828e272 --- /dev/null +++ b/sources/app/Core/Security/AuthenticationProviderInterface.php @@ -0,0 +1,28 @@ +accessMap = $accessMap; + } + + /** + * Check if the given role is allowed to access to the specified resource + * + * @access public + * @param string $controller + * @param string $method + * @param string $role + * @return boolean + */ + public function isAllowed($controller, $method, $role) + { + $roles = $this->accessMap->getRoles($controller, $method); + return in_array($role, $roles); + } +} diff --git a/sources/app/Core/Security/OAuthAuthenticationProviderInterface.php b/sources/app/Core/Security/OAuthAuthenticationProviderInterface.php new file mode 100644 index 0000000..c32339e --- /dev/null +++ b/sources/app/Core/Security/OAuthAuthenticationProviderInterface.php @@ -0,0 +1,46 @@ + 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 role name + * + * @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/sources/app/Core/Security/SessionCheckProviderInterface.php b/sources/app/Core/Security/SessionCheckProviderInterface.php new file mode 100644 index 0000000..232fe1d --- /dev/null +++ b/sources/app/Core/Security/SessionCheckProviderInterface.php @@ -0,0 +1,20 @@ +container['sessionStorage']->setStorage($_SESSION); + $this->sessionStorage->setStorage($_SESSION); } /** @@ -51,6 +57,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 +88,7 @@ class SessionManager extends Base SESSION_DURATION, $this->helper->url->dir() ?: '/', null, - Request::isHTTPS(), + $this->request->isHTTPS(), true ); @@ -92,7 +100,7 @@ class SessionManager extends Base ini_set('session.use_strict_mode', '1'); // Better session hash - ini_set('session.hash_function', 'sha512'); + ini_set('session.hash_function', '1'); // 'sha512' is not compatible with FreeBSD, only MD5 '0' and SHA-1 '1' seems to work ini_set('session.hash_bits_per_character', 6); // Set an additional entropy diff --git a/sources/app/Core/Session/SessionStorage.php b/sources/app/Core/Session/SessionStorage.php index 703d2fb..667d925 100644 --- a/sources/app/Core/Session/SessionStorage.php +++ b/sources/app/Core/Session/SessionStorage.php @@ -8,17 +8,19 @@ namespace Kanboard\Core\Session; * @package session * @author Frederic Guillot * - * @property array $config * @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 + * @property bool $twoFactorBeforeCodeCalled + * @property string $twoFactorSecret */ class SessionStorage { @@ -60,6 +62,21 @@ class SessionStorage return $session; } + /** + * Flush session data + * + * @access public + */ + public function flush() + { + $session = get_object_vars($this); + unset($session['storage']); + + foreach (array_keys($session) as $property) { + unset($this->$property); + } + } + /** * Copy class properties to external storage * diff --git a/sources/app/Core/Template.php b/sources/app/Core/Template.php index ce2884a..8ded6f7 100644 --- a/sources/app/Core/Template.php +++ b/sources/app/Core/Template.php @@ -18,6 +18,50 @@ class Template extends Helper */ private $overrides = array(); + /** + * Rendering start time + * + * @access private + * @var float + */ + private $startTime = 0; + + /** + * Total rendering time + * + * @access private + * @var float + */ + private $renderingTime = 0; + + /** + * Method executed before the rendering + * + * @access protected + * @param string $template + */ + protected function beforeRender($template) + { + if (DEBUG) { + $this->startTime = microtime(true); + } + } + + /** + * Method executed after the rendering + * + * @access protected + * @param string $template + */ + protected function afterRender($template) + { + if (DEBUG) { + $duration = microtime(true) - $this->startTime; + $this->renderingTime += $duration; + $this->container['logger']->debug('Rendering '.$template.' in '.$duration.'s, total='.$this->renderingTime); + } + } + /** * Render a template * @@ -32,11 +76,16 @@ class Template extends Helper */ public function render($__template_name, array $__template_args = array()) { - extract($__template_args); + $this->beforeRender($__template_name); + extract($__template_args); ob_start(); include $this->getTemplateFile($__template_name); - return ob_get_clean(); + $html = ob_get_clean(); + + $this->afterRender($__template_name); + + return $html; } /** diff --git a/sources/app/Core/User/GroupSync.php b/sources/app/Core/User/GroupSync.php new file mode 100644 index 0000000..573acd4 --- /dev/null +++ b/sources/app/Core/User/GroupSync.php @@ -0,0 +1,32 @@ +group->getByExternalId($groupId); + + if (! empty($group) && ! $this->groupMember->isMember($group['id'], $userId)) { + $this->groupMember->addUser($group['id'], $userId); + } + } + } +} diff --git a/sources/app/Core/User/UserProfile.php b/sources/app/Core/User/UserProfile.php new file mode 100644 index 0000000..ccbc7f0 --- /dev/null +++ b/sources/app/Core/User/UserProfile.php @@ -0,0 +1,62 @@ +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/sources/app/Core/User/UserProperty.php b/sources/app/Core/User/UserProperty.php new file mode 100644 index 0000000..f8b08a3 --- /dev/null +++ b/sources/app/Core/User/UserProperty.php @@ -0,0 +1,70 @@ + $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/sources/app/Core/User/UserProviderInterface.php b/sources/app/Core/User/UserProviderInterface.php new file mode 100644 index 0000000..07e01f4 --- /dev/null +++ b/sources/app/Core/User/UserProviderInterface.php @@ -0,0 +1,103 @@ +sessionStorage->user = $user; - $this->sessionStorage->postAuth = array('validated' => false); + $this->sessionStorage->postAuthenticationValidated = false; + } + + /** + * Get user application role + * + * @access public + * @return string + */ + public function getRole() + { + return $this->sessionStorage->user['role']; } /** @@ -42,9 +52,19 @@ class UserSession extends Base * @access public * @return bool */ - public function check2FA() + public function isPostAuthenticationValidated() { - return isset($this->sessionStorage->postAuth['validated']) && $this->sessionStorage->postAuth['validated'] === true; + return isset($this->sessionStorage->postAuthenticationValidated) && $this->sessionStorage->postAuthenticationValidated === true; + } + + /** + * Validate 2FA for the current session + * + * @access public + */ + public function validatePostAuthentication() + { + $this->sessionStorage->postAuthenticationValidated = true; } /** @@ -53,7 +73,7 @@ class UserSession extends Base * @access public * @return bool */ - public function has2FA() + public function hasPostAuthentication() { return isset($this->sessionStorage->user['twofactor_activated']) && $this->sessionStorage->user['twofactor_activated'] === true; } @@ -63,7 +83,7 @@ class UserSession extends Base * * @access public */ - public function disable2FA() + public function disablePostAuthentication() { $this->sessionStorage->user['twofactor_activated'] = false; } @@ -76,18 +96,7 @@ class UserSession extends Base */ public function isAdmin() { - return isset($this->sessionStorage->user['is_admin']) && $this->sessionStorage->user['is_admin'] === true; - } - - /** - * Return true if the logged user is project admin - * - * @access public - * @return bool - */ - public function isProjectAdmin() - { - 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; } /** @@ -105,7 +114,7 @@ class UserSession extends Base * Get username * * @access public - * @return integer + * @return string */ public function getUsername() { diff --git a/sources/app/Core/User/UserSync.php b/sources/app/Core/User/UserSync.php new file mode 100644 index 0000000..d450a0b --- /dev/null +++ b/sources/app/Core/User/UserSync.php @@ -0,0 +1,76 @@ +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/sources/app/Event/AuthEvent.php b/sources/app/Event/AuthEvent.php deleted file mode 100644 index 7cbced8..0000000 --- a/sources/app/Event/AuthEvent.php +++ /dev/null @@ -1,27 +0,0 @@ -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/sources/app/Event/AuthFailureEvent.php b/sources/app/Event/AuthFailureEvent.php new file mode 100644 index 0000000..225ac04 --- /dev/null +++ b/sources/app/Event/AuthFailureEvent.php @@ -0,0 +1,44 @@ +username = $username; + } + + /** + * Get username + * + * @access public + * @return string + */ + public function getUsername() + { + return $this->username; + } +} diff --git a/sources/app/Event/AuthSuccessEvent.php b/sources/app/Event/AuthSuccessEvent.php new file mode 100644 index 0000000..38323e8 --- /dev/null +++ b/sources/app/Event/AuthSuccessEvent.php @@ -0,0 +1,43 @@ +authType = $authType; + } + + /** + * Get authentication type + * + * @return string + */ + public function getAuthType() + { + return $this->authType; + } +} diff --git a/sources/app/Formatter/GroupAutoCompleteFormatter.php b/sources/app/Formatter/GroupAutoCompleteFormatter.php new file mode 100644 index 0000000..7023e36 --- /dev/null +++ b/sources/app/Formatter/GroupAutoCompleteFormatter.php @@ -0,0 +1,55 @@ +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/sources/app/Formatter/ProjectGanttFormatter.php b/sources/app/Formatter/ProjectGanttFormatter.php index 1749608..4f73e21 100644 --- a/sources/app/Formatter/ProjectGanttFormatter.php +++ b/sources/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/sources/app/Formatter/UserFilterAutoCompleteFormatter.php b/sources/app/Formatter/UserFilterAutoCompleteFormatter.php new file mode 100644 index 0000000..b98e0d6 --- /dev/null +++ b/sources/app/Formatter/UserFilterAutoCompleteFormatter.php @@ -0,0 +1,38 @@ +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/sources/app/Group/DatabaseBackendGroupProvider.php b/sources/app/Group/DatabaseBackendGroupProvider.php new file mode 100644 index 0000000..6dbaa43 --- /dev/null +++ b/sources/app/Group/DatabaseBackendGroupProvider.php @@ -0,0 +1,34 @@ +group->search($input); + + foreach ($groups as $group) { + $result[] = new DatabaseGroupProvider($group); + } + + return $result; + } +} diff --git a/sources/app/Group/DatabaseGroupProvider.php b/sources/app/Group/DatabaseGroupProvider.php new file mode 100644 index 0000000..430121a --- /dev/null +++ b/sources/app/Group/DatabaseGroupProvider.php @@ -0,0 +1,66 @@ +group = $group; + } + + /** + * Get internal id + * + * @access public + * @return integer + */ + public function getInternalId() + { + return (int) $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/sources/app/Group/LdapBackendGroupProvider.php b/sources/app/Group/LdapBackendGroupProvider.php new file mode 100644 index 0000000..cad732c --- /dev/null +++ b/sources/app/Group/LdapBackendGroupProvider.php @@ -0,0 +1,54 @@ +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 (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/sources/app/Group/LdapGroupProvider.php b/sources/app/Group/LdapGroupProvider.php new file mode 100644 index 0000000..b497d48 --- /dev/null +++ b/sources/app/Group/LdapGroupProvider.php @@ -0,0 +1,76 @@ +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/sources/app/Helper/App.php b/sources/app/Helper/App.php index 33729f2..0593795 100644 --- a/sources/app/Helper/App.php +++ b/sources/app/Helper/App.php @@ -2,14 +2,63 @@ namespace Kanboard\Helper; +use Kanboard\Core\Base; + /** * Application helpers * * @package helper * @author Frederic Guillot */ -class App extends \Kanboard\Core\Base +class App extends Base { + /** + * Get config variable + * + * @access public + * @param string $param + * @return mixed + */ + public function config($param) + { + return $this->config->get($param); + } + + /** + * Make sidebar menu active + * + * @access public + * @param string $controller + * @param string $action + * @param string $plugin + * @return string + */ + public function checkMenuSelection($controller, $action = '', $plugin = '') + { + $result = strtolower($this->getRouterController()) === strtolower($controller); + + if ($result && $action !== '') { + $result = strtolower($this->getRouterAction()) === strtolower($action); + } + + if ($result && $plugin !== '') { + $result = strtolower($this->getPluginName()) === strtolower($plugin); + } + + return $result ? 'class="active"' : ''; + } + + /** + * Get plugin name from route + * + * @access public + * @return string + */ + public function getPluginName() + { + return $this->router->getPlugin(); + } + /** * Get router controller * diff --git a/sources/app/Helper/Subtask.php b/sources/app/Helper/Subtask.php index 4bb26e7..90bd733 100644 --- a/sources/app/Helper/Subtask.php +++ b/sources/app/Helper/Subtask.php @@ -14,13 +14,18 @@ class Subtask extends \Kanboard\Core\Base * Get the link to toggle subtask status * * @access public - * @param array $subtask - * @param string $redirect + * @param array $subtask + * @param string $redirect + * @param integer $project_id * @return string */ - public function toggleStatus(array $subtask, $redirect) + public function toggleStatus(array $subtask, $redirect, $project_id = 0) { - if ($subtask['status'] == 0 && isset($this->sessionStorage->hasSubtaskInProgress) && $this->sessionStorage->hasSubtaskInProgress === true) { + if ($project_id > 0 && ! $this->helper->user->hasProjectAccess('subtask', 'edit', $project_id)) { + return trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']); + } + + if ($subtask['status'] == 0 && isset($this->sessionStorage->hasSubtaskInProgress) && $this->sessionStorage->hasSubtaskInProgress) { return $this->helper->url->link( trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']), 'subtask', diff --git a/sources/app/Helper/Text.php b/sources/app/Helper/Text.php index d2075fe..59bfd99 100644 --- a/sources/app/Helper/Text.php +++ b/sources/app/Helper/Text.php @@ -3,14 +3,15 @@ namespace Kanboard\Helper; use Kanboard\Core\Markdown; +use Kanboard\Core\Base; /** - * Text helpers + * Text Helpers * * @package helper * @author Frederic Guillot */ -class Text extends \Kanboard\Core\Base +class Text extends Base { /** * Markdown transformation @@ -21,7 +22,7 @@ class Text extends \Kanboard\Core\Base */ public function markdown($text, array $link = array()) { - $parser = new Markdown($link, $this->helper->url); + $parser = new Markdown($this->container, $link); $parser->setMarkupEscaped(MARKDOWN_ESCAPE_HTML); return $parser->text($text); } diff --git a/sources/app/Helper/Url.php b/sources/app/Helper/Url.php index edb2684..720297c 100644 --- a/sources/app/Helper/Url.php +++ b/sources/app/Helper/Url.php @@ -2,7 +2,6 @@ namespace Kanboard\Helper; -use Kanboard\Core\Http\Request; use Kanboard\Core\Base; /** @@ -104,8 +103,8 @@ class Url extends Base */ public function dir() { - if (empty($this->directory) && isset($_SERVER['REQUEST_METHOD'])) { - $this->directory = str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])); + if ($this->directory === '' && $this->request->getMethod() !== '') { + $this->directory = str_replace('\\', '/', dirname($this->request->getServerVariable('PHP_SELF'))); $this->directory = $this->directory !== '/' ? $this->directory.'/' : '/'; $this->directory = str_replace('//', '/', $this->directory); } @@ -121,13 +120,13 @@ class Url extends Base */ public function server() { - if (empty($_SERVER['SERVER_NAME'])) { + if ($this->request->getServerVariable('SERVER_NAME') === '') { return 'http://localhost/'; } - $url = Request::isHTTPS() ? 'https://' : 'http://'; - $url .= $_SERVER['SERVER_NAME']; - $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; + $url = $this->request->isHTTPS() ? 'https://' : 'http://'; + $url .= $this->request->getServerVariable('SERVER_NAME'); + $url .= $this->request->getServerVariable('SERVER_PORT') == 80 || $this->request->getServerVariable('SERVER_PORT') == 443 ? '' : ':'.$this->request->getServerVariable('SERVER_PORT'); $url .= $this->dir() ?: '/'; return $url; @@ -148,13 +147,15 @@ class Url extends Base */ private function build($separator, $controller, $action, array $params = array(), $csrf = false, $anchor = '', $absolute = false) { - $path = $this->router->findUrl($controller, $action, $params); + $path = $this->route->findUrl($controller, $action, $params); $qs = array(); if (empty($path)) { $qs['controller'] = $controller; $qs['action'] = $action; $qs += $params; + } else { + unset($params['plugin']); } if ($csrf) { diff --git a/sources/app/Helper/User.php b/sources/app/Helper/User.php index 9ef20b3..29844df 100644 --- a/sources/app/Helper/User.php +++ b/sources/app/Helper/User.php @@ -50,21 +50,6 @@ class User extends \Kanboard\Core\Base return $this->userSession->getId(); } - /** - * Get user profile - * - * @access public - * @return string - */ - public function getProfileLink() - { - return $this->helper->url->link( - $this->helper->e($this->getFullname()), - 'user', - 'show', - array('user_id' => $this->userSession->getId()) - ); - } /** * Check if the given user_id is the connected user * @@ -88,44 +73,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) + { + $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 $result; + } + + /** + * Check project access + * + * @param string $controller + * @param string $action + * @param integer $project_id + * @return bool + */ + public function hasProjectAccess($controller, $action, $project_id) { if ($this->userSession->isAdmin()) { return true; } - return $this->memoryCache->proxy($this->container['acl'], 'handleProjectAdminPermissions', $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; } /** - * Check for project management actions access (Regular users who are Project Managers) + * Get project role for the current user * * @access public - * @return boolean + * @param integer $project_id + * @return string */ - public function isProjectManagementAllowed($project_id) + public function getProjectUserRole($project_id) { - if ($this->userSession->isAdmin()) { - return true; - } - - return $this->memoryCache->proxy($this->container['acl'], 'handleProjectManagerPermissions', $project_id); + return $this->memoryCache->proxy($this->projectUserRole, 'getUserRole', $project_id, $this->userSession->getId()); } /** diff --git a/sources/app/Integration/BitbucketWebhook.php b/sources/app/Integration/BitbucketWebhook.php deleted file mode 100644 index 97a3943..0000000 --- a/sources/app/Integration/BitbucketWebhook.php +++ /dev/null @@ -1,312 +0,0 @@ -project_id = $project_id; - } - - /** - * Parse incoming events - * - * @access public - * @param string $type Bitbucket event type - * @param array $payload Bitbucket event - * @return boolean - */ - public function parsePayload($type, array $payload) - { - switch ($type) { - case 'issue:comment_created': - return $this->handleCommentCreated($payload); - case 'issue:created': - return $this->handleIssueOpened($payload); - case 'issue:updated': - return $this->handleIssueUpdated($payload); - case 'repo:push': - return $this->handlePush($payload); - } - - return false; - } - - /** - * Parse comment issue events - * - * @access public - * @param array $payload - * @return boolean - */ - public function handleCommentCreated(array $payload) - { - $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['id']); - - if (! empty($task)) { - $user = $this->user->getByUsername($payload['actor']['username']); - - if (! empty($user) && ! $this->projectPermission->isMember($this->project_id, $user['id'])) { - $user = array(); - } - - $event = array( - 'project_id' => $this->project_id, - 'reference' => $payload['comment']['id'], - 'comment' => $payload['comment']['content']['raw']."\n\n[".t('By @%s on Bitbucket', $payload['actor']['display_name']).']('.$payload['comment']['links']['html']['href'].')', - 'user_id' => ! empty($user) ? $user['id'] : 0, - 'task_id' => $task['id'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_COMMENT, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle new issues - * - * @access public - * @param array $payload - * @return boolean - */ - public function handleIssueOpened(array $payload) - { - $event = array( - 'project_id' => $this->project_id, - 'reference' => $payload['issue']['id'], - 'title' => $payload['issue']['title'], - 'description' => $payload['issue']['content']['raw']."\n\n[".t('Bitbucket Issue').']('.$payload['issue']['links']['html']['href'].')', - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_OPENED, - new GenericEvent($event) - ); - - return true; - } - - /** - * Handle issue updates - * - * @access public - * @param array $payload - * @return boolean - */ - public function handleIssueUpdated(array $payload) - { - $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['id']); - - if (empty($task)) { - return false; - } - - if (isset($payload['changes']['status'])) { - return $this->handleStatusChange($task, $payload); - } elseif (isset($payload['changes']['assignee'])) { - return $this->handleAssigneeChange($task, $payload); - } - - return false; - } - - /** - * Handle issue status change - * - * @access public - * @param array $task - * @param array $payload - * @return boolean - */ - public function handleStatusChange(array $task, array $payload) - { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $payload['issue']['id'], - ); - - switch ($payload['issue']['state']) { - case 'closed': - $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_CLOSED, new GenericEvent($event)); - return true; - case 'open': - $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_REOPENED, new GenericEvent($event)); - return true; - } - - return false; - } - - /** - * Handle issue assignee change - * - * @access public - * @param array $task - * @param array $payload - * @return boolean - */ - public function handleAssigneeChange(array $task, array $payload) - { - if (empty($payload['issue']['assignee'])) { - return $this->handleIssueUnassigned($task, $payload); - } - - return $this->handleIssueAssigned($task, $payload); - } - - /** - * Handle issue assigned - * - * @access public - * @param array $task - * @param array $payload - * @return boolean - */ - public function handleIssueAssigned(array $task, array $payload) - { - $user = $this->user->getByUsername($payload['issue']['assignee']['username']); - - if (empty($user)) { - return false; - } - - if (! $this->projectPermission->isMember($this->project_id, $user['id'])) { - return false; - } - - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'owner_id' => $user['id'], - 'reference' => $payload['issue']['id'], - ); - - $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_ASSIGNEE_CHANGE, new GenericEvent($event)); - - return true; - } - - /** - * Handle issue unassigned - * - * @access public - * @param array $task - * @param array $payload - * @return boolean - */ - public function handleIssueUnassigned(array $task, array $payload) - { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'owner_id' => 0, - 'reference' => $payload['issue']['id'], - ); - - $this->container['dispatcher']->dispatch(self::EVENT_ISSUE_ASSIGNEE_CHANGE, new GenericEvent($event)); - - return true; - } - - /** - * Parse push events - * - * @access public - * @param array $payload - * @return boolean - */ - public function handlePush(array $payload) - { - if (isset($payload['push']['changes'])) { - foreach ($payload['push']['changes'] as $change) { - if (isset($change['new']['target']) && $this->handleCommit($change['new']['target'], $payload['actor'])) { - return true; - } - } - } - - return false; - } - - /** - * Parse commit - * - * @access public - * @param array $commit Bitbucket commit - * @param array $actor Bitbucket actor - * @return boolean - */ - public function handleCommit(array $commit, array $actor) - { - $task_id = $this->task->getTaskIdFromText($commit['message']); - - if (empty($task_id)) { - return false; - } - - $task = $this->taskFinder->getById($task_id); - - if (empty($task)) { - return false; - } - - if ($task['project_id'] != $this->project_id) { - return false; - } - - $this->container['dispatcher']->dispatch( - self::EVENT_COMMIT, - new GenericEvent(array( - 'task_id' => $task_id, - 'commit_message' => $commit['message'], - 'commit_url' => $commit['links']['html']['href'], - 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Bitbucket', $actor['display_name']).']('.$commit['links']['html']['href'].')', - ) + $task) - ); - - return true; - } -} diff --git a/sources/app/Integration/GithubWebhook.php b/sources/app/Integration/GithubWebhook.php deleted file mode 100644 index c8b53e3..0000000 --- a/sources/app/Integration/GithubWebhook.php +++ /dev/null @@ -1,380 +0,0 @@ -project_id = $project_id; - } - - /** - * Parse Github events - * - * @access public - * @param string $type Github event type - * @param array $payload Github event - * @return boolean - */ - public function parsePayload($type, array $payload) - { - switch ($type) { - case 'push': - return $this->parsePushEvent($payload); - case 'issues': - return $this->parseIssueEvent($payload); - case 'issue_comment': - return $this->parseCommentIssueEvent($payload); - } - - return false; - } - - /** - * Parse Push events (list of commits) - * - * @access public - * @param array $payload Event data - * @return boolean - */ - public function parsePushEvent(array $payload) - { - foreach ($payload['commits'] as $commit) { - $task_id = $this->task->getTaskIdFromText($commit['message']); - - if (empty($task_id)) { - continue; - } - - $task = $this->taskFinder->getById($task_id); - - if (empty($task)) { - continue; - } - - if ($task['project_id'] != $this->project_id) { - continue; - } - - $this->container['dispatcher']->dispatch( - self::EVENT_COMMIT, - new GenericEvent(array( - 'task_id' => $task_id, - 'commit_message' => $commit['message'], - 'commit_url' => $commit['url'], - 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Github', $commit['author']['username']).']('.$commit['url'].')' - ) + $task) - ); - } - - return true; - } - - /** - * Parse issue events - * - * @access public - * @param array $payload Event data - * @return boolean - */ - public function parseIssueEvent(array $payload) - { - switch ($payload['action']) { - case 'opened': - return $this->handleIssueOpened($payload['issue']); - case 'closed': - return $this->handleIssueClosed($payload['issue']); - case 'reopened': - return $this->handleIssueReopened($payload['issue']); - case 'assigned': - return $this->handleIssueAssigned($payload['issue']); - case 'unassigned': - return $this->handleIssueUnassigned($payload['issue']); - case 'labeled': - return $this->handleIssueLabeled($payload['issue'], $payload['label']); - case 'unlabeled': - return $this->handleIssueUnlabeled($payload['issue'], $payload['label']); - } - - return false; - } - - /** - * Parse comment issue events - * - * @access public - * @param array $payload Event data - * @return boolean - */ - public function parseCommentIssueEvent(array $payload) - { - $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['number']); - - if (! empty($task)) { - $user = $this->user->getByUsername($payload['comment']['user']['login']); - - if (! empty($user) && ! $this->projectPermission->isMember($this->project_id, $user['id'])) { - $user = array(); - } - - $event = array( - 'project_id' => $this->project_id, - 'reference' => $payload['comment']['id'], - 'comment' => $payload['comment']['body']."\n\n[".t('By @%s on Github', $payload['comment']['user']['login']).']('.$payload['comment']['html_url'].')', - 'user_id' => ! empty($user) ? $user['id'] : 0, - 'task_id' => $task['id'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_COMMENT, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle new issues - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueOpened(array $issue) - { - $event = array( - 'project_id' => $this->project_id, - 'reference' => $issue['number'], - 'title' => $issue['title'], - 'description' => $issue['body']."\n\n[".t('Github Issue').']('.$issue['html_url'].')', - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_OPENED, - new GenericEvent($event) - ); - - return true; - } - - /** - * Handle issue closing - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueClosed(array $issue) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['number'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_CLOSED, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle issue reopened - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueReopened(array $issue) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['number'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_REOPENED, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle issue assignee change - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueAssigned(array $issue) - { - $user = $this->user->getByUsername($issue['assignee']['login']); - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($user) && ! empty($task) && $this->projectPermission->isMember($this->project_id, $user['id'])) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'owner_id' => $user['id'], - 'reference' => $issue['number'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_ASSIGNEE_CHANGE, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle unassigned issue - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueUnassigned(array $issue) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'owner_id' => 0, - 'reference' => $issue['number'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_ASSIGNEE_CHANGE, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle labeled issue - * - * @access public - * @param array $issue Issue data - * @param array $label Label data - * @return boolean - */ - public function handleIssueLabeled(array $issue, array $label) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['number'], - 'label' => $label['name'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_LABEL_CHANGE, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Handle unlabeled issue - * - * @access public - * @param array $issue Issue data - * @param array $label Label data - * @return boolean - */ - public function handleIssueUnlabeled(array $issue, array $label) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['number'], - 'label' => $label['name'], - 'category_id' => 0, - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_LABEL_CHANGE, - new GenericEvent($event) - ); - - return true; - } - - return false; - } -} diff --git a/sources/app/Integration/GitlabWebhook.php b/sources/app/Integration/GitlabWebhook.php deleted file mode 100644 index b3f9b0b..0000000 --- a/sources/app/Integration/GitlabWebhook.php +++ /dev/null @@ -1,265 +0,0 @@ -project_id = $project_id; - } - - /** - * Parse events - * - * @access public - * @param array $payload Gitlab event - * @return boolean - */ - public function parsePayload(array $payload) - { - switch ($this->getType($payload)) { - case self::TYPE_PUSH: - return $this->handlePushEvent($payload); - case self::TYPE_ISSUE; - return $this->handleIssueEvent($payload); - case self::TYPE_COMMENT; - return $this->handleCommentEvent($payload); - } - - return false; - } - - /** - * Get event type - * - * @access public - * @param array $payload Gitlab event - * @return string - */ - public function getType(array $payload) - { - if (empty($payload['object_kind'])) { - return ''; - } - - switch ($payload['object_kind']) { - case 'issue': - return self::TYPE_ISSUE; - case 'note': - return self::TYPE_COMMENT; - case 'push': - return self::TYPE_PUSH; - default: - return ''; - } - } - - /** - * Parse push event - * - * @access public - * @param array $payload Gitlab event - * @return boolean - */ - public function handlePushEvent(array $payload) - { - foreach ($payload['commits'] as $commit) { - $this->handleCommit($commit); - } - - return true; - } - - /** - * Parse commit - * - * @access public - * @param array $commit Gitlab commit - * @return boolean - */ - public function handleCommit(array $commit) - { - $task_id = $this->task->getTaskIdFromText($commit['message']); - - if (empty($task_id)) { - return false; - } - - $task = $this->taskFinder->getById($task_id); - - if (empty($task)) { - return false; - } - - if ($task['project_id'] != $this->project_id) { - return false; - } - - $this->container['dispatcher']->dispatch( - self::EVENT_COMMIT, - new GenericEvent(array( - 'task_id' => $task_id, - 'commit_message' => $commit['message'], - 'commit_url' => $commit['url'], - 'commit_comment' => $commit['message']."\n\n[".t('Commit made by @%s on Gitlab', $commit['author']['name']).']('.$commit['url'].')' - ) + $task) - ); - - return true; - } - - /** - * Parse issue event - * - * @access public - * @param array $payload Gitlab event - * @return boolean - */ - public function handleIssueEvent(array $payload) - { - switch ($payload['object_attributes']['action']) { - case 'open': - return $this->handleIssueOpened($payload['object_attributes']); - case 'close': - return $this->handleIssueClosed($payload['object_attributes']); - } - - return false; - } - - /** - * Handle new issues - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueOpened(array $issue) - { - $event = array( - 'project_id' => $this->project_id, - 'reference' => $issue['id'], - 'title' => $issue['title'], - 'description' => $issue['description']."\n\n[".t('Gitlab Issue').']('.$issue['url'].')', - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_OPENED, - new GenericEvent($event) - ); - - return true; - } - - /** - * Handle issue closing - * - * @access public - * @param array $issue Issue data - * @return boolean - */ - public function handleIssueClosed(array $issue) - { - $task = $this->taskFinder->getByReference($this->project_id, $issue['id']); - - if (! empty($task)) { - $event = array( - 'project_id' => $this->project_id, - 'task_id' => $task['id'], - 'reference' => $issue['id'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_CLOSED, - new GenericEvent($event) - ); - - return true; - } - - return false; - } - - /** - * Parse comment issue events - * - * @access public - * @param array $payload Event data - * @return boolean - */ - public function handleCommentEvent(array $payload) - { - if (! isset($payload['issue'])) { - return false; - } - - $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['id']); - - if (! empty($task)) { - $user = $this->user->getByUsername($payload['user']['username']); - - if (! empty($user) && ! $this->projectPermission->isMember($this->project_id, $user['id'])) { - $user = array(); - } - - $event = array( - 'project_id' => $this->project_id, - 'reference' => $payload['object_attributes']['id'], - 'comment' => $payload['object_attributes']['note']."\n\n[".t('By @%s on Gitlab', $payload['user']['username']).']('.$payload['object_attributes']['url'].')', - 'user_id' => ! empty($user) ? $user['id'] : 0, - 'task_id' => $task['id'], - ); - - $this->container['dispatcher']->dispatch( - self::EVENT_ISSUE_COMMENT, - new GenericEvent($event) - ); - - return true; - } - - return false; - } -} diff --git a/sources/app/Locale/bs_BA/translations.php b/sources/app/Locale/bs_BA/translations.php index 9faa10e..6ad3a78 100644 --- a/sources/app/Locale/bs_BA/translations.php +++ b/sources/app/Locale/bs_BA/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Složenost', 'Task limit' => 'Najviše zadataka', 'Task count' => 'Broj zadataka', - 'Edit project access list' => 'Uredi prava pristupa projektu', - 'Allow this user' => 'Dozvoli ovog korisnika', - 'Don\'t forget that administrators have access to everything.' => 'Zapamti: Administrator može pristupiti svemu!', - 'Revoke' => 'Opozovi', - 'List of authorized users' => 'Spisak odobrenih korisnika', 'User' => 'Korisnik', - 'Nobody have access to this project.' => 'Niko nema pristup ovom projektu', 'Comments' => 'Komentari', 'Write your text in Markdown' => 'Pisanje teksta pomoću Markdown', 'Leave a comment' => 'Ostavi komentar', @@ -396,8 +390,6 @@ return array( 'Email:' => 'Email: ', 'Notifications:' => 'Obavještenja: ', 'Notifications' => 'Obavještenja', - 'Group:' => 'Grupa:', - 'Regular user' => 'Standardni korisnik', 'Account type:' => 'Vrsta korisničkog računa:', 'Edit profile' => 'Uredi profil', 'Change password' => 'Promijeni šifru', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s promijenio izvršioca za zadatak %s u %s', 'New password for the user "%s"' => 'Nova šifra korisnika "%s"', 'Choose an event' => 'Izaberi događaj', - 'Github commit received' => 'Github: commit dobijen', - 'Github issue opened' => 'Github: otvoren problem', - 'Github issue closed' => 'Github: zatvoren problem', - 'Github issue reopened' => 'Github: ponovo otvoren problem', - 'Github issue assignee change' => 'Github: izmijenjen izvršioc problema', - 'Github issue label change' => 'Github: izmjena etikete problema', 'Create a task from an external provider' => 'Kreiraj zadatak preko posrednika', 'Change the assignee based on an external username' => 'Izmijene izvršioca bazirano na vanjskom korisničkom imenu', 'Change the category based on an external label' => 'Izmijene kategorije bazirano na vanjskoj etiketi', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Svima je dozvoljen pristup ovom projektu.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Pomoć na Github webhooks', 'Create a comment from an external provider' => 'Napravi komentar preko vanjskog posrednika', - 'Github issue comment created' => 'Github: dodan komentar za problem', 'Project management' => 'Upravljanje projektima', 'My projects' => 'Moji projekti', 'Columns' => 'Kolone', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Zaduženja korisnika za "%s"', 'Clone this project' => 'Kloniraj ovaj projekat', 'Column removed successfully.' => 'Kolona uspješno uklonjena.', - 'Github Issue' => 'Github problemi', 'Not enough data to show the graph.' => 'Nedovoljno podataka za prikaz na grafikonu.', 'Previous' => 'Prethodni', 'The id must be an integer' => 'ID mora biti cjeloviti broj', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Podrazumijevana swimline traka', 'Do you really want to remove this swimlane: "%s"?' => 'Da li zaista želiš ukloniti ovu swimline traku: "%s"?', 'Inactive swimlanes' => 'Neaktivne swimline trake', - 'Set project manager' => 'Postavi kao projekt menadžera', - 'Set project member' => 'Postavi kao člana projekta ', 'Remove a swimlane' => 'Ukloni swimline traku', 'Rename' => 'Preimenuj', 'Show default swimlane' => 'Prikaži podrazumijevanu swimline traku', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Swimline traka je uspješno kreirana.', 'Example: "Bug, Feature Request, Improvement"' => 'Npr: "Greška, Zahtjev za izmjenama, Poboljšanje"', 'Default categories for new projects (Comma-separated)' => 'Podrazumijevane kategorije za novi projekat', - 'Gitlab commit received' => 'Gitlab: commit dobijen', - 'Gitlab issue opened' => 'Gitlab: problem otvoren', - 'Gitlab issue closed' => 'Gitlab: problem zatvoren', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Pomoc na Gitlab webhooks', 'Integrations' => 'Integracije', 'Integration with third-party services' => 'Integracija sa uslugama vanjskih servisa', - 'Role for this project' => 'Uloga u ovom projektu', - 'Project manager' => 'Manadžer projekta', - 'Project member' => 'Učesnik projekta', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Projekt menadžer može mijenjati postavke projekta i ima više prava od standardnog korisnika.', - 'Gitlab Issue' => 'Gitlab problemi', 'Subtask Id' => 'ID pod-zadatka', 'Subtasks' => 'Pod-zadaci', 'Subtasks Export' => 'Izvoz pod-zadataka', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Već imaš jedan pod-zadatak "u radu"', 'Which parts of the project do you want to duplicate?' => 'Koje delove projekta želiš duplicirati?', 'Disallow login form' => 'Zabrani prijavnu formu', - 'Bitbucket commit received' => 'Bitbucket: commit dobijen', - 'Bitbucket webhooks' => 'Bitbucket: webhooks', - 'Help on Bitbucket webhooks' => 'Pomoć na Bitbucket webhooks', 'Start' => 'Početak', 'End' => 'Kraj', 'Task age in days' => 'Trajanje zadatka u danima', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Faktor-dva autentifikacionog koda je validan.', 'Code' => 'Kod', 'Two factor authentication' => 'Faktor-dva autentifikacija', - 'Enable/disable two factor authentication' => 'Omogući/onemogući faktor-dva autentifikaciju', 'This QR code contains the key URI: ' => 'Ovaj QR kod sadržava ključni URL: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Sačuvaj tajni klju u svom TOTP softveru (npr. Google Authenticator or FreeOTP)', 'Check my code' => 'Provjeri moj kod', 'Secret key: ' => 'Tajni ključ: ', 'Test your device' => 'Testiraj svoj uređaj', @@ -724,7 +689,7 @@ return array( 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Identifikator projekta je opcionalni alfanumerički kod koji se koristi za identifikaciju projekta.', 'Identifier' => 'Identifikator', 'Disable two factor authentication' => 'Onemogući faktor-dva autentifikaciju', - 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Da li zaista želiš onemogućiti faktor-dva autentifikaciju: %s?', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Da li zaista želiš onemogućiti faktor-dva autentifikaciju: "%s"?', 'Edit link' => 'Uredi vezu', 'Start to type task title...' => 'Počni pisati naslov zadatka...', 'A task cannot be linked to itself' => 'Zadatak ne može biti povezan sa samim sobom', @@ -749,7 +714,7 @@ return array( 'Factor to calculate new due date: ' => 'Faktor za računanje novog datuma završetka: ', 'Month(s)' => 'Mjesec(i)', 'Recurrence' => 'Referenca', - 'This task has been created by: ' => 'Ovaj zadatak će napravio: ', + 'This task has been created by: ' => 'Ovaj zadatak je napravio: ', 'Recurrent task has been generated:' => 'Ponavljajući zadatak je napravio:', 'Timeframe to calculate new due date: ' => 'Vremenski okvir za računanje novog datuma završetka:', 'Trigger to generate recurrent task: ' => 'Okidač za pravljenje ponavljajućeg zadatka', @@ -776,21 +741,10 @@ return array( 'User that will receive the email' => 'Korisnik će dobiti email', 'Email subject' => 'Predmet email-a', 'Date' => 'Datum', - 'By @%s on Bitbucket' => 'Od @%s na Bitbucket', - 'Bitbucket Issue' => 'Bitbucket problem', - 'Commit made by @%s on Bitbucket' => 'Commit-ao @%s na Bitbucket', - 'Commit made by @%s on Github' => 'Commit-ao @%s na Github', - 'By @%s on Github' => '@%s na Github', - 'Commit made by @%s on Gitlab' => 'Commit-ao @%s na Gitlab', 'Add a comment log when moving the task between columns' => 'Dodaj komentar u dnevnik kada se pomjeri zadatak između kolona', 'Move the task to another column when the category is changed' => 'Pomjeri zadatak u drugu kolonu kada je kategorija promijenjena', 'Send a task by email to someone' => 'Pošalji zadatak nekome emailom', 'Reopen a task' => 'Ponovo otvori zadatak', - 'Bitbucket issue opened' => 'Bitbucket: otvoren problem', - 'Bitbucket issue closed' => 'Bitbucket: zatvoren problem', - 'Bitbucket issue reopened' => 'Bitbucket: problem ponovo otvoren', - 'Bitbucket issue assignee change' => 'Bitbucket: promijenjen izvršilac problema', - 'Bitbucket issue comment created' => 'Bitbucket: dodan komentar na problemu', 'Column change' => 'Promijena kolone', 'Position change' => 'Promjena pozicije', 'Swimlane change' => 'Promjena swimline trake', @@ -910,8 +864,6 @@ return array( 'Remote user' => 'Vanjski korisnik', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Vanjski korisnik ne čuva šifru u Kanboard bazi, npr: LDAP, Google i Github korisnički računi.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Ako ste označili kvadratić "Zabrani prijavnu formu", unos pristupnih podataka u prijavnoj formi će biti ignorisan.', - 'By @%s on Gitlab' => 'Od @%s s Gitlab-om', - 'Gitlab issue comment created' => 'Gitlab: dodan komentar za problem', 'New remote user' => 'Novi vanjski korisnik', 'New local user' => 'Novi lokalni korisnik', 'Default task color' => 'Podrazumijevana boja zadatka', @@ -931,7 +883,6 @@ return array( 'contributors' => 'saradnici', 'License:' => 'Licenca:', 'License' => 'Licenca', - 'Project Administrator' => 'Administrator projekta', 'Enter the text below' => 'Unesi tekst ispod', 'Gantt chart for %s' => 'Gantogram za %s', 'Sort by position' => 'Sortiraj po poziciji', @@ -955,7 +906,6 @@ return array( 'Members' => 'Članovi', 'Shared project' => 'Dijeljeni projekti', 'Project managers' => 'Menadžeri projekta', - 'Project members' => 'Članovi projekta', 'Gantt chart for all projects' => 'Gantogram za sve projekte', 'Projects list' => 'Lista projekata', 'Gantt chart for this project' => 'Gantogram za ovaj projekat', @@ -982,7 +932,6 @@ return array( 'Documentation' => 'Dokumentacija', 'Table of contents' => 'Sadržaj', 'Gantt' => 'Gantogram', - 'Help with project permissions' => 'Pomoć s pravima nad projektom', 'Author' => 'Autor', 'Version' => 'Verzija', 'Plugins' => 'Dodaci', @@ -1039,13 +988,12 @@ return array( 'Vertical bar' => 'Vertikalna traka', 'Double Quote' => 'Dvostruki navodnici', 'Single Quote' => 'Jednostruki navodnici', - '%s attached a file to the task #%d' => '%s file je prirodan zadatku #%d', + '%s attached a file to the task #%d' => '%s je dodao novi fajl u zadatak %d', 'There is no column or swimlane activated in your project!' => 'Nema kolone ili swimline trake aktivirane za ovaj projekat!', 'Append filter (instead of replacement)' => 'Dodaj filter (umjesto zamjene postojećeg)', 'Append/Replace' => 'Dodaj/Zamijeni', 'Append' => 'Dodaj', 'Replace' => 'Zamijeni', - 'There is no notification method registered.' => 'Nema registrovanih metoda za obavještenja', 'Import' => 'Uvoz', 'change sorting' => 'Promijeni sortiranje', 'Tasks Importation' => 'Uvoz zadataka', @@ -1064,8 +1012,96 @@ return array( 'Usernames must be lowercase and unique' => 'Korisničko ime mora biti malim slovima i jedinstveno', 'Passwords will be encrypted if present' => 'Šifra će biti kriptovana', // '%s attached a new file to the task %s' => '', - // 'Assign automatically a category based on a link' => '', - 'BAM - Konvertibile Mark' => 'BAM - Konvertibilna marka', + 'Assign automatically a category based on a link' => 'Automatsko pridruživanje kategorije bazirano na vezi', + 'BAM - Konvertible Mark' => 'BAM - Konvertibilna marka', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + 'Project members' => 'Članovi projekta', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/cs_CZ/translations.php b/sources/app/Locale/cs_CZ/translations.php index 5d6af0b..2c6cb7e 100644 --- a/sources/app/Locale/cs_CZ/translations.php +++ b/sources/app/Locale/cs_CZ/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Složitost', 'Task limit' => 'Maximální počet úkolů', 'Task count' => 'Počet úkolů', - 'Edit project access list' => 'Upravit přístupový seznam projektu', - 'Allow this user' => 'Povolit tomuto uživateli', - 'Don\'t forget that administrators have access to everything.' => 'Nezapomeňte, že administrátoři mají přistup ke všem údajům.', - 'Revoke' => 'Odebrat', - 'List of authorized users' => 'Seznam autorizovaných uživatelů', 'User' => 'Uživatel', - 'Nobody have access to this project.' => 'Nikdo nemá přístup k tomuto projektu.', 'Comments' => 'Komentáře', 'Write your text in Markdown' => 'Můžete použít i Markdown-syntaxi', 'Leave a comment' => 'Zanechte komentář', @@ -396,8 +390,6 @@ return array( 'Email:' => 'e-mail', 'Notifications:' => 'Upozornění:', 'Notifications' => 'Upozornění', - 'Group:' => 'Skupina', - 'Regular user' => 'Pravidelný uživatel', 'Account type:' => 'Typ účtu:', 'Edit profile' => 'Upravit profil', 'Change password' => 'Změnit heslo', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s změnil řešitele úkolu %s na uživatele %s', 'New password for the user "%s"' => 'Nové heslo pro uživatele "%s"', 'Choose an event' => 'Vybrat událost', - 'Github commit received' => 'Github commit empfangen', - 'Github issue opened' => 'Github Fehler geöffnet', - 'Github issue closed' => 'Github Fehler geschlossen', - 'Github issue reopened' => 'Github Fehler erneut geöffnet', - 'Github issue assignee change' => 'Github Fehlerzuständigkeit geändert', - 'Github issue label change' => 'Github Fehlerkennzeichnung verändert', 'Create a task from an external provider' => 'Vytvořit úkol externím poskytovatelem', 'Change the assignee based on an external username' => 'Změna přiřazení uživatele závislá na externím uživateli', 'Change the category based on an external label' => 'Změna kategorie závislá na externím popisku', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Přístup k tomuto projektu má kdokoliv.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github Webhook', - 'Help on Github webhooks' => 'Hilfe für Github Webhooks', 'Create a comment from an external provider' => 'Vytvořit komentář pomocí externího poskytovatele', - 'Github issue comment created' => 'Github Fehler Kommentar hinzugefügt', 'Project management' => 'Správa projektů', 'My projects' => 'Moje projekty', 'Columns' => 'Sloupce', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Rozdělení podle uživatelů pro "%s"', 'Clone this project' => 'Duplokovat projekt', 'Column removed successfully.' => 'Sloupec byl odstraněn.', - 'Github Issue' => 'Github Issue', 'Not enough data to show the graph.' => 'Pro zobrazení grafu není dostatek dat.', 'Previous' => 'Předchozí', 'The id must be an integer' => 'ID musí být celé číslo', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Výchozí Swimlane', 'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?', 'Inactive swimlanes' => 'Inaktive Swimlane', - 'Set project manager' => 'Nastavit práva vedoucího projektu', - 'Set project member' => 'Nastavit práva člena projektu', 'Remove a swimlane' => 'Odstranit swimlane', 'Rename' => 'Přejmenovat', 'Show default swimlane' => 'Standard Swimlane anzeigen', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.', 'Example: "Bug, Feature Request, Improvement"' => 'Beispiel: "Bug, Funktionswünsche, Verbesserung"', 'Default categories for new projects (Comma-separated)' => 'Výchozí kategorie pro nové projekty (oddělené čárkou)', - 'Gitlab commit received' => 'Gitlab commit erhalten', - 'Gitlab issue opened' => 'Gitlab Fehler eröffnet', - 'Gitlab issue closed' => 'Gitlab Fehler geschlossen', - 'Gitlab webhooks' => 'Gitlab Webhook', - 'Help on Gitlab webhooks' => 'Hilfe für Gitlab Webhooks', 'Integrations' => 'Integrace', 'Integration with third-party services' => 'Integration von Fremdleistungen', - 'Role for this project' => 'Role pro tento projekt', - 'Project manager' => 'Projektový manažer', - 'Project member' => 'Člen projektu', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Manažer projektu může změnit nastavení projektu a zároveň má více práv než standardní uživatel.', - 'Gitlab Issue' => 'Gitlab Fehler', 'Subtask Id' => 'Dílčí úkol Id', 'Subtasks' => 'Dílčí úkoly', 'Subtasks Export' => 'Export dílčích úkolů', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Jeden dílčí úkol již aktuálně řešíte', 'Which parts of the project do you want to duplicate?' => 'Které části projektu chcete duplikovat?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket commit erhalten', - 'Bitbucket webhooks' => 'Bitbucket webhooks', - 'Help on Bitbucket webhooks' => 'Hilfe für Bitbucket webhooks', 'Start' => 'Začátek', 'End' => 'Konec', 'Task age in days' => 'Doba trvání úkolu ve dnech', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.', 'Code' => 'Code', 'Two factor authentication' => 'Dvouúrovňová autorizace', - 'Enable/disable two factor authentication' => 'Povolit / zakázat dvou úrovňovou autorizaci', 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Speichere den geheimen Schlüssel in deiner TOTP software (z.B. Google Authenticator oder FreeOTP).', 'Check my code' => 'Kontrola mého kódu', 'Secret key: ' => 'Tajný klíč', 'Test your device' => 'Test Vašeho zařízení', @@ -776,21 +741,10 @@ return array( 'User that will receive the email' => 'Uživatel, který dostane E-mail', 'Email subject' => 'E-mail Předmět', 'Date' => 'Datum', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', 'Add a comment log when moving the task between columns' => 'Přidat komentář když je úkol přesouván mezi sloupci', 'Move the task to another column when the category is changed' => 'Přesun úkolu do jiného sloupce když je změněna kategorie', 'Send a task by email to someone' => 'Poslat někomu úkol poštou', 'Reopen a task' => 'Znovu otevřít úkol', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', 'Column change' => 'Spalte geändert', 'Position change' => 'Position geändert', 'Swimlane change' => 'Swimlane geändert', @@ -910,8 +864,6 @@ return array( 'Remote user' => 'Vzdálený uživatel', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Hesla vzdáleným uživatelům se neukládají do databáze Kanboard. Naříklad: LDAP, Google a Github účty.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Pokud zaškrtnete políčko "Zakázat přihlašovací formulář", budou pověření zadané do přihlašovacího formuláře ignorovány.', - 'By @%s on Gitlab' => 'uživatelem @%s na Gitlab', - 'Gitlab issue comment created' => 'Vytvořen komentář problému na Gitlab', 'New remote user' => 'Nový vzdálený uživatel', 'New local user' => 'Nový lokální uživatel', 'Default task color' => 'Výchozí barva úkolu', @@ -931,7 +883,6 @@ return array( 'contributors' => 'přispěvatelé', 'License:' => 'Licence:', 'License' => 'Licence', - 'Project Administrator' => 'Administrátor projektu', 'Enter the text below' => 'Zadejte text níže', 'Gantt chart for %s' => 'Gantt graf pro %s', 'Sort by position' => 'Třídit podle pozice', @@ -955,7 +906,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/da_DK/translations.php b/sources/app/Locale/da_DK/translations.php index a5ac1d3..820b4d1 100644 --- a/sources/app/Locale/da_DK/translations.php +++ b/sources/app/Locale/da_DK/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Kompleksitet', 'Task limit' => 'Opgave begrænsning', // 'Task count' => '', - 'Edit project access list' => 'Rediger adgangstilladelser for projektet', - 'Allow this user' => 'Tillad denne bruger', - 'Don\'t forget that administrators have access to everything.' => 'Glem ikke at administratorer har adgang til alt.', - 'Revoke' => 'Fjern', - 'List of authorized users' => 'Liste over autoriserede brugere', 'User' => 'Bruger', - 'Nobody have access to this project.' => 'Ingen har adgang til dette projekt.', 'Comments' => 'Kommentarer', 'Write your text in Markdown' => 'Skriv din tekst i markdown', 'Leave a comment' => 'Efterlad en kommentar', @@ -396,8 +390,6 @@ return array( 'Email:' => 'Email:', 'Notifications:' => 'Notifikationer:', 'Notifications' => 'Notifikationer', - 'Group:' => 'Gruppe:', - 'Regular user' => 'Normal bruger', 'Account type:' => 'Konto type:', 'Edit profile' => 'Rediger profil', 'Change password' => 'Skift adgangskode', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s skift ansvarlig for opgaven %s til %s', 'New password for the user "%s"' => 'Ny adgangskode for brugeren "%s"', 'Choose an event' => 'Vælg et event', - 'Github commit received' => 'Github commit modtaget', - 'Github issue opened' => 'Github problem åbet', - 'Github issue closed' => 'Github problem lukket', - 'Github issue reopened' => 'Github problem genåbnet', - 'Github issue assignee change' => 'Github problem ansvarlig skift', - 'Github issue label change' => 'Github problem label skift', 'Create a task from an external provider' => 'Opret en opgave fra en ekstern udbyder', 'Change the assignee based on an external username' => 'Skift den ansvarlige baseret på et eksternt brugernavn', 'Change the category based on an external label' => 'Skift kategorien baseret på en ekstern label', @@ -495,10 +481,7 @@ return array( // 'Everybody have access to this project.' => '', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', - // 'Github issue comment created' => '', // 'Project management' => '', // 'My projects' => '', // 'Columns' => '', @@ -516,7 +499,6 @@ return array( // 'User repartition for "%s"' => '', // 'Clone this project' => '', // 'Column removed successfully.' => '', - // 'Github Issue' => '', // 'Not enough data to show the graph.' => '', // 'Previous' => '', // 'The id must be an integer' => '', @@ -546,8 +528,6 @@ return array( // 'Default swimlane' => '', // 'Do you really want to remove this swimlane: "%s"?' => '', // 'Inactive swimlanes' => '', - // 'Set project manager' => '', - // 'Set project member' => '', // 'Remove a swimlane' => '', // 'Rename' => '', // 'Show default swimlane' => '', @@ -563,18 +543,8 @@ return array( // 'Your swimlane have been created successfully.' => '', // 'Example: "Bug, Feature Request, Improvement"' => '', // 'Default categories for new projects (Comma-separated)' => '', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', // 'Integrations' => '', // 'Integration with third-party services' => '', - // 'Role for this project' => '', - // 'Project manager' => '', - // 'Project member' => '', - // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '', - // 'Gitlab Issue' => '', // 'Subtask Id' => '', // 'Subtasks' => '', // 'Subtasks Export' => '', @@ -602,9 +572,6 @@ return array( // 'You already have one subtask in progress' => '', // 'Which parts of the project do you want to duplicate?' => '', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', // 'Start' => '', // 'End' => '', // 'Task age in days' => '', @@ -702,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -776,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -910,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -931,7 +883,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +906,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/de_DE/translations.php b/sources/app/Locale/de_DE/translations.php index 13fa754..f1c89ca 100644 --- a/sources/app/Locale/de_DE/translations.php +++ b/sources/app/Locale/de_DE/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Komplexität', 'Task limit' => 'Maximale Anzahl von Aufgaben', 'Task count' => 'Aufgabenanzahl', - 'Edit project access list' => 'Zugriffsberechtigungen des Projektes bearbeiten', - 'Allow this user' => 'Diesen Benutzer autorisieren', - 'Don\'t forget that administrators have access to everything.' => 'Nicht vergessen: Administratoren haben überall Zugriff.', - 'Revoke' => 'Entfernen', - 'List of authorized users' => 'Liste der autorisierten Benutzer', 'User' => 'Benutzer', - 'Nobody have access to this project.' => 'Niemand hat Zugriff auf dieses Projekt.', 'Comments' => 'Kommentare', 'Write your text in Markdown' => 'Schreibe deinen Text in Markdown-Syntax', 'Leave a comment' => 'Kommentar eingeben', @@ -396,8 +390,6 @@ return array( 'Email:' => 'E-Mail', 'Notifications:' => 'Benachrichtigungen:', 'Notifications' => 'Benachrichtigungen', - 'Group:' => 'Gruppe', - 'Regular user' => 'Standardbenutzer', 'Account type:' => 'Accounttyp:', 'Edit profile' => 'Profil bearbeiten', 'Change password' => 'Passwort ändern', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s hat die Zuständigkeit der Aufgabe %s geändert um %s', 'New password for the user "%s"' => 'Neues Passwort des Benutzers "%s"', 'Choose an event' => 'Aktion wählen', - 'Github commit received' => 'Github commit empfangen', - 'Github issue opened' => 'Github Fehler geöffnet', - 'Github issue closed' => 'Github Fehler geschlossen', - 'Github issue reopened' => 'Github Fehler erneut geöffnet', - 'Github issue assignee change' => 'Github Fehlerzuständigkeit geändert', - 'Github issue label change' => 'Github Fehlerkennzeichnung verändert', 'Create a task from an external provider' => 'Eine Aufgabe durch einen externen Provider hinzufügen', 'Change the assignee based on an external username' => 'Zuordnung ändern basierend auf externem Benutzernamen', 'Change the category based on an external label' => 'Kategorie basierend auf einer externen Kennzeichnung ändern', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Jeder hat Zugriff zu diesem Projekt', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github-Webhook', - 'Help on Github webhooks' => 'Hilfe für Github-Webhooks', 'Create a comment from an external provider' => 'Kommentar eines externen Providers hinzufügen', - 'Github issue comment created' => 'Kommentar zum Github-Issue hinzugefügt', 'Project management' => 'Projektmanagement', 'My projects' => 'Meine Projekte', 'Columns' => 'Spalten', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Benutzerverteilung für "%s"', 'Clone this project' => 'Projekt kopieren', 'Column removed successfully.' => 'Spalte erfolgreich entfernt.', - 'Github Issue' => 'Github Issue', 'Not enough data to show the graph.' => 'Nicht genügend Daten, um die Grafik zu zeigen.', 'Previous' => 'Vorherige', 'The id must be an integer' => 'Die Id muss eine ganze Zahl sein', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Standard-Swimlane', 'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?', 'Inactive swimlanes' => 'Inaktive Swimlane', - 'Set project manager' => 'zum Projektmanager machen', - 'Set project member' => 'zum Projektmitglied machen', 'Remove a swimlane' => 'Swimlane entfernen', 'Rename' => 'umbenennen', 'Show default swimlane' => 'Standard-Swimlane anzeigen', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.', 'Example: "Bug, Feature Request, Improvement"' => 'Beispiel: "Bug, Funktionswünsche, Verbesserung"', 'Default categories for new projects (Comma-separated)' => 'Standard-Kategorien für neue Projekte (Komma-getrennt)', - 'Gitlab commit received' => 'Gitlab-Commit erhalten', - 'Gitlab issue opened' => 'Gitlab-Issue eröffnet', - 'Gitlab issue closed' => 'Gitlab-Issue geschlossen', - 'Gitlab webhooks' => 'Gitlab-Webhook', - 'Help on Gitlab webhooks' => 'Hilfe für Gitlab-Webhooks', 'Integrations' => 'Integration', 'Integration with third-party services' => 'Integration von externen Diensten', - 'Role for this project' => 'Rolle für dieses Projekt', - 'Project manager' => 'Projektmanager', - 'Project member' => 'Projektmitglied', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Ein Projektmanager kann die Projekteinstellungen ändern und hat mehr Rechte als ein normaler Benutzer.', - 'Gitlab Issue' => 'Gitlab-Issue', 'Subtask Id' => 'Teilaufgaben-ID', 'Subtasks' => 'Teilaufgaben', 'Subtasks Export' => 'Export von Teilaufgaben', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Bereits eine Teilaufgabe in Bearbeitung', 'Which parts of the project do you want to duplicate?' => 'Welcher Teil des Projekts soll kopiert werden?', 'Disallow login form' => 'Verbiete Login-Formular', - 'Bitbucket commit received' => 'Bitbucket-Commit erhalten', - 'Bitbucket webhooks' => 'Bitbucket-Webhooks', - 'Help on Bitbucket webhooks' => 'Hilfe für Bitbucket-Webhooks', 'Start' => 'Start', 'End' => 'Ende', 'Task age in days' => 'Aufgabenalter in Tagen', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.', 'Code' => 'Code', 'Two factor authentication' => 'Zwei-Faktor-Authentifizierung', - 'Enable/disable two factor authentication' => 'Aktiviere/Deaktiviere Zwei-Faktor-Authentifizierung', 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Speichere den geheimen Schlüssel in deiner TOTP software (z.B. Google Authenticator oder FreeOTP).', 'Check my code' => 'Überprüfe meinen Code', 'Secret key: ' => 'Geheimer Schlüssel', 'Test your device' => 'Teste dein Gerät', @@ -776,21 +741,10 @@ return array( 'User that will receive the email' => 'Empfänger der E-Mail', 'Email subject' => 'E-Mail-Betreff', 'Date' => 'Datum', - 'By @%s on Bitbucket' => 'Durch @%s auf Bitbucket', - 'Bitbucket Issue' => 'Bitbucket-Issue', - 'Commit made by @%s on Bitbucket' => 'Commit von @%s auf Bitbucket', - 'Commit made by @%s on Github' => 'Commit von @%s auf Github', - 'By @%s on Github' => 'Durch @%s auf Github', - 'Commit made by @%s on Gitlab' => 'Commit von @%s auf Gitlab', 'Add a comment log when moving the task between columns' => 'Kommentar hinzufügen, wenn Aufgabe in andere Spalte verschoben wird', 'Move the task to another column when the category is changed' => 'Aufgabe in andere Spalte verschieben, wenn Kategorie geändert wird', 'Send a task by email to someone' => 'Aufgabe per E-Mail versenden', 'Reopen a task' => 'Aufgabe wieder öffnen', - 'Bitbucket issue opened' => 'Bitbucket Ticket eröffnet', - 'Bitbucket issue closed' => 'Bitbucket Ticket geschlossen', - 'Bitbucket issue reopened' => 'Bitbucket Ticket wieder eröffnet', - 'Bitbucket issue assignee change' => 'Bitbucket Ticket Zuordnung geändert', - 'Bitbucket issue comment created' => 'Bitbucket Ticket Kommentar erstellt', 'Column change' => 'Spalte geändert', 'Position change' => 'Position geändert', 'Swimlane change' => 'Swimlane geändert', @@ -910,8 +864,6 @@ return array( 'Remote user' => 'Remote-Benutzer', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel LDAP, Goole und Github Accounts', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Wenn die Box "Verbiete Login-Formular" angeschaltet ist, werden Eingaben in das Login Formular ignoriert.', - 'By @%s on Gitlab' => 'Durch @%s auf Gitlab', - 'Gitlab issue comment created' => 'Gitlab Ticket Kommentar erstellt', 'New remote user' => 'Neuer Remote-Benutzer', 'New local user' => 'Neuer lokaler Benutzer', 'Default task color' => 'Voreingestellte Aufgabenfarbe', @@ -931,7 +883,6 @@ return array( 'contributors' => 'Mitwirkende', 'License:' => 'Lizenz:', 'License' => 'Lizenz', - 'Project Administrator' => 'Projektadministrator', 'Enter the text below' => 'Text unten eingeben', 'Gantt chart for %s' => 'Gantt Diagramm für %s', 'Sort by position' => 'Nach Position sortieren', @@ -955,7 +906,6 @@ return array( 'Members' => 'Mitglieder', 'Shared project' => 'Geteiltes Projekt', 'Project managers' => 'Projektmanager', - 'Project members' => 'Projektmitglieder', 'Gantt chart for all projects' => 'Gantt Diagramm für alle Projekte', 'Projects list' => 'Projektliste', 'Gantt chart for this project' => 'Gantt Diagramm für dieses Projekt', @@ -982,7 +932,6 @@ return array( 'Documentation' => 'Dokumentation', 'Table of contents' => 'Inhaltsverzeichnis', 'Gantt' => 'Gantt', - 'Help with project permissions' => 'Hilfe bei Projektberechtigungen', 'Author' => 'Autor', 'Version' => 'Version', 'Plugins' => 'Plugins', @@ -1027,45 +976,132 @@ return array( 'Unread notifications' => 'Ungelesene Benachrichtigungen', 'My filters' => 'Meine Filter', 'Notification methods:' => 'Benachrichtigungs-Methoden:', - // 'Import tasks from CSV file' => '', - // 'Unable to read your file' => '', - // '%d task(s) have been imported successfully.' => '', - // 'Nothing have been imported!' => '', - // 'Import users from CSV file' => '', - // '%d user(s) have been imported successfully.' => '', - // 'Comma' => '', - // 'Semi-colon' => '', - // 'Tab' => '', - // 'Vertical bar' => '', - // 'Double Quote' => '', - // 'Single Quote' => '', - // '%s attached a file to the task #%d' => '', - // 'There is no column or swimlane activated in your project!' => '', - // 'Append filter (instead of replacement)' => '', - // 'Append/Replace' => '', - // 'Append' => '', - // 'Replace' => '', - // 'There is no notification method registered.' => '', - // 'Import' => '', - // 'change sorting' => '', - // 'Tasks Importation' => '', - // 'Delimiter' => '', - // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', - // 'No external integration registered.' => '', - // 'Duplicates are not imported' => '', - // 'Usernames must be lowercase and unique' => '', - // 'Passwords will be encrypted if present' => '', - // '%s attached a new file to the task %s' => '', - // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', + 'Import tasks from CSV file' => 'Importiere Aufgaben aus CSV Datei', + 'Unable to read your file' => 'Die Datei kann nicht gelesen werden', + '%d task(s) have been imported successfully.' => '%d Aufgabe(n) wurde(n) erfolgreich importiert', + 'Nothing have been imported!' => 'Es wurde nichts importiert!', + 'Import users from CSV file' => 'Importiere Benutzer aus CSV Datei', + '%d user(s) have been imported successfully.' => '%d Benutzer wurde(n) erfolgreich importiert.', + 'Comma' => 'Komma', + 'Semi-colon' => 'Semikolon', + 'Tab' => 'Tabulator', + 'Vertical bar' => 'senkrechter Strich', + 'Double Quote' => 'Doppelte Anführungszeichen', + 'Single Quote' => 'Einfache Anführungszeichen', + '%s attached a file to the task #%d' => '%s hat eine Datei zur Aufgabe #%d hinzugefügt', + 'There is no column or swimlane activated in your project!' => 'Es ist keine Spalte oder Swimlane in Ihrem Projekt aktiviert!', + 'Append filter (instead of replacement)' => 'Filter anhängen (statt zu ersetzen)', + 'Append/Replace' => 'Anhängen/Ersetzen', + 'Append' => 'Anhängen', + 'Replace' => 'Ersetzen', + 'Import' => 'Import', + 'change sorting' => 'Sortierung ändern', + 'Tasks Importation' => 'Aufgaben Import', + 'Delimiter' => 'Trennzeichen', + 'Enclosure' => 'Anlage', + 'CSV File' => 'CSV Datei', + 'Instructions' => 'Anweisungen', + 'Your file must use the predefined CSV format' => 'Ihre Datei muss das vorgegebene CSV Format haben', + 'Your file must be encoded in UTF-8' => 'Ihre Datei muss UTF-8 kodiert sein', + 'The first row must be the header' => 'Die erste Zeile muss die Kopfzeile sein', + 'Duplicates are not verified for you' => 'Duplikate werden nicht für Sie geprüft', + 'The due date must use the ISO format: YYYY-MM-DD' => 'Das Fälligkeitsdatum muss das ISO Format haben: YYYY-MM-DD', + 'Download CSV template' => 'CSV Vorlage herunterladen', + 'No external integration registered.' => 'Keine externe Integration registriert', + 'Duplicates are not imported' => 'Duplikate wurden nicht importiert', + 'Usernames must be lowercase and unique' => 'Benutzernamen müssen in Kleinbuschstaben und eindeutig sein', + 'Passwords will be encrypted if present' => 'Passwörter werden verschlüsselt wenn vorhanden', + '%s attached a new file to the task %s' => '%s hat eine neue Datei zur Aufgabe %s hinzufgefügt', + 'Assign automatically a category based on a link' => 'Linkbasiert eine Kategorie automatisch zuordnen', + 'BAM - Konvertible Mark' => 'BAM - Konvertible Mark', + 'Assignee Username' => 'Benutzername des Zuständigen', + 'Assignee Name' => 'Name des Zuständigen', + 'Groups' => 'Gruppen', + 'Members of %s' => 'Mitglied von %s', + 'New group' => 'Neue Gruppe', + 'Group created successfully.' => 'Gruppe erfolgreich angelegt.', + 'Unable to create your group.' => 'Gruppe konnte nicht angelegt werden', + 'Edit group' => 'Gruppe bearbeiten', + 'Group updated successfully.' => 'Gruppe erfolgreich aktualisiert', + 'Unable to update your group.' => 'Gruppe konnte nicht aktualisiert werden', + 'Add group member to "%s"' => 'Gruppenmitglied zu "%s" hinzufügen', + 'Group member added successfully.' => 'Gruppenmitglied erfolgreich hinzugefügt', + 'Unable to add group member.' => 'Gruppenmitglied konnte nicht hinzugefügt werden.', + 'Remove user from group "%s"' => 'Benutzer aus Gruppe "%s" löschen', + 'User removed successfully from this group.' => 'Benutzer erfolgreich aus dieser Gruppe gelöscht.', + 'Unable to remove this user from the group.' => 'Benutzer konnte nicht aus dieser Gruppe gelöscht werden.', + 'Remove group' => 'Gruppe löschen', + 'Group removed successfully.' => 'Gruppe erfolgreich gelöscht.', + 'Unable to remove this group.' => 'Gruppe konnte nicht gelöscht werden.', + 'Project Permissions' => 'Projekt Berechtigungen', + 'Manager' => 'Manager', + 'Project Manager' => 'Projekt Manager', + 'Project Member' => 'Projekt Mitglied', + 'Project Viewer' => 'Projekt Betrachter', + 'Your account is locked for %d minutes' => 'Ihr Zugang wurde für %d Minuten gesperrt', + 'Invalid captcha' => 'Ungültiges Captcha', + 'The name must be unique' => 'Der Name muss eindeutig sein', + 'View all groups' => 'Alle Gruppen anzeigen', + 'View group members' => 'Gruppenmitglieder anzeigen', + 'There is no user available.' => 'Es ist kein Benutzer verfügbar.', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Wollen Sie den Benutzer "%s" wirklich aus der Gruppe "%s" löschen?', + 'There is no group.' => 'Es gibt keine Gruppe.', + 'External Id' => 'Externe ID', + 'Add group member' => 'Gruppenmitglied hinzufügen', + 'Do you really want to remove this group: "%s"?' => 'Wollen Sie die Gruppe "%s" wirklich löschen?', + 'There is no user in this group.' => 'Es gibt keinen Benutzer in dieser Gruppe.', + 'Remove this user' => 'Diesen Benutzer löschen', + 'Permissions' => 'Berechtigungen', + 'Allowed Users' => 'Berechtigte Benutzer', + 'No user have been allowed specifically.' => 'Keine Benutzer mit ausdrücklicher Berechtigung.', + 'Role' => 'Rolle', + 'Enter user name...' => 'Geben Sie den Benutzernamem ein...', + 'Allowed Groups' => 'Berechtigte Gruppen', + 'No group have been allowed specifically.' => 'Keine Gruppen mit ausdrücklicher Berechtigung.', + 'Group' => 'Gruppe', + 'Group Name' => 'Gruppenname', + 'Enter group name...' => 'Geben Sie den Gruppennamen ein...', + 'Role:' => 'Rolle:', + 'Project members' => 'Projektmitglieder', + 'Compare hours for "%s"' => 'Vergleich der Stunden für %s', + '%s mentioned you in the task #%d' => '%s erwähnte Sie in Aufgabe #%d', + '%s mentioned you in a comment on the task #%d' => '%s erwähnte Sie in einem Kommentar zur Aufgabe #%d', + 'You were mentioned in the task #%d' => 'Sie wurden in der Aufgabe #%d erwähnt', + 'You were mentioned in a comment on the task #%d' => 'Sie wurden in einem Kommentar zur Aufgabe #%d erwähnt', + 'Mentioned' => 'Erwähnt', + 'Compare Estimated Time vs Actual Time' => 'Vergleich zwischen erwartetem und tatsächlichem Zeitaufwand', + 'Estimated hours: ' => 'Erwarteter Zeitaufwand (Stunden): ', + 'Actual hours: ' => 'Tatsächlich aufgewändete Stunden: ', + 'Hours Spent' => 'Stunden aufgewändet', + 'Hours Estimated' => 'Stunden erwartet', + 'Estimated Time' => 'Erwartete Zeit', + 'Actual Time' => 'Aktuelle Zeit', + 'Estimated vs actual time' => 'Erwarteter vs. tatsächlicher Zeitaufwand', + 'RUB - Russian Ruble' => 'Russischer Rubel', + 'Assign the task to the person who does the action when the column is changed' => 'Aufgabe der Person zuordnen, die die Aktion durchführt, wenn die Spalte geändert wird', + 'Close a task in a specific column' => 'Schliesse eine Aufgabe in einer bestimmten Spalte', + 'Time-based One-time Password Algorithm' => 'Zeitbasierter Einmalpasswort Algorithmus', + 'Two-Factor Provider: ' => '2FA Anbieter: ', + 'Disable two-factor authentication' => 'Zwei-Faktor-Authentifizierung deaktivieren', + 'Enable two-factor authentication' => 'Zwei-Faktor-Authentifizierung aktivieren', + 'There is no integration registered at the moment.' => 'Derzeit ist kein externer Dienst registriert.', + 'Password Reset for Kanboard' => 'Zurücksetzen des Passwortes für Kanboard', + 'Forgot password?' => 'Passwort vergessen?', + 'Enable "Forget Password"' => 'Passwortrücksetzung aktivieren', + 'Password Reset' => 'Passwort zurücksetzen', + 'New password' => 'Neues Passwort', + 'Change Password' => 'Passwort ändern', + 'To reset your password click on this link:' => 'Bitte auf den Link klicken, um Ihr Passwort zurückzusetzen.', + 'Last Password Reset' => 'Verlauf der Passwortrücksetzung', + 'The password has never been reinitialized.' => 'Das Passwort wurde noch nie zurückgesetzt.', + 'Creation' => 'Erstellung', + 'Expiration' => 'Ablauf', + 'Password reset history' => 'Verlauf Passwortrücksetzung', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/es_ES/translations.php b/sources/app/Locale/es_ES/translations.php index 02c6961..9248943 100644 --- a/sources/app/Locale/es_ES/translations.php +++ b/sources/app/Locale/es_ES/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Complejidad', 'Task limit' => 'Número máximo de tareas', 'Task count' => 'Contador de tareas', - 'Edit project access list' => 'Editar los permisos del proyecto', - 'Allow this user' => 'Autorizar a este usuario', - 'Don\'t forget that administrators have access to everything.' => 'No olvide que los administradores tienen acceso a todo.', - 'Revoke' => 'Revocar', - 'List of authorized users' => 'Lista de los usuarios autorizados', 'User' => 'Usuario', - 'Nobody have access to this project.' => 'Nadie tiene acceso a este proyecto', 'Comments' => 'Comentarios', 'Write your text in Markdown' => 'Redacta el texto en Markdown', 'Leave a comment' => 'Dejar un comentario', @@ -396,8 +390,6 @@ return array( 'Email:' => 'Correo electrónico:', 'Notifications:' => 'Notificaciones:', 'Notifications' => 'Notificaciones', - 'Group:' => 'Grupo:', - 'Regular user' => 'Usuario regular', 'Account type:' => 'Tipo de Cuenta:', 'Edit profile' => 'Editar perfil', 'Change password' => 'Cambiar contraseña', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s cambió el concesionario de la tarea %s por %s', 'New password for the user "%s"' => 'Nueva contraseña para el usuario "%s"', 'Choose an event' => 'Seleccione un evento', - 'Github commit received' => 'Envío a Github recibido', - 'Github issue opened' => 'Abierto asunto en Github', - 'Github issue closed' => 'Cerrado asunto en Github', - 'Github issue reopened' => 'Reabierto asunto en Github', - 'Github issue assignee change' => 'Cambio en concesionario de asunto de Github', - 'Github issue label change' => 'Cambio en etiqueta de asunto de Github', 'Create a task from an external provider' => 'Crear una tarea a partir de un proveedor externo', 'Change the assignee based on an external username' => 'Cambiar el concesionario basado en un nombre de usuario externo', 'Change the category based on an external label' => 'Cambiar la categoría basado en una etiqueta externa', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Cualquiera tiene acceso a este proyecto', 'Webhooks' => 'Disparadores Web (Webhooks)', 'API' => 'API', - 'Github webhooks' => 'Disparadores Web (Webhooks) de Github', - 'Help on Github webhooks' => 'Ayuda con los Disparadores Web (Webhook) de Github', 'Create a comment from an external provider' => 'Crear un comentario a partir de un proveedor externo', - 'Github issue comment created' => 'Creado el comentario del problema en Github', 'Project management' => 'Administración del proyecto', 'My projects' => 'Mis proyectos', 'Columns' => 'Columnas', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Repartición para "%s"', 'Clone this project' => 'Clonar este proyecto', 'Column removed successfully.' => 'Columna eliminada correctamente', - 'Github Issue' => 'Problema con Github', 'Not enough data to show the graph.' => 'No hay suficiente información para mostrar el gráfico.', 'Previous' => 'Anterior', 'The id must be an integer' => 'El id debe ser un entero', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Calle por defecto', 'Do you really want to remove this swimlane: "%s"?' => '¿Realmente quiere quitar esta calle: "%s"?', 'Inactive swimlanes' => 'Calles inactivas', - 'Set project manager' => 'Asignar administrador del proyecto', - 'Set project member' => 'Asignar miembro del proyecto', 'Remove a swimlane' => 'Quitar un calle', 'Rename' => 'Renombrar', 'Show default swimlane' => 'Mostrar calle por defecto', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Su calle ha sido creada correctamente', 'Example: "Bug, Feature Request, Improvement"' => 'Ejemplo: "Error, Solicitud de característica, Mejora', 'Default categories for new projects (Comma-separated)' => 'Categorías por defecto para nuevos proyectos (separadas por comas)', - 'Gitlab commit received' => 'Recibido envío desde Gitlab', - 'Gitlab issue opened' => 'Abierto asunto de Gitlab', - 'Gitlab issue closed' => 'Cerrado asunto de Gitlab', - 'Gitlab webhooks' => 'Disparadores Web (Webhooks) de Gitlab', - 'Help on Gitlab webhooks' => 'Ayuda sobre Disparadores Web (Webhooks) de Gitlab', 'Integrations' => 'Integraciones', 'Integration with third-party services' => 'Integración con servicios de terceros', - 'Role for this project' => 'Papel de este proyecto', - 'Project manager' => 'Administrador de proyecto', - 'Project member' => 'Miembro de proyecto', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Un administrador de proyecto puede cambiar sus valores y tener más privilegios que un usuario estándar.', - 'Gitlab Issue' => 'Asunto Gitlab', 'Subtask Id' => 'Id de Subtarea', 'Subtasks' => 'Subtareas', 'Subtasks Export' => 'Exportación de Subtareas', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Ya dispones de una subtarea en progreso', 'Which parts of the project do you want to duplicate?' => '¿Qué partes del proyecto desea duplicar?', 'Disallow login form' => 'Deshabilitar formulario de ingreso', - 'Bitbucket commit received' => 'Recibido envío desde Bitbucket', - 'Bitbucket webhooks' => 'Disparadores Web (webhooks) de Bitbucket', - 'Help on Bitbucket webhooks' => 'Ayuda sobre disparadores web (webhooks) de Bitbucket', 'Start' => 'Inicio', 'End' => 'Fin', 'Task age in days' => 'Edad de la tarea en días', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'El código de autenticación de dos factores es válido', 'Code' => 'Código', 'Two factor authentication' => 'Autenticación de dos factores', - 'Enable/disable two factor authentication' => 'Activar/desactivar autenticación de dos factores', 'This QR code contains the key URI: ' => 'Este código QR contiene la clave URI: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Guarda la clave secreta en su software TOTP (por ejemplo Autenticación de Google o FreeOTP).', 'Check my code' => 'Revisar mi código', 'Secret key: ' => 'Clave secreta: ', 'Test your device' => 'Probar su dispositivo', @@ -776,21 +741,10 @@ return array( 'User that will receive the email' => 'Usuario que recibirá el correo', 'Email subject' => 'Asunto del correo', 'Date' => 'Fecha', - 'By @%s on Bitbucket' => 'Mediante @%s en Bitbucket', - 'Bitbucket Issue' => 'Asunto de Bitbucket', - 'Commit made by @%s on Bitbucket' => 'Envío realizado por @%s en Bitbucket', - 'Commit made by @%s on Github' => 'Envío realizado por @%s en Github', - 'By @%s on Github' => 'Por @%s en Github', - 'Commit made by @%s on Gitlab' => 'Envío realizado por @%s en Gitlab', 'Add a comment log when moving the task between columns' => 'Añadir un comentario al mover la tarea entre columnas', 'Move the task to another column when the category is changed' => 'Mover la tarea a otra columna cuando cambia la categoría', 'Send a task by email to someone' => 'Enviar una tarea a alguien por correo', 'Reopen a task' => 'Reabrir tarea', - 'Bitbucket issue opened' => 'Abierto asunto de Bitbucket', - 'Bitbucket issue closed' => 'Cerrado asunto de Bitbucket', - 'Bitbucket issue reopened' => 'Reabierto asunto de Bitbucket', - 'Bitbucket issue assignee change' => 'Cambiado concesionario de asunto de Bitbucket', - 'Bitbucket issue comment created' => 'Creado comentario de asunto de Bitbucket', 'Column change' => 'Cambio de columna', 'Position change' => 'Cambio de posición', 'Swimlane change' => 'Cambio de calle', @@ -910,8 +864,6 @@ return array( 'Remote user' => 'Usuario remoto', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Los usuarios remotos no almacenan sus contraseñas en la base de datos Kanboard, por ejemplo: cuentas de LDAP, Google y Github', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si marcas la caja de edición "Desactivar formulario de ingreso", se ignoran las credenciales entradas en el formulario de ingreso.', - 'By @%s on Gitlab' => 'Por @%s en Gitlab', - 'Gitlab issue comment created' => 'Creado comentario de asunto de Gitlab', 'New remote user' => 'Nuevo usuario remoto', 'New local user' => 'Nuevo usuario local', 'Default task color' => 'Color por defecto de tarea', @@ -931,7 +883,6 @@ return array( 'contributors' => 'contribuyentes', 'License:' => 'Licencia:', 'License' => 'Licencia', - 'Project Administrator' => 'Administrador del Proyecto', 'Enter the text below' => 'Digita el texto de abajo', 'Gantt chart for %s' => 'Diagrama de Gantt para %s', 'Sort by position' => 'Clasificado mediante posición', @@ -955,7 +906,6 @@ return array( 'Members' => 'Miembros', 'Shared project' => 'Proyecto compartido', 'Project managers' => 'Administradores de proyecto', - 'Project members' => 'Miembros de proyecto', 'Gantt chart for all projects' => 'Diagrama de Gantt para todos los proyectos', 'Projects list' => 'Lista de proyectos', 'Gantt chart for this project' => 'Diagrama de Gantt para este proyecto', @@ -982,7 +932,6 @@ return array( 'Documentation' => 'Documentación', 'Table of contents' => 'Tabla de contenido', 'Gantt' => 'Gantt', - 'Help with project permissions' => 'Ayuda con permisos del proyecto', 'Author' => 'Autor', 'Version' => 'Versión', 'Plugins' => 'Plugins', @@ -1045,7 +994,6 @@ return array( 'Append/Replace' => 'Añadir/Reemplazar', 'Append' => 'Añadir', 'Replace' => 'Reemplazar', - 'There is no notification method registered.' => 'No hay método de notificación registrado', 'Import' => 'Importar', 'change sorting' => 'Cambiar orden', 'Tasks Importation' => 'Importación de tareas', @@ -1065,7 +1013,95 @@ return array( 'Passwords will be encrypted if present' => 'Las contraseñas serán cifradas si es que existen', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + 'Project members' => 'Miembros de proyecto', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + 'Estimated vs actual time' => 'Tiempo estimado vs real', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/fi_FI/translations.php b/sources/app/Locale/fi_FI/translations.php index bff0f05..18553ec 100644 --- a/sources/app/Locale/fi_FI/translations.php +++ b/sources/app/Locale/fi_FI/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Monimutkaisuus', 'Task limit' => 'Tehtävien maksimimäärä', 'Task count' => 'Tehtävien määrä', - 'Edit project access list' => 'Muuta projektin käyttäjiä', - 'Allow this user' => 'Salli tämä projekti', - 'Don\'t forget that administrators have access to everything.' => 'Muista että ylläpitäjät pääsevät kaikkialle.', - 'Revoke' => 'Poista', - 'List of authorized users' => 'Sallittujen käyttäjien lista', 'User' => 'Käyttäjät', - // 'Nobody have access to this project.' => '', 'Comments' => 'Kommentit', 'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla', 'Leave a comment' => 'Lisää kommentti', @@ -396,8 +390,6 @@ return array( 'Email:' => 'Sähköpostiosoite:', 'Notifications:' => 'Ilmoitukset:', 'Notifications' => 'Ilmoitukset', - 'Group:' => 'Ryhmä:', - 'Regular user' => 'Peruskäyttäjä', 'Account type:' => 'Tilin tyyppi:', 'Edit profile' => 'Muokkaa profiilia', 'Change password' => 'Vaihda salasana', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s vaihtoi tehtävän %s saajaksi %s', 'New password for the user "%s"' => 'Uusi salasana käyttäjälle "%s"', 'Choose an event' => 'Valitse toiminta', - 'Github commit received' => 'Github-kommitti vastaanotettu', - 'Github issue opened' => 'Github-issue avattu', - 'Github issue closed' => 'Github-issue suljettu', - 'Github issue reopened' => 'Github-issue uudelleenavattu', - 'Github issue assignee change' => 'Github-issuen saajan vaihto', - 'Github issue label change' => 'Github-issuen labelin vaihto', 'Create a task from an external provider' => 'Luo tehtävä ulkoiselta tarjoajalta', 'Change the assignee based on an external username' => 'Vaihda tehtävän saajaa perustuen ulkoiseen käyttäjänimeen', 'Change the category based on an external label' => 'Vaihda kategoriaa perustuen ulkoiseen labeliin', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Kaikilla on käyttöoikeus projektiin.', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', - // 'Github issue comment created' => '', 'Project management' => 'Projektin hallinta', 'My projects' => 'Minun projektini', 'Columns' => 'Sarakkeet', @@ -516,7 +499,6 @@ return array( // 'User repartition for "%s"' => '', 'Clone this project' => 'Kahdenna projekti', 'Column removed successfully.' => 'Sarake poistettu onnstuneesti.', - 'Github Issue' => 'Github-issue', 'Not enough data to show the graph.' => 'Ei riittävästi dataa graafin näyttämiseksi.', 'Previous' => 'Edellinen', 'The id must be an integer' => 'ID:n on oltava kokonaisluku', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Oletuskaista', 'Do you really want to remove this swimlane: "%s"?' => 'Haluatko varmasti poistaa tämän kaistan: "%s"?', 'Inactive swimlanes' => 'Passiiviset kaistat', - // 'Set project manager' => '', - // 'Set project member' => '', 'Remove a swimlane' => 'Poista kaista', 'Rename' => 'Uudelleennimeä', 'Show default swimlane' => 'Näytä oletuskaista', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Kaista luotu onnistuneesti.', 'Example: "Bug, Feature Request, Improvement"' => 'Esimerkiksi: "Bugit, Ominaisuuspyynnöt, Parannukset"', 'Default categories for new projects (Comma-separated)' => 'Oletuskategoriat uusille projekteille (pilkuin eroteltu)', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', // 'Integrations' => '', // 'Integration with third-party services' => '', - // 'Role for this project' => '', - // 'Project manager' => '', - // 'Project member' => '', - // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '', - // 'Gitlab Issue' => '', // 'Subtask Id' => '', // 'Subtasks' => '', // 'Subtasks Export' => '', @@ -602,9 +572,6 @@ return array( // 'You already have one subtask in progress' => '', // 'Which parts of the project do you want to duplicate?' => '', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', // 'Start' => '', // 'End' => '', // 'Task age in days' => '', @@ -702,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -776,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -910,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -931,7 +883,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +906,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/fr_FR/translations.php b/sources/app/Locale/fr_FR/translations.php index 36ecf2e..26490b1 100644 --- a/sources/app/Locale/fr_FR/translations.php +++ b/sources/app/Locale/fr_FR/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Complexité', 'Task limit' => 'Tâches Max.', 'Task count' => 'Nombre de tâches', - 'Edit project access list' => 'Modifier l\'accès au projet', - 'Allow this user' => 'Autoriser cet utilisateur', - 'Don\'t forget that administrators have access to everything.' => 'N\'oubliez pas que les administrateurs ont accès à tout.', - 'Revoke' => 'Révoquer', - 'List of authorized users' => 'Liste des utilisateurs autorisés', 'User' => 'Utilisateur', - 'Nobody have access to this project.' => 'Personne n\'est autorisé à accéder au projet.', 'Comments' => 'Commentaires', 'Write your text in Markdown' => 'Écrivez votre texte en Markdown', 'Leave a comment' => 'Laissez un commentaire', @@ -398,8 +392,6 @@ return array( 'Email:' => 'Email :', 'Notifications:' => 'Notifications :', 'Notifications' => 'Notifications', - 'Group:' => 'Groupe :', - 'Regular user' => 'Utilisateur normal', 'Account type:' => 'Type de compte :', 'Edit profile' => 'Modifier le profil', 'Change password' => 'Changer le mot de passe', @@ -447,12 +439,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s a changé la personne assignée à la tâche %s pour %s', 'New password for the user "%s"' => 'Nouveau mot de passe pour l\'utilisateur « %s »', 'Choose an event' => 'Choisir un événement', - 'Github commit received' => 'Commit reçu via Github', - 'Github issue opened' => 'Ouverture d\'un ticket sur Github', - 'Github issue closed' => 'Fermeture d\'un ticket sur Github', - 'Github issue reopened' => 'Réouverture d\'un ticket sur Github', - 'Github issue assignee change' => 'Changement d\'assigné sur un ticket Github', - 'Github issue label change' => 'Changement de libellé sur un ticket Github', 'Create a task from an external provider' => 'Créer une tâche depuis un fournisseur externe', 'Change the assignee based on an external username' => 'Changer l\'assigné en fonction d\'un utilisateur externe', 'Change the category based on an external label' => 'Changer la catégorie en fonction d\'un libellé externe', @@ -483,7 +469,7 @@ return array( 'This project is private' => 'Ce projet est privé', 'Type here to create a new sub-task' => 'Créer une sous-tâche en écrivant le titre ici', 'Add' => 'Ajouter', - 'Estimated time: %s hours' => 'Temps estimé: %s hours', + 'Estimated time: %s hours' => 'Temps estimé: %s heures', 'Time spent: %s hours' => 'Temps passé : %s heures', 'Started on %B %e, %Y' => 'Commençé le %d/%m/%Y', 'Start date' => 'Date de début', @@ -494,13 +480,10 @@ return array( 'Dashboard' => 'Tableau de bord', 'Confirmation' => 'Confirmation', 'Allow everybody to access to this project' => 'Autoriser tout le monde à accéder à ce projet', - 'Everybody have access to this project.' => 'Tout le monde a acccès à ce projet.', + 'Everybody have access to this project.' => 'Tout le monde a accès à ce projet.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Webhook Github', - 'Help on Github webhooks' => 'Aide sur les webhooks Github', 'Create a comment from an external provider' => 'Créer un commentaire depuis un fournisseur externe', - 'Github issue comment created' => 'Commentaire créé sur un ticket Github', 'Project management' => 'Gestion des projets', 'My projects' => 'Mes projets', 'Columns' => 'Colonnes', @@ -518,7 +501,6 @@ return array( 'User repartition for "%s"' => 'Répartition des utilisateurs pour « %s »', 'Clone this project' => 'Cloner ce projet', 'Column removed successfully.' => 'Colonne supprimée avec succès.', - 'Github Issue' => 'Ticket Github', 'Not enough data to show the graph.' => 'Pas assez de données pour afficher le graphique.', 'Previous' => 'Précédent', 'The id must be an integer' => 'L\'id doit être un entier', @@ -548,8 +530,6 @@ return array( 'Default swimlane' => 'Swimlane par défaut', 'Do you really want to remove this swimlane: "%s"?' => 'Voulez-vous vraiment supprimer cette swimlane : « %s » ?', 'Inactive swimlanes' => 'Swimlanes inactives', - 'Set project manager' => 'Mettre chef de projet', - 'Set project member' => 'Mettre membre du projet', 'Remove a swimlane' => 'Supprimer une swimlane', 'Rename' => 'Renommer', 'Show default swimlane' => 'Afficher la swimlane par défaut', @@ -565,18 +545,8 @@ return array( 'Your swimlane have been created successfully.' => 'Votre swimlane a été créée avec succès.', 'Example: "Bug, Feature Request, Improvement"' => 'Exemple: « Incident, Demande de fonctionnalité, Amélioration »', 'Default categories for new projects (Comma-separated)' => 'Catégories par défaut pour les nouveaux projets (séparation par des virgules)', - 'Gitlab commit received' => 'Commit reçu via Gitlab', - 'Gitlab issue opened' => 'Ouverture d\'un ticket sur Gitlab', - 'Gitlab issue closed' => 'Fermeture d\'un ticket sur Gitlab', - 'Gitlab webhooks' => 'Webhook Gitlab', - 'Help on Gitlab webhooks' => 'Aide sur les webhooks Gitlab', 'Integrations' => 'Intégrations', 'Integration with third-party services' => 'Intégration avec des services externes', - 'Role for this project' => 'Rôle pour ce projet', - 'Project manager' => 'Chef de projet', - 'Project member' => 'Membre du projet', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Un chef de projet peut changer les paramètres du projet et possède plus de privilèges qu\'un utilisateur standard.', - 'Gitlab Issue' => 'Ticket Gitlab', 'Subtask Id' => 'Identifiant de la sous-tâche', 'Subtasks' => 'Sous-tâches', 'Subtasks Export' => 'Exportation des sous-tâches', @@ -604,9 +574,6 @@ return array( 'You already have one subtask in progress' => 'Vous avez déjà une sous-tâche en progrès', 'Which parts of the project do you want to duplicate?' => 'Quelles parties du projet voulez-vous dupliquer ?', 'Disallow login form' => 'Interdire le formulaire d\'authentification', - 'Bitbucket commit received' => 'Commit reçu via Bitbucket', - 'Bitbucket webhooks' => 'Webhook Bitbucket', - 'Help on Bitbucket webhooks' => 'Aide sur les webhooks Bitbucket', 'Start' => 'Début', 'End' => 'Fin', 'Task age in days' => 'Âge de la tâche en jours', @@ -704,9 +671,7 @@ return array( 'The two factor authentication code is valid.' => 'Le code pour l\'authentification à deux-facteurs est valide.', 'Code' => 'Code', 'Two factor authentication' => 'Authentification à deux-facteurs', - 'Enable/disable two factor authentication' => 'Activer/désactiver l\'authentification à deux-facteurs', 'This QR code contains the key URI: ' => 'Ce code QR contient l\'url de la clé : ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Sauvegardez cette clé secrète dans votre logiciel TOTP (par exemple Google Authenticator ou FreeOTP).', 'Check my code' => 'Vérifier mon code', 'Secret key: ' => 'Clé secrète : ', 'Test your device' => 'Testez votre appareil', @@ -728,7 +693,7 @@ return array( 'Disable two factor authentication' => 'Désactiver l\'authentification à deux facteurs', 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Voulez-vous vraiment désactiver l\'authentification à deux facteurs pour cet utilisateur : « %s » ?', 'Edit link' => 'Modifier un lien', - 'Start to type task title...' => 'Entrez le titre de la tâche…', + 'Start to type task title...' => 'Entrez le titre de la tâche...', 'A task cannot be linked to itself' => 'Une tâche ne peut être liée à elle-même', 'The exact same link already exists' => 'Un lien identique existe déjà', 'Recurrent task is scheduled to be generated' => 'La tâche récurrente est programmée pour être créée', @@ -778,21 +743,10 @@ return array( 'User that will receive the email' => 'Utilisateur qui va reçevoir l\'email', 'Email subject' => 'Sujet de l\'email', 'Date' => 'Date', - 'By @%s on Bitbucket' => 'Par @%s sur Bitbucket', - 'Bitbucket Issue' => 'Ticket Bitbucket', - 'Commit made by @%s on Bitbucket' => 'Commit fait par @%s sur Bitbucket', - 'Commit made by @%s on Github' => 'Commit fait par @%s sur Github', - 'By @%s on Github' => 'Par @%s sur Github', - 'Commit made by @%s on Gitlab' => 'Commit fait par @%s sur Gitlab', 'Add a comment log when moving the task between columns' => 'Ajouter un commentaire d\'information lorsque une tâche est déplacée dans une autre colonne', 'Move the task to another column when the category is changed' => 'Déplacer une tâche vers une autre colonne lorsque la catégorie a changé', 'Send a task by email to someone' => 'Envoyer une tâche par email à quelqu\'un', 'Reopen a task' => 'Rouvrir une tâche', - 'Bitbucket issue opened' => 'Ticket Bitbucket ouvert', - 'Bitbucket issue closed' => 'Ticket Bitbucket fermé', - 'Bitbucket issue reopened' => 'Ticket Bitbucket rouvert', - 'Bitbucket issue assignee change' => 'Changement d\'assigné sur un ticket Bitbucket', - 'Bitbucket issue comment created' => 'Commentaire créé sur un ticket Bitbucket', 'Column change' => 'Changement de colonne', 'Position change' => 'Changement de position', 'Swimlane change' => 'Changement de swimlane', @@ -912,8 +866,6 @@ return array( 'Remote user' => 'Utilisateur distant', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Les utilisateurs distants ne stockent pas leur mot de passe dans la base de données de Kanboard, exemples : comptes LDAP, Github ou Google.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si vous cochez la case « Interdire le formulaire d\'authentification », les identifiants entrés dans le formulaire d\'authentification seront ignorés.', - 'By @%s on Gitlab' => 'Par @%s sur Gitlab', - 'Gitlab issue comment created' => 'Commentaire créé sur un ticket Gitlab', 'New remote user' => 'Créer un utilisateur distant', 'New local user' => 'Créer un utilisateur local', 'Default task color' => 'Couleur par défaut des tâches', @@ -933,7 +885,6 @@ return array( 'contributors' => 'contributeurs', 'License:' => 'Licence :', 'License' => 'Licence', - 'Project Administrator' => 'Administrateur de projet', 'Enter the text below' => 'Entrez le texte ci-dessous', 'Gantt chart for %s' => 'Diagramme de Gantt pour %s', 'Sort by position' => 'Trier par position', @@ -957,7 +908,6 @@ return array( 'Members' => 'Membres', 'Shared project' => 'Projet partagé', 'Project managers' => 'Gestionnaires de projet', - 'Project members' => 'Membres de projet', 'Gantt chart for all projects' => 'Diagramme de Gantt pour tous les projets', 'Projects list' => 'Liste des projets', 'Gantt chart for this project' => 'Diagramme de Gantt pour ce projet', @@ -984,7 +934,6 @@ return array( 'Documentation' => 'Documentation', 'Table of contents' => 'Table des matières', 'Gantt' => 'Gantt', - 'Help with project permissions' => 'Aide avec les permissions des projets', 'Author' => 'Auteur', 'Version' => 'Version', 'Plugins' => 'Extensions', @@ -1047,7 +996,6 @@ return array( 'Append/Replace' => 'Ajouter/Remplaçer', 'Append' => 'Ajouter', 'Replace' => 'Remplaçer', - 'There is no notification method registered.' => 'Il n\'y a aucune méthode de notification enregistrée.', 'Import' => 'Importation', 'change sorting' => 'changer l\'ordre', 'Tasks Importation' => 'Importation des tâches', @@ -1068,7 +1016,95 @@ return array( '%s attached a new file to the task %s' => '%s a attaché un nouveau fichier à la tâche %s', 'Link type' => 'Type de lien', 'Assign automatically a category based on a link' => 'Assigner automatiquement une catégorie en fonction d\'un lien', - 'BAM - Konvertibile Mark' => 'BAM - Mark convertible', + 'BAM - Konvertible Mark' => 'BAM - Mark convertible', 'Assignee Username' => 'Utilisateur assigné', 'Assignee Name' => 'Nom de l\'assigné', + 'Groups' => 'Groupes', + 'Members of %s' => 'Membres de %s', + 'New group' => 'Nouveau groupe', + 'Group created successfully.' => 'Groupe créé avec succès.', + 'Unable to create your group.' => 'Impossible de créé votre groupe.', + 'Edit group' => 'Modifier le groupe', + 'Group updated successfully.' => 'Groupe mis à jour avec succès.', + 'Unable to update your group.' => 'Impossible de mettre à jour votre groupe.', + 'Add group member to "%s"' => 'Ajouter un membre au groupe « %s »', + 'Group member added successfully.' => 'Membre ajouté avec succès au groupe.', + 'Unable to add group member.' => 'Impossible d\'ajouter ce membre au groupe.', + 'Remove user from group "%s"' => 'Supprimer un utilisateur d\'un groupe « %s »', + 'User removed successfully from this group.' => 'Utilisateur supprimé avec succès de ce groupe.', + 'Unable to remove this user from the group.' => 'Impossible de supprimer cet utilisateur du groupe.', + 'Remove group' => 'Supprimer le groupe', + 'Group removed successfully.' => 'Groupe supprimé avec succès.', + 'Unable to remove this group.' => 'Impossible de supprimer ce groupe.', + 'Project Permissions' => 'Permissions du projet', + 'Manager' => 'Gestionnaire', + 'Project Manager' => 'Chef de projet', + 'Project Member' => 'Membre du projet', + 'Project Viewer' => 'Visualiseur de projet', + 'Your account is locked for %d minutes' => 'Votre compte est vérouillé pour %d minutes', + 'Invalid captcha' => 'Captcha invalid', + 'The name must be unique' => 'Le nom doit être unique', + 'View all groups' => 'Voir tous les groupes', + 'View group members' => 'Voir les membres du groupe', + 'There is no user available.' => 'Il n\'y a aucun utilisateur disponible', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Voulez-vous vraiment supprimer l\'utilisateur « %s » du groupe « %s » ?', + 'There is no group.' => 'Il n\'y a aucun groupe.', + 'External Id' => 'Identifiant externe', + 'Add group member' => 'Ajouter un membre au groupe', + 'Do you really want to remove this group: "%s"?' => 'Voulez-vous vraiment supprimer ce groupe : « %s » ?', + 'There is no user in this group.' => 'Il n\'y a aucun utilisateur dans ce groupe', + 'Remove this user' => 'Supprimer cet utilisateur', + 'Permissions' => 'Permissions', + 'Allowed Users' => 'Utilisateurs autorisés', + 'No user have been allowed specifically.' => 'Aucun utilisateur a été autorisé spécifiquement.', + 'Role' => 'Rôle', + 'Enter user name...' => 'Entrez le nom de l\'utilisateur...', + 'Allowed Groups' => 'Groupes autorisés', + 'No group have been allowed specifically.' => 'Aucun groupe a été autorisé spécifiquement.', + 'Group' => 'Groupe', + 'Group Name' => 'Nom du groupe', + 'Enter group name...' => 'Entrez le nom du groupe...', + 'Role:' => 'Rôle :', + 'Project members' => 'Membres du projet', + 'Compare hours for "%s"' => 'Comparer les heures pour « %s »', + '%s mentioned you in the task #%d' => '%s vous a mentionné dans la tâche n°%d', + '%s mentioned you in a comment on the task #%d' => '%s vous a mentionné dans un commentaire de la tâche n°%d', + 'You were mentioned in the task #%d' => 'Vous avez été mentionné dans la tâche n°%d', + 'You were mentioned in a comment on the task #%d' => 'Vous avez été mentionné dans un commentaire de la tâche n°%d', + 'Mentioned' => 'Mentionné', + 'Compare Estimated Time vs Actual Time' => 'Comparer le temps estimé et le temps actuel', + 'Estimated hours: ' => 'Heures estimées : ', + 'Actual hours: ' => 'Heures actuelles : ', + 'Hours Spent' => 'Heures passées', + 'Hours Estimated' => 'Heures estimées', + 'Estimated Time' => 'Temps estimé', + 'Actual Time' => 'Temps actuel', + 'Estimated vs actual time' => 'Temps estimé vs actuel', + 'RUB - Russian Ruble' => 'RUB - Rouble russe', + 'Assign the task to the person who does the action when the column is changed' => 'Assigner la tâche à la personne qui fait l\'action lorsque la colonne est changée', + 'Close a task in a specific column' => 'Fermer une tâche dans une colonne specifique', + 'Time-based One-time Password Algorithm' => 'Mot de passe à usage unique basé sur le temps', + 'Two-Factor Provider: ' => 'Fournisseur d\'authentification à deux facteurs : ', + 'Disable two-factor authentication' => 'Désactiver l\'authentification à deux-facteurs', + 'Enable two-factor authentication' => 'Activer l\'authentification à deux-facteurs', + 'There is no integration registered at the moment.' => 'Il n\'y a aucune intégration enregistrée pour le moment.', + 'Password Reset for Kanboard' => 'Réinitialisation du mot de passe pour Kanboard', + 'Forgot password?' => 'Mot de passe oublié ?', + 'Enable "Forget Password"' => 'Activer la fonctionnalité « Mot de passe oublié »', + 'Password Reset' => 'Réinitialisation du mot de passe', + 'New password' => 'Nouveau mot de passe', + 'Change Password' => 'Changer de mot de passe', + 'To reset your password click on this link:' => 'Pour réinitialiser votre mot de passe cliquer sur ce lien :', + 'Last Password Reset' => 'Dernières réinitialisation de mot de passe', + 'The password has never been reinitialized.' => 'Le mot de passe n\'a jamais été réinitialisé.', + 'Creation' => 'Création', + 'Expiration' => 'Expiration', + 'Password reset history' => 'Historique de la réinitialisation du mot de passe', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Toutes les tâches de la colonne « %s » et de la swimlane « %s » ont été fermées avec succès.', + 'Do you really want to close all tasks of this column?' => 'Voulez-vous vraiment fermer toutes les tâches de cette colonne ?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tâche(s) dans la colonne « %s » et la swimlane « %s » seront fermées.', + 'Close all tasks of this column' => 'Fermer toutes les tâches de cette colonne', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Aucun plugin n\'a enregistré une méthode de notification de projet. Vous pouvez toujours configurer les notifications individuelles dans votre profil d\'utilisateur.', + 'My dashboard' => 'Mon tableau de bord', + 'My profile' => 'Mon profile', ); diff --git a/sources/app/Locale/hu_HU/translations.php b/sources/app/Locale/hu_HU/translations.php index 1a34a5b..8f93d1c 100644 --- a/sources/app/Locale/hu_HU/translations.php +++ b/sources/app/Locale/hu_HU/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Bonyolultság', 'Task limit' => 'Maximális számú feladat', 'Task count' => 'Feladatok száma', - 'Edit project access list' => 'Projekt hozzáférés módosítása', - 'Allow this user' => 'Engedélyezi ezt a felhasználót', - 'Don\'t forget that administrators have access to everything.' => 'Ne felejtsük el: a rendszergazdák mindenhez hozzáférnek.', - 'Revoke' => 'Visszavon', - 'List of authorized users' => 'Az engedélyezett felhasználók', 'User' => 'Felhasználó', - 'Nobody have access to this project.' => 'Senkinek sincs hozzáférése a projekthez.', 'Comments' => 'Hozzászólások', 'Write your text in Markdown' => 'Írja be a szöveget Markdown szintaxissal', 'Leave a comment' => 'Írjon hozzászólást ...', @@ -396,8 +390,6 @@ return array( 'Email:' => 'E-mail:', 'Notifications:' => 'Értesítések:', 'Notifications' => 'Értesítések', - 'Group:' => 'Csoport:', - 'Regular user' => 'Default User', 'Account type:' => 'Fiók típusa:', 'Edit profile' => 'Profil szerkesztése', 'Change password' => 'Jelszó módosítása', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s a felelőst %s módosította: %s', 'New password for the user "%s"' => 'Felhasználó új jelszava: %s', 'Choose an event' => 'Válasszon eseményt', - 'Github commit received' => 'Github commit érkezett', - 'Github issue opened' => 'Github issue nyitás', - 'Github issue closed' => 'Github issue zárás', - 'Github issue reopened' => 'Github issue újranyitva', - 'Github issue assignee change' => 'Github issue felelős változás', - 'Github issue label change' => 'Github issue címke változás', 'Create a task from an external provider' => 'Feladat létrehozása külsős számára', 'Change the assignee based on an external username' => 'Felelős módosítása külső felhasználónév alapján', 'Change the category based on an external label' => 'Kategória módosítása külső címke alapján', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Mindenki elérheti a projektet', 'Webhooks' => 'Webhook', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Github Webhook súgó', 'Create a comment from an external provider' => 'Megjegyzés létrehozása külső felhasználótól', - 'Github issue comment created' => 'Github issue megjegyzés létrehozva', 'Project management' => 'Projekt menedzsment', 'My projects' => 'Projektjeim', 'Columns' => 'Oszlopok', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Felhasználó újrafelosztás: %s', 'Clone this project' => 'Projekt másolása', 'Column removed successfully.' => 'Oszlop sikeresen törölve.', - 'Github Issue' => 'Github issue', 'Not enough data to show the graph.' => 'Nincs elég adat a grafikonhoz.', 'Previous' => 'Előző', 'The id must be an integer' => 'Az ID csak egész szám lehet', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Alapértelmezett folyamat', 'Do you really want to remove this swimlane: "%s"?' => 'Valóban törli a folyamatot:%s ?', 'Inactive swimlanes' => 'Inaktív folyamatok', - 'Set project manager' => 'Beállítás projekt kezelőnek', - 'Set project member' => 'Beállítás projekt felhasználónak', 'Remove a swimlane' => 'Folyamat törlés', 'Rename' => 'Átnevezés', 'Show default swimlane' => 'Alapértelmezett folyamat megjelenítése', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'A folyamat sikeresen létrehozva.', 'Example: "Bug, Feature Request, Improvement"' => 'Például: Hiba, Új funkció, Fejlesztés', 'Default categories for new projects (Comma-separated)' => 'Alapértelmezett kategóriák az új projektekben (Vesszővel elválasztva)', - 'Gitlab commit received' => 'Gitlab commit érkezett', - 'Gitlab issue opened' => 'Gitlab issue nyitás', - 'Gitlab issue closed' => 'Gitlab issue zárás', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Gitlab webhooks súgó', 'Integrations' => 'Integráció', 'Integration with third-party services' => 'Integráció harmadik féllel', - 'Role for this project' => 'Projekt szerepkör', - 'Project manager' => 'Projekt kezelő', - 'Project member' => 'Projekt felhasználó', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'A projekt kezelő képes megváltoztatni a projekt beállításait és több joggal rendelkezik mint az alap felhasználók.', - 'Gitlab Issue' => 'Gitlab issue', 'Subtask Id' => 'Részfeladat id', 'Subtasks' => 'Részfeladatok', 'Subtasks Export' => 'Részfeladat exportálás', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Már van egy folyamatban levő részfeladata', 'Which parts of the project do you want to duplicate?' => 'A projekt mely részeit szeretné másolni?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket commit érkezett', - 'Bitbucket webhooks' => 'Bitbucket webhooks', - 'Help on Bitbucket webhooks' => 'Bitbucket webhooks súgó', 'Start' => 'Kezdet', 'End' => 'Vég', 'Task age in days' => 'Feladat életkora napokban', @@ -702,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -776,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -910,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -931,7 +883,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +906,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/id_ID/translations.php b/sources/app/Locale/id_ID/translations.php index 3e80025..170a5b7 100644 --- a/sources/app/Locale/id_ID/translations.php +++ b/sources/app/Locale/id_ID/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Kompleksitas', 'Task limit' => 'Batas tugas.', 'Task count' => 'Jumlah tugas', - 'Edit project access list' => 'Modifikasi hak akses proyek', - 'Allow this user' => 'Memperbolehkan pengguna ini', - 'Don\'t forget that administrators have access to everything.' => 'Ingat bahwa administrator memiliki akses ke semua.', - 'Revoke' => 'Mencabut', - 'List of authorized users' => 'Daftar pengguna yang berwenang', 'User' => 'Pengguna', - 'Nobody have access to this project.' => 'Tidak ada yang berwenang untuk mengakses proyek.', 'Comments' => 'Komentar', 'Write your text in Markdown' => 'Menulis teks anda didalam Markdown', 'Leave a comment' => 'Tinggalkan komentar', @@ -396,8 +390,6 @@ return array( 'Email:' => 'Email :', 'Notifications:' => 'Pemberitahuan :', 'Notifications' => 'Pemberitahuan', - 'Group:' => 'Grup :', - 'Regular user' => 'Pengguna normal', 'Account type:' => 'Tipe akun :', 'Edit profile' => 'Modifikasi profil', 'Change password' => 'Rubah kata sandri', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s mengubah orang yang ditugaskan dari tugas %s ke %s', 'New password for the user "%s"' => 'Kata sandi baru untuk pengguna « %s »', 'Choose an event' => 'Pilih acara', - 'Github commit received' => 'Menerima komit dari Github', - 'Github issue opened' => 'Tiket Github dibuka', - 'Github issue closed' => 'Tiket Github ditutup', - 'Github issue reopened' => 'Tiket Github dibuka kembali', - 'Github issue assignee change' => 'Rubah penugasan tiket Github', - 'Github issue label change' => 'Perubahan label pada tiket Github', 'Create a task from an external provider' => 'Buat tugas dari pemasok eksternal', 'Change the assignee based on an external username' => 'Rubah penugasan berdasarkan nama pengguna eksternal', 'Change the category based on an external label' => 'Rubah kategori berdasarkan label eksternal', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Semua orang mendapat akses untuk proyek ini.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Webhook Github', - 'Help on Github webhooks' => 'Bantuan pada webhook Github', 'Create a comment from an external provider' => 'Buat komentar dari pemasok eksternal', - 'Github issue comment created' => 'Komentar dibuat pada tiket Github', 'Project management' => 'Manajemen proyek', 'My projects' => 'Proyek saya', 'Columns' => 'Kolom', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Partisi ulang pengguna untuk « %s »', 'Clone this project' => 'Gandakan proyek ini', 'Column removed successfully.' => 'Kolom berhasil dihapus.', - 'Github Issue' => 'Tiket Github', 'Not enough data to show the graph.' => 'Tidak cukup data untuk menampilkan grafik.', 'Previous' => 'Sebelumnya', 'The id must be an integer' => 'Id harus integer', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Standar swimlane', 'Do you really want to remove this swimlane: "%s"?' => 'Apakah anda yakin akan menghapus swimlane ini : « %s » ?', 'Inactive swimlanes' => 'Swimlanes tidak aktif', - 'Set project manager' => 'Masukan manajer proyek', - 'Set project member' => 'Masukan anggota proyek ', 'Remove a swimlane' => 'Supprimer une swimlane', 'Rename' => 'Ganti nama', 'Show default swimlane' => 'Perlihatkan standar swimlane', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Swimlane anda berhasil dibuat.', 'Example: "Bug, Feature Request, Improvement"' => 'Contoh: « Insiden, Permintaan Fitur, Perbaikan »', 'Default categories for new projects (Comma-separated)' => 'Standar kategori untuk proyek baru (dipisahkan dengan koma)', - 'Gitlab commit received' => 'Menerima komit Gitlab', - 'Gitlab issue opened' => 'Tiket Gitlab dibuka', - 'Gitlab issue closed' => 'Tiket Gitlab ditutup', - 'Gitlab webhooks' => 'Webhook Gitlab', - 'Help on Gitlab webhooks' => 'Bantuan pada webhook Gitlab', 'Integrations' => 'Integrasi', 'Integration with third-party services' => 'Integrasi dengan layanan pihak ketiga', - 'Role for this project' => 'Peran untuk proyek ini', - 'Project manager' => 'Manajer proyek', - 'Project member' => 'Anggota proyek', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Seorang manajer proyek dapat mengubah pengaturan proyek dan memiliki lebih banyak keistimewaan dibandingkan dengan pengguna biasa.', - 'Gitlab Issue' => 'Tiket Gitlab', 'Subtask Id' => 'Id Subtugas', 'Subtasks' => 'Subtugas', 'Subtasks Export' => 'Ekspor Subtugas', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Anda sudah ada satu subtugas dalam proses', 'Which parts of the project do you want to duplicate?' => 'Bagian dalam proyek mana yang ingin anda duplikasi?', 'Disallow login form' => 'Larang formulir masuk', - 'Bitbucket commit received' => 'Menerima komit Bitbucket', - 'Bitbucket webhooks' => 'Webhook Bitbucket', - 'Help on Bitbucket webhooks' => 'Bantuan pada webhook Bitbucket', 'Start' => 'Mulai', 'End' => 'Selesai', 'Task age in days' => 'Usia tugas dalam hari', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Kode dua faktor kode otentifikasi valid.', 'Code' => 'Kode', 'Two factor authentication' => 'Dua faktor otentifikasi', - 'Enable/disable two factor authentication' => 'Matikan/hidupkan dua faktor otentifikasi', 'This QR code contains the key URI: ' => 'kode QR ini mengandung kunci URI : ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Menyimpan kunci rahasia ini dalam perangkat lunak TOTP anda(misalnya Googel Authenticator atau FreeOTP).', 'Check my code' => 'Memeriksa kode saya', 'Secret key: ' => 'Kunci rahasia : ', 'Test your device' => 'Menguji perangkat anda', @@ -776,21 +741,10 @@ return array( 'User that will receive the email' => 'Pengguna yang akan menerima email', 'Email subject' => 'Subjek Email', 'Date' => 'Tanggal', - 'By @%s on Bitbucket' => 'Oleh @%s pada Bitbucket', - 'Bitbucket Issue' => 'Tiket Bitbucket', - 'Commit made by @%s on Bitbucket' => 'Komit dibuat oleh @%s pada Bitbucket', - 'Commit made by @%s on Github' => 'Komit dibuat oleh @%s pada Github', - 'By @%s on Github' => 'Oleh @%s pada Github', - 'Commit made by @%s on Gitlab' => 'Komit dibuat oleh @%s pada Gitlab', 'Add a comment log when moving the task between columns' => 'Menambahkan log komentar ketika memindahkan tugas antara kolom', 'Move the task to another column when the category is changed' => 'Pindahkan tugas ke kolom lain ketika kategori berubah', 'Send a task by email to someone' => 'Kirim tugas melalui email ke seseorang', 'Reopen a task' => 'Membuka kembali tugas', - 'Bitbucket issue opened' => 'Tiket Bitbucket dibuka', - 'Bitbucket issue closed' => 'Tiket Bitbucket ditutup', - 'Bitbucket issue reopened' => 'Tiket Bitbucket dibuka kembali', - 'Bitbucket issue assignee change' => 'Perubahan penugasan tiket Bitbucket', - 'Bitbucket issue comment created' => 'Komentar dibuat tiket Bitbucket', 'Column change' => 'Kolom berubah', 'Position change' => 'Posisi berubah', 'Swimlane change' => 'Swimlane berubah', @@ -910,8 +864,6 @@ return array( 'Remote user' => 'Pengguna jauh', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Pengguna jauh tidak menyimpan kata sandi mereka dalam basis data Kanboard, contoh: akun LDAP, Google dan Github.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Jika anda mencentang kotak "Larang formulir login", kredensial masuk ke formulis login akan diabaikan.', - 'By @%s on Gitlab' => 'Dengan @%s pada Gitlab', - 'Gitlab issue comment created' => 'Komentar dibuat pada tiket Gitlab', 'New remote user' => 'Pengguna baru jauh', 'New local user' => 'Pengguna baru lokal', 'Default task color' => 'Standar warna tugas', @@ -931,7 +883,6 @@ return array( 'contributors' => 'kontributor', 'License:' => 'Lisensi :', 'License' => 'Lisensi', - 'Project Administrator' => 'Administrator proyek', 'Enter the text below' => 'Masukkan teks di bawah', 'Gantt chart for %s' => 'Grafik Gantt untuk %s', 'Sort by position' => 'Urutkan berdasarkan posisi', @@ -955,7 +906,6 @@ return array( 'Members' => 'Anggota', 'Shared project' => 'Proyek bersama', 'Project managers' => 'Manajer proyek', - 'Project members' => 'Anggota proyek', 'Gantt chart for all projects' => 'Grafik Gantt untuk semua proyek', 'Projects list' => 'Daftar proyek', 'Gantt chart for this project' => 'Grafik Gantt untuk proyek ini', @@ -982,7 +932,6 @@ return array( 'Documentation' => 'Dokumentasi', 'Table of contents' => 'Daftar isi', 'Gantt' => 'Gantt', - 'Help with project permissions' => 'Bantuan dengan izin proyek', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + 'Project members' => 'Anggota proyek', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/it_IT/translations.php b/sources/app/Locale/it_IT/translations.php index 7fd39d9..cd86c98 100644 --- a/sources/app/Locale/it_IT/translations.php +++ b/sources/app/Locale/it_IT/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Complessità', 'Task limit' => 'Numero massimo di compiti', 'Task count' => 'Numero di compiti', - 'Edit project access list' => 'Modificare i permessi del progetto', - 'Allow this user' => 'Permettere a questo utente', - 'Don\'t forget that administrators have access to everything.' => 'Non dimenticare che gli amministratori hanno accesso a tutto.', - 'Revoke' => 'Revocare', - 'List of authorized users' => 'Lista di utenti autorizzati', 'User' => 'Utente', - 'Nobody have access to this project.' => 'Nessuno ha accesso a questo progetto.', 'Comments' => 'Commenti', 'Write your text in Markdown' => 'Scrivi il testo in Markdown', 'Leave a comment' => 'Lasciare un commento', @@ -396,8 +390,6 @@ return array( // 'Email:' => '', 'Notifications:' => 'Notifiche:', 'Notifications' => 'Notifiche', - 'Group:' => 'Gruppo', - 'Regular user' => 'Utente regolare', 'Account type:' => 'Tipo di account', 'Edit profile' => 'Modifica il profilo', 'Change password' => 'Cambia password', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s ha cambiato l\'assegnatario del compito %s a %s', 'New password for the user "%s"' => 'Nuova password per l\'utente "%s"', 'Choose an event' => 'Scegli un evento', - 'Github commit received' => 'Commit di Github ricevuto', - 'Github issue opened' => 'Issue di Github ricevuto', - 'Github issue closed' => 'Issue di Github chiusa', - 'Github issue reopened' => 'Issue di Github riaperta', - 'Github issue assignee change' => 'Assegnatario dell\'issue di Github cambiato', - 'Github issue label change' => 'Etichetta dell\'issue di Github cambiata', 'Create a task from an external provider' => 'Crea un compito da un provider esterno', 'Change the assignee based on an external username' => 'Cambia l\'assegnatario basandosi su un username esterno', 'Change the category based on an external label' => 'Cambia la categoria basandosi su un\'etichetta esterna', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Tutti hanno accesso a questo progetto', // 'Webhooks' => '', // 'API' => '', - 'Github webhooks' => 'Webhooks di Github', - 'Help on Github webhooks' => 'Guida ai Webhooks di Github', 'Create a comment from an external provider' => 'Crea un commit da un provider esterno', - 'Github issue comment created' => 'Commento ad un Issue di Github creato', 'Project management' => 'Gestione del progetto', 'My projects' => 'I miei progetti', 'Columns' => 'Colonne', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Ripartizione utente per "%s"', 'Clone this project' => 'Clona questo progetto', 'Column removed successfully.' => 'Colonna rimossa con successo', - 'Github Issue' => 'Issue di Github', 'Not enough data to show the graph.' => 'Non ci sono abbastanza dati per visualizzare il grafico.', 'Previous' => 'Precendete', 'The id must be an integer' => 'L\'id deve essere un intero', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Corsia di default', 'Do you really want to remove this swimlane: "%s"?' => 'Vuoi davvero rimuovere questa corsia: "%s"?', 'Inactive swimlanes' => 'Corsie inattive', - 'Set project manager' => 'Imposta un manager del progetto', - 'Set project member' => 'Imposta un membro del progetto', 'Remove a swimlane' => 'Rimuovi una corsia', 'Rename' => 'Rinomina', 'Show default swimlane' => 'Mostra le corsie di default', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'La sua corsia è stata creata con successo', 'Example: "Bug, Feature Request, Improvement"' => 'Esempio: "Bug, Richiesta di Funzioni, Migliorie"', 'Default categories for new projects (Comma-separated)' => 'Categorie di default per i progetti (Separati da virgola)', - 'Gitlab commit received' => 'Commit ricevuto da Gitlab', - 'Gitlab issue opened' => 'Issue di Gitlab aperta', - 'Gitlab issue closed' => 'Issue di Gitlab chiusa', - 'Gitlab webhooks' => 'Webhooks di Gitlab', - 'Help on Gitlab webhooks' => 'Guida ai Webhooks di Gitlab', 'Integrations' => 'Integrazioni', 'Integration with third-party services' => 'Integrazione con servizi di terze parti', - 'Role for this project' => 'Ruolo per questo progetto', - 'Project manager' => 'Manager del progetto', - 'Project member' => 'Membro del progetto', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Un manager del progetto può cambiare le impostazioni del progetto ed avere più privilegi di un utente standard.', - 'Gitlab Issue' => 'Issue di Gitlab', 'Subtask Id' => 'Id del sotto-compito', 'Subtasks' => 'Sotto-compiti', 'Subtasks Export' => 'Esporta sotto-compiti', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Hai già un sotto-compito in progresso', 'Which parts of the project do you want to duplicate?' => 'Quali parti del progetto vuoi duplicare?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Commit ricevuto da Bitbucket', - 'Bitbucket webhooks' => 'Webhooks di Bitbucket', - 'Help on Bitbucket webhooks' => 'Guida ai Webhooks di Bitbucket', 'Start' => 'Inizio', 'End' => 'Fine', 'Task age in days' => 'Anzianità del compito in giorni', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Il codice di autenticazione a due fattori è valido', 'Code' => 'Codice', 'Two factor authentication' => 'Autenticazione a due fattori', - 'Enable/disable two factor authentication' => 'Abilita/disabilita autenticazione a due fattori', 'This QR code contains the key URI: ' => 'Questo QR code contiene l\'URI: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Salva la chiave privata nel tuo software TOTP (per esempio Google Authenticator oppure FreeOTP).', 'Check my code' => 'Controlla il mio codice', 'Secret key: ' => 'Chiave privata:', 'Test your device' => 'Testa il tuo dispositivo', @@ -776,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -910,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -931,7 +883,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +906,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/ja_JP/translations.php b/sources/app/Locale/ja_JP/translations.php index 8447113..e91a3bf 100644 --- a/sources/app/Locale/ja_JP/translations.php +++ b/sources/app/Locale/ja_JP/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => '複雑さ', 'Task limit' => 'タスク数制限', 'Task count' => 'タスク数', - 'Edit project access list' => 'プロジェクトのアクセス許可を変更', - 'Allow this user' => 'このユーザを許可する', - 'Don\'t forget that administrators have access to everything.' => '管理者には全ての権限が与えられます。', - 'Revoke' => '許可を取り下げる', - 'List of authorized users' => '許可されたユーザ', 'User' => 'ユーザ', - 'Nobody have access to this project.' => 'だれもプロジェクトにアクセスできません。', 'Comments' => 'コメント', 'Write your text in Markdown' => 'Markdown 記法で書く', 'Leave a comment' => 'コメントを書く', @@ -396,8 +390,6 @@ return array( 'Email:' => 'Email:', 'Notifications:' => '通知:', 'Notifications' => '通知', - 'Group:' => 'グループ:', - 'Regular user' => '通常のユーザ', 'Account type:' => 'アカウントの種類:', 'Edit profile' => 'プロフィールの変更', 'Change password' => 'パスワードの変更', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s がタスク %s の担当を %s に変更しました', 'New password for the user "%s"' => 'ユーザ「%s」の新しいパスワード', 'Choose an event' => 'イベントの選択', - 'Github commit received' => 'Github のコミットを受け取った', - 'Github issue opened' => 'Github Issue がオープンされた', - 'Github issue closed' => 'Github Issue がクローズされた', - 'Github issue reopened' => 'Github Issue が再オープンされた', - 'Github issue assignee change' => 'Github Issue の担当が変更された', - 'Github issue label change' => 'Github のラベルが変更された', 'Create a task from an external provider' => 'タスクを外部サービスから作成する', 'Change the assignee based on an external username' => '担当者を外部サービスに基いて変更する', 'Change the category based on an external label' => 'カテゴリを外部サービスに基いて変更する', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => '誰でもこのプロジェクトにアクセスできます。', 'Webhooks' => 'Webhook', 'API' => 'API', - 'Github webhooks' => 'Github Webhook', - 'Help on Github webhooks' => 'Github webhook のヘルプ', 'Create a comment from an external provider' => '外部サービスからコメントを作成する', - 'Github issue comment created' => 'Github Issue コメントが作られました', 'Project management' => 'プロジェクト・マネジメント', 'My projects' => '自分のプロジェクト', 'Columns' => 'カラム', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => '「%s」の担当者分布', 'Clone this project' => 'このプロジェクトを複製する', 'Column removed successfully.' => 'カラムを削除しました', - 'Github Issue' => 'Github Issue', 'Not enough data to show the graph.' => 'グラフを描画するには出たが足りません', 'Previous' => '戻る', 'The id must be an integer' => 'id は数字でなければなりません', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'デフォルトスイムレーン', 'Do you really want to remove this swimlane: "%s"?' => 'このスイムレーン「%s」を本当に削除しますか?', 'Inactive swimlanes' => 'インタラクティブなスイムレーン', - 'Set project manager' => 'プロジェクトマネジャーをセット', - 'Set project member' => 'プロジェクトメンバーをセット', 'Remove a swimlane' => 'スイムレーンの削除', 'Rename' => '名前の変更', 'Show default swimlane' => 'デフォルトスイムレーンの表示', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'スイムレーンが作成されました。', 'Example: "Bug, Feature Request, Improvement"' => '例: バグ, 機能, 改善', 'Default categories for new projects (Comma-separated)' => '新しいプロジェクトのデフォルトカテゴリー (コンマ区切り)', - 'Gitlab commit received' => 'Gitlab コミットを受診しました', - 'Gitlab issue opened' => 'Gitlab Issue がオープンされました', - 'Gitlab issue closed' => 'Gitlab Issue がクローズされました', - 'Gitlab webhooks' => 'Gitlab Webhooks', - 'Help on Gitlab webhooks' => 'Gitlab Webhooks のヘルプ', 'Integrations' => '連携', 'Integration with third-party services' => 'サードパーティサービスとの連携', - 'Role for this project' => 'このプロジェクトの役割', - 'Project manager' => 'プロジェクトマネジャー', - 'Project member' => 'プロジェクトメンバー', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'プロジェクトマネジャーはプロジェクトの設定を変更するなどの通常ユーザにはない権限があります。', - 'Gitlab Issue' => 'Gitlab Issue', 'Subtask Id' => 'サブタスク Id', 'Subtasks' => 'サブタスク', 'Subtasks Export' => 'サブタスクの出力', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'すでに進行中のサブタスクがあります。', 'Which parts of the project do you want to duplicate?' => 'プロジェクトの何を複製しますか?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket コミットを受信しました', - 'Bitbucket webhooks' => 'Bitbucket Webhooks', - 'Help on Bitbucket webhooks' => 'Bitbucket Webhooks のヘルプ', 'Start' => '開始', 'End' => '終了', 'Task age in days' => 'タスクの経過日数', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => '2 段認証コードは有効です。', 'Code' => 'コード', 'Two factor authentication' => '2 段認証', - 'Enable/disable two factor authentication' => '2 段認証の有効/無効', 'This QR code contains the key URI: ' => 'この QR コードが URI キーを含んでいます: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '秘密鍵を TOTP ソフトに保存 (Google Authenticator や FreeOTP など)', 'Check my code' => '自分のコードをチェック', 'Secret key: ' => '秘密鍵: ', 'Test your device' => 'デバイスをテストする', @@ -776,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -910,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -931,7 +883,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +906,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/my_MY/translations.php b/sources/app/Locale/my_MY/translations.php new file mode 100644 index 0000000..e83f915 --- /dev/null +++ b/sources/app/Locale/my_MY/translations.php @@ -0,0 +1,1107 @@ + '.', + 'number.thousands_separator' => ',', + 'None' => 'Tiada', + 'edit' => 'sunting', + 'Edit' => 'Sunting', + 'remove' => 'hapus', + 'Remove' => 'Hapus', + 'Update' => 'Kemaskini', + 'Yes' => 'Ya', + 'No' => 'Tidak', + 'cancel' => 'batal', + 'or' => 'atau', + 'Yellow' => 'Kuning', + 'Blue' => 'Biru', + 'Green' => 'Hijau', + 'Purple' => 'Ungu', + 'Red' => 'Merah', + 'Orange' => 'Oren', + 'Grey' => 'Kelabu', + 'Brown' => 'Coklat', + 'Deep Orange' => 'Oren Gelap', + 'Dark Grey' => 'Kelabu Malap', + 'Pink' => 'Merah Jambu', + 'Teal' => 'Teal', + 'Cyan' => 'Sian', + 'Lime' => 'Lime', + 'Light Green' => 'Hijau Muda', + 'Amber' => 'Amber', + 'Save' => 'Simpan', + 'Login' => 'Masuk', + 'Official website:' => 'Laman rasmi :', + 'Unassigned' => 'Belum ditugaskan', + 'View this task' => 'Lihat tugas ini', + 'Remove user' => 'Hapus pengguna', + 'Do you really want to remove this user: "%s"?' => 'Anda yakin mahu menghapus pengguna ini : « %s » ?', + 'New user' => 'Pengguna baru', + 'All users' => 'Semua pengguna', + 'Username' => 'Nama pengguna', + 'Password' => 'Kata laluan', + 'Administrator' => 'Pentadbir', + 'Sign in' => 'Masuk', + 'Users' => 'Para Pengguna', + 'No user' => 'Tiada pengguna', + 'Forbidden' => 'Larangan', + 'Access Forbidden' => 'Akses Dilarang', + 'Edit user' => 'Ubah Pengguna', + 'Logout' => 'Keluar', + 'Bad username or password' => 'Nama pengguna atau kata laluan tidak sepadan', + 'Edit project' => 'Ubah projek', + 'Name' => 'Nama', + 'Projects' => 'Projek', + 'No project' => 'Tiada projek', + 'Project' => 'Projek', + 'Status' => 'Status', + 'Tasks' => 'Tugasan', + 'Board' => 'Papan', + 'Actions' => 'Tindakan', + 'Inactive' => 'Tidak Aktif', + 'Active' => 'Aktif', + 'Add this column' => 'Tambahkan kolom ini', + '%d tasks on the board' => '%d tugasan di papan', + '%d tasks in total' => 'Sejumlah %d tugasan', + 'Unable to update this board.' => 'Tidak berupaya mengemaskini papan ini', + 'Edit board' => 'ubah papan', + 'Disable' => 'Nyah-Upaya', + 'Enable' => 'Aktifkan', + 'New project' => 'Projek Baru', + 'Do you really want to remove this project: "%s"?' => 'Anda yakin mahu menghapus projek ini : « %s » ?', + 'Remove project' => 'Hapus projek', + 'Edit the board for "%s"' => 'Ubah papan untuk « %s »', + 'All projects' => 'Semua projek', + 'Change columns' => 'Ubah kolom', + 'Add a new column' => 'Tambah kolom baru', + 'Title' => 'Judul', + 'Nobody assigned' => 'Tidak ada yang ditugaskan', + 'Assigned to %s' => 'Ditugaskan ke %s', + 'Remove a column' => 'Hapus kolom', + 'Remove a column from a board' => 'Hapus kolom dari papan', + 'Unable to remove this column.' => 'Tidak dapat menghapus kolom ini.', + 'Do you really want to remove this column: "%s"?' => 'Apakah anda yakin akan menghapus kolom ini : « %s » ?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'tindakan ini akan MENGHAPUS SEMUA TUGAS yang terkait dengan kolom ini!', + 'Settings' => 'Penetapan', + 'Application settings' => 'Penetapan aplikasi', + 'Language' => 'Bahasa', + 'Webhook token:' => 'Token webhook :', + 'API token:' => 'Token API :', + 'Database size:' => 'Saiz pengkalan data:', + 'Download the database' => 'Muat turun pengkalan data', + 'Optimize the database' => 'Optimakan pengkalan data', + '(VACUUM command)' => '(perintah VACUUM)', + '(Gzip compressed Sqlite file)' => '(File Sqlite yang termampat Gzip)', + 'Close a task' => 'Tutup tugas', + 'Edit a task' => 'Sunting tugas', + 'Column' => 'Kolom', + 'Color' => 'Warna', + 'Assignee' => 'Orang yang ditugaskan', + 'Create another task' => 'Buat tugas lain', + 'New task' => 'Tugasan baru', + 'Open a task' => 'Buka tugas', + 'Do you really want to open this task: "%s"?' => 'Anda yakin untuk buka tugas ini : « %s » ?', + 'Back to the board' => 'Kembali ke papan', + 'Created on %B %e, %Y at %k:%M %p' => 'Dicipta pada tanggal %d/%m/%Y à %H:%M', + 'There is nobody assigned' => 'Tidak ada orang yand ditugaskan', + 'Column on the board:' => 'Kolom di dalam papan : ', + 'Status is open' => 'Status terbuka', + 'Status is closed' => 'Status ditutup', + 'Close this task' => 'Tutup tugas ini', + 'Open this task' => 'Buka tugas ini', + 'There is no description.' => 'Tidak ada keterangan.', + 'Add a new task' => 'Tambah tugas baru', + 'The username is required' => 'Nama pengguna adalah wajib', + 'The maximum length is %d characters' => 'Panjang maksimum adalah %d karakter', + 'The minimum length is %d characters' => 'Panjang minimum adalah %d karakter', + 'The password is required' => 'Kata laluan adalah wajib', + 'This value must be an integer' => 'Nilai ini harus integer', + 'The username must be unique' => 'Nama pengguna semestinya unik', + 'The user id is required' => 'Id Pengguna adalah wajib', + 'Passwords don\'t match' => 'Kata laluan tidak sepadan', + 'The confirmation is required' => 'Pengesahan diperlukan', + 'The project is required' => 'Projek diperlukan', + 'The id is required' => 'Id diperlukan', + 'The project id is required' => 'Id projek diperlukan', + 'The project name is required' => 'Nama projek diperlukan', + 'The title is required' => 'Judul diperlukan', + 'Settings saved successfully.' => 'Penetapan berjaya disimpan.', + 'Unable to save your settings.' => 'Tidak dapat menyimpan penetapan anda.', + 'Database optimization done.' => 'Optimasi pengkalan data selesai.', + 'Your project have been created successfully.' => 'Projek anda berhasil dibuat.', + 'Unable to create your project.' => 'Tidak dapat membuat projek anda.', + 'Project updated successfully.' => 'projek berhasil diperbaharui.', + 'Unable to update this project.' => 'Tidak dapat memperbaharui projek ini.', + 'Unable to remove this project.' => 'Tidak dapat menghapus projek ini.', + 'Project removed successfully.' => 'projek berhasil dihapus.', + 'Project activated successfully.' => 'projek berhasil diaktivasi.', + 'Unable to activate this project.' => 'Tidak dapat mengaktifkan projek ini.', + 'Project disabled successfully.' => 'projek berhasil dinonaktifkan.', + 'Unable to disable this project.' => 'Tidak dapat menonaktifkan projek ini.', + 'Unable to open this task.' => 'Tidak dapat membuka tugas ini.', + 'Task opened successfully.' => 'Tugas berhasil dibuka.', + 'Unable to close this task.' => 'Tidak dapat menutup tugas ini.', + 'Task closed successfully.' => 'Tugas berhasil ditutup.', + 'Unable to update your task.' => 'Tidak dapat memperbaharui tugas ini.', + 'Task updated successfully.' => 'Tugas berhasil diperbaharui.', + 'Unable to create your task.' => 'Tidak dapat membuat tugas anda.', + 'Task created successfully.' => 'Tugas berhasil dibuat.', + 'User created successfully.' => 'Pengguna berhasil dibuat.', + 'Unable to create your user.' => 'Tidak dapat membuat pengguna anda.', + 'User updated successfully.' => 'Pengguna berhasil diperbaharui.', + 'Unable to update your user.' => 'Tidak dapat memperbaharui pengguna anda.', + 'User removed successfully.' => 'pengguna berhasil dihapus.', + 'Unable to remove this user.' => 'Tidak dapat menghapus pengguna ini.', + 'Board updated successfully.' => 'Papan berhasil diperbaharui.', + 'Ready' => 'Siap', + 'Backlog' => 'Tertunda', + 'Work in progress' => 'Sedang dalam pengerjaan', + 'Done' => 'Selesai', + 'Application version:' => 'Versi aplikasi :', + 'Completed on %B %e, %Y at %k:%M %p' => 'Diselesaikan pada tanggal %d/%m/%Y à %H:%M', + '%B %e, %Y at %k:%M %p' => '%d/%m/%Y à %H:%M', + 'Date created' => 'Tanggal dibuat', + 'Date completed' => 'Tanggal diselesaikan', + 'Id' => 'Id.', + '%d closed tasks' => '%d tugas yang ditutup', + 'No task for this project' => 'Tidak ada tugas dalam projek ini', + 'Public link' => 'Pautan publik', + 'Change assignee' => 'Mengubah orang yand ditugaskan', + 'Change assignee for the task "%s"' => 'Mengubah orang yang ditugaskan untuk tugas « %s »', + 'Timezone' => 'Zona waktu', + 'Sorry, I didn\'t find this information in my database!' => 'Maaf, saya tidak menemukan informasi ini dalam basis data saya !', + 'Page not found' => 'Halaman tidak ditemukan', + 'Complexity' => 'Kompleksitas', + 'Task limit' => 'Batas tugas.', + 'Task count' => 'Jumlah tugas', + 'User' => 'Pengguna', + 'Comments' => 'Komentar', + 'Write your text in Markdown' => 'Menulis teks anda didalam Markdown', + 'Leave a comment' => 'Tinggalkan komentar', + 'Comment is required' => 'Komentar diperlukan', + 'Leave a description' => 'Tinggalkan deskripsi', + 'Comment added successfully.' => 'Komentar berhasil ditambahkan.', + 'Unable to create your comment.' => 'Tidak dapat menambahkan komentar anda.', + 'Edit this task' => 'Modifikasi tugas ini', + 'Due Date' => 'Batas Tanggal Terakhir', + 'Invalid date' => 'Tanggal tidak valid', + 'Must be done before %B %e, %Y' => 'Harus diselesaikan sebelum tanggal %d/%m/%Y', + '%B %e, %Y' => '%d %B %Y', + '%b %e, %Y' => '%d/%m/%Y', + 'Automatic actions' => 'Tindakan otomatis', + 'Your automatic action have been created successfully.' => 'Tindakan otomatis anda berhasil dibuat.', + 'Unable to create your automatic action.' => 'Tidak dapat membuat tindakan otomatis anda.', + 'Remove an action' => 'Hapus tindakan', + 'Unable to remove this action.' => 'Tidak dapat menghapus tindakan ini', + 'Action removed successfully.' => 'Tindakan berhasil dihapus.', + 'Automatic actions for the project "%s"' => 'Tindakan otomatis untuk projek ini « %s »', + 'Defined actions' => 'Tindakan didefinisikan', + 'Add an action' => 'Tambah tindakan', + 'Event name' => 'Nama acara', + 'Action name' => 'Nama tindakan', + 'Action parameters' => 'Parameter tindakan', + 'Action' => 'Tindakan', + 'Event' => 'Acara', + 'When the selected event occurs execute the corresponding action.' => 'Ketika acara yang dipilih terjadi, melakukan tindakan yang sesuai.', + 'Next step' => 'Langkah selanjutnya', + 'Define action parameters' => 'Definisi parameter tindakan', + 'Save this action' => 'Simpan tindakan ini', + 'Do you really want to remove this action: "%s"?' => 'Apakah anda yakin akan menghapus tindakan ini « %s » ?', + 'Remove an automatic action' => 'Hapus tindakan otomatis', + 'Assign the task to a specific user' => 'Menetapkan tugas untuk pengguna tertentu', + 'Assign the task to the person who does the action' => 'Memberikan tugas untuk orang yang melakukan tindakan', + 'Duplicate the task to another project' => 'Duplikasi tugas ke projek lain', + 'Move a task to another column' => 'Pindahkan tugas ke kolom lain', + 'Task modification' => 'Modifikasi tugas', + 'Task creation' => 'Membuat tugas', + 'Closing a task' => 'Menutup tugas', + 'Assign a color to a specific user' => 'Menetapkan warna untuk pengguna tertentu', + 'Column title' => 'Judul kolom', + 'Position' => 'Posisi', + 'Move Up' => 'Pindah ke atas', + 'Move Down' => 'Pindah ke bawah', + 'Duplicate to another project' => 'Duplikasi ke projek lain', + 'Duplicate' => 'Duplikasi', + 'link' => 'Pautan', + 'Comment updated successfully.' => 'Komentar berhasil diperbaharui.', + 'Unable to update your comment.' => 'Tidak dapat memperbaharui komentar anda.', + 'Remove a comment' => 'Hapus komentar', + 'Comment removed successfully.' => 'Komentar berhasil dihapus.', + 'Unable to remove this comment.' => 'Tidak dapat menghapus komentar ini.', + 'Do you really want to remove this comment?' => 'Apakah anda yakin akan menghapus komentar ini ?', + 'Only administrators or the creator of the comment can access to this page.' => 'Hanya administrator atau pembuat komentar yang dapat mengakses halaman ini.', + 'Current password for the user "%s"' => 'Kata laluan saat ini untuk pengguna « %s »', + 'The current password is required' => 'Kata laluan saat ini diperlukan', + 'Wrong password' => 'Kata laluan salah', + 'Unknown' => 'Tidak diketahui', + 'Last logins' => 'Masuk terakhir', + 'Login date' => 'Tanggal masuk', + 'Authentication method' => 'Metode otentifikasi', + 'IP address' => 'Alamat IP', + 'User agent' => 'Agen Pengguna', + 'Persistent connections' => 'Koneksi persisten', + 'No session.' => 'Tidak ada sesi.', + 'Expiration date' => 'Tanggal kadaluarsa', + 'Remember Me' => 'Ingat Saya', + 'Creation date' => 'Tanggal dibuat', + 'Everybody' => 'Semua orang', + 'Open' => 'Terbuka', + 'Closed' => 'Ditutup', + 'Search' => 'Cari', + 'Nothing found.' => 'Tidak ditemukan.', + 'Due date' => 'Batas tanggal terakhir', + 'Others formats accepted: %s and %s' => 'Format lain yang didukung : %s et %s', + 'Description' => 'Deskripsi', + '%d comments' => '%d komentar', + '%d comment' => '%d komentar', + 'Email address invalid' => 'Alamat email tidak valid', + 'Your external account is not linked anymore to your profile.' => 'Akaun eksternal anda tidak lagi terhubung ke profil anda.', + 'Unable to unlink your external account.' => 'Tidak dapat memutuskan Akaun eksternal anda.', + 'External authentication failed' => 'Otentifikasi eksternal gagal', + 'Your external account is linked to your profile successfully.' => 'Akaun eksternal anda berhasil dihubungkan ke profil anda.', + 'Email' => 'Email', + 'Link my Google Account' => 'Hubungkan Akaun Google saya', + 'Unlink my Google Account' => 'Putuskan Akaun Google saya', + 'Login with my Google Account' => 'Masuk menggunakan Akaun Google saya', + 'Project not found.' => 'projek tidak ditemukan.', + 'Task removed successfully.' => 'Tugas berhasil dihapus.', + 'Unable to remove this task.' => 'Tidak dapat menghapus tugas ini.', + 'Remove a task' => 'Hapus tugas', + 'Do you really want to remove this task: "%s"?' => 'Apakah anda yakin akan menghapus tugas ini « %s » ?', + 'Assign automatically a color based on a category' => 'Otomatis menetapkan warna berdasarkan kategori', + 'Assign automatically a category based on a color' => 'Otomatis menetapkan kategori berdasarkan warna', + 'Task creation or modification' => 'Tugas dibuat atau di mofifikasi', + 'Category' => 'Kategori', + 'Category:' => 'Kategori :', + 'Categories' => 'Kategori', + 'Category not found.' => 'Kategori tidak ditemukan', + 'Your category have been created successfully.' => 'Kategori anda berhasil dibuat.', + 'Unable to create your category.' => 'Tidak dapat membuat kategori anda.', + 'Your category have been updated successfully.' => 'Kategori anda berhasil diperbaharui.', + 'Unable to update your category.' => 'Tidak dapat memperbaharui kategori anda.', + 'Remove a category' => 'Hapus kategori', + 'Category removed successfully.' => 'Kategori berhasil dihapus.', + 'Unable to remove this category.' => 'Tidak dapat menghapus kategori ini.', + 'Category modification for the project "%s"' => 'Modifikasi kategori untuk projek « %s »', + 'Category Name' => 'Nama Kategori', + 'Add a new category' => 'Tambah kategori baru', + 'Do you really want to remove this category: "%s"?' => 'Apakah anda yakin akan menghapus kategori ini « %s » ?', + 'All categories' => 'Semua kategori', + 'No category' => 'Tidak ada kategori', + 'The name is required' => 'Nama diperlukan', + 'Remove a file' => 'Hapus berkas', + 'Unable to remove this file.' => 'Tidak dapat menghapus berkas ini.', + 'File removed successfully.' => 'Berkas berhasil dihapus.', + 'Attach a document' => 'Lampirkan dokumen', + 'Do you really want to remove this file: "%s"?' => 'Apakah anda yakin akan menghapus berkas ini « %s » ?', + 'Attachments' => 'Lampiran', + 'Edit the task' => 'Modifikasi tugas', + 'Edit the description' => 'Modifikasi deskripsi', + 'Add a comment' => 'Tambahkan komentar', + 'Edit a comment' => 'Modifikasi komentar', + 'Summary' => 'Ringkasan', + 'Time tracking' => 'Pelacakan waktu', + 'Estimate:' => 'Estimasi :', + 'Spent:' => 'Menghabiskan:', + 'Do you really want to remove this sub-task?' => 'Apakah anda yakin akan menghapus sub-tugas ini ?', + 'Remaining:' => 'Tersisa:', + 'hours' => 'jam', + 'spent' => 'menghabiskan', + 'estimated' => 'perkiraan', + 'Sub-Tasks' => 'Sub-tugas', + 'Add a sub-task' => 'Tambahkan sub-tugas', + 'Original estimate' => 'Perkiraan semula', + 'Create another sub-task' => 'Tambahkan sub-tugas lainnya', + 'Time spent' => 'Waktu yang dihabiskan', + 'Edit a sub-task' => 'Modifikasi sub-tugas', + 'Remove a sub-task' => 'Hapus sub-tugas', + 'The time must be a numeric value' => 'Waktu harus berisikan numerik', + 'Todo' => 'Yang harus dilakukan', + 'In progress' => 'Sedang proses', + 'Sub-task removed successfully.' => 'Sub-tugas berhasil dihapus.', + 'Unable to remove this sub-task.' => 'Tidak dapat menghapus sub-tugas.', + 'Sub-task updated successfully.' => 'Sub-tugas berhasil diperbaharui.', + 'Unable to update your sub-task.' => 'Tidak dapat memperbaharui sub-tugas anda.', + 'Unable to create your sub-task.' => 'Tidak dapat membuat sub-tugas anda.', + 'Sub-task added successfully.' => 'Sub-tugas berhasil dibuat.', + 'Maximum size: ' => 'Ukuran maksimum: ', + 'Unable to upload the file.' => 'Tidak dapat mengunggah berkas.', + 'Display another project' => 'Lihat projek lain', + 'Login with my Github Account' => 'Masuk menggunakan Akaun Github saya', + 'Link my Github Account' => 'Hubungkan Akaun Github saya ', + 'Unlink my Github Account' => 'Putuskan Akaun Github saya', + 'Created by %s' => 'Dibuat oleh %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Modifikasi terakhir pada tanggal %d/%m/%Y à %H:%M', + 'Tasks Export' => 'Ekspor Tugas', + 'Tasks exportation for "%s"' => 'Tugas di ekspor untuk « %s »', + 'Start Date' => 'Tanggal Mulai', + 'End Date' => 'Tanggal Berakhir', + 'Execute' => 'Eksekusi', + 'Task Id' => 'Id Tugas', + 'Creator' => 'Pembuat', + 'Modification date' => 'Tanggal modifikasi', + 'Completion date' => 'Tanggal penyelesaian', + 'Clone' => 'Klon', + 'Project cloned successfully.' => 'Kloning projek berhasil.', + 'Unable to clone this project.' => 'Tidak dapat mengkloning projek.', + 'Enable email notifications' => 'Aktifkan pemberitahuan dari email', + 'Task position:' => 'Posisi tugas :', + 'The task #%d have been opened.' => 'Tugas #%d telah dibuka.', + 'The task #%d have been closed.' => 'Tugas #%d telah ditutup.', + 'Sub-task updated' => 'Sub-tugas diperbaharui', + 'Title:' => 'Judul :', + 'Status:' => 'Status :', + 'Assignee:' => 'Ditugaskan ke :', + 'Time tracking:' => 'Pelacakan waktu :', + 'New sub-task' => 'Sub-tugas baru', + 'New attachment added "%s"' => 'Lampiran baru ditambahkan « %s »', + 'Comment updated' => 'Komentar diperbaharui', + 'New comment posted by %s' => 'Komentar baru ditambahkan oleh « %s »', + 'New attachment' => 'Lampirkan baru', + 'New comment' => 'Komentar baru', + 'New subtask' => 'Sub-tugas baru', + 'Subtask updated' => 'Sub-tugas diperbaharui', + 'Task updated' => 'Tugas diperbaharui', + 'Task closed' => 'Tugas ditutup', + 'Task opened' => 'Tugas dibuka', + 'I want to receive notifications only for those projects:' => 'Saya ingin menerima pemberitahuan hanya untuk projek-projek yang dipilih :', + 'view the task on Kanboard' => 'lihat tugas di Kanboard', + 'Public access' => 'Akses awam', + 'User management' => 'Manajemen pengguna', + 'Active tasks' => 'Tugas aktif', + 'Disable public access' => 'Nyahaktifkan akses awam', + 'Enable public access' => 'Aktifkan akses awam', + 'Public access disabled' => 'Akses awam dinyahaktif', + 'Do you really want to disable this project: "%s"?' => 'Anda yakin menyah-aktifkan projek ini : « %s » ?', + 'Do you really want to enable this project: "%s"?' => 'Anda yakin untuk mengaktifkan projek ini : « %s » ?', + 'Project activation' => 'Aktifkan projek', + 'Move the task to another project' => 'Pindahkan tugas ke projek lain', + 'Move to another project' => 'Pindahkan ke projek lain', + 'Do you really want to duplicate this task?' => 'Anda yakin mengembarkan tugas ini ?', + 'Duplicate a task' => 'Kembarkan tugas', + 'External accounts' => 'Akaun luaran', + 'Account type' => 'Jenis Akaun', + 'Local' => 'Lokal', + 'Remote' => 'Jauh', + 'Enabled' => 'Aktif', + 'Disabled' => 'Tidak aktif', + 'Username:' => 'Nama pengguna :', + 'Name:' => 'Nama:', + 'Email:' => 'Emel:', + 'Notifications:' => 'Makluman:', + 'Notifications' => 'Makluman', + 'Account type:' => 'Jenis Akaun :', + 'Edit profile' => 'Sunting profil', + 'Change password' => 'Rubah kata sandri', + 'Password modification' => 'Modifikasi kata laluan', + 'External authentications' => 'Otentifikasi eksternal', + 'Google Account' => 'Akaun Google', + 'Github Account' => 'Akaun Github', + 'Never connected.' => 'Tidak pernah terhubung.', + 'No account linked.' => 'Tidak ada Akaun terhubung.', + 'Account linked.' => 'Akaun terhubung.', + 'No external authentication enabled.' => 'Tidak ada otentifikasi eksternal yang aktif.', + 'Password modified successfully.' => 'Kata laluan telah berjaya ditukar.', + 'Unable to change the password.' => 'Tidak dapat merubah kata laluanr.', + 'Change category for the task "%s"' => 'Rubah kategori untuk tugas « %s »', + 'Change category' => 'Tukar kategori', + '%s updated the task %s' => '%s memperbaharui tugas %s', + '%s opened the task %s' => '%s membuka tugas %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s memindahkan tugas %s ke posisi n°%d dalam kolom « %s »', + '%s moved the task %s to the column "%s"' => '%s memindahkan tugas %s ke kolom « %s »', + '%s created the task %s' => '%s membuat tugas %s', + '%s closed the task %s' => '%s menutup tugas %s', + '%s created a subtask for the task %s' => '%s membuat subtugas untuk tugas %s', + '%s updated a subtask for the task %s' => '%s memperbaharui subtugas untuk tugas %s', + 'Assigned to %s with an estimate of %s/%sh' => 'Ditugaskan untuk %s dengan perkiraan %s/%sh', + 'Not assigned, estimate of %sh' => 'Tiada yang ditugaskan, perkiraan %sh', + '%s updated a comment on the task %s' => '%s memperbaharui komentar pada tugas %s', + '%s commented the task %s' => '%s memberikan komentar pada tugas %s', + '%s\'s activity' => 'Aktifitas dari %s', + 'RSS feed' => 'RSS feed', + '%s updated a comment on the task #%d' => '%s memperbaharui komentar pada tugas n°%d', + '%s commented on the task #%d' => '%s memberikan komentar pada tugas n°%d', + '%s updated a subtask for the task #%d' => '%s memperbaharui subtugas untuk tugas n°%d', + '%s created a subtask for the task #%d' => '%s membuat subtugas untuk tugas n°%d', + '%s updated the task #%d' => '%s memperbaharui tugas n°%d', + '%s created the task #%d' => '%s membuat tugas n°%d', + '%s closed the task #%d' => '%s menutup tugas n°%d', + '%s open the task #%d' => '%s membuka tugas n°%d', + '%s moved the task #%d to the column "%s"' => '%s memindahkan tugas n°%d ke kolom « %s »', + '%s moved the task #%d to the position %d in the column "%s"' => '%s memindahkan tugas n°%d ke posisi n°%d dalam kolom « %s »', + 'Activity' => 'Aktifitas', + 'Default values are "%s"' => 'Standar nilai adalah« %s »', + 'Default columns for new projects (Comma-separated)' => 'Kolom default untuk projek baru (dipisahkan dengan koma)', + 'Task assignee change' => 'Mengubah orang ditugaskan untuk tugas', + '%s change the assignee of the task #%d to %s' => '%s rubah orang yang ditugaskan dari tugas n%d ke %s', + '%s changed the assignee of the task %s to %s' => '%s mengubah orang yang ditugaskan dari tugas %s ke %s', + 'New password for the user "%s"' => 'Kata laluan baru untuk pengguna « %s »', + 'Choose an event' => 'Pilih sebuah acara', + 'Create a task from an external provider' => 'Buat tugas dari pemasok eksternal', + 'Change the assignee based on an external username' => 'Rubah penugasan berdasarkan nama pengguna eksternal', + 'Change the category based on an external label' => 'Rubah kategori berdasarkan label eksternal', + 'Reference' => 'Referensi', + 'Reference: %s' => 'Referensi : %s', + 'Label' => 'Label', + 'Database' => 'Pengkalan data', + 'About' => 'Tentang', + 'Database driver:' => 'Driver pengkalan data:', + 'Board settings' => 'Pengaturan papan', + 'URL and token' => 'URL dan token', + 'Webhook settings' => 'Penetapan webhook', + 'URL for task creation:' => 'URL untuk cipta tugas:', + 'Reset token' => 'Menetap semula token', + 'API endpoint:' => 'API endpoint :', + 'Refresh interval for private board' => 'Interval pembaruan untuk papan pribadi', + 'Refresh interval for public board' => 'Interval pembaruan untuk papan publik', + 'Task highlight period' => 'Periode puncak tugas', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periode (dalam detik) untuk mempertimbangkan tugas yang baru dimodifikasi (0 untuk menonaktifkan, standar 2 hari)', + 'Frequency in second (60 seconds by default)' => 'Frequensi dalam detik (standar 60 saat)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekuensi dalam detik (0 untuk menonaktifkan fitur ini, standar 10 detik)', + 'Application URL' => 'URL Aplikasi', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Contoh: http://example.kanboard.net/ (digunakan untuk pemberitahuan email)', + 'Token regenerated.' => 'Token diregenerasi.', + 'Date format' => 'Format tarikh', + 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalunya diterima, contoh: « %s » et « %s »', + 'New private project' => 'Projek peribadi baharu', + 'This project is private' => 'projek ini adalah peribadi', + 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru', + 'Add' => 'Tambah', + 'Estimated time: %s hours' => 'Anggaran waktu: %s jam', + 'Time spent: %s hours' => 'Waktu dihabiskan : %s jam', + 'Started on %B %e, %Y' => 'Dimulai pada %d/%m/%Y', + 'Start date' => 'Tarikh mula', + 'Time estimated' => 'Anggaran masa', + 'There is nothing assigned to you.' => 'Tidak ada yang diberikan kepada anda.', + 'My tasks' => 'Tugas saya', + 'Activity stream' => 'Arus aktifitas', + 'Dashboard' => 'Dasbor', + 'Confirmation' => 'Konfirmasi', + 'Allow everybody to access to this project' => 'Memungkinkan semua orang untuk mengakses projek ini', + 'Everybody have access to this project.' => 'Semua orang mendapat akses untuk projek ini.', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Create a comment from an external provider' => 'Buat komentar dari pemasok eksternal', + 'Project management' => 'Manajemen projek', + 'My projects' => 'projek saya', + 'Columns' => 'Kolom', + 'Task' => 'Tugas', + 'Your are not member of any project.' => 'Anda bukan anggota dari setiap projek.', + 'Percentage' => 'Persentasi', + 'Number of tasks' => 'Jumlah dari tugas', + 'Task distribution' => 'Pembagian tugas', + 'Reportings' => 'Pelaporan', + 'Task repartition for "%s"' => 'Pembagian tugas untuk « %s »', + 'Analytics' => 'Analitis', + 'Subtask' => 'Subtugas', + 'My subtasks' => 'Subtugas saya', + 'User repartition' => 'Partisi ulang pengguna', + 'User repartition for "%s"' => 'Partisi ulang pengguna untuk « %s »', + 'Clone this project' => 'Gandakan projek ini', + 'Column removed successfully.' => 'Kolom berhasil dihapus.', + 'Not enough data to show the graph.' => 'Tidak cukup data untuk menampilkan grafik.', + 'Previous' => 'Sebelumnya', + 'The id must be an integer' => 'Id harus integer', + 'The project id must be an integer' => 'Id projek harus integer', + 'The status must be an integer' => 'Status harus integer', + 'The subtask id is required' => 'Id subtugas diperlukan', + 'The subtask id must be an integer' => 'Id subtugas harus integer', + 'The task id is required' => 'Id tugas diperlukan', + 'The task id must be an integer' => 'Id tugas harus integer', + 'The user id must be an integer' => 'Id user harus integer', + 'This value is required' => 'Nilai ini diperlukan', + 'This value must be numeric' => 'Nilai ini harus angka', + 'Unable to create this task.' => 'Tidak dapat membuat tugas ini', + 'Cumulative flow diagram' => 'Diagram alir kumulatif', + 'Cumulative flow diagram for "%s"' => 'Diagram alir kumulatif untuk « %s »', + 'Daily project summary' => 'Ringkasan projek harian', + 'Daily project summary export' => 'Ekspot ringkasan projek harian', + 'Daily project summary export for "%s"' => 'Ekspor ringkasan projek harian untuk « %s »', + 'Exports' => 'Ekspor', + 'This export contains the number of tasks per column grouped per day.' => 'Ekspor ini berisi jumlah dari tugas per kolom dikelompokan perhari.', + 'Nothing to preview...' => 'Tiada yang dapat diintai...', + 'Preview' => 'Intai', + 'Write' => 'Tulis', + 'Active swimlanes' => 'Swimlanes aktif', + 'Add a new swimlane' => 'Tambah swimlane baharu', + 'Change default swimlane' => 'Tukar piawai swimlane', + 'Default swimlane' => 'Piawai swimlane', + 'Do you really want to remove this swimlane: "%s"?' => 'Anda yakin untuk menghapus swimlane ini : « %s » ?', + 'Inactive swimlanes' => 'Swimlanes tidak aktif', + 'Remove a swimlane' => 'Padam swimlane', + 'Rename' => 'Namakan semula', + 'Show default swimlane' => 'Tampilkan piawai swimlane', + 'Swimlane modification for the project "%s"' => 'Modifikasi swimlane untuk projek « %s »', + 'Swimlane not found.' => 'Swimlane tidak ditemui.', + 'Swimlane removed successfully.' => 'Swimlane telah dipadamkan.', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane telah dikemaskini.', + 'The default swimlane have been updated successfully.' => 'Standar swimlane berhasil diperbaharui.', + 'Unable to create your swimlane.' => 'Tidak dapat membuat swimlane anda.', + 'Unable to remove this swimlane.' => 'Tidak dapat menghapus swimlane ini.', + 'Unable to update this swimlane.' => 'Tidak dapat memperbaharui swimlane ini.', + 'Your swimlane have been created successfully.' => 'Swimlane anda berhasil dibuat.', + 'Example: "Bug, Feature Request, Improvement"' => 'Contoh: « Insiden, Permintaan Ciri, Pembaikan »', + 'Default categories for new projects (Comma-separated)' => 'Piawaian kategori untuk projek baru (asingkan guna koma)', + 'Integrations' => 'Integrasi', + 'Integration with third-party services' => 'Integrasi dengan khidmat pihak ketiga', + 'Subtask Id' => 'Id Subtugas', + 'Subtasks' => 'Subtugas', + 'Subtasks Export' => 'Ekspot Subtugas', + 'Subtasks exportation for "%s"' => 'Ekspor subtugas untuk « %s »', + 'Task Title' => 'Judul Tugas', + 'Untitled' => 'Tanpa nama', + 'Application default' => 'Aplikasi Piawaian', + 'Language:' => 'Bahasa:', + 'Timezone:' => 'Zon masa:', + 'All columns' => 'Semua kolom', + 'Calendar' => 'Kalender', + 'Next' => 'Selanjutnya', + '#%d' => 'n°%d', + 'All swimlanes' => 'Semua swimlane', + 'All colors' => 'Semua warna', + 'Moved to column %s' => 'Pindah ke kolom %s', + 'Change description' => 'Ubah keterangan', + 'User dashboard' => 'Papan Kenyataan pengguna', + 'Allow only one subtask in progress at the same time for a user' => 'Izinkan hanya satu subtugas dalam proses secara bersamaan untuk satu pengguna', + 'Edit column "%s"' => 'Modifikasi kolom « %s »', + 'Select the new status of the subtask: "%s"' => 'Pilih status baru untuk subtugas : « %s »', + 'Subtask timesheet' => 'Subtugas absen', + 'There is nothing to show.' => 'Tidak ada yang dapat diperlihatkan.', + 'Time Tracking' => 'Pelacakan waktu', + 'You already have one subtask in progress' => 'Anda sudah ada satu subtugas dalam proses', + 'Which parts of the project do you want to duplicate?' => 'Bagian dalam projek mana yang ingin anda duplikasi?', + 'Disallow login form' => 'Larang formulir masuk', + 'Start' => 'Mula', + 'End' => 'Selesai', + 'Task age in days' => 'Usia tugas dalam bentuk harian', + 'Days in this column' => 'Hari dalam kolom ini', + '%dd' => '%dj', + 'Add a link' => 'Menambahkan pautan', + 'Add a new link' => 'Tambah Pautan baru', + 'Do you really want to remove this link: "%s"?' => 'Anda yakin akan menghapus Pautan ini : « %s » ?', + 'Do you really want to remove this link with task #%d?' => 'Anda yakin akan menghapus Pautan ini dengan tugas n°%d ?', + 'Field required' => 'Medan diperlukan', + 'Link added successfully.' => 'Pautan berhasil ditambahkan.', + 'Link updated successfully.' => 'Pautan berhasil diperbaharui.', + 'Link removed successfully.' => 'Pautan berhasil dihapus.', + 'Link labels' => 'Label Pautan', + 'Link modification' => 'Modifikasi Pautan', + 'Links' => 'Pautan', + 'Link settings' => 'Pengaturan Pautan', + 'Opposite label' => 'Label berlawanan', + 'Remove a link' => 'Hapus Pautan', + 'Task\'s links' => 'Pautan tugas', + 'The labels must be different' => 'Label harus berbeda', + 'There is no link.' => 'Tidak ada Pautan.', + 'This label must be unique' => 'Label ini harus unik', + 'Unable to create your link.' => 'Tidak dapat membuat Pautan anda.', + 'Unable to update your link.' => 'Tidak dapat memperbaharui Pautan anda.', + 'Unable to remove this link.' => 'Tidak dapat menghapus Pautan ini.', + 'relates to' => 'berhubungan dengan', + 'blocks' => 'blok', + 'is blocked by' => 'diblokir oleh', + 'duplicates' => 'duplikat', + 'is duplicated by' => 'diduplikasi oleh', + 'is a child of' => 'anak dari', + 'is a parent of' => 'orant tua dari', + 'targets milestone' => 'milestone target', + 'is a milestone of' => 'adalah milestone dari', + 'fixes' => 'perbaikan', + 'is fixed by' => 'diperbaiki oleh', + 'This task' => 'Tugas ini', + '<1h' => '<1h', + '%dh' => '%dh', + '%b %e' => '%e %b', + 'Expand tasks' => 'Perluas tugas', + 'Collapse tasks' => 'Lipat tugas', + 'Expand/collapse tasks' => 'Perluas/lipat tugas', + 'Close dialog box' => 'Tutup kotak dialog', + 'Submit a form' => 'Submit formulir', + 'Board view' => 'Table halaman', + 'Keyboard shortcuts' => 'pintas keyboard', + 'Open board switcher' => 'Buka table switcher', + 'Application' => 'Aplikasi', + 'since %B %e, %Y at %k:%M %p' => 'sejak %d/%m/%Y à %H:%M', + 'Compact view' => 'Tampilan kompak', + 'Horizontal scrolling' => 'Horisontal bergulir', + 'Compact/wide view' => 'Beralih antara tampilan kompak dan diperluas', + 'No results match:' => 'Tidak ada hasil :', + 'Currency' => 'Mata uang', + 'Files' => 'Arsip', + 'Images' => 'Gambar', + 'Private project' => 'projek pribadi', + 'AUD - Australian Dollar' => 'AUD - Dollar Australia', + 'CAD - Canadian Dollar' => 'CAD - Dollar Kanada', + 'CHF - Swiss Francs' => 'CHF - Swiss Prancis', + 'Custom Stylesheet' => 'Kustomisasi Stylesheet', + 'download' => 'unduh', + 'EUR - Euro' => 'EUR - Euro', + 'GBP - British Pound' => 'GBP - Poundsterling inggris', + 'INR - Indian Rupee' => 'INR - Rupe India', + 'JPY - Japanese Yen' => 'JPY - Yen Jepang', + 'NZD - New Zealand Dollar' => 'NZD - Dollar Selandia baru', + 'RSD - Serbian dinar' => 'RSD - Dinar Serbia', + 'USD - US Dollar' => 'USD - Dollar Amerika', + 'Destination column' => 'Kolom tujuan', + 'Move the task to another column when assigned to a user' => 'Pindahkan tugas ke kolom lain ketika ditugaskan ke pengguna', + 'Move the task to another column when assignee is cleared' => 'Pindahkan tugas ke kolom lain ketika orang yang ditugaskan dibersihkan', + 'Source column' => 'Sumber kolom', + 'Transitions' => 'Transisi', + 'Executer' => 'Eksekusi', + 'Time spent in the column' => 'Waktu yang dihabiskan dalam kolom', + 'Task transitions' => 'Transisi tugas', + 'Task transitions export' => 'Ekspor transisi tugas', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Laporan ini berisi semua kolom yang pindah untuk setiap tugas dengan tanggal, pengguna dan waktu yang dihabiskan untuk setiap transisi.', + 'Currency rates' => 'Nilai tukar mata uang', + 'Rate' => 'Tarif', + 'Change reference currency' => 'Mengubah referensi mata uang', + 'Add a new currency rate' => 'Tambahkan nilai tukar mata uang baru', + 'Reference currency' => 'Referensi mata uang', + 'The currency rate have been added successfully.' => 'Nilai tukar mata uang berhasil ditambahkan.', + 'Unable to add this currency rate.' => 'Tidak dapat menambahkan nilai tukar mata uang', + 'Webhook URL' => 'URL webhook', + '%s remove the assignee of the task %s' => '%s menghapus penugasan dari tugas %s', + 'Enable Gravatar images' => 'Mengaktifkan gambar Gravatar', + 'Information' => 'Informasi', + 'Check two factor authentication code' => 'Cek dua faktor kode otentifikasi', + 'The two factor authentication code is not valid.' => 'Kode dua faktor kode otentifikasi tidak valid.', + 'The two factor authentication code is valid.' => 'Kode dua faktor kode otentifikasi valid.', + 'Code' => 'Kode', + 'Two factor authentication' => 'Dua faktor otentifikasi', + 'This QR code contains the key URI: ' => 'kode QR ini mengandung kunci URI : ', + 'Check my code' => 'Memeriksa kode saya', + 'Secret key: ' => 'Kunci rahasia : ', + 'Test your device' => 'Menguji perangkat anda', + 'Assign a color when the task is moved to a specific column' => 'Menetapkan warna ketika tugas tersebut dipindahkan ke kolom tertentu', + '%s via Kanboard' => '%s via Kanboard', + 'uploaded by: %s' => 'diunggah oleh %s', + 'uploaded on: %s' => 'diunggah pada %s', + 'size: %s' => 'ukuran : %s', + 'Burndown chart for "%s"' => 'Grafik Burndown untku « %s »', + 'Burndown chart' => 'Grafik Burndown', + 'This chart show the task complexity over the time (Work Remaining).' => 'Grafik ini menunjukkan kompleksitas tugas dari waktu ke waktu (Sisa Pekerjaan).', + 'Screenshot taken %s' => 'Screenshot diambil %s', + 'Add a screenshot' => 'Tambah screenshot', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + 'Screenshot uploaded successfully.' => 'Screenshot berhasil diunggah.', + 'SEK - Swedish Krona' => 'SEK - Krona Swedia', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Identifier projek adalah kode alfanumerik opsional digunakan untuk mengidentifikasi projek Anda.', + 'Identifier' => 'Identifier', + 'Disable two factor authentication' => 'Matikan dua faktor otentifikasi', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Apakah anda yakin akan mematikan dua faktor otentifikasi untuk pengguna ini : « %s » ?', + 'Edit link' => 'Modifikasi Pautan', + 'Start to type task title...' => 'Mulai mengetik judul tugas...', + 'A task cannot be linked to itself' => 'Sebuah tugas tidak dapat dikaitkan dengan dirinya sendiri', + 'The exact same link already exists' => 'Pautan yang sama persis sudah ada', + 'Recurrent task is scheduled to be generated' => 'Tugas berulang dijadwalkan akan dihasilkan', + 'Recurring information' => 'Informasi berulang', + 'Score' => 'Skor', + 'The identifier must be unique' => 'Identifier harus unik', + 'This linked task id doesn\'t exists' => 'Id tugas terkait tidak ada', + 'This value must be alphanumeric' => 'Nilai harus alfanumerik', + 'Edit recurrence' => 'Modifikasi pengulangan', + 'Generate recurrent task' => 'Menghasilkan tugas berulang', + 'Trigger to generate recurrent task' => 'Memicu untuk menghasilkan tugas berulang', + 'Factor to calculate new due date' => 'Faktor untuk menghitung tanggal jatuh tempo baru', + 'Timeframe to calculate new due date' => 'Jangka waktu untuk menghitung tanggal jatuh tempo baru', + 'Base date to calculate new due date' => 'Tanggal dasar untuk menghitung tanggal jatuh tempo baru', + 'Action date' => 'Tanggal aksi', + 'Base date to calculate new due date: ' => 'Tanggal dasar untuk menghitung tanggal jatuh tempo baru: ', + 'This task has created this child task: ' => 'Tugas ini telah menciptakan tugas anak ini: ', + 'Day(s)' => 'Hari', + 'Existing due date' => 'Batas waktu yang ada', + 'Factor to calculate new due date: ' => 'Faktor untuk menghitung tanggal jatuh tempo baru: ', + 'Month(s)' => 'Bulan', + 'Recurrence' => 'Pengulangan', + 'This task has been created by: ' => 'Tugas ini telah dibuat oleh:', + 'Recurrent task has been generated:' => 'Tugas berulang telah dihasilkan:', + 'Timeframe to calculate new due date: ' => 'Jangka waktu untuk menghitung tanggal jatuh tempo baru: ', + 'Trigger to generate recurrent task: ' => 'Pemicu untuk menghasilkan tugas berulang: ', + 'When task is closed' => 'Ketika tugas ditutup', + 'When task is moved from first column' => 'Ketika tugas dipindahkan dari kolom pertama', + 'When task is moved to last column' => 'Ketika tugas dipindahkan ke kolom terakhir', + 'Year(s)' => 'Tahun', + 'Calendar settings' => 'Pengaturan kalender', + 'Project calendar view' => 'Tampilan kalender projek', + 'Project settings' => 'Pengaturan projek', + 'Show subtasks based on the time tracking' => 'Tampilkan subtugas berdasarkan pelacakan waktu', + 'Show tasks based on the creation date' => 'Tampilkan tugas berdasarkan tanggal pembuatan', + 'Show tasks based on the start date' => 'Tampilkan tugas berdasarkan tanggal mulai', + 'Subtasks time tracking' => 'Pelacakan waktu subtgas', + 'User calendar view' => 'Pengguna tampilan kalender', + 'Automatically update the start date' => 'Otomatikkan pengemaskinian tanggal', + 'iCal feed' => 'iCal feed', + 'Preferences' => 'Keutamaan', + 'Security' => 'Keamanan', + 'Two factor authentication disabled' => 'Otentifikasi dua faktor dimatikan', + 'Two factor authentication enabled' => 'Otentifikasi dua faktor dihidupkan', + 'Unable to update this user.' => 'Tidak dapat memperbarui pengguna ini.', + 'There is no user management for private projects.' => 'Tidak ada manajemen pengguna untuk projek-projek pribadi.', + 'User that will receive the email' => 'Pengguna yang akan menerima email', + 'Email subject' => 'Subjek Emel', + 'Date' => 'Tanggal', + 'Add a comment log when moving the task between columns' => 'Menambahkan log komentar ketika memindahkan tugas antara kolom', + 'Move the task to another column when the category is changed' => 'Pindahkan tugas ke kolom lain ketika kategori berubah', + 'Send a task by email to someone' => 'Kirim tugas melalui email ke seseorang', + 'Reopen a task' => 'Membuka kembali tugas', + 'Column change' => 'Kolom berubah', + 'Position change' => 'Posisi berubah', + 'Swimlane change' => 'Swimlane berubah', + 'Assignee change' => 'Penerima berubah', + '[%s] Overdue tasks' => '[%s] Tugas terlambat', + 'Notification' => 'Pemberitahuan', + '%s moved the task #%d to the first swimlane' => '%s memindahkan tugas n°%d ke swimlane pertama', + '%s moved the task #%d to the swimlane "%s"' => '%s memindahkan tugas n°%d ke swimlane « %s »', + 'Swimlane' => 'Swimlane', + 'Gravatar' => 'Gravatar', + '%s moved the task %s to the first swimlane' => '%s memindahkan tugas %s ke swimlane pertama', + '%s moved the task %s to the swimlane "%s"' => '%s memindahkan tugas %s ke swimlane « %s »', + 'This report contains all subtasks information for the given date range.' => 'Laporan ini berisi semua informasi subtugas untuk rentang tanggal tertentu.', + 'This report contains all tasks information for the given date range.' => 'Laporan ini berisi semua informasi tugas untuk rentang tanggal tertentu.', + 'Project activities for %s' => 'Aktifitas projek untuk « %s »', + 'view the board on Kanboard' => 'lihat papan di Kanboard', + 'The task have been moved to the first swimlane' => 'Tugas telah dipindahkan ke swimlane pertama', + 'The task have been moved to another swimlane:' => 'Tugas telah dipindahkan ke swimlane lain:', + 'Overdue tasks for the project "%s"' => 'Tugas terlambat untuk projek « %s »', + 'New title: %s' => 'Judul baru : %s', + 'The task is not assigned anymore' => 'Tugas tidak ditugaskan lagi', + 'New assignee: %s' => 'Penerima baru : %s', + 'There is no category now' => 'Tidak ada kategori untuk sekarang', + 'New category: %s' => 'Kategori baru : %s', + 'New color: %s' => 'Warna baru : %s', + 'New complexity: %d' => 'Kompleksitas baru : %d', + 'The due date have been removed' => 'Tanggal jatuh tempo telah dihapus', + 'There is no description anymore' => 'Tidak ada deskripsi lagi', + 'Recurrence settings have been modified' => 'Pengaturan pengulangan telah dimodifikasi', + 'Time spent changed: %sh' => 'Waktu yang dihabiskan berubah : %sh', + 'Time estimated changed: %sh' => 'Perkiraan waktu berubah : %sh', + 'The field "%s" have been updated' => 'Field « %s » telah diperbaharui', + 'The description have been modified' => 'Deskripsi telah dimodifikasi', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'Apakah anda yakin akan menutup tugas « %s » beserta semua sub-tugasnya ?', + 'Swimlane: %s' => 'Swimlane : %s', + 'I want to receive notifications for:' => 'Saya ingin menerima pemberitahuan untuk :', + 'All tasks' => 'Semua tugas', + 'Only for tasks assigned to me' => 'Hanya untuk tugas yang ditugaskan ke saya', + 'Only for tasks created by me' => 'Hanya untuk tugas yang dibuat oleh saya', + 'Only for tasks created by me and assigned to me' => 'Hanya untuk tugas yang dibuat oleh saya dan ditugaskan ke saya', + '%A' => '%A', + '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M', + 'New due date: %B %e, %Y' => 'Tanggal jatuh tempo baru : %d/%m/%Y', + 'Start date changed: %B %e, %Y' => 'Tanggal mulai berubah : %d/%m/%Y', + '%k:%M %p' => '%H:%M', + '%%Y-%%m-%%d' => '%%d/%%m/%%Y', + 'Total for all columns' => 'Total untuk semua kolom', + 'You need at least 2 days of data to show the chart.' => 'Anda memerlukan setidaknya 2 hari dari data yang menunjukkan grafik.', + '<15m' => '<15m', + '<30m' => '<30m', + 'Stop timer' => 'Hentikan timer', + 'Start timer' => 'Mulai timer', + 'Add project member' => 'Tambahkan anggota projek', + 'Enable notifications' => 'Aktifkan pemberitahuan', + 'My activity stream' => 'Aliran kegiatan saya', + 'My calendar' => 'Kalender saya', + 'Search tasks' => 'Cari tugas', + 'Back to the calendar' => 'Kembali ke kalender', + 'Filters' => 'Filter', + 'Reset filters' => 'Reset ulang filter', + 'My tasks due tomorrow' => 'Tugas saya yang berakhir besok', + 'Tasks due today' => 'Tugas yang berakhir hari ini', + 'Tasks due tomorrow' => 'Tugas yang berakhir besok', + 'Tasks due yesterday' => 'Tugas yang berakhir kemarin', + 'Closed tasks' => 'Tugas yang ditutup', + 'Open tasks' => 'Buka Tugas', + 'Not assigned' => 'Tidak ditugaskan', + 'View advanced search syntax' => 'Lihat sintaks pencarian lanjutan', + 'Overview' => 'Ikhtisar', + '%b %e %Y' => '%b %e %Y', + 'Board/Calendar/List view' => 'Tampilan Papan/Kalender/Daftar', + 'Switch to the board view' => 'Beralih ke tampilan papan', + 'Switch to the calendar view' => 'Beralih ke tampilan kalender', + 'Switch to the list view' => 'Beralih ke tampilan daftar', + 'Go to the search/filter box' => 'Pergi ke kotak pencarian/filter', + 'There is no activity yet.' => 'Tidak ada aktifitas saat ini.', + 'No tasks found.' => 'Tidak ada tugas yang ditemukan.', + 'Keyboard shortcut: "%s"' => 'Keyboard shortcut : « %s »', + 'List' => 'Daftar', + 'Filter' => 'Filter', + 'Advanced search' => 'Pencarian lanjutan', + 'Example of query: ' => 'Contoh dari query : ', + 'Search by project: ' => 'Pencarian berdasarkan projek : ', + 'Search by column: ' => 'Pencarian berdasarkan kolom : ', + 'Search by assignee: ' => 'Pencarian berdasarkan penerima : ', + 'Search by color: ' => 'Pencarian berdasarkan warna : ', + 'Search by category: ' => 'Pencarian berdasarkan kategori : ', + 'Search by description: ' => 'Pencarian berdasarkan deskripsi : ', + 'Search by due date: ' => 'Pencarian berdasarkan tanggal jatuh tempo : ', + 'Lead and Cycle time for "%s"' => 'Memimpin dan Siklus waktu untuk « %s »', + 'Average time spent into each column for "%s"' => 'Rata-rata waktu yang dihabiskan dalam setiap kolom untuk « %s »', + 'Average time spent into each column' => 'Rata-rata waktu yang dihabiskan dalam setiap kolom', + 'Average time spent' => 'Rata-rata waktu yang dihabiskan', + 'This chart show the average time spent into each column for the last %d tasks.' => 'Grafik ini menunjukkan rata-rata waktu yang dihabiskan dalam setiap kolom untuk %d tugas.', + 'Average Lead and Cycle time' => 'Rata-rata Memimpin dan Siklus waktu', + 'Average lead time: ' => 'Rata-rata waktu pimpinan : ', + 'Average cycle time: ' => 'Rata-rata siklus waktu : ', + 'Cycle Time' => 'Siklus Waktu', + 'Lead Time' => 'Lead Time', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Grafik ini menunjukkan memimpin rata-rata dan waktu siklus untuk %d tugas terakhir dari waktu ke waktu.', + 'Average time into each column' => 'Rata-rata waktu ke setiap kolom', + 'Lead and cycle time' => 'Lead dan siklus waktu', + 'Google Authentication' => 'Google Otentifikasi', + 'Help on Google authentication' => 'Bantuan pada otentifikasi Google', + 'Github Authentication' => 'Otentifikasi Github', + 'Help on Github authentication' => 'Bantuan pada otentifikasi Github', + 'Lead time: ' => 'Lead time : ', + 'Cycle time: ' => 'Siklus waktu : ', + 'Time spent into each column' => 'Waktu yang dihabiskan di setiap kolom', + 'The lead time is the duration between the task creation and the completion.' => 'Lead time adalah durasi antara pembuatan tugas dan penyelesaian.', + 'The cycle time is the duration between the start date and the completion.' => 'Siklus waktu adalah durasi antara tanggal mulai dan tanggal penyelesaian.', + 'If the task is not closed the current time is used instead of the completion date.' => 'Jika tugas tidak ditutup waktu saat ini yang digunakan sebagai pengganti tanggal penyelesaian.', + 'Set automatically the start date' => 'Secara otomatis mengatur tanggal mulai', + 'Edit Authentication' => 'Modifikasi Otentifikasi', + 'Google Id' => 'Id Google', + 'Github Id' => 'Id Github', + 'Remote user' => 'Pengguna jauh', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Pengguna jauh tidak menyimpan kata laluan mereka dalam basis data Kanboard, contoh: Akaun LDAP, Google dan Github.', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Jika anda mencentang kotak "Larang formulir login", kredensial masuk ke formulis login akan diabaikan.', + 'New remote user' => 'Pengguna baru jauh', + 'New local user' => 'Pengguna baru lokal', + 'Default task color' => 'Standar warna tugas', + 'Hide sidebar' => 'Sembunyikan sidebar', + 'Expand sidebar' => 'Perluas sidebar', + 'This feature does not work with all browsers.' => 'Ciri ini tidak dapat digunakan pada semua browsers', + 'There is no destination project available.' => 'Tiada destinasi projek yang tersedia.', + 'Trigger automatically subtask time tracking' => 'Picu pengesanan subtugas secara otomatik', + 'Include closed tasks in the cumulative flow diagram' => 'Termasuk tugas yang ditutup pada diagram aliran kumulatif', + 'Current swimlane: %s' => 'Swimlane saat ini : %s', + 'Current column: %s' => 'Kolom saat ini : %s', + 'Current category: %s' => 'Kategori saat ini : %s', + 'no category' => 'tiada kategori', + 'Current assignee: %s' => 'Saat ini ditugaskan pada: %s', + 'not assigned' => 'Belum ditugaskan', + 'Author:' => 'Penulis:', + 'contributors' => 'Penggiat', + 'License:' => 'Lesen:', + 'License' => 'Lesen', + 'Enter the text below' => 'Masukkan teks di bawah', + 'Gantt chart for %s' => 'Carta Gantt untuk %s', + 'Sort by position' => 'Urutkan berdasarkan posisi', + 'Sort by date' => 'Urutkan berdasarkan tanggal', + 'Add task' => 'Tambah tugas', + 'Start date:' => 'Tanggal mulai:', + 'Due date:' => 'Batas waktu:', + 'There is no start date or due date for this task.' => 'Tiada tanggal mulai dan batas waktu untuk tugas ini.', + 'Moving or resizing a task will change the start and due date of the task.' => 'Memindahkan atau mengubah ukuran tugas anda akan mengubah tanggal mulai dan batas waktu dari tugas ini.', + 'There is no task in your project.' => 'Tiada tugas didalam projek anda.', + 'Gantt chart' => 'Carta Gantt', + 'People who are project managers' => 'Orang-orang yang menjadi pengurus projek', + 'People who are project members' => 'Orang-orang yang menjadi anggota projek', + 'NOK - Norwegian Krone' => 'NOK - Krone Norwegia', + 'Show this column' => 'Perlihatkan kolom ini', + 'Hide this column' => 'Sembunyikan kolom ini', + 'open file' => 'buka fail', + 'End date' => 'Waktu berakhir', + 'Users overview' => 'Ikhtisar pengguna', + 'Managers' => 'Pengurus', + 'Members' => 'Anggota', + 'Shared project' => 'projek bersama', + 'Project managers' => 'Pengurus projek', + 'Gantt chart for all projects' => 'Carta Gantt untuk kesemua projek', + 'Projects list' => 'Senarai projek', + 'Gantt chart for this project' => 'Carta Gantt untuk projek ini', + 'Project board' => 'Papan projek', + 'End date:' => 'Waktu berakhir :', + 'There is no start date or end date for this project.' => 'Tidak ada waktu mula atau waktu berakhir pada projek ini', + 'Projects Gantt chart' => 'projekkan carta Gantt', + 'Start date: %s' => 'Waktu mulai: %s', + 'End date: %s' => 'Waktu berakhir: %s', + 'Link type' => 'Jenis pautan', + 'Change task color when using a specific task link' => 'Rubah warna tugas ketika menggunakan Pautan tugas yang spesifik', + 'Task link creation or modification' => 'Pautan tugas pada penciptaan atau penyuntingan', + 'Login with my Gitlab Account' => 'Masuk menggunakan Akaun Gitlab saya', + 'Milestone' => 'Batu Tanda', + 'Gitlab Authentication' => 'Otentifikasi Gitlab', + 'Help on Gitlab authentication' => 'Bantuan pada otentifikasi Gitlab', + 'Gitlab Id' => 'Id Gitlab', + 'Gitlab Account' => 'Akaun Gitlab', + 'Link my Gitlab Account' => 'Hubungkan akaun Gitlab saya', + 'Unlink my Gitlab Account' => 'Putuskan akaun Gitlab saya', + 'Documentation: %s' => 'Dokumentasi : %s', + 'Switch to the Gantt chart view' => 'Beralih ke tampilan Carta Gantt', + 'Reset the search/filter box' => 'Tetap semula pencarian/saringan', + 'Documentation' => 'Dokumentasi', + 'Table of contents' => 'Isi kandungan', + 'Gantt' => 'Gantt', + // 'Author' => '', + // 'Version' => '', + // 'Plugins' => '', + // 'There is no plugin loaded.' => '', + // 'Set maximum column height' => '', + // 'Remove maximum column height' => '', + // 'My notifications' => '', + // 'Custom filters' => '', + // 'Your custom filter have been created successfully.' => '', + // 'Unable to create your custom filter.' => '', + // 'Custom filter removed successfully.' => '', + // 'Unable to remove this custom filter.' => '', + // 'Edit custom filter' => '', + // 'Your custom filter have been updated successfully.' => '', + // 'Unable to update custom filter.' => '', + // 'Web' => '', + // 'New attachment on task #%d: %s' => '', + // 'New comment on task #%d' => '', + // 'Comment updated on task #%d' => '', + // 'New subtask on task #%d' => '', + // 'Subtask updated on task #%d' => '', + // 'New task #%d: %s' => '', + // 'Task updated #%d' => '', + // 'Task #%d closed' => '', + // 'Task #%d opened' => '', + // 'Column changed for task #%d' => '', + // 'New position for task #%d' => '', + // 'Swimlane changed for task #%d' => '', + // 'Assignee changed on task #%d' => '', + // '%d overdue tasks' => '', + // 'Task #%d is overdue' => '', + // 'No new notifications.' => '', + // 'Mark all as read' => '', + // 'Mark as read' => '', + // 'Total number of tasks in this column across all swimlanes' => '', + // 'Collapse swimlane' => '', + // 'Expand swimlane' => '', + // 'Add a new filter' => '', + // 'Share with all project members' => '', + // 'Shared' => '', + // 'Owner' => '', + // 'Unread notifications' => '', + // 'My filters' => '', + // 'Notification methods:' => '', + // 'Import tasks from CSV file' => '', + // 'Unable to read your file' => '', + // '%d task(s) have been imported successfully.' => '', + // 'Nothing have been imported!' => '', + // 'Import users from CSV file' => '', + // '%d user(s) have been imported successfully.' => '', + // 'Comma' => '', + // 'Semi-colon' => '', + // 'Tab' => '', + // 'Vertical bar' => '', + // 'Double Quote' => '', + // 'Single Quote' => '', + // '%s attached a file to the task #%d' => '', + // 'There is no column or swimlane activated in your project!' => '', + // 'Append filter (instead of replacement)' => '', + // 'Append/Replace' => '', + // 'Append' => '', + // 'Replace' => '', + // 'Import' => '', + // 'change sorting' => '', + // 'Tasks Importation' => '', + // 'Delimiter' => '', + // 'Enclosure' => '', + // 'CSV File' => '', + // 'Instructions' => '', + // 'Your file must use the predefined CSV format' => '', + // 'Your file must be encoded in UTF-8' => '', + // 'The first row must be the header' => '', + // 'Duplicates are not verified for you' => '', + // 'The due date must use the ISO format: YYYY-MM-DD' => '', + // 'Download CSV template' => '', + // 'No external integration registered.' => '', + // 'Duplicates are not imported' => '', + // 'Usernames must be lowercase and unique' => '', + // 'Passwords will be encrypted if present' => '', + // '%s attached a new file to the task %s' => '', + // 'Assign automatically a category based on a link' => '', + // 'BAM - Konvertible Mark' => '', + // 'Assignee Username' => '', + // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + 'Role:' => 'Peranan', + 'Project members' => 'Anggota projek', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + 'Creation' => 'Ciptaan', + 'Expiration' => 'Jangka hayat', + 'Password reset history' => 'Sirah tetap semula kata laluan', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', +); diff --git a/sources/app/Locale/nb_NO/translations.php b/sources/app/Locale/nb_NO/translations.php index b0fa064..04f8e6a 100644 --- a/sources/app/Locale/nb_NO/translations.php +++ b/sources/app/Locale/nb_NO/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Kompleksitet', 'Task limit' => 'Oppgave begrensning', 'Task count' => 'Antall oppgaver', - 'Edit project access list' => 'Endre tillatelser for prosjektet', - 'Allow this user' => 'Tillat denne brukeren', - 'Don\'t forget that administrators have access to everything.' => 'Husk at administratorer har tilgang til alt.', - 'Revoke' => 'Fjern', - 'List of authorized users' => 'Liste over autoriserte brukere', 'User' => 'Bruker', - 'Nobody have access to this project.' => 'Ingen har tilgang til dette prosjektet.', 'Comments' => 'Kommentarer', 'Write your text in Markdown' => 'Skriv din tekst i markdown', 'Leave a comment' => 'Legg inn en kommentar', @@ -396,8 +390,6 @@ return array( 'Email:' => 'Epost:', 'Notifications:' => 'Varslinger:', 'Notifications' => 'Varslinger', - 'Group:' => 'Gruppe:', - 'Regular user' => 'Normal bruker', 'Account type:' => 'Konto type:', 'Edit profile' => 'Rediger profil', 'Change password' => 'Endre passord', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s endret ansvarlig for oppgaven %s til %s', 'New password for the user "%s"' => 'Nytt passord for brukeren "%s"', 'Choose an event' => 'Velg en hendelse', - 'Github commit received' => 'Github forpliktelse mottatt', - 'Github issue opened' => 'Github problem åpnet', - 'Github issue closed' => 'Github problem lukket', - 'Github issue reopened' => 'Github problem gjenåpnet', - 'Github issue assignee change' => 'Endre ansvarlig for Github problem', - 'Github issue label change' => 'Endre etikett for Github problem', 'Create a task from an external provider' => 'Oppret en oppgave fra en ekstern tilbyder', 'Change the assignee based on an external username' => 'Endre ansvarlige baseret på et eksternt brukernavn', 'Change the category based on an external label' => 'Endre kategorien basert på en ekstern etikett', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Alle har tilgang til dette prosjektet', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', 'Create a comment from an external provider' => 'Opprett en kommentar fra en ekstern tilbyder', - // 'Github issue comment created' => '', 'Project management' => 'Prosjektinnstillinger', 'My projects' => 'Mine prosjekter', 'Columns' => 'Kolonner', @@ -516,7 +499,6 @@ return array( // 'User repartition for "%s"' => '', 'Clone this project' => 'Kopier dette prosjektet', 'Column removed successfully.' => 'Kolonne flyttet', - // 'Github Issue' => '', // 'Not enough data to show the graph.' => '', 'Previous' => 'Forrige', // 'The id must be an integer' => '', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Standard svømmebane', // 'Do you really want to remove this swimlane: "%s"?' => '', // 'Inactive swimlanes' => '', - 'Set project manager' => 'Velg prosjektleder', - 'Set project member' => 'Velg prosjektmedlem', 'Remove a swimlane' => 'Fjern en svømmebane', 'Rename' => 'Endre navn', 'Show default swimlane' => 'Vis standard svømmebane', @@ -563,18 +543,8 @@ return array( // 'Your swimlane have been created successfully.' => '', // 'Example: "Bug, Feature Request, Improvement"' => '', // 'Default categories for new projects (Comma-separated)' => '', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', 'Integrations' => 'Integrasjoner', 'Integration with third-party services' => 'Integrasjoner med tredje-parts tjenester', - 'Role for this project' => 'Rolle for dette prosjektet', - 'Project manager' => 'Prosjektleder', - 'Project member' => 'Prosjektmedlem', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Prosjektlederen kan endre flere innstillinger for prosjektet enn den en vanlig bruker kan.', - // 'Gitlab Issue' => '', 'Subtask Id' => 'Deloppgave ID', 'Subtasks' => 'Deloppgaver', 'Subtasks Export' => 'Eksporter deloppgaver', @@ -602,9 +572,6 @@ return array( // 'You already have one subtask in progress' => '', 'Which parts of the project do you want to duplicate?' => 'Hvilke deler av dette prosjektet ønsker du å kopiere?', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', 'Start' => 'Start', 'End' => 'Slutt', 'Task age in days' => 'Dager siden oppgaven ble opprettet', @@ -702,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', 'Two factor authentication' => 'Dobbel godkjenning', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -776,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', 'Date' => 'Dato', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', 'Add a comment log when moving the task between columns' => 'Legg til en kommentar i loggen når en oppgave flyttes mellom kolonnene', 'Move the task to another column when the category is changed' => 'Flytt oppgaven til en annen kolonne når kategorien endres', 'Send a task by email to someone' => 'Send en oppgave på epost til noen', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', 'Column change' => 'Endret kolonne', 'Position change' => 'Posisjonsendring', 'Swimlane change' => 'Endret svømmebane', @@ -910,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', 'New remote user' => 'Ny eksternbruker', 'New local user' => 'Ny internbruker', 'Default task color' => 'Standard oppgavefarge', @@ -931,7 +883,6 @@ return array( 'contributors' => 'bidragsytere', 'License:' => 'Lisens:', 'License' => 'Lisens', - 'Project Administrator' => 'Prosjektadministrator', 'Enter the text below' => 'Legg inn teksten nedenfor', 'Gantt chart for %s' => 'Gantt skjema for %s', // 'Sort by position' => '', @@ -955,7 +906,6 @@ return array( 'Members' => 'Medlemmer', 'Shared project' => 'Delt prosjekt', 'Project managers' => 'Prosjektledere', - 'Project members' => 'Prosjektmedlemmer', // 'Gantt chart for all projects' => '', 'Projects list' => 'Prosjektliste', 'Gantt chart for this project' => 'Gantt skjema for dette prosjektet', @@ -982,7 +932,6 @@ return array( 'Documentation' => 'Dokumentasjon', 'Table of contents' => 'Innholdsfortegnelse', 'Gantt' => 'Gantt', - 'Help with project permissions' => 'Hjelp med prosjekttilganger', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + 'Project members' => 'Prosjektmedlemmer', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/nl_NL/translations.php b/sources/app/Locale/nl_NL/translations.php index eaeca45..420f835 100644 --- a/sources/app/Locale/nl_NL/translations.php +++ b/sources/app/Locale/nl_NL/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Complexiteit', 'Task limit' => 'Taak limiet.', 'Task count' => 'Aantal taken', - 'Edit project access list' => 'Aanpassen toegangsrechten project', - 'Allow this user' => 'Deze gebruiker toestaan', - 'Don\'t forget that administrators have access to everything.' => 'Vergeet niet dat administrators overal toegang hebben.', - 'Revoke' => 'Intrekken', - 'List of authorized users' => 'Lijst met geautoriseerde gebruikers', 'User' => 'Gebruiker', - 'Nobody have access to this project.' => 'Niemand heeft toegang tot dit project', 'Comments' => 'Commentaar', 'Write your text in Markdown' => 'Schrijf uw tekst in Markdown', 'Leave a comment' => 'Schrijf een commentaar', @@ -396,8 +390,6 @@ return array( 'Email:' => 'Email :', 'Notifications:' => 'Notificaties :', 'Notifications' => 'Notificaties', - 'Group:' => 'Groep :', - 'Regular user' => 'Normale gebruiker', 'Account type:' => 'Account type:', 'Edit profile' => 'Profiel aanpassen', 'Change password' => 'Wachtwoord aanpassen', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s heeft de toegewezene voor taak %s veranderd in %s', 'New password for the user "%s"' => 'Nieuw wachtwoord voor gebruiker « %s »', 'Choose an event' => 'Kies een gebeurtenis', - 'Github commit received' => 'Github commentaar ontvangen', - 'Github issue opened' => 'Github issue geopend', - 'Github issue closed' => 'Github issue gesloten', - 'Github issue reopened' => 'Github issue heropend', - 'Github issue assignee change' => 'Github toegewezen veranderd', - 'Github issue label change' => 'Github issue label verander', 'Create a task from an external provider' => 'Maak een taak aan vanuit een externe provider', 'Change the assignee based on an external username' => 'Verander de toegewezene aan de hand van de externe gebruikersnaam', 'Change the category based on an external label' => 'Verander de categorie aan de hand van een extern label', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Iedereen heeft toegang tot dit project.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Hulp bij Github webhooks', 'Create a comment from an external provider' => 'Voeg een commentaar toe van een externe provider', - 'Github issue comment created' => 'Github issue commentaar aangemaakt', 'Project management' => 'Project management', 'My projects' => 'Mijn projecten', 'Columns' => 'Kolommen', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Gebruikerverdeling voor « %s »', 'Clone this project' => 'Kloon dit project', 'Column removed successfully.' => 'Kolom succesvol verwijderd.', - 'Github Issue' => 'Github issue', 'Not enough data to show the graph.' => 'Niet genoeg data om de grafiek te laten zien.', // 'Previous' => '', 'The id must be an integer' => 'Het id moet een integer zijn', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Standaard swinlane', 'Do you really want to remove this swimlane: "%s"?' => 'Weet u zeker dat u deze swimlane wil verwijderen : « %s » ?', 'Inactive swimlanes' => 'Inactieve swinlanes', - 'Set project manager' => 'Project manager instellen', - 'Set project member' => 'Project lid instellen', 'Remove a swimlane' => 'Verwijder swinlane', 'Rename' => 'Hernoemen', 'Show default swimlane' => 'Standaard swimlane tonen', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Swimlane succesvol aangemaakt.', 'Example: "Bug, Feature Request, Improvement"' => 'Voorbeeld: « Bug, Feature Request, Improvement »', 'Default categories for new projects (Comma-separated)' => 'Standaard categorieën voor nieuwe projecten (komma gescheiden)', - 'Gitlab commit received' => 'Gitlab commir ontvangen', - 'Gitlab issue opened' => 'Gitlab issue geopend', - 'Gitlab issue closed' => 'Gitlab issue gesloten', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Hulp bij Gitlab webhooks', 'Integrations' => 'Integraties', 'Integration with third-party services' => 'Integratie met derde-partij-services', - 'Role for this project' => 'Rol voor dit project', - 'Project manager' => 'Project manager', - 'Project member' => 'Project lid', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Een project manager kan de instellingen van het project wijzigen en heeft meer rechten dan een normale gebruiker.', - 'Gitlab Issue' => 'Gitlab issue', 'Subtask Id' => 'Subtaak id', 'Subtasks' => 'Subtaken', 'Subtasks Export' => 'Subtaken exporteren', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'U heeft al een subtaak in behandeling', 'Which parts of the project do you want to duplicate?' => 'Welke onderdelen van het project wilt u dupliceren?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket commit ontvangen', - 'Bitbucket webhooks' => 'Bitbucket webhooks', - 'Help on Bitbucket webhooks' => 'Help bij Bitbucket webhooks', 'Start' => 'Start', 'End' => 'Eind', 'Task age in days' => 'Leeftijd taak in dagen', @@ -702,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -776,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -910,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -931,7 +883,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +906,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/pl_PL/translations.php b/sources/app/Locale/pl_PL/translations.php index 7419e13..ecea090 100644 --- a/sources/app/Locale/pl_PL/translations.php +++ b/sources/app/Locale/pl_PL/translations.php @@ -20,15 +20,15 @@ return array( 'Red' => 'Czerwony', 'Orange' => 'Pomarańczowy', 'Grey' => 'Szary', - // 'Brown' => '', - // 'Deep Orange' => '', - // 'Dark Grey' => '', - // 'Pink' => '', - // 'Teal' => '', - // 'Cyan' => '', - // 'Lime' => '', - // 'Light Green' => '', - // 'Amber' => '', + 'Brown' => 'Brąz', + 'Deep Orange' => 'Ciemnopomarańczowy', + 'Dark Grey' => 'Ciemnoszary', + 'Pink' => 'Różowy', + 'Teal' => 'Turkusowy', + 'Cyan' => 'Cyjan', + 'Lime' => 'Limonkowy', + 'Light Green' => 'Jasnozielony', + 'Amber' => 'Amber', 'Save' => 'Zapisz', 'Login' => 'Login', 'Official website:' => 'Oficjalna strona:', @@ -148,7 +148,7 @@ return array( 'Task created successfully.' => 'Zadanie zostało utworzone.', 'User created successfully.' => 'Użytkownik dodany', 'Unable to create your user.' => 'Nie udało się dodać użytkownika.', - 'User updated successfully.' => 'Użytkownik zaktualizowany.', + 'User updated successfully.' => 'Profil użytkownika został zaaktualizowany.', 'Unable to update your user.' => 'Nie udało się zaktualizować użytkownika.', 'User removed successfully.' => 'Użytkownik usunięty.', 'Unable to remove this user.' => 'Nie udało się usunąć użytkownika.', @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Poziom trudności', 'Task limit' => 'Limit zadań', 'Task count' => 'Liczba zadań', - 'Edit project access list' => 'Edycja list dostępu dla projektu', - 'Allow this user' => 'Dodaj użytkownika', - 'Don\'t forget that administrators have access to everything.' => 'Pamiętaj: Administratorzy mają zawsze dostęp do wszystkiego!', - 'Revoke' => 'Odbierz dostęp', - 'List of authorized users' => 'Lista użytkowników mających dostęp', 'User' => 'Użytkownik', - 'Nobody have access to this project.' => 'Żaden użytkownik nie ma dostępu do tego projektu', 'Comments' => 'Komentarze', 'Write your text in Markdown' => 'Możesz użyć Markdown', 'Leave a comment' => 'Zostaw komentarz', @@ -370,7 +364,7 @@ return array( 'Task updated' => 'Zaktualizowane zadanie', 'Task closed' => 'Zadanie zamknięte', 'Task opened' => 'Zadanie otwarte', - 'I want to receive notifications only for those projects:' => 'Chcę otrzymywać powiadiomienia tylko dla tych projektów:', + 'I want to receive notifications only for those projects:' => 'Chcę otrzymywać powiadomienia tylko dla poniższych projektów:', 'view the task on Kanboard' => 'Zobacz zadanie', 'Public access' => 'Dostęp publiczny', 'User management' => 'Zarządzanie użytkownikami', @@ -396,8 +390,6 @@ return array( 'Email:' => 'Email: ', 'Notifications:' => 'Powiadomienia: ', 'Notifications' => 'Powiadomienia', - 'Group:' => 'Grupa:', - 'Regular user' => 'Zwykły użytkownik', 'Account type:' => 'Typ konta:', 'Edit profile' => 'Edytuj profil', 'Change password' => 'Zmień hasło', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s zmienił osobę odpowiedzialną za zadanie %s na %s', 'New password for the user "%s"' => 'Nowe hasło użytkownika "%s"', 'Choose an event' => 'Wybierz zdarzenie', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', 'Create a task from an external provider' => 'Utwórz zadanie z dostawcy zewnętrznego', 'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika', 'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrznej etykiety', @@ -495,14 +481,11 @@ return array( 'Everybody have access to this project.' => 'Wszyscy mają dostęp do tego projektu.', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', 'Create a comment from an external provider' => 'Utwórz komentarz od zewnętrznego dostawcy', - // 'Github issue comment created' => '', 'Project management' => 'Menadżer projektu', 'My projects' => 'Moje projekty', 'Columns' => 'Kolumny', - 'Task' => 'zadania', + 'Task' => 'Zadanie', 'Your are not member of any project.' => 'Nie bierzesz udziału w żadnym projekcie', 'Percentage' => 'Procent', 'Number of tasks' => 'Liczba zadań', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Przydział użytkownika dla "%s"', 'Clone this project' => 'Sklonuj ten projekt', 'Column removed successfully.' => 'Kolumna usunięta pomyślnie.', - // 'Github Issue' => '', 'Not enough data to show the graph.' => 'Za mało danych do utworzenia wykresu.', 'Previous' => 'Poprzedni', 'The id must be an integer' => 'ID musi być liczbą całkowitą', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Domyślny proces', 'Do you really want to remove this swimlane: "%s"?' => 'Czy na pewno chcesz usunąć proces: "%s"?', 'Inactive swimlanes' => 'Nieaktywne procesy', - 'Set project manager' => 'Ustaw menadżera projektu', - 'Set project member' => 'Ustaw członka projektu', 'Remove a swimlane' => 'Usuń proces', 'Rename' => 'Zmień nazwe', 'Show default swimlane' => 'Pokaż domyślny proces', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Proces tworzony pomyślnie.', 'Example: "Bug, Feature Request, Improvement"' => 'Przykład: "Błąd, Żądanie Funkcjonalności, Udoskonalenia"', 'Default categories for new projects (Comma-separated)' => 'Domyślne kategorie dla nowych projektów (oddzielone przecinkiem)', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', 'Integrations' => 'Integracje', 'Integration with third-party services' => 'Integracja z usługami firm trzecich', - 'Role for this project' => 'Rola w tym projekcie', - 'Project manager' => 'Manadżer projektu', - 'Project member' => 'Członek projektu', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Menadżer projektu może zmieniać ustawienia projektu i posiada większe uprawnienia od zwykłego użytkownika', - // 'Gitlab Issue' => '', 'Subtask Id' => 'ID pod-zadania', 'Subtasks' => 'Pod-zadania', 'Subtasks Export' => 'Eksport pod-zadań', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Masz już zadanie o statusie "w trakcie"', 'Which parts of the project do you want to duplicate?' => 'Które elementy projektu chcesz zduplikować?', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', 'Start' => 'Początek', 'End' => 'Koniec', 'Task age in days' => 'Wiek zadania w dniach', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Kod weryfikujący poprawny', 'Code' => 'Kod', 'Two factor authentication' => 'Uwierzytelnianie dwustopniowe', - 'Enable/disable two factor authentication' => 'Włącz/Wyłącz uwierzytelnianie dwustopniowe', 'This QR code contains the key URI: ' => 'Ten kod QR zawiera URI klucza: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Zapisz sekretny klucz w swoim oprogramowaniu TOTP (na przykład FreeOTP lub Google Authenticator)', 'Check my code' => 'Sprawdź kod', 'Secret key: ' => 'Tajny kod: ', 'Test your device' => 'Przetestuj urządzenie', @@ -713,8 +678,8 @@ return array( 'uploaded by: %s' => 'Dodane przez: %s', 'uploaded on: %s' => 'Data dodania: %s', 'size: %s' => 'Rozmiar: %s', - // 'Burndown chart for "%s"' => '', - // 'Burndown chart' => '', + 'Burndown chart for "%s"' => 'Wykres Burndown dla "%s"', + 'Burndown chart' => 'Wykres Burndown', // 'This chart show the task complexity over the time (Work Remaining).' => '', 'Screenshot taken %s' => 'Zrzut ekranu zapisany %s', 'Add a screenshot' => 'Dodaj zrzut ekranu', @@ -776,27 +741,16 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', - // 'Send a task by email to someone' => '', - // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', - // 'Column change' => '', - // 'Position change' => '', - // 'Swimlane change' => '', - // 'Assignee change' => '', + 'Send a task by email to someone' => 'Wyślij zadanie mailem do kogokolwiek', + 'Reopen a task' => 'Otwórz ponownie zadanie', + 'Column change' => 'Zmiana kolumny', + 'Position change' => 'Zmiana pozycji', + 'Swimlane change' => 'Zmiana Swimlane', + 'Assignee change' => 'Zmiana przypisanego użytkownika', // '[%s] Overdue tasks' => '', - // 'Notification' => '', + 'Notification' => 'Powiadomienie', // '%s moved the task #%d to the first swimlane' => '', // '%s moved the task #%d to the swimlane "%s"' => '', // 'Swimlane' => '', @@ -810,12 +764,12 @@ return array( // 'The task have been moved to the first swimlane' => '', // 'The task have been moved to another swimlane:' => '', // 'Overdue tasks for the project "%s"' => '', - // 'New title: %s' => '', - // 'The task is not assigned anymore' => '', - // 'New assignee: %s' => '', - // 'There is no category now' => '', - // 'New category: %s' => '', - // 'New color: %s' => '', + 'New title: %s' => 'Nowy tytuł: %s', + 'The task is not assigned anymore' => 'Brak osoby odpowiedzialnej za zadanie', + 'New assignee: %s' => 'Nowy odpowiedzialny: %s', + 'There is no category now' => 'Aktualnie zadanie nie posiada kategorii', + 'New category: %s' => 'Nowa kategoria: %s', + 'New color: %s' => 'Nowy kolor: %s', // 'New complexity: %d' => '', // 'The due date have been removed' => '', // 'There is no description anymore' => '', @@ -826,60 +780,60 @@ return array( // 'The description have been modified' => '', // 'Do you really want to close the task "%s" as well as all subtasks?' => '', // 'Swimlane: %s' => '', - // 'I want to receive notifications for:' => '', - // 'All tasks' => '', - // 'Only for tasks assigned to me' => '', - // 'Only for tasks created by me' => '', - // 'Only for tasks created by me and assigned to me' => '', + 'I want to receive notifications for:' => 'Wysyłaj powiadomienia dla:', + 'All tasks' => 'Wszystkich zadań', + 'Only for tasks assigned to me' => 'Tylko zadań przypisanych do mnie', + 'Only for tasks created by me' => 'Tylko zadań utworzonych przeze mnie', + 'Only for tasks created by me and assigned to me' => 'Tylko zadań przypisanych lub utworzonych przeze mnie', // '%A' => '', // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', + 'New due date: %B %e, %Y' => 'Nowy termin: %B %e, %Y', + 'Start date changed: %B %e, %Y' => 'Zmiana daty rozpoczęcia: %B %e, %Y', // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', // 'Total for all columns' => '', // 'You need at least 2 days of data to show the chart.' => '', // '<15m' => '', // '<30m' => '', - // 'Stop timer' => '', - // 'Start timer' => '', - // 'Add project member' => '', - // 'Enable notifications' => '', - // 'My activity stream' => '', - // 'My calendar' => '', - // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', - // 'Reset filters' => '', - // 'My tasks due tomorrow' => '', - // 'Tasks due today' => '', - // 'Tasks due tomorrow' => '', - // 'Tasks due yesterday' => '', - // 'Closed tasks' => '', - // 'Open tasks' => '', - // 'Not assigned' => '', - // 'View advanced search syntax' => '', - // 'Overview' => '', + 'Stop timer' => 'Zatrzymaj pomiar czasu', + 'Start timer' => 'Uruchom pomiar czasu', + 'Add project member' => 'Dodaj członka projektu', + 'Enable notifications' => 'Włącz powiadomienia', + 'My activity stream' => 'Moja aktywność', + 'My calendar' => 'Mój kalendarz', + 'Search tasks' => 'Szukaj zadań', + 'Back to the calendar' => 'Wróć do kalendarza', + 'Filters' => 'Filtry', + 'Reset filters' => 'Resetuj zastosowane filtry', + 'My tasks due tomorrow' => 'Moje zadania do jutra', + 'Tasks due today' => 'Zadania do dzisiaj', + 'Tasks due tomorrow' => 'Zadania do jutra', + 'Tasks due yesterday' => 'Zadania na wczoraj', + 'Closed tasks' => 'Zamknięte zadania', + 'Open tasks' => 'Otwarte zadania', + 'Not assigned' => 'Nieprzypisane zadania', + 'View advanced search syntax' => 'Pomoc dotycząca budowania filtrów', + 'Overview' => 'Przegląd', // '%b %e %Y' => '', - // 'Board/Calendar/List view' => '', - // 'Switch to the board view' => '', - // 'Switch to the calendar view' => '', - // 'Switch to the list view' => '', - // 'Go to the search/filter box' => '', - // 'There is no activity yet.' => '', - // 'No tasks found.' => '', - // 'Keyboard shortcut: "%s"' => '', - // 'List' => '', - // 'Filter' => '', - // 'Advanced search' => '', - // 'Example of query: ' => '', - // 'Search by project: ' => '', - // 'Search by column: ' => '', - // 'Search by assignee: ' => '', - // 'Search by color: ' => '', - // 'Search by category: ' => '', - // 'Search by description: ' => '', - // 'Search by due date: ' => '', + 'Board/Calendar/List view' => 'Widok: Tablica/Kalendarz/Lista', + 'Switch to the board view' => 'Przełącz na tablicę', + 'Switch to the calendar view' => 'Przełącz na kalendarz', + 'Switch to the list view' => 'Przełącz na listę', + 'Go to the search/filter box' => 'Użyj pola wyszukiwania/filtrów', + 'There is no activity yet.' => 'Brak powiadomień', + 'No tasks found.' => 'Nie znaleziono zadań', + 'Keyboard shortcut: "%s"' => 'Skrót klawiaturowy: "%s"', + 'List' => 'Lista', + 'Filter' => 'Filtr', + 'Advanced search' => 'Zaawansowane wyszukiwanie', + 'Example of query: ' => 'Przykładowe zapytanie:', + 'Search by project: ' => 'Szukaj wg projektów:', + 'Search by column: ' => 'Szukaj wg kolumn:', + 'Search by assignee: ' => 'Szukaj wg użytkownika:', + 'Search by color: ' => 'Szukaj wg koloru:', + 'Search by category: ' => 'Szukaj wg kategorii:', + 'Search by description: ' => 'Szukaj wg opisu:', + 'Search by due date: ' => 'Szukaj wg terminu:', // 'Lead and Cycle time for "%s"' => '', // 'Average time spent into each column for "%s"' => '', // 'Average time spent into each column' => '', @@ -904,72 +858,68 @@ return array( // 'The cycle time is the duration between the start date and the completion.' => '', // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', - // 'Edit Authentication' => '', + 'Edit Authentication' => 'Edycja autoryzacji', // 'Google Id' => '', // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', - // 'New remote user' => '', - // 'New local user' => '', - // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', - // 'This feature does not work with all browsers.' => '', + 'New remote user' => 'Nowy użytkownik zdalny', + 'New local user' => 'Nowy użytkownik lokalny', + 'Default task color' => 'Domyślny kolor zadań', + 'Hide sidebar' => 'Ukryj menu boczne', + 'Expand sidebar' => 'Pokaż menu boczne', + 'This feature does not work with all browsers.' => 'Ta funkcja może nie działać z każdą przeglądarką', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', // 'Include closed tasks in the cumulative flow diagram' => '', - // 'Current swimlane: %s' => '', - // 'Current column: %s' => '', - // 'Current category: %s' => '', - // 'no category' => '', + 'Current swimlane: %s' => 'Bieżący swimlane: %s', + 'Current column: %s' => 'Bieżąca kolumna: %s', + 'Current category: %s' => 'Bieżąca kategoria: %s', + 'no category' => 'brak kategorii', // 'Current assignee: %s' => '', - // 'not assigned' => '', - // 'Author:' => '', - // 'contributors' => '', - // 'License:' => '', - // 'License' => '', - // 'Project Administrator' => '', - // 'Enter the text below' => '', - // 'Gantt chart for %s' => '', - // 'Sort by position' => '', - // 'Sort by date' => '', - // 'Add task' => '', - // 'Start date:' => '', - // 'Due date:' => '', - // 'There is no start date or due date for this task.' => '', + 'not assigned' => 'Brak osoby odpowiedzialnej', + 'Author:' => 'Autor', + 'contributors' => 'współautorzy', + 'License:' => 'Licencja:', + 'License' => 'Licencja', + 'Enter the text below' => 'Wpisz tekst poniżej', + 'Gantt chart for %s' => 'Wykres Gantt dla %s', + 'Sort by position' => 'Sortuj wg pozycji', + 'Sort by date' => 'Sortuj wg daty', + 'Add task' => 'Dodaj zadanie', + 'Start date:' => 'Data rozpoczęcia:', + 'Due date:' => 'Termin', + 'There is no start date or due date for this task.' => 'Brak daty rozpoczęcia lub terminu zadania', // 'Moving or resizing a task will change the start and due date of the task.' => '', // 'There is no task in your project.' => '', - // 'Gantt chart' => '', - // 'People who are project managers' => '', - // 'People who are project members' => '', + 'Gantt chart' => 'Wykres Gantta', + 'People who are project managers' => 'Użytkownicy będący menedżerami projektu', + 'People who are project members' => 'Użytkownicy będący uczestnikami projektu', // 'NOK - Norwegian Krone' => '', - // 'Show this column' => '', - // 'Hide this column' => '', - // 'open file' => '', - // 'End date' => '', - // 'Users overview' => '', - // 'Managers' => '', - // 'Members' => '', - // 'Shared project' => '', - // 'Project managers' => '', - // 'Project members' => '', - // 'Gantt chart for all projects' => '', - // 'Projects list' => '', - // 'Gantt chart for this project' => '', - // 'Project board' => '', - // 'End date:' => '', - // 'There is no start date or end date for this project.' => '', - // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', - // 'Link type' => '', - // 'Change task color when using a specific task link' => '', - // 'Task link creation or modification' => '', + 'Show this column' => 'Pokaż tą kolumnę', + 'Hide this column' => 'Ukryj tą kolumnę', + 'open file' => 'otwórz plik', + 'End date' => 'Data zakończenia', + 'Users overview' => 'Przegląd użytkowników', + 'Managers' => 'Menedżerowie', + 'Members' => 'Uczestnicy', + 'Shared project' => 'Projekt udostępniony', + 'Project managers' => 'Menedżerowie projektu', + 'Gantt chart for all projects' => 'Wykres Gantta dla wszystkich projektów', + 'Projects list' => 'Lista projektów', + 'Gantt chart for this project' => 'Wykres Gantta dla bieżacego projektu', + 'Project board' => 'Talica projektu', + 'End date:' => 'Data zakończenia:', + 'There is no start date or end date for this project.' => 'Nie zdefiniowano czasu trwania projektu', + 'Projects Gantt chart' => 'Wykres Gantta dla projektów', + 'Start date: %s' => 'Data rozpoczęcia: %s', + 'End date: %s' => 'Data zakończenia: %s', + 'Link type' => 'Typ adresu URL', + 'Change task color when using a specific task link' => 'Zmień kolor zadania używając specjalnego adresu URL', + 'Task link creation or modification' => 'Adres URL do utworzenia zadania lub modyfikacji', // 'Login with my Gitlab Account' => '', - // 'Milestone' => '', + 'Milestone' => 'Kamień milowy', // 'Gitlab Authentication' => '', // 'Help on Gitlab authentication' => '', // 'Gitlab Id' => '', @@ -979,18 +929,17 @@ return array( // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', - // 'Documentation' => '', + 'Documentation' => 'Dokumentacja', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', - // 'Author' => '', - // 'Version' => '', - // 'Plugins' => '', - // 'There is no plugin loaded.' => '', - // 'Set maximum column height' => '', - // 'Remove maximum column height' => '', - // 'My notifications' => '', - // 'Custom filters' => '', + 'Author' => 'Autor', + 'Version' => 'Wersja', + 'Plugins' => 'Wtyczki', + 'There is no plugin loaded.' => 'Nie wykryto żadnych wtyczek.', + 'Set maximum column height' => 'Ustaw maksymalną wysokość kolumn', + 'Remove maximum column height' => 'Usuń maksymalną wysokość kolumn', + 'My notifications' => 'Moje powiadomienia', + 'Custom filters' => 'Dostosuj filtry', // 'Your custom filter have been created successfully.' => '', // 'Unable to create your custom filter.' => '', // 'Custom filter removed successfully.' => '', @@ -999,73 +948,160 @@ return array( // 'Your custom filter have been updated successfully.' => '', // 'Unable to update custom filter.' => '', // 'Web' => '', - // 'New attachment on task #%d: %s' => '', - // 'New comment on task #%d' => '', - // 'Comment updated on task #%d' => '', - // 'New subtask on task #%d' => '', - // 'Subtask updated on task #%d' => '', - // 'New task #%d: %s' => '', - // 'Task updated #%d' => '', - // 'Task #%d closed' => '', - // 'Task #%d opened' => '', - // 'Column changed for task #%d' => '', - // 'New position for task #%d' => '', - // 'Swimlane changed for task #%d' => '', - // 'Assignee changed on task #%d' => '', + 'New attachment on task #%d: %s' => 'Nowy załącznik do zadania #%d: %s', + 'New comment on task #%d' => 'Nowy załącznik #%d', + 'Comment updated on task #%d' => 'Comment updated on task #%d', + 'New subtask on task #%d' => 'Nowe pod-zadanie dla zadania #%d', + 'Subtask updated on task #%d' => 'Aktualizacja pod-zadania w zadaniu #%d', + 'New task #%d: %s' => 'Nowe zadanie #%d: %s', + 'Task updated #%d' => 'Aktualizacja zadania #%d', + 'Task #%d closed' => 'Zamknięto zadanie #%d', + 'Task #%d opened' => 'Otwarto zadanie #%d', + 'Column changed for task #%d' => 'Zmieniono kolumnę zadania #%d', + 'New position for task #%d' => 'Ustalono nową pozycję zadania #%d', + 'Swimlane changed for task #%d' => 'Zmieniono swimlane dla zadania #%d', + 'Assignee changed on task #%d' => 'Zmieniono osobę odpowiedzialną dla zadania #%d', // '%d overdue tasks' => '', // 'Task #%d is overdue' => '', - // 'No new notifications.' => '', - // 'Mark all as read' => '', - // 'Mark as read' => '', + 'No new notifications.' => 'Brak nowych powiadomień.', + 'Mark all as read' => 'Oznacz wszystkie jako przeczytane', + 'Mark as read' => 'Oznacz jako przeczytane', // 'Total number of tasks in this column across all swimlanes' => '', - // 'Collapse swimlane' => '', - // 'Expand swimlane' => '', - // 'Add a new filter' => '', - // 'Share with all project members' => '', + 'Collapse swimlane' => 'Zwiń swimlane', + 'Expand swimlane' => 'Rozwiń swimlane', + 'Add a new filter' => 'Dodaj nowy filtr', + 'Share with all project members' => 'Udostępnij wszystkim uczestnikom projektu', // 'Shared' => '', - // 'Owner' => '', - // 'Unread notifications' => '', - // 'My filters' => '', - // 'Notification methods:' => '', + 'Owner' => 'Właściciel', + 'Unread notifications' => 'Nieprzeczytane powiadomienia', + 'My filters' => 'Moje filtry', + 'Notification methods:' => 'Metody powiadomień:', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', // '%d task(s) have been imported successfully.' => '', // 'Nothing have been imported!' => '', // 'Import users from CSV file' => '', // '%d user(s) have been imported successfully.' => '', - // 'Comma' => '', - // 'Semi-colon' => '', - // 'Tab' => '', - // 'Vertical bar' => '', - // 'Double Quote' => '', - // 'Single Quote' => '', + 'Comma' => 'Przecinek', + 'Semi-colon' => 'Średnik', + 'Tab' => 'Tabulacja', + 'Vertical bar' => 'Kreska pionowa', + 'Double Quote' => 'Cudzysłów', + 'Single Quote' => 'Apostrof', // '%s attached a file to the task #%d' => '', // 'There is no column or swimlane activated in your project!' => '', - // 'Append filter (instead of replacement)' => '', + 'Append filter (instead of replacement)' => 'Dołączaj filtr do zastosowanego filtru(zamiast przełączać)', // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', - // 'Tasks Importation' => '', - // 'Delimiter' => '', - // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', + 'Tasks Importation' => 'Import zadań', + 'Delimiter' => 'Separator pola', + 'Enclosure' => 'Separator tekstu', + 'CSV File' => 'Plik CSV', + 'Instructions' => 'Instrukcje', + 'Your file must use the predefined CSV format' => 'Twój plik musi być zgodny z predefiniowanym formatem CSV (pobierz szablon)', + 'Your file must be encoded in UTF-8' => 'Twój plik musi być kodowany w UTF-8', + 'The first row must be the header' => 'Pierwszy wiersz pliku musi definiować nagłówki', + 'Duplicates are not verified for you' => 'Duplikaty nie będą weryfikowane', + 'The due date must use the ISO format: YYYY-MM-DD' => 'Data musi być w formacie ISO: YYYY-MM-DD', + 'Download CSV template' => 'Pobierz szablon pliku CSV', // 'No external integration registered.' => '', - // 'Duplicates are not imported' => '', + 'Duplicates are not imported' => 'Duplikaty nie zostaną zaimportowane', // 'Usernames must be lowercase and unique' => '', // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + 'Groups' => 'Grupy', + 'Members of %s' => 'Członkowie %s', + 'New group' => 'Nowa grupa', + 'Group created successfully.' => 'Grupa została utworzona.', + 'Unable to create your group.' => 'Nie można utworzyć grupy.', + 'Edit group' => 'Edytuj grupę', + 'Group updated successfully.' => 'Grupa została zaaktualizowana.', + 'Unable to update your group.' => 'Nie można zaaktualizować grupy.', + 'Add group member to "%s"' => 'Dodaj członka do grupy "%s"', + 'Group member added successfully.' => 'Użytkownik został dodany do grupy.', + 'Unable to add group member.' => 'Nie można dodać użytkownika do grupy.', + 'Remove user from group "%s"' => 'Usuń użytkownika z grupy "%s"', + 'User removed successfully from this group.' => 'Użytkownik został usunięty z grupy.', + 'Unable to remove this user from the group.' => 'Nie można usunąć użytkownika z grupy.', + 'Remove group' => 'Usuń grupę', + 'Group removed successfully.' => 'Grupa została usunięta.', + 'Unable to remove this group.' => 'Nie można usunąć grupy.', + 'Project Permissions' => 'Prawa dostępowe projektu', + 'Manager' => 'Menedżer', + 'Project Manager' => 'Menedżer projektu', + 'Project Member' => 'Uczestnik projektu', + 'Project Viewer' => 'Obserwator projektu', + 'Your account is locked for %d minutes' => 'Twoje konto zostało zablokowane na %d minut', + 'Invalid captcha' => 'Błędny kod z obrazka (captcha)', + 'The name must be unique' => 'Nazwa musi być unikatowa', + 'View all groups' => 'Wyświetl wszystkie grupy', + 'View group members' => 'Wyświetl wszystkich członków grupy', + 'There is no user available.' => 'Żaden użytkownik nie jest dostępny.', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Czy napewno chcesz usunąć użytkownika "%s" z grupy "%s"?', + 'There is no group.' => 'Brak grup.', + 'External Id' => 'Zewnętrzny Id', + 'Add group member' => 'Dodaj członka grupy', + 'Do you really want to remove this group: "%s"?' => 'Czy napewno chcesz usunąć grupę "%s"?', + 'There is no user in this group.' => 'Wybrana grupa nie posiada członków.', + 'Remove this user' => 'Usuń użytkownika', + 'Permissions' => 'Prawa dostępu', + 'Allowed Users' => 'Użytkownicy z dostępem', + 'No user have been allowed specifically.' => 'Żaden użytkownik nie ma przyznanego dostępu.', + 'Role' => 'Rola', + 'Enter user name...' => 'Wprowadź nazwę użytkownika...', + 'Allowed Groups' => 'Dostępne grupy', + 'No group have been allowed specifically.' => 'Żadna grupa nie ma przyznanego dostępu.', + 'Group' => 'Grupa', + 'Group Name' => 'Nazwa grupy', + 'Enter group name...' => 'Wprowadź nazwę grupy...', + 'Role:' => 'Rola:', + 'Project members' => 'Uczestnicy projektu', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/pt_BR/translations.php b/sources/app/Locale/pt_BR/translations.php index f03be30..e985f90 100644 --- a/sources/app/Locale/pt_BR/translations.php +++ b/sources/app/Locale/pt_BR/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Complexidade', 'Task limit' => 'Limite de tarefas', 'Task count' => 'Número de tarefas', - 'Edit project access list' => 'Editar lista de acesso ao projeto', - 'Allow this user' => 'Permitir este usuário', - 'Don\'t forget that administrators have access to everything.' => 'Não esqueça que administradores têm acesso a tudo.', - 'Revoke' => 'Revogar', - 'List of authorized users' => 'Lista de usuários autorizados', 'User' => 'Usuário', - 'Nobody have access to this project.' => 'Ninguém tem acesso a este projeto.', 'Comments' => 'Comentários', 'Write your text in Markdown' => 'Escreva seu texto em Markdown', 'Leave a comment' => 'Deixe um comentário', @@ -396,8 +390,6 @@ return array( 'Email:' => 'E-mail:', 'Notifications:' => 'Notificações:', 'Notifications' => 'Notificações', - 'Group:' => 'Grupo:', - 'Regular user' => 'Usuário comum', 'Account type:' => 'Tipo de conta:', 'Edit profile' => 'Editar perfil', 'Change password' => 'Alterar senha', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s mudou a designação da tarefa %s para %s', 'New password for the user "%s"' => 'Nova senha para o usuário "%s"', 'Choose an event' => 'Escolher um evento', - 'Github commit received' => 'Github commit received', - 'Github issue opened' => 'Github issue opened', - 'Github issue closed' => 'Github issue closed', - 'Github issue reopened' => 'Github issue reopened', - 'Github issue assignee change' => 'Github issue assignee change', - 'Github issue label change' => 'Github issue label change', 'Create a task from an external provider' => 'Criar uma tarefa por meio de um serviço externo', 'Change the assignee based on an external username' => 'Alterar designação com base em um usuário externo', 'Change the category based on an external label' => 'Alterar categoria com base em um rótulo externo', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Todos possuem acesso a este projeto.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Ajuda sobre os webhooks do GitHub', 'Create a comment from an external provider' => 'Criar um comentário por meio de um serviço externo', - 'Github issue comment created' => 'Github issue comment created', 'Project management' => 'Gerenciamento de projetos', 'My projects' => 'Meus projetos', 'Columns' => 'Colunas', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Redistribuição de usuário para "%s"', 'Clone this project' => 'Clonar este projeto', 'Column removed successfully.' => 'Coluna removida com sucesso.', - 'Github Issue' => 'Github Issue', 'Not enough data to show the graph.' => 'Não há dados suficientes para mostrar o gráfico.', 'Previous' => 'Anterior', 'The id must be an integer' => 'O ID deve ser um número inteiro', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Swimlane padrão', 'Do you really want to remove this swimlane: "%s"?' => 'Você realmente deseja remover esta swimlane: "%s"?', 'Inactive swimlanes' => 'Desativar swimlanes', - 'Set project manager' => 'Definir gerente do projeto', - 'Set project member' => 'Definir membro do projeto', 'Remove a swimlane' => 'Remover uma swimlane', 'Rename' => 'Renomear', 'Show default swimlane' => 'Exibir swimlane padrão', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Sua swimlane foi criada com sucesso.', 'Example: "Bug, Feature Request, Improvement"' => 'Exemplo: "Bug, Solicitação de Recurso, Melhoria"', 'Default categories for new projects (Comma-separated)' => 'Categorias padrões para novos projetos (separadas por vírgula)', - 'Gitlab commit received' => 'Gitlab commit received', - 'Gitlab issue opened' => 'Gitlab issue opened', - 'Gitlab issue closed' => 'Gitlab issue closed', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Ajuda sobre os webhooks do GitLab', 'Integrations' => 'Integrações', 'Integration with third-party services' => 'Integração com serviços de terceiros', - 'Role for this project' => 'Função para este projeto', - 'Project manager' => 'Gerente do projeto', - 'Project member' => 'Membro do projeto', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Um gerente de projeto pode alterar as configurações do projeto e ter mais privilégios que um usuário padrão.', - 'Gitlab Issue' => 'Gitlab Issue', 'Subtask Id' => 'ID da subtarefa', 'Subtasks' => 'Subtarefas', 'Subtasks Export' => 'Exportar subtarefas', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Você já tem um subtarefa em andamento', 'Which parts of the project do you want to duplicate?' => 'Quais partes do projeto você deseja duplicar?', 'Disallow login form' => 'Proibir o formulário de login', - 'Bitbucket commit received' => '"Commit" recebido via Bitbucket', - 'Bitbucket webhooks' => 'Webhook Bitbucket', - 'Help on Bitbucket webhooks' => 'Ajuda sobre os webhooks do Bitbucket', 'Start' => 'Início', 'End' => 'Fim', 'Task age in days' => 'Idade da tarefa em dias', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'O código de autenticação em duas etapas é válido.', 'Code' => 'Código', 'Two factor authentication' => 'Autenticação em duas etapas', - 'Enable/disable two factor authentication' => 'Ativar/desativar autenticação em duas etapas', 'This QR code contains the key URI: ' => 'Este Código QR contém a chave URI:', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Salve esta chave secreta no seu software TOTP (por exemplo Google Authenticator ou FreeOTP).', 'Check my code' => 'Verifique o meu código', 'Secret key: ' => 'Chave secreta:', 'Test your device' => 'Teste o seu dispositivo', @@ -776,21 +741,10 @@ return array( 'User that will receive the email' => 'O usuário que vai receber o e-mail', 'Email subject' => 'Assunto do e-mail', 'Date' => 'Data', - 'By @%s on Bitbucket' => 'Por @%s no Bitbucket', - 'Bitbucket Issue' => 'Bitbucket Issue', - 'Commit made by @%s on Bitbucket' => 'Commit feito por @%s no Bitbucket', - 'Commit made by @%s on Github' => 'Commit feito por @%s no Github', - 'By @%s on Github' => 'Por @%s no Github', - 'Commit made by @%s on Gitlab' => 'Commit feito por @%s no Gitlab', 'Add a comment log when moving the task between columns' => 'Adicionar um comentário de log quando uma tarefa é movida para uma outra coluna', 'Move the task to another column when the category is changed' => 'Mover uma tarefa para outra coluna quando a categoria mudou', 'Send a task by email to someone' => 'Enviar uma tarefa por e-mail a alguém', 'Reopen a task' => 'Reabrir uma tarefa', - 'Bitbucket issue opened' => 'Bitbucket issue opened', - 'Bitbucket issue closed' => 'Bitbucket issue closed', - 'Bitbucket issue reopened' => 'Bitbucket issue reopened', - 'Bitbucket issue assignee change' => 'Bitbucket issue assignee change', - 'Bitbucket issue comment created' => 'Bitbucket issue comment created', 'Column change' => 'Mudança de coluna', 'Position change' => 'Mudança de posição', 'Swimlane change' => 'Mudança de swimlane', @@ -910,8 +864,6 @@ return array( 'Remote user' => 'Usuário remoto', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Os usuários remotos não conservam as suas senhas no banco de dados Kanboard, exemplos: contas LDAP, Github ou Google.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se você marcar "Interdir o formulário de autenticação", os identificadores entrados no formulário de login serão ignorado.', - 'By @%s on Gitlab' => 'Por @%s no Gitlab', - 'Gitlab issue comment created' => 'Comentário criado em um bilhete Gitlab', 'New remote user' => 'Criar um usuário remoto', 'New local user' => 'Criar um usuário local', 'Default task color' => 'Cor padrão para as tarefas', @@ -931,7 +883,6 @@ return array( 'contributors' => 'contribuidores', 'License:' => 'Licença:', 'License' => 'Licença', - 'Project Administrator' => 'Administrador de projeto', 'Enter the text below' => 'Entre o texto abaixo', 'Gantt chart for %s' => 'Gráfico de Gantt para %s', 'Sort by position' => 'Ordenar por posição', @@ -955,7 +906,6 @@ return array( 'Members' => 'Membros', 'Shared project' => 'Projeto compartilhado', 'Project managers' => 'Gerentes de projeto', - 'Project members' => 'Membros de projeto', 'Gantt chart for all projects' => 'Gráfico de Gantt para todos os projetos', 'Projects list' => 'Lista dos projetos', 'Gantt chart for this project' => 'Gráfico de Gantt para este projeto', @@ -982,7 +932,6 @@ return array( 'Documentation' => 'Documentação', 'Table of contents' => 'Índice', 'Gantt' => 'Gantt', - 'Help with project permissions' => 'Ajuda com as permissões de projetos', 'Author' => 'Autor', 'Version' => 'Versão', 'Plugins' => 'Extensões', @@ -1045,7 +994,6 @@ return array( 'Append/Replace' => 'Adicionar/Substituir', 'Append' => 'Adicionar', 'Replace' => 'Substituir', - 'There is no notification method registered.' => 'Não há metodo de notificação registrado.', 'Import' => 'Importar', 'change sorting' => 'alterar ordenação', 'Tasks Importation' => 'Importação de Tarefas', @@ -1065,7 +1013,95 @@ return array( 'Passwords will be encrypted if present' => 'Senhas serão encriptadas, se presentes', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + 'Project members' => 'Membros de projeto', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/pt_PT/translations.php b/sources/app/Locale/pt_PT/translations.php index 5e6c57d..bfe9803 100644 --- a/sources/app/Locale/pt_PT/translations.php +++ b/sources/app/Locale/pt_PT/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Complexidade', 'Task limit' => 'Limite da tarefa', 'Task count' => 'Número de tarefas', - 'Edit project access list' => 'Editar lista de acesso ao projecto', - 'Allow this user' => 'Permitir este utilizador', - 'Don\'t forget that administrators have access to everything.' => 'Não se esqueça que administradores têm acesso a tudo.', - 'Revoke' => 'Revogar', - 'List of authorized users' => 'Lista de utilizadores autorizados', 'User' => 'Utilizador', - 'Nobody have access to this project.' => 'Ninguém tem acesso a este projecto.', 'Comments' => 'Comentários', 'Write your text in Markdown' => 'Escreva o seu texto em Markdown', 'Leave a comment' => 'Deixe um comentário', @@ -396,8 +390,6 @@ return array( 'Email:' => 'E-mail:', 'Notifications:' => 'Notificações:', 'Notifications' => 'Notificações', - 'Group:' => 'Grupo:', - 'Regular user' => 'Utilizador comum', 'Account type:' => 'Tipo de conta:', 'Edit profile' => 'Editar perfil', 'Change password' => 'Alterar senha', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s mudou a assignação da tarefa %s para %s', 'New password for the user "%s"' => 'Nova senha para o utilizador "%s"', 'Choose an event' => 'Escolher um evento', - 'Github commit received' => 'Recebido commit do Github', - 'Github issue opened' => 'Problema aberto no Github', - 'Github issue closed' => 'Problema fechado no Github', - 'Github issue reopened' => 'Problema reaberto no Github', - 'Github issue assignee change' => 'Alterar assignação ao problema no Githubnge', - 'Github issue label change' => 'Alterar etiqueta do problema no Github', 'Create a task from an external provider' => 'Criar uma tarefa por meio de um serviço externo', 'Change the assignee based on an external username' => 'Alterar assignação com base num utilizador externo', 'Change the category based on an external label' => 'Alterar categoria com base num rótulo externo', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Todos possuem acesso a este projecto.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Ajuda para o Github webhooks', 'Create a comment from an external provider' => 'Criar um comentário por meio de um serviço externo', - 'Github issue comment created' => 'Criado comentário ao problema no Github', 'Project management' => 'Gestão de projectos', 'My projects' => 'Os meus projectos', 'Columns' => 'Colunas', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Redistribuição de utilizador para "%s"', 'Clone this project' => 'Clonar este projecto', 'Column removed successfully.' => 'Coluna removida com sucesso.', - 'Github Issue' => 'Problema no Github', 'Not enough data to show the graph.' => 'Não há dados suficientes para mostrar o gráfico.', 'Previous' => 'Anterior', 'The id must be an integer' => 'O ID deve ser um número inteiro', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Swimlane padrão', 'Do you really want to remove this swimlane: "%s"?' => 'Tem a certeza que quer remover este swimlane: "%s"?', 'Inactive swimlanes' => 'Desactivar swimlanes', - 'Set project manager' => 'Definir gerente do projecto', - 'Set project member' => 'Definir membro do projecto', 'Remove a swimlane' => 'Remover um swimlane', 'Rename' => 'Renomear', 'Show default swimlane' => 'Mostrar swimlane padrão', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Seu swimlane foi criado com sucesso.', 'Example: "Bug, Feature Request, Improvement"' => 'Exemplo: "Bug, Feature Request, Improvement"', 'Default categories for new projects (Comma-separated)' => 'Categorias padrão para novos projectos (Separadas por vírgula)', - 'Gitlab commit received' => 'Commit recebido do Gitlab', - 'Gitlab issue opened' => 'Problema aberto no Gitlab', - 'Gitlab issue closed' => 'Problema fechado no Gitlab', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Ajuda sobre Gitlab webhooks', 'Integrations' => 'Integrações', 'Integration with third-party services' => 'Integração com serviços de terceiros', - 'Role for this project' => 'Função para este projecto', - 'Project manager' => 'Gerente do projecto', - 'Project member' => 'Membro do projecto', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Um gerente do projecto pode alterar as configurações do projecto e ter mais privilégios que um utilizador padrão.', - 'Gitlab Issue' => 'Problema Gitlab', 'Subtask Id' => 'ID da subtarefa', 'Subtasks' => 'Subtarefas', 'Subtasks Export' => 'Exportar subtarefas', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Já tem uma subtarefa em andamento', 'Which parts of the project do you want to duplicate?' => 'Quais as partes do projecto que deseja duplicar?', 'Disallow login form' => 'Desactivar login', - 'Bitbucket commit received' => '"Commit" recebido via Bitbucket', - 'Bitbucket webhooks' => 'Webhook Bitbucket', - 'Help on Bitbucket webhooks' => 'Ajuda sobre os webhooks Bitbucket', 'Start' => 'Inicio', 'End' => 'Fim', 'Task age in days' => 'Idade da tarefa em dias', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'O código de autenticação com factor duplo é válido', 'Code' => 'Código', 'Two factor authentication' => 'Autenticação com factor duplo', - 'Enable/disable two factor authentication' => 'Activar/Desactivar autenticação com factor duplo', 'This QR code contains the key URI: ' => 'Este Código QR contém a chave URI: ', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Guarde esta chave secreta no seu software TOTP (por exemplo Google Authenticator ou FreeOTP).', 'Check my code' => 'Verificar o meu código', 'Secret key: ' => 'Chave secreta: ', 'Test your device' => 'Teste o seu dispositivo', @@ -738,20 +703,20 @@ return array( 'Edit recurrence' => 'Modificar a recorrência', 'Generate recurrent task' => 'Gerar uma tarefa recorrente', 'Trigger to generate recurrent task' => 'Activador para gerar tarefa recorrente', - 'Factor to calculate new due date' => 'Factor para o cálculo do nova data limite', - 'Timeframe to calculate new due date' => 'Escala de tempo para o cálculo da nova data limite', - 'Base date to calculate new due date' => 'Data a ser utilizada para calcular a nova data limite', + 'Factor to calculate new due date' => 'Factor para o cálculo do nova data de vencimento', + 'Timeframe to calculate new due date' => 'Escala de tempo para o cálculo da nova data de vencimento', + 'Base date to calculate new due date' => 'Data a ser utilizada para calcular a nova data de vencimento', 'Action date' => 'Data da acção', - 'Base date to calculate new due date: ' => 'Data a ser utilizada para calcular a nova data limite: ', + 'Base date to calculate new due date: ' => 'Data a ser utilizada para calcular a nova data de vencimento: ', 'This task has created this child task: ' => 'Esta tarefa criou a tarefa filha: ', 'Day(s)' => 'Dia(s)', - 'Existing due date' => 'Data limite existente', - 'Factor to calculate new due date: ' => 'Factor para calcular a nova data limite: ', + 'Existing due date' => 'Data de vencimento existente', + 'Factor to calculate new due date: ' => 'Factor para calcular a nova data de vencimento: ', 'Month(s)' => 'Mês(es)', 'Recurrence' => 'Recorrência', 'This task has been created by: ' => 'Esta tarefa foi criada por: ', 'Recurrent task has been generated:' => 'A tarefa recorrente foi gerada:', - 'Timeframe to calculate new due date: ' => 'Escala de tempo para o cálculo da nova data limite: ', + 'Timeframe to calculate new due date: ' => 'Escala de tempo para o cálculo da nova data de vencimento: ', 'Trigger to generate recurrent task: ' => 'Activador para gerar tarefa recorrente: ', 'When task is closed' => 'Quando a tarefa é fechada', 'When task is moved from first column' => 'Quando a tarefa é movida fora da primeira coluna', @@ -776,21 +741,10 @@ return array( 'User that will receive the email' => 'O utilizador que vai receber o e-mail', 'Email subject' => 'Assunto do e-mail', 'Date' => 'Data', - 'By @%s on Bitbucket' => 'Por @%s no Bitbucket', - 'Bitbucket Issue' => 'Problema Bitbucket', - 'Commit made by @%s on Bitbucket' => 'Commit feito por @%s no Bitbucket', - 'Commit made by @%s on Github' => 'Commit feito por @%s no Github', - 'By @%s on Github' => 'Por @%s no Github', - 'Commit made by @%s on Gitlab' => 'Commit feito por @%s no Gitlab', 'Add a comment log when moving the task between columns' => 'Adicionar um comentário de log quando uma tarefa é movida para uma outra coluna', 'Move the task to another column when the category is changed' => 'Mover uma tarefa para outra coluna quando a categoria mudar', 'Send a task by email to someone' => 'Enviar uma tarefa por e-mail a alguém', 'Reopen a task' => 'Reabrir uma tarefa', - 'Bitbucket issue opened' => 'Problema aberto no Bitbucket', - 'Bitbucket issue closed' => 'Problema fechado no Bitbucket', - 'Bitbucket issue reopened' => 'Problema reaberto no Bitbucket', - 'Bitbucket issue assignee change' => 'Alterar assignação do problema no Bitbucket', - 'Bitbucket issue comment created' => 'Comentário ao problema adicionado ao Bitbucket', 'Column change' => 'Mudança de coluna', 'Position change' => 'Mudança de posição', 'Swimlane change' => 'Mudança de swimlane', @@ -817,7 +771,7 @@ return array( 'New category: %s' => 'Nova categoria: %s', 'New color: %s' => 'Nova cor: %s', 'New complexity: %d' => 'Nova complexidade: %d', - 'The due date have been removed' => 'A data limite foi retirada', + 'The due date have been removed' => 'A data de vencimento foi retirada', 'There is no description anymore' => 'Já não há descrição', 'Recurrence settings have been modified' => 'As configurações da recorrência foram modificadas', 'Time spent changed: %sh' => 'O tempo despendido foi mudado: %sh', @@ -833,7 +787,7 @@ return array( 'Only for tasks created by me and assigned to me' => 'Apenas as tarefas que eu criei e aquelas atribuídas a mim', '%A' => '%A', '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M', - 'New due date: %B %e, %Y' => 'Nova data limite: %d/%m/%Y', + 'New due date: %B %e, %Y' => 'Nova data de vencimento: %d/%m/%Y', 'Start date changed: %B %e, %Y' => 'Data de início alterada: %d/%m/%Y', '%k:%M %p' => '%H:%M', '%%Y-%%m-%%d' => '%%d/%%m/%%Y', @@ -879,7 +833,7 @@ return array( 'Search by color: ' => 'Pesquisar por cor: ', 'Search by category: ' => 'Pesquisar por categoria: ', 'Search by description: ' => 'Pesquisar por descrição: ', - 'Search by due date: ' => 'Pesquisar por data de expiração: ', + 'Search by due date: ' => 'Pesquisar por data de vencimento: ', 'Lead and Cycle time for "%s"' => 'Tempo de Espera e Ciclo para "%s"', 'Average time spent into each column for "%s"' => 'Tempo médio gasto em cada coluna para "%s"', 'Average time spent into each column' => 'Tempo médio gasto em cada coluna', @@ -910,8 +864,6 @@ return array( 'Remote user' => 'Utilizador remoto', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Utilizadores remotos não guardam a password na base de dados do Kanboard, por exemplo: LDAP, contas do Google e Github.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se activar a opção "Desactivar login", as credenciais digitadas no login serão ignoradas.', - 'By @%s on Gitlab' => 'Por @%s no Gitlab', - 'Gitlab issue comment created' => 'Comentário a problema no Gitlab adicionado', 'New remote user' => 'Novo utilizador remoto', 'New local user' => 'Novo utilizador local', 'Default task color' => 'Cor de tarefa por defeito', @@ -931,7 +883,6 @@ return array( 'contributors' => 'contribuidores', 'License:' => 'Licença:', 'License' => 'Licença', - 'Project Administrator' => 'Administrador do Projecto', 'Enter the text below' => 'Escreva o texto em baixo', 'Gantt chart for %s' => 'Gráfico de Gantt para %s', 'Sort by position' => 'Ordenar por posição', @@ -944,7 +895,7 @@ return array( 'There is no task in your project.' => 'Não existe tarefa no seu projecto.', 'Gantt chart' => 'Gráfico de Gantt', 'People who are project managers' => 'Pessoas que são gestores do projecto', - 'People who are project members' => 'Pessoas que são membors do projecto', + 'People who are project members' => 'Pessoas que são membros do projecto', 'NOK - Norwegian Krone' => 'NOK - Coroa Norueguesa', 'Show this column' => 'Mostrar esta coluna', 'Hide this column' => 'Esconder esta coluna', @@ -955,7 +906,6 @@ return array( 'Members' => 'Membros', 'Shared project' => 'Projecto partilhado', 'Project managers' => 'Gestores do projecto', - 'Project members' => 'Membros do projecto', 'Gantt chart for all projects' => 'Gráfico de Gantt para todos os projectos', 'Projects list' => 'Lista de projectos', 'Gantt chart for this project' => 'Gráfico de Gantt para este projecto', @@ -982,7 +932,6 @@ return array( 'Documentation' => 'Documentação', 'Table of contents' => 'Tabela de conteúdos', 'Gantt' => 'Gantt', - 'Help with project permissions' => 'Ajuda com permissões de projecto', 'Author' => 'Autor', 'Version' => 'Versão', 'Plugins' => 'Extras', @@ -1045,7 +994,6 @@ return array( 'Append/Replace' => 'Acrescentar/Substituir', 'Append' => 'Acrescentar', 'Replace' => 'Substituir', - 'There is no notification method registered.' => 'Não existe método de notificação registrado.', 'Import' => 'Importar', 'change sorting' => 'alterar ordernação', 'Tasks Importation' => 'Importação de Tarefas', @@ -1065,7 +1013,95 @@ return array( 'Passwords will be encrypted if present' => 'Senhas serão encriptadas se presentes', '%s attached a new file to the task %s' => '%s anexou um novo ficheiro à tarefa %s', 'Assign automatically a category based on a link' => 'Assignar automáticamente a categoria baseada num link', - // 'BAM - Konvertibile Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', + 'BAM - Konvertible Mark' => 'BAM - Marca Conversível', + 'Assignee Username' => 'Utilizador do Assignado', + 'Assignee Name' => 'Nome do Assignado', + 'Groups' => 'Grupos', + 'Members of %s' => 'Membros de %s', + 'New group' => 'Novo grupo', + 'Group created successfully.' => 'Grupo criado com sucesso.', + 'Unable to create your group.' => 'Não foi possivel criar o seu grupo.', + 'Edit group' => 'Editar grupo', + 'Group updated successfully.' => 'Grupo actualizado com sucesso.', + 'Unable to update your group.' => 'Não foi possivel actualizar o seu grupo.', + 'Add group member to "%s"' => 'Adicionar membro do grupo a "%s"', + 'Group member added successfully.' => 'Membro de grupo adicionado com sucesso.', + 'Unable to add group member.' => 'Não foi possivel adicionar membro de grupo.', + 'Remove user from group "%s"' => 'Remover utilizador do grupo "%s"', + 'User removed successfully from this group.' => 'Utilizador removido com sucesso deste grupo.', + 'Unable to remove this user from the group.' => 'Não foi possivel remover este utilizador do grupo.', + 'Remove group' => 'Remover grupo.', + 'Group removed successfully.' => 'Grupo removido com sucesso.', + 'Unable to remove this group.' => 'Não foi possivel remover este grupo.', + 'Project Permissions' => 'Permissões de Projecto', + 'Manager' => 'Gestor', + 'Project Manager' => 'Gestor de Projecto', + 'Project Member' => 'Membro de Projecto', + 'Project Viewer' => 'Visualizador de Projecto', + 'Your account is locked for %d minutes' => 'A sua conta está bloqueada por %d minutos', + 'Invalid captcha' => 'Captcha inválido', + 'The name must be unique' => 'O nome deve ser único', + 'View all groups' => 'Ver todos os grupos', + 'View group members' => 'Ver membros do grupo', + 'There is no user available.' => 'Não existe utilizador disponivel.', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Tem a certeza que quer remover o utilizador "%s" do grupo "%s"?', + 'There is no group.' => 'Não existe grupo.', + 'External Id' => 'Id externo', + 'Add group member' => 'Adicionar membro de grupo', + 'Do you really want to remove this group: "%s"?' => 'Tem a certeza que quer remover este grupo: "%s"?', + 'There is no user in this group.' => 'Não existe utilizadores neste grupo.', + 'Remove this user' => 'Remover este utilizador', + 'Permissions' => 'Permissões', + 'Allowed Users' => 'Utilizadores Permitidos', + 'No user have been allowed specifically.' => 'Nenhum utilizador foi especificamente permitido.', + 'Role' => 'Função', + 'Enter user name...' => 'Escreva o nome do utilizador...', + 'Allowed Groups' => 'Grupos Permitidos', + 'No group have been allowed specifically.' => 'Nenhum grupo foi especificamente permitido.', + 'Group' => 'Grupo', + 'Group Name' => 'Nome do Grupo', + 'Enter group name...' => 'Escreva o nome do Grupo', + 'Role:' => 'Função:', + 'Project members' => 'Membros do projecto', + 'Compare hours for "%s"' => 'Comparar horas para "%s"', + '%s mentioned you in the task #%d' => '%s mencionou-te na tarefa #%d', + '%s mentioned you in a comment on the task #%d' => '%s mencionou-te num comentário na tarefa #%d', + 'You were mentioned in the task #%d' => 'Foi mencionado na tarefa #%d', + 'You were mentioned in a comment on the task #%d' => 'Foi mencionado num comentário na tarefa #%d', + 'Mentioned' => 'Mencionado', + 'Compare Estimated Time vs Actual Time' => 'Comparar Tempo Estimado vs Tempo Real', + 'Estimated hours: ' => 'Horas estimadas: ', + 'Actual hours: ' => 'Horas reais: ', + 'Hours Spent' => 'Horas Gastas', + 'Hours Estimated' => 'Horas Estimadas', + 'Estimated Time' => 'Tempo Estimado', + 'Actual Time' => 'Tempo Real', + 'Estimated vs actual time' => 'Tempo estimado vs real', + 'RUB - Russian Ruble' => 'RUB - Rublo Russo', + 'Assign the task to the person who does the action when the column is changed' => 'Assignar a tarefa à pessoa que realiza a acção quando a coluna é alterada', + 'Close a task in a specific column' => 'Fechar tarefa numa coluna especifica', + 'Time-based One-time Password Algorithm' => 'Algoritmo de password para uso único baseado em tempo', + 'Two-Factor Provider: ' => 'Provedor de Dois Passos: ', + 'Disable two-factor authentication' => 'Desactivar autenticação de dois passos', + 'Enable two-factor authentication' => 'Activar autenticação de dois passos', + 'There is no integration registered at the moment.' => 'Não existe nenhuma integração registrada até ao momento.', + 'Password Reset for Kanboard' => 'Redefinir Password para Kanboard', + 'Forgot password?' => 'Esqueceu a password?', + 'Enable "Forget Password"' => 'Activar "Esqueceu a password"', + 'Password Reset' => 'Redefinir a Password', + 'New password' => 'Nova Password', + 'Change Password' => 'Alterar Password', + 'To reset your password click on this link:' => 'Para redefinir a sua password click nesta ligação:', + 'Last Password Reset' => 'Última Redefinição da Password', + 'The password has never been reinitialized.' => 'A password nunca foi redefinida.', + 'Creation' => 'Criação', + 'Expiration' => 'Expiração', + 'Password reset history' => 'Histórico da redefinição da password', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Todas as tarefas na coluna "%s" e na swimlane "%s" foram fechadas com successo.', + 'Do you really want to close all tasks of this column?' => 'Tem a certeza que quer fechar todas as tarefas nesta coluna?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tarefa(s) na coluna "%s" e na swimlane "%s" serão fechadas.', + 'Close all tasks of this column' => 'Fechar todas as tarefas nesta coluna', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/ru_RU/translations.php b/sources/app/Locale/ru_RU/translations.php index e7f2d7a..7e406c9 100644 --- a/sources/app/Locale/ru_RU/translations.php +++ b/sources/app/Locale/ru_RU/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Сложность', 'Task limit' => 'Лимит задач', 'Task count' => 'Количество задач', - 'Edit project access list' => 'Изменить доступ к проекту', - 'Allow this user' => 'Разрешить этого пользователя', - 'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет неограниченные права.', - 'Revoke' => 'Отозвать', - 'List of authorized users' => 'Список авторизованных пользователей', 'User' => 'Пользователь', - 'Nobody have access to this project.' => 'Ни у кого нет доступа к этому проекту', 'Comments' => 'Комментарии', 'Write your text in Markdown' => 'Справка по синтаксису Markdown', 'Leave a comment' => 'Оставить комментарий 2', @@ -300,7 +294,7 @@ return array( 'File removed successfully.' => 'Файл удален.', 'Attach a document' => 'Прикрепить файл', 'Do you really want to remove this file: "%s"?' => 'Вы точно хотите удалить этот файл « %s » ?', - 'Attachments' => 'Приложение', + 'Attachments' => 'Вложения', 'Edit the task' => 'Изменить задачу', 'Edit the description' => 'Изменить описание', 'Add a comment' => 'Добавить комментарий', @@ -316,7 +310,7 @@ return array( 'estimated' => 'расчетное', 'Sub-Tasks' => 'Подзадачи', 'Add a sub-task' => 'Добавить подзадачу', - 'Original estimate' => 'Запланировано', + 'Original estimate' => 'Заплан.', 'Create another sub-task' => 'Создать другую подзадачу', 'Time spent' => 'Времени затрачено', 'Edit a sub-task' => 'Изменить подзадачу', @@ -396,8 +390,6 @@ return array( 'Email:' => 'E-mail:', 'Notifications:' => 'Уведомления:', 'Notifications' => 'Уведомления', - 'Group:' => 'Группа:', - 'Regular user' => 'Обычный пользователь', 'Account type:' => 'Тип профиля:', 'Edit profile' => 'Редактировать профиль', 'Change password' => 'Сменить пароль', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s сменил назначенного для задачи %s на %s', 'New password for the user "%s"' => 'Новый пароль для пользователя "%s"', 'Choose an event' => 'Выберите событие', - 'Github commit received' => 'Github: коммит получен', - 'Github issue opened' => 'Github: новая проблема', - 'Github issue closed' => 'Github: проблема закрыта', - 'Github issue reopened' => 'Github: проблема переоткрыта', - 'Github issue assignee change' => 'Github: сменить ответственного за проблему', - 'Github issue label change' => 'Github: ярлык проблемы изменен', 'Create a task from an external provider' => 'Создать задачу из внешнего источника', 'Change the assignee based on an external username' => 'Изменить назначенного основываясь на внешнем имени пользователя', 'Change the category based on an external label' => 'Изменить категорию основываясь на внешнем ярлыке', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Любой может получить доступ к этому проекту.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Помощь по Github webhooks', 'Create a comment from an external provider' => 'Создать комментарий из внешнего источника', - 'Github issue comment created' => 'Github issue комментарий создан', 'Project management' => 'Управление проектом', 'My projects' => 'Мои проекты', 'Columns' => 'Колонки', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Перераспределение пользователей для "%s"', 'Clone this project' => 'Клонировать проект', 'Column removed successfully.' => 'Колонка успешно удалена.', - 'Github Issue' => 'Вопрос на Github', 'Not enough data to show the graph.' => 'Недостаточно данных, чтобы показать график.', 'Previous' => 'Предыдущий', 'The id must be an integer' => 'Этот id должен быть целочисленным', @@ -540,14 +522,12 @@ return array( 'Nothing to preview...' => 'Нет данных для предпросмотра...', 'Preview' => 'Предпросмотр', 'Write' => 'Написание', - 'Active swimlanes' => 'Активные ', + 'Active swimlanes' => 'Активные дорожки', 'Add a new swimlane' => 'Добавить новую дорожку', 'Change default swimlane' => 'Сменить стандартную дорожку', 'Default swimlane' => 'Стандартная дорожка', 'Do you really want to remove this swimlane: "%s"?' => 'Вы действительно хотите удалить дорожку "%s"?', 'Inactive swimlanes' => 'Неактивные дорожки', - 'Set project manager' => 'Установить менеджера проекта', - 'Set project member' => 'Установить участника проекта', 'Remove a swimlane' => 'Удалить дорожку', 'Rename' => 'Переименовать', 'Show default swimlane' => 'Показать стандартную дорожку', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Ваша дорожка была успешно создан.', 'Example: "Bug, Feature Request, Improvement"' => 'Например: "Баг, Фича, Улучшение"', 'Default categories for new projects (Comma-separated)' => 'Стандартные категории для нового проекта (разделяются запятыми)', - // 'Gitlab commit received' => '', - 'Gitlab issue opened' => 'Gitlab вопрос открыт', - 'Gitlab issue closed' => 'Gitlab вопрос закрыт', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Помощь по Gitlab webhooks', 'Integrations' => 'Интеграции', 'Integration with third-party services' => 'Интеграция со сторонними сервисами', - 'Role for this project' => 'Роли для этого проекта', - 'Project manager' => 'Менеджер проекта', - 'Project member' => 'Участник проекта', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Менеджер проекта может изменять настройки проекта и имеет больше привелегий чем стандартный пользователь.', - 'Gitlab Issue' => 'Gitlab вопросы', 'Subtask Id' => 'Id подзадачи', 'Subtasks' => 'Подзадачи', 'Subtasks Export' => 'Экспортировать подзадачи', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'У вас уже есть одна задача в разработке', 'Which parts of the project do you want to duplicate?' => 'Какие части проекта должны быть дублированы?', 'Disallow login form' => 'Запретить форму входа', - // 'Bitbucket commit received' => '', - 'Bitbucket webhooks' => 'BitBucket webhooks', - 'Help on Bitbucket webhooks' => 'Помощь по BitBucket webhooks', 'Start' => 'Начало', 'End' => 'Конец', 'Task age in days' => 'Возраст задачи в днях', @@ -627,7 +594,7 @@ return array( 'Task\'s links' => 'Ссылки задачи', 'The labels must be different' => 'Ярлыки должны быть разными', 'There is no link.' => 'Это не ссылка', - 'This label must be unique' => 'Этот ярлык должна быть уникальной ', + 'This label must be unique' => 'Этот ярлык должен быть уникальным ', 'Unable to create your link.' => 'Не удается создать эту ссылку.', 'Unable to update your link.' => 'Не удается обновить эту ссылку.', 'Unable to remove this link.' => 'Не удается удалить эту ссылку.', @@ -655,7 +622,7 @@ return array( 'Keyboard shortcuts' => 'Горячие клавиши', 'Open board switcher' => 'Открыть переключатель доски', 'Application' => 'Приложение', - // 'since %B %e, %Y at %k:%M %p' => '', + 'since %B %e, %Y at %k:%M %p' => 'с %B %e, %Y - %k:%M %p', 'Compact view' => 'Компактный вид', 'Horizontal scrolling' => 'Широкий вид', 'Compact/wide view' => 'Компактный/широкий вид', @@ -666,7 +633,7 @@ return array( 'Private project' => 'Приватный проект', 'AUD - Australian Dollar' => 'AUD - Австралийский доллар', 'CAD - Canadian Dollar' => 'CAD - Канадский доллар', - 'CHF - Swiss Francs' => 'CHF - Швейцарский Франк', + 'CHF - Swiss Francs' => 'CHF - Швейцарский франк', 'Custom Stylesheet' => 'Пользовательский стиль', 'download' => 'загрузить', 'EUR - Euro' => 'EUR - Евро', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Код двухфакторной авторизации валиден', 'Code' => 'Код', 'Two factor authentication' => 'Двухфакторная авторизация', - 'Enable/disable two factor authentication' => 'Включить/выключить двухфакторную авторизацию', 'This QR code contains the key URI: ' => 'Это QR-код содержит ключевую URI:', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Сохраните Ваш секретный ключ в TOTP программе (например Google Autentificator или FreeOTP).', 'Check my code' => 'Проверить мой код', 'Secret key: ' => 'Секретный ключ: ', 'Test your device' => 'Проверьте свое устройство', @@ -768,7 +733,7 @@ return array( 'Automatically update the start date' => 'Автоматическое обновление даты начала', 'iCal feed' => 'iCal данные', 'Preferences' => 'Предпочтения', - 'Security' => 'Безопастность', + 'Security' => 'Безопасность', 'Two factor authentication disabled' => 'Двухфакторная аутентификация отключена', 'Two factor authentication enabled' => 'Включена двухфакторная аутентификация', 'Unable to update this user.' => 'Не удается обновить этого пользователя.', @@ -776,43 +741,32 @@ return array( 'User that will receive the email' => 'Пользователь, который будет получать e-mail', 'Email subject' => 'Тема e-mail', 'Date' => 'Дата', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', - // 'Add a comment log when moving the task between columns' => '', - // 'Move the task to another column when the category is changed' => '', - // 'Send a task by email to someone' => '', - // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', + 'Add a comment log when moving the task between columns' => 'Добавлять запись при перемещении задачи между колонками', + 'Move the task to another column when the category is changed' => 'Переносить задачи в другую колонку при изменении категории', + 'Send a task by email to someone' => 'Отправить задачу по email', + 'Reopen a task' => 'Переоткрыть задачу', 'Column change' => 'Изменение колонки', 'Position change' => 'Позиция изменена', 'Swimlane change' => 'Дорожка изменена', - // 'Assignee change' => '', + 'Assignee change' => 'Назначенный пользователь изменен', '[%s] Overdue tasks' => '[%s] просроченные задачи', 'Notification' => 'Уведомления', '%s moved the task #%d to the first swimlane' => '%s задач перемещено #%d в первой дорожке', '%s moved the task #%d to the swimlane "%s"' => '%s задач перемещено #%d в дорожке "%s"', 'Swimlane' => 'Дорожки', 'Gravatar' => 'Граватар', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', + '%s moved the task %s to the first swimlane' => '%s переместил задачу %s на первую дорожку', + '%s moved the task %s to the swimlane "%s"' => '%s переместил задачу %s на дорожку "%s"', 'This report contains all subtasks information for the given date range.' => 'Этот отчет содержит всю информацию подзадач в заданном диапазоне дат.', 'This report contains all tasks information for the given date range.' => 'Этот отчет содержит всю информацию для задачи в заданном диапазоне дат.', 'Project activities for %s' => 'Активность проекта для %s', - // 'view the board on Kanboard' => '', + 'view the board on Kanboard' => 'посмотреть доску на Kanboard', 'The task have been moved to the first swimlane' => 'Эта задача была перемещена в первую дорожку', 'The task have been moved to another swimlane:' => 'Эта задача была перемещена в другую дорожку:', 'Overdue tasks for the project "%s"' => 'Просроченные задачи для проекта "%s"', 'New title: %s' => 'Новый заголовок: %s', 'The task is not assigned anymore' => 'Задача больше не назначена', - // 'New assignee: %s' => '', + 'New assignee: %s' => 'Новый назначенный: %s', 'There is no category now' => 'В настоящее время здесь нет категорий', 'New category: %s' => 'Новая категория: %s', 'New color: %s' => 'Новый цвет: %s', @@ -844,7 +798,7 @@ return array( 'Stop timer' => 'Остановить таймер', 'Start timer' => 'Запустить таймер', 'Add project member' => 'Добавить номер проекта', - 'Enable notifications' => 'Отключить уведомления', + 'Enable notifications' => 'Включить уведомления', 'My activity stream' => 'Лента моей активности', 'My calendar' => 'Мой календарь', 'Search tasks' => 'Поиск задачи', @@ -902,16 +856,14 @@ return array( 'Time spent into each column' => 'Время, проведенное в каждой колонке', 'The lead time is the duration between the task creation and the completion.' => 'Время выполнения - период между созданием задачи и завершения.', 'The cycle time is the duration between the start date and the completion.' => 'Время цикла - период времени между датой начала и завершения.', - // 'If the task is not closed the current time is used instead of the completion date.' => '', + 'If the task is not closed the current time is used instead of the completion date.' => 'Если задача не закрыта, то текущая дата будет указана в дате завершения задачи.', 'Set automatically the start date' => 'Установить автоматическую дату начала', 'Edit Authentication' => 'Редактировать авторизацию', - 'Google Id' => 'Google I', + 'Google Id' => 'Google Id', 'Github Id' => 'Github Id', 'Remote user' => 'Удаленный пользователь', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Учетные данные для входа через LDAP, Google и Github не будут сохранены в Kanboard.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Если вы установите флажок "Запретить форму входа", учетные данные, введенные в форму входа будет игнорироваться.', - 'By @%s on Gitlab' => 'От @%s на Gitlab', - 'Gitlab issue comment created' => 'Был создан комментарий к задаче на Gitlab', 'New remote user' => 'Новый удаленный пользователь', 'New local user' => 'Новый локальный пользователь', 'Default task color' => 'Стандартные цвета задач', @@ -931,7 +883,6 @@ return array( 'contributors' => 'соавторы', 'License:' => 'Лицензия:', 'License' => 'Лицензия', - 'Project Administrator' => 'Администратор проекта', 'Enter the text below' => 'Введите текст ниже', 'Gantt chart for %s' => 'Диаграмма Гантта для %s', 'Sort by position' => 'Сортировать по позиции', @@ -955,7 +906,6 @@ return array( 'Members' => 'Участники', 'Shared project' => 'Общие/публичные проекты', 'Project managers' => 'Менеджер проекта', - 'Project members' => 'Участники проекта', 'Gantt chart for all projects' => 'Диаграмма Гантта для всех проектов', 'Projects list' => 'Список проектов', 'Gantt chart for this project' => 'Диаграмма Гантта для этого проекта', @@ -980,92 +930,178 @@ return array( 'Switch to the Gantt chart view' => 'Переключиться в режим диаграммы Гантта', 'Reset the search/filter box' => 'Сбросить поиск/фильтр', 'Documentation' => 'Документация', - 'Table of contents' => 'Сожержание', + 'Table of contents' => 'Содержание', 'Gantt' => 'Гантт', - 'Help with project permissions' => 'Помощь с правами доступа по проекту', - // 'Author' => '', - // 'Version' => '', - // 'Plugins' => '', - // 'There is no plugin loaded.' => '', - // 'Set maximum column height' => '', - // 'Remove maximum column height' => '', - // 'My notifications' => '', - // 'Custom filters' => '', - // 'Your custom filter have been created successfully.' => '', - // 'Unable to create your custom filter.' => '', - // 'Custom filter removed successfully.' => '', - // 'Unable to remove this custom filter.' => '', - // 'Edit custom filter' => '', - // 'Your custom filter have been updated successfully.' => '', - // 'Unable to update custom filter.' => '', - // 'Web' => '', - // 'New attachment on task #%d: %s' => '', - // 'New comment on task #%d' => '', - // 'Comment updated on task #%d' => '', - // 'New subtask on task #%d' => '', - // 'Subtask updated on task #%d' => '', - // 'New task #%d: %s' => '', - // 'Task updated #%d' => '', - // 'Task #%d closed' => '', - // 'Task #%d opened' => '', - // 'Column changed for task #%d' => '', - // 'New position for task #%d' => '', - // 'Swimlane changed for task #%d' => '', - // 'Assignee changed on task #%d' => '', - // '%d overdue tasks' => '', - // 'Task #%d is overdue' => '', - // 'No new notifications.' => '', - // 'Mark all as read' => '', - // 'Mark as read' => '', - // 'Total number of tasks in this column across all swimlanes' => '', - // 'Collapse swimlane' => '', - // 'Expand swimlane' => '', - // 'Add a new filter' => '', - // 'Share with all project members' => '', - // 'Shared' => '', - // 'Owner' => '', - // 'Unread notifications' => '', - // 'My filters' => '', - // 'Notification methods:' => '', - // 'Import tasks from CSV file' => '', - // 'Unable to read your file' => '', - // '%d task(s) have been imported successfully.' => '', - // 'Nothing have been imported!' => '', - // 'Import users from CSV file' => '', - // '%d user(s) have been imported successfully.' => '', - // 'Comma' => '', - // 'Semi-colon' => '', - // 'Tab' => '', - // 'Vertical bar' => '', - // 'Double Quote' => '', - // 'Single Quote' => '', - // '%s attached a file to the task #%d' => '', - // 'There is no column or swimlane activated in your project!' => '', - // 'Append filter (instead of replacement)' => '', - // 'Append/Replace' => '', - // 'Append' => '', - // 'Replace' => '', - // 'There is no notification method registered.' => '', - // 'Import' => '', - // 'change sorting' => '', - // 'Tasks Importation' => '', - // 'Delimiter' => '', - // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', - // 'No external integration registered.' => '', - // 'Duplicates are not imported' => '', - // 'Usernames must be lowercase and unique' => '', - // 'Passwords will be encrypted if present' => '', - // '%s attached a new file to the task %s' => '', - // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', + 'Author' => 'Автор', + 'Version' => 'Версия', + 'Plugins' => 'Плагины', + 'There is no plugin loaded.' => 'Нет установленных плагинов.', + 'Set maximum column height' => 'Установить максимальную высоту колонки', + 'Remove maximum column height' => 'Сбросить максимальную высоту колонки', + 'My notifications' => 'Мои уведомления', + 'Custom filters' => 'Пользовательские фильтры', + 'Your custom filter have been created successfully.' => 'Фильтр был успешно создан.', + 'Unable to create your custom filter.' => 'Невозможно создать фильтр.', + 'Custom filter removed successfully.' => 'Пользовательский фильтр был успешно удален.', + 'Unable to remove this custom filter.' => 'Невозможно удалить фильтр.', + 'Edit custom filter' => 'Изменить пользовательский фильтр', + 'Your custom filter have been updated successfully.' => 'Пользовательский фильтр был успешно обновлен.', + 'Unable to update custom filter.' => 'Невозможно обновить фильтр.', + 'Web' => 'Интернет', + 'New attachment on task #%d: %s' => 'Новое вложение для задачи #%d: %s', + 'New comment on task #%d' => 'Новый комментарий для задачи #%d', + 'Comment updated on task #%d' => 'Обновлен комментарий у задачи #%d', + 'New subtask on task #%d' => 'Новая подзадача у задачи #%d', + 'Subtask updated on task #%d' => 'Подзадача обновлена у задачи #%d', + 'New task #%d: %s' => 'Новая задача #%d: %s', + 'Task updated #%d' => 'Обновлена задача #%d', + 'Task #%d closed' => 'Задача #%d закрыта', + 'Task #%d opened' => 'Задача #%d открыта', + 'Column changed for task #%d' => 'Обновлена колонка у задачи #%d', + 'New position for task #%d' => 'Новая позиция для задачи #%d', + 'Swimlane changed for task #%d' => 'Изменена дорожка у задачи #%d', + 'Assignee changed on task #%d' => 'Изменен назначенный у задачи #%d', + '%d overdue tasks' => '%d просроченных задач', + 'Task #%d is overdue' => 'Задача #%d просрочена', + 'No new notifications.' => 'Нет новых уведомлений.', + 'Mark all as read' => 'Пометить все прочитанными', + 'Mark as read' => 'Пометить прочитанным', + 'Total number of tasks in this column across all swimlanes' => 'Общее число задач в этой колонке на всех дорожках', + 'Collapse swimlane' => 'Свернуть дорожку', + 'Expand swimlane' => 'Развернуть дорожку', + 'Add a new filter' => 'Добавить новый фильтр', + 'Share with all project members' => 'Сделать общим для всех участников проекта', + 'Shared' => 'Общие', + 'Owner' => 'Владелец', + 'Unread notifications' => 'Непрочитанные уведомления', + 'My filters' => 'Мои фильтры', + 'Notification methods:' => 'Способы уведомления:', + 'Import tasks from CSV file' => 'Импорт задач из CSV-файла', + 'Unable to read your file' => 'Невозможно прочитать файл', + '%d task(s) have been imported successfully.' => '%d задач было успешно импортировано.', + 'Nothing have been imported!' => 'Ничего не было импортировано!', + 'Import users from CSV file' => 'Импорт пользователей из CSV-файла', + '%d user(s) have been imported successfully.' => '%d пользователей было успешно импортировано.', + 'Comma' => 'Запятая', + 'Semi-colon' => 'Точка с запятой', + 'Tab' => 'Пробел (Tab)', + 'Vertical bar' => 'Вертикальная черта (|)', + 'Double Quote' => 'Одинарные кавычки', + 'Single Quote' => 'Двойные кавычки', + '%s attached a file to the task #%d' => '%s добавил файл к задаче #%d', + 'There is no column or swimlane activated in your project!' => 'В вашей задаче нет активных колонок или дорожек!', + 'Append filter (instead of replacement)' => 'Добавляющий фильтр (не заменяющий)', + 'Append/Replace' => 'Добавление/Замена', + 'Append' => 'Добавление', + 'Replace' => 'Замена', + 'Import' => 'Импорт', + 'change sorting' => 'изменить сортировку', + 'Tasks Importation' => 'Импортирование задач', + 'Delimiter' => 'Разделитель', + 'Enclosure' => 'Тип кавычек', + 'CSV File' => 'CSV-файл', + 'Instructions' => 'Инструкции', + 'Your file must use the predefined CSV format' => 'Ваш файл должен использовать структуру формата CSV', + 'Your file must be encoded in UTF-8' => 'Ваш файл должен иметь кодировку UTF-8', + 'The first row must be the header' => 'В первой строке должны быть заголовки столбцов', + 'Duplicates are not verified for you' => 'Проверка на дубликаты не осуществляется', + 'The due date must use the ISO format: YYYY-MM-DD' => 'Дата просрочки должна быть в формате ISO: ГГГГ-ММ-ДД', + 'Download CSV template' => 'Скачать шаблон CSV-файла', + 'No external integration registered.' => 'Нет зарегистрированных внешних интеграций.', + 'Duplicates are not imported' => 'Дубликаты не импортируются', + 'Usernames must be lowercase and unique' => 'Логины пользователей должны быть строчными и уникальными', + 'Passwords will be encrypted if present' => 'Пароли будут зашифрованы (если указаны)', + '%s attached a new file to the task %s' => '%s добавил новый файл к задаче %s', + 'Assign automatically a category based on a link' => 'Автоматически назначать категории на основе ссылки', + 'BAM - Konvertible Mark' => 'BAM - Конвертируемая марка', + 'Assignee Username' => 'Логин назначенного', + 'Assignee Name' => 'Имя назначенного', + 'Groups' => 'Группы', + 'Members of %s' => 'Участник группы %s', + 'New group' => 'Новая группа', + 'Group created successfully.' => 'Группа успешно создана.', + 'Unable to create your group.' => 'Невозможно создать группу.', + 'Edit group' => 'Именить группу', + 'Group updated successfully.' => 'Группы успешно обновлена.', + 'Unable to update your group.' => 'Невозможно обновить группу.', + 'Add group member to "%s"' => 'Добавить участника в "%s"', + 'Group member added successfully.' => 'Участник группы успешно добавлен.', + 'Unable to add group member.' => 'Невозможно добавить участника.', + 'Remove user from group "%s"' => 'Удалить пользователя из группы "%s"', + 'User removed successfully from this group.' => 'Пользователь успешно удален из группы.', + 'Unable to remove this user from the group.' => 'Невозможно удалить пользователя из группы.', + 'Remove group' => 'Удалить группу', + 'Group removed successfully.' => 'Группа успешно удалена.', + 'Unable to remove this group.' => 'Невозможно удалить группу.', + 'Project Permissions' => 'Разрешения проекта', + 'Manager' => 'Менеджер', + 'Project Manager' => 'Менеджер проекта', + 'Project Member' => 'Участник проекта', + 'Project Viewer' => 'Наблюдатель проекта', + 'Your account is locked for %d minutes' => 'Ваш аккаунт заблокирован на %d минут', + 'Invalid captcha' => 'Неверный код подтверждения', + 'The name must be unique' => 'Имя должно быть уникальным', + 'View all groups' => 'Просмотр всех группы', + 'View group members' => 'Просмотр всех группы участников группы', + 'There is no user available.' => 'Нет доступных пользователей.', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Вы действительно хотите удалить пользователя "%s" из группы "%s"?', + 'There is no group.' => 'Нет созданных групп.', + 'External Id' => 'Внешний Id', + 'Add group member' => 'Добавить участника в группу', + 'Do you really want to remove this group: "%s"?' => 'Вы действительно хотите удалить группу "%s"?', + 'There is no user in this group.' => 'В этой группе нет участников.', + 'Remove this user' => 'Удалить пользователя.', + 'Permissions' => 'Разрешения', + 'Allowed Users' => 'Разрешенные пользователи', + 'No user have been allowed specifically.' => 'Нет заданных разрешений для пользователей.', + 'Role' => 'Роль', + 'Enter user name...' => 'Введите имя пользователя...', + 'Allowed Groups' => 'Разрешенные группы', + 'No group have been allowed specifically.' => 'Нет заданных разрешений для групп.', + 'Group' => 'Группа', + 'Group Name' => 'Имя группы', + 'Enter group name...' => 'Введите имя группы...', + 'Role:' => 'Роль:', + 'Project members' => 'Участники проекта', + 'Compare hours for "%s"' => 'Сравнить часы для "%s"', + '%s mentioned you in the task #%d' => '%s упомянул вас в задаче #%d', + '%s mentioned you in a comment on the task #%d' => '%s упомянул вас в комментарии к задаче #%d', + 'You were mentioned in the task #%d' => 'Вы упомянуты в задаче #%d', + 'You were mentioned in a comment on the task #%d' => 'Вы упомянуты в комментарии к задаче #%d', + 'Mentioned' => 'Упоминания', + 'Compare Estimated Time vs Actual Time' => 'Сравнить запланированное время и реальное', + 'Estimated hours: ' => 'Запланировано часов: ', + 'Actual hours: ' => 'Реально затрачено часов: ', + 'Hours Spent' => 'Затрачено часов', + 'Hours Estimated' => 'Запланировано часов', + 'Estimated Time' => 'Запланировано времени', + 'Actual Time' => 'Затрачено времени', + 'Estimated vs actual time' => 'Запланировано и реально затрачено времени', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/sr_Latn_RS/translations.php b/sources/app/Locale/sr_Latn_RS/translations.php index af785f9..995cd48 100644 --- a/sources/app/Locale/sr_Latn_RS/translations.php +++ b/sources/app/Locale/sr_Latn_RS/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Složenost', 'Task limit' => 'Ograničenje zadatka', 'Task count' => 'Broj zadataka', - 'Edit project access list' => 'Izmeni prava pristupa projektu', - 'Allow this user' => 'Dozvoli ovog korisnika', - 'Don\'t forget that administrators have access to everything.' => 'Zapamti: Administrator može pristupiti svemu!', - 'Revoke' => 'Povuci', - 'List of authorized users' => 'Spisak odobrenih korisnika', 'User' => 'Korisnik', - 'Nobody have access to this project.' => 'Niko nema pristup ovom projektu', 'Comments' => 'Komentari', 'Write your text in Markdown' => 'Pisanje teksta pomoću Markdown', 'Leave a comment' => 'Ostavi komentar', @@ -396,8 +390,6 @@ return array( 'Email:' => 'Email: ', 'Notifications:' => 'Obaveštenja: ', 'Notifications' => 'Obaveštenja', - 'Group:' => 'Grupa:', - 'Regular user' => 'Standardni korisnik', 'Account type:' => 'Vrsta naloga:', 'Edit profile' => 'Izmeni profil', 'Change password' => 'Izmeni lozinku', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s zamena dodele za zadatak %s na %s', 'New password for the user "%s"' => 'Nova lozinka za korisnika "%s"', 'Choose an event' => 'Izaberi događaj', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', 'Create a task from an external provider' => 'Kreiraj zadatak preko posrednika', 'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika', 'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrzenj etykiety', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Svima je dozvoljen pristup.', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', - // 'Github issue comment created' => '', 'Project management' => 'Uređivanje projekata', 'My projects' => 'Moji projekti', 'Columns' => 'Kolone', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Zaduženja korisnika za "%s"', 'Clone this project' => 'Kopiraj projekat', 'Column removed successfully.' => 'Kolumna usunięta pomyslnie.', - // 'Github Issue' => '', 'Not enough data to show the graph.' => 'Nedovoljno podataka za grafikon.', 'Previous' => 'Prethodni', 'The id must be an integer' => 'ID musi być liczbą całkowitą', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Osnovni razdelnik', 'Do you really want to remove this swimlane: "%s"?' => 'Da li da uklonim razdelnik: "%s"?', 'Inactive swimlanes' => 'Neaktivni razdelniki', - 'Set project manager' => 'Podesi menadžera projekta', - 'Set project member' => 'Podesi učesnika projekat', 'Remove a swimlane' => 'Ukloni razdelnik', 'Rename' => 'Preimenuj', 'Show default swimlane' => 'Prikaži osnovni razdelnik', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Razdelnik je uspešno kreiran.', 'Example: "Bug, Feature Request, Improvement"' => 'Npr: "Greška, Zahtev za izmenama, Poboljšanje"', 'Default categories for new projects (Comma-separated)' => 'Osnovne kategorije za projekat', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', 'Integrations' => 'Integracje', 'Integration with third-party services' => 'Integracja sa uslugama spoljnih servisa', - 'Role for this project' => 'Uloga u ovom projektu', - 'Project manager' => 'Manadžer projekta', - 'Project member' => 'Učesnik projekta', - // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '', - // 'Gitlab Issue' => '', 'Subtask Id' => 'ID pod-zadania', 'Subtasks' => 'Pod-zadataka', 'Subtasks Export' => 'Eksport pod-zadań', @@ -602,9 +572,6 @@ return array( // 'You already have one subtask in progress' => '', 'Which parts of the project do you want to duplicate?' => 'Koje delove projekta želite da kopirate', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', // 'Start' => '', // 'End' => '', // 'Task age in days' => '', @@ -702,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', // 'Code' => '', // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', // 'Check my code' => '', // 'Secret key: ' => '', // 'Test your device' => '', @@ -776,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -910,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -931,7 +883,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +906,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/sv_SE/translations.php b/sources/app/Locale/sv_SE/translations.php index 188b2bd..af14409 100644 --- a/sources/app/Locale/sv_SE/translations.php +++ b/sources/app/Locale/sv_SE/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'Komplexitet', 'Task limit' => 'Uppgiftsbegränsning', 'Task count' => 'Antal uppgifter', - 'Edit project access list' => 'Ändra projektåtkomst lista', - 'Allow this user' => 'Tillåt användare', - 'Don\'t forget that administrators have access to everything.' => 'Glöm inte att administratörerna har rätt att göra allt.', - 'Revoke' => 'Dra tillbaka behörighet', - 'List of authorized users' => 'Lista med behöriga användare', 'User' => 'Användare', - 'Nobody have access to this project.' => 'Ingen har tillgång till detta projekt.', 'Comments' => 'Kommentarer', 'Write your text in Markdown' => 'Exempelsyntax för text', 'Leave a comment' => 'Lämna en kommentar', @@ -396,8 +390,6 @@ return array( 'Email:' => 'E-post:', 'Notifications:' => 'Notiser:', 'Notifications' => 'Notiser', - 'Group:' => 'Grupp:', - 'Regular user' => 'Normal användare', 'Account type:' => 'Kontotyp:', 'Edit profile' => 'Ändra profil', 'Change password' => 'Byt lösenord', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s byt tilldelning av uppgiften %s till %s', 'New password for the user "%s"' => 'Nytt lösenord för användaren "%s"', 'Choose an event' => 'Välj en händelse', - 'Github commit received' => 'Github-bidrag mottaget', - 'Github issue opened' => 'Github-fråga öppnad', - 'Github issue closed' => 'Github-fråga stängd', - 'Github issue reopened' => 'Github-fråga öppnad på nytt', - 'Github issue assignee change' => 'Github-fråga ny tilldelning', - 'Github issue label change' => 'Github-fråga etikettförändring', 'Create a task from an external provider' => 'Skapa en uppgift från en extern leverantör', 'Change the assignee based on an external username' => 'Ändra tilldelning baserat på ett externt användarnamn', 'Change the category based on an external label' => 'Ändra kategori baserat på en extern etikett', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Alla har tillgång till projektet', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github webhooks', - 'Help on Github webhooks' => 'Hjälp för Github webhooks', 'Create a comment from an external provider' => 'Skapa en kommentar från en extern leverantör', - 'Github issue comment created' => 'Github frågekommentar skapad', 'Project management' => 'Projekthantering', 'My projects' => 'Mina projekt', 'Columns' => 'Kolumner', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'Användardeltagande för "%s"', 'Clone this project' => 'Klona projektet', 'Column removed successfully.' => 'Kolumnen togs bort', - 'Github Issue' => 'Github fråga', 'Not enough data to show the graph.' => 'Inte tillräckligt med data för att visa graf', 'Previous' => 'Föregående', 'The id must be an integer' => 'ID måste vara ett heltal', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Standard swimlane', 'Do you really want to remove this swimlane: "%s"?' => 'Vill du verkligen ta bort denna swimlane: "%s"?', 'Inactive swimlanes' => 'Inaktiv swimlane', - 'Set project manager' => 'Sätt Projektadministratör', - 'Set project member' => 'Sätt projektmedlem', 'Remove a swimlane' => 'Ta bort en swimlane', 'Rename' => 'Byt namn', 'Show default swimlane' => 'Visa standard swimlane', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Din swimlane har skapats', 'Example: "Bug, Feature Request, Improvement"' => 'Exempel: "Bug, ny funktionalitet, förbättringar"', 'Default categories for new projects (Comma-separated)' => 'Standardkategorier för nya projekt (komma-separerade)', - 'Gitlab commit received' => 'Gitlab bidrag mottaget', - 'Gitlab issue opened' => 'Gitlab fråga öppnad', - 'Gitlab issue closed' => 'Gitlab fråga stängd', - 'Gitlab webhooks' => 'Gitlab webhooks', - 'Help on Gitlab webhooks' => 'Hjälp för Gitlab webhooks', 'Integrations' => 'Integrationer', 'Integration with third-party services' => 'Integration med tjänst från tredjepart', - 'Role for this project' => 'Roll för detta projekt', - 'Project manager' => 'Projektadministratör', - 'Project member' => 'Projektmedlem', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'En projektadministratör kan ändra inställningar för projektet och har mer rättigheter än en standardanvändare.', - 'Gitlab Issue' => 'Gitlab fråga', 'Subtask Id' => 'Deluppgifts-ID', 'Subtasks' => 'Deluppgift', 'Subtasks Export' => 'Export av deluppgifter', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'Du har redan en deluppgift igång', 'Which parts of the project do you want to duplicate?' => 'Vilka delar av projektet vill du duplicera?', // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket bidrag mottaget', - 'Bitbucket webhooks' => 'Bitbucket webhooks', - 'Help on Bitbucket webhooks' => 'Hjälp för Bitbucket webhooks', 'Start' => 'Start', 'End' => 'Slut', 'Task age in days' => 'Uppgiftsålder i dagar', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => 'Tvåfaktorsverifieringskoden är giltig.', 'Code' => 'Kod', 'Two factor authentication' => 'Tvåfaktorsverifiering', - 'Enable/disable two factor authentication' => 'Aktivera/avaktivera tvåfaktorsverifiering', 'This QR code contains the key URI: ' => 'Denna QR-kod innehåller nyckel-URI:n', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Spara säkerhetsnyckeln i din TOTP mjukvara (med exempelvis Google Authenticator eller FreeOTP).', 'Check my code' => 'Kolla min kod', 'Secret key: ' => 'Säkerhetsnyckel:', 'Test your device' => 'Testa din enhet', @@ -776,21 +741,10 @@ return array( 'User that will receive the email' => 'Användare som kommer att ta emot mailet', 'Email subject' => 'E-post ämne', 'Date' => 'Datum', - 'By @%s on Bitbucket' => 'Av @%s på Bitbucket', - 'Bitbucket Issue' => 'Bitbucket fråga', - 'Commit made by @%s on Bitbucket' => 'Bidrag gjort av @%s på Bitbucket', - 'Commit made by @%s on Github' => 'Bidrag gjort av @%s på Github', - 'By @%s on Github' => 'Av @%s på Github', - 'Commit made by @%s on Gitlab' => 'Bidrag gjort av @%s på Gitlab', 'Add a comment log when moving the task between columns' => 'Lägg till en kommentarslogg när en uppgift flyttas mellan kolumner', 'Move the task to another column when the category is changed' => 'Flyttas uppgiften till en annan kolumn när kategorin ändras', 'Send a task by email to someone' => 'Skicka en uppgift med e-post till någon', 'Reopen a task' => 'Återöppna en uppgift', - 'Bitbucket issue opened' => 'Bitbucketfråga öppnad', - 'Bitbucket issue closed' => 'Bitbucketfråga stängd', - 'Bitbucket issue reopened' => 'Bitbucketfråga återöppnad', - 'Bitbucket issue assignee change' => 'Bitbucketfråga tilldelningsändring', - 'Bitbucket issue comment created' => 'Bitbucketfråga kommentar skapad', 'Column change' => 'Kolumnändring', 'Position change' => 'Positionsändring', 'Swimlane change' => 'Swimlaneändring', @@ -910,8 +864,6 @@ return array( 'Remote user' => 'Extern användare', 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Externa användares lösenord lagras inte i Kanboard-databasen, exempel: LDAP, Google och Github-konton.', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Om du aktiverar boxen "Tillåt inte loginformulär" kommer inloggningsuppgifter i formuläret att ignoreras.', - 'By @%s on Gitlab' => 'Av @%s på Gitlab', - 'Gitlab issue comment created' => 'Gitlab frågekommentar skapad', 'New remote user' => 'Ny extern användare', 'New local user' => 'Ny lokal användare', 'Default task color' => 'Standardfärg för uppgifter', @@ -931,7 +883,6 @@ return array( 'contributors' => 'bidragare:', 'License:' => 'Licens:', 'License' => 'Licens', - 'Project Administrator' => 'Projektadministratör', 'Enter the text below' => 'Fyll i texten nedan', 'Gantt chart for %s' => 'Gantt-schema för %s', 'Sort by position' => 'Sortera efter position', @@ -955,7 +906,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/th_TH/translations.php b/sources/app/Locale/th_TH/translations.php index d180c5e..07733f2 100644 --- a/sources/app/Locale/th_TH/translations.php +++ b/sources/app/Locale/th_TH/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => 'ความซับซ้อน', 'Task limit' => 'จำกัดงาน', 'Task count' => 'นับงาน', - 'Edit project access list' => 'แก้ไขการเข้าถึงรายชื่อโปรเจค', - 'Allow this user' => 'อนุญาตผู้ใช้นี้', - 'Don\'t forget that administrators have access to everything.' => 'อย่าลืมผู้ดูแลระบบสามารถเข้าถึงได้ทุกอย่าง', - 'Revoke' => 'ยกเลิก', - 'List of authorized users' => 'รายชื่อผู้ใช้ที่ได้รับการยืนยัน', 'User' => 'ผู้ใช้', - // 'Nobody have access to this project.' => '', 'Comments' => 'ความคิดเห็น', 'Write your text in Markdown' => 'เขียนข้อความในรูปแบบ Markdown', 'Leave a comment' => 'ออกความคิดเห็น', @@ -396,8 +390,6 @@ return array( 'Email:' => 'อีเมล:', 'Notifications:' => 'แจ้งเตือน:', 'Notifications' => 'การแจ้งเตือน', - 'Group:' => 'กลุ่ม:', - 'Regular user' => 'ผู้ใช้ปกติ:', 'Account type:' => 'ชนิดบัญชี:', 'Edit profile' => 'แก้ไขประวัติ', 'Change password' => 'เปลี่ยนรหัสผ่าน', @@ -445,12 +437,6 @@ return array( // '%s changed the assignee of the task %s to %s' => '', 'New password for the user "%s"' => 'รหัสผ่านใหม่สำหรับผู้ใช้ "%s"', 'Choose an event' => 'เลือกเหตุการณ์', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', // 'Create a task from an external provider' => '', // 'Change the assignee based on an external username' => '', // 'Change the category based on an external label' => '', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'ทุกคนสามารถเข้าถึงโปรเจคนี้', // 'Webhooks' => '', // 'API' => '', - // 'Github webhooks' => '', - // 'Help on Github webhooks' => '', // 'Create a comment from an external provider' => '', - // 'Github issue comment created' => '', 'Project management' => 'การจัดการโปรเจค', 'My projects' => 'โปรเจคของฉัน', 'Columns' => 'คอลัมน์', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => 'การแบ่งงานของผู้ใช้ "%s"', 'Clone this project' => 'เลียนแบบโปรเจคนี้', 'Column removed successfully.' => 'ลบคอลัมน์สำเร็จ', - // 'Github Issue' => '', 'Not enough data to show the graph.' => 'ไม่มีข้อมูลแสดงเป็นกราฟ', 'Previous' => 'ก่อนหน้า', 'The id must be an integer' => 'ไอดีต้องเป็นตัวเลขจำนวนเต็ม', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'สวิมเลนเริ่มต้น', 'Do you really want to remove this swimlane: "%s"?' => 'คุณต้องการลบสวิมเลนนี้ : "%s"?', 'Inactive swimlanes' => 'สวิมเลนไม่ทำงาน', - 'Set project manager' => 'กำหนดผู้จัดการโปรเจค', - 'Set project member' => 'กำหนดสมาชิกโปรเจค', 'Remove a swimlane' => 'ลบสวิมเลน', 'Rename' => 'เปลี่ยนชื่อ', 'Show default swimlane' => 'แสดงสวิมเลนเริ่มต้น', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'สวิมเลนของคุณถูกสร้างเรียบร้อยแล้ว', 'Example: "Bug, Feature Request, Improvement"' => 'ตัวอย่าง: "Bug, Feature Request, Improvement"', 'Default categories for new projects (Comma-separated)' => 'ค่าเริ่มต้นกลุ่มสำหรับโปรเจคใหม่ (Comma-separated)', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', 'Integrations' => 'การใช้ร่วมกัน', 'Integration with third-party services' => 'การใช้งานร่วมกับบริการ third-party', - // 'Role for this project' => '', - 'Project manager' => 'ผู้จัดการโปรเจค', - 'Project member' => 'สมาชิกโปรเจค', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'ผู้จัดการโปรเจคสามารถตั้งค่าของโปรเจคและมีสิทธิ์มากกว่าผู้ใช้ทั่วไป', - // 'Gitlab Issue' => '', 'Subtask Id' => 'รหัสงานย่อย', 'Subtasks' => 'งานย่อย', 'Subtasks Export' => 'ส่งออก งานย่อย', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => 'คุณมีหนึ่งงานย่อยที่กำลังทำงาน', // 'Which parts of the project do you want to duplicate?' => '', // 'Disallow login form' => '', - // 'Bitbucket commit received' => '', - // 'Bitbucket webhooks' => '', - // 'Help on Bitbucket webhooks' => '', 'Start' => 'เริ่ม', 'End' => 'จบ', 'Task age in days' => 'อายุงาน', @@ -702,9 +669,7 @@ return array( // 'The two factor authentication code is valid.' => '', 'Code' => 'รหัส', // 'Two factor authentication' => '', - 'Enable/disable two factor authentication' => 'เปิด/ปิด การยืนยันตัวตนสองชั้น', // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', 'Check my code' => 'ตรวจสอบรหัสของฉัน', 'Secret key: ' => 'กุญแจลับ', 'Test your device' => 'ทดสอบอุปกรณ์ของคุณ', @@ -776,21 +741,10 @@ return array( // 'User that will receive the email' => '', // 'Email subject' => '', // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -910,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', // 'Default task color' => '', @@ -931,7 +883,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +906,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/tr_TR/translations.php b/sources/app/Locale/tr_TR/translations.php index fca425e..8fb2483 100644 --- a/sources/app/Locale/tr_TR/translations.php +++ b/sources/app/Locale/tr_TR/translations.php @@ -20,15 +20,15 @@ return array( 'Red' => 'Kırmızı', 'Orange' => 'Turuncu', 'Grey' => 'Gri', - // 'Brown' => '', - // 'Deep Orange' => '', - // 'Dark Grey' => '', - // 'Pink' => '', - // 'Teal' => '', - // 'Cyan' => '', - // 'Lime' => '', - // 'Light Green' => '', - // 'Amber' => '', + 'Brown' => 'Kahverengi', + 'Deep Orange' => 'Koyu Turuncu', + 'Dark Grey' => 'Koyu Gri', + 'Pink' => 'Pembe', + 'Teal' => 'Turkuaz', + 'Cyan' => 'Cam Göbeği', + 'Lime' => 'Limon rengi', + 'Light Green' => 'Açık Yeşil', + 'Amber' => 'Koyu sarı', 'Save' => 'Kaydet', 'Login' => 'Giriş', 'Official website:' => 'Resmi internet sitesi:', @@ -86,7 +86,7 @@ return array( 'Application settings' => 'Uygulama ayarları', 'Language' => 'Dil', // 'Webhook token:' => '', - 'API token:' => 'API Token:', + 'API token:' => 'API belirteci:', 'Database size:' => 'Veritabanı boyutu :', 'Download the database' => 'Veritabanını indir', 'Optimize the database' => 'Veritabanını optimize et', @@ -98,11 +98,11 @@ return array( 'Color' => 'Renk', 'Assignee' => 'Atanan', 'Create another task' => 'Başka bir görev oluştur', - 'New task' => 'Nouvelle tâche', + 'New task' => 'Yeni görev', 'Open a task' => 'Bir görevi aç', 'Do you really want to open this task: "%s"?' => 'Bu görevi gerçekten açmak istiyor musunuz: "%s"?', 'Back to the board' => 'Tabloya dön', - // 'Created on %B %e, %Y at %k:%M %p' => '', + 'Created on %B %e, %Y at %k:%M %p' => '%B %e, %Y, saat %k:%M %p da oluşturuldu', 'There is nobody assigned' => 'Kimse atanmamış', 'Column on the board:' => 'Tablodaki sütun:', 'Status is open' => 'Açık durumda', @@ -158,29 +158,23 @@ return array( 'Work in progress' => 'İşlemde', 'Done' => 'Tamamlandı', 'Application version:' => 'Uygulama versiyonu:', - // 'Completed on %B %e, %Y at %k:%M %p' => '', - // '%B %e, %Y at %k:%M %p' => '', + 'Completed on %B %e, %Y at %k:%M %p' => '%B %e, %Y saat %k:%M %p da tamamlandı', + '%B %e, %Y at %k:%M %p' => '%B %e, %Y saat %k:%M %p', 'Date created' => 'Oluşturulma tarihi', 'Date completed' => 'Tamamlanma tarihi', 'Id' => 'Kod', '%d closed tasks' => '%d kapatılmış görevler', - // 'No task for this project' => '', + 'No task for this project' => 'Bu proje için görev yok', 'Public link' => 'Dışa açık link', 'Change assignee' => 'Atanmış Kullanıcıyı değiştir', 'Change assignee for the task "%s"' => '"%s" görevi için atanmış kullanıcıyı değiştir', 'Timezone' => 'Saat dilimi', - // 'Sorry, I didn\'t find this information in my database!' => '', + 'Sorry, I didn\'t find this information in my database!' => 'Üzgünüm, bu bilgiyi veri tabanımda bulamadım.', 'Page not found' => 'Sayfa bulunamadı', 'Complexity' => 'Zorluk seviyesi', 'Task limit' => 'Görev limiti', 'Task count' => 'Görev sayısı', - 'Edit project access list' => 'Proje erişim listesini düzenle', - 'Allow this user' => 'Bu kullanıcıya izin ver', - 'Don\'t forget that administrators have access to everything.' => 'Dikkat: Yöneticilerin herşeye erişimi olduğunu unutmayın!', - 'Revoke' => 'Iptal et', - 'List of authorized users' => 'Yetkili kullanıcıların listesi', 'User' => 'Kullanıcı', - 'Nobody have access to this project.' => 'Bu projeye kimsenin erişimi yok.', 'Comments' => 'Yorumlar', 'Write your text in Markdown' => 'Yazınızı Markdown ile yazın', 'Leave a comment' => 'Bir yorum ekle', @@ -189,9 +183,9 @@ return array( 'Comment added successfully.' => 'Yorum eklendi', 'Unable to create your comment.' => 'Yorumunuz oluşturulamadı', 'Edit this task' => 'Bu görevi değiştir', - 'Due Date' => 'Termin', + 'Due Date' => 'Bitiş Tarihi', 'Invalid date' => 'Geçersiz tarihi', - // 'Must be done before %B %e, %Y' => '', + 'Must be done before %B %e, %Y' => '%B %e, %Y tarihinden önce yapılmalı', '%B %e, %Y' => '%d %B %Y', '%b %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Otomatik işlemler', @@ -228,7 +222,7 @@ return array( 'Move Down' => 'Aşağı taşı', 'Duplicate to another project' => 'Başka bir projeye kopyala', 'Duplicate' => 'Kopya oluştur', - 'link' => 'link', + 'link' => 'bağlantı', 'Comment updated successfully.' => 'Yorum güncellendi.', 'Unable to update your comment.' => 'Yorum güncellenemedi.', 'Remove a comment' => 'Bir yorumu sil', @@ -238,7 +232,7 @@ return array( 'Only administrators or the creator of the comment can access to this page.' => 'Bu sayfaya yalnızca yorum sahibi ve yöneticiler erişebilir.', 'Current password for the user "%s"' => 'Kullanıcı için mevcut şifre "%s"', 'The current password is required' => 'Mevcut şifre gerekli', - 'Wrong password' => 'Yanlış Şifre', + 'Wrong password' => 'Yanlış şifre', 'Unknown' => 'Bilinmeyen', 'Last logins' => 'Son kullanıcı girişleri', 'Login date' => 'Giriş tarihi', @@ -246,7 +240,7 @@ return array( 'IP address' => 'IP adresi', 'User agent' => 'Kullanıcı sistemi', 'Persistent connections' => 'Kalıcı bağlantılar', - // 'No session.' => '', + 'No session.' => 'Oturum yok.', 'Expiration date' => 'Geçerlilik sonu', 'Remember Me' => 'Beni hatırla', 'Creation date' => 'Oluşturulma tarihi', @@ -254,41 +248,41 @@ return array( 'Open' => 'Açık', 'Closed' => 'Kapalı', 'Search' => 'Ara', - 'Nothing found.' => 'Hiçbir şey bulunamadı', - 'Due date' => 'Termin', + 'Nothing found.' => 'Hiçbir şey bulunamadı.', + 'Due date' => 'Bitiş tarihi', 'Others formats accepted: %s and %s' => 'Diğer kabul edilen formatlar: %s ve %s', 'Description' => 'Açıklama', - '%d comments' => '%d yorumlar', + '%d comments' => '%d yorum', '%d comment' => '%d yorum', - 'Email address invalid' => 'E-Posta adresi geçersiz', - // 'Your external account is not linked anymore to your profile.' => '', - // 'Unable to unlink your external account.' => '', - // 'External authentication failed' => '', - // 'Your external account is linked to your profile successfully.' => '', - 'Email' => 'E-Posta', + 'Email address invalid' => 'E-posta adresi geçersiz', + 'Your external account is not linked anymore to your profile.' => 'Harici hesabınız artık profilinize bağlı değil', + 'Unable to unlink your external account.' => 'Harici hesabınızla bağlantı koparılamadı', + 'External authentication failed' => 'Harici hesap doğrulaması başarısız', + 'Your external account is linked to your profile successfully.' => 'Harici hesabınız profilinizle başarıyla bağlandı.', + 'Email' => 'E-posta', 'Link my Google Account' => 'Google hesabımla bağ oluştur', 'Unlink my Google Account' => 'Google hesabımla bağı kaldır', 'Login with my Google Account' => 'Google hesabımla giriş yap', 'Project not found.' => 'Proje bulunamadı', - 'Task removed successfully.' => 'Görev silindi', - 'Unable to remove this task.' => 'Görev silinemiyor', + 'Task removed successfully.' => 'Görev başarıyla silindi.', + 'Unable to remove this task.' => 'Görev silinemiyor.', 'Remove a task' => 'Bir görevi sil', 'Do you really want to remove this task: "%s"?' => 'Bu görevi silmek istediğinize emin misiniz: "%s"?', 'Assign automatically a color based on a category' => 'Kategoriye göre otomatik renk ata', 'Assign automatically a category based on a color' => 'Rengine göre otomatik kategori ata', - 'Task creation or modification' => 'Görev oluşturma veya değiştirme', + 'Task creation or modification' => 'Görev oluşturma veya düzenleme', 'Category' => 'Kategori', 'Category:' => 'Kategori:', 'Categories' => 'Kategoriler', - 'Category not found.' => 'Kategori bulunamadı', - 'Your category have been created successfully.' => 'Kategori oluşturuldu', - 'Unable to create your category.' => 'Kategori oluşturulamadı', - 'Your category have been updated successfully.' => 'Kategori başarıyla güncellendi', - 'Unable to update your category.' => 'Kategori güncellenemedi', + 'Category not found.' => 'Kategori bulunamadı.', + 'Your category have been created successfully.' => 'Kategori başarıyla oluşturuldu.', + 'Unable to create your category.' => 'Kategori oluşturulamadı.', + 'Your category have been updated successfully.' => 'Kategori başarıyla güncellendi.', + 'Unable to update your category.' => 'Kategori güncellenemedi.', 'Remove a category' => 'Bir kategoriyi sil', - 'Category removed successfully.' => 'Kategori silindi', - 'Unable to remove this category.' => 'Bu kategori silinemedi', - 'Category modification for the project "%s"' => '"%s" projesi için kategori değiştirme', + 'Category removed successfully.' => 'Kategori başarıyla silindi.', + 'Unable to remove this category.' => 'Bu kategori silinemedi.', + 'Category modification for the project "%s"' => '"%s" projesi için kategori düzenleme', 'Category Name' => 'Kategori adı', 'Add a new category' => 'Yeni kategori ekle', 'Do you really want to remove this category: "%s"?' => 'Bu kategoriyi silmek istediğinize emin misiniz: "%s"?', @@ -316,26 +310,26 @@ return array( 'estimated' => 'tahmini', 'Sub-Tasks' => 'Alt Görev', 'Add a sub-task' => 'Alt görev ekle', - // 'Original estimate' => '', + 'Original estimate' => 'Orjinal tahmin', 'Create another sub-task' => 'Başka bir alt görev daha oluştur', - // 'Time spent' => '', + 'Time spent' => 'Harcanan zaman', 'Edit a sub-task' => 'Alt görev düzenle', 'Remove a sub-task' => 'Alt görev sil', - 'The time must be a numeric value' => 'Zaman alfanumerik bir değer olmalı', + 'The time must be a numeric value' => 'Zaman alfanümerik bir değer olmalı', 'Todo' => 'Yapılacaklar', - 'In progress' => 'İşlemde', - 'Sub-task removed successfully.' => 'Alt görev silindi', - 'Unable to remove this sub-task.' => 'Alt görev silinemedi', - 'Sub-task updated successfully.' => 'Alt görev güncellendi', - 'Unable to update your sub-task.' => 'Alt görev güncellenemiyor', - 'Unable to create your sub-task.' => 'Alt görev oluşturulamadı', - 'Sub-task added successfully.' => 'Alt görev başarıyla eklendii', + 'In progress' => 'Devam etmekte', + 'Sub-task removed successfully.' => 'Alt görev başarıyla silindi.', + 'Unable to remove this sub-task.' => 'Alt görev silinemedi.', + 'Sub-task updated successfully.' => 'Alt görev güncellendi.', + 'Unable to update your sub-task.' => 'Alt görev güncellenemiyor.', + 'Unable to create your sub-task.' => 'Alt görev oluşturulamadı.', + 'Sub-task added successfully.' => 'Alt görev başarıyla eklendi.', 'Maximum size: ' => 'Maksimum boyutu', - 'Unable to upload the file.' => 'Karşıya yükleme başarısız', + 'Unable to upload the file.' => 'Dosya yüklenemiyor.', 'Display another project' => 'Başka bir proje göster', - // 'Login with my Github Account' => '', - // 'Link my Github Account' => '', - // 'Unlink my Github Account' => '', + 'Login with my Github Account' => 'Github hesabımla giriş yap', + 'Link my Github Account' => 'Github hesabını ilişkilendir', + 'Unlink my Github Account' => 'Github hesabıyla bağlantıyı kopar', 'Created by %s' => '%s tarafından oluşturuldu', 'Last modified on %B %e, %Y at %k:%M %p' => 'Son değişiklik tarihi %d.%m.%Y, saati %H:%M', 'Tasks Export' => 'Görevleri dışa aktar', @@ -343,14 +337,14 @@ return array( 'Start Date' => 'Başlangıç tarihi', 'End Date' => 'Bitiş tarihi', 'Execute' => 'Gerçekleştir', - 'Task Id' => 'Görev No', + 'Task Id' => 'Görev Kimliği', 'Creator' => 'Oluşturan', 'Modification date' => 'Değişiklik tarihi', 'Completion date' => 'Tamamlanma tarihi', 'Clone' => 'Kopya oluştur', 'Project cloned successfully.' => 'Proje kopyası başarıyla oluşturuldu.', - 'Unable to clone this project.' => 'Proje kopyası oluşturulamadı.', - 'Enable email notifications' => 'E-Posta bilgilendirmesini aç', + 'Unable to clone this project.' => 'Proje kopyası oluşturulamıyor.', + 'Enable email notifications' => 'E-posta bilgilendirmesini aç', 'Task position:' => 'Görev pozisyonu', 'The task #%d have been opened.' => '#%d numaralı görev açıldı.', 'The task #%d have been closed.' => '#%d numaralı görev kapatıldı.', @@ -371,7 +365,7 @@ return array( 'Task closed' => 'Görev kapatıldı', 'Task opened' => 'Görev açıldı', 'I want to receive notifications only for those projects:' => 'Yalnızca bu projelerle ilgili bildirim almak istiyorum:', - 'view the task on Kanboard' => 'bu görevi Kanboard\'da göster', + 'view the task on Kanboard' => 'bu görevi Kanboard üzerinde göster', 'Public access' => 'Dışa açık erişim', 'User management' => 'Kullanıcı yönetimi', 'Active tasks' => 'Aktif görevler', @@ -393,11 +387,9 @@ return array( 'Disabled' => 'Devre dışı bırakıldı', 'Username:' => 'Kullanıcı adı', 'Name:' => 'Ad', - 'Email:' => 'E-Posta', + 'Email:' => 'E-posta', 'Notifications:' => 'Bildirimler:', 'Notifications' => 'Bildirimler', - 'Group:' => 'Grup', - 'Regular user' => 'Varsayılan kullanıcı', 'Account type:' => 'Hesap türü:', 'Edit profile' => 'Profili değiştir', 'Change password' => 'Şifre değiştir', @@ -407,10 +399,10 @@ return array( 'Github Account' => 'Github hesabı', 'Never connected.' => 'Hiç bağlanmamış.', 'No account linked.' => 'Bağlanmış hesap yok.', - 'Account linked.' => 'Hesap bağlandı', + 'Account linked.' => 'Hesap bağlandı.', 'No external authentication enabled.' => 'Dış kimlik doğrulama kapalı.', 'Password modified successfully.' => 'Şifre başarıyla değiştirildi.', - 'Unable to change the password.' => 'Şifre değiştirilemedi.', + 'Unable to change the password.' => 'Şifre değiştirilemiyor.', 'Change category for the task "%s"' => '"%s" görevi için kategori değiştirme', 'Change category' => 'Kategori değiştirme', '%s updated the task %s' => '%s kullanıcısı %s görevini güncelledi', @@ -445,27 +437,21 @@ return array( '%s changed the assignee of the task %s to %s' => '%s kullanıcısı %s görevinin sorumlusunu %s olarak değiştirdi', 'New password for the user "%s"' => '"%s" kullanıcısı için yeni şifre', 'Choose an event' => 'Bir durum seçin', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', 'Create a task from an external provider' => 'Dış sağlayıcı ile bir görev oluştur', 'Change the assignee based on an external username' => 'Dış kaynaklı kullanıcı adı ile göreve atananı değiştir', 'Change the category based on an external label' => 'Dış kaynaklı bir etiket ile kategori değiştir', 'Reference' => 'Referans', 'Reference: %s' => 'Referans: %s', 'Label' => 'Etiket', - 'Database' => 'Veri bankası', + 'Database' => 'Veritabanı', 'About' => 'Hakkında', - 'Database driver:' => 'Veri bankası sürücüsü', + 'Database driver:' => 'Veritabanı sürücüsü:', 'Board settings' => 'Tablo ayarları', - 'URL and token' => 'URL veya Token', + 'URL and token' => 'URL veya Belirteç', 'Webhook settings' => 'Webhook ayarları', 'URL for task creation:' => 'Görev oluşturma için URL', - 'Reset token' => 'Reset Token', - 'API endpoint:' => 'API endpoint', + 'Reset token' => 'Belirteci sıfırla', + 'API endpoint:' => 'API bitiş noktası:', 'Refresh interval for private board' => 'Özel tablolar için yenileme sıklığı', 'Refresh interval for public board' => 'Dışa açık tablolar için yenileme sıklığı', 'Task highlight period' => 'Görevi öne çıkarma süresi', @@ -474,7 +460,7 @@ return array( 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Saniye olarak frekans (Bu özelliği iptal etmek için 0, varsayılan değer 10 saniye)', 'Application URL' => 'Uygulama URL', 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Örneğin: http://example.kanboard.net/ (E-posta bildirimleri için kullanılıyor)', - 'Token regenerated.' => 'Token yeniden oluşturuldu.', + 'Token regenerated.' => 'Beliteç yeniden oluşturuldu.', 'Date format' => 'Tarih formatı', 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formatı her zaman kabul edilir, örneğin: "%s" ve "%s"', 'New private project' => 'Yeni özel proje', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => 'Bu projeye herkesin erişimi var.', 'Webhooks' => 'Webhooks', 'API' => 'API', - 'Github webhooks' => 'Github Webhook', - 'Help on Github webhooks' => 'Github Webhooks hakkında yardım', 'Create a comment from an external provider' => 'Dış sağlayıcı ile bir yorum oluştur', - 'Github issue comment created' => 'Github hata yorumu oluşturuldu', 'Project management' => 'Proje yönetimi', 'My projects' => 'Projelerim', 'Columns' => 'Sütunlar', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => '"%s" için kullanıcı dağılımı', 'Clone this project' => 'Projenin kopyasını oluştur', 'Column removed successfully.' => 'Sütun başarıyla kaldırıldı.', - 'Github Issue' => 'Github Issue', 'Not enough data to show the graph.' => 'Grafik gösterimi için yeterli veri yok.', 'Previous' => 'Önceki', 'The id must be an integer' => 'ID bir tamsayı olmalı', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => 'Varsayılan Kulvar', 'Do you really want to remove this swimlane: "%s"?' => 'Bu Kulvarı silmek istediğinize emin misiniz?: "%s"?', 'Inactive swimlanes' => 'Pasif Kulvarlar', - 'Set project manager' => 'Proje yöneticisi olarak ata', - 'Set project member' => 'Proje üyesi olarak ata', 'Remove a swimlane' => 'Kulvarı sil', 'Rename' => 'Yeniden adlandır', 'Show default swimlane' => 'Varsayılan Kulvarı göster', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => 'Kulvar başarıyla oluşturuldu.', 'Example: "Bug, Feature Request, Improvement"' => 'Örnek: "Sorun, Özellik talebi, İyileştirme"', 'Default categories for new projects (Comma-separated)' => 'Yeni projeler için varsayılan kategoriler (Virgül ile ayrılmış)', - // 'Gitlab commit received' => '', - // 'Gitlab issue opened' => '', - // 'Gitlab issue closed' => '', - // 'Gitlab webhooks' => '', - // 'Help on Gitlab webhooks' => '', 'Integrations' => 'Entegrasyon', 'Integration with third-party services' => 'Dış servislerle entegrasyon', - 'Role for this project' => 'Bu proje için rol', - 'Project manager' => 'Proje Yöneticisi', - 'Project member' => 'Proje Üyesi', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Bir Proje Yöneticisi proje ayarlarını değiştirebilir ve bir üyeden daha fazla yetkiye sahiptir.', - // 'Gitlab Issue' => '', 'Subtask Id' => 'Alt görev No:', 'Subtasks' => 'Alt görevler', 'Subtasks Export' => 'Alt görevleri dışa aktar', @@ -601,10 +571,7 @@ return array( 'Time Tracking' => 'Zaman takibi', 'You already have one subtask in progress' => 'Zaten işlemde olan bir alt görev var', 'Which parts of the project do you want to duplicate?' => 'Projenin hangi kısımlarının kopyasını oluşturmak istiyorsunuz?', - // 'Disallow login form' => '', - 'Bitbucket commit received' => 'Bitbucket commit alındı', - 'Bitbucket webhooks' => 'Bitbucket webhooks', - 'Help on Bitbucket webhooks' => 'Bitbucket webhooks için yardım', + 'Disallow login form' => 'Giriş formu erişimini engelle', 'Start' => 'Başlangıç', 'End' => 'Son', 'Task age in days' => 'Görev yaşı gün olarak', @@ -659,16 +626,16 @@ return array( 'Compact view' => 'Ekrana sığdır', 'Horizontal scrolling' => 'Geniş görünüm', 'Compact/wide view' => 'Ekrana sığdır / Geniş görünüm', - // 'No results match:' => '', - // 'Currency' => '', - // 'Files' => '', - // 'Images' => '', - // 'Private project' => '', + 'No results match:' => 'Uygun sonuç bulunamadı', + 'Currency' => 'Para birimi', + 'Files' => 'Dosyalar', + 'Images' => 'Resimler', + 'Private project' => 'Özel proje', // 'AUD - Australian Dollar' => '', // 'CAD - Canadian Dollar' => '', // 'CHF - Swiss Francs' => '', // 'Custom Stylesheet' => '', - // 'download' => '', + 'download' => 'indir', // 'EUR - Euro' => '', // 'GBP - British Pound' => '', // 'INR - Indian Rupee' => '', @@ -676,396 +643,465 @@ return array( // 'NZD - New Zealand Dollar' => '', // 'RSD - Serbian dinar' => '', // 'USD - US Dollar' => '', - // 'Destination column' => '', - // 'Move the task to another column when assigned to a user' => '', - // 'Move the task to another column when assignee is cleared' => '', - // 'Source column' => '', - // 'Transitions' => '', - // 'Executer' => '', - // 'Time spent in the column' => '', - // 'Task transitions' => '', - // 'Task transitions export' => '', - // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', - // 'Currency rates' => '', - // 'Rate' => '', - // 'Change reference currency' => '', - // 'Add a new currency rate' => '', - // 'Reference currency' => '', - // 'The currency rate have been added successfully.' => '', - // 'Unable to add this currency rate.' => '', + 'Destination column' => 'Hedef Sütun', + 'Move the task to another column when assigned to a user' => 'Bir kullanıcıya atandığında görevi başka bir sütuna taşı', + 'Move the task to another column when assignee is cleared' => 'Atanmış kullanıcı kaldırıldığında görevi başka bir sütuna taşı', + 'Source column' => 'Kaynak sütun', + 'Transitions' => 'Geçişler', + 'Executer' => 'Uygulayıcı', + 'Time spent in the column' => 'Sütunda harcanan süre', + 'Task transitions' => 'Görev geçişleri', + 'Task transitions export' => 'Görev geçişlerini dışa aktar', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Bu rapor her bir görevin sütunlar arası geçişlerini tarih, kullanıcı ve sütunda harcanan zaman detaylarıyla içerir.', + 'Currency rates' => 'Döviz kurları', + 'Rate' => 'Kurlar', + 'Change reference currency' => 'Referans kur değiştir', + 'Add a new currency rate' => 'Yeni bir kur ekle', + 'Reference currency' => 'Referans kur', + 'The currency rate have been added successfully.' => 'Kur başarıyla eklendi', + 'Unable to add this currency rate.' => 'Bu kur eklenemedi', // 'Webhook URL' => '', - // '%s remove the assignee of the task %s' => '', - // 'Enable Gravatar images' => '', - // 'Information' => '', - // 'Check two factor authentication code' => '', - // 'The two factor authentication code is not valid.' => '', - // 'The two factor authentication code is valid.' => '', - // 'Code' => '', - // 'Two factor authentication' => '', - // 'Enable/disable two factor authentication' => '', - // 'This QR code contains the key URI: ' => '', - // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', - // 'Check my code' => '', - // 'Secret key: ' => '', - // 'Test your device' => '', - // 'Assign a color when the task is moved to a specific column' => '', - // '%s via Kanboard' => '', - // 'uploaded by: %s' => '', - // 'uploaded on: %s' => '', - // 'size: %s' => '', - // 'Burndown chart for "%s"' => '', - // 'Burndown chart' => '', - // 'This chart show the task complexity over the time (Work Remaining).' => '', - // 'Screenshot taken %s' => '', - // 'Add a screenshot' => '', - // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', - // 'Screenshot uploaded successfully.' => '', + '%s remove the assignee of the task %s' => '%s, %s görevinin atanan bilgisini kaldırdı', + 'Enable Gravatar images' => 'Gravatar resimlerini kullanıma aç', + 'Information' => 'Bilgi', + 'Check two factor authentication code' => 'İki kademeli doğrulama kodunu kontrol et', + 'The two factor authentication code is not valid.' => 'İki kademeli doğrulama kodu geçersiz', + 'The two factor authentication code is valid.' => 'İki kademeli doğrulama kodu onaylandı', + 'Code' => 'Kod', + 'Two factor authentication' => 'İki kademeli doğrulama', + 'This QR code contains the key URI: ' => 'Bu QR kodu anahtar URI içerir', + 'Check my code' => 'Kodu kontrol et', + 'Secret key: ' => 'Gizli anahtar', + 'Test your device' => 'Cihazınızı test edin', + 'Assign a color when the task is moved to a specific column' => 'Görev belirli bir sütuna taşındığında rengini değiştir', + '%s via Kanboard' => '%s Kanboard ile', + 'uploaded by: %s' => '%s tarafından yüklendi', + 'uploaded on: %s' => '%s tarihinda yüklendi', + 'size: %s' => 'Boyut: %s', + 'Burndown chart for "%s"' => '%s icin kalan iş grafiği', + 'Burndown chart' => 'Kalan iş grafiği', + 'This chart show the task complexity over the time (Work Remaining).' => 'Bu grafik zorluk seviyesini zamana göre gösterir (kalan iş)', + 'Screenshot taken %s' => 'Ekran görüntüsü alındı %s', + 'Add a screenshot' => 'Bir ekran görüntüsü ekle', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Bir ekran görüntüsü alın ve buraya yapıştırmak için CTRL+V veya ⌘+V tuşlarına basın.', + 'Screenshot uploaded successfully.' => 'Ekran görüntüsü başarıyla yüklendi', // 'SEK - Swedish Krona' => '', - // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', - // 'Identifier' => '', - // 'Disable two factor authentication' => '', - // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', - // 'Edit link' => '', - // 'Start to type task title...' => '', - // 'A task cannot be linked to itself' => '', - // 'The exact same link already exists' => '', - // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', - // 'Score' => '', - // 'The identifier must be unique' => '', - // 'This linked task id doesn\'t exists' => '', - // 'This value must be alphanumeric' => '', - // 'Edit recurrence' => '', - // 'Generate recurrent task' => '', - // 'Trigger to generate recurrent task' => '', - // 'Factor to calculate new due date' => '', - // 'Timeframe to calculate new due date' => '', - // 'Base date to calculate new due date' => '', - // 'Action date' => '', - // 'Base date to calculate new due date: ' => '', - // 'This task has created this child task: ' => '', - // 'Day(s)' => '', - // 'Existing due date' => '', - // 'Factor to calculate new due date: ' => '', - // 'Month(s)' => '', - // 'Recurrence' => '', - // 'This task has been created by: ' => '', - // 'Recurrent task has been generated:' => '', - // 'Timeframe to calculate new due date: ' => '', - // 'Trigger to generate recurrent task: ' => '', - // 'When task is closed' => '', - // 'When task is moved from first column' => '', - // 'When task is moved to last column' => '', - // 'Year(s)' => '', - // 'Calendar settings' => '', - // 'Project calendar view' => '', - // 'Project settings' => '', - // 'Show subtasks based on the time tracking' => '', - // 'Show tasks based on the creation date' => '', - // 'Show tasks based on the start date' => '', - // 'Subtasks time tracking' => '', - // 'User calendar view' => '', - // 'Automatically update the start date' => '', - // 'iCal feed' => '', - // 'Preferences' => '', - // 'Security' => '', - // 'Two factor authentication disabled' => '', - // 'Two factor authentication enabled' => '', - // 'Unable to update this user.' => '', - // 'There is no user management for private projects.' => '', - // 'User that will receive the email' => '', - // 'Email subject' => '', - // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', - // 'Add a comment log when moving the task between columns' => '', - // 'Move the task to another column when the category is changed' => '', - // 'Send a task by email to someone' => '', - // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', - // 'Column change' => '', - // 'Position change' => '', - // 'Swimlane change' => '', - // 'Assignee change' => '', - // '[%s] Overdue tasks' => '', - // 'Notification' => '', - // '%s moved the task #%d to the first swimlane' => '', - // '%s moved the task #%d to the swimlane "%s"' => '', - // 'Swimlane' => '', - // 'Gravatar' => '', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', - // 'This report contains all subtasks information for the given date range.' => '', - // 'This report contains all tasks information for the given date range.' => '', - // 'Project activities for %s' => '', - // 'view the board on Kanboard' => '', - // 'The task have been moved to the first swimlane' => '', - // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', - // 'New title: %s' => '', - // 'The task is not assigned anymore' => '', - // 'New assignee: %s' => '', - // 'There is no category now' => '', - // 'New category: %s' => '', - // 'New color: %s' => '', - // 'New complexity: %d' => '', - // 'The due date have been removed' => '', - // 'There is no description anymore' => '', - // 'Recurrence settings have been modified' => '', - // 'Time spent changed: %sh' => '', - // 'Time estimated changed: %sh' => '', - // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', - // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', - // 'I want to receive notifications for:' => '', - // 'All tasks' => '', - // 'Only for tasks assigned to me' => '', - // 'Only for tasks created by me' => '', - // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', - // '%%Y-%%m-%%d' => '', - // 'Total for all columns' => '', - // 'You need at least 2 days of data to show the chart.' => '', - // '<15m' => '', - // '<30m' => '', - // 'Stop timer' => '', - // 'Start timer' => '', - // 'Add project member' => '', - // 'Enable notifications' => '', - // 'My activity stream' => '', - // 'My calendar' => '', - // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', - // 'Reset filters' => '', - // 'My tasks due tomorrow' => '', - // 'Tasks due today' => '', - // 'Tasks due tomorrow' => '', - // 'Tasks due yesterday' => '', - // 'Closed tasks' => '', - // 'Open tasks' => '', - // 'Not assigned' => '', - // 'View advanced search syntax' => '', - // 'Overview' => '', - // '%b %e %Y' => '', - // 'Board/Calendar/List view' => '', - // 'Switch to the board view' => '', - // 'Switch to the calendar view' => '', - // 'Switch to the list view' => '', - // 'Go to the search/filter box' => '', - // 'There is no activity yet.' => '', - // 'No tasks found.' => '', - // 'Keyboard shortcut: "%s"' => '', - // 'List' => '', - // 'Filter' => '', - // 'Advanced search' => '', - // 'Example of query: ' => '', - // 'Search by project: ' => '', - // 'Search by column: ' => '', - // 'Search by assignee: ' => '', - // 'Search by color: ' => '', - // 'Search by category: ' => '', - // 'Search by description: ' => '', - // 'Search by due date: ' => '', - // 'Lead and Cycle time for "%s"' => '', - // 'Average time spent into each column for "%s"' => '', - // 'Average time spent into each column' => '', - // 'Average time spent' => '', - // 'This chart show the average time spent into each column for the last %d tasks.' => '', - // 'Average Lead and Cycle time' => '', - // 'Average lead time: ' => '', - // 'Average cycle time: ' => '', - // 'Cycle Time' => '', - // 'Lead Time' => '', - // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', - // 'Average time into each column' => '', - // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', - // 'Lead time: ' => '', - // 'Cycle time: ' => '', - // 'Time spent into each column' => '', - // 'The lead time is the duration between the task creation and the completion.' => '', - // 'The cycle time is the duration between the start date and the completion.' => '', - // 'If the task is not closed the current time is used instead of the completion date.' => '', - // 'Set automatically the start date' => '', - // 'Edit Authentication' => '', - // 'Google Id' => '', - // 'Github Id' => '', - // 'Remote user' => '', - // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', - // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', - // 'New remote user' => '', - // 'New local user' => '', - // 'Default task color' => '', - // 'Hide sidebar' => '', - // 'Expand sidebar' => '', - // 'This feature does not work with all browsers.' => '', - // 'There is no destination project available.' => '', - // 'Trigger automatically subtask time tracking' => '', - // 'Include closed tasks in the cumulative flow diagram' => '', - // 'Current swimlane: %s' => '', - // 'Current column: %s' => '', - // 'Current category: %s' => '', - // 'no category' => '', - // 'Current assignee: %s' => '', - // 'not assigned' => '', - // 'Author:' => '', - // 'contributors' => '', - // 'License:' => '', - // 'License' => '', - // 'Project Administrator' => '', - // 'Enter the text below' => '', - // 'Gantt chart for %s' => '', - // 'Sort by position' => '', - // 'Sort by date' => '', - // 'Add task' => '', - // 'Start date:' => '', - // 'Due date:' => '', - // 'There is no start date or due date for this task.' => '', - // 'Moving or resizing a task will change the start and due date of the task.' => '', - // 'There is no task in your project.' => '', - // 'Gantt chart' => '', - // 'People who are project managers' => '', - // 'People who are project members' => '', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Proje kimliği, projeyi tanımlamak için kullanılan opsiyonel bir alfanumerik koddur.', + 'Identifier' => 'Kimlik', + 'Disable two factor authentication' => 'İki kademeli doğrulamayı iptal et', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Bu kullanıcı için iki kademeli doğrulamayı iptal etmek istediğinize emin misiniz: "%s"?', + 'Edit link' => 'Linki düzenle', + 'Start to type task title...' => 'Görev başlığını yazmaya başlayın...', + 'A task cannot be linked to itself' => 'Bir görevden kendine link atanamaz', + 'The exact same link already exists' => 'Birebir aynı link zaten var', + 'Recurrent task is scheduled to be generated' => 'Tekrarlanan görev oluşturulması zamanlandı', + 'Recurring information' => 'Tekrarlanan bilgi', + 'Score' => 'Skor', + 'The identifier must be unique' => 'Kimlik özgün olmalı', + 'This linked task id doesn\'t exists' => 'Link oluşturulan görec kodu mevcut değil', + 'This value must be alphanumeric' => 'Bu değer alfanumerik olmalı', + 'Edit recurrence' => 'Tekrarlanmayı düzenle', + 'Generate recurrent task' => 'Tekrarlanan görev oluştur', + 'Trigger to generate recurrent task' => 'Tekrarlanan görev oluşturmak için tetikleyici', + 'Factor to calculate new due date' => 'Yeni tamamlanma tarihi için hesaplama faktörü', + 'Timeframe to calculate new due date' => 'Yeni tamamlanma tarihinin hesaplanması için zaman dilimi', + 'Base date to calculate new due date' => 'Yeni tamamlanma tarihinin hesaplanması için baz alınacak tarih', + 'Action date' => 'Hareket tarihi', + 'Base date to calculate new due date: ' => 'Yeni tamamlanma tarihinin hesaplanması için baz alınacak tarih', + 'This task has created this child task: ' => 'Bu görev şu alt görevi oluşturdu:', + 'Day(s)' => 'Gün(ler)', + 'Existing due date' => 'Mevcut tamamlanma tarihi', + 'Factor to calculate new due date: ' => 'Yeni tamamlanma tarihi için hesaplama faktörü', + 'Month(s)' => 'Ay(lar)', + 'Recurrence' => 'Tekrar', + 'This task has been created by: ' => 'Bu görev şunun tarafından oluşturuldu:', + 'Recurrent task has been generated:' => 'Tekrarlanan görev oluşturuldu:', + 'Timeframe to calculate new due date: ' => 'Yeni tamamlanma tarihinin hesaplanması için zaman dilimi', + 'Trigger to generate recurrent task: ' => 'Tekrarlanan görev oluşturmak için tetikleyici', + 'When task is closed' => 'Görev kapatıldığı zaman', + 'When task is moved from first column' => 'Görev ilk sütundan taşındığı zaman', + 'When task is moved to last column' => 'Görev son sütuna taşındığı zaman', + 'Year(s)' => 'Yıl(lar)', + 'Calendar settings' => 'Takvim ayarları', + 'Project calendar view' => 'Proje takvim görünümü', + 'Project settings' => 'Proje ayarları', + 'Show subtasks based on the time tracking' => 'Zaman takibi bazında alt görevleri göster', + 'Show tasks based on the creation date' => 'Oluşturulma zamanına göre görevleri göster', + 'Show tasks based on the start date' => 'Başlangıç zamanına göre görevleri göster', + 'Subtasks time tracking' => 'Alt görevler zaman takibi', + 'User calendar view' => 'Kullanıcı takvim görünümü', + 'Automatically update the start date' => 'Başlangıç tarihini otomatik olarak güncelle', + 'iCal feed' => 'iCal akışı', + 'Preferences' => 'Ayarlar', + 'Security' => 'Güvenlik', + 'Two factor authentication disabled' => 'İki kademeli doğrulamayı devre dışı bırak', + 'Two factor authentication enabled' => 'İki kademeli doğrulamayı etkinleştir', + 'Unable to update this user.' => 'Bu kullanıcı güncellenemiyor', + 'There is no user management for private projects.' => 'Özel projeler için kullanıcı yönetimi yoktur.', + 'User that will receive the email' => 'Email alacak kullanıcı', + 'Email subject' => 'Email başlığı', + 'Date' => 'Tarih', + 'Add a comment log when moving the task between columns' => 'Görevi sütunlar arasında taşırken yorum kaydı ekle', + 'Move the task to another column when the category is changed' => 'Kategori değiştirildiğinde görevi başka sütuna taşı', + 'Send a task by email to someone' => 'Bir görevi email ile birine gönder', + 'Reopen a task' => 'Bir görevi tekrar aç', + 'Column change' => 'Sütun değişikliği', + 'Position change' => 'Konum değişikliği', + 'Swimlane change' => 'Kulvar değişikliği', + 'Assignee change' => 'Atanan değişikliği', + '[%s] Overdue tasks' => '[%s] Gecikmiş görevler', + 'Notification' => 'Uyarılar', + '%s moved the task #%d to the first swimlane' => '%s, #%d görevini birinci kulvara taşıdı', + '%s moved the task #%d to the swimlane "%s"' => '%s, #%d görevini "%s" kulvarına taşıdı', + 'Swimlane' => 'Kulvar', + 'Gravatar' => 'Gravatar', + '%s moved the task %s to the first swimlane' => '%s, %s görevini ilk kulvara taşıdı', + '%s moved the task %s to the swimlane "%s"' => '%s, %s görevini "%s" kulvarına taşıdı', + 'This report contains all subtasks information for the given date range.' => 'Bu rapor belirtilen tarih aralığında tüm alt görev bilgilerini içerir.', + 'This report contains all tasks information for the given date range.' => 'Bu rapor belirtilen tarih aralığında tüm görev bilgilerini içerir.', + 'Project activities for %s' => '%s için proje aktiviteleri', + 'view the board on Kanboard' => 'Tabloyu Kanboard\'da görüntüle', + 'The task have been moved to the first swimlane' => 'Görev birinci kulvara taşındı', + 'The task have been moved to another swimlane:' => 'Görev başka bir kulvara taşındı:', + 'Overdue tasks for the project "%s"' => '"%s" projesi için gecikmiş görevler', + 'New title: %s' => 'Yeni başlık: %s', + 'The task is not assigned anymore' => 'Görev artık atanmamış', + 'New assignee: %s' => 'Yeni atanan: %s', + 'There is no category now' => 'Şu anda kategori yok', + 'New category: %s' => 'Yeni kategori: %s', + 'New color: %s' => 'Yeni renk: %s', + 'New complexity: %d' => 'Yeni zorluk seviyesi: %d', + 'The due date have been removed' => 'Tamamlanma tarihi silindi', + 'There is no description anymore' => 'Artık açıklama yok', + 'Recurrence settings have been modified' => 'Tekrarlanma ayarları değiştirildi', + 'Time spent changed: %sh' => 'Harcanan zaman değiştirildi: %sh', + 'Time estimated changed: %sh' => 'Tahmini süre değiştirildi: %sh', + 'The field "%s" have been updated' => '"%s" hanesi değiştirildi', + 'The description have been modified' => 'Açıklama değiştirildi', + 'Do you really want to close the task "%s" as well as all subtasks?' => '"%s" görevini ve tüm alt görevlerini kapatmak istediğinize emin misiniz?', + 'Swimlane: %s' => 'Kulvar: %s', + 'I want to receive notifications for:' => 'Bununla ilgili bildirimler almak istiyorum:', + 'All tasks' => 'Tüm görevler', + 'Only for tasks assigned to me' => 'Yalnızca bana atanmış görevler için', + 'Only for tasks created by me' => 'Yalnızca benim oluşturduğum görevler için', + 'Only for tasks created by me and assigned to me' => 'Yalnızca benim oluşturduğum ve bana atanmış görevler için', + '%A' => '%A', + '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p', + 'New due date: %B %e, %Y' => 'Yeni tamamlanma tarihi: %B %e, %Y', + 'Start date changed: %B %e, %Y' => 'Başlangıç tarihi değiştirildi: %B %e, %Y', + '%k:%M %p' => '%k:%M %p', + '%%Y-%%m-%%d' => '%%Y-%%m-%%d', + 'Total for all columns' => 'Tüm sütunlar için toplam', + 'You need at least 2 days of data to show the chart.' => 'Grafiği göstermek için en az iki günlük veriye ihtiyaç var.', + '<15m' => '<15dk', + '<30m' => '<30dk', + 'Stop timer' => 'Zamanlayıcıyı durdur', + 'Start timer' => 'Zamanlayıcıyı başlat', + 'Add project member' => 'Proje üyesi ekle', + 'Enable notifications' => 'Bildirimleri etkinleştir', + 'My activity stream' => 'Olay akışım', + 'My calendar' => 'Takvimim', + 'Search tasks' => 'Görevleri ara', + 'Back to the calendar' => 'Takvime geri dön', + 'Filters' => 'Filtreler', + 'Reset filters' => 'Filtreleri sıfırla', + 'My tasks due tomorrow' => 'Yarına tamamlanması gereken görevlerim', + 'Tasks due today' => 'Bugün tamamlanması gereken görevler', + 'Tasks due tomorrow' => 'Yarına tamamlanması gereken görevler', + 'Tasks due yesterday' => 'Dün tamamlanmış olması gereken görevler', + 'Closed tasks' => 'Kapatılmış görevler', + 'Open tasks' => 'Açık görevler', + 'Not assigned' => 'Atanmamış', + 'View advanced search syntax' => 'Gelişmiş arama kodlarını göster', + 'Overview' => 'Genel bakış', + '%b %e %Y' => '%b %e %Y', + 'Board/Calendar/List view' => 'Tablo/Takvim/Liste görünümü', + 'Switch to the board view' => 'Tablo görünümüne geç', + 'Switch to the calendar view' => 'Takvim görünümüne geç', + 'Switch to the list view' => 'Liste görünümüne geç', + 'Go to the search/filter box' => 'Arama/Filtreleme kutusuna git', + 'There is no activity yet.' => 'Henüz bir aktivite yok.', + 'No tasks found.' => 'Hiç görev bulunamadı.', + 'Keyboard shortcut: "%s"' => 'Klavye kısayolu: "%s"', + 'List' => 'Liste', + 'Filter' => 'Filtre', + 'Advanced search' => 'Gelişmiş arama', + 'Example of query: ' => 'Sorgu örneği', + 'Search by project: ' => 'Projeye göre ara', + 'Search by column: ' => 'Sütuna göre ara', + 'Search by assignee: ' => 'Atanana göre ara', + 'Search by color: ' => 'Renge göre ara', + 'Search by category: ' => 'Kategoriye göre ara', + 'Search by description: ' => 'Açıklamaya göre ara', + 'Search by due date: ' => 'Tamamlanma tarihine göre ara', + 'Lead and Cycle time for "%s"' => '"%s" için teslim ve çevrim süresi', + 'Average time spent into each column for "%s"' => '"%s" için her bir sütunda geçirilen ortalama zaman', + 'Average time spent into each column' => 'Her bir sütunda geçirilen ortalama zaman', + 'Average time spent' => 'Harcanan ortalama zaman', + 'This chart show the average time spent into each column for the last %d tasks.' => 'Bu grafik son %d görev için her bir sütunda geçirilen ortalama zamanı gösterir.', + 'Average Lead and Cycle time' => 'Ortalama teslim ve çevrim süresi', + 'Average lead time: ' => 'Ortalama teslim süresi', + 'Average cycle time: ' => 'Ortalama çevrim süresi', + 'Cycle Time' => 'Çevrim süresi', + 'Lead Time' => 'Teslim süresi', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Bu grafik son %d görev için zaman içinde gerçekleşen ortalama teslim ve çevrim sürelerini gösterir.', + 'Average time into each column' => 'Her bir sütunda ortalama zaman', + 'Lead and cycle time' => 'Teslim ve çevrim süresi', + 'Google Authentication' => 'Google doğrulaması', + 'Help on Google authentication' => 'Google doğrulaması hakkında yardım', + 'Github Authentication' => 'Github doğrulaması', + 'Help on Github authentication' => 'Github doğrulaması hakkında yardım', + 'Lead time: ' => 'Teslim süresi: ', + 'Cycle time: ' => 'Çevrim süresi: ', + 'Time spent into each column' => 'Her sütunda harcanan zaman', + 'The lead time is the duration between the task creation and the completion.' => 'Teslim süresi, görevin oluşturulması ile tamamlanması arasında geçen süredir.', + 'The cycle time is the duration between the start date and the completion.' => 'Çevrim süresi, görevin başlangıç tarihi ile tamamlanması arasında geçen süredir.', + 'If the task is not closed the current time is used instead of the completion date.' => 'Eğer görev henüz kapatılmamışsa, tamamlanma tarihi yerine şu anki tarih kullanılır.', + 'Set automatically the start date' => 'Başlangıç tarihini otomatik olarak belirle', + 'Edit Authentication' => 'Doğrulamayı düzenle', + 'Google Id' => 'Google kimliği', + 'Github Id' => 'Github Kimliği', + 'Remote user' => 'Uzak kullanıcı', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Uzak kullanıcıların şifreleri Kanboard veritabanında saklanmaz, örnek: LDAP, Google ve Github hesapları', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Eğer giriş formuna erişimi engelleyi seçerseniz, giriş formuna girilen bilgiler gözardı edilir.', + 'New remote user' => 'Yeni uzak kullanıcı', + 'New local user' => 'Yeni yerel kullanıcı', + 'Default task color' => 'Varsayılan görev rengi', + 'Hide sidebar' => 'Yan menüyü gizle', + 'Expand sidebar' => 'Yan menüyü genişlet', + 'This feature does not work with all browsers.' => 'Bu özellik tüm tarayıcılarla çalışmaz', + 'There is no destination project available.' => 'Seçilebilecek hedef proje yok.', + 'Trigger automatically subtask time tracking' => 'Altgörev zamanlayıcıyı otomatik olarak başlat', + 'Include closed tasks in the cumulative flow diagram' => 'Kümülatif akış diyagramında kapatılmış görevleri de dahil et', + 'Current swimlane: %s' => 'Geçerli kulvar: %s', + 'Current column: %s' => 'Geçerli sütun: %s', + 'Current category: %s' => 'Geçerli kategori: %s', + 'no category' => 'kategori yok', + 'Current assignee: %s' => 'Geçerli atanan: %s', + 'not assigned' => 'atanmamış', + 'Author:' => 'Yazar', + 'contributors' => 'Katkı sağlayanlar', + 'License:' => 'Lisans:', + 'License' => 'Lisans', + 'Enter the text below' => 'Aşağıdaki metni girin', + 'Gantt chart for %s' => '%s için Gantt diyagramı', + 'Sort by position' => 'Pozisyona göre sırala', + 'Sort by date' => 'Tarihe göre sırala', + 'Add task' => 'Görev ekle', + 'Start date:' => 'Başlangıç tarihi:', + 'Due date:' => 'Tamamlanması gereken tarih:', + 'There is no start date or due date for this task.' => 'Bu görev için başlangıç veya tamamlanması gereken tarih yok.', + 'Moving or resizing a task will change the start and due date of the task.' => 'Bir görevin boyutunu değiştirmek, görevin başlangıç ve tamamlanması gereken tarihlerini değiştirir.', + 'There is no task in your project.' => 'Projenizde hiç görev yok.', + 'Gantt chart' => 'Gantt diyagramı', + 'People who are project managers' => 'Proje yöneticisi olan kişiler', + 'People who are project members' => 'Proje üyesi olan kişiler', // 'NOK - Norwegian Krone' => '', - // 'Show this column' => '', - // 'Hide this column' => '', - // 'open file' => '', - // 'End date' => '', - // 'Users overview' => '', - // 'Managers' => '', - // 'Members' => '', - // 'Shared project' => '', - // 'Project managers' => '', - // 'Project members' => '', - // 'Gantt chart for all projects' => '', - // 'Projects list' => '', - // 'Gantt chart for this project' => '', - // 'Project board' => '', - // 'End date:' => '', - // 'There is no start date or end date for this project.' => '', - // 'Projects Gantt chart' => '', - // 'Start date: %s' => '', - // 'End date: %s' => '', - // 'Link type' => '', - // 'Change task color when using a specific task link' => '', - // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', - // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', - // 'Documentation: %s' => '', - // 'Switch to the Gantt chart view' => '', - // 'Reset the search/filter box' => '', - // 'Documentation' => '', - // 'Table of contents' => '', - // 'Gantt' => '', - // 'Help with project permissions' => '', - // 'Author' => '', - // 'Version' => '', - // 'Plugins' => '', - // 'There is no plugin loaded.' => '', - // 'Set maximum column height' => '', - // 'Remove maximum column height' => '', - // 'My notifications' => '', - // 'Custom filters' => '', - // 'Your custom filter have been created successfully.' => '', - // 'Unable to create your custom filter.' => '', - // 'Custom filter removed successfully.' => '', - // 'Unable to remove this custom filter.' => '', - // 'Edit custom filter' => '', - // 'Your custom filter have been updated successfully.' => '', - // 'Unable to update custom filter.' => '', - // 'Web' => '', - // 'New attachment on task #%d: %s' => '', - // 'New comment on task #%d' => '', - // 'Comment updated on task #%d' => '', - // 'New subtask on task #%d' => '', - // 'Subtask updated on task #%d' => '', - // 'New task #%d: %s' => '', - // 'Task updated #%d' => '', - // 'Task #%d closed' => '', - // 'Task #%d opened' => '', - // 'Column changed for task #%d' => '', - // 'New position for task #%d' => '', - // 'Swimlane changed for task #%d' => '', - // 'Assignee changed on task #%d' => '', - // '%d overdue tasks' => '', - // 'Task #%d is overdue' => '', - // 'No new notifications.' => '', - // 'Mark all as read' => '', - // 'Mark as read' => '', - // 'Total number of tasks in this column across all swimlanes' => '', - // 'Collapse swimlane' => '', - // 'Expand swimlane' => '', - // 'Add a new filter' => '', - // 'Share with all project members' => '', - // 'Shared' => '', - // 'Owner' => '', - // 'Unread notifications' => '', - // 'My filters' => '', - // 'Notification methods:' => '', - // 'Import tasks from CSV file' => '', - // 'Unable to read your file' => '', - // '%d task(s) have been imported successfully.' => '', - // 'Nothing have been imported!' => '', - // 'Import users from CSV file' => '', - // '%d user(s) have been imported successfully.' => '', - // 'Comma' => '', - // 'Semi-colon' => '', - // 'Tab' => '', - // 'Vertical bar' => '', - // 'Double Quote' => '', - // 'Single Quote' => '', - // '%s attached a file to the task #%d' => '', - // 'There is no column or swimlane activated in your project!' => '', - // 'Append filter (instead of replacement)' => '', - // 'Append/Replace' => '', - // 'Append' => '', - // 'Replace' => '', - // 'There is no notification method registered.' => '', - // 'Import' => '', - // 'change sorting' => '', - // 'Tasks Importation' => '', - // 'Delimiter' => '', - // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', - // 'No external integration registered.' => '', - // 'Duplicates are not imported' => '', - // 'Usernames must be lowercase and unique' => '', - // 'Passwords will be encrypted if present' => '', - // '%s attached a new file to the task %s' => '', - // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', + 'Show this column' => 'Bu sütunu göster', + 'Hide this column' => 'Bu sütunu gizle', + 'open file' => 'dosyayı aç', + 'End date' => 'Bitiş tarihi', + 'Users overview' => 'Kullanıcılara genel bakış', + 'Managers' => 'Yöneticiler', + 'Members' => 'Üyeler', + 'Shared project' => 'Paylaşılan proje', + 'Project managers' => 'Proje yöneticileri', + 'Gantt chart for all projects' => 'Tüm projeler için Gantt diyagramı', + 'Projects list' => 'Proje listesi', + 'Gantt chart for this project' => 'Bu proje için Gantt diyagramı', + 'Project board' => 'Proje tablosu', + 'End date:' => 'Bitiş tarihi:', + 'There is no start date or end date for this project.' => 'Bu proje için başlangıç veya bitiş tarihi yok.', + 'Projects Gantt chart' => 'Projeler Gantt diyagramı', + 'Start date: %s' => 'Başlangıç tarihi: %s', + 'End date: %s' => 'Bitiş tarihi: %s', + 'Link type' => 'Bağlantı türü', + 'Change task color when using a specific task link' => 'Belirli bir görev bağlantısı kullanıldığında görevin rengini değiştir', + 'Task link creation or modification' => 'Görev bağlantısı oluşturulması veya değiştirilmesi', + 'Login with my Gitlab Account' => 'Gitlab hesabımla giriş yap', + 'Milestone' => 'Kilometre taşı', + 'Gitlab Authentication' => 'Gitlab doğrulaması', + 'Help on Gitlab authentication' => 'Gitlab doğrulaması hakkında yardım', + 'Gitlab Id' => 'Gitlab kimliği', + 'Gitlab Account' => 'Gitlab hesabı', + 'Link my Gitlab Account' => 'Gitlab hesabını ilişkilendir', + 'Unlink my Gitlab Account' => 'Gitlab hesabımla bağlantıyı kopar', + 'Documentation: %s' => 'Dokümantasyon: %s', + 'Switch to the Gantt chart view' => 'Gantt diyagramı görünümüne geç', + 'Reset the search/filter box' => 'Arama/Filtre kutusunu sıfırla', + 'Documentation' => 'Dokümantasyon', + 'Table of contents' => 'İçindekiler', + 'Gantt' => 'Gantt', + 'Author' => 'Yazar', + 'Version' => 'Versiyon', + 'Plugins' => 'Eklentiler', + 'There is no plugin loaded.' => 'Yüklenmiş bir eklendi yok', + 'Set maximum column height' => 'Maksimum sütun yüksekliğini belirle', + 'Remove maximum column height' => 'Maksimum sütun yüksekliğini iptal et', + 'My notifications' => 'Bildirimlerim', + 'Custom filters' => 'Özel filtreler', + 'Your custom filter have been created successfully.' => 'Özel filtreleriniz başarıyla oluşturuldu.', + 'Unable to create your custom filter.' => 'Özel filtreniz oluşturulamadı.', + 'Custom filter removed successfully.' => 'Özel filtreniz başarıyla silindi.', + 'Unable to remove this custom filter.' => 'Bu özel filtre silinemiyor.', + 'Edit custom filter' => 'Özel filtreyi düzenle', + 'Your custom filter have been updated successfully.' => 'Özel filtreleriniz başarıyla güncellendi.', + 'Unable to update custom filter.' => 'Özel filtre güncellenemiyor.', + 'Web' => 'İnternet', + 'New attachment on task #%d: %s' => '#%d görevinde yeni dosya: %s', + 'New comment on task #%d' => '#%d görevinde yeni yorum', + 'Comment updated on task #%d' => '#%d görevinde yorum güncellendi', + 'New subtask on task #%d' => '#%d görevinde yeni alt görev', + 'Subtask updated on task #%d' => '#%d görevinde alt görev güncellendi', + 'New task #%d: %s' => 'Yeni #%d görevi: %s', + 'Task updated #%d' => '#%d görevi güncellendi', + 'Task #%d closed' => '#%d görevi kapatıldı', + 'Task #%d opened' => '#%d görevi oluşturuldu', + 'Column changed for task #%d' => '#%d görevinin sütunu değişti', + 'New position for task #%d' => '#%d görevinin konumu değişti', + 'Swimlane changed for task #%d' => '#%d görevinin kulvarı değişti', + 'Assignee changed on task #%d' => '#%d görevine atanan değişti', + '%d overdue tasks' => '%d gecikmiş görev', + 'Task #%d is overdue' => '#%d görevi gecikti', + 'No new notifications.' => 'Yeni bildirim yok.', + 'Mark all as read' => 'Tümünü okunmuş olarak işaretle', + 'Mark as read' => 'Okunmuş olarak işaretle', + 'Total number of tasks in this column across all swimlanes' => 'Bu sutündaki görev sayısının tüm kulvarlardaki toplamı', + 'Collapse swimlane' => 'Kulvarı daralt', + 'Expand swimlane' => 'Kulvarı genişlet', + 'Add a new filter' => 'Yeni bir filtre ekle', + 'Share with all project members' => 'Tüm proje üyeleriyle paylaş', + 'Shared' => 'Paylaşılan', + 'Owner' => 'Sahibi', + 'Unread notifications' => 'Okunmamış bildirimler', + 'My filters' => 'Filtrelerim', + 'Notification methods:' => 'Bildirim yöntemleri:', + 'Import tasks from CSV file' => 'CSV dosyasından görevleri içeri aktar', + 'Unable to read your file' => 'Dosya okunamıyor', + '%d task(s) have been imported successfully.' => '%d görev başarıyla içeri aktarıldı.', + 'Nothing have been imported!' => 'Hiçbir şey içeri aktarılamadı!', + 'Import users from CSV file' => 'CSV dosyasından kullanıcıları içeri aktar', + '%d user(s) have been imported successfully.' => '%d kullanıcı başarıyla içeri aktarıldı.', + 'Comma' => 'Virgül', + 'Semi-colon' => 'Noktalı virgül', + 'Tab' => 'Tab', + 'Vertical bar' => 'Dikey çizgi', + 'Double Quote' => 'Tırnak işareti', + 'Single Quote' => 'Kesme işareti', + '%s attached a file to the task #%d' => '%s, #%d görevine bir dosya ekledi.', + 'There is no column or swimlane activated in your project!' => 'Projenizde etkinleştirilmiş hiç bir sütun veya kulvar yok!', + 'Append filter (instead of replacement)' => 'Fıltreye ekle (üzerine yazmak yerine)', + 'Append/Replace' => 'Ekle/Üzerine yaz', + 'Append' => 'Ekle', + 'Replace' => 'Üzerine yaz', + 'Import' => 'İçeri aktar', + 'change sorting' => 'sıralamayı değiştir', + 'Tasks Importation' => 'Görevleri içeri aktar', + 'Delimiter' => 'Ayırıcı', + 'Enclosure' => 'Enclosure', + 'CSV File' => 'CSV Dosyası', + 'Instructions' => 'Yönergeler', + 'Your file must use the predefined CSV format' => 'Dosyanız önceden belirlenmiş CSV formatını kullanmalı', + 'Your file must be encoded in UTF-8' => 'Dosyanız UTF-8 kodlamasında olmalı', + 'The first row must be the header' => 'İlk satır başlık olmalı', + 'Duplicates are not verified for you' => 'Çift girişler sizin için onaylanmamış', + 'The due date must use the ISO format: YYYY-MM-DD' => 'Tamamlanma tarihi ISO formatını kullanmalı: YYYY-MM-DD', + 'Download CSV template' => 'CSV taslağını indir', + 'No external integration registered.' => 'Hiç dış entegrasyon kaydedilmemiş.', + 'Duplicates are not imported' => 'Çift girişler içeri aktarılmaz', + 'Usernames must be lowercase and unique' => 'Kullanıcı adları küçük harf ve tekil olmalı', + 'Passwords will be encrypted if present' => 'Şifreler (eğer varsa) kriptolanır', + '%s attached a new file to the task %s' => '%s, %s görevine yeni dosya ekledi', + 'Assign automatically a category based on a link' => 'Bir bağlantıya göre otomatik olarak kategori ata', + 'BAM - Konvertible Mark' => 'BAM - Konvertible Mark', + 'Assignee Username' => 'Atanan kullanıcı adı', + 'Assignee Name' => 'Atanan İsmi', + 'Groups' => 'Gruplar', + 'Members of %s' => '%s in üyeleri', + 'New group' => 'Yeni grup', + 'Group created successfully.' => 'Grup başarıyla oluşturuldu.', + 'Unable to create your group.' => 'Grup oluşturulamadı.', + 'Edit group' => 'Grubu düzenle', + 'Group updated successfully.' => 'Grup başarıyla güncellendi.', + 'Unable to update your group.' => 'Grup güncellenemedi.', + 'Add group member to "%s"' => '"%s" e grup üyesi ekle', + 'Group member added successfully.' => 'Grup üyesi başarıyla eklendi.', + 'Unable to add group member.' => 'Grup üyesi eklenemedi.', + 'Remove user from group "%s"' => '"%s" grubundan kullanıcı çıkar', + 'User removed successfully from this group.' => 'Kullanıcı bu gruptan başarıyla çıkarıldı.', + 'Unable to remove this user from the group.' => 'Bu kullanıcı bu grubtan çıkarılamadı', + 'Remove group' => 'Grubu sil', + 'Group removed successfully.' => 'Grup başarıyla silindi.', + 'Unable to remove this group.' => 'Grup silinemedi.', + 'Project Permissions' => 'Proje izimleri', + 'Manager' => 'Yönetici', + 'Project Manager' => 'Proje yöneticisi', + 'Project Member' => 'Proje üyesi', + 'Project Viewer' => 'Proje izleyicisi', + 'Your account is locked for %d minutes' => 'Hesabınız %d dakika boyunca kilitlendi', + 'Invalid captcha' => 'Geçersiz captcha', + 'The name must be unique' => 'İsim tekil olmalı', + 'View all groups' => 'Tüm grupları görüntüle', + 'View group members' => 'Grup üyelerini görüntüle', + 'There is no user available.' => 'Uygun üye yok', + 'Do you really want to remove the user "%s" from the group "%s"?' => '"%s" kullanıcısını "%s" grubundan çıkarmak istediğinize emin misiniz?', + 'There is no group.' => 'Hiç grup yok.', + 'External Id' => 'Harici Kimlik', + 'Add group member' => 'Grup üyesi ekle', + 'Do you really want to remove this group: "%s"?' => '"%s" grubunu silmek istediğinize emin misiniz?', + 'There is no user in this group.' => 'Bu grupta hiç kullanıcı yok.', + 'Remove this user' => 'Bu kullanıcıyı sil', + 'Permissions' => 'İzinler', + 'Allowed Users' => 'İzin verilen kullanıcı', + 'No user have been allowed specifically.' => 'Hiç bir kullanıcıya özel olarak izin verilmemiş.', + 'Role' => 'Rol', + 'Enter user name...' => 'Kullanıcı adını girin...', + 'Allowed Groups' => 'İzinli gruplar', + 'No group have been allowed specifically.' => 'Hiç bir gruba özel olarak izin verilmemiş', + 'Group' => 'Grup', + 'Group Name' => 'Grup adı', + 'Enter group name...' => 'Grup adını girin...', + 'Role:' => 'Rol:', + 'Project members' => 'Proje üyeleri', + 'Compare hours for "%s"' => '"%s" için saatleri karşılaştır', + '%s mentioned you in the task #%d' => '%s sizden #%d görevinde bahsetti', + '%s mentioned you in a comment on the task #%d' => '%s sizden #%d görevindeki bir yorumda bahsetti', + 'You were mentioned in the task #%d' => '#%d görevinde sizden bahsedildi', + 'You were mentioned in a comment on the task #%d' => '#%d görevindeki bir yorumda sizden bahsedildi', + 'Mentioned' => 'Bahsedilmiş', + 'Compare Estimated Time vs Actual Time' => 'Tahmini süre ile gerçekleşen süreyi karşılaştır', + 'Estimated hours: ' => 'Tahmini saat:', + 'Actual hours: ' => 'Gerçekleşen saat:', + 'Hours Spent' => 'Harcanan saat', + 'Hours Estimated' => 'Tahmini saat', + 'Estimated Time' => 'Tahmini süre', + 'Actual Time' => 'Gerçekleşen süre', + 'Estimated vs actual time' => 'Tahmini vs gerçekleşen süre', + // 'RUB - Russian Ruble' => '', + 'Assign the task to the person who does the action when the column is changed' => 'Sütun değiştirildiği zaman görevi eylemi gerçekleştiren kişiye ata', + 'Close a task in a specific column' => 'Belirli bir sütundaki görevi kapat', + 'Time-based One-time Password Algorithm' => 'Zamana bağlı tek kullanımlık şifre algoritması', + 'Two-Factor Provider: ' => 'Çift kademeli doğrulama sağlayıcısı', + 'Disable two-factor authentication' => 'Çift kademeli doğrulamayı devre dışı bırak', + 'Enable two-factor authentication' => 'Çift kademeli doğrulamayı etkinleştir', + 'There is no integration registered at the moment.' => 'Şu anda kayıtlı bir entegrasyon bulunmuyor.', + 'Password Reset for Kanboard' => 'Kanboard için şifre sıfırlama', + 'Forgot password?' => 'Şifrenizi mi unuttunuz?', + 'Enable "Forget Password"' => '"Şifremi unuttum" komutunu etkinleştir', + 'Password Reset' => 'Şifre sıfırlama', + 'New password' => 'Yeni şifre', + 'Change Password' => 'Şifreyi değiştir', + 'To reset your password click on this link:' => 'Şifrenizi sıfırlamak için bu linke tıklayın:', + 'Last Password Reset' => 'Son şifre sıfırlama', + 'The password has never been reinitialized.' => 'Şifre hiç bir zaman tekrar başlatılmamış.', + 'Creation' => 'Oluşturulma', + 'Expiration' => 'Sona erme', + 'Password reset history' => 'Şifre sıfırlama geçmişi', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '"%s" sütunu ve "%s" kulvarındaki tüm görevler başarıyla kapatıldı.', + 'Do you really want to close all tasks of this column?' => 'Bu sütundaki tüm görevleri kapatmak istediğinize emin misiniz?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '"%s" sütunu ve "%s" kulvarındaki %d görev kapatılacak.', + 'Close all tasks of this column' => 'Bu sütundaki tüm görevleri kapat', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Locale/zh_CN/translations.php b/sources/app/Locale/zh_CN/translations.php index 7642e01..c591fa6 100644 --- a/sources/app/Locale/zh_CN/translations.php +++ b/sources/app/Locale/zh_CN/translations.php @@ -174,13 +174,7 @@ return array( 'Complexity' => '复杂度', 'Task limit' => '任务限制', 'Task count' => '任务数', - 'Edit project access list' => '编辑项目存取列表', - 'Allow this user' => '允许该用户', - 'Don\'t forget that administrators have access to everything.' => '别忘了管理员有一切的权限。', - 'Revoke' => '撤销', - 'List of authorized users' => '已授权的用户列表', 'User' => '用户', - 'Nobody have access to this project.' => '无用户可以访问此项目.', 'Comments' => '评论', 'Write your text in Markdown' => '用Markdown格式编写', 'Leave a comment' => '留言', @@ -396,8 +390,6 @@ return array( 'Email:' => '电子邮件:', 'Notifications:' => '通知:', 'Notifications' => '通知', - 'Group:' => '群组:', - 'Regular user' => '常规用户', 'Account type:' => '账户类型:', 'Edit profile' => '编辑属性', 'Change password' => '修改密码', @@ -445,12 +437,6 @@ return array( '%s changed the assignee of the task %s to %s' => '%s 将任务 %s 分配给 %s', 'New password for the user "%s"' => '用户"%s"的新密码', 'Choose an event' => '选择一个事件', - 'Github commit received' => '收到了Github提交', - 'Github issue opened' => '开启了Github问题报告', - 'Github issue closed' => '关闭了Github问题报告', - 'Github issue reopened' => '重新开启Github问题', - 'Github issue assignee change' => 'Github问题负责人已经变更', - 'Github issue label change' => 'Github任务标签修改', 'Create a task from an external provider' => '从外部创建任务', 'Change the assignee based on an external username' => '根据外部用户名修改任务分配', 'Change the category based on an external label' => '根据外部标签修改分类', @@ -495,10 +481,7 @@ return array( 'Everybody have access to this project.' => '所有人都可以访问此项目', 'Webhooks' => '网络钩子', 'API' => '应用程序接口', - 'Github webhooks' => 'Github 网络钩子', - 'Help on Github webhooks' => 'Github 网络钩子帮助', 'Create a comment from an external provider' => '从外部创建一个评论', - 'Github issue comment created' => '已经创建了Github问题评论', 'Project management' => '项目管理', 'My projects' => '我的项目', 'Columns' => '栏目', @@ -516,7 +499,6 @@ return array( 'User repartition for "%s"' => '"%s"的用户分析', 'Clone this project' => '复制此项目', 'Column removed successfully.' => '成功删除了栏目。', - 'Github Issue' => 'Github 任务报告', 'Not enough data to show the graph.' => '数据不足,无法绘图。', 'Previous' => '后退', 'The id must be an integer' => '编号必须为整数', @@ -546,8 +528,6 @@ return array( 'Default swimlane' => '默认泳道', 'Do you really want to remove this swimlane: "%s"?' => '确定要删除泳道:"%s"?', 'Inactive swimlanes' => '非活动泳道', - 'Set project manager' => '设为项目经理', - 'Set project member' => '设为项目成员', 'Remove a swimlane' => '删除泳道', 'Rename' => '重命名', 'Show default swimlane' => '显示默认泳道', @@ -563,18 +543,8 @@ return array( 'Your swimlane have been created successfully.' => '已经成功创建泳道。', 'Example: "Bug, Feature Request, Improvement"' => '示例:“缺陷,功能需求,提升', 'Default categories for new projects (Comma-separated)' => '新项目的默认分类(用逗号分隔)', - 'Gitlab commit received' => '收到 Gitlab 提交', - 'Gitlab issue opened' => '开启 Gitlab 问题', - 'Gitlab issue closed' => '关闭 Gitlab 问题', - 'Gitlab webhooks' => 'Gitlab 网络钩子', - 'Help on Gitlab webhooks' => 'Gitlab 网络钩子帮助', 'Integrations' => '整合', 'Integration with third-party services' => '与第三方服务进行整合', - 'Role for this project' => '项目角色', - 'Project manager' => '项目管理员', - 'Project member' => '项目成员', - 'A project manager can change the settings of the project and have more privileges than a standard user.' => '项目经理可以修改项目的设置,比标准用户多了一些权限', - 'Gitlab Issue' => 'Gitlab 问题', 'Subtask Id' => '子任务 Id', 'Subtasks' => '子任务', 'Subtasks Export' => '子任务导出', @@ -602,9 +572,6 @@ return array( 'You already have one subtask in progress' => '你已经有了一个进行中的子任务', 'Which parts of the project do you want to duplicate?' => '要复制项目的哪些内容?', // 'Disallow login form' => '', - 'Bitbucket commit received' => '收到Bitbucket提交', - 'Bitbucket webhooks' => 'Bitbucket网络钩子', - 'Help on Bitbucket webhooks' => 'Bitbucket网络钩子帮助', 'Start' => '开始', 'End' => '结束', 'Task age in days' => '任务存在天数', @@ -702,9 +669,7 @@ return array( 'The two factor authentication code is valid.' => '双重认证码正确。', 'Code' => '认证码', 'Two factor authentication' => '双重认证', - 'Enable/disable two factor authentication' => '启用/禁用双重认证', 'This QR code contains the key URI: ' => '此二维码包含密码 URI:', - 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '将密码保存到 TOTP 软件(例如Google 认证或 FreeOTP)', 'Check my code' => '检查我的认证码', 'Secret key: ' => '密码:', 'Test your device' => '测试设备', @@ -776,21 +741,10 @@ return array( // 'User that will receive the email' => '', 'Email subject' => '邮件主题', 'Date' => '日期', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', // 'Add a comment log when moving the task between columns' => '', // 'Move the task to another column when the category is changed' => '', // 'Send a task by email to someone' => '', 'Reopen a task' => '重新开始一个任务', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', // 'Column change' => '', // 'Position change' => '', // 'Swimlane change' => '', @@ -910,8 +864,6 @@ return array( // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'By @%s on Gitlab' => '', - // 'Gitlab issue comment created' => '', // 'New remote user' => '', // 'New local user' => '', 'Default task color' => '默认任务颜色', @@ -931,7 +883,6 @@ return array( 'contributors' => '贡献者', 'License:' => '授权许可:', 'License' => '授权许可', - 'Project Administrator' => '项目管理者', 'Enter the text below' => '输入下方的文本', 'Gantt chart for %s' => '%s的甘特图', 'Sort by position' => '按位置排序', @@ -955,7 +906,6 @@ return array( 'Members' => '成员', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +932,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1045,7 +994,6 @@ return array( // 'Append/Replace' => '', // 'Append' => '', // 'Replace' => '', - // 'There is no notification method registered.' => '', // 'Import' => '', // 'change sorting' => '', // 'Tasks Importation' => '', @@ -1065,7 +1013,95 @@ return array( // 'Passwords will be encrypted if present' => '', // '%s attached a new file to the task %s' => '', // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertibile Mark' => '', + // 'BAM - Konvertible Mark' => '', // 'Assignee Username' => '', // 'Assignee Name' => '', + // 'Groups' => '', + // 'Members of %s' => '', + // 'New group' => '', + // 'Group created successfully.' => '', + // 'Unable to create your group.' => '', + // 'Edit group' => '', + // 'Group updated successfully.' => '', + // 'Unable to update your group.' => '', + // 'Add group member to "%s"' => '', + // 'Group member added successfully.' => '', + // 'Unable to add group member.' => '', + // 'Remove user from group "%s"' => '', + // 'User removed successfully from this group.' => '', + // 'Unable to remove this user from the group.' => '', + // 'Remove group' => '', + // 'Group removed successfully.' => '', + // 'Unable to remove this group.' => '', + // 'Project Permissions' => '', + // 'Manager' => '', + // 'Project Manager' => '', + // 'Project Member' => '', + // 'Project Viewer' => '', + // 'Your account is locked for %d minutes' => '', + // 'Invalid captcha' => '', + // 'The name must be unique' => '', + // 'View all groups' => '', + // 'View group members' => '', + // 'There is no user available.' => '', + // 'Do you really want to remove the user "%s" from the group "%s"?' => '', + // 'There is no group.' => '', + // 'External Id' => '', + // 'Add group member' => '', + // 'Do you really want to remove this group: "%s"?' => '', + // 'There is no user in this group.' => '', + // 'Remove this user' => '', + // 'Permissions' => '', + // 'Allowed Users' => '', + // 'No user have been allowed specifically.' => '', + // 'Role' => '', + // 'Enter user name...' => '', + // 'Allowed Groups' => '', + // 'No group have been allowed specifically.' => '', + // 'Group' => '', + // 'Group Name' => '', + // 'Enter group name...' => '', + // 'Role:' => '', + // 'Project members' => '', + // 'Compare hours for "%s"' => '', + // '%s mentioned you in the task #%d' => '', + // '%s mentioned you in a comment on the task #%d' => '', + // 'You were mentioned in the task #%d' => '', + // 'You were mentioned in a comment on the task #%d' => '', + // 'Mentioned' => '', + // 'Compare Estimated Time vs Actual Time' => '', + // 'Estimated hours: ' => '', + // 'Actual hours: ' => '', + // 'Hours Spent' => '', + // 'Hours Estimated' => '', + // 'Estimated Time' => '', + // 'Actual Time' => '', + // 'Estimated vs actual time' => '', + // 'RUB - Russian Ruble' => '', + // 'Assign the task to the person who does the action when the column is changed' => '', + // 'Close a task in a specific column' => '', + // 'Time-based One-time Password Algorithm' => '', + // 'Two-Factor Provider: ' => '', + // 'Disable two-factor authentication' => '', + // 'Enable two-factor authentication' => '', + // 'There is no integration registered at the moment.' => '', + // 'Password Reset for Kanboard' => '', + // 'Forgot password?' => '', + // 'Enable "Forget Password"' => '', + // 'Password Reset' => '', + // 'New password' => '', + // 'Change Password' => '', + // 'To reset your password click on this link:' => '', + // 'Last Password Reset' => '', + // 'The password has never been reinitialized.' => '', + // 'Creation' => '', + // 'Expiration' => '', + // 'Password reset history' => '', + // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', + // 'Do you really want to close all tasks of this column?' => '', + // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + // 'Close all tasks of this column' => '', + // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + // 'My dashboard' => '', + // 'My profile' => '', ); diff --git a/sources/app/Model/Acl.php b/sources/app/Model/Acl.php deleted file mode 100644 index 62f850c..0000000 --- a/sources/app/Model/Acl.php +++ /dev/null @@ -1,289 +0,0 @@ - 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/sources/app/Model/Action.php b/sources/app/Model/Action.php index dbf17e4..4da2fb8 100644 --- a/sources/app/Model/Action.php +++ b/sources/app/Model/Action.php @@ -2,14 +2,8 @@ namespace Kanboard\Model; -use Kanboard\Integration\GitlabWebhook; -use Kanboard\Integration\GithubWebhook; -use Kanboard\Integration\BitbucketWebhook; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** - * Action model + * Action Model * * @package model * @author Frederic Guillot @@ -24,153 +18,38 @@ class Action extends Base const TABLE = 'actions'; /** - * SQL table name for action parameters - * - * @var string - */ - const TABLE_PARAMS = 'action_has_params'; - - /** - * Extended actions - * - * @access private - * @var array - */ - private $actions = array(); - - /** - * Extend the list of default actions - * - * @access public - * @param string $className - * @param string $description - * @return Action - */ - public function extendActions($className, $description) - { - $this->actions[$className] = $description; - return $this; - } - - /** - * Return the name and description of available actions + * Return actions and parameters for a given user * * @access public + * @param integer $user_id * @return array */ - public function getAvailableActions() + public function getAllByUser($user_id) { - $values = array( - 'TaskClose' => t('Close a task'), - 'TaskOpen' => t('Open a task'), - 'TaskAssignSpecificUser' => t('Assign the task to a specific user'), - 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'), - 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'), - 'TaskMoveAnotherProject' => t('Move the task to another project'), - 'TaskMoveColumnAssigned' => t('Move the task to another column when assigned to a user'), - 'TaskMoveColumnUnAssigned' => t('Move the task to another column when assignee is cleared'), - 'TaskAssignColorColumn' => t('Assign a color when the task is moved to a specific column'), - 'TaskAssignColorUser' => t('Assign a color to a specific user'), - 'TaskAssignColorCategory' => t('Assign automatically a color based on a category'), - 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'), - 'TaskAssignCategoryLink' => t('Assign automatically a category based on a link'), - 'CommentCreation' => t('Create a comment from an external provider'), - 'TaskCreation' => t('Create a task from an external provider'), - 'TaskLogMoveAnotherColumn' => t('Add a comment log when moving the task between columns'), - 'TaskAssignUser' => t('Change the assignee based on an external username'), - 'TaskAssignCategoryLabel' => t('Change the category based on an external label'), - 'TaskUpdateStartDate' => t('Automatically update the start date'), - 'TaskMoveColumnCategoryChange' => t('Move the task to another column when the category is changed'), - 'TaskEmail' => t('Send a task by email to someone'), - 'TaskAssignColorLink' => t('Change task color when using a specific task link'), - ); + $project_ids = $this->projectPermission->getActiveProjectIds($user_id); + $actions = array(); - $values = array_merge($values, $this->actions); - - asort($values); - - return $values; - } - - /** - * Return the name and description of available actions - * - * @access public - * @return array - */ - public function getAvailableEvents() - { - $values = array( - TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'), - Task::EVENT_MOVE_COLUMN => t('Move a task to another column'), - Task::EVENT_UPDATE => t('Task modification'), - Task::EVENT_CREATE => t('Task creation'), - Task::EVENT_OPEN => t('Reopen a task'), - Task::EVENT_CLOSE => t('Closing a task'), - Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), - Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'), - GithubWebhook::EVENT_COMMIT => t('Github commit received'), - GithubWebhook::EVENT_ISSUE_OPENED => t('Github issue opened'), - GithubWebhook::EVENT_ISSUE_CLOSED => t('Github issue closed'), - GithubWebhook::EVENT_ISSUE_REOPENED => t('Github issue reopened'), - GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Github issue assignee change'), - GithubWebhook::EVENT_ISSUE_LABEL_CHANGE => t('Github issue label change'), - GithubWebhook::EVENT_ISSUE_COMMENT => t('Github issue comment created'), - GitlabWebhook::EVENT_COMMIT => t('Gitlab commit received'), - GitlabWebhook::EVENT_ISSUE_OPENED => t('Gitlab issue opened'), - GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'), - GitlabWebhook::EVENT_ISSUE_COMMENT => t('Gitlab issue comment created'), - BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'), - BitbucketWebhook::EVENT_ISSUE_OPENED => t('Bitbucket issue opened'), - BitbucketWebhook::EVENT_ISSUE_CLOSED => t('Bitbucket issue closed'), - BitbucketWebhook::EVENT_ISSUE_REOPENED => t('Bitbucket issue reopened'), - BitbucketWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Bitbucket issue assignee change'), - BitbucketWebhook::EVENT_ISSUE_COMMENT => t('Bitbucket issue comment created'), - ); - - asort($values); - - return $values; - } - - /** - * Return the name and description of compatible actions - * - * @access public - * @param string $action_name Action name - * @return array - */ - public function getCompatibleEvents($action_name) - { - $action = $this->load($action_name, 0, ''); - $compatible_events = $action->getCompatibleEvents(); - $events = array(); - - foreach ($this->getAvailableEvents() as $event_name => $event_description) { - if (in_array($event_name, $compatible_events)) { - $events[$event_name] = $event_description; - } + if (! empty($project_ids)) { + $actions = $this->db->table(self::TABLE)->in('project_id', $project_ids)->findAll(); + $params = $this->actionParameter->getAllByActions(array_column($actions, 'id')); + $this->attachParamsToActions($actions, $params); } - return $events; + return $actions; } /** * Return actions and parameters for a given project * * @access public - * @param $project_id + * @param integer $project_id * @return array */ public function getAllByProject($project_id) { $actions = $this->db->table(self::TABLE)->eq('project_id', $project_id)->findAll(); - - foreach ($actions as &$action) { - $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll(); - } - - return $actions; + $params = $this->actionParameter->getAllByActions(array_column($actions, 'id')); + return $this->attachParamsToActions($actions, $params); } /** @@ -182,63 +61,51 @@ class Action extends Base public function getAll() { $actions = $this->db->table(self::TABLE)->findAll(); - $params = $this->db->table(self::TABLE_PARAMS)->findAll(); - - foreach ($actions as &$action) { - $action['params'] = array(); - - foreach ($params as $param) { - if ($param['action_id'] === $action['id']) { - $action['params'][] = $param; - } - } - } - - return $actions; - } - - /** - * Get all required action parameters for all registered actions - * - * @access public - * @return array All required parameters for all actions - */ - public function getAllActionParameters() - { - $params = array(); - - foreach ($this->getAll() as $action) { - $action = $this->load($action['action_name'], $action['project_id'], $action['event_name']); - $params += $action->getActionRequiredParameters(); - } - - return $params; + $params = $this->actionParameter->getAll(); + return $this->attachParamsToActions($actions, $params); } /** * Fetch an action * * @access public - * @param integer $action_id Action id - * @return array Action data + * @param integer $action_id + * @return array */ public function getById($action_id) { $action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne(); if (! empty($action)) { - $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action_id)->findAll(); + $action['params'] = $this->actionParameter->getAllByAction($action_id); } return $action; } + /** + * Attach parameters to actions + * + * @access private + * @param array &$actions + * @param array &$params + * @return array + */ + private function attachParamsToActions(array &$actions, array &$params) + { + foreach ($actions as &$action) { + $action['params'] = isset($params[$action['id']]) ? $params[$action['id']] : array(); + } + + return $actions; + } + /** * Remove an action * * @access public - * @param integer $action_id Action id - * @return bool Success or not + * @param integer $action_id + * @return bool */ public function remove($action_id) { @@ -262,24 +129,16 @@ class Action extends Base 'action_name' => $values['action_name'], ); - if (! $this->db->table(self::TABLE)->save($action)) { + if (! $this->db->table(self::TABLE)->insert($action)) { $this->db->cancelTransaction(); return false; } $action_id = $this->db->getLastId(); - foreach ($values['params'] as $param_name => $param_value) { - $action_param = array( - 'action_id' => $action_id, - 'name' => $param_name, - 'value' => $param_value, - ); - - if (! $this->db->table(self::TABLE_PARAMS)->save($action_param)) { - $this->db->cancelTransaction(); - return false; - } + if (! $this->actionParameter->create($action_id, $values)) { + $this->db->cancelTransaction(); + return false; } $this->db->closeTransaction(); @@ -287,42 +146,6 @@ class Action extends Base return $action_id; } - /** - * Load all actions and attach events - * - * @access public - */ - public function attachEvents() - { - $actions = $this->getAll(); - - foreach ($actions as $action) { - $listener = $this->load($action['action_name'], $action['project_id'], $action['event_name']); - - foreach ($action['params'] as $param) { - $listener->setParam($param['name'], $param['value']); - } - - $this->container['dispatcher']->addListener($action['event_name'], array($listener, 'execute')); - } - } - - /** - * Load an action - * - * @access public - * @param string $name Action class name - * @param integer $project_id Project id - * @param string $event Event name - * @return \Action\Base - */ - public function load($name, $project_id, $event) - { - $className = $name{0} - !== '\\' ? '\Kanboard\Action\\'.$name : $name; - return new $className($this->container, $project_id, $event); - } - /** * Copy actions from a project to another one (skip actions that cannot resolve parameters) * @@ -345,15 +168,14 @@ class Action extends Base ); if (! $this->db->table(self::TABLE)->insert($values)) { - $this->container['logger']->debug('Action::duplicate => unable to create '.$action['action_name']); $this->db->cancelTransaction(); continue; } $action_id = $this->db->getLastId(); - if (! $this->duplicateParameters($dst_project_id, $action_id, $action['params'])) { - $this->container['logger']->debug('Action::duplicate => unable to copy parameters for '.$action['action_name']); + if (! $this->actionParameter->duplicateParameters($dst_project_id, $action_id, $action['params'])) { + $this->logger->error('Action::duplicate => skip action '.$action['action_name'].' '.$action['id']); $this->db->cancelTransaction(); continue; } @@ -363,95 +185,4 @@ class Action extends Base return true; } - - /** - * Duplicate action parameters - * - * @access public - * @param integer $project_id - * @param integer $action_id - * @param array $params - * @return boolean - */ - public function duplicateParameters($project_id, $action_id, array $params) - { - foreach ($params as $param) { - $value = $this->resolveParameters($param, $project_id); - - if ($value === false) { - $this->container['logger']->debug('Action::duplicateParameters => unable to resolve '.$param['name'].'='.$param['value']); - return false; - } - - $values = array( - 'action_id' => $action_id, - 'name' => $param['name'], - 'value' => $value, - ); - - if (! $this->db->table(self::TABLE_PARAMS)->insert($values)) { - return false; - } - } - - return true; - } - - /** - * Resolve action parameter values according to another project - * - * @author Antonio Rabelo - * @access public - * @param array $param Action parameter - * @param integer $project_id Project to find the corresponding values - * @return mixed - */ - public function resolveParameters(array $param, $project_id) - { - switch ($param['name']) { - case 'project_id': - return $project_id; - case 'category_id': - return $this->category->getIdByName($project_id, $this->category->getNameById($param['value'])) ?: false; - case 'src_column_id': - case 'dest_column_id': - case 'dst_column_id': - case 'column_id': - $column = $this->board->getColumn($param['value']); - - if (empty($column)) { - return false; - } - - return $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false; - case 'user_id': - case 'owner_id': - return $this->projectPermission->isMember($project_id, $param['value']) ? $param['value'] : false; - default: - return $param['value']; - } - } - - /** - * Validate action creation - * - * @access public - * @param array $values Required parameters to save an action - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, array( - new Validators\Required('project_id', t('The project id is required')), - new Validators\Integer('project_id', t('This value must be an integer')), - new Validators\Required('event_name', t('This value is required')), - new Validators\Required('action_name', t('This value is required')), - new Validators\Required('params', t('This value is required')), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/ActionParameter.php b/sources/app/Model/ActionParameter.php new file mode 100644 index 0000000..62b0314 --- /dev/null +++ b/sources/app/Model/ActionParameter.php @@ -0,0 +1,162 @@ +db->table(self::TABLE)->findAll(); + return $this->toDictionary($params); + } + + /** + * Get all params for a list of actions + * + * @access public + * @param array $action_ids + * @return array + */ + public function getAllByActions(array $action_ids) + { + $params = $this->db->table(self::TABLE)->in('action_id', $action_ids)->findAll(); + return $this->toDictionary($params); + } + + /** + * Build params dictionary + * + * @access private + * @param array $params + * @return array + */ + private function toDictionary(array $params) + { + $result = array(); + + foreach ($params as $param) { + $result[$param['action_id']][$param['name']] = $param['value']; + } + + return $result; + } + + /** + * Get all action params for a given action + * + * @access public + * @param integer $action_id + * @return array + */ + public function getAllByAction($action_id) + { + return $this->db->hashtable(self::TABLE)->eq('action_id', $action_id)->getAll('name', 'value'); + } + + /** + * Insert new parameters for an action + * + * @access public + * @param integer $action_id + * @param array $values + * @return boolean + */ + public function create($action_id, array $values) + { + foreach ($values['params'] as $name => $value) { + $param = array( + 'action_id' => $action_id, + 'name' => $name, + 'value' => $value, + ); + + if (! $this->db->table(self::TABLE)->save($param)) { + return false; + } + } + + return true; + } + + /** + * Duplicate action parameters + * + * @access public + * @param integer $project_id + * @param integer $action_id + * @param array $params + * @return boolean + */ + public function duplicateParameters($project_id, $action_id, array $params) + { + foreach ($params as $name => $value) { + $value = $this->resolveParameter($project_id, $name, $value); + + if ($value === false) { + $this->logger->error('ActionParameter::duplicateParameters => unable to resolve '.$name.'='.$value); + return false; + } + + $values = array( + 'action_id' => $action_id, + 'name' => $name, + 'value' => $value, + ); + + if (! $this->db->table(self::TABLE)->insert($values)) { + return false; + } + } + + return true; + } + + /** + * Resolve action parameter values according to another project + * + * @access private + * @param integer $project_id + * @param string $name + * @param string $value + * @return mixed + */ + private function resolveParameter($project_id, $name, $value) + { + switch ($name) { + case 'project_id': + return $value != $project_id ? $value : false; + case 'category_id': + return $this->category->getIdByName($project_id, $this->category->getNameById($value)) ?: false; + case 'src_column_id': + case 'dest_column_id': + case 'dst_column_id': + case 'column_id': + $column = $this->board->getColumn($value); + return empty($column) ? false : $this->board->getColumnIdByTitle($project_id, $column['title']) ?: false; + case 'user_id': + case 'owner_id': + return $this->projectPermission->isAssignable($project_id, $value) ? $value : false; + default: + return $value; + } + } +} diff --git a/sources/app/Model/Authentication.php b/sources/app/Model/Authentication.php deleted file mode 100644 index 83d8543..0000000 --- a/sources/app/Model/Authentication.php +++ /dev/null @@ -1,206 +0,0 @@ -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 - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateForm(array $values) - { - list($result, $errors) = $this->validateFormCredentials($values); - - 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'); - } - } - - return array($result, $errors); - } - - /** - * Validate credentials syntax - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateFormCredentials(array $values) - { - $v = new Validator($values, array( - new Validators\Required('username', t('The username is required')), - new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), - new Validators\Required('password', t('The password is required')), - )); - - return array( - $v->execute(), - $v->getErrors(), - ); - } - - /** - * Validate captcha - * - * @access public - * @param array $values Form values - * @return boolean - */ - public function validateFormCaptcha(array $values) - { - if ($this->hasCaptcha($values['username'])) { - if (! isset($this->sessionStorage->captcha)) { - return false; - } - - $builder = new CaptchaBuilder; - $builder->setPhrase($this->sessionStorage->captcha); - return $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : ''); - } - - return true; - } - - /** - * Create remember me session if necessary - * - * @access private - * @param array $values Form values - */ - private function createRememberMeSession(array $values) - { - if (REMEMBER_ME_AUTH && ! empty($values['remember_me'])) { - $credentials = $this->backend('rememberMe') - ->create($this->userSession->getId(), Request::getIpAddress(), Request::getUserAgent()); - - $this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']); - } - } -} diff --git a/sources/app/Model/Board.php b/sources/app/Model/Board.php index 79a1a92..0f980f6 100644 --- a/sources/app/Model/Board.php +++ b/sources/app/Model/Board.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; use PicoDb\Database; /** @@ -436,47 +434,4 @@ class Board extends Base { return $this->db->table(self::TABLE)->eq('id', $column_id)->remove(); } - - /** - * Validate column modification - * - * @access public - * @param array $values Required parameters to update a column - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $v = new Validator($values, array( - new Validators\Integer('task_limit', t('This value must be an integer')), - new Validators\Required('title', t('The title is required')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate column creation - * - * @access public - * @param array $values Required parameters to save an action - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, array( - new Validators\Required('project_id', t('The project id is required')), - new Validators\Integer('project_id', t('This value must be an integer')), - new Validators\Required('title', t('The title is required')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/Category.php b/sources/app/Model/Category.php index bf40c60..58cee73 100644 --- a/sources/app/Model/Category.php +++ b/sources/app/Model/Category.php @@ -2,9 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** * Category model * @@ -212,63 +209,4 @@ class Category extends Base return true; } - - /** - * Validate category creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('project_id', t('The project id is required')), - new Validators\Required('name', t('The name is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate category modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The id is required')), - new Validators\Required('name', t('The name is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('The id must be an integer')), - new Validators\Integer('project_id', t('The project id must be an integer')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) - ); - } } diff --git a/sources/app/Model/Comment.php b/sources/app/Model/Comment.php index c7125a2..6eb4a1e 100644 --- a/sources/app/Model/Comment.php +++ b/sources/app/Model/Comment.php @@ -3,8 +3,6 @@ namespace Kanboard\Model; use Kanboard\Event\CommentEvent; -use SimpleValidator\Validator; -use SimpleValidator\Validators; /** * Comment model @@ -26,8 +24,9 @@ class Comment extends Base * * @var string */ - const EVENT_UPDATE = 'comment.update'; - const EVENT_CREATE = 'comment.create'; + const EVENT_UPDATE = 'comment.update'; + const EVENT_CREATE = 'comment.create'; + const EVENT_USER_MENTION = 'comment.user.mention'; /** * Get all comments for a given task @@ -74,6 +73,7 @@ class Comment extends Base self::TABLE.'.user_id', self::TABLE.'.date_creation', self::TABLE.'.comment', + self::TABLE.'.reference', User::TABLE.'.username', User::TABLE.'.name' ) @@ -110,7 +110,9 @@ class Comment extends Base $comment_id = $this->persist(self::TABLE, $values); if ($comment_id) { - $this->container['dispatcher']->dispatch(self::EVENT_CREATE, new CommentEvent(array('id' => $comment_id) + $values)); + $event = new CommentEvent(array('id' => $comment_id) + $values); + $this->dispatcher->dispatch(self::EVENT_CREATE, $event); + $this->userMention->fireEvents($values['comment'], self::EVENT_USER_MENTION, $event); } return $comment_id; @@ -148,62 +150,4 @@ class Comment extends Base { return $this->db->table(self::TABLE)->eq('id', $comment_id)->remove(); } - - /** - * Validate comment creation - * - * @access public - * @param array $values Required parameters to save an action - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('task_id', t('This value is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate comment modification - * - * @access public - * @param array $values Required parameters to save an action - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('This value is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('This value must be an integer')), - new Validators\Integer('task_id', t('This value must be an integer')), - new Validators\Integer('user_id', t('This value must be an integer')), - new Validators\Required('comment', t('Comment is required')) - ); - } } diff --git a/sources/app/Model/Config.php b/sources/app/Model/Config.php index 273f884..5599931 100644 --- a/sources/app/Model/Config.php +++ b/sources/app/Model/Config.php @@ -4,7 +4,6 @@ namespace Kanboard\Model; use Kanboard\Core\Translator; use Kanboard\Core\Security\Token; -use Kanboard\Core\Session\SessionManager; /** * Config model @@ -14,31 +13,6 @@ use Kanboard\Core\Session\SessionManager; */ class Config extends Setting { - /** - * Get available currencies - * - * @access public - * @return array - */ - public function getCurrencies() - { - return array( - 'USD' => t('USD - US Dollar'), - 'EUR' => t('EUR - Euro'), - 'GBP' => t('GBP - British Pound'), - 'CHF' => t('CHF - Swiss Francs'), - 'CAD' => t('CAD - Canadian Dollar'), - 'AUD' => t('AUD - Australian Dollar'), - 'NZD' => t('NZD - New Zealand Dollar'), - 'INR' => t('INR - Indian Rupee'), - 'JPY' => t('JPY - Japanese Yen'), - 'RSD' => t('RSD - Serbian dinar'), - 'SEK' => t('SEK - Swedish Krona'), - 'NOK' => t('NOK - Norwegian Krone'), - 'BAM' => t('BAM - Konvertibile Mark'), - ); - } - /** * Get available timezones * @@ -58,6 +32,31 @@ class Config extends Setting return $listing; } + /** + * Get current timezone + * + * @access public + * @return string + */ + public function getCurrentTimezone() + { + if ($this->userSession->isLogged() && ! empty($this->sessionStorage->user['timezone'])) { + return $this->sessionStorage->user['timezone']; + } + + return $this->get('application_timezone', 'UTC'); + } + + /** + * Set timezone + * + * @access public + */ + public function setupTimezone() + { + date_default_timezone_set($this->getCurrentTimezone()); + } + /** * Get available languages * @@ -79,6 +78,7 @@ class Config extends Setting 'fr_FR' => 'Français', 'it_IT' => 'Italiano', 'hu_HU' => 'Magyar', + 'my_MY' => 'Melayu', 'nl_NL' => 'Nederlands', 'nb_NO' => 'Norsk', 'pl_PL' => 'Polski', @@ -154,43 +154,6 @@ class Config extends Setting return $this->get('application_language', 'en_US'); } - /** - * Get a config variable from the session or the database - * - * @access public - * @param string $name Parameter name - * @param string $default_value Default value of the parameter - * @return string - */ - public function get($name, $default_value = '') - { - if (! SessionManager::isOpen()) { - return $this->getOption($name, $default_value); - } - - // Cache config in session - if (! isset($this->sessionStorage->config[$name])) { - $this->sessionStorage->config = $this->getAll(); - } - - if (! empty($this->sessionStorage->config[$name])) { - return $this->sessionStorage->config[$name]; - } - - return $default_value; - } - - /** - * Reload settings in the session and the translations - * - * @access public - */ - public function reload() - { - $this->sessionStorage->config = $this->getAll(); - $this->setupTranslations(); - } - /** * Load translations * @@ -202,28 +165,27 @@ class Config extends Setting } /** - * Get current timezone + * Get a config variable from the session or the database * * @access public + * @param string $name Parameter name + * @param string $default_value Default value of the parameter * @return string */ - public function getCurrentTimezone() + public function get($name, $default_value = '') { - if ($this->userSession->isLogged() && ! empty($this->sessionStorage->user['timezone'])) { - return $this->sessionStorage->user['timezone']; - } - - return $this->get('application_timezone', 'UTC'); + $options = $this->memoryCache->proxy($this, 'getAll'); + return isset($options[$name]) && $options[$name] !== '' ? $options[$name] : $default_value; } /** - * Set timezone + * Reload settings in the session and the translations * * @access public */ - public function setupTimezone() + public function reload() { - date_default_timezone_set($this->getCurrentTimezone()); + $this->setupTranslations(); } /** @@ -234,7 +196,7 @@ class Config extends Setting */ public function optimizeDatabase() { - return $this->db->getconnection()->exec("VACUUM"); + return $this->db->getconnection()->exec('VACUUM'); } /** @@ -264,10 +226,11 @@ class Config extends Setting * * @access public * @param string $option Parameter name + * @return boolean */ public function regenerateToken($option) { - $this->save(array($option => Token::getToken())); + return $this->save(array($option => Token::getToken())); } /** diff --git a/sources/app/Model/Currency.php b/sources/app/Model/Currency.php index c115661..abcce2f 100644 --- a/sources/app/Model/Currency.php +++ b/sources/app/Model/Currency.php @@ -2,9 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** * Currency * @@ -20,6 +17,32 @@ class Currency extends Base */ const TABLE = 'currencies'; + /** + * Get available application currencies + * + * @access public + * @return array + */ + public function getCurrencies() + { + return array( + 'USD' => t('USD - US Dollar'), + 'EUR' => t('EUR - Euro'), + 'GBP' => t('GBP - British Pound'), + 'CHF' => t('CHF - Swiss Francs'), + 'CAD' => t('CAD - Canadian Dollar'), + 'AUD' => t('AUD - Australian Dollar'), + 'NZD' => t('NZD - New Zealand Dollar'), + 'INR' => t('INR - Indian Rupee'), + 'JPY' => t('JPY - Japanese Yen'), + 'RSD' => t('RSD - Serbian dinar'), + 'SEK' => t('SEK - Swedish Krona'), + 'NOK' => t('NOK - Norwegian Krone'), + 'BAM' => t('BAM - Konvertible Mark'), + 'RUB' => t('RUB - Russian Ruble'), + ); + } + /** * Get all currency rates * @@ -45,7 +68,7 @@ class Currency extends Base $reference = $this->config->get('application_currency', 'USD'); if ($reference !== $currency) { - $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : array(); + $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : $rates; $rate = isset($rates[$currency]) ? $rates[$currency] : 1; return $rate * $price; @@ -68,7 +91,7 @@ class Currency extends Base return $this->update($currency, $rate); } - return $this->persist(self::TABLE, compact('currency', 'rate')); + return $this->db->table(self::TABLE)->insert(array('currency' => $currency, 'rate' => $rate)); } /** @@ -83,24 +106,4 @@ class Currency extends Base { return $this->db->table(self::TABLE)->eq('currency', $currency)->update(array('rate' => $rate)); } - - /** - * Validate - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validate(array $values) - { - $v = new Validator($values, array( - new Validators\Required('currency', t('Field required')), - new Validators\Required('rate', t('Field required')), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/CustomFilter.php b/sources/app/Model/CustomFilter.php index 6550b4a..3a6a1a3 100644 --- a/sources/app/Model/CustomFilter.php +++ b/sources/app/Model/CustomFilter.php @@ -2,9 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** * Custom Filter model * @@ -102,63 +99,4 @@ class CustomFilter extends Base { return $this->db->table(self::TABLE)->eq('id', $filter_id)->remove(); } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Required('project_id', t('Field required')), - new Validators\Required('user_id', t('Field required')), - new Validators\Required('name', t('Field required')), - new Validators\Required('filter', t('Field required')), - new Validators\Integer('user_id', t('This value must be an integer')), - new Validators\Integer('project_id', t('This value must be an integer')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 100), 100), - new Validators\MaxLength('filter', t('The maximum length is %d characters', 100), 100) - ); - } - - /** - * Validate filter creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, $this->commonValidationRules()); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate filter modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('Field required')), - new Validators\Integer('id', t('This value must be an integer')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/File.php b/sources/app/Model/File.php index daade51..be62cdb 100644 --- a/sources/app/Model/File.php +++ b/sources/app/Model/File.php @@ -263,14 +263,16 @@ class File extends Base public function uploadFiles($project_id, $task_id, $form_name) { try { - if (empty($_FILES[$form_name])) { + $file = $this->request->getFileInfo($form_name); + + if (empty($file)) { return false; } - foreach ($_FILES[$form_name]['error'] as $key => $error) { - if ($error == UPLOAD_ERR_OK && $_FILES[$form_name]['size'][$key] > 0) { - $original_filename = $_FILES[$form_name]['name'][$key]; - $uploaded_filename = $_FILES[$form_name]['tmp_name'][$key]; + foreach ($file['error'] as $key => $error) { + if ($error == UPLOAD_ERR_OK && $file['size'][$key] > 0) { + $original_filename = $file['name'][$key]; + $uploaded_filename = $file['tmp_name'][$key]; $destination_filename = $this->generatePath($project_id, $task_id, $original_filename); if ($this->isImage($original_filename)) { @@ -283,7 +285,7 @@ class File extends Base $task_id, $original_filename, $destination_filename, - $_FILES[$form_name]['size'][$key] + $file['size'][$key] ); } } diff --git a/sources/app/Model/Group.php b/sources/app/Model/Group.php new file mode 100644 index 0000000..6789950 --- /dev/null +++ b/sources/app/Model/Group.php @@ -0,0 +1,117 @@ +db->table(self::TABLE); + } + + /** + * Get a specific group by id + * + * @access public + * @param integer $group_id + * @return array + */ + public function getById($group_id) + { + return $this->getQuery()->eq('id', $group_id)->findOne(); + } + + /** + * 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 + * @return array + */ + public function getAll() + { + return $this->getQuery()->asc('name')->findAll(); + } + + /** + * Search groups by name + * + * @access public + * @param string $input + * @return array + */ + public function search($input) + { + return $this->db->table(self::TABLE)->ilike('name', '%'.$input.'%')->asc('name')->findAll(); + } + + /** + * Remove a group + * + * @access public + * @param integer $group_id + * @return array + */ + public function remove($group_id) + { + return $this->db->table(self::TABLE)->eq('id', $group_id)->remove(); + } + + /** + * Create a new group + * + * @access public + * @param string $name + * @param string $external_id + * @return integer|boolean + */ + public function create($name, $external_id = '') + { + return $this->persist(self::TABLE, array( + 'name' => $name, + 'external_id' => $external_id, + )); + } + + /** + * Update existing group + * + * @access public + * @param array $values + * @return boolean + */ + public function update(array $values) + { + return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); + } +} diff --git a/sources/app/Model/GroupMember.php b/sources/app/Model/GroupMember.php new file mode 100644 index 0000000..7ed5f73 --- /dev/null +++ b/sources/app/Model/GroupMember.php @@ -0,0 +1,111 @@ +db->table(self::TABLE) + ->join(User::TABLE, 'id', 'user_id') + ->eq('group_id', $group_id); + } + + /** + * Get all users + * + * @access public + * @param integer $group_id + * @return array + */ + public function getMembers($group_id) + { + return $this->getQuery($group_id)->findAll(); + } + + /** + * Get all not members + * + * @access public + * @param integer $group_id + * @return array + */ + public function getNotMembers($group_id) + { + $subquery = $this->db->table(self::TABLE) + ->columns('user_id') + ->eq('group_id', $group_id); + + return $this->db->table(User::TABLE) + ->notInSubquery('id', $subquery) + ->findAll(); + } + + /** + * Add user to a group + * + * @access public + * @param integer $group_id + * @param integer $user_id + * @return boolean + */ + public function addUser($group_id, $user_id) + { + return $this->db->table(self::TABLE)->insert(array( + 'group_id' => $group_id, + 'user_id' => $user_id, + )); + } + + /** + * Remove user from a group + * + * @access public + * @param integer $group_id + * @param integer $user_id + * @return boolean + */ + public function removeUser($group_id, $user_id) + { + return $this->db->table(self::TABLE) + ->eq('group_id', $group_id) + ->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/sources/app/Model/LastLogin.php b/sources/app/Model/LastLogin.php index 0f148ea..f5be020 100644 --- a/sources/app/Model/LastLogin.php +++ b/sources/app/Model/LastLogin.php @@ -36,11 +36,31 @@ class LastLogin extends Base */ public function create($auth_type, $user_id, $ip, $user_agent) { - // Cleanup old sessions if necessary + $this->cleanup($user_id); + + return $this->db + ->table(self::TABLE) + ->insert(array( + 'auth_type' => $auth_type, + 'user_id' => $user_id, + 'ip' => $ip, + 'user_agent' => substr($user_agent, 0, 255), + 'date_creation' => time(), + )); + } + + /** + * Cleanup login history + * + * @access public + * @param integer $user_id + */ + public function cleanup($user_id) + { $connections = $this->db ->table(self::TABLE) ->eq('user_id', $user_id) - ->desc('date_creation') + ->desc('id') ->findAllByColumn('id'); if (count($connections) >= self::NB_LOGINS) { @@ -49,16 +69,6 @@ class LastLogin extends Base ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1)) ->remove(); } - - return $this->db - ->table(self::TABLE) - ->insert(array( - 'auth_type' => $auth_type, - 'user_id' => $user_id, - 'ip' => $ip, - 'user_agent' => $user_agent, - 'date_creation' => time(), - )); } /** @@ -73,7 +83,7 @@ class LastLogin extends Base return $this->db ->table(self::TABLE) ->eq('user_id', $user_id) - ->desc('date_creation') + ->desc('id') ->columns('id', 'auth_type', 'ip', 'user_agent', 'date_creation') ->findAll(); } diff --git a/sources/app/Model/Link.php b/sources/app/Model/Link.php index 00b6dfc..7b81a23 100644 --- a/sources/app/Model/Link.php +++ b/sources/app/Model/Link.php @@ -3,8 +3,6 @@ namespace Kanboard\Model; use PDO; -use SimpleValidator\Validator; -use SimpleValidator\Validators; /** * Link model @@ -176,47 +174,4 @@ class Link extends Base $this->db->table(self::TABLE)->eq('opposite_id', $link_id)->update(array('opposite_id' => 0)); return $this->db->table(self::TABLE)->eq('id', $link_id)->remove(); } - - /** - * Validate creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, array( - new Validators\Required('label', t('Field required')), - new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE), - new Validators\NotEquals('label', 'opposite_label', t('The labels must be different')), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $v = new Validator($values, array( - new Validators\Required('id', t('Field required')), - new Validators\Required('opposite_id', t('Field required')), - new Validators\Required('label', t('Field required')), - new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE), - )); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/Metadata.php b/sources/app/Model/Metadata.php index 83c8f49..690b226 100644 --- a/sources/app/Model/Metadata.php +++ b/sources/app/Model/Metadata.php @@ -95,4 +95,17 @@ abstract class Metadata extends Base return ! in_array(false, $results, true); } + + /** + * Remove a metadata + * + * @access public + * @param integer $entity_id + * @param string $name + * @return bool + */ + public function remove($entity_id, $name) + { + return $this->db->table(static::TABLE)->eq($this->getEntityKey(), $entity_id)->eq('name', $name)->remove(); + } } diff --git a/sources/app/Model/Notification.php b/sources/app/Model/Notification.php index f112299..87c1a79 100644 --- a/sources/app/Model/Notification.php +++ b/sources/app/Model/Notification.php @@ -74,6 +74,10 @@ class Notification extends Base return e('%s commented on the task #%d', $event_author, $event_data['task']['id']); case File::EVENT_CREATE: return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']); + case Task::EVENT_USER_MENTION: + return e('%s mentioned you in the task #%d', $event_author, $event_data['task']['id']); + case Comment::EVENT_USER_MENTION: + return e('%s mentioned you in a comment on the task #%d', $event_author, $event_data['task']['id']); default: return e('Notification'); } @@ -91,52 +95,40 @@ class Notification extends Base { switch ($event_name) { case File::EVENT_CREATE: - $title = e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); - break; + return e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); case Comment::EVENT_CREATE: - $title = e('New comment on task #%d', $event_data['comment']['task_id']); - break; + return e('New comment on task #%d', $event_data['comment']['task_id']); case Comment::EVENT_UPDATE: - $title = e('Comment updated on task #%d', $event_data['comment']['task_id']); - break; + return e('Comment updated on task #%d', $event_data['comment']['task_id']); case Subtask::EVENT_CREATE: - $title = e('New subtask on task #%d', $event_data['subtask']['task_id']); - break; + return e('New subtask on task #%d', $event_data['subtask']['task_id']); case Subtask::EVENT_UPDATE: - $title = e('Subtask updated on task #%d', $event_data['subtask']['task_id']); - break; + return e('Subtask updated on task #%d', $event_data['subtask']['task_id']); case Task::EVENT_CREATE: - $title = e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); - break; + return e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); case Task::EVENT_UPDATE: - $title = e('Task updated #%d', $event_data['task']['id']); - break; + return e('Task updated #%d', $event_data['task']['id']); case Task::EVENT_CLOSE: - $title = e('Task #%d closed', $event_data['task']['id']); - break; + return e('Task #%d closed', $event_data['task']['id']); case Task::EVENT_OPEN: - $title = e('Task #%d opened', $event_data['task']['id']); - break; + return e('Task #%d opened', $event_data['task']['id']); case Task::EVENT_MOVE_COLUMN: - $title = e('Column changed for task #%d', $event_data['task']['id']); - break; + return e('Column changed for task #%d', $event_data['task']['id']); case Task::EVENT_MOVE_POSITION: - $title = e('New position for task #%d', $event_data['task']['id']); - break; + return e('New position for task #%d', $event_data['task']['id']); case Task::EVENT_MOVE_SWIMLANE: - $title = e('Swimlane changed for task #%d', $event_data['task']['id']); - break; + return e('Swimlane changed for task #%d', $event_data['task']['id']); case Task::EVENT_ASSIGNEE_CHANGE: - $title = e('Assignee changed on task #%d', $event_data['task']['id']); - break; + return e('Assignee changed on task #%d', $event_data['task']['id']); case Task::EVENT_OVERDUE: $nb = count($event_data['tasks']); - $title = $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']); - break; + return $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']); + case Task::EVENT_USER_MENTION: + return e('You were mentioned in the task #%d', $event_data['task']['id']); + case Comment::EVENT_USER_MENTION: + return e('You were mentioned in a comment on the task #%d', $event_data['task']['id']); default: - $title = e('Notification'); + return e('Notification'); } - - return $title; } } diff --git a/sources/app/Model/PasswordReset.php b/sources/app/Model/PasswordReset.php new file mode 100644 index 0000000..c2d7dde --- /dev/null +++ b/sources/app/Model/PasswordReset.php @@ -0,0 +1,93 @@ +db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_creation')->limit(100)->findAll(); + } + + /** + * Generate a new reset token for a user + * + * @access public + * @param string $username + * @param integer $expiration + * @return boolean|string + */ + public function create($username, $expiration = 0) + { + $user_id = $this->db->table(User::TABLE)->eq('username', $username)->neq('email', '')->notNull('email')->findOneColumn('id'); + + if (! $user_id) { + return false; + } + + $token = $this->token->getToken(); + + $result = $this->db->table(self::TABLE)->insert(array( + 'token' => $token, + 'user_id' => $user_id, + 'date_expiration' => $expiration ?: time() + self::DURATION, + 'date_creation' => time(), + 'ip' => $this->request->getIpAddress(), + 'user_agent' => $this->request->getUserAgent(), + 'is_active' => 1, + )); + + return $result ? $token : false; + } + + /** + * Get user id from the token + * + * @access public + * @param string $token + * @return integer + */ + public function getUserIdByToken($token) + { + return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_active', 1)->gte('date_expiration', time())->findOneColumn('user_id'); + } + + /** + * Disable all tokens for a user + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function disable($user_id) + { + return $this->db->table(self::TABLE)->eq('user_id', $user_id)->update(array('is_active' => 0)); + } +} diff --git a/sources/app/Model/Project.php b/sources/app/Model/Project.php index a7f9309..6307734 100644 --- a/sources/app/Model/Project.php +++ b/sources/app/Model/Project.php @@ -2,9 +2,8 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; use Kanboard\Core\Security\Token; +use Kanboard\Core\Security\Role; /** * Project model @@ -287,7 +286,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 +364,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); @@ -509,71 +508,4 @@ class Project extends Base ->eq('id', $project_id) ->save(array('is_public' => 0, 'token' => '')); } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('This value must be an integer')), - new Validators\Integer('is_active', t('This value must be an integer')), - new Validators\Required('name', t('The project name is required')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50), - new Validators\MaxLength('identifier', t('The maximum length is %d characters', 50), 50), - new Validators\MaxLength('start_date', t('The maximum length is %d characters', 10), 10), - new Validators\MaxLength('end_date', t('The maximum length is %d characters', 10), 10), - new Validators\AlphaNumeric('identifier', t('This value must be alphanumeric')) , - new Validators\Unique('identifier', t('The identifier must be unique'), $this->db->getConnection(), self::TABLE), - ); - } - - /** - * Validate project creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - if (! empty($values['identifier'])) { - $values['identifier'] = strtoupper($values['identifier']); - } - - $v = new Validator($values, $this->commonValidationRules()); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate project modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - if (! empty($values['identifier'])) { - $values['identifier'] = strtoupper($values['identifier']); - } - - $rules = array( - new Validators\Required('id', t('This value is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/ProjectActivity.php b/sources/app/Model/ProjectActivity.php index 309bab9..74df26a 100644 --- a/sources/app/Model/ProjectActivity.php +++ b/sources/app/Model/ProjectActivity.php @@ -168,15 +168,11 @@ class ProjectActivity extends Base */ public function cleanup($max) { - if ($this->db->table(self::TABLE)->count() > $max) { - $this->db->execute(' - DELETE FROM '.self::TABLE.' - WHERE id <= ( - SELECT id FROM ( - SELECT id FROM '.self::TABLE.' ORDER BY id DESC LIMIT 1 OFFSET '.$max.' - ) foo - )' - ); + $total = $this->db->table(self::TABLE)->count(); + + if ($total > $max) { + $ids = $this->db->table(self::TABLE)->asc('id')->limit($total - $max)->findAllByColumn('id'); + $this->db->table(self::TABLE)->in('id', $ids)->remove(); } } diff --git a/sources/app/Model/ProjectAnalytic.php b/sources/app/Model/ProjectAnalytic.php deleted file mode 100644 index 92364c0..0000000 --- a/sources/app/Model/ProjectAnalytic.php +++ /dev/null @@ -1,182 +0,0 @@ -board->getColumns($project_id); - - foreach ($columns as $column) { - $nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']); - $total += $nb_tasks; - - $metrics[] = array( - 'column_title' => $column['title'], - 'nb_tasks' => $nb_tasks, - ); - } - - if ($total === 0) { - return array(); - } - - foreach ($metrics as &$metric) { - $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); - } - - return $metrics; - } - - /** - * Get users repartition - * - * @access public - * @param integer $project_id - * @return array - */ - public function getUserRepartition($project_id) - { - $metrics = array(); - $total = 0; - $tasks = $this->taskFinder->getAll($project_id); - $users = $this->projectPermission->getMemberList($project_id); - - foreach ($tasks as $task) { - $user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0]; - $total++; - - if (! isset($metrics[$user])) { - $metrics[$user] = array( - 'nb_tasks' => 0, - 'percentage' => 0, - 'user' => $user, - ); - } - - $metrics[$user]['nb_tasks']++; - } - - if ($total === 0) { - return array(); - } - - foreach ($metrics as &$metric) { - $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); - } - - ksort($metrics); - - return array_values($metrics); - } - - /** - * Get the average lead and cycle time - * - * @access public - * @param integer $project_id - * @return array - */ - public function getAverageLeadAndCycleTime($project_id) - { - $stats = array( - 'count' => 0, - 'total_lead_time' => 0, - 'total_cycle_time' => 0, - 'avg_lead_time' => 0, - 'avg_cycle_time' => 0, - ); - - $tasks = $this->db - ->table(Task::TABLE) - ->columns('date_completed', 'date_creation', 'date_started') - ->eq('project_id', $project_id) - ->desc('id') - ->limit(1000) - ->findAll(); - - foreach ($tasks as &$task) { - $stats['count']++; - $stats['total_lead_time'] += ($task['date_completed'] ?: time()) - $task['date_creation']; - $stats['total_cycle_time'] += empty($task['date_started']) ? 0 : ($task['date_completed'] ?: time()) - $task['date_started']; - } - - $stats['avg_lead_time'] = $stats['count'] > 0 ? (int) ($stats['total_lead_time'] / $stats['count']) : 0; - $stats['avg_cycle_time'] = $stats['count'] > 0 ? (int) ($stats['total_cycle_time'] / $stats['count']) : 0; - - return $stats; - } - - /** - * Get the average time spent into each column - * - * @access public - * @param integer $project_id - * @return array - */ - public function getAverageTimeSpentByColumn($project_id) - { - $stats = array(); - $columns = $this->board->getColumnsList($project_id); - - // Get the time spent of the last move for each tasks - $tasks = $this->db - ->table(Task::TABLE) - ->columns('id', 'date_completed', 'date_moved', 'column_id') - ->eq('project_id', $project_id) - ->desc('id') - ->limit(1000) - ->findAll(); - - // Init values - foreach ($columns as $column_id => $column_title) { - $stats[$column_id] = array( - 'count' => 0, - 'time_spent' => 0, - 'average' => 0, - 'title' => $column_title, - ); - } - - // Get time spent foreach task/column and take into account the last move - foreach ($tasks as &$task) { - $sums = $this->transition->getTimeSpentByTask($task['id']); - - if (! isset($sums[$task['column_id']])) { - $sums[$task['column_id']] = 0; - } - - $sums[$task['column_id']] += ($task['date_completed'] ?: time()) - $task['date_moved']; - - foreach ($sums as $column_id => $time_spent) { - if (isset($stats[$column_id])) { - $stats[$column_id]['count']++; - $stats[$column_id]['time_spent'] += $time_spent; - } - } - } - - // Calculate average for each column - foreach ($columns as $column_id => $column_title) { - $stats[$column_id]['average'] = $stats[$column_id]['count'] > 0 ? (int) ($stats[$column_id]['time_spent'] / $stats[$column_id]['count']) : 0; - } - - return $stats; - } -} diff --git a/sources/app/Model/ProjectDailyColumnStats.php b/sources/app/Model/ProjectDailyColumnStats.php index 4b75fff..cf79be8 100644 --- a/sources/app/Model/ProjectDailyColumnStats.php +++ b/sources/app/Model/ProjectDailyColumnStats.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use PicoDb\Database; - /** * Project Daily Column Stats * @@ -20,7 +18,7 @@ class ProjectDailyColumnStats extends Base const TABLE = 'project_daily_column_stats'; /** - * Update daily totals for the project and foreach column + * Update daily totals for the project and for each column * * "total" is the number open of tasks in the column * "score" is the sum of tasks score in the column @@ -32,48 +30,17 @@ class ProjectDailyColumnStats extends Base */ public function updateTotals($project_id, $date) { - $status = $this->config->get('cfd_include_closed_tasks') == 1 ? array(Task::STATUS_OPEN, Task::STATUS_CLOSED) : array(Task::STATUS_OPEN); - $this->db->startTransaction(); + $this->db->table(self::TABLE)->eq('project_id', $project_id)->eq('day', $date)->remove(); - $column_ids = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->findAllByColumn('id'); - - foreach ($column_ids as $column_id) { - - $exists = $this->db->table(ProjectDailyColumnStats::TABLE) - ->eq('project_id', $project_id) - ->eq('column_id', $column_id) - ->eq('day', $date) - ->exists(); - - $score = $this->db->table(Task::TABLE) - ->eq('project_id', $project_id) - ->eq('column_id', $column_id) - ->eq('is_active', Task::STATUS_OPEN) - ->sum('score'); - - $total = $this->db->table(Task::TABLE) - ->eq('project_id', $project_id) - ->eq('column_id', $column_id) - ->in('is_active', $status) - ->count(); - - if ($exists) { - $this->db->table(ProjectDailyColumnStats::TABLE) - ->eq('project_id', $project_id) - ->eq('column_id', $column_id) - ->eq('day', $date) - ->update(array('score' => $score, 'total' => $total)); - - } else { - $this->db->table(ProjectDailyColumnStats::TABLE)->insert(array( - 'day' => $date, - 'project_id' => $project_id, - 'column_id' => $column_id, - 'total' => $total, - 'score' => $score, - )); - } + foreach ($this->getStatsByColumns($project_id) as $column_id => $column) { + $this->db->table(self::TABLE)->insert(array( + 'day' => $date, + 'project_id' => $project_id, + 'column_id' => $column_id, + 'total' => $column['total'], + 'score' => $column['score'], + )); } $this->db->closeTransaction(); @@ -92,64 +59,11 @@ class ProjectDailyColumnStats extends Base */ public function countDays($project_id, $from, $to) { - $rq = $this->db->execute( - 'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ? AND project_id=?', - array($from, $to, $project_id) - ); - - return $rq !== false ? $rq->fetchColumn(0) : 0; - } - - /** - * Get raw metrics for the project within a data range - * - * @access public - * @param integer $project_id Project id - * @param string $from Start date (ISO format YYYY-MM-DD) - * @param string $to End date - * @return array - */ - public function getRawMetrics($project_id, $from, $to) - { - return $this->db->table(ProjectDailyColumnStats::TABLE) - ->columns( - ProjectDailyColumnStats::TABLE.'.column_id', - ProjectDailyColumnStats::TABLE.'.day', - ProjectDailyColumnStats::TABLE.'.total', - ProjectDailyColumnStats::TABLE.'.score', - Board::TABLE.'.title AS column_title' - ) - ->join(Board::TABLE, 'id', 'column_id') - ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id) - ->gte('day', $from) - ->lte('day', $to) - ->asc(ProjectDailyColumnStats::TABLE.'.day') - ->findAll(); - } - - /** - * Get raw metrics for the project within a data range grouped by day - * - * @access public - * @param integer $project_id Project id - * @param string $from Start date (ISO format YYYY-MM-DD) - * @param string $to End date - * @return array - */ - public function getRawMetricsByDay($project_id, $from, $to) - { - return $this->db->table(ProjectDailyColumnStats::TABLE) - ->columns( - ProjectDailyColumnStats::TABLE.'.day', - 'SUM('.ProjectDailyColumnStats::TABLE.'.total) AS total', - 'SUM('.ProjectDailyColumnStats::TABLE.'.score) AS score' - ) - ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id) - ->gte('day', $from) - ->lte('day', $to) - ->asc(ProjectDailyColumnStats::TABLE.'.day') - ->groupBy(ProjectDailyColumnStats::TABLE.'.day') - ->findAll(); + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->findOneColumn('COUNT(DISTINCT day)'); } /** @@ -165,43 +79,174 @@ class ProjectDailyColumnStats extends Base * @param integer $project_id Project id * @param string $from Start date (ISO format YYYY-MM-DD) * @param string $to End date - * @param string $column Column to aggregate + * @param string $field Column to aggregate * @return array */ - public function getAggregatedMetrics($project_id, $from, $to, $column = 'total') + public function getAggregatedMetrics($project_id, $from, $to, $field = 'total') { $columns = $this->board->getColumnsList($project_id); + $metrics = $this->getMetrics($project_id, $from, $to); + return $this->buildAggregate($metrics, $columns, $field); + } + + /** + * Fetch metrics + * + * @access public + * @param integer $project_id Project id + * @param string $from Start date (ISO format YYYY-MM-DD) + * @param string $to End date + * @return array + */ + public function getMetrics($project_id, $from, $to) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc(self::TABLE.'.day') + ->findAll(); + } + + /** + * Build aggregate + * + * @access private + * @param array $metrics + * @param array $columns + * @param string $field + * @return array + */ + private function buildAggregate(array &$metrics, array &$columns, $field) + { $column_ids = array_keys($columns); - $metrics = array(array_merge(array(e('Date')), array_values($columns))); - $aggregates = array(); + $days = array_unique(array_column($metrics, 'day')); + $rows = array(array_merge(array(e('Date')), array_values($columns))); - // Fetch metrics for the project - $records = $this->db->table(ProjectDailyColumnStats::TABLE) - ->eq('project_id', $project_id) - ->gte('day', $from) - ->lte('day', $to) - ->findAll(); - - // Aggregate by day - foreach ($records as $record) { - if (! isset($aggregates[$record['day']])) { - $aggregates[$record['day']] = array($record['day']); - } - - $aggregates[$record['day']][$record['column_id']] = $record[$column]; + foreach ($days as $day) { + $rows[] = $this->buildRowAggregate($metrics, $column_ids, $day, $field); } - // Aggregate by row - foreach ($aggregates as $aggregate) { - $row = array($aggregate[0]); + return $rows; + } - foreach ($column_ids as $column_id) { - $row[] = (int) $aggregate[$column_id]; - } + /** + * Build one row of the aggregate + * + * @access private + * @param array $metrics + * @param array $column_ids + * @param string $day + * @param string $field + * @return array + */ + private function buildRowAggregate(array &$metrics, array &$column_ids, $day, $field) + { + $row = array($day); - $metrics[] = $row; + foreach ($column_ids as $column_id) { + $row[] = $this->findValueInMetrics($metrics, $day, $column_id, $field); } - return $metrics; + return $row; + } + + /** + * Find the value in the metrics + * + * @access private + * @param array $metrics + * @param string $day + * @param string $column_id + * @param string $field + * @return integer + */ + private function findValueInMetrics(array &$metrics, $day, $column_id, $field) + { + foreach ($metrics as $metric) { + if ($metric['day'] === $day && $metric['column_id'] == $column_id) { + return $metric[$field]; + } + } + + return 0; + } + + /** + * Get number of tasks and score by columns + * + * @access private + * @param integer $project_id + * @return array + */ + private function getStatsByColumns($project_id) + { + $totals = $this->getTotalByColumns($project_id); + $scores = $this->getScoreByColumns($project_id); + $columns = array(); + + foreach ($totals as $column_id => $total) { + $columns[$column_id] = array('total' => $total, 'score' => 0); + } + + foreach ($scores as $column_id => $score) { + $columns[$column_id]['score'] = (int) $score; + } + + return $columns; + } + + /** + * Get number of tasks and score by columns + * + * @access private + * @param integer $project_id + * @return array + */ + private function getScoreByColumns($project_id) + { + $stats = $this->db->table(Task::TABLE) + ->columns('column_id', 'SUM(score) AS score') + ->eq('project_id', $project_id) + ->eq('is_active', Task::STATUS_OPEN) + ->notNull('score') + ->groupBy('column_id') + ->findAll(); + + return array_column($stats, 'score', 'column_id'); + } + + /** + * Get number of tasks and score by columns + * + * @access private + * @param integer $project_id + * @return array + */ + private function getTotalByColumns($project_id) + { + $stats = $this->db->table(Task::TABLE) + ->columns('column_id', 'COUNT(*) AS total') + ->eq('project_id', $project_id) + ->in('is_active', $this->getTaskStatusConfig()) + ->groupBy('column_id') + ->findAll(); + + return array_column($stats, 'total', 'column_id'); + } + + /** + * Get task status to use for total calculation + * + * @access private + * @return array + */ + private function getTaskStatusConfig() + { + if ($this->config->get('cfd_include_closed_tasks') == 1) { + return array(Task::STATUS_OPEN, Task::STATUS_CLOSED); + } + + return array(Task::STATUS_OPEN); } } diff --git a/sources/app/Model/ProjectDailyStats.php b/sources/app/Model/ProjectDailyStats.php index 7ec1ee2..957ad51 100644 --- a/sources/app/Model/ProjectDailyStats.php +++ b/sources/app/Model/ProjectDailyStats.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use PicoDb\Database; - /** * Project Daily Stats * @@ -31,29 +29,16 @@ class ProjectDailyStats extends Base { $this->db->startTransaction(); - $lead_cycle_time = $this->projectAnalytic->getAverageLeadAndCycleTime($project_id); + $lead_cycle_time = $this->averageLeadCycleTimeAnalytic->build($project_id); - $exists = $this->db->table(ProjectDailyStats::TABLE) - ->eq('day', $date) - ->eq('project_id', $project_id) - ->exists(); + $this->db->table(self::TABLE)->eq('day', $date)->eq('project_id', $project_id)->remove(); - if ($exists) { - $this->db->table(ProjectDailyStats::TABLE) - ->eq('project_id', $project_id) - ->eq('day', $date) - ->update(array( - 'avg_lead_time' => $lead_cycle_time['avg_lead_time'], - 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'], - )); - } else { - $this->db->table(ProjectDailyStats::TABLE)->insert(array( - 'day' => $date, - 'project_id' => $project_id, - 'avg_lead_time' => $lead_cycle_time['avg_lead_time'], - 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'], - )); - } + $this->db->table(self::TABLE)->insert(array( + 'day' => $date, + 'project_id' => $project_id, + 'avg_lead_time' => $lead_cycle_time['avg_lead_time'], + 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'], + )); $this->db->closeTransaction(); @@ -72,11 +57,11 @@ class ProjectDailyStats extends Base public function getRawMetrics($project_id, $from, $to) { return $this->db->table(self::TABLE) - ->columns('day', 'avg_lead_time', 'avg_cycle_time') - ->eq(self::TABLE.'.project_id', $project_id) - ->gte('day', $from) - ->lte('day', $to) - ->asc(self::TABLE.'.day') - ->findAll(); + ->columns('day', 'avg_lead_time', 'avg_cycle_time') + ->eq('project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc('day') + ->findAll(); } } diff --git a/sources/app/Model/ProjectGroupRole.php b/sources/app/Model/ProjectGroupRole.php new file mode 100644 index 0000000..591b28c --- /dev/null +++ b/sources/app/Model/ProjectGroupRole.php @@ -0,0 +1,189 @@ +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) + { + $roles = $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) + ->findAllByColumn('role'); + + return $this->projectAccessMap->getHighestRole($roles); + } + + /** + * 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(User::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') + ->join(GroupMember::TABLE, 'user_id', 'id', User::TABLE) + ->join(self::TABLE, 'group_id', 'group_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/sources/app/Model/ProjectGroupRoleFilter.php b/sources/app/Model/ProjectGroupRoleFilter.php new file mode 100644 index 0000000..989d307 --- /dev/null +++ b/sources/app/Model/ProjectGroupRoleFilter.php @@ -0,0 +1,89 @@ +query = $this->db->table(ProjectGroupRole::TABLE); + return $this; + } + + /** + * Get all results of the filter + * + * @access public + * @param string $column + * @return array + */ + public function findAll($column = '') + { + if ($column !== '') { + return $this->query->asc($column)->findAllByColumn($column); + } + + return $this->query->findAll(); + } + + /** + * Get the PicoDb query + * + * @access public + * @return \PicoDb\Table + */ + public function getQuery() + { + return $this->query; + } + + /** + * Filter by project id + * + * @access public + * @param integer $project_id + * @return ProjectUserRoleFilter + */ + public function filterByProjectId($project_id) + { + $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $project_id); + return $this; + } + + /** + * Filter by username + * + * @access public + * @param string $input + * @return ProjectUserRoleFilter + */ + public function startWithUsername($input) + { + $this->query + ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE) + ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) + ->ilike(User::TABLE.'.username', $input.'%'); + + return $this; + } +} diff --git a/sources/app/Model/ProjectPermission.php b/sources/app/Model/ProjectPermission.php index d9eef4d..cea62e1 100644 --- a/sources/app/Model/ProjectPermission.php +++ b/sources/app/Model/ProjectPermission.php @@ -2,129 +2,25 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; +use Kanboard\Core\Security\Role; /** - * Project permission model + * Project Permission * * @package model * @author Frederic Guillot */ 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); @@ -132,10 +28,10 @@ class ProjectPermission extends Base return $this ->db - ->table(self::TABLE) + ->table(ProjectUserRole::TABLE) ->join(User::TABLE, 'id', 'user_id') ->join(Project::TABLE, 'id', 'project_id') - ->eq(self::TABLE.'.is_owner', $is_owner) + ->eq(ProjectUserRole::TABLE.'.role', $role) ->eq(Project::TABLE.'.is_private', 0) ->in(Project::TABLE.'.id', $project_ids) ->columns( @@ -148,169 +44,22 @@ class ProjectPermission extends Base } /** - * Get a list of people associated to the project + * Get all usernames (fetch users from groups) * * @access public - * @param integer $project_id Project id + * @param integer $project_id + * @param string $input * @return array */ - public function getAssociatedUsers($project_id) + public function findUsernames($project_id, $input) { - $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(); + $userMembers = $this->projectUserRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username'); + $groupMembers = $this->projectGroupRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username'); + $members = array_unique(array_merge($userMembers, $groupMembers)); - return $this->user->prepareList($users); - } + sort($members); - /** - * 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 $members; } /** @@ -330,172 +79,72 @@ 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 - */ - public function getAllowedProjects($user_id) - { - if ($this->user->isAdmin($user_id)) { - return $this->project->getListByStatus(Project::ACTIVE); - } - - 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 a list of active project ids where the user is member - * - * @access public - * @param integer $user_id User id - * @return array - */ - public function getActiveMemberProjectIds($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 a list of active projects where the user is member - * - * @access public - * @param integer $user_id User id - * @return array - */ - public function getActiveMemberProjects($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'); - } - - /** - * Copy user access from a project to another one - * - * @param integer $project_src Project Template - * @return integer $project_dst Project that receives the copy + * @param integer $project_id + * @param integer $user_id * @return boolean */ - public function duplicate($project_src, $project_dst) + public function isUserAllowed($project_id, $user_id) { - $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; - } + if ($this->userSession->isAdmin()) { + return true; } - 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) - { - $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() + return in_array( + $this->projectUserRole->getUserRole($project_id, $user_id), + array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER, Role::PROJECT_VIEWER) ); } /** - * Validate allow everybody + * Return true if the user is assignable * * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors + * @param integer $project_id + * @param integer $user_id + * @return boolean */ - public function validateProjectModification(array $values) + public function isAssignable($project_id, $user_id) { - $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 in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); + } - return array( - $v->execute(), - $v->getErrors() - ); + /** + * Return true if the user is member + * + * @access public + * @param integer $project_id + * @param integer $user_id + * @return boolean + */ + public function isMember($project_id, $user_id) + { + return in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER, Role::PROJECT_VIEWER)); + } + + /** + * Get active project ids by user + * + * @access public + * @param integer $user_id + * @return array + */ + public function getActiveProjectIds($user_id) + { + return array_keys($this->projectUserRole->getActiveProjectsByUser($user_id)); + } + + /** + * Copy permissions to another project + * + * @param integer $project_src_id Project Template + * @param integer $project_dst_id Project that receives the copy + * @return boolean + */ + public function duplicate($project_src_id, $project_dst_id) + { + return $this->projectUserRole->duplicate($project_src_id, $project_dst_id) && + $this->projectGroupRole->duplicate($project_src_id, $project_dst_id); } } diff --git a/sources/app/Model/ProjectUserRole.php b/sources/app/Model/ProjectUserRole.php new file mode 100644 index 0000000..8149a25 --- /dev/null +++ b/sources/app/Model/ProjectUserRole.php @@ -0,0 +1,275 @@ +getProjectsByUser($user_id, array(Project::ACTIVE)); + } + + /** + * Get the list of project visible for 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); + $projects = $userProjects + $groupProjects; + + asort($projects); + + return $projects; + } + + /** + * 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/sources/app/Model/ProjectUserRoleFilter.php b/sources/app/Model/ProjectUserRoleFilter.php new file mode 100644 index 0000000..6440364 --- /dev/null +++ b/sources/app/Model/ProjectUserRoleFilter.php @@ -0,0 +1,88 @@ +query = $this->db->table(ProjectUserRole::TABLE); + return $this; + } + + /** + * Get all results of the filter + * + * @access public + * @param string $column + * @return array + */ + public function findAll($column = '') + { + if ($column !== '') { + return $this->query->asc($column)->findAllByColumn($column); + } + + return $this->query->findAll(); + } + + /** + * Get the PicoDb query + * + * @access public + * @return \PicoDb\Table + */ + public function getQuery() + { + return $this->query; + } + + /** + * Filter by project id + * + * @access public + * @param integer $project_id + * @return ProjectUserRoleFilter + */ + public function filterByProjectId($project_id) + { + $this->query->eq(ProjectUserRole::TABLE.'.project_id', $project_id); + return $this; + } + + /** + * Filter by username + * + * @access public + * @param string $input + * @return ProjectUserRoleFilter + */ + public function startWithUsername($input) + { + $this->query + ->join(User::TABLE, 'id', 'user_id') + ->ilike(User::TABLE.'.username', $input.'%'); + + return $this; + } +} diff --git a/sources/app/Model/RememberMeSession.php b/sources/app/Model/RememberMeSession.php new file mode 100644 index 0000000..8989a6d --- /dev/null +++ b/sources/app/Model/RememberMeSession.php @@ -0,0 +1,151 @@ +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/sources/app/Model/Setting.php b/sources/app/Model/Setting.php index 3507d42..44e6c06 100644 --- a/sources/app/Model/Setting.php +++ b/sources/app/Model/Setting.php @@ -47,10 +47,12 @@ abstract class Setting extends Base */ public function getOption($name, $default = '') { - return $this->db + $value = $this->db ->table(self::TABLE) ->eq('option', $name) - ->findOneColumn('value') ?: $default; + ->findOneColumn('value'); + + return $value === null || $value === false || $value === '' ? $default : $value; } /** diff --git a/sources/app/Model/Subtask.php b/sources/app/Model/Subtask.php index 664e41e..0e039bb 100644 --- a/sources/app/Model/Subtask.php +++ b/sources/app/Model/Subtask.php @@ -4,11 +4,9 @@ namespace Kanboard\Model; use PicoDb\Database; use Kanboard\Event\SubtaskEvent; -use SimpleValidator\Validator; -use SimpleValidator\Validators; /** - * Subtask model + * Subtask Model * * @package model * @author Frederic Guillot @@ -451,90 +449,4 @@ class Subtask extends Base } }); } - - /** - * Validate creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('task_id', t('The task id is required')), - new Validators\Required('title', t('The title is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The subtask id is required')), - new Validators\Required('task_id', t('The task id is required')), - new Validators\Required('title', t('The title is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate API modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateApiModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The subtask id is required')), - new Validators\Required('task_id', t('The task id is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('The subtask id must be an integer')), - new Validators\Integer('task_id', t('The task id must be an integer')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 255), 255), - new Validators\Integer('user_id', t('The user id must be an integer')), - new Validators\Integer('status', t('The status must be an integer')), - new Validators\Numeric('time_estimated', t('The time must be a numeric value')), - new Validators\Numeric('time_spent', t('The time must be a numeric value')), - ); - } } diff --git a/sources/app/Model/Swimlane.php b/sources/app/Model/Swimlane.php index df44985..e5124e8 100644 --- a/sources/app/Model/Swimlane.php +++ b/sources/app/Model/Swimlane.php @@ -2,9 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; - /** * Swimlanes * @@ -470,85 +467,4 @@ class Swimlane extends Base return true; } - - /** - * Validate creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('project_id', t('The project id is required')), - new Validators\Required('name', t('The name is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The id is required')), - new Validators\Required('name', t('The name is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate default swimlane modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateDefaultModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The id is required')), - new Validators\Required('default_swimlane', t('The name is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('The id must be an integer')), - new Validators\Integer('project_id', t('The project id must be an integer')), - new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) - ); - } } diff --git a/sources/app/Model/Task.php b/sources/app/Model/Task.php index f1cd094..7aa9e31 100644 --- a/sources/app/Model/Task.php +++ b/sources/app/Model/Task.php @@ -41,6 +41,7 @@ class Task extends Base const EVENT_CREATE_UPDATE = 'task.create_update'; const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change'; const EVENT_OVERDUE = 'task.overdue'; + const EVENT_USER_MENTION = 'task.user.mention'; /** * Recurrence: status diff --git a/sources/app/Model/TaskCreation.php b/sources/app/Model/TaskCreation.php index 5ef1a04..975cc7a 100644 --- a/sources/app/Model/TaskCreation.php +++ b/sources/app/Model/TaskCreation.php @@ -86,8 +86,16 @@ class TaskCreation extends Base */ private function fireEvents($task_id, array $values) { - $values['task_id'] = $task_id; - $this->container['dispatcher']->dispatch(Task::EVENT_CREATE_UPDATE, new TaskEvent($values)); - $this->container['dispatcher']->dispatch(Task::EVENT_CREATE, new TaskEvent($values)); + $event = new TaskEvent(array('task_id' => $task_id) + $values); + + $this->logger->debug('Event fired: '.Task::EVENT_CREATE_UPDATE); + $this->logger->debug('Event fired: '.Task::EVENT_CREATE); + + $this->dispatcher->dispatch(Task::EVENT_CREATE_UPDATE, $event); + $this->dispatcher->dispatch(Task::EVENT_CREATE, $event); + + if (! empty($values['description'])) { + $this->userMention->fireEvents($values['description'], Task::EVENT_USER_MENTION, $event); + } } } diff --git a/sources/app/Model/TaskFilter.php b/sources/app/Model/TaskFilter.php index 137a7a8..7ceb4a9 100644 --- a/sources/app/Model/TaskFilter.php +++ b/sources/app/Model/TaskFilter.php @@ -30,6 +30,7 @@ class TaskFilter extends Base 'T_COLUMN' => 'filterByColumnName', 'T_REFERENCE' => 'filterByReference', 'T_SWIMLANE' => 'filterBySwimlaneName', + 'T_LINK' => 'filterByLinkName', ); /** @@ -107,6 +108,22 @@ class TaskFilter extends Base ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE); } + /** + * Create a new link query + * + * @access public + * @return \PicoDb\Table + */ + public function createLinkQuery() + { + return $this->db->table(TaskLink::TABLE) + ->columns( + TaskLink::TABLE.'.task_id', + Link::TABLE.'.label' + ) + ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE); + } + /** * Clone the filter * @@ -506,6 +523,30 @@ class TaskFilter extends Base return $this; } + /** + * Filter by link + * + * @access public + * @param array $values List of links + * @return TaskFilter + */ + public function filterByLinkName(array $values) + { + $this->query->beginOr(); + + $link_query = $this->createLinkQuery()->in(Link::TABLE.'.label', $values); + $matching_task_ids = $link_query->findAllByColumn('task_id'); + if (empty($matching_task_ids)) { + $this->query->eq(Task::TABLE.'.id', 0); + } else { + $this->query->in(Task::TABLE.'.id', $matching_task_ids); + } + + $this->query->closeOr(); + + return $this; + } + /** * Filter by due date * diff --git a/sources/app/Model/TaskFinder.php b/sources/app/Model/TaskFinder.php index 9514fe4..836fbe4 100644 --- a/sources/app/Model/TaskFinder.php +++ b/sources/app/Model/TaskFinder.php @@ -122,6 +122,7 @@ class TaskFinder extends Base 'tasks.recurrence_parent', 'tasks.recurrence_child', 'tasks.time_estimated', + 'tasks.time_spent', User::TABLE.'.username AS assignee_username', User::TABLE.'.name AS assignee_name', Category::TABLE.'.name AS category_name', diff --git a/sources/app/Model/TaskLink.php b/sources/app/Model/TaskLink.php index 1ac5920..87aae55 100644 --- a/sources/app/Model/TaskLink.php +++ b/sources/app/Model/TaskLink.php @@ -2,8 +2,6 @@ namespace Kanboard\Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; use Kanboard\Event\TaskLinkEvent; /** @@ -261,59 +259,4 @@ class TaskLink extends Base return true; } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Required('task_id', t('Field required')), - new Validators\Required('opposite_task_id', t('Field required')), - new Validators\Required('link_id', t('Field required')), - new Validators\NotEquals('opposite_task_id', 'task_id', t('A task cannot be linked to itself')), - new Validators\Exists('opposite_task_id', t('This linked task id doesn\'t exists'), $this->db->getConnection(), Task::TABLE, 'id') - ); - } - - /** - * Validate creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $v = new Validator($values, $this->commonValidationRules()); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('Field required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } } diff --git a/sources/app/Model/TaskModification.php b/sources/app/Model/TaskModification.php index 781646b..a0ad292 100644 --- a/sources/app/Model/TaskModification.php +++ b/sources/app/Model/TaskModification.php @@ -17,16 +17,17 @@ class TaskModification extends Base * * @access public * @param array $values + * @param boolean $fire_events * @return boolean */ - public function update(array $values) + public function update(array $values, $fire_events = true) { $original_task = $this->taskFinder->getById($values['id']); $this->prepare($values); $result = $this->db->table(Task::TABLE)->eq('id', $original_task['id'])->update($values); - if ($result) { + if ($fire_events && $result) { $this->fireEvents($original_task, $values); } @@ -51,13 +52,14 @@ class TaskModification extends Base if ($this->isFieldModified('owner_id', $event_data['changes'])) { $events[] = Task::EVENT_ASSIGNEE_CHANGE; - } else { + } elseif (! empty($event_data['changes'])) { $events[] = Task::EVENT_CREATE_UPDATE; $events[] = Task::EVENT_UPDATE; } foreach ($events as $event) { - $this->container['dispatcher']->dispatch($event, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.$event); + $this->dispatcher->dispatch($event, new TaskEvent($event_data)); } } diff --git a/sources/app/Model/TaskPermission.php b/sources/app/Model/TaskPermission.php index 4bbe6d1..fac2153 100644 --- a/sources/app/Model/TaskPermission.php +++ b/sources/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/sources/app/Model/TaskPosition.php b/sources/app/Model/TaskPosition.php index da363cb..4c9928d 100644 --- a/sources/app/Model/TaskPosition.php +++ b/sources/app/Model/TaskPosition.php @@ -32,7 +32,6 @@ class TaskPosition extends Base $task = $this->taskFinder->getById($task_id); - // Ignore closed tasks if ($task['is_active'] == Task::STATUS_CLOSED) { return true; } @@ -167,7 +166,12 @@ class TaskPosition extends Base return false; } - return true; + $now = time(); + + return $this->db->table(Task::TABLE)->eq('id', $task_id)->update(array( + 'date_moved' => $now, + 'date_modification' => $now, + )); } /** @@ -221,11 +225,14 @@ class TaskPosition extends Base ); if ($task['swimlane_id'] != $new_swimlane_id) { - $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.Task::EVENT_MOVE_SWIMLANE); + $this->dispatcher->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); } elseif ($task['column_id'] != $new_column_id) { - $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.Task::EVENT_MOVE_COLUMN); + $this->dispatcher->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); } elseif ($task['position'] != $new_position) { - $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.Task::EVENT_MOVE_POSITION); + $this->dispatcher->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data)); } } } diff --git a/sources/app/Model/TaskStatus.php b/sources/app/Model/TaskStatus.php index a5199ed..2b90281 100644 --- a/sources/app/Model/TaskStatus.php +++ b/sources/app/Model/TaskStatus.php @@ -61,6 +61,32 @@ class TaskStatus extends Base return $this->changeStatus($task_id, Task::STATUS_OPEN, 0, Task::EVENT_OPEN); } + /** + * Close multiple tasks + * + * @access public + * @param array $task_ids + */ + public function closeMultipleTasks(array $task_ids) + { + foreach ($task_ids as $task_id) { + $this->close($task_id); + } + } + + /** + * Close all tasks within a column/swimlane + * + * @access public + * @param integer $swimlane_id + * @param integer $column_id + */ + public function closeTasksBySwimlaneAndColumn($swimlane_id, $column_id) + { + $task_ids = $this->db->table(Task::TABLE)->eq('swimlane_id', $swimlane_id)->eq('column_id', $column_id)->findAllByColumn('id'); + $this->closeMultipleTasks($task_ids); + } + /** * Common method to change the status of task * @@ -87,10 +113,8 @@ class TaskStatus extends Base )); if ($result) { - $this->container['dispatcher']->dispatch( - $event, - new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id)) - ); + $this->logger->debug('Event fired: '.$event); + $this->dispatcher->dispatch($event, new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id))); } return $result; diff --git a/sources/app/Model/User.php b/sources/app/Model/User.php index 88361ce..ac0e749 100644 --- a/sources/app/Model/User.php +++ b/sources/app/Model/User.php @@ -3,10 +3,8 @@ namespace Kanboard\Model; use PicoDb\Database; -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 +55,7 @@ class User extends Base 'username', 'name', 'email', - 'is_admin', - 'is_project_admin', + 'role', 'is_ldap_user', 'notifications_enabled', 'google_id', @@ -91,7 +88,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 +108,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 +255,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')); } @@ -320,7 +286,7 @@ class User extends Base $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); // If the user is connected refresh his session - if (SessionManager::isOpen() && $this->userSession->getId() == $values['id']) { + if ($this->userSession->getId() == $values['id']) { $this->userSession->initialize($this->getById($this->userSession->getId())); } @@ -355,10 +321,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(); @@ -400,200 +366,4 @@ class User extends Base ->eq('id', $user_id) ->save(array('token' => '')); } - - /** - * 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 - * @return array - */ - private function commonValidationRules() - { - return array( - 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')), - ); - } - - /** - * Common password validation rules - * - * @access private - * @return array - */ - private function commonPasswordValidationRules() - { - return array( - new Validators\Required('password', t('The password is required')), - new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6), - new Validators\Required('confirmation', t('The confirmation is required')), - new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')), - ); - } - - /** - * Validate user creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('username', t('The username is required')), - ); - - if (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1) { - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - } else { - $v = new Validator($values, array_merge($rules, $this->commonValidationRules(), $this->commonPasswordValidationRules())); - } - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate user modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The user id is required')), - new Validators\Required('username', t('The username is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate user API modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateApiModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The user id is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate password modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validatePasswordModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The user id is required')), - new Validators\Required('current_password', t('The current password is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonPasswordValidationRules())); - - if ($v->execute()) { - - // Check password - if ($this->authentication->authenticate($this->userSession->getUsername(), $values['current_password'])) { - return array(true, array()); - } else { - return array(false, array('current_password' => array(t('Wrong password')))); - } - } - - return array(false, $v->getErrors()); - } } diff --git a/sources/app/Model/UserFilter.php b/sources/app/Model/UserFilter.php new file mode 100644 index 0000000..ff546e9 --- /dev/null +++ b/sources/app/Model/UserFilter.php @@ -0,0 +1,80 @@ +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/sources/app/Model/UserImport.php b/sources/app/Model/UserImport.php index 3c9e7a5..0ec4e80 100644 --- a/sources/app/Model/UserImport.php +++ b/sources/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/sources/app/Model/UserLocking.php b/sources/app/Model/UserLocking.php new file mode 100644 index 0000000..67e4c24 --- /dev/null +++ b/sources/app/Model/UserLocking.php @@ -0,0 +1,103 @@ +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/sources/app/Model/UserMention.php b/sources/app/Model/UserMention.php new file mode 100644 index 0000000..97a4e41 --- /dev/null +++ b/sources/app/Model/UserMention.php @@ -0,0 +1,61 @@ +db->table(User::TABLE) + ->columns('id', 'username', 'name', 'email', 'language') + ->eq('notifications_enabled', 1) + ->neq('id', $this->userSession->getId()) + ->in('username', array_unique($matches[1])) + ->findAll(); + } + + return $users; + } + + /** + * Fire events for user mentions + * + * @access public + * @param string $content + * @param string $eventName + * @param GenericEvent $event + */ + public function fireEvents($content, $eventName, GenericEvent $event) + { + if (empty($event['project_id'])) { + $event['project_id'] = $this->taskFinder->getProjectId($event['task_id']); + } + + $users = $this->getMentionedUsers($content); + + foreach ($users as $user) { + if ($this->projectPermission->isMember($event['project_id'], $user['id'])) { + $event['mention'] = $user; + $this->dispatcher->dispatch($eventName, $event); + } + } + } +} diff --git a/sources/app/Model/UserNotification.php b/sources/app/Model/UserNotification.php index 3d98ebe..e8a967a 100644 --- a/sources/app/Model/UserNotification.php +++ b/sources/app/Model/UserNotification.php @@ -21,18 +21,12 @@ class UserNotification extends Base */ public function sendNotifications($event_name, array $event_data) { - $logged_user_id = $this->userSession->isLogged() ? $this->userSession->getId() : 0; - $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $logged_user_id); + $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $this->userSession->getId()); - if (! empty($users)) { - foreach ($users as $user) { - if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) { - $this->sendUserNotification($user, $event_name, $event_data); - } + foreach ($users as $user) { + if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) { + $this->sendUserNotification($user, $event_name, $event_data); } - - // Restore locales - $this->config->setupTranslations(); } } @@ -58,6 +52,9 @@ class UserNotification extends Base foreach ($this->userNotificationType->getSelectedTypes($user['id']) as $type) { $this->userNotificationType->getType($type)->notifyUser($user, $event_name, $event_data); } + + // Restore locales + $this->config->setupTranslations(); } /** @@ -74,7 +71,17 @@ class UserNotification extends Base return $this->getEverybodyWithNotificationEnabled($exclude_user_id); } - return $this->getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id); + $users = array(); + $members = $this->getProjectUserMembersWithNotificationEnabled($project_id, $exclude_user_id); + $groups = $this->getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id); + + foreach (array_merge($members, $groups) as $user) { + if (! isset($users[$user['id']])) { + $users[$user['id']] = $user; + } + } + + return array_values($users); } /** @@ -145,17 +152,17 @@ class UserNotification extends Base } /** - * Get a list of project members with notification enabled + * Get a list of group members with notification enabled * * @access private * @param integer $project_id Project id * @param integer $exclude_user_id User id to exclude * @return array */ - private function getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id) + private function getProjectUserMembersWithNotificationEnabled($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) @@ -164,6 +171,19 @@ class UserNotification extends Base ->findAll(); } + private function getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id) + { + return $this->db + ->table(ProjectGroupRole::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') + ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE) + ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) + ->eq(ProjectGroupRole::TABLE.'.project_id', $project_id) + ->eq(User::TABLE.'.notifications_enabled', '1') + ->neq(User::TABLE.'.id', $exclude_user_id) + ->findAll(); + } + /** * Get a list of project members with notification enabled * diff --git a/sources/app/Notification/Mail.php b/sources/app/Notification/Mail.php index 98bffef..d05dbdf 100644 --- a/sources/app/Notification/Mail.php +++ b/sources/app/Notification/Mail.php @@ -121,6 +121,10 @@ class Mail extends Base implements NotificationInterface case Task::EVENT_ASSIGNEE_CHANGE: $subject = $this->getStandardMailSubject(e('Assignee change'), $event_data); break; + case Task::EVENT_USER_MENTION: + case Comment::EVENT_USER_MENTION: + $subject = $this->getStandardMailSubject(e('Mentioned'), $event_data); + break; case Task::EVENT_OVERDUE: $subject = e('[%s] Overdue tasks', $event_data['project_name']); break; diff --git a/sources/app/Schema/Mysql.php b/sources/app/Schema/Mysql.php index 52a73fb..c98e083 100644 --- a/sources/app/Schema/Mysql.php +++ b/sources/app/Schema/Mysql.php @@ -4,8 +4,140 @@ namespace Schema; use PDO; use Kanboard\Core\Security\Token; +use Kanboard\Core\Security\Role; -const VERSION = 94; +const VERSION = 101; + +function version_101(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE password_reset ( + token VARCHAR(80) PRIMARY KEY, + user_id INT NOT NULL, + date_expiration INT NOT NULL, + date_creation INT NOT NULL, + ip VARCHAR(45) NOT NULL, + user_agent VARCHAR(255) NOT NULL, + is_active TINYINT(1) NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')"); +} + +function version_100(PDO $pdo) +{ + $pdo->exec('ALTER TABLE `actions` MODIFY `action_name` VARCHAR(255)'); +} + +function version_99(PDO $pdo) +{ + $rq = $pdo->prepare('SELECT * FROM actions'); + $rq->execute(); + $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array(); + + $rq = $pdo->prepare('UPDATE actions SET action_name=? WHERE id=?'); + + foreach ($rows as $row) { + if ($row['action_name'] === 'TaskAssignCurrentUser' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskAssignCurrentUserColumn'; + } elseif ($row['action_name'] === 'TaskClose' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskCloseColumn'; + } elseif ($row['action_name'] === 'TaskLogMoveAnotherColumn') { + $row['action_name'] = '\Kanboard\Action\CommentCreationMoveTaskColumn'; + } elseif ($row['action_name']{0} !== '\\') { + $row['action_name'] = '\Kanboard\Action\\'.$row['action_name']; + } + + $rq->execute(array($row['action_name'], $row['id'])); + } +} + +function version_98(PDO $pdo) +{ + $pdo->exec('ALTER TABLE `users` MODIFY `language` VARCHAR(5)'); +} + +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) +{ + $pdo->exec(" + CREATE TABLE groups ( + id INT NOT NULL AUTO_INCREMENT, + external_id VARCHAR(255) DEFAULT '', + name VARCHAR(100) NOT NULL UNIQUE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE group_has_users ( + group_id INT NOT NULL, + user_id INT NOT NULL, + FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(group_id, user_id) + ) ENGINE=InnoDB CHARSET=utf8 + "); +} function version_94(PDO $pdo) { @@ -949,7 +1081,7 @@ function version_12(PDO $pdo) CREATE TABLE remember_me ( id INT NOT NULL AUTO_INCREMENT, user_id INT, - ip VARCHAR(40), + ip VARCHAR(45), user_agent VARCHAR(255), token VARCHAR(255), sequence VARCHAR(255), @@ -965,7 +1097,7 @@ function version_12(PDO $pdo) id INT NOT NULL AUTO_INCREMENT, auth_type VARCHAR(25), user_id INT, - ip VARCHAR(40), + ip VARCHAR(45), user_agent VARCHAR(255), date_creation INT, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, diff --git a/sources/app/Schema/Postgres.php b/sources/app/Schema/Postgres.php index 5cd1a7d..961d8f4 100644 --- a/sources/app/Schema/Postgres.php +++ b/sources/app/Schema/Postgres.php @@ -4,8 +4,139 @@ namespace Schema; use PDO; use Kanboard\Core\Security\Token; +use Kanboard\Core\Security\Role; -const VERSION = 74; +const VERSION = 81; + +function version_81(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE password_reset ( + token VARCHAR(80) PRIMARY KEY, + user_id INTEGER NOT NULL, + date_expiration INTEGER NOT NULL, + date_creation INTEGER NOT NULL, + ip VARCHAR(45) NOT NULL, + user_agent VARCHAR(255) NOT NULL, + is_active BOOLEAN NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + "); + + $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')"); +} + +function version_80(PDO $pdo) +{ + $pdo->exec('ALTER TABLE "actions" ALTER COLUMN "action_name" TYPE VARCHAR(255)'); +} + +function version_79(PDO $pdo) +{ + $rq = $pdo->prepare('SELECT * FROM actions'); + $rq->execute(); + $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array(); + + $rq = $pdo->prepare('UPDATE actions SET action_name=? WHERE id=?'); + + foreach ($rows as $row) { + if ($row['action_name'] === 'TaskAssignCurrentUser' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskAssignCurrentUserColumn'; + } elseif ($row['action_name'] === 'TaskClose' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskCloseColumn'; + } elseif ($row['action_name'] === 'TaskLogMoveAnotherColumn') { + $row['action_name'] = '\Kanboard\Action\CommentCreationMoveTaskColumn'; + } elseif ($row['action_name']{0} !== '\\') { + $row['action_name'] = '\Kanboard\Action\\'.$row['action_name']; + } + + $rq->execute(array($row['action_name'], $row['id'])); + } +} + +function version_78(PDO $pdo) +{ + $pdo->exec('ALTER TABLE "users" ALTER COLUMN "language" TYPE VARCHAR(5)'); +} + +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) +{ + $pdo->exec(" + CREATE TABLE groups ( + id SERIAL PRIMARY KEY, + external_id VARCHAR(255) DEFAULT '', + name VARCHAR(100) NOT NULL UNIQUE + ) + "); + + $pdo->exec(" + CREATE TABLE group_has_users ( + group_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(group_id, user_id) + ) + "); +} function version_74(PDO $pdo) { @@ -870,7 +1001,7 @@ function version_1(PDO $pdo) CREATE TABLE remember_me ( id SERIAL PRIMARY KEY, user_id INTEGER, - ip VARCHAR(40), + ip VARCHAR(45), user_agent VARCHAR(255), token VARCHAR(255), sequence VARCHAR(255), @@ -883,7 +1014,7 @@ function version_1(PDO $pdo) id SERIAL PRIMARY KEY, auth_type VARCHAR(25), user_id INTEGER, - ip VARCHAR(40), + ip VARCHAR(45), user_agent VARCHAR(255), date_creation INTEGER, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE diff --git a/sources/app/Schema/Sqlite.php b/sources/app/Schema/Sqlite.php index fa26b15..f1be0cf 100644 --- a/sources/app/Schema/Sqlite.php +++ b/sources/app/Schema/Sqlite.php @@ -3,9 +3,124 @@ namespace Schema; use Kanboard\Core\Security\Token; +use Kanboard\Core\Security\Role; use PDO; -const VERSION = 88; +const VERSION = 93; + +function version_93(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE password_reset ( + token TEXT PRIMARY KEY, + user_id INTEGER NOT NULL, + date_expiration INTEGER NOT NULL, + date_creation INTEGER NOT NULL, + ip TEXT NOT NULL, + user_agent TEXT NOT NULL, + is_active INTEGER NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + ) + "); + + $pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')"); +} + +function version_92(PDO $pdo) +{ + $rq = $pdo->prepare('SELECT * FROM actions'); + $rq->execute(); + $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array(); + + $rq = $pdo->prepare('UPDATE actions SET action_name=? WHERE id=?'); + + foreach ($rows as $row) { + if ($row['action_name'] === 'TaskAssignCurrentUser' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskAssignCurrentUserColumn'; + } elseif ($row['action_name'] === 'TaskClose' && $row['event_name'] === 'task.move.column') { + $row['action_name'] = '\Kanboard\Action\TaskCloseColumn'; + } elseif ($row['action_name'] === 'TaskLogMoveAnotherColumn') { + $row['action_name'] = '\Kanboard\Action\CommentCreationMoveTaskColumn'; + } elseif ($row['action_name']{0} !== '\\') { + $row['action_name'] = '\Kanboard\Action\\'.$row['action_name']; + } + + $rq->execute(array($row['action_name'], $row['id'])); + } +} + +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) +{ + $pdo->exec(" + CREATE TABLE project_has_groups ( + group_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + role TEXT 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 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) +{ + $pdo->exec(" + CREATE TABLE groups ( + id INTEGER PRIMARY KEY, + external_id TEXT DEFAULT '', + name TEXT NOCASE NOT NULL UNIQUE + ) + "); + + $pdo->exec(" + CREATE TABLE group_has_users ( + group_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(group_id, user_id) + ) + "); +} function version_88(PDO $pdo) { @@ -969,7 +1084,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/sources/app/ServiceProvider/ActionProvider.php b/sources/app/ServiceProvider/ActionProvider.php new file mode 100644 index 0000000..0aba29f --- /dev/null +++ b/sources/app/ServiceProvider/ActionProvider.php @@ -0,0 +1,78 @@ +register(new CommentCreation($container)); + $container['actionManager']->register(new CommentCreationMoveTaskColumn($container)); + $container['actionManager']->register(new TaskAssignCategoryColor($container)); + $container['actionManager']->register(new TaskAssignCategoryLabel($container)); + $container['actionManager']->register(new TaskAssignCategoryLink($container)); + $container['actionManager']->register(new TaskAssignColorCategory($container)); + $container['actionManager']->register(new TaskAssignColorColumn($container)); + $container['actionManager']->register(new TaskAssignColorLink($container)); + $container['actionManager']->register(new TaskAssignColorUser($container)); + $container['actionManager']->register(new TaskAssignCurrentUser($container)); + $container['actionManager']->register(new TaskAssignCurrentUserColumn($container)); + $container['actionManager']->register(new TaskAssignSpecificUser($container)); + $container['actionManager']->register(new TaskAssignUser($container)); + $container['actionManager']->register(new TaskClose($container)); + $container['actionManager']->register(new TaskCloseColumn($container)); + $container['actionManager']->register(new TaskCreation($container)); + $container['actionManager']->register(new TaskDuplicateAnotherProject($container)); + $container['actionManager']->register(new TaskEmail($container)); + $container['actionManager']->register(new TaskMoveAnotherProject($container)); + $container['actionManager']->register(new TaskMoveColumnAssigned($container)); + $container['actionManager']->register(new TaskMoveColumnCategoryChange($container)); + $container['actionManager']->register(new TaskMoveColumnUnAssigned($container)); + $container['actionManager']->register(new TaskOpen($container)); + $container['actionManager']->register(new TaskUpdateStartDate($container)); + + return $container; + } +} diff --git a/sources/app/ServiceProvider/AuthenticationProvider.php b/sources/app/ServiceProvider/AuthenticationProvider.php new file mode 100644 index 0000000..7617ba9 --- /dev/null +++ b/sources/app/ServiceProvider/AuthenticationProvider.php @@ -0,0 +1,152 @@ +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); + $acl->add('UserHelper', array('mention'), 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'), Role::APP_PUBLIC); + $acl->add('Captcha', '*', Role::APP_PUBLIC); + $acl->add('PasswordReset', '*', 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', array('projects', 'saveProjectDate'), 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_USER); + $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/sources/app/ServiceProvider/ClassProvider.php b/sources/app/ServiceProvider/ClassProvider.php index 9c9bc23..df4e183 100644 --- a/sources/app/ServiceProvider/ClassProvider.php +++ b/sources/app/ServiceProvider/ClassProvider.php @@ -4,26 +4,26 @@ 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( + 'Analytic' => array( + 'TaskDistributionAnalytic', + 'UserDistributionAnalytic', + 'EstimatedTimeComparisonAnalytic', + 'AverageLeadCycleTimeAnalytic', + 'AverageTimeSpentColumnAnalytic', + ), 'Model' => array( - 'Acl', 'Action', - 'Authentication', + 'ActionParameter', 'Board', 'Category', 'Color', @@ -32,19 +32,26 @@ class ClassProvider implements ServiceProviderInterface 'Currency', 'CustomFilter', 'File', + 'Group', + 'GroupMember', 'LastLogin', 'Link', 'Notification', 'OverdueNotification', + 'PasswordReset', 'Project', 'ProjectActivity', - 'ProjectAnalytic', 'ProjectDuplication', 'ProjectDailyColumnStats', 'ProjectDailyStats', 'ProjectPermission', 'ProjectNotification', 'ProjectMetadata', + 'ProjectGroupRole', + 'ProjectGroupRoleFilter', + 'ProjectUserRole', + 'ProjectUserRoleFilter', + 'RememberMeSession', 'Subtask', 'SubtaskExport', 'SubtaskTimeTracking', @@ -61,15 +68,14 @@ class ClassProvider implements ServiceProviderInterface 'TaskPermission', 'TaskPosition', 'TaskStatus', - 'TaskValidator', 'TaskImport', 'TaskMetadata', 'Transition', 'User', 'UserImport', - 'UserSession', + 'UserLocking', + 'UserMention', 'UserNotification', - 'UserNotificationType', 'UserNotificationFilter', 'UserUnreadNotification', 'UserMetadata', @@ -80,6 +86,26 @@ class ClassProvider implements ServiceProviderInterface 'TaskFilterCalendarFormatter', 'TaskFilterICalendarFormatter', 'ProjectGanttFormatter', + 'UserFilterAutoCompleteFormatter', + 'GroupAutoCompleteFormatter', + ), + 'Validator' => array( + 'ActionValidator', + 'AuthValidator', + 'CategoryValidator', + 'ColumnValidator', + 'CommentValidator', + 'CurrencyValidator', + 'CustomFilterValidator', + 'GroupValidator', + 'LinkValidator', + 'PasswordResetValidator', + 'ProjectValidator', + 'SubtaskValidator', + 'SwimlaneValidator', + 'TaskValidator', + 'TaskLinkValidator', + 'UserValidator', ), 'Core' => array( 'DateParser', @@ -87,10 +113,13 @@ class ClassProvider implements ServiceProviderInterface 'Lexer', 'Template', ), + 'Core\Event' => array( + 'EventManager', + ), 'Core\Http' => array( 'Request', 'Response', - 'Router', + 'RememberMeCookie', ), 'Core\Cache' => array( 'MemoryCache', @@ -100,11 +129,13 @@ class ClassProvider implements ServiceProviderInterface ), 'Core\Security' => array( 'Token', + 'Role', ), - 'Integration' => array( - 'BitbucketWebhook', - 'GithubWebhook', - 'GitlabWebhook', + 'Core\User' => array( + 'GroupSync', + 'UserSync', + 'UserSession', + 'UserProfile', ) ); @@ -124,10 +155,6 @@ class ClassProvider implements ServiceProviderInterface return new HttpClient($c); }; - $container['htmlConverter'] = function () { - return new HtmlConverter(array('strip_tags' => true)); - }; - $container['objectStorage'] = function () { return new FileStorage(FILES_DIR); }; @@ -140,23 +167,11 @@ 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:'); + $container['cspRules'] = array( + 'default-src' => "'self'", + 'style-src' => "'self' 'unsafe-inline'", + 'img-src' => '* data:', + ); return $container; } diff --git a/sources/app/ServiceProvider/EventDispatcherProvider.php b/sources/app/ServiceProvider/EventDispatcherProvider.php index 17141fd..880caa4 100644 --- a/sources/app/ServiceProvider/EventDispatcherProvider.php +++ b/sources/app/ServiceProvider/EventDispatcherProvider.php @@ -11,7 +11,6 @@ use Kanboard\Subscriber\NotificationSubscriber; use Kanboard\Subscriber\ProjectDailySummarySubscriber; use Kanboard\Subscriber\ProjectModificationDateSubscriber; use Kanboard\Subscriber\SubtaskTimeTrackingSubscriber; -use Kanboard\Subscriber\TaskMovedDateSubscriber; use Kanboard\Subscriber\TransitionSubscriber; use Kanboard\Subscriber\RecurringTaskSubscriber; @@ -26,13 +25,9 @@ class EventDispatcherProvider implements ServiceProviderInterface $container['dispatcher']->addSubscriber(new ProjectModificationDateSubscriber($container)); $container['dispatcher']->addSubscriber(new NotificationSubscriber($container)); $container['dispatcher']->addSubscriber(new SubtaskTimeTrackingSubscriber($container)); - $container['dispatcher']->addSubscriber(new TaskMovedDateSubscriber($container)); $container['dispatcher']->addSubscriber(new TransitionSubscriber($container)); $container['dispatcher']->addSubscriber(new RecurringTaskSubscriber($container)); - // Automatic actions - $container['action']->attachEvents(); - return $container; } } diff --git a/sources/app/ServiceProvider/GroupProvider.php b/sources/app/ServiceProvider/GroupProvider.php new file mode 100644 index 0000000..dff4b23 --- /dev/null +++ b/sources/app/ServiceProvider/GroupProvider.php @@ -0,0 +1,37 @@ +register(new DatabaseBackendGroupProvider($container)); + + if (LDAP_AUTH && LDAP_GROUP_PROVIDER) { + $container['groupManager']->register(new LdapBackendGroupProvider($container)); + } + + return $container; + } +} diff --git a/sources/app/ServiceProvider/NotificationProvider.php b/sources/app/ServiceProvider/NotificationProvider.php new file mode 100644 index 0000000..83daf65 --- /dev/null +++ b/sources/app/ServiceProvider/NotificationProvider.php @@ -0,0 +1,45 @@ +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/sources/app/ServiceProvider/PluginProvider.php b/sources/app/ServiceProvider/PluginProvider.php new file mode 100644 index 0000000..d2f1666 --- /dev/null +++ b/sources/app/ServiceProvider/PluginProvider.php @@ -0,0 +1,31 @@ +scan(); + + return $container; + } +} diff --git a/sources/app/ServiceProvider/RouteProvider.php b/sources/app/ServiceProvider/RouteProvider.php new file mode 100644 index 0000000..ce66090 --- /dev/null +++ b/sources/app/ServiceProvider/RouteProvider.php @@ -0,0 +1,216 @@ +enable(); + + // Dashboard + $container['route']->addRoute('dashboard', 'app', 'index'); + $container['route']->addRoute('dashboard/:user_id', 'app', 'index'); + $container['route']->addRoute('dashboard/:user_id/projects', 'app', 'projects'); + $container['route']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks'); + $container['route']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks'); + $container['route']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar'); + $container['route']->addRoute('dashboard/:user_id/activity', 'app', 'activity'); + + // Search routes + $container['route']->addRoute('search', 'search', 'index'); + $container['route']->addRoute('search/:search', 'search', 'index'); + + // Project routes + $container['route']->addRoute('projects', 'project', 'index'); + $container['route']->addRoute('project/create', 'project', 'create'); + $container['route']->addRoute('project/create/private', 'project', 'createPrivate'); + $container['route']->addRoute('project/:project_id', 'project', 'show'); + $container['route']->addRoute('p/:project_id', 'project', 'show'); + $container['route']->addRoute('project/:project_id/customer-filter', 'customfilter', 'index'); + $container['route']->addRoute('project/:project_id/share', 'project', 'share'); + $container['route']->addRoute('project/:project_id/notifications', 'project', 'notifications'); + $container['route']->addRoute('project/:project_id/edit', 'project', 'edit'); + $container['route']->addRoute('project/:project_id/integrations', 'project', 'integrations'); + $container['route']->addRoute('project/:project_id/duplicate', 'project', 'duplicate'); + $container['route']->addRoute('project/:project_id/remove', 'project', 'remove'); + $container['route']->addRoute('project/:project_id/disable', 'project', 'disable'); + $container['route']->addRoute('project/:project_id/enable', 'project', 'enable'); + $container['route']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index'); + $container['route']->addRoute('project/:project_id/import', 'taskImport', 'step1'); + + // ProjectUser routes + $container['route']->addRoute('projects/managers/:user_id', 'projectuser', 'managers'); + $container['route']->addRoute('projects/members/:user_id', 'projectuser', 'members'); + $container['route']->addRoute('projects/tasks/:user_id/opens', 'projectuser', 'opens'); + $container['route']->addRoute('projects/tasks/:user_id/closed', 'projectuser', 'closed'); + $container['route']->addRoute('projects/managers', 'projectuser', 'managers'); + + // Action routes + $container['route']->addRoute('project/:project_id/actions', 'action', 'index'); + $container['route']->addRoute('project/:project_id/action/:action_id/confirm', 'action', 'confirm'); + + // Column routes + $container['route']->addRoute('project/:project_id/columns', 'column', 'index'); + $container['route']->addRoute('project/:project_id/column/:column_id/edit', 'column', 'edit'); + $container['route']->addRoute('project/:project_id/column/:column_id/confirm', 'column', 'confirm'); + $container['route']->addRoute('project/:project_id/column/:column_id/move/:direction', 'column', 'move'); + + // Swimlane routes + $container['route']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/edit', 'swimlane', 'edit'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/confirm', 'swimlane', 'confirm'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/disable', 'swimlane', 'disable'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/enable', 'swimlane', 'enable'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/up', 'swimlane', 'moveup'); + $container['route']->addRoute('project/:project_id/swimlane/:swimlane_id/down', 'swimlane', 'movedown'); + + // Category routes + $container['route']->addRoute('project/:project_id/categories', 'category', 'index'); + $container['route']->addRoute('project/:project_id/category/:category_id/edit', 'category', 'edit'); + $container['route']->addRoute('project/:project_id/category/:category_id/confirm', 'category', 'confirm'); + + // Task routes + $container['route']->addRoute('project/:project_id/task/:task_id', 'task', 'show'); + $container['route']->addRoute('t/:task_id', 'task', 'show'); + $container['route']->addRoute('public/task/:task_id/:token', 'task', 'readonly'); + + $container['route']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task'); + $container['route']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot'); + $container['route']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create'); + $container['route']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create'); + $container['route']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create'); + $container['route']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions'); + $container['route']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics'); + $container['route']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove'); + + $container['route']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit'); + $container['route']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description'); + $container['route']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence'); + + $container['route']->addRoute('project/:project_id/task/:task_id/close', 'taskstatus', 'close'); + $container['route']->addRoute('project/:project_id/task/:task_id/open', 'taskstatus', 'open'); + + $container['route']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate'); + $container['route']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy'); + $container['route']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy'); + $container['route']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move'); + $container['route']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move'); + + // Exports + $container['route']->addRoute('export/tasks/:project_id', 'export', 'tasks'); + $container['route']->addRoute('export/subtasks/:project_id', 'export', 'subtasks'); + $container['route']->addRoute('export/transitions/:project_id', 'export', 'transitions'); + $container['route']->addRoute('export/summary/:project_id', 'export', 'summary'); + + // Board routes + $container['route']->addRoute('board/:project_id', 'board', 'show'); + $container['route']->addRoute('b/:project_id', 'board', 'show'); + $container['route']->addRoute('public/board/:token', 'board', 'readonly'); + + // Calendar routes + $container['route']->addRoute('calendar/:project_id', 'calendar', 'show'); + $container['route']->addRoute('c/:project_id', 'calendar', 'show'); + + // Listing routes + $container['route']->addRoute('list/:project_id', 'listing', 'show'); + $container['route']->addRoute('l/:project_id', 'listing', 'show'); + + // Gantt routes + $container['route']->addRoute('gantt/:project_id', 'gantt', 'project'); + $container['route']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project'); + + // Subtask routes + $container['route']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create'); + $container['route']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm'); + $container['route']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/edit', 'subtask', 'edit'); + + // Feed routes + $container['route']->addRoute('feed/project/:token', 'feed', 'project'); + $container['route']->addRoute('feed/user/:token', 'feed', 'user'); + + // Ical routes + $container['route']->addRoute('ical/project/:token', 'ical', 'project'); + $container['route']->addRoute('ical/user/:token', 'ical', 'user'); + + // Users + $container['route']->addRoute('users', 'user', 'index'); + $container['route']->addRoute('user/profile/:user_id', 'user', 'profile'); + $container['route']->addRoute('user/show/:user_id', 'user', 'show'); + $container['route']->addRoute('user/show/:user_id/timesheet', 'user', 'timesheet'); + $container['route']->addRoute('user/show/:user_id/last-logins', 'user', 'last'); + $container['route']->addRoute('user/show/:user_id/sessions', 'user', 'sessions'); + $container['route']->addRoute('user/:user_id/edit', 'user', 'edit'); + $container['route']->addRoute('user/:user_id/password', 'user', 'password'); + $container['route']->addRoute('user/:user_id/share', 'user', 'share'); + $container['route']->addRoute('user/:user_id/notifications', 'user', 'notifications'); + $container['route']->addRoute('user/:user_id/accounts', 'user', 'external'); + $container['route']->addRoute('user/:user_id/integrations', 'user', 'integrations'); + $container['route']->addRoute('user/:user_id/authentication', 'user', 'authentication'); + $container['route']->addRoute('user/:user_id/remove', 'user', 'remove'); + $container['route']->addRoute('user/:user_id/2fa', 'twofactor', 'index'); + + // Groups + $container['route']->addRoute('groups', 'group', 'index'); + $container['route']->addRoute('groups/create', 'group', 'create'); + $container['route']->addRoute('group/:group_id/associate', 'group', 'associate'); + $container['route']->addRoute('group/:group_id/dissociate/:user_id', 'group', 'dissociate'); + $container['route']->addRoute('group/:group_id/edit', 'group', 'edit'); + $container['route']->addRoute('group/:group_id/members', 'group', 'users'); + $container['route']->addRoute('group/:group_id/remove', 'group', 'confirm'); + + // Config + $container['route']->addRoute('settings', 'config', 'index'); + $container['route']->addRoute('settings/plugins', 'config', 'plugins'); + $container['route']->addRoute('settings/application', 'config', 'application'); + $container['route']->addRoute('settings/project', 'config', 'project'); + $container['route']->addRoute('settings/project', 'config', 'project'); + $container['route']->addRoute('settings/board', 'config', 'board'); + $container['route']->addRoute('settings/calendar', 'config', 'calendar'); + $container['route']->addRoute('settings/integrations', 'config', 'integrations'); + $container['route']->addRoute('settings/webhook', 'config', 'webhook'); + $container['route']->addRoute('settings/api', 'config', 'api'); + $container['route']->addRoute('settings/links', 'link', 'index'); + $container['route']->addRoute('settings/currencies', 'currency', 'index'); + + // Doc + $container['route']->addRoute('documentation/:file', 'doc', 'show'); + $container['route']->addRoute('documentation', 'doc', 'show'); + + // Auth routes + $container['route']->addRoute('oauth/google', 'oauth', 'google'); + $container['route']->addRoute('oauth/github', 'oauth', 'github'); + $container['route']->addRoute('oauth/gitlab', 'oauth', 'gitlab'); + $container['route']->addRoute('login', 'auth', 'login'); + $container['route']->addRoute('logout', 'auth', 'logout'); + + // PasswordReset + $container['route']->addRoute('forgot-password', 'PasswordReset', 'create'); + $container['route']->addRoute('forgot-password/change/:token', 'PasswordReset', 'change'); + } + + return $container; + } +} diff --git a/sources/app/ServiceProvider/SessionProvider.php b/sources/app/ServiceProvider/SessionProvider.php index 414d957..0999d53 100644 --- a/sources/app/ServiceProvider/SessionProvider.php +++ b/sources/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/sources/app/Subscriber/AuthSubscriber.php b/sources/app/Subscriber/AuthSubscriber.php index 77a3994..e839385 100644 --- a/sources/app/Subscriber/AuthSubscriber.php +++ b/sources/app/Subscriber/AuthSubscriber.php @@ -2,26 +2,105 @@ namespace Kanboard\Subscriber; -use Kanboard\Core\Http\Request; -use Kanboard\Event\AuthEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +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 BaseSubscriber 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) { + $this->logger->debug('Subscriber executed: '.__METHOD__); + + $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 ); + + if ($event->getAuthType() === 'RememberMe') { + $this->userSession->validatePostAuthentication(); + } + + 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() + { + $this->logger->debug('Subscriber executed: '.__METHOD__); + $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) + { + $this->logger->debug('Subscriber executed: '.__METHOD__); + $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/sources/app/Subscriber/BaseSubscriber.php b/sources/app/Subscriber/BaseSubscriber.php new file mode 100644 index 0000000..2e41da7 --- /dev/null +++ b/sources/app/Subscriber/BaseSubscriber.php @@ -0,0 +1,40 @@ +called[$key])) { + return true; + } + + $this->called[$key] = true; + + return false; + } +} diff --git a/sources/app/Subscriber/BootstrapSubscriber.php b/sources/app/Subscriber/BootstrapSubscriber.php index 25b919f..ef0215f 100644 --- a/sources/app/Subscriber/BootstrapSubscriber.php +++ b/sources/app/Subscriber/BootstrapSubscriber.php @@ -4,20 +4,39 @@ namespace Kanboard\Subscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class BootstrapSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class BootstrapSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - 'session.bootstrap' => array('setup', 0), - 'api.bootstrap' => array('setup', 0), - 'console.bootstrap' => array('setup', 0), + 'app.bootstrap' => 'execute', ); } - public function setup() + public function execute() { + $this->logger->debug('Subscriber executed: '.__METHOD__); $this->config->setupTranslations(); $this->config->setupTimezone(); + $this->actionManager->attachEvents(); + + if ($this->userSession->isLogged()) { + $this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); + } + } + + 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()); + $this->logger->debug('###############################################'); + } } } diff --git a/sources/app/Subscriber/NotificationSubscriber.php b/sources/app/Subscriber/NotificationSubscriber.php index 394573e..0766005 100644 --- a/sources/app/Subscriber/NotificationSubscriber.php +++ b/sources/app/Subscriber/NotificationSubscriber.php @@ -9,34 +9,43 @@ use Kanboard\Model\Subtask; use Kanboard\Model\File; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class NotificationSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class NotificationSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Task::EVENT_CREATE => array('execute', 0), - Task::EVENT_UPDATE => array('execute', 0), - Task::EVENT_CLOSE => array('execute', 0), - Task::EVENT_OPEN => array('execute', 0), - Task::EVENT_MOVE_COLUMN => array('execute', 0), - Task::EVENT_MOVE_POSITION => array('execute', 0), - Task::EVENT_MOVE_SWIMLANE => array('execute', 0), - Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0), - Subtask::EVENT_CREATE => array('execute', 0), - Subtask::EVENT_UPDATE => array('execute', 0), - Comment::EVENT_CREATE => array('execute', 0), - Comment::EVENT_UPDATE => array('execute', 0), - File::EVENT_CREATE => array('execute', 0), + Task::EVENT_USER_MENTION => 'handleEvent', + Task::EVENT_CREATE => 'handleEvent', + Task::EVENT_UPDATE => 'handleEvent', + Task::EVENT_CLOSE => 'handleEvent', + Task::EVENT_OPEN => 'handleEvent', + Task::EVENT_MOVE_COLUMN => 'handleEvent', + Task::EVENT_MOVE_POSITION => 'handleEvent', + Task::EVENT_MOVE_SWIMLANE => 'handleEvent', + Task::EVENT_ASSIGNEE_CHANGE => 'handleEvent', + Subtask::EVENT_CREATE => 'handleEvent', + Subtask::EVENT_UPDATE => 'handleEvent', + Comment::EVENT_CREATE => 'handleEvent', + Comment::EVENT_UPDATE => 'handleEvent', + Comment::EVENT_USER_MENTION => 'handleEvent', + File::EVENT_CREATE => 'handleEvent', ); } - public function execute(GenericEvent $event, $event_name) + public function handleEvent(GenericEvent $event, $event_name) { - $event_data = $this->getEventData($event); + if (! $this->isExecuted($event_name)) { + $this->logger->debug('Subscriber executed: '.__METHOD__); + $event_data = $this->getEventData($event); - if (! empty($event_data)) { - $this->userNotification->sendNotifications($event_name, $event_data); - $this->projectNotification->sendNotifications($event_data['task']['project_id'], $event_name, $event_data); + if (! empty($event_data)) { + if (! empty($event['mention'])) { + $this->userNotification->sendUserNotification($event['mention'], $event_name, $event_data); + } else { + $this->userNotification->sendNotifications($event_name, $event_data); + $this->projectNotification->sendNotifications($event_data['task']['project_id'], $event_name, $event_data); + } + } } } diff --git a/sources/app/Subscriber/ProjectDailySummarySubscriber.php b/sources/app/Subscriber/ProjectDailySummarySubscriber.php index bfa6cd4..44138f4 100644 --- a/sources/app/Subscriber/ProjectDailySummarySubscriber.php +++ b/sources/app/Subscriber/ProjectDailySummarySubscriber.php @@ -6,22 +6,23 @@ use Kanboard\Event\TaskEvent; use Kanboard\Model\Task; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class ProjectDailySummarySubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class ProjectDailySummarySubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Task::EVENT_CREATE => array('execute', 0), - Task::EVENT_UPDATE => array('execute', 0), - Task::EVENT_CLOSE => array('execute', 0), - Task::EVENT_OPEN => array('execute', 0), - Task::EVENT_MOVE_COLUMN => array('execute', 0), + Task::EVENT_CREATE_UPDATE => 'execute', + Task::EVENT_CLOSE => 'execute', + Task::EVENT_OPEN => 'execute', + Task::EVENT_MOVE_COLUMN => 'execute', + Task::EVENT_MOVE_SWIMLANE => 'execute', ); } public function execute(TaskEvent $event) { - if (isset($event['project_id'])) { + if (isset($event['project_id']) && !$this->isExecuted()) { + $this->logger->debug('Subscriber executed: '.__METHOD__); $this->projectDailyColumnStats->updateTotals($event['project_id'], date('Y-m-d')); $this->projectDailyStats->updateTotals($event['project_id'], date('Y-m-d')); } diff --git a/sources/app/Subscriber/ProjectModificationDateSubscriber.php b/sources/app/Subscriber/ProjectModificationDateSubscriber.php index 1f99840..62804a8 100644 --- a/sources/app/Subscriber/ProjectModificationDateSubscriber.php +++ b/sources/app/Subscriber/ProjectModificationDateSubscriber.php @@ -6,25 +6,26 @@ use Kanboard\Event\GenericEvent; use Kanboard\Model\Task; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class ProjectModificationDateSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class ProjectModificationDateSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Task::EVENT_CREATE_UPDATE => array('execute', 0), - Task::EVENT_CLOSE => array('execute', 0), - Task::EVENT_OPEN => array('execute', 0), - Task::EVENT_MOVE_SWIMLANE => array('execute', 0), - Task::EVENT_MOVE_COLUMN => array('execute', 0), - Task::EVENT_MOVE_POSITION => array('execute', 0), - Task::EVENT_MOVE_PROJECT => array('execute', 0), - Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0), + Task::EVENT_CREATE_UPDATE => 'execute', + Task::EVENT_CLOSE => 'execute', + Task::EVENT_OPEN => 'execute', + Task::EVENT_MOVE_SWIMLANE => 'execute', + Task::EVENT_MOVE_COLUMN => 'execute', + Task::EVENT_MOVE_POSITION => 'execute', + Task::EVENT_MOVE_PROJECT => 'execute', + Task::EVENT_ASSIGNEE_CHANGE => 'execute', ); } public function execute(GenericEvent $event) { - if (isset($event['project_id'])) { + if (isset($event['project_id']) && !$this->isExecuted()) { + $this->logger->debug('Subscriber executed: '.__METHOD__); $this->project->updateModificationDate($event['project_id']); } } diff --git a/sources/app/Subscriber/RecurringTaskSubscriber.php b/sources/app/Subscriber/RecurringTaskSubscriber.php index b03ffcf..6d5aee8 100644 --- a/sources/app/Subscriber/RecurringTaskSubscriber.php +++ b/sources/app/Subscriber/RecurringTaskSubscriber.php @@ -6,18 +6,20 @@ use Kanboard\Event\TaskEvent; use Kanboard\Model\Task; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class RecurringTaskSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Task::EVENT_MOVE_COLUMN => array('onMove', 0), - Task::EVENT_CLOSE => array('onClose', 0), + Task::EVENT_MOVE_COLUMN => 'onMove', + Task::EVENT_CLOSE => 'onClose', ); } public function onMove(TaskEvent $event) { + $this->logger->debug('Subscriber executed: '.__METHOD__); + if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING) { if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->board->getFirstColumn($event['project_id']) == $event['src_column_id']) { $this->taskDuplication->duplicateRecurringTask($event['task_id']); @@ -29,6 +31,8 @@ class RecurringTaskSubscriber extends \Kanboard\Core\Base implements EventSubscr public function onClose(TaskEvent $event) { + $this->logger->debug('Subscriber executed: '.__METHOD__); + if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == Task::RECURRING_TRIGGER_CLOSE) { $this->taskDuplication->duplicateRecurringTask($event['task_id']); } diff --git a/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php b/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php index b5e0354..c0852bc 100644 --- a/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php +++ b/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php @@ -6,13 +6,13 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Kanboard\Model\Subtask; use Kanboard\Event\SubtaskEvent; -class SubtaskTimeTrackingSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class SubtaskTimeTrackingSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Subtask::EVENT_CREATE => array('updateTaskTime', 0), - Subtask::EVENT_DELETE => array('updateTaskTime', 0), + Subtask::EVENT_CREATE => 'updateTaskTime', + Subtask::EVENT_DELETE => 'updateTaskTime', Subtask::EVENT_UPDATE => array( array('logStartEnd', 10), array('updateTaskTime', 0), @@ -23,6 +23,7 @@ class SubtaskTimeTrackingSubscriber extends \Kanboard\Core\Base implements Event public function updateTaskTime(SubtaskEvent $event) { if (isset($event['task_id'])) { + $this->logger->debug('Subscriber executed: '.__METHOD__); $this->subtaskTimeTracking->updateTaskTimeTracking($event['task_id']); } } @@ -30,6 +31,7 @@ class SubtaskTimeTrackingSubscriber extends \Kanboard\Core\Base implements Event public function logStartEnd(SubtaskEvent $event) { if (isset($event['status']) && $this->config->get('subtask_time_tracking') == 1) { + $this->logger->debug('Subscriber executed: '.__METHOD__); $subtask = $this->subtask->getById($event['id']); if (empty($subtask['user_id'])) { diff --git a/sources/app/Subscriber/TaskMovedDateSubscriber.php b/sources/app/Subscriber/TaskMovedDateSubscriber.php deleted file mode 100644 index 9857f4b..0000000 --- a/sources/app/Subscriber/TaskMovedDateSubscriber.php +++ /dev/null @@ -1,25 +0,0 @@ - array('execute', 0), - Task::EVENT_MOVE_SWIMLANE => array('execute', 0), - ); - } - - public function execute(TaskEvent $event) - { - if (isset($event['task_id'])) { - $this->container['db']->table(Task::TABLE)->eq('id', $event['task_id'])->update(array('date_moved' => time())); - } - } -} diff --git a/sources/app/Subscriber/TransitionSubscriber.php b/sources/app/Subscriber/TransitionSubscriber.php index 35352dc..bd53748 100644 --- a/sources/app/Subscriber/TransitionSubscriber.php +++ b/sources/app/Subscriber/TransitionSubscriber.php @@ -6,17 +6,19 @@ use Kanboard\Event\TaskEvent; use Kanboard\Model\Task; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -class TransitionSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +class TransitionSubscriber extends BaseSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( - Task::EVENT_MOVE_COLUMN => array('execute', 0), + Task::EVENT_MOVE_COLUMN => 'execute', ); } public function execute(TaskEvent $event) { + $this->logger->debug('Subscriber executed: '.__METHOD__); + $user_id = $this->userSession->getId(); if (! empty($user_id)) { diff --git a/sources/app/Template/action/index.php b/sources/app/Template/action/index.php index bf2f747..8275f08 100644 --- a/sources/app/Template/action/index.php +++ b/sources/app/Template/action/index.php @@ -28,24 +28,24 @@