From 9b8806775ae403bcb9f588aa3c68b9471a5fe7ca Mon Sep 17 00:00:00 2001 From: mbugeia Date: Tue, 29 Dec 2015 01:24:09 +0100 Subject: [PATCH] Update sources to kanboard v1.0.22 --- sources/ChangeLog | 34 +- sources/app/Action/TaskOpen.php | 2 + sources/app/Api/Auth.php | 50 +- sources/app/Api/Me.php | 8 +- sources/app/Api/ProjectPermission.php | 8 +- sources/app/Api/Task.php | 24 +- sources/app/Api/User.php | 57 +- sources/app/Auth/Database.php | 49 - sources/app/Auth/DatabaseAuth.php | 125 + sources/app/Auth/Github.php | 123 - sources/app/Auth/GithubAuth.php | 143 + sources/app/Auth/Gitlab.php | 123 - sources/app/Auth/GitlabAuth.php | 143 + sources/app/Auth/Google.php | 124 - sources/app/Auth/GoogleAuth.php | 143 + sources/app/Auth/Ldap.php | 521 -- sources/app/Auth/LdapAuth.php | 172 + sources/app/Auth/RememberMe.php | 323 -- sources/app/Auth/RememberMeAuth.php | 79 + sources/app/Auth/ReverseProxy.php | 83 - sources/app/Auth/ReverseProxyAuth.php | 76 + sources/app/Auth/TotpAuth.php | 126 + sources/app/Controller/Action.php | 4 +- sources/app/Controller/Activity.php | 2 +- sources/app/Controller/Analytic.php | 5 +- sources/app/Controller/App.php | 51 +- sources/app/Controller/Auth.php | 30 +- sources/app/Controller/Base.php | 114 +- sources/app/Controller/Board.php | 193 +- sources/app/Controller/BoardPopover.php | 101 + sources/app/Controller/BoardTooltip.php | 112 + sources/app/Controller/Config.php | 2 +- sources/app/Controller/Currency.php | 2 +- sources/app/Controller/Customfilter.php | 4 +- sources/app/Controller/Doc.php | 2 +- sources/app/Controller/Feed.php | 4 +- sources/app/Controller/Gantt.php | 8 +- sources/app/Controller/Group.php | 255 + sources/app/Controller/GroupHelper.php | 24 + sources/app/Controller/Link.php | 2 +- sources/app/Controller/Oauth.php | 43 +- sources/app/Controller/Project.php | 139 +- sources/app/Controller/ProjectPermission.php | 177 + sources/app/Controller/Projectuser.php | 13 +- sources/app/Controller/Search.php | 2 +- sources/app/Controller/Subtask.php | 4 +- sources/app/Controller/Task.php | 2 +- sources/app/Controller/TaskHelper.php | 57 + sources/app/Controller/Taskcreation.php | 2 +- sources/app/Controller/Taskduplication.php | 6 +- sources/app/Controller/Taskmodification.php | 2 +- sources/app/Controller/Twofactor.php | 33 +- sources/app/Controller/User.php | 35 +- sources/app/Controller/UserHelper.php | 24 + sources/app/Core/Base.php | 50 +- sources/app/Core/Cache/MemoryCache.php | 2 +- sources/app/Core/Csv.php | 2 +- .../Group/GroupBackendProviderInterface.php | 21 + sources/app/Core/Group/GroupManager.php | 71 + .../app/Core/Group/GroupProviderInterface.php | 40 + sources/app/Core/{ => Http}/OAuth2.php | 8 +- sources/app/Core/Http/RememberMeCookie.php | 120 + sources/app/Core/Http/Request.php | 125 +- sources/app/Core/Http/Response.php | 2 +- sources/app/Core/Ldap/Client.php | 165 + sources/app/Core/Ldap/ClientException.php | 15 + sources/app/Core/Ldap/Entries.php | 63 + sources/app/Core/Ldap/Entry.php | 91 + sources/app/Core/Ldap/Group.php | 130 + sources/app/Core/Ldap/Query.php | 87 + sources/app/Core/Ldap/User.php | 223 + sources/app/Core/Lexer.php | 2 + sources/app/Core/Mail/Transport/Mail.php | 2 +- sources/app/Core/Mail/Transport/Sendmail.php | 2 +- sources/app/Core/Mail/Transport/Smtp.php | 2 +- sources/app/Core/Security/AccessMap.php | 155 + .../Core/Security/AuthenticationManager.php | 187 + .../AuthenticationProviderInterface.php | 28 + sources/app/Core/Security/Authorization.php | 46 + .../OAuthAuthenticationProviderInterface.php | 46 + ...asswordAuthenticationProviderInterface.php | 36 + .../PostAuthenticationProviderInterface.php | 54 + .../PreAuthenticationProviderInterface.php | 20 + sources/app/Core/Security/Role.php | 64 + .../SessionCheckProviderInterface.php | 20 + sources/app/Core/Session/SessionManager.php | 14 +- sources/app/Core/Session/SessionStorage.php | 18 +- sources/app/Core/User/GroupSync.php | 32 + sources/app/Core/User/UserProfile.php | 62 + sources/app/Core/User/UserProperty.php | 70 + .../app/Core/User/UserProviderInterface.php | 103 + .../app/{Model => Core/User}/UserSession.php | 65 +- sources/app/Core/User/UserSync.php | 76 + sources/app/Event/AuthEvent.php | 27 - sources/app/Event/AuthFailureEvent.php | 44 + sources/app/Event/AuthSuccessEvent.php | 43 + .../Formatter/GroupAutoCompleteFormatter.php | 55 + .../app/Formatter/ProjectGanttFormatter.php | 2 +- .../UserFilterAutoCompleteFormatter.php | 38 + .../Group/DatabaseBackendGroupProvider.php | 34 + sources/app/Group/DatabaseGroupProvider.php | 66 + .../app/Group/LdapBackendGroupProvider.php | 54 + sources/app/Group/LdapGroupProvider.php | 76 + sources/app/Helper/Subtask.php | 13 +- sources/app/Helper/Url.php | 3 +- sources/app/Helper/User.php | 68 +- sources/app/Integration/GitlabWebhook.php | 33 + sources/app/Locale/bs_BA/translations.php | 75 +- sources/app/Locale/cs_CZ/translations.php | 67 +- sources/app/Locale/da_DK/translations.php | 67 +- sources/app/Locale/de_DE/translations.php | 147 +- sources/app/Locale/es_ES/translations.php | 67 +- sources/app/Locale/fi_FI/translations.php | 67 +- sources/app/Locale/fr_FR/translations.php | 69 +- sources/app/Locale/hu_HU/translations.php | 67 +- sources/app/Locale/id_ID/translations.php | 67 +- sources/app/Locale/it_IT/translations.php | 67 +- sources/app/Locale/ja_JP/translations.php | 67 +- sources/app/Locale/nb_NO/translations.php | 67 +- sources/app/Locale/nl_NL/translations.php | 67 +- sources/app/Locale/pl_PL/translations.php | 67 +- sources/app/Locale/pt_BR/translations.php | 67 +- sources/app/Locale/pt_PT/translations.php | 91 +- sources/app/Locale/ru_RU/translations.php | 69 +- .../app/Locale/sr_Latn_RS/translations.php | 67 +- sources/app/Locale/sv_SE/translations.php | 67 +- sources/app/Locale/th_TH/translations.php | 67 +- sources/app/Locale/tr_TR/translations.php | 67 +- sources/app/Locale/zh_CN/translations.php | 67 +- sources/app/Model/Acl.php | 289 -- sources/app/Model/Action.php | 1 + sources/app/Model/Authentication.php | 214 +- sources/app/Model/Config.php | 2 +- sources/app/Model/Group.php | 175 + sources/app/Model/GroupMember.php | 111 + sources/app/Model/Project.php | 5 +- sources/app/Model/ProjectAnalytic.php | 2 +- sources/app/Model/ProjectDailyColumnStats.php | 2 - sources/app/Model/ProjectDailyStats.php | 2 - sources/app/Model/ProjectGroupRole.php | 187 + sources/app/Model/ProjectPermission.php | 471 +- sources/app/Model/ProjectUserRole.php | 263 + sources/app/Model/RememberMeSession.php | 151 + sources/app/Model/TaskFilter.php | 41 + sources/app/Model/TaskPermission.php | 4 +- sources/app/Model/User.php | 131 +- sources/app/Model/UserFilter.php | 80 + sources/app/Model/UserImport.php | 18 +- sources/app/Model/UserLocking.php | 103 + sources/app/Model/UserNotification.php | 2 +- sources/app/Schema/Mysql.php | 88 +- sources/app/Schema/Postgres.php | 87 +- sources/app/Schema/Sqlite.php | 77 +- .../AuthenticationProvider.php | 149 + sources/app/ServiceProvider/ClassProvider.php | 43 +- sources/app/ServiceProvider/GroupProvider.php | 37 + .../ServiceProvider/NotificationProvider.php | 45 + .../app/ServiceProvider/PluginProvider.php | 31 + sources/app/ServiceProvider/RouteProvider.php | 200 + .../app/ServiceProvider/SessionProvider.php | 13 + sources/app/Subscriber/AuthSubscriber.php | 92 +- .../app/Subscriber/BootstrapSubscriber.php | 20 +- sources/app/Template/activity/project.php | 2 +- sources/app/Template/analytic/layout.php | 2 +- sources/app/Template/app/layout.php | 6 +- sources/app/Template/app/projects.php | 2 +- .../app/Template/board/popover_assignee.php | 2 +- .../app/Template/board/popover_category.php | 2 +- sources/app/Template/board/table_column.php | 2 +- sources/app/Template/board/table_swimlane.php | 2 +- sources/app/Template/board/task_footer.php | 14 +- sources/app/Template/board/task_menu.php | 6 +- sources/app/Template/board/task_private.php | 36 +- .../app/Template/board/tooltip_subtasks.php | 2 +- sources/app/Template/calendar/show.php | 2 +- sources/app/Template/comment/show.php | 2 +- sources/app/Template/custom_filter/add.php | 2 +- sources/app/Template/custom_filter/edit.php | 2 +- sources/app/Template/custom_filter/index.php | 2 +- sources/app/Template/file/show.php | 10 +- sources/app/Template/gantt/projects.php | 4 +- sources/app/Template/group/associate.php | 25 + sources/app/Template/group/create.php | 19 + sources/app/Template/group/dissociate.php | 17 + sources/app/Template/group/edit.php | 22 + sources/app/Template/group/index.php | 43 + sources/app/Template/group/remove.php | 17 + sources/app/Template/group/users.php | 42 + sources/app/Template/layout.php | 2 +- sources/app/Template/project/dropdown.php | 11 +- sources/app/Template/project/edit.php | 2 +- sources/app/Template/project/filters.php | 2 +- sources/app/Template/project/index.php | 40 +- sources/app/Template/project/roles.php | 7 + sources/app/Template/project/sidebar.php | 12 +- sources/app/Template/project/users.php | 82 - .../app/Template/project_permission/index.php | 141 + sources/app/Template/project_user/layout.php | 4 +- sources/app/Template/subtask/show.php | 26 +- sources/app/Template/task/comments.php | 4 +- sources/app/Template/task/details.php | 2 +- sources/app/Template/task/layout.php | 2 +- sources/app/Template/task/public.php | 11 +- sources/app/Template/task/show.php | 40 +- sources/app/Template/task/sidebar.php | 2 + sources/app/Template/tasklink/create.php | 4 +- sources/app/Template/tasklink/edit.php | 4 +- sources/app/Template/tasklink/show.php | 22 +- sources/app/Template/twofactor/index.php | 12 +- sources/app/Template/user/create_local.php | 23 +- sources/app/Template/user/create_remote.php | 23 +- sources/app/Template/user/edit.php | 14 +- sources/app/Template/user/external.php | 6 +- sources/app/Template/user/index.php | 11 +- sources/app/Template/user/layout.php | 2 +- sources/app/Template/user/sessions.php | 2 +- sources/app/Template/user/show.php | 2 +- sources/app/Template/user/sidebar.php | 6 +- sources/app/Template/user_import/step1.php | 2 +- sources/app/User/DatabaseUserProvider.php | 143 + sources/app/User/GithubUserProvider.php | 23 + sources/app/User/GitlabUserProvider.php | 23 + sources/app/User/GoogleUserProvider.php | 23 + sources/app/User/LdapUserProvider.php | 206 + sources/app/User/OAuthUserProvider.php | 140 + sources/app/User/ReverseProxyUserProvider.php | 147 + sources/app/check_setup.php | 9 +- sources/app/common.php | 16 +- sources/app/constants.php | 30 +- sources/app/routes.php | 117 - sources/assets/js/app.js | 2 +- sources/assets/js/src/App.js | 29 +- sources/assets/js/src/Gantt.js | 32 +- sources/assets/js/src/Project.js | 18 + sources/config.default.php | 80 +- sources/doc/2fa.markdown | 12 +- sources/doc/analytics-tasks.markdown | 6 +- sources/doc/analytics.markdown | 30 +- sources/doc/api-action-procedures.markdown | 245 + .../doc/api-application-procedures.markdown | 231 + sources/doc/api-authentication.markdown | 66 + sources/doc/api-board-procedures.markdown | 416 ++ sources/doc/api-category-procedures.markdown | 172 + sources/doc/api-comment-procedures.markdown | 182 + sources/doc/api-examples.markdown | 184 + sources/doc/api-file-procedures.markdown | 217 + sources/doc/api-json-rpc.markdown | 4526 +---------------- sources/doc/api-link-procedures.markdown | 470 ++ sources/doc/api-me-procedures.markdown | 385 ++ sources/doc/api-project-procedures.markdown | 512 ++ sources/doc/api-subtask-procedures.markdown | 194 + sources/doc/api-swimlane-procedures.markdown | 469 ++ sources/doc/api-task-procedures.markdown | 562 ++ sources/doc/api-user-procedures.markdown | 222 + .../doc/application-configuration.markdown | 12 +- sources/doc/automatic-actions.markdown | 42 +- sources/doc/bitbucket-webhooks.markdown | 10 +- sources/doc/board-collapsed-expanded.markdown | 2 +- sources/doc/board-configuration.markdown | 6 +- ...zontal-scrolling-and-compact-view.markdown | 4 +- sources/doc/board-show-hide-columns.markdown | 2 +- sources/doc/bruteforce-protection.markdown | 10 +- sources/doc/calendar-configuration.markdown | 12 +- sources/doc/calendar.markdown | 8 +- sources/doc/centos-installation.markdown | 2 +- sources/doc/cli.markdown | 4 +- sources/doc/closing-tasks.markdown | 10 +- sources/doc/coding-standards.markdown | 2 +- sources/doc/config.markdown | 112 +- sources/doc/contributing.markdown | 15 +- sources/doc/create-tasks-by-email.markdown | 23 +- sources/doc/creating-projects.markdown | 6 +- sources/doc/creating-tasks.markdown | 10 +- sources/doc/currency-rate.markdown | 2 +- sources/doc/custom-filters.markdown | 8 +- sources/doc/duplicate-move-tasks.markdown | 4 +- sources/doc/editing-projects.markdown | 6 +- sources/doc/email-configuration.markdown | 10 +- sources/doc/faq.markdown | 20 +- sources/doc/gantt-chart-projects.markdown | 6 +- sources/doc/gantt-chart-tasks.markdown | 4 +- sources/doc/gitlab-webhooks.markdown | 7 +- sources/doc/groups.markdown | 17 + sources/doc/heroku.markdown | 5 +- sources/doc/ical.markdown | 22 +- sources/doc/index.markdown | 10 +- sources/doc/installation.markdown | 2 +- sources/doc/kanban-vs-todo-and-scrum.markdown | 4 +- sources/doc/keyboard-shortcuts.markdown | 2 +- sources/doc/ldap-authentication.markdown | 144 +- sources/doc/ldap-group-sync.markdown | 47 +- sources/doc/ldap-parameters.markdown | 33 + sources/doc/link-labels.markdown | 2 +- sources/doc/mysql-configuration.markdown | 4 +- sources/doc/nginx-ssl-php-fpm.markdown | 238 - sources/doc/nice-urls.markdown | 13 +- sources/doc/notifications.markdown | 6 +- ...lugin-authentication-architecture.markdown | 99 + sources/doc/plugin-authentication.markdown | 39 + ...plugin-authorization-architecture.markdown | 39 + sources/doc/plugin-group-provider.markdown | 55 + sources/doc/plugin-hooks.markdown | 12 +- sources/doc/plugin-ldap-client.markdown | 99 + sources/doc/plugin-mail-transports.markdown | 6 +- sources/doc/plugin-metadata.markdown | 4 +- sources/doc/plugin-notifications.markdown | 4 +- sources/doc/plugin-overrides.markdown | 2 +- sources/doc/plugin-registration.markdown | 43 +- sources/doc/plugin-schema-migrations.markdown | 5 +- sources/doc/plugins.markdown | 10 +- sources/doc/postgresql-configuration.markdown | 8 +- sources/doc/project-configuration.markdown | 4 +- sources/doc/project-permissions.markdown | 54 +- sources/doc/project-types.markdown | 14 + sources/doc/project-views.markdown | 22 +- .../doc/recommended-configuration.markdown | 6 +- sources/doc/recurring-tasks.markdown | 4 +- .../doc/reverse-proxy-authentication.markdown | 12 +- sources/doc/roles.markdown | 25 + sources/doc/rss.markdown | 4 +- sources/doc/screenshots.markdown | 6 +- sources/doc/screenshots/groups-management.png | Bin 0 -> 17922 bytes .../doc/screenshots/project-permissions.png | Bin 0 -> 64960 bytes sources/doc/search.markdown | 44 +- sources/doc/sharing-projects.markdown | 2 +- sources/doc/subtasks.markdown | 4 +- sources/doc/swimlanes.markdown | 6 +- sources/doc/task-links.markdown | 2 +- sources/doc/tests.markdown | 6 +- sources/doc/time-tracking.markdown | 4 +- sources/doc/transitions.markdown | 4 +- sources/doc/translations.markdown | 14 +- sources/doc/ubuntu-installation.markdown | 2 +- sources/doc/update.markdown | 4 +- sources/doc/usage-examples.markdown | 2 +- sources/doc/user-management.markdown | 54 +- sources/doc/user-types.markdown | 14 + sources/doc/vagrant.markdown | 4 +- sources/doc/webhooks.markdown | 26 +- sources/doc/what-is-kanban.markdown | 10 +- .../doc/windows-apache-installation.markdown | 8 +- sources/doc/windows-iis-installation.markdown | 6 +- sources/kanboard | 2 +- sources/vendor/autoload.php | 2 +- sources/vendor/bin/html-to-markdown | 108 + sources/vendor/composer/autoload_classmap.php | 90 +- sources/vendor/composer/autoload_files.php | 1 + sources/vendor/composer/autoload_psr4.php | 1 + sources/vendor/composer/autoload_real.php | 10 +- sources/vendor/composer/installed.json | 147 +- .../fguillot/picodb/lib/PicoDb/Condition.php | 26 + .../fguillot/picodb/lib/PicoDb/Database.php | 4 + .../picodb/lib/PicoDb/Driver/Mssql.php | 164 + .../fguillot/picodb/lib/PicoDb/Table.php | 21 + .../league/html-to-markdown/CHANGELOG.md | 18 +- .../vendor/league/html-to-markdown/CONDUCT.md | 22 + .../html-to-markdown/bin/html-to-markdown | 108 + .../src/Converter/ParagraphConverter.php | 2 +- .../src/Converter/TextConverter.php | 6 +- .../html-to-markdown/src/HtmlConverter.php | 2 +- .../vendor/symfony/console/Application.php | 98 +- sources/vendor/symfony/console/CHANGELOG.md | 6 + .../symfony/console/Command/Command.php | 85 +- .../Descriptor/ApplicationDescription.php | 13 +- .../symfony/console/Descriptor/Descriptor.php | 3 +- .../console/Descriptor/TextDescriptor.php | 4 +- .../Exception/CommandNotFoundException.php | 43 + .../console/Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 19 + .../Exception/InvalidOptionException.php | 21 + .../console/Exception/LogicException.php | 19 + .../console/Exception/RuntimeException.php | 19 + .../console/Formatter/OutputFormatter.php | 22 +- .../Formatter/OutputFormatterInterface.php | 14 - .../Formatter/OutputFormatterStyle.php | 28 +- .../OutputFormatterStyleInterface.php | 8 - .../Formatter/OutputFormatterStyleStack.php | 6 +- .../console/Helper/DescriptorHelper.php | 5 +- .../symfony/console/Helper/DialogHelper.php | 22 +- .../vendor/symfony/console/Helper/Helper.php | 6 +- .../console/Helper/HelperInterface.php | 8 - .../symfony/console/Helper/HelperSet.php | 5 +- .../symfony/console/Helper/ProgressBar.php | 57 +- .../symfony/console/Helper/ProgressHelper.php | 15 +- .../console/Helper/ProgressIndicator.php | 322 ++ .../symfony/console/Helper/QuestionHelper.php | 44 +- .../console/Helper/SymfonyQuestionHelper.php | 3 +- .../vendor/symfony/console/Helper/Table.php | 103 +- .../symfony/console/Helper/TableCell.php | 4 +- .../symfony/console/Helper/TableHelper.php | 5 +- .../symfony/console/Helper/TableStyle.php | 7 +- .../symfony/console/Input/ArgvInput.php | 26 +- .../symfony/console/Input/ArrayInput.php | 23 +- .../vendor/symfony/console/Input/Input.php | 32 +- .../symfony/console/Input/InputArgument.php | 17 +- .../symfony/console/Input/InputDefinition.php | 58 +- .../symfony/console/Input/InputInterface.php | 4 +- .../symfony/console/Input/InputOption.php | 23 +- .../symfony/console/Input/StringInput.php | 10 +- .../symfony/console/Output/ConsoleOutput.php | 12 +- .../symfony/console/Output/NullOutput.php | 6 +- .../vendor/symfony/console/Output/Output.php | 24 +- .../console/Output/OutputInterface.php | 48 +- .../symfony/console/Output/StreamOutput.php | 16 +- .../console/Question/ChoiceQuestion.php | 8 +- .../symfony/console/Question/Question.php | 23 +- sources/vendor/symfony/console/Shell.php | 7 +- .../symfony/console/Style/SymfonyStyle.php | 31 +- .../symfony/console/Tests/ApplicationTest.php | 143 +- .../symfony/console/Tests/ClockMock.php | 41 - .../console/Tests/Command/CommandTest.php | 52 +- .../console/Tests/Command/ListCommandTest.php | 48 + .../console/Tests/Fixtures/Foo6Command.php | 12 + .../Style/SymfonyStyle/command/command_0.php | 4 +- .../Style/SymfonyStyle/command/command_1.php | 4 +- .../Style/SymfonyStyle/command/command_2.php | 4 +- .../Style/SymfonyStyle/command/command_3.php | 4 +- .../Style/SymfonyStyle/command/command_4.php | 4 +- .../Style/SymfonyStyle/command/command_5.php | 12 +- .../Style/SymfonyStyle/command/command_6.php | 4 +- .../Style/SymfonyStyle/command/command_7.php | 4 +- .../Style/SymfonyStyle/output/output_5.txt | 4 + .../Style/SymfonyStyle/output/output_7.txt | 2 +- .../Fixtures/application_renderexception1.txt | 8 +- .../Fixtures/application_renderexception2.txt | 8 +- .../Fixtures/application_renderexception4.txt | 10 +- .../Tests/Helper/FormatterHelperTest.php | 7 - .../console/Tests/Helper/HelperSetTest.php | 28 +- .../Tests/Helper/LegacyDialogHelperTest.php | 3 +- .../Tests/Helper/LegacyProgressHelperTest.php | 18 +- .../Tests/Helper/LegacyTableHelperTest.php | 8 - .../console/Tests/Helper/ProgressBarTest.php | 68 +- .../Tests/Helper/ProgressIndicatorTest.php | 179 + .../console/Tests/Helper/TableTest.php | 61 +- .../symfony/console/Tests/Input/InputTest.php | 13 +- .../console/Tests/Output/OutputTest.php | 39 +- .../console/Tests/Style/SymfonyStyleTest.php | 27 +- .../ContainerAwareEventDispatcher.php | 10 + .../Debug/TraceableEventDispatcher.php | 32 + .../vendor/symfony/event-dispatcher/Event.php | 14 - .../event-dispatcher/EventDispatcher.php | 27 +- .../EventDispatcherInterface.php | 8 - .../EventSubscriberInterface.php | 4 - .../ImmutableEventDispatcher.php | 8 + .../Tests/AbstractEventDispatcherTest.php | 14 + .../ContainerAwareEventDispatcherTest.php | 4 + .../Debug/TraceableEventDispatcherTest.php | 18 +- .../vendor/symfony/polyfill-mbstring/LICENSE | 19 + .../symfony/polyfill-mbstring/Mbstring.php | 604 +++ .../Resources/unidata/lowerCase.ser | 1 + .../Resources/unidata/upperCase.ser | 1 + .../symfony/polyfill-mbstring/bootstrap.php | 51 + 452 files changed, 17917 insertions(+), 10094 deletions(-) delete mode 100644 sources/app/Auth/Database.php create mode 100644 sources/app/Auth/DatabaseAuth.php delete mode 100644 sources/app/Auth/Github.php create mode 100644 sources/app/Auth/GithubAuth.php delete mode 100644 sources/app/Auth/Gitlab.php create mode 100644 sources/app/Auth/GitlabAuth.php delete mode 100644 sources/app/Auth/Google.php create mode 100644 sources/app/Auth/GoogleAuth.php delete mode 100644 sources/app/Auth/Ldap.php create mode 100644 sources/app/Auth/LdapAuth.php delete mode 100644 sources/app/Auth/RememberMe.php create mode 100644 sources/app/Auth/RememberMeAuth.php delete mode 100644 sources/app/Auth/ReverseProxy.php create mode 100644 sources/app/Auth/ReverseProxyAuth.php create mode 100644 sources/app/Auth/TotpAuth.php create mode 100644 sources/app/Controller/BoardPopover.php create mode 100644 sources/app/Controller/BoardTooltip.php create mode 100644 sources/app/Controller/Group.php create mode 100644 sources/app/Controller/GroupHelper.php create mode 100644 sources/app/Controller/ProjectPermission.php create mode 100644 sources/app/Controller/TaskHelper.php create mode 100644 sources/app/Controller/UserHelper.php create mode 100644 sources/app/Core/Group/GroupBackendProviderInterface.php create mode 100644 sources/app/Core/Group/GroupManager.php create mode 100644 sources/app/Core/Group/GroupProviderInterface.php rename sources/app/Core/{ => Http}/OAuth2.php (96%) create mode 100644 sources/app/Core/Http/RememberMeCookie.php create mode 100644 sources/app/Core/Ldap/Client.php create mode 100644 sources/app/Core/Ldap/ClientException.php create mode 100644 sources/app/Core/Ldap/Entries.php create mode 100644 sources/app/Core/Ldap/Entry.php create mode 100644 sources/app/Core/Ldap/Group.php create mode 100644 sources/app/Core/Ldap/Query.php create mode 100644 sources/app/Core/Ldap/User.php create mode 100644 sources/app/Core/Security/AccessMap.php create mode 100644 sources/app/Core/Security/AuthenticationManager.php create mode 100644 sources/app/Core/Security/AuthenticationProviderInterface.php create mode 100644 sources/app/Core/Security/Authorization.php create mode 100644 sources/app/Core/Security/OAuthAuthenticationProviderInterface.php create mode 100644 sources/app/Core/Security/PasswordAuthenticationProviderInterface.php create mode 100644 sources/app/Core/Security/PostAuthenticationProviderInterface.php create mode 100644 sources/app/Core/Security/PreAuthenticationProviderInterface.php create mode 100644 sources/app/Core/Security/Role.php create mode 100644 sources/app/Core/Security/SessionCheckProviderInterface.php create mode 100644 sources/app/Core/User/GroupSync.php create mode 100644 sources/app/Core/User/UserProfile.php create mode 100644 sources/app/Core/User/UserProperty.php create mode 100644 sources/app/Core/User/UserProviderInterface.php rename sources/app/{Model => Core/User}/UserSession.php (77%) create mode 100644 sources/app/Core/User/UserSync.php delete mode 100644 sources/app/Event/AuthEvent.php create mode 100644 sources/app/Event/AuthFailureEvent.php create mode 100644 sources/app/Event/AuthSuccessEvent.php create mode 100644 sources/app/Formatter/GroupAutoCompleteFormatter.php create mode 100644 sources/app/Formatter/UserFilterAutoCompleteFormatter.php create mode 100644 sources/app/Group/DatabaseBackendGroupProvider.php create mode 100644 sources/app/Group/DatabaseGroupProvider.php create mode 100644 sources/app/Group/LdapBackendGroupProvider.php create mode 100644 sources/app/Group/LdapGroupProvider.php delete mode 100644 sources/app/Model/Acl.php create mode 100644 sources/app/Model/Group.php create mode 100644 sources/app/Model/GroupMember.php create mode 100644 sources/app/Model/ProjectGroupRole.php create mode 100644 sources/app/Model/ProjectUserRole.php create mode 100644 sources/app/Model/RememberMeSession.php create mode 100644 sources/app/Model/UserFilter.php create mode 100644 sources/app/Model/UserLocking.php create mode 100644 sources/app/ServiceProvider/AuthenticationProvider.php create mode 100644 sources/app/ServiceProvider/GroupProvider.php create mode 100644 sources/app/ServiceProvider/NotificationProvider.php create mode 100644 sources/app/ServiceProvider/PluginProvider.php create mode 100644 sources/app/ServiceProvider/RouteProvider.php create mode 100644 sources/app/Template/group/associate.php create mode 100644 sources/app/Template/group/create.php create mode 100644 sources/app/Template/group/dissociate.php create mode 100644 sources/app/Template/group/edit.php create mode 100644 sources/app/Template/group/index.php create mode 100644 sources/app/Template/group/remove.php create mode 100644 sources/app/Template/group/users.php create mode 100644 sources/app/Template/project/roles.php delete mode 100644 sources/app/Template/project/users.php create mode 100644 sources/app/Template/project_permission/index.php create mode 100644 sources/app/User/DatabaseUserProvider.php create mode 100644 sources/app/User/GithubUserProvider.php create mode 100644 sources/app/User/GitlabUserProvider.php create mode 100644 sources/app/User/GoogleUserProvider.php create mode 100644 sources/app/User/LdapUserProvider.php create mode 100644 sources/app/User/OAuthUserProvider.php create mode 100644 sources/app/User/ReverseProxyUserProvider.php delete mode 100644 sources/app/routes.php create mode 100644 sources/assets/js/src/Project.js create mode 100644 sources/doc/api-action-procedures.markdown create mode 100644 sources/doc/api-application-procedures.markdown create mode 100644 sources/doc/api-authentication.markdown create mode 100644 sources/doc/api-board-procedures.markdown create mode 100644 sources/doc/api-category-procedures.markdown create mode 100644 sources/doc/api-comment-procedures.markdown create mode 100644 sources/doc/api-examples.markdown create mode 100644 sources/doc/api-file-procedures.markdown create mode 100644 sources/doc/api-link-procedures.markdown create mode 100644 sources/doc/api-me-procedures.markdown create mode 100644 sources/doc/api-project-procedures.markdown create mode 100644 sources/doc/api-subtask-procedures.markdown create mode 100644 sources/doc/api-swimlane-procedures.markdown create mode 100644 sources/doc/api-task-procedures.markdown create mode 100644 sources/doc/api-user-procedures.markdown create mode 100644 sources/doc/groups.markdown create mode 100644 sources/doc/ldap-parameters.markdown delete mode 100644 sources/doc/nginx-ssl-php-fpm.markdown create mode 100644 sources/doc/plugin-authentication-architecture.markdown create mode 100644 sources/doc/plugin-authentication.markdown create mode 100644 sources/doc/plugin-authorization-architecture.markdown create mode 100644 sources/doc/plugin-group-provider.markdown create mode 100644 sources/doc/plugin-ldap-client.markdown create mode 100644 sources/doc/project-types.markdown create mode 100644 sources/doc/roles.markdown create mode 100644 sources/doc/screenshots/groups-management.png create mode 100644 sources/doc/screenshots/project-permissions.png create mode 100644 sources/doc/user-types.markdown create mode 100755 sources/vendor/bin/html-to-markdown create mode 100644 sources/vendor/fguillot/picodb/lib/PicoDb/Driver/Mssql.php create mode 100644 sources/vendor/league/html-to-markdown/CONDUCT.md create mode 100755 sources/vendor/league/html-to-markdown/bin/html-to-markdown create mode 100644 sources/vendor/symfony/console/Exception/CommandNotFoundException.php create mode 100644 sources/vendor/symfony/console/Exception/ExceptionInterface.php create mode 100644 sources/vendor/symfony/console/Exception/InvalidArgumentException.php create mode 100644 sources/vendor/symfony/console/Exception/InvalidOptionException.php create mode 100644 sources/vendor/symfony/console/Exception/LogicException.php create mode 100644 sources/vendor/symfony/console/Exception/RuntimeException.php create mode 100644 sources/vendor/symfony/console/Helper/ProgressIndicator.php delete mode 100644 sources/vendor/symfony/console/Tests/ClockMock.php create mode 100644 sources/vendor/symfony/console/Tests/Fixtures/Foo6Command.php create mode 100644 sources/vendor/symfony/console/Tests/Helper/ProgressIndicatorTest.php create mode 100644 sources/vendor/symfony/polyfill-mbstring/LICENSE create mode 100644 sources/vendor/symfony/polyfill-mbstring/Mbstring.php create mode 100644 sources/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.ser create mode 100644 sources/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.ser create mode 100644 sources/vendor/symfony/polyfill-mbstring/bootstrap.php diff --git a/sources/ChangeLog b/sources/ChangeLog index 56bbcdf..7ad0649 100644 --- a/sources/ChangeLog +++ b/sources/ChangeLog @@ -1,11 +1,41 @@ +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 + - For Postgres and Mysql the unique constraint is removed by database migration + - However Sqlite does not support alter table, only new databases will have the unique constraint removed New features: diff --git a/sources/app/Action/TaskOpen.php b/sources/app/Action/TaskOpen.php index 2e53efa..2e84c69 100644 --- a/sources/app/Action/TaskOpen.php +++ b/sources/app/Action/TaskOpen.php @@ -3,6 +3,7 @@ namespace Kanboard\Action; use Kanboard\Integration\GithubWebhook; +use Kanboard\Integration\GitlabWebhook; use Kanboard\Integration\BitbucketWebhook; /** @@ -23,6 +24,7 @@ class TaskOpen extends Base { return array( GithubWebhook::EVENT_ISSUE_REOPENED, + GitlabWebhook::EVENT_ISSUE_REOPENED, BitbucketWebhook::EVENT_ISSUE_REOPENED, ); } diff --git a/sources/app/Api/Auth.php b/sources/app/Api/Auth.php index a084d6e..a9d1617 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->container['dispatcher']->dispatch('app.bootstrap'); - if ($username !== 'jsonrpc' && ! $this->authentication->hasCaptcha($username) && $this->authentication->authenticate($username, $password)) { + if ($this->isUserAuthenticated($username, $password)) { $this->checkProcedurePermission(true, $method); $this->userSession->initialize($this->user->getByUsername($username)); - } elseif ($username === 'jsonrpc' && $password === $this->config->get('api_token')) { + } elseif ($this->isAppAuthenticated($username, $password)) { $this->checkProcedurePermission(false, $method); } else { throw new AuthenticationFailure('Wrong credentials'); } } + + /** + * Check user credentials + * + * @access public + * @param string $username + * @param string $password + * @return boolean + */ + private function isUserAuthenticated($username, $password) + { + return $username !== 'jsonrpc' && + ! $this->userLocking->isLocked($username) && + $this->authenticationManager->passwordAuthentication($username, $password); + } + + /** + * Check administrative credentials + * + * @access public + * @param string $username + * @param string $password + * @return boolean + */ + private function isAppAuthenticated($username, $password) + { + return $username === 'jsonrpc' && $password === $this->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/Me.php b/sources/app/Api/Me.php index 2c4161f..3785173 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); } @@ -50,7 +50,7 @@ class Me extends Base public function getMyProjectsList() { - return $this->projectPermission->getMemberProjects($this->userSession->getId()); + return $this->projectUserRole->getProjectsByUser($this->userSession->getId()); } public function getMyOverdueTasks() @@ -60,7 +60,7 @@ class Me extends Base public function getMyProjects() { - $project_ids = $this->projectPermission->getActiveMemberProjectIds($this->userSession->getId()); + $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); $projects = $this->project->getAllByIds($project_ids); return $this->formatProjects($projects); diff --git a/sources/app/Api/ProjectPermission.php b/sources/app/Api/ProjectPermission.php index 8032339..d440819 100644 --- a/sources/app/Api/ProjectPermission.php +++ b/sources/app/Api/ProjectPermission.php @@ -2,6 +2,8 @@ namespace Kanboard\Api; +use Kanboard\Core\Security\Role; + /** * ProjectPermission API controller * @@ -12,16 +14,16 @@ class ProjectPermission extends \Kanboard\Core\Base { public function getMembers($project_id) { - return $this->projectPermission->getMembers($project_id); + return $this->projectUserRole->getAllUsers($project_id); } public function revokeUser($project_id, $user_id) { - return $this->projectPermission->revokeMember($project_id, $user_id); + return $this->projectUserRole->removeUser($project_id, $user_id); } public function allowUser($project_id, $user_id) { - return $this->projectPermission->addMember($project_id, $user_id); + return $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MEMBER); } } diff --git a/sources/app/Api/Task.php b/sources/app/Api/Task.php index 0dceb20..4a7ee93 100644 --- a/sources/app/Api/Task.php +++ b/sources/app/Api/Task.php @@ -71,6 +71,14 @@ class Task extends Base { $this->checkProjectPermission($project_id); + if ($owner_id !== 0 && ! $this->projectPermission->isMember($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 +104,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 && ! $this->projectPermission->isMember($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..06e305f 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); 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) { 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..69260fe --- /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..606ab30 --- /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..084e8ab --- /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..2a1f1b4 --- /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..85234ed --- /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..5a0e48e --- /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..f41fabd --- /dev/null +++ b/sources/app/Auth/TotpAuth.php @@ -0,0 +1,126 @@ +checkTotp(Base32::decode($this->secret), $this->code); + } + + /** + * Set validation code + * + * @access public + * @param string $code + */ + public function setCode($code) + { + $this->code = $code; + } + + /** + * Set secret token + * + * @access public + * @param string $secret + */ + public function setSecret($secret) + { + $this->secret = $secret; + } + + /** + * Get secret token + * + * @access public + * @return string + */ + public function getSecret() + { + if (empty($this->secret)) { + $this->secret = GoogleAuthenticator::generateRandom(); + } + + return $this->secret; + } + + /** + * Get QR code url + * + * @access public + * @param string $label + * @return string + */ + public function getQrCodeUrl($label) + { + if (empty($this->secret)) { + return ''; + } + + return GoogleAuthenticator::getQrCodeUrl('totp', $label, $this->secret); + } + + /** + * Get key url (empty if no url can be provided) + * + * @access public + * @param string $label + * @return string + */ + public function getKeyUrl($label) + { + if (empty($this->secret)) { + return ''; + } + + return GoogleAuthenticator::getKeyUri('totp', $label, $this->secret); + } +} diff --git a/sources/app/Controller/Action.php b/sources/app/Controller/Action.php index ad13606..3caea45 100644 --- a/sources/app/Controller/Action.php +++ b/sources/app/Controller/Action.php @@ -27,7 +27,7 @@ class Action extends Base 'available_events' => $this->action->getAvailableEvents(), 'available_params' => $this->action->getAllActionParameters(), 'columns_list' => $this->board->getColumnsList($project['id']), - 'users_list' => $this->projectPermission->getMemberList($project['id']), + 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), 'projects_list' => $this->project->getList(false), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), @@ -86,7 +86,7 @@ class Action extends Base 'values' => $values, 'action_params' => $action_params, 'columns_list' => $this->board->getColumnsList($project['id']), - 'users_list' => $this->projectPermission->getMemberList($project['id']), + 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), 'projects_list' => $projects_list, 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), diff --git a/sources/app/Controller/Activity.php b/sources/app/Controller/Activity.php index 24327c2..71d5e94 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->getProjectsByUser($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..e03d8ca 100644 --- a/sources/app/Controller/Analytic.php +++ b/sources/app/Controller/Analytic.php @@ -20,7 +20,7 @@ class Analytic extends Base */ private function layout($template, array $params) { - $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); $params['content_for_sublayout'] = $this->template->render($template, $params); return $this->template->layout('analytic/layout', $params); @@ -132,6 +132,9 @@ class Analytic extends Base * Common method for CFD and Burdown chart * * @access private + * @param string $template + * @param string $column + * @param string $title */ private function commonAggregateMetrics($template, $column, $title) { diff --git a/sources/app/Controller/App.php b/sources/app/Controller/App.php index 2fae004..c596b4a 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->getProjectsByUser($this->userSession->getId()); $params['content_for_sublayout'] = $this->template->render($template, $params); return $this->template->layout('app/layout', $params); @@ -42,7 +42,7 @@ class App extends Base ->setUrl('app', $action, array('pagination' => 'projects', 'user_id' => $user_id)) ->setMax($max) ->setOrder('name') - ->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id))) + ->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id))) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects'); } @@ -169,7 +169,7 @@ class App extends Base $this->response->html($this->layout('app/activity', array( 'title' => t('My activity stream'), - 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveMemberProjectIds($user['id']), 100), + 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100), 'user' => $user, ))); } @@ -202,49 +202,4 @@ class App extends Base 'user' => $user, ))); } - - /** - * Render Markdown text and reply with the HTML Code - * - * @access public - */ - public function preview() - { - $payload = $this->request->getJson(); - - if (empty($payload['text'])) { - $this->response->html('

'.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..cd1dd16 100644 --- a/sources/app/Controller/Auth.php +++ b/sources/app/Controller/Auth.php @@ -24,7 +24,7 @@ class Auth extends Base } $this->response->html($this->template->layout('auth/index', array( - 'captcha' => isset($values['username']) && $this->authentication->hasCaptcha($values['username']), + 'captcha' => ! empty($values['username']) && $this->userLocking->hasCaptcha($values['username']), 'errors' => $errors, 'values' => $values, 'no_layout' => true, @@ -40,18 +40,11 @@ class Auth extends Base public function check() { $values = $this->request->getValues(); + $this->sessionStorage->hasRememberMe = ! empty($values['remember_me']); list($valid, $errors) = $this->authentication->validateForm($values); if ($valid) { - if (isset($this->sessionStorage->redirectAfterLogin) - && ! empty($this->sessionStorage->redirectAfterLogin) - && ! filter_var($this->sessionStorage->redirectAfterLogin, FILTER_VALIDATE_URL)) { - $redirect = $this->sessionStorage->redirectAfterLogin; - unset($this->sessionStorage->redirectAfterLogin); - $this->response->redirect($redirect); - } - - $this->response->redirect($this->helper->url->to('app', 'index')); + $this->redirectAfterLogin(); } $this->login($values, $errors); @@ -64,7 +57,6 @@ class Auth extends Base */ public function logout() { - $this->authentication->backend('rememberMe')->destroy($this->userSession->getId()); $this->sessionManager->close(); $this->response->redirect($this->helper->url->to('auth', 'login')); } @@ -83,4 +75,20 @@ class Auth extends Base $this->sessionStorage->captcha = $builder->getPhrase(); $builder->output(); } + + /** + * Redirect the user after the authentication + * + * @access private + */ + private function redirectAfterLogin() + { + if (isset($this->sessionStorage->redirectAfterLogin) && ! empty($this->sessionStorage->redirectAfterLogin) && ! filter_var($this->sessionStorage->redirectAfterLogin, FILTER_VALIDATE_URL)) { + $redirect = $this->sessionStorage->redirectAfterLogin; + unset($this->sessionStorage->redirectAfterLogin); + $this->response->redirect($redirect); + } + + $this->response->redirect($this->helper->url->to('app', 'index')); + } } diff --git a/sources/app/Controller/Base.php b/sources/app/Controller/Base.php index 8630f00..35ceee0 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($controller, $action) { - if (DEBUG) { - foreach ($this->db->getLogMessages() as $message) { - $this->logger->debug($message); - } + $this->sessionManager->open(); + $this->dispatcher->dispatch('app.bootstrap'); + $this->sendHeaders($action); + $this->authenticationManager->checkCurrentSession(); - $this->logger->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries)); - $this->logger->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT'])); - $this->logger->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage())); - $this->logger->debug('END_REQUEST='.$_SERVER['REQUEST_URI']); + if (! $this->applicationAuthorization->isAllowed($controller, $action, Role::APP_PUBLIC)) { + $this->handleAuthentication(); + $this->handlePostAuthentication($controller, $action); + $this->checkApplicationAuthorization($controller, $action); + $this->checkProjectAuthorization($controller, $action); } } @@ -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,15 @@ abstract class Base extends \Kanboard\Core\Base } /** - * Check 2FA + * Handle Post-Authentication (2FA) * - * @access public + * @access private */ - public function handle2FA($controller, $action) + private function handlePostAuthentication($controller, $action) { $ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout'); - if ($ignore === false && $this->userSession->has2FA() && ! $this->userSession->check2FA()) { + if ($ignore === false && $this->userSession->hasPostAuthentication() && ! $this->userSession->isPostAuthenticationValidated()) { if ($this->request->isAjax()) { $this->response->text('Not Authorized', 401); } @@ -125,11 +90,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($controller, $action) + { + if (! $this->helper->user->hasAccess($controller, $action)) { + $this->forbidden(); + } + } + + /** + * Check project authorization + * + * @access private + */ + private function checkProjectAuthorization($controller, $action) { $project_id = $this->request->getIntegerParam('project_id'); $task_id = $this->request->getIntegerParam('task_id'); @@ -139,7 +116,7 @@ abstract class Base extends \Kanboard\Core\Base $project_id = $this->taskFinder->getProjectId($task_id); } - if (! $this->acl->isAllowed($controller, $action, $project_id)) { + if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($controller, $action, $project_id)) { $this->forbidden(); } } @@ -147,10 +124,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 +138,15 @@ abstract class Base extends \Kanboard\Core\Base /** * Application forbidden page * - * @access public + * @access protected * @param boolean $no_layout Display the layout or not */ - public function forbidden($no_layout = false) + protected function forbidden($no_layout = false) { + if ($this->request->isAjax()) { + $this->response->text('Not Authorized', 401); + } + $this->response->html($this->template->layout('app/forbidden', array( 'title' => t('Access Forbidden'), 'no_layout' => $no_layout, @@ -209,7 +190,7 @@ abstract class Base extends \Kanboard\Core\Base $content = $this->template->render($template, $params); $params['task_content_for_layout'] = $content; $params['title'] = $params['task']['project_name'].' > '.$params['task']['title']; - $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); return $this->template->layout('task/layout', $params); } @@ -227,7 +208,7 @@ abstract class Base extends \Kanboard\Core\Base $content = $this->template->render($template, $params); $params['project_content_for_layout'] = $content; $params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' > '.$params['title']; - $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); $params['sidebar_template'] = $sidebar_template; return $this->template->layout('project/layout', $params); @@ -300,12 +281,15 @@ abstract class Base extends \Kanboard\Core\Base * Common method to get project filters * * @access protected + * @param string $controller + * @param string $action + * @return array */ protected function getProjectFilters($controller, $action) { $project = $this->getProject(); $search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id'])); - $board_selector = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $board_selector = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); unset($board_selector[$project['id']]); $filters = array( diff --git a/sources/app/Controller/Board.php b/sources/app/Controller/Board.php index 7442ff2..a75fea3 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'], @@ -142,195 +142,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 +166,7 @@ class Board extends Base * Change display mode * * @access private + * @param boolean $mode */ private function changeDisplayMode($mode) { @@ -372,6 +184,7 @@ class Board extends Base * Render board * * @access private + * @param integer $project_id */ private function renderBoard($project_id) { diff --git a/sources/app/Controller/BoardPopover.php b/sources/app/Controller/BoardPopover.php new file mode 100644 index 0000000..51ec9bc --- /dev/null +++ b/sources/app/Controller/BoardPopover.php @@ -0,0 +1,101 @@ +getTask(); + $project = $this->project->getById($task['project_id']); + + $this->response->html($this->template->render('board/popover_assignee', array( + 'values' => $task, + 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), + 'project' => $project, + ))); + } + + /** + * Validate an assignee modification + * + * @access public + */ + public function updateAssignee() + { + $values = $this->request->getValues(); + + list($valid, ) = $this->taskValidator->validateAssigneeModification($values); + + if ($valid && $this->taskModification->update($values)) { + $this->flash->success(t('Task updated successfully.')); + } else { + $this->flash->failure(t('Unable to update your task.')); + } + + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id']))); + } + + /** + * Change a task category directly from the board + * + * @access public + */ + public function changeCategory() + { + $task = $this->getTask(); + $project = $this->project->getById($task['project_id']); + + $this->response->html($this->template->render('board/popover_category', array( + 'values' => $task, + 'categories_list' => $this->category->getList($project['id']), + 'project' => $project, + ))); + } + + /** + * Validate a category modification + * + * @access public + */ + public function updateCategory() + { + $values = $this->request->getValues(); + + list($valid, ) = $this->taskValidator->validateCategoryModification($values); + + if ($valid && $this->taskModification->update($values)) { + $this->flash->success(t('Task updated successfully.')); + } else { + $this->flash->failure(t('Unable to update your task.')); + } + + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id']))); + } + + /** + * Screenshot popover + * + * @access public + */ + public function screenshot() + { + $task = $this->getTask(); + + $this->response->html($this->template->render('file/screenshot', array( + 'task' => $task, + 'redirect' => 'board', + ))); + } +} diff --git a/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/Config.php b/sources/app/Controller/Config.php index 4980614..c813c79 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->getProjectsByUser($this->userSession->getId()); $params['values'] = $this->config->getAll(); $params['errors'] = array(); $params['config_content_for_layout'] = $this->template->render($template, $params); diff --git a/sources/app/Controller/Currency.php b/sources/app/Controller/Currency.php index 118b2c4..89e3856 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->getProjectsByUser($this->userSession->getId()); $params['config_content_for_layout'] = $this->template->render($template, $params); return $this->template->layout('config/layout', $params); diff --git a/sources/app/Controller/Customfilter.php b/sources/app/Controller/Customfilter.php index d686310..12cc8e7 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 * @@ -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..08561aa 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->getProjectsByUser($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..f3954a2 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->getProjectsByUser($this->userSession->getId()), ))); } @@ -66,7 +66,7 @@ class Gantt extends Base } $this->response->html($this->template->layout('gantt/project', $params + array( - 'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false), + 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false), 'sorting' => $sorting, 'tasks' => $filter->format(), ))); @@ -109,7 +109,7 @@ class Gantt extends Base 'column_id' => $this->board->getFirstColumn($project['id']), 'position' => 1 ), - 'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true), + 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), 'swimlanes_list' => $this->swimlane->getList($project['id'], false, true), diff --git a/sources/app/Controller/Group.php b/sources/app/Controller/Group.php new file mode 100644 index 0000000..395a954 --- /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->getProjectsByUser($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') + ->setMax(30) + ->setOrder('username') + ->setQuery($this->groupMember->getQuery($group_id)) + ->calculate(); + + $this->response->html($this->template->layout('group/users', array( + 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), + '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->getProjectsByUser($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->group->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->getProjectsByUser($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->group->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->getProjectsByUser($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..33ec668 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->getProjectsByUser($this->userSession->getId()); $params['config_content_for_layout'] = $this->template->render($template, $params); return $this->template->layout('config/layout', $params); 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/Project.php b/sources/app/Controller/Project.php index 2d9c25d..80c95aa 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->getProjectsByUser($this->userSession->getId()), 'paginator' => $paginator, 'nb_projects' => $nb_projects, 'title' => t('Projects').' ('.$nb_projects.')' @@ -160,11 +160,11 @@ class Project extends Base $values = $this->request->getValues(); if (isset($values['is_private'])) { - if (! $this->helper->user->isProjectAdministrationAllowed($project['id'])) { + if (! $this->helper->user->hasProjectAccess('project', 'create', $project['id'])) { unset($values['is_private']); } } elseif ($project['is_private'] == 1 && ! isset($values['is_private'])) { - if ($this->helper->user->isProjectAdministrationAllowed($project['id'])) { + if ($this->helper->user->hasProjectAccess('project', 'create', $project['id'])) { $values += array('is_private' => 0); } } @@ -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->getProjectsByUser($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 * 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..3459576 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->getProjectsByUser($this->userSession->getId()); $params['content_for_sublayout'] = $this->template->render($template, $params); $params['filter'] = array('user_id' => $params['user_id']); @@ -37,17 +38,17 @@ class Projectuser extends Base if ($this->userSession->isAdmin()) { $project_ids = $this->project->getAllIds(); } else { - $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId()); + $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); } return array($user_id, $project_ids, $this->user->getList(true)); } - private function role($is_owner, $action, $title, $title_user) + private function role($role, $action, $title, $title_user) { list($user_id, $project_ids, $users) = $this->common(); - $query = $this->projectPermission->getQueryByRole($project_ids, $is_owner)->callback(array($this->project, 'applyColumnStats')); + $query = $this->projectPermission->getQueryByRole($project_ids, $role)->callback(array($this->project, 'applyColumnStats')); if ($user_id !== UserModel::EVERYBODY_ID) { $query->eq(UserModel::TABLE.'.id', $user_id); @@ -101,7 +102,7 @@ class Projectuser extends Base */ public function managers() { - $this->role(1, 'managers', t('People who are project managers'), 'Projects where "%s" is manager'); + $this->role(Role::PROJECT_MANAGER, 'managers', t('People who are project managers'), 'Projects where "%s" is manager'); } /** @@ -110,7 +111,7 @@ class Projectuser extends Base */ public function members() { - $this->role(0, 'members', t('People who are project members'), 'Projects where "%s" is member'); + $this->role(ROLE::PROJECT_MEMBER, 'members', t('People who are project members'), 'Projects where "%s" is member'); } /** diff --git a/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..c93b637 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, ))); } @@ -95,7 +95,7 @@ class Subtask extends Base $this->response->html($this->taskLayout('subtask/edit', array( 'values' => empty($values) ? $subtask : $values, 'errors' => $errors, - 'users_list' => $this->projectPermission->getMemberList($task['project_id']), + 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']), 'status_list' => $this->subtask->getStatusList(), 'subtask' => $subtask, 'task' => $task, diff --git a/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/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..aeb13ac 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 * @@ -36,12 +32,15 @@ class Twofactor extends User $user = $this->getUser(); $this->checkCurrentUser($user); + $provider = $this->authenticationManager->getPostAuthenticationProvider(); $label = $user['email'] ?: $user['username']; + $provider->setSecret($user['twofactor_secret']); + $this->response->html($this->layout('twofactor/index', array( 'user' => $user, - 'qrcode_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getQrCodeUrl('totp', $label, $user['twofactor_secret']) : '', - 'key_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getKeyUri('totp', $label, $user['twofactor_secret']) : '', + 'qrcode_url' => $user['twofactor_activated'] == 1 ? $provider->getQrCodeUrl($label) : '', + 'key_url' => $user['twofactor_activated'] == 1 ? $provider->getKeyUrl($label) : '', ))); } @@ -61,7 +60,7 @@ class Twofactor extends User $this->user->update(array( 'id' => $user['id'], 'twofactor_activated' => 1, - 'twofactor_secret' => GoogleAuthenticator::generateRandom(), + 'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(), )); } else { $this->user->update(array( @@ -72,14 +71,14 @@ class Twofactor extends User } // Allow the user to test or disable the feature - $this->userSession->disable2FA(); + $this->userSession->disablePostAuthentication(); $this->flash->success(t('User updated successfully.')); $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); } /** - * Test 2FA + * Test code * * @access public */ @@ -88,10 +87,13 @@ class Twofactor extends User $user = $this->getUser(); $this->checkCurrentUser($user); - $otp = new Otp; $values = $this->request->getValues(); - if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) { + $provider = $this->authenticationManager->getPostAuthenticationProvider(); + $provider->setCode(empty($values['code']) ? '' : $values['code']); + $provider->setSecret($user['twofactor_secret']); + + if ($provider->authenticate()) { $this->flash->success(t('The two factor authentication code is valid.')); } else { $this->flash->failure(t('The two factor authentication code is not valid.')); @@ -110,11 +112,14 @@ class Twofactor extends User $user = $this->getUser(); $this->checkCurrentUser($user); - $otp = new Otp; $values = $this->request->getValues(); - if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) { - $this->sessionStorage->postAuth['validated'] = true; + $provider = $this->authenticationManager->getPostAuthenticationProvider(); + $provider->setCode(empty($values['code']) ? '' : $values['code']); + $provider->setSecret($user['twofactor_secret']); + + if ($provider->authenticate()) { + $this->userSession->validatePostAuthentication(); $this->flash->success(t('The two factor authentication code is valid.')); $this->response->redirect($this->helper->url->to('app', 'index')); } else { diff --git a/sources/app/Controller/User.php b/sources/app/Controller/User.php index 23e1982..aa54864 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->getProjectsByUser($this->userSession->getId()); if (isset($params['user'])) { $params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')'; @@ -49,7 +51,7 @@ class User extends Base $this->response->html( $this->template->layout('user/index', array( - 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), 'title' => t('Users').' ('.$paginator->getTotal().')', 'paginator' => $paginator, ))); @@ -67,10 +69,11 @@ class User extends Base $this->response->html($this->template->layout($is_remote ? 'user/create_remote' : 'user/create_local', array( 'timezones' => $this->config->getTimezones(true), 'languages' => $this->config->getLanguages(true), - 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'roles' => $this->role->getApplicationRoles(), + 'board_selector' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), 'projects' => $this->project->getList(), 'errors' => $errors, - 'values' => $values, + 'values' => $values + array('role' => Role::APP_USER), 'title' => t('New user') ))); } @@ -92,7 +95,7 @@ class User extends Base $user_id = $this->user->create($values); if ($user_id !== false) { - $this->projectPermission->addMember($project_id, $user_id); + $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MEMBER); if (! empty($values['notifications_enabled'])) { $this->userNotificationType->saveSelectedTypes($user_id, array(MailNotification::TYPE)); @@ -170,7 +173,7 @@ class User extends Base { $user = $this->getUser(); $this->response->html($this->layout('user/sessions', array( - 'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']), + 'sessions' => $this->rememberMeSession->getAll($user['id']), 'user' => $user, ))); } @@ -184,8 +187,8 @@ class User extends Base { $this->checkCSRFParam(); $user = $this->getUser(); - $this->authentication->backend('rememberMe')->remove($this->request->getIntegerParam('id')); - $this->response->redirect($this->helper->url->to('user', 'session', array('user_id' => $user['id']))); + $this->rememberMeSession->remove($this->request->getIntegerParam('id')); + $this->response->redirect($this->helper->url->to('user', 'sessions', array('user_id' => $user['id']))); } /** @@ -205,7 +208,7 @@ class User extends Base } $this->response->html($this->layout('user/notifications', array( - 'projects' => $this->projectPermission->getMemberProjects($user['id']), + 'projects' => $this->projectUserRole->getProjectsByUser($user['id'], array(ProjectModel::ACTIVE)), 'notifications' => $this->userNotification->readSettings($user['id']), 'types' => $this->userNotificationType->getTypes(), 'filters' => $this->userNotificationFilter->getFilters(), @@ -326,16 +329,9 @@ class User extends Base if ($this->request->isPost()) { $values = $this->request->getValues(); - if ($this->userSession->isAdmin()) { - $values += array('is_admin' => 0, 'is_project_admin' => 0); - } else { - // Regular users can't be admin - if (isset($values['is_admin'])) { - unset($values['is_admin']); - } - - if (isset($values['is_project_admin'])) { - unset($values['is_project_admin']); + if (! $this->userSession->isAdmin()) { + if (isset($values['role'])) { + unset($values['role']); } } @@ -358,6 +354,7 @@ class User extends Base 'user' => $user, 'timezones' => $this->config->getTimezones(true), 'languages' => $this->config->getLanguages(true), + 'roles' => $this->role->getApplicationRoles(), ))); } diff --git a/sources/app/Controller/UserHelper.php b/sources/app/Controller/UserHelper.php new file mode 100644 index 0000000..f164d0a --- /dev/null +++ b/sources/app/Controller/UserHelper.php @@ -0,0 +1,24 @@ +request->getStringParam('term'); + $users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format(); + $this->response->json($users); + } +} diff --git a/sources/app/Core/Base.php b/sources/app/Core/Base.php index d317102..2d00e52 100644 --- a/sources/app/Core/Base.php +++ b/sources/app/Core/Base.php @@ -5,29 +5,43 @@ namespace Kanboard\Core; use Pimple\Container; /** - * Base class + * Base Class * * @package core * @author Frederic Guillot * - * @property \Kanboard\Core\Session\SessionManager $sessionManager - * @property \Kanboard\Core\Session\SessionStorage $sessionStorage - * @property \Kanboard\Core\Session\FlashMessage $flash - * @property \Kanboard\Core\Helper $helper - * @property \Kanboard\Core\Mail\Client $emailClient - * @property \Kanboard\Core\Paginator $paginator + * @property \Kanboard\Core\Cache\MemoryCache $memoryCache + * @property \Kanboard\Core\Group\GroupManager $groupManager * @property \Kanboard\Core\Http\Client $httpClient + * @property \Kanboard\Core\Http\OAuth2 $oauth + * @property \Kanboard\Core\Http\RememberMeCookie $rememberMeCookie * @property \Kanboard\Core\Http\Request $request - * @property \Kanboard\Core\Http\Router $router * @property \Kanboard\Core\Http\Response $response - * @property \Kanboard\Core\Template $template - * @property \Kanboard\Core\OAuth2 $oauth - * @property \Kanboard\Core\Lexer $lexer + * @property \Kanboard\Core\Http\Router $router + * @property \Kanboard\Core\Mail\Client $emailClient * @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage - * @property \Kanboard\Core\Cache\Cache $memoryCache * @property \Kanboard\Core\Plugin\Hook $hook * @property \Kanboard\Core\Plugin\Loader $pluginLoader + * @property \Kanboard\Core\Security\AccessMap $projectAccessMap + * @property \Kanboard\Core\Security\AuthenticationManager $authenticationManager + * @property \Kanboard\Core\Security\AccessMap $applicationAccessMap + * @property \Kanboard\Core\Security\AccessMap $projectAccessMap + * @property \Kanboard\Core\Security\Authorization $applicationAuthorization + * @property \Kanboard\Core\Security\Authorization $projectAuthorization + * @property \Kanboard\Core\Security\Role $role * @property \Kanboard\Core\Security\Token $token + * @property \Kanboard\Core\Session\FlashMessage $flash + * @property \Kanboard\Core\Session\SessionManager $sessionManager + * @property \Kanboard\Core\Session\SessionStorage $sessionStorage + * @property \Kanboard\Core\User\GroupSync $groupSync + * @property \Kanboard\Core\User\UserProfile $userProfile + * @property \Kanboard\Core\User\UserSync $userSync + * @property \Kanboard\Core\User\UserSession $userSession + * @property \Kanboard\Core\DateParser $dateParser + * @property \Kanboard\Core\Helper $helper + * @property \Kanboard\Core\Lexer $lexer + * @property \Kanboard\Core\Paginator $paginator + * @property \Kanboard\Core\Template $template * @property \Kanboard\Integration\BitbucketWebhook $bitbucketWebhook * @property \Kanboard\Integration\GithubWebhook $githubWebhook * @property \Kanboard\Integration\GitlabWebhook $gitlabWebhook @@ -36,7 +50,8 @@ use Pimple\Container; * @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter * @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter * @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter - * @property \Kanboard\Model\Acl $acl + * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter + * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter * @property \Kanboard\Model\Action $action * @property \Kanboard\Model\Authentication $authentication * @property \Kanboard\Model\Board $board @@ -46,8 +61,9 @@ use Pimple\Container; * @property \Kanboard\Model\Config $config * @property \Kanboard\Model\Currency $currency * @property \Kanboard\Model\CustomFilter $customFilter - * @property \Kanboard\Model\DateParser $dateParser * @property \Kanboard\Model\File $file + * @property \Kanboard\Model\Group $group + * @property \Kanboard\Model\GroupMember $groupMember * @property \Kanboard\Model\LastLogin $lastLogin * @property \Kanboard\Model\Link $link * @property \Kanboard\Model\Notification $notification @@ -60,8 +76,11 @@ use Pimple\Container; * @property \Kanboard\Model\ProjectDailyStats $projectDailyStats * @property \Kanboard\Model\ProjectMetadata $projectMetadata * @property \Kanboard\Model\ProjectPermission $projectPermission + * @property \Kanboard\Model\ProjectUserRole $projectUserRole + * @property \Kanboard\Model\ProjectGroupRole $projectGroupRole * @property \Kanboard\Model\ProjectNotification $projectNotification * @property \Kanboard\Model\ProjectNotificationType $projectNotificationType + * @property \Kanboard\Model\RememberMeSession $rememberMeSession * @property \Kanboard\Model\Subtask $subtask * @property \Kanboard\Model\SubtaskExport $subtaskExport * @property \Kanboard\Model\SubtaskTimeTracking $subtaskTimeTracking @@ -84,16 +103,17 @@ use Pimple\Container; * @property \Kanboard\Model\Transition $transition * @property \Kanboard\Model\User $user * @property \Kanboard\Model\UserImport $userImport + * @property \Kanboard\Model\UserLocking $userLocking * @property \Kanboard\Model\UserNotification $userNotification * @property \Kanboard\Model\UserNotificationType $userNotificationType * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter * @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification - * @property \Kanboard\Model\UserSession $userSession * @property \Kanboard\Model\UserMetadata $userMetadata * @property \Kanboard\Model\Webhook $webhook * @property \Psr\Log\LoggerInterface $logger * @property \League\HTMLToMarkdown\HtmlConverter $htmlConverter * @property \PicoDb\Database $db + * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher */ abstract class Base { diff --git a/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/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..c626f5b 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,35 @@ use Kanboard\Core\Base; class Request extends Base { /** - * Get URL string parameter + * Pointer to PHP environment variables + * + * @access private + * @var array + */ + private $server; + private $get; + private $post; + private $files; + private $cookies; + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container, array $server = array(), array $get = array(), array $post = array(), array $files = array(), array $cookies = array()) + { + parent::__construct($container); + $this->server = empty($server) ? $_SERVER : $server; + $this->get = empty($get) ? $_GET : $get; + $this->post = empty($post) ? $_POST : $post; + $this->files = empty($files) ? $_FILES : $files; + $this->cookies = empty($cookies) ? $_COOKIE : $cookies; + } + + /** + * Get query string string parameter * * @access public * @param string $name Parameter name @@ -22,11 +51,11 @@ class Request extends Base */ public function getStringParam($name, $default_value = '') { - return isset($_GET[$name]) ? $_GET[$name] : $default_value; + return isset($this->get[$name]) ? $this->get[$name] : $default_value; } /** - * Get URL integer parameter + * Get query string integer parameter * * @access public * @param string $name Parameter name @@ -35,7 +64,7 @@ class Request extends Base */ public function getIntegerParam($name, $default_value = 0) { - return isset($_GET[$name]) && ctype_digit($_GET[$name]) ? (int) $_GET[$name] : $default_value; + return isset($this->get[$name]) && ctype_digit($this->get[$name]) ? (int) $this->get[$name] : $default_value; } /** @@ -59,9 +88,9 @@ class Request extends Base */ public function getValues() { - if (! empty($_POST) && ! empty($_POST['csrf_token']) && $this->token->validateCSRFToken($_POST['csrf_token'])) { - unset($_POST['csrf_token']); - return $_POST; + if (! empty($this->post) && ! empty($this->post['csrf_token']) && $this->token->validateCSRFToken($this->post['csrf_token'])) { + unset($this->post['csrf_token']); + return $this->post; } return array(); @@ -98,8 +127,8 @@ class Request extends Base */ public function getFileContent($name) { - if (isset($_FILES[$name])) { - return file_get_contents($_FILES[$name]['tmp_name']); + if (isset($this->files[$name]['tmp_name'])) { + return file_get_contents($this->files[$name]['tmp_name']); } return ''; @@ -114,7 +143,7 @@ class Request extends Base */ public function getFilePath($name) { - return isset($_FILES[$name]['tmp_name']) ? $_FILES[$name]['tmp_name'] : ''; + return isset($this->files[$name]['tmp_name']) ? $this->files[$name]['tmp_name'] : ''; } /** @@ -125,7 +154,7 @@ class Request extends Base */ public function isPost() { - return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST'; + return isset($this->server['REQUEST_METHOD']) && $this->server['REQUEST_METHOD'] === 'POST'; } /** @@ -144,13 +173,24 @@ class Request extends Base * * Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS * - * @static * @access public * @return boolean */ - public static function isHTTPS() + public function isHTTPS() { - return isset($_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,7 +203,18 @@ class Request extends Base public function getHeader($name) { $name = 'HTTP_'.str_replace('-', '_', strtoupper($name)); - return isset($_SERVER[$name]) ? $_SERVER[$name] : ''; + return isset($this->server[$name]) ? $this->server[$name] : ''; + } + + /** + * Get remote user + * + * @access public + * @return string + */ + public function getRemoteUser() + { + return isset($this->server[REVERSE_PROXY_USER_HEADER]) ? $this->server[REVERSE_PROXY_USER_HEADER] : ''; } /** @@ -174,41 +225,38 @@ class Request extends Base */ public function getQueryString() { - return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; + return isset($this->server['QUERY_STRING']) ? $this->server['QUERY_STRING'] : ''; } /** - * Returns uri + * Return URI * * @access public * @return string */ public function getUri() { - return isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; + return isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : ''; } /** * Get the user agent * - * @static * @access public * @return string */ - public static function getUserAgent() + public function getUserAgent() { - return empty($_SERVER['HTTP_USER_AGENT']) ? t('Unknown') : $_SERVER['HTTP_USER_AGENT']; + return empty($this->server['HTTP_USER_AGENT']) ? t('Unknown') : $this->server['HTTP_USER_AGENT']; } /** - * Get the real IP address of the user + * Get the IP address of the user * - * @static * @access public - * @param bool $only_public Return only public IP address * @return string */ - public static function getIpAddress($only_public = false) + public function getIpAddress() { $keys = array( 'HTTP_CLIENT_IP', @@ -221,23 +269,24 @@ class Request extends Base ); foreach ($keys as $key) { - if (isset($_SERVER[$key])) { - foreach (explode(',', $_SERVER[$key]) as $ip_address) { - $ip_address = trim($ip_address); - - if ($only_public) { - - // Return only public IP address - if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { - return $ip_address; - } - } else { - return $ip_address; - } + if (! empty($this->server[$key])) { + foreach (explode(',', $this->server[$key]) as $ipAddress) { + return trim($ipAddress); } } } return t('Unknown'); } + + /** + * Get start time + * + * @access public + * @return float + */ + public function getStartTime() + { + return isset($this->server['REQUEST_TIME_FLOAT']) ? $this->server['REQUEST_TIME_FLOAT'] : 0; + } } diff --git a/sources/app/Core/Http/Response.php b/sources/app/Core/Http/Response.php index c5a5d3c..fc21401 100644 --- a/sources/app/Core/Http/Response.php +++ b/sources/app/Core/Http/Response.php @@ -257,7 +257,7 @@ class Response extends Base */ public function hsts() { - if (Request::isHTTPS()) { + if ($this->request->isHTTPS()) { header('Strict-Transport-Security: max-age=31536000'); } } diff --git a/sources/app/Core/Ldap/Client.php b/sources/app/Core/Ldap/Client.php new file mode 100644 index 0000000..5d481cd --- /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..e67dd62 --- /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..e11e8ec --- /dev/null +++ b/sources/app/Core/Ldap/Group.php @@ -0,0 +1,130 @@ +query = $query; + } + + /** + * Get groups + * + * @static + * @access public + * @param Client $client + * @param string $query + * @return array + */ + public static function getGroups(Client $client, $query) + { + $self = new self(new Query($client)); + return $self->find($query); + } + + /** + * Find groups + * + * @access public + * @param string $query + * @return array + */ + public function find($query) + { + $this->query->execute($this->getBasDn(), $query, $this->getAttributes()); + $groups = array(); + + if ($this->query->hasResult()) { + $groups = $this->build(); + } + + return $groups; + } + + /** + * Build groups list + * + * @access protected + * @return array + */ + protected function build() + { + $groups = array(); + + foreach ($this->query->getEntries()->getAll() as $entry) { + $groups[] = new LdapGroupProvider($entry->getDn(), $entry->getFirstValue($this->getAttributeName())); + } + + return $groups; + } + + /** + * Ge the list of attributes to fetch when reading the LDAP group entry + * + * Must returns array with index that start at 0 otherwise ldap_search returns a warning "Array initialization wrong" + * + * @access public + * @return array + */ + public function getAttributes() + { + return array_values(array_filter(array( + $this->getAttributeName(), + ))); + } + + /** + * Get LDAP group name attribute + * + * @access public + * @return string + */ + public function getAttributeName() + { + if (! LDAP_GROUP_ATTRIBUTE_NAME) { + throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_GROUP_ATTRIBUTE_NAME'); + } + + return LDAP_GROUP_ATTRIBUTE_NAME; + } + + /** + * Get LDAP group base DN + * + * @access public + * @return string + */ + public function getBasDn() + { + if (! LDAP_GROUP_BASE_DN) { + throw new LogicException('LDAP group base DN empty, check the parameter LDAP_GROUP_BASE_DN'); + } + + return LDAP_GROUP_BASE_DN; + } +} diff --git a/sources/app/Core/Ldap/Query.php b/sources/app/Core/Ldap/Query.php new file mode 100644 index 0000000..6ca4bc9 --- /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..0c9df63 --- /dev/null +++ b/sources/app/Core/Ldap/User.php @@ -0,0 +1,223 @@ +query = $query; + } + + /** + * Get user profile + * + * @static + * @access public + * @param Client $client + * @param string $username + * @return array + */ + public static function getUser(Client $client, $username) + { + $self = new self(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/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/Security/AccessMap.php b/sources/app/Core/Security/AccessMap.php new file mode 100644 index 0000000..02a4ca4 --- /dev/null +++ b/sources/app/Core/Security/AccessMap.php @@ -0,0 +1,155 @@ +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; + } + + /** + * 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 application roles + * + * @access public + * @param string $role + * @return string + */ + public function getRoleName($role) + { + $roles = $this->getApplicationRoles() + $this->getProjectRoles(); + return isset($roles[$role]) ? $roles[$role] : t('Unknown'); + } +} diff --git a/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 ); diff --git a/sources/app/Core/Session/SessionStorage.php b/sources/app/Core/Session/SessionStorage.php index 703d2fb..f55a7ee 100644 --- a/sources/app/Core/Session/SessionStorage.php +++ b/sources/app/Core/Session/SessionStorage.php @@ -12,12 +12,13 @@ namespace Kanboard\Core\Session; * @property array $user * @property array $flash * @property array $csrf - * @property array $postAuth + * @property array $postAuthenticationValidated * @property array $filters * @property string $redirectAfterLogin * @property string $captcha * @property string $commentSorting * @property bool $hasSubtaskInProgress + * @property bool $hasRememberMe * @property bool $boardCollapsed */ class SessionStorage @@ -60,6 +61,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/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..d65a986 --- /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 (empty(LDAP_GROUP_FILTER)) { + throw new LogicException('LDAP group filter empty, check the parameter LDAP_GROUP_FILTER'); + } + + return sprintf(LDAP_GROUP_FILTER, $input); + } +} diff --git a/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/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/Url.php b/sources/app/Helper/Url.php index edb2684..6ada806 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; /** @@ -125,7 +124,7 @@ class Url extends Base return 'http://localhost/'; } - $url = Request::isHTTPS() ? 'https://' : 'http://'; + $url = $this->request->isHTTPS() ? 'https://' : 'http://'; $url .= $_SERVER['SERVER_NAME']; $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; $url .= $this->dir() ?: '/'; diff --git a/sources/app/Helper/User.php b/sources/app/Helper/User.php index 9ef20b3..101d8db 100644 --- a/sources/app/Helper/User.php +++ b/sources/app/Helper/User.php @@ -65,6 +65,7 @@ class User extends \Kanboard\Core\Base array('user_id' => $this->userSession->getId()) ); } + /** * Check if the given user_id is the connected user * @@ -88,44 +89,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/GitlabWebhook.php b/sources/app/Integration/GitlabWebhook.php index b3f9b0b..17b6da7 100644 --- a/sources/app/Integration/GitlabWebhook.php +++ b/sources/app/Integration/GitlabWebhook.php @@ -19,6 +19,7 @@ class GitlabWebhook extends \Kanboard\Core\Base */ const EVENT_ISSUE_OPENED = 'gitlab.webhook.issue.opened'; const EVENT_ISSUE_CLOSED = 'gitlab.webhook.issue.closed'; + const EVENT_ISSUE_REOPENED = 'gitlab.webhook.issue.reopened'; const EVENT_COMMIT = 'gitlab.webhook.commit'; const EVENT_ISSUE_COMMENT = 'gitlab.webhook.issue.commented'; @@ -164,6 +165,8 @@ class GitlabWebhook extends \Kanboard\Core\Base return $this->handleIssueOpened($payload['object_attributes']); case 'close': return $this->handleIssueClosed($payload['object_attributes']); + case 'reopen': + return $this->handleIssueReopened($payload['object_attributes']); } return false; @@ -193,6 +196,36 @@ class GitlabWebhook extends \Kanboard\Core\Base return true; } + /** + * Handle issue reopening + * + * @access public + * @param array $issue Issue data + * @return boolean + */ + public function handleIssueReopened(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_REOPENED, + new GenericEvent($event) + ); + + return true; + } + + return false; + } + + /** * Handle issue closing * diff --git a/sources/app/Locale/bs_BA/translations.php b/sources/app/Locale/bs_BA/translations.php index 9faa10e..08fd382 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -724,7 +710,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 +735,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', @@ -931,7 +917,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 +940,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 +966,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,7 +1022,7 @@ 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', @@ -1064,8 +1047,56 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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', ); diff --git a/sources/app/Locale/cs_CZ/translations.php b/sources/app/Locale/cs_CZ/translations.php index 5d6af0b..6b5ad8d 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -931,7 +917,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 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/da_DK/translations.php b/sources/app/Locale/da_DK/translations.php index a5ac1d3..1428626 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', @@ -546,8 +538,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' => '', @@ -570,10 +560,6 @@ return array( // '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' => '', @@ -931,7 +917,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/de_DE/translations.php b/sources/app/Locale/de_DE/translations.php index 13fa754..e244ede 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -931,7 +917,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 +940,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 +966,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 +1010,93 @@ 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', + 'There is no notification method registered.' => 'Es ist keine Benachrichtigungsmethode registriert', + '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', + 'Gitlab issue reopened' => 'Gitlab Thema wiedereröffnet', + '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', ); diff --git a/sources/app/Locale/es_ES/translations.php b/sources/app/Locale/es_ES/translations.php index 02c6961..b6ad2df 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -931,7 +917,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 +940,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 +966,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', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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', ); diff --git a/sources/app/Locale/fi_FI/translations.php b/sources/app/Locale/fi_FI/translations.php index bff0f05..e4e18ab 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( // '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' => '', @@ -931,7 +917,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/fr_FR/translations.php b/sources/app/Locale/fr_FR/translations.php index 36ecf2e..5c730eb 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', @@ -494,7 +486,7 @@ 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', @@ -548,8 +540,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', @@ -572,10 +562,6 @@ return array( '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', @@ -933,7 +919,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 +942,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 +968,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', @@ -1068,7 +1051,55 @@ 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', + 'Gitlab issue reopened' => 'Ticket Gitlab rouvert', + '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 project', ); diff --git a/sources/app/Locale/hu_HU/translations.php b/sources/app/Locale/hu_HU/translations.php index 1a34a5b..6fb1801 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -931,7 +917,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/id_ID/translations.php b/sources/app/Locale/id_ID/translations.php index 3e80025..686124f 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -931,7 +917,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 +940,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 +966,6 @@ return array( 'Documentation' => 'Dokumentasi', 'Table of contents' => 'Daftar isi', 'Gantt' => 'Gantt', - 'Help with project permissions' => 'Bantuan dengan izin proyek', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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', ); diff --git a/sources/app/Locale/it_IT/translations.php b/sources/app/Locale/it_IT/translations.php index 7fd39d9..69975f4 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -931,7 +917,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/ja_JP/translations.php b/sources/app/Locale/ja_JP/translations.php index 8447113..b4bcfac 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' => 'パスワードの変更', @@ -546,8 +538,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' => 'デフォルトスイムレーンの表示', @@ -570,10 +560,6 @@ return array( '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' => 'サブタスク', @@ -931,7 +917,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/nb_NO/translations.php b/sources/app/Locale/nb_NO/translations.php index b0fa064..5d03959 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( // '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', @@ -931,7 +917,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 +940,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 +966,6 @@ return array( 'Documentation' => 'Dokumentasjon', 'Table of contents' => 'Innholdsfortegnelse', 'Gantt' => 'Gantt', - 'Help with project permissions' => 'Hjelp med prosjekttilganger', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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', ); diff --git a/sources/app/Locale/nl_NL/translations.php b/sources/app/Locale/nl_NL/translations.php index eaeca45..1662165 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -931,7 +917,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/pl_PL/translations.php b/sources/app/Locale/pl_PL/translations.php index 7419e13..2ac6414 100644 --- a/sources/app/Locale/pl_PL/translations.php +++ b/sources/app/Locale/pl_PL/translations.php @@ -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', @@ -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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( // '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', @@ -931,7 +917,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/pt_BR/translations.php b/sources/app/Locale/pt_BR/translations.php index f03be30..f3e988e 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -931,7 +917,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 +940,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 +966,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', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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', ); diff --git a/sources/app/Locale/pt_PT/translations.php b/sources/app/Locale/pt_PT/translations.php index 5e6c57d..5f5e9c8 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -738,20 +724,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', @@ -817,7 +803,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 +819,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 +865,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', @@ -931,7 +917,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', @@ -955,7 +940,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 +966,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', @@ -1065,7 +1048,55 @@ 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' => '', + // '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' => '', + // 'Gitlab issue reopened' => '', + // '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 do projecto', ); diff --git a/sources/app/Locale/ru_RU/translations.php b/sources/app/Locale/ru_RU/translations.php index e7f2d7a..06b3b9d 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', @@ -396,8 +390,6 @@ return array( 'Email:' => 'E-mail:', 'Notifications:' => 'Уведомления:', 'Notifications' => 'Уведомления', - 'Group:' => 'Группа:', - 'Regular user' => 'Обычный пользователь', 'Account type:' => 'Тип профиля:', 'Edit profile' => 'Редактировать профиль', 'Change password' => 'Сменить пароль', @@ -546,8 +538,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' => 'Показать стандартную дорожку', @@ -570,10 +560,6 @@ return array( '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' => 'Подзадачи', @@ -844,7 +830,7 @@ return array( 'Stop timer' => 'Остановить таймер', 'Start timer' => 'Запустить таймер', 'Add project member' => 'Добавить номер проекта', - 'Enable notifications' => 'Отключить уведомления', + 'Enable notifications' => 'Включить уведомления', 'My activity stream' => 'Лента моей активности', 'My calendar' => 'Мой календарь', 'Search tasks' => 'Поиск задачи', @@ -931,7 +917,6 @@ return array( 'contributors' => 'соавторы', 'License:' => 'Лицензия:', 'License' => 'Лицензия', - 'Project Administrator' => 'Администратор проекта', 'Enter the text below' => 'Введите текст ниже', 'Gantt chart for %s' => 'Диаграмма Гантта для %s', 'Sort by position' => 'Сортировать по позиции', @@ -955,7 +940,6 @@ return array( 'Members' => 'Участники', 'Shared project' => 'Общие/публичные проекты', 'Project managers' => 'Менеджер проекта', - 'Project members' => 'Участники проекта', 'Gantt chart for all projects' => 'Диаграмма Гантта для всех проектов', 'Projects list' => 'Список проектов', 'Gantt chart for this project' => 'Диаграмма Гантта для этого проекта', @@ -982,7 +966,6 @@ return array( 'Documentation' => 'Документация', 'Table of contents' => 'Сожержание', 'Gantt' => 'Гантт', - 'Help with project permissions' => 'Помощь с правами доступа по проекту', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => 'Участники проекта', ); diff --git a/sources/app/Locale/sr_Latn_RS/translations.php b/sources/app/Locale/sr_Latn_RS/translations.php index af785f9..8ad8d00 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( // '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', @@ -931,7 +917,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/sv_SE/translations.php b/sources/app/Locale/sv_SE/translations.php index 188b2bd..015fbfc 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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( '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', @@ -931,7 +917,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 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/th_TH/translations.php b/sources/app/Locale/th_TH/translations.php index d180c5e..7660641 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' => 'เปลี่ยนรหัสผ่าน', @@ -546,8 +538,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' => 'แสดงสวิมเลนเริ่มต้น', @@ -570,10 +560,6 @@ return array( // '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' => 'งานย่อย', @@ -931,7 +917,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/tr_TR/translations.php b/sources/app/Locale/tr_TR/translations.php index fca425e..6d6f1dc 100644 --- a/sources/app/Locale/tr_TR/translations.php +++ b/sources/app/Locale/tr_TR/translations.php @@ -174,13 +174,7 @@ return array( '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', @@ -396,8 +390,6 @@ return array( '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', @@ -546,8 +538,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', @@ -570,10 +560,6 @@ return array( // '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', @@ -931,7 +917,6 @@ return array( // 'contributors' => '', // 'License:' => '', // 'License' => '', - // 'Project Administrator' => '', // 'Enter the text below' => '', // 'Gantt chart for %s' => '', // 'Sort by position' => '', @@ -955,7 +940,6 @@ return array( // 'Members' => '', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); diff --git a/sources/app/Locale/zh_CN/translations.php b/sources/app/Locale/zh_CN/translations.php index 7642e01..0355b1f 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' => '修改密码', @@ -546,8 +538,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' => '显示默认泳道', @@ -570,10 +560,6 @@ return array( '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' => '子任务', @@ -931,7 +917,6 @@ return array( 'contributors' => '贡献者', 'License:' => '授权许可:', 'License' => '授权许可', - 'Project Administrator' => '项目管理者', 'Enter the text below' => '输入下方的文本', 'Gantt chart for %s' => '%s的甘特图', 'Sort by position' => '按位置排序', @@ -955,7 +940,6 @@ return array( 'Members' => '成员', // 'Shared project' => '', // 'Project managers' => '', - // 'Project members' => '', // 'Gantt chart for all projects' => '', // 'Projects list' => '', // 'Gantt chart for this project' => '', @@ -982,7 +966,6 @@ return array( // 'Documentation' => '', // 'Table of contents' => '', // 'Gantt' => '', - // 'Help with project permissions' => '', // 'Author' => '', // 'Version' => '', // 'Plugins' => '', @@ -1065,7 +1048,55 @@ 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' => '', + // 'Gitlab issue reopened' => '', + // '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' => '', ); 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..289471f 100644 --- a/sources/app/Model/Action.php +++ b/sources/app/Model/Action.php @@ -118,6 +118,7 @@ class Action extends Base 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_REOPENED => t('Gitlab issue reopened'), GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'), GitlabWebhook::EVENT_ISSUE_COMMENT => t('Gitlab issue comment created'), BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'), diff --git a/sources/app/Model/Authentication.php b/sources/app/Model/Authentication.php index 83d8543..d10f2bf 100644 --- a/sources/app/Model/Authentication.php +++ b/sources/app/Model/Authentication.php @@ -2,7 +2,6 @@ namespace Kanboard\Model; -use Kanboard\Core\Http\Request; use SimpleValidator\Validator; use SimpleValidator\Validators; use Gregwar\Captcha\CaptchaBuilder; @@ -15,113 +14,6 @@ use Gregwar\Captcha\CaptchaBuilder; */ class Authentication extends Base { - /** - * Load automatically an authentication backend - * - * @access public - * @param string $name Backend class name - * @return mixed - */ - public function backend($name) - { - if (! isset($this->container[$name])) { - $class = '\Kanboard\Auth\\'.ucfirst($name); - $this->container[$name] = new $class($this->container); - } - - return $this->container[$name]; - } - - /** - * Check if the current user is authenticated - * - * @access public - * @return bool - */ - public function isAuthenticated() - { - // If the user is already logged it's ok - if ($this->userSession->isLogged()) { - - // Check if the user session match an existing user - $userNotFound = ! $this->user->exists($this->userSession->getId()); - $reverseProxyWrongUser = REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->getUsername() !== $this->userSession->getUsername(); - - if ($userNotFound || $reverseProxyWrongUser) { - $this->backend('rememberMe')->destroy($this->userSession->getId()); - $this->sessionManager->close(); - return false; - } - - return true; - } - - // We try first with the RememberMe cookie - if (REMEMBER_ME_AUTH && $this->backend('rememberMe')->authenticate()) { - return true; - } - - // Then with the ReverseProxy authentication - if (REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->authenticate()) { - return true; - } - - return false; - } - - /** - * Authenticate a user by different methods - * - * @access public - * @param string $username Username - * @param string $password Password - * @return boolean - */ - public function authenticate($username, $password) - { - if ($this->user->isLocked($username)) { - $this->container['logger']->error('Account locked: '.$username); - return false; - } elseif ($this->backend('database')->authenticate($username, $password)) { - $this->user->resetFailedLogin($username); - return true; - } elseif (LDAP_AUTH && $this->backend('ldap')->authenticate($username, $password)) { - $this->user->resetFailedLogin($username); - return true; - } - - $this->handleFailedLogin($username); - return false; - } - - /** - * Return true if the captcha must be shown - * - * @access public - * @param string $username - * @return boolean - */ - public function hasCaptcha($username) - { - return $this->user->getFailedLogin($username) >= BRUTEFORCE_CAPTCHA; - } - - /** - * Handle failed login - * - * @access public - * @param string $username - */ - public function handleFailedLogin($username) - { - $this->user->incrementFailedLogin($username); - - if ($this->user->getFailedLogin($username) >= BRUTEFORCE_LOCKDOWN) { - $this->container['logger']->critical('Locking account: '.$username); - $this->user->lock($username, BRUTEFORCE_LOCKDOWN_DURATION); - } - } - /** * Validate user login form * @@ -131,14 +23,14 @@ class Authentication extends Base */ public function validateForm(array $values) { - list($result, $errors) = $this->validateFormCredentials($values); + $result = false; + $errors = array(); - if ($result) { - if ($this->validateFormCaptcha($values) && $this->authenticate($values['username'], $values['password'])) { - $this->createRememberMeSession($values); - } else { - $result = false; - $errors['login'] = t('Bad username or password'); + foreach (array('validateFields', 'validateLocking', 'validateCaptcha', 'validateCredentials') as $method) { + list($result, $errors) = $this->$method($values); + + if (! $result) { + break; } } @@ -148,11 +40,11 @@ class Authentication extends Base /** * Validate credentials syntax * - * @access public + * @access private * @param array $values Form values * @return array $valid, $errors [0] = Success or not, [1] = List of errors */ - public function validateFormCredentials(array $values) + private function validateFields(array $values) { $v = new Validator($values, array( new Validators\Required('username', t('The username is required')), @@ -167,40 +59,72 @@ class Authentication extends Base } /** - * Validate captcha - * - * @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 + * Validate user locking * * @access private * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors */ - private function createRememberMeSession(array $values) + private function validateLocking(array $values) { - if (REMEMBER_ME_AUTH && ! empty($values['remember_me'])) { - $credentials = $this->backend('rememberMe') - ->create($this->userSession->getId(), Request::getIpAddress(), Request::getUserAgent()); + $result = true; + $errors = array(); - $this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']); + if ($this->userLocking->isLocked($values['username'])) { + $result = false; + $errors['login'] = t('Your account is locked for %d minutes', BRUTEFORCE_LOCKDOWN_DURATION); + $this->logger->error('Account locked: '.$values['username']); } + + return array($result, $errors); + } + + /** + * Validate password syntax + * + * @access private + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + private function validateCredentials(array $values) + { + $result = true; + $errors = array(); + + if (! $this->authenticationManager->passwordAuthentication($values['username'], $values['password'])) { + $result = false; + $errors['login'] = t('Bad username or password'); + } + + return array($result, $errors); + } + + /** + * Validate captcha + * + * @access private + * @param array $values Form values + * @return boolean + */ + private function validateCaptcha(array $values) + { + $result = true; + $errors = array(); + + if ($this->userLocking->hasCaptcha($values['username'])) { + if (! isset($this->sessionStorage->captcha)) { + $result = false; + } else { + $builder = new CaptchaBuilder; + $builder->setPhrase($this->sessionStorage->captcha); + $result = $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : ''); + + if (! $result) { + $errors['login'] = t('Invalid captcha'); + } + } + } + + return array($result, $errors);; } } diff --git a/sources/app/Model/Config.php b/sources/app/Model/Config.php index 273f884..be9d4ae 100644 --- a/sources/app/Model/Config.php +++ b/sources/app/Model/Config.php @@ -35,7 +35,7 @@ class Config extends Setting 'RSD' => t('RSD - Serbian dinar'), 'SEK' => t('SEK - Swedish Krona'), 'NOK' => t('NOK - Norwegian Krone'), - 'BAM' => t('BAM - Konvertibile Mark'), + 'BAM' => t('BAM - Konvertible Mark'), ); } diff --git a/sources/app/Model/Group.php b/sources/app/Model/Group.php new file mode 100644 index 0000000..a086fe9 --- /dev/null +++ b/sources/app/Model/Group.php @@ -0,0 +1,175 @@ +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); + } + + /** + * 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('The 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\Required('name', t('The name is required')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 100), 100), + new Validators\Unique('name', t('The name must be unique'), $this->db->getConnection(), self::TABLE, 'id'), + new Validators\MaxLength('external_id', t('The maximum length is %d characters', 255), 255), + new Validators\Integer('id', t('This value must be an integer')), + ); + } +} 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/Project.php b/sources/app/Model/Project.php index a7f9309..8a949ba 100644 --- a/sources/app/Model/Project.php +++ b/sources/app/Model/Project.php @@ -5,6 +5,7 @@ namespace Kanboard\Model; use SimpleValidator\Validator; use SimpleValidator\Validators; use Kanboard\Core\Security\Token; +use Kanboard\Core\Security\Role; /** * Project model @@ -287,7 +288,7 @@ class Project extends Base { foreach ($projects as &$project) { $this->getColumnStats($project); - $project = array_merge($project, $this->projectPermission->getProjectUsers($project['id'])); + $project = array_merge($project, $this->projectUserRole->getAllUsersGroupedByRole($project['id'])); } return $projects; @@ -365,7 +366,7 @@ class Project extends Base } if ($add_user && $user_id) { - $this->projectPermission->addManager($project_id, $user_id); + $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MANAGER); } $this->category->createDefaultCategories($project_id); diff --git a/sources/app/Model/ProjectAnalytic.php b/sources/app/Model/ProjectAnalytic.php index 92364c0..e77a036 100644 --- a/sources/app/Model/ProjectAnalytic.php +++ b/sources/app/Model/ProjectAnalytic.php @@ -56,7 +56,7 @@ class ProjectAnalytic extends Base $metrics = array(); $total = 0; $tasks = $this->taskFinder->getAll($project_id); - $users = $this->projectPermission->getMemberList($project_id); + $users = $this->projectUserRole->getAssignableUsersList($project_id); foreach ($tasks as $task) { $user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0]; diff --git a/sources/app/Model/ProjectDailyColumnStats.php b/sources/app/Model/ProjectDailyColumnStats.php index 4b75fff..7c89283 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 * diff --git a/sources/app/Model/ProjectDailyStats.php b/sources/app/Model/ProjectDailyStats.php index 7ec1ee2..e79af37 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 * diff --git a/sources/app/Model/ProjectGroupRole.php b/sources/app/Model/ProjectGroupRole.php new file mode 100644 index 0000000..2fe22ca --- /dev/null +++ b/sources/app/Model/ProjectGroupRole.php @@ -0,0 +1,187 @@ +db + ->hashtable(Project::TABLE) + ->join(self::TABLE, 'project_id', 'id') + ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE) + ->eq(GroupMember::TABLE.'.user_id', $user_id) + ->in(Project::TABLE.'.is_active', $status) + ->getAll(Project::TABLE.'.id', Project::TABLE.'.name'); + } + + /** + * For a given project get the role of the specified user + * + * @access public + * @param integer $project_id + * @param integer $user_id + * @return string + */ + public function getUserRole($project_id, $user_id) + { + return $this->db->table(self::TABLE) + ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE) + ->eq(GroupMember::TABLE.'.user_id', $user_id) + ->eq(self::TABLE.'.project_id', $project_id) + ->findOneColumn('role'); + } + + /** + * Get all groups associated directly to the project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getGroups($project_id) + { + return $this->db->table(self::TABLE) + ->columns(Group::TABLE.'.id', Group::TABLE.'.name', self::TABLE.'.role') + ->join(Group::TABLE, 'id', 'group_id') + ->eq('project_id', $project_id) + ->asc('name') + ->findAll(); + } + + /** + * From groups get all users associated to the project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getUsers($project_id) + { + return $this->db->table(self::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.role') + ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE) + ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) + ->eq(self::TABLE.'.project_id', $project_id) + ->asc(User::TABLE.'.username') + ->findAll(); + } + + /** + * From groups get all users assignable to tasks + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAssignableUsers($project_id) + { + return $this->db->table(self::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') + ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE) + ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) + ->eq(self::TABLE.'.project_id', $project_id) + ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER)) + ->asc(User::TABLE.'.username') + ->findAll(); + } + + /** + * Add a group to the project + * + * @access public + * @param integer $project_id + * @param integer $group_id + * @param string $role + * @return boolean + */ + public function addGroup($project_id, $group_id, $role) + { + return $this->db->table(self::TABLE)->insert(array( + 'group_id' => $group_id, + 'project_id' => $project_id, + 'role' => $role, + )); + } + + /** + * Remove a group from the project + * + * @access public + * @param integer $project_id + * @param integer $group_id + * @return boolean + */ + public function removeGroup($project_id, $group_id) + { + return $this->db->table(self::TABLE)->eq('group_id', $group_id)->eq('project_id', $project_id)->remove(); + } + + /** + * Change a group role for the project + * + * @access public + * @param integer $project_id + * @param integer $group_id + * @param string $role + * @return boolean + */ + public function changeGroupRole($project_id, $group_id, $role) + { + return $this->db->table(self::TABLE) + ->eq('group_id', $group_id) + ->eq('project_id', $project_id) + ->update(array( + 'role' => $role, + )); + } + + /** + * Copy group access from a project to another one + * + * @param integer $project_src_id Project Template + * @return integer $project_dst_id Project that receives the copy + * @return boolean + */ + public function duplicate($project_src_id, $project_dst_id) + { + $rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll(); + + foreach ($rows as $row) { + $result = $this->db->table(self::TABLE)->save(array( + 'project_id' => $project_dst_id, + 'group_id' => $row['group_id'], + 'role' => $row['role'], + )); + + if (! $result) { + return false; + } + } + + return true; + } +} diff --git a/sources/app/Model/ProjectPermission.php b/sources/app/Model/ProjectPermission.php index d9eef4d..b311c10 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); @@ -135,7 +31,7 @@ class ProjectPermission extends Base ->table(self::TABLE) ->join(User::TABLE, 'id', 'user_id') ->join(Project::TABLE, 'id', 'project_id') - ->eq(self::TABLE.'.is_owner', $is_owner) + ->eq(self::TABLE.'.role', $role) ->eq(Project::TABLE.'.is_private', 0) ->in(Project::TABLE.'.id', $project_ids) ->columns( @@ -147,172 +43,6 @@ class ProjectPermission extends Base ); } - /** - * Get a list of people associated to the project - * - * @access public - * @param integer $project_id Project id - * @return array - */ - public function getAssociatedUsers($project_id) - { - $users = $this->db - ->table(self::TABLE) - ->join(User::TABLE, 'id', 'user_id') - ->eq('project_id', $project_id) - ->asc('username') - ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') - ->findAll(); - - return $this->user->prepareList($users); - } - - /** - * Get allowed and not allowed users for a project - * - * @access public - * @param integer $project_id Project id - * @return array - */ - public function getAllUsers($project_id) - { - $users = array( - 'allowed' => array(), - 'not_allowed' => array(), - 'managers' => array(), - ); - - $all_users = $this->user->getList(); - - $users['allowed'] = $this->getMembers($project_id); - $users['managers'] = $this->getManagers($project_id); - - foreach ($all_users as $user_id => $username) { - if (! isset($users['allowed'][$user_id])) { - $users['not_allowed'][$user_id] = $username; - } - } - - return $users; - } - - /** - * Add a new project member - * - * @access public - * @param integer $project_id Project id - * @param integer $user_id User id - * @return bool - */ - public function addMember($project_id, $user_id) - { - return $this->db - ->table(self::TABLE) - ->save(array('project_id' => $project_id, 'user_id' => $user_id)); - } - - /** - * Remove a member - * - * @access public - * @param integer $project_id Project id - * @param integer $user_id User id - * @return bool - */ - public function revokeMember($project_id, $user_id) - { - return $this->db - ->table(self::TABLE) - ->eq('project_id', $project_id) - ->eq('user_id', $user_id) - ->remove(); - } - - /** - * Add a project manager - * - * @access public - * @param integer $project_id Project id - * @param integer $user_id User id - * @return bool - */ - public function addManager($project_id, $user_id) - { - return $this->db - ->table(self::TABLE) - ->save(array('project_id' => $project_id, 'user_id' => $user_id, 'is_owner' => 1)); - } - - /** - * Change the role of a member - * - * @access public - * @param integer $project_id Project id - * @param integer $user_id User id - * @param integer $is_owner Is user owner of the project - * @return bool - */ - public function changeRole($project_id, $user_id, $is_owner) - { - return $this->db - ->table(self::TABLE) - ->eq('project_id', $project_id) - ->eq('user_id', $user_id) - ->update(array('is_owner' => (int) $is_owner)); - } - - /** - * Check if a specific user is member of a project - * - * @access public - * @param integer $project_id Project id - * @param integer $user_id User id - * @return bool - */ - public function isMember($project_id, $user_id) - { - if ($this->isEverybodyAllowed($project_id)) { - return true; - } - - return $this->db - ->table(self::TABLE) - ->eq('project_id', $project_id) - ->eq('user_id', $user_id) - ->exists(); - } - - /** - * Check if a specific user is manager of a given project - * - * @access public - * @param integer $project_id Project id - * @param integer $user_id User id - * @return bool - */ - public function isManager($project_id, $user_id) - { - return $this->db - ->table(self::TABLE) - ->eq('project_id', $project_id) - ->eq('user_id', $user_id) - ->eq('is_owner', 1) - ->exists(); - } - - /** - * Check if a specific user is allowed to access to a given project - * - * @access public - * @param integer $project_id Project id - * @param integer $user_id User id - * @return bool - */ - public function isUserAllowed($project_id, $user_id) - { - return $project_id === 0 || $this->user->isAdmin($user_id) || $this->isMember($project_id, $user_id); - } - /** * Return true if everybody is allowed for the project * @@ -330,172 +60,59 @@ class ProjectPermission extends Base } /** - * Return a list of allowed active projects for a given user + * Return true if the user is allowed to access a project * - * @access public - * @param integer $user_id User id - * @return array - */ - 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 isMember($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() - ); + /** + * Get active project ids by user + * + * @access public + * @param integer $user_id + * @return array + */ + public function getActiveProjectIds($user_id) + { + return array_keys($this->projectUserRole->getProjectsByUser($user_id, array(Project::ACTIVE))); + } + + /** + * 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..28e6c8c --- /dev/null +++ b/sources/app/Model/ProjectUserRole.php @@ -0,0 +1,263 @@ +db + ->hashtable(Project::TABLE) + ->beginOr() + ->eq(self::TABLE.'.user_id', $user_id) + ->eq(Project::TABLE.'.is_everybody_allowed', 1) + ->closeOr() + ->in(Project::TABLE.'.is_active', $status) + ->join(self::TABLE, 'project_id', 'id') + ->getAll(Project::TABLE.'.id', Project::TABLE.'.name'); + + $groupProjects = $this->projectGroupRole->getProjectsByUser($user_id, $status); + $groups = $userProjects + $groupProjects; + + asort($groups); + + return $groups; + } + + /** + * For a given project get the role of the specified user + * + * @access public + * @param integer $project_id + * @param integer $user_id + * @return string + */ + public function getUserRole($project_id, $user_id) + { + if ($this->projectPermission->isEverybodyAllowed($project_id)) { + return Role::PROJECT_MEMBER; + } + + $role = $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->findOneColumn('role'); + + if (empty($role)) { + $role = $this->projectGroupRole->getUserRole($project_id, $user_id); + } + + return $role; + } + + /** + * Get all users associated directly to the project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getUsers($project_id) + { + return $this->db->table(self::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.role') + ->join(User::TABLE, 'id', 'user_id') + ->eq('project_id', $project_id) + ->asc(User::TABLE.'.username') + ->asc(User::TABLE.'.name') + ->findAll(); + } + + /** + * Get all users (fetch users from groups) + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAllUsers($project_id) + { + $userMembers = $this->getUsers($project_id); + $groupMembers = $this->projectGroupRole->getUsers($project_id); + $members = array_merge($userMembers, $groupMembers); + + return $this->user->prepareList($members); + } + + /** + * Get users grouped by role + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getAllUsersGroupedByRole($project_id) + { + $users = array(); + + $userMembers = $this->getUsers($project_id); + $groupMembers = $this->projectGroupRole->getUsers($project_id); + $members = array_merge($userMembers, $groupMembers); + + foreach ($members as $user) { + if (! isset($users[$user['role']])) { + $users[$user['role']] = array(); + } + + $users[$user['role']][$user['id']] = $user['name'] ?: $user['username']; + } + + return $users; + } + + /** + * Get list of users that can be assigned to a task (only Manager and Member) + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAssignableUsers($project_id) + { + if ($this->projectPermission->isEverybodyAllowed($project_id)) { + return $this->user->getList(); + } + + $userMembers = $this->db->table(self::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') + ->join(User::TABLE, 'id', 'user_id') + ->eq('project_id', $project_id) + ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER)) + ->findAll(); + + $groupMembers = $this->projectGroupRole->getAssignableUsers($project_id); + $members = array_merge($userMembers, $groupMembers); + + return $this->user->prepareList($members); + } + + /** + * Get list of users that can be assigned to a task (only Manager and Member) + * + * @access public + * @param integer $project_id Project id + * @param bool $unassigned Prepend the 'Unassigned' value + * @param bool $everybody Prepend the 'Everbody' value + * @param bool $singleUser If there is only one user return only this user + * @return array + */ + public function getAssignableUsersList($project_id, $unassigned = true, $everybody = false, $singleUser = false) + { + $users = $this->getAssignableUsers($project_id); + + if ($singleUser && count($users) === 1) { + return $users; + } + + if ($unassigned) { + $users = array(t('Unassigned')) + $users; + } + + if ($everybody) { + $users = array(User::EVERYBODY_ID => t('Everybody')) + $users; + } + + return $users; + } + + /** + * Add a user to the project + * + * @access public + * @param integer $project_id + * @param integer $user_id + * @param string $role + * @return boolean + */ + public function addUser($project_id, $user_id, $role) + { + return $this->db->table(self::TABLE)->insert(array( + 'user_id' => $user_id, + 'project_id' => $project_id, + 'role' => $role, + )); + } + + /** + * Remove a user from the project + * + * @access public + * @param integer $project_id + * @param integer $user_id + * @return boolean + */ + public function removeUser($project_id, $user_id) + { + return $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->remove(); + } + + /** + * Change a user role for the project + * + * @access public + * @param integer $project_id + * @param integer $user_id + * @param string $role + * @return boolean + */ + public function changeUserRole($project_id, $user_id, $role) + { + return $this->db->table(self::TABLE) + ->eq('user_id', $user_id) + ->eq('project_id', $project_id) + ->update(array( + 'role' => $role, + )); + } + + /** + * Copy user access from a project to another one + * + * @param integer $project_src_id Project Template + * @return integer $project_dst_id Project that receives the copy + * @return boolean + */ + public function duplicate($project_src_id, $project_dst_id) + { + $rows = $this->db->table(self::TABLE)->eq('project_id', $project_src_id)->findAll(); + + foreach ($rows as $row) { + $result = $this->db->table(self::TABLE)->save(array( + 'project_id' => $project_dst_id, + 'user_id' => $row['user_id'], + 'role' => $row['role'], + )); + + if (! $result) { + return false; + } + } + + return true; + } +} diff --git a/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/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/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/User.php b/sources/app/Model/User.php index 88361ce..50e9b31 100644 --- a/sources/app/Model/User.php +++ b/sources/app/Model/User.php @@ -7,6 +7,7 @@ use SimpleValidator\Validator; use SimpleValidator\Validators; use Kanboard\Core\Session\SessionManager; use Kanboard\Core\Security\Token; +use Kanboard\Core\Security\Role; /** * User model @@ -57,8 +58,7 @@ class User extends Base 'username', 'name', 'email', - 'is_admin', - 'is_project_admin', + 'role', 'is_ldap_user', 'notifications_enabled', 'google_id', @@ -91,7 +91,7 @@ class User extends Base $this->db ->table(User::TABLE) ->eq('id', $user_id) - ->eq('is_admin', 1) + ->eq('role', Role::APP_ADMIN) ->exists(); } @@ -111,48 +111,17 @@ class User extends Base * Get a specific user by the Google id * * @access public - * @param string $google_id Google unique id + * @param string $column + * @param string $id * @return array|boolean */ - public function getByGoogleId($google_id) + public function getByExternalId($column, $id) { - if (empty($google_id)) { + if (empty($id)) { return false; } - return $this->db->table(self::TABLE)->eq('google_id', $google_id)->findOne(); - } - - /** - * Get a specific user by the Github id - * - * @access public - * @param string $github_id Github user id - * @return array|boolean - */ - public function getByGithubId($github_id) - { - if (empty($github_id)) { - return false; - } - - return $this->db->table(self::TABLE)->eq('github_id', $github_id)->findOne(); - } - - /** - * Get a specific user by the Gitlab id - * - * @access public - * @param string $gitlab_id Gitlab user id - * @return array|boolean - */ - public function getByGitlabId($gitlab_id) - { - if (empty($gitlab_id)) { - return false; - } - - return $this->db->table(self::TABLE)->eq('gitlab_id', $gitlab_id)->findOne(); + return $this->db->table(self::TABLE)->eq($column, $id)->findOne(); } /** @@ -289,7 +258,7 @@ class User extends Base } $this->removeFields($values, array('confirmation', 'current_password')); - $this->resetFields($values, array('is_admin', 'is_ldap_user', 'is_project_admin', 'disable_login_form')); + $this->resetFields($values, array('is_ldap_user', 'disable_login_form')); $this->convertNullFields($values, array('gitlab_id')); $this->convertIntegerFields($values, array('gitlab_id')); } @@ -320,7 +289,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 +324,10 @@ class User extends Base // All private projects are removed $project_ids = $db->table(Project::TABLE) - ->eq('is_private', 1) - ->eq(ProjectPermission::TABLE.'.user_id', $user_id) - ->join(ProjectPermission::TABLE, 'project_id', 'id') - ->findAllByColumn(Project::TABLE.'.id'); + ->eq('is_private', 1) + ->eq(ProjectUserRole::TABLE.'.user_id', $user_id) + ->join(ProjectUserRole::TABLE, 'project_id', 'id') + ->findAllByColumn(Project::TABLE.'.id'); if (! empty($project_ids)) { $db->table(Project::TABLE)->in('id', $project_ids)->remove(); @@ -401,71 +370,6 @@ class User extends Base ->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 * @@ -475,11 +379,10 @@ class User extends Base private function commonValidationRules() { return array( + new Validators\MaxLength('role', t('The maximum length is %d characters', 25), 25), new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), self::TABLE, 'id'), new Validators\Email('email', t('Email address invalid')), - new Validators\Integer('is_admin', t('This value must be an integer')), - new Validators\Integer('is_project_admin', t('This value must be an integer')), new Validators\Integer('is_ldap_user', t('This value must be an integer')), ); } @@ -585,9 +488,7 @@ class User extends Base $v = new Validator($values, array_merge($rules, $this->commonPasswordValidationRules())); if ($v->execute()) { - - // Check password - if ($this->authentication->authenticate($this->userSession->getUsername(), $values['current_password'])) { + if ($this->authenticationManager->passwordAuthentication($this->userSession->getUsername(), $values['current_password'], false)) { return array(true, array()); } else { return array(false, array('current_password' => array(t('Wrong password')))); diff --git a/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/UserNotification.php b/sources/app/Model/UserNotification.php index 3d98ebe..e00f23c 100644 --- a/sources/app/Model/UserNotification.php +++ b/sources/app/Model/UserNotification.php @@ -155,7 +155,7 @@ class UserNotification extends Base private function getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id) { return $this->db - ->table(ProjectPermission::TABLE) + ->table(ProjectUserRole::TABLE) ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') ->join(User::TABLE, 'id', 'user_id') ->eq('project_id', $project_id) diff --git a/sources/app/Schema/Mysql.php b/sources/app/Schema/Mysql.php index 52a73fb..3bdcc5e 100644 --- a/sources/app/Schema/Mysql.php +++ b/sources/app/Schema/Mysql.php @@ -4,8 +4,94 @@ namespace Schema; use PDO; use Kanboard\Core\Security\Token; +use Kanboard\Core\Security\Role; -const VERSION = 94; +const VERSION = 98; + +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) { diff --git a/sources/app/Schema/Postgres.php b/sources/app/Schema/Postgres.php index 5cd1a7d..23ed840 100644 --- a/sources/app/Schema/Postgres.php +++ b/sources/app/Schema/Postgres.php @@ -4,8 +4,93 @@ namespace Schema; use PDO; use Kanboard\Core\Security\Token; +use Kanboard\Core\Security\Role; -const VERSION = 74; +const VERSION = 78; + +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) { diff --git a/sources/app/Schema/Sqlite.php b/sources/app/Schema/Sqlite.php index fa26b15..534c3f3 100644 --- a/sources/app/Schema/Sqlite.php +++ b/sources/app/Schema/Sqlite.php @@ -3,9 +3,83 @@ namespace Schema; use Kanboard\Core\Security\Token; +use Kanboard\Core\Security\Role; use PDO; -const VERSION = 88; +const VERSION = 91; + +function version_91(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT '".Role::APP_USER."'"); + + $rq = $pdo->prepare('SELECT * FROM users'); + $rq->execute(); + $rows = $rq->fetchAll(PDO::FETCH_ASSOC) ?: array(); + + $rq = $pdo->prepare('UPDATE users SET "role"=? WHERE "id"=?'); + + foreach ($rows as $row) { + $role = Role::APP_USER; + + if ($row['is_admin'] == 1) { + $role = Role::APP_ADMIN; + } else if ($row['is_project_admin']) { + $role = Role::APP_MANAGER; + } + + $rq->execute(array($role, $row['id'])); + } +} + +function version_90(PDO $pdo) +{ + $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 +1043,6 @@ function version_7(PDO $pdo) { $pdo->exec(" CREATE TABLE project_has_users ( - id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL, user_id INTEGER NOT NULL, FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, diff --git a/sources/app/ServiceProvider/AuthenticationProvider.php b/sources/app/ServiceProvider/AuthenticationProvider.php new file mode 100644 index 0000000..46ba5be --- /dev/null +++ b/sources/app/ServiceProvider/AuthenticationProvider.php @@ -0,0 +1,149 @@ +register(new TotpAuth($container)); + $container['authenticationManager']->register(new RememberMeAuth($container)); + $container['authenticationManager']->register(new DatabaseAuth($container)); + + if (REVERSE_PROXY_AUTH) { + $container['authenticationManager']->register(new ReverseProxyAuth($container)); + } + + if (LDAP_AUTH) { + $container['authenticationManager']->register(new LdapAuth($container)); + } + + if (GITLAB_AUTH) { + $container['authenticationManager']->register(new GitlabAuth($container)); + } + + if (GITHUB_AUTH) { + $container['authenticationManager']->register(new GithubAuth($container)); + } + + if (GOOGLE_AUTH) { + $container['authenticationManager']->register(new GoogleAuth($container)); + } + + $container['projectAccessMap'] = $this->getProjectAccessMap(); + $container['applicationAccessMap'] = $this->getApplicationAccessMap(); + + $container['projectAuthorization'] = new Authorization($container['projectAccessMap']); + $container['applicationAuthorization'] = new Authorization($container['applicationAccessMap']); + + return $container; + } + + /** + * Get ACL for projects + * + * @access public + * @return AccessMap + */ + public function getProjectAccessMap() + { + $acl = new AccessMap; + $acl->setDefaultRole(Role::PROJECT_VIEWER); + $acl->setRoleHierarchy(Role::PROJECT_MANAGER, array(Role::PROJECT_MEMBER, Role::PROJECT_VIEWER)); + $acl->setRoleHierarchy(Role::PROJECT_MEMBER, array(Role::PROJECT_VIEWER)); + + $acl->add('Action', '*', Role::PROJECT_MANAGER); + $acl->add('Analytic', '*', Role::PROJECT_MANAGER); + $acl->add('Board', 'save', Role::PROJECT_MEMBER); + $acl->add('BoardPopover', '*', Role::PROJECT_MEMBER); + $acl->add('Calendar', 'save', Role::PROJECT_MEMBER); + $acl->add('Category', '*', Role::PROJECT_MANAGER); + $acl->add('Column', '*', Role::PROJECT_MANAGER); + $acl->add('Comment', '*', Role::PROJECT_MEMBER); + $acl->add('Customfilter', '*', Role::PROJECT_MEMBER); + $acl->add('Export', '*', Role::PROJECT_MANAGER); + $acl->add('File', array('screenshot', 'create', 'save', 'remove', 'confirm'), Role::PROJECT_MEMBER); + $acl->add('Gantt', '*', Role::PROJECT_MANAGER); + $acl->add('Project', array('share', 'integrations', 'notifications', 'edit', 'update', 'duplicate', 'disable', 'enable', 'remove'), Role::PROJECT_MANAGER); + $acl->add('ProjectPermission', '*', Role::PROJECT_MANAGER); + $acl->add('Projectuser', '*', Role::PROJECT_MANAGER); + $acl->add('Subtask', '*', Role::PROJECT_MEMBER); + $acl->add('Swimlane', '*', Role::PROJECT_MANAGER); + $acl->add('Task', 'remove', Role::PROJECT_MEMBER); + $acl->add('Taskcreation', '*', Role::PROJECT_MEMBER); + $acl->add('Taskduplication', '*', Role::PROJECT_MEMBER); + $acl->add('TaskImport', '*', Role::PROJECT_MANAGER); + $acl->add('Tasklink', '*', Role::PROJECT_MEMBER); + $acl->add('Taskmodification', '*', Role::PROJECT_MEMBER); + $acl->add('Taskstatus', '*', Role::PROJECT_MEMBER); + $acl->add('Timer', '*', Role::PROJECT_MEMBER); + + return $acl; + } + + /** + * Get ACL for the application + * + * @access public + * @return AccessMap + */ + public function getApplicationAccessMap() + { + $acl = new AccessMap; + $acl->setDefaultRole(Role::APP_USER); + $acl->setRoleHierarchy(Role::APP_ADMIN, array(Role::APP_MANAGER, Role::APP_USER, Role::APP_PUBLIC)); + $acl->setRoleHierarchy(Role::APP_MANAGER, array(Role::APP_USER, Role::APP_PUBLIC)); + $acl->setRoleHierarchy(Role::APP_USER, array(Role::APP_PUBLIC)); + + $acl->add('Oauth', array('google', 'github', 'gitlab'), Role::APP_PUBLIC); + $acl->add('Auth', array('login', 'check', 'captcha'), Role::APP_PUBLIC); + $acl->add('Webhook', '*', Role::APP_PUBLIC); + $acl->add('Task', 'readonly', Role::APP_PUBLIC); + $acl->add('Board', 'readonly', Role::APP_PUBLIC); + $acl->add('Ical', '*', Role::APP_PUBLIC); + $acl->add('Feed', '*', Role::APP_PUBLIC); + + $acl->add('Config', '*', Role::APP_ADMIN); + $acl->add('Currency', '*', Role::APP_ADMIN); + $acl->add('Gantt', 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..fad7304 100644 --- a/sources/app/ServiceProvider/ClassProvider.php +++ b/sources/app/ServiceProvider/ClassProvider.php @@ -5,23 +5,17 @@ namespace Kanboard\ServiceProvider; use Pimple\Container; use Pimple\ServiceProviderInterface; use League\HTMLToMarkdown\HtmlConverter; -use Kanboard\Core\Plugin\Loader; use Kanboard\Core\Mail\Client as EmailClient; use Kanboard\Core\ObjectStorage\FileStorage; use Kanboard\Core\Paginator; -use Kanboard\Core\OAuth2; +use Kanboard\Core\Http\OAuth2; use Kanboard\Core\Tool; use Kanboard\Core\Http\Client as HttpClient; -use Kanboard\Model\UserNotificationType; -use Kanboard\Model\ProjectNotificationType; -use Kanboard\Notification\Mail as MailNotification; -use Kanboard\Notification\Web as WebNotification; class ClassProvider implements ServiceProviderInterface { private $classes = array( 'Model' => array( - 'Acl', 'Action', 'Authentication', 'Board', @@ -32,6 +26,8 @@ class ClassProvider implements ServiceProviderInterface 'Currency', 'CustomFilter', 'File', + 'Group', + 'GroupMember', 'LastLogin', 'Link', 'Notification', @@ -45,6 +41,9 @@ class ClassProvider implements ServiceProviderInterface 'ProjectPermission', 'ProjectNotification', 'ProjectMetadata', + 'ProjectGroupRole', + 'ProjectUserRole', + 'RememberMeSession', 'Subtask', 'SubtaskExport', 'SubtaskTimeTracking', @@ -67,9 +66,8 @@ class ClassProvider implements ServiceProviderInterface 'Transition', 'User', 'UserImport', - 'UserSession', + 'UserLocking', 'UserNotification', - 'UserNotificationType', 'UserNotificationFilter', 'UserUnreadNotification', 'UserMetadata', @@ -80,6 +78,8 @@ class ClassProvider implements ServiceProviderInterface 'TaskFilterCalendarFormatter', 'TaskFilterICalendarFormatter', 'ProjectGanttFormatter', + 'UserFilterAutoCompleteFormatter', + 'GroupAutoCompleteFormatter', ), 'Core' => array( 'DateParser', @@ -90,7 +90,7 @@ class ClassProvider implements ServiceProviderInterface 'Core\Http' => array( 'Request', 'Response', - 'Router', + 'RememberMeCookie', ), 'Core\Cache' => array( 'MemoryCache', @@ -100,6 +100,13 @@ class ClassProvider implements ServiceProviderInterface ), 'Core\Security' => array( 'Token', + 'Role', + ), + 'Core\User' => array( + 'GroupSync', + 'UserSync', + 'UserSession', + 'UserProfile', ), 'Integration' => array( 'BitbucketWebhook', @@ -140,22 +147,6 @@ class ClassProvider implements ServiceProviderInterface return $mailer; }; - $container['userNotificationType'] = function ($container) { - $type = new UserNotificationType($container); - $type->setType(MailNotification::TYPE, t('Email'), '\Kanboard\Notification\Mail'); - $type->setType(WebNotification::TYPE, t('Web'), '\Kanboard\Notification\Web'); - return $type; - }; - - $container['projectNotificationType'] = function ($container) { - $type = new ProjectNotificationType($container); - $type->setType('webhook', 'Webhook', '\Kanboard\Notification\Webhook', true); - $type->setType('activity_stream', 'ActivityStream', '\Kanboard\Notification\ActivityStream', true); - return $type; - }; - - $container['pluginLoader'] = new Loader($container); - $container['cspRules'] = array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '* data:'); return $container; diff --git a/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..6b1d0d8 --- /dev/null +++ b/sources/app/ServiceProvider/RouteProvider.php @@ -0,0 +1,200 @@ +addRoute('dashboard', 'app', 'index'); + $container['router']->addRoute('dashboard/:user_id', 'app', 'index', array('user_id')); + $container['router']->addRoute('dashboard/:user_id/projects', 'app', 'projects', array('user_id')); + $container['router']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks', array('user_id')); + $container['router']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks', array('user_id')); + $container['router']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar', array('user_id')); + $container['router']->addRoute('dashboard/:user_id/activity', 'app', 'activity', array('user_id')); + + // Search routes + $container['router']->addRoute('search', 'search', 'index'); + $container['router']->addRoute('search/:search', 'search', 'index', array('search')); + + // Project routes + $container['router']->addRoute('projects', 'project', 'index'); + $container['router']->addRoute('project/create', 'project', 'create'); + $container['router']->addRoute('project/create/private', 'project', 'createPrivate'); + $container['router']->addRoute('project/:project_id', 'project', 'show', array('project_id')); + $container['router']->addRoute('p/:project_id', 'project', 'show', array('project_id')); + $container['router']->addRoute('project/:project_id/customer-filter', 'customfilter', 'index', array('project_id')); + $container['router']->addRoute('project/:project_id/share', 'project', 'share', array('project_id')); + $container['router']->addRoute('project/:project_id/notifications', 'project', 'notifications', array('project_id')); + $container['router']->addRoute('project/:project_id/edit', 'project', 'edit', array('project_id')); + $container['router']->addRoute('project/:project_id/integrations', 'project', 'integrations', array('project_id')); + $container['router']->addRoute('project/:project_id/duplicate', 'project', 'duplicate', array('project_id')); + $container['router']->addRoute('project/:project_id/remove', 'project', 'remove', array('project_id')); + $container['router']->addRoute('project/:project_id/disable', 'project', 'disable', array('project_id')); + $container['router']->addRoute('project/:project_id/enable', 'project', 'enable', array('project_id')); + $container['router']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index', array('project_id')); + $container['router']->addRoute('project/:project_id/import', 'taskImport', 'step1', array('project_id')); + + // Action routes + $container['router']->addRoute('project/:project_id/actions', 'action', 'index', array('project_id')); + $container['router']->addRoute('project/:project_id/action/:action_id/confirm', 'action', 'confirm', array('project_id', 'action_id')); + + // Column routes + $container['router']->addRoute('project/:project_id/columns', 'column', 'index', array('project_id')); + $container['router']->addRoute('project/:project_id/column/:column_id/edit', 'column', 'edit', array('project_id', 'column_id')); + $container['router']->addRoute('project/:project_id/column/:column_id/confirm', 'column', 'confirm', array('project_id', 'column_id')); + $container['router']->addRoute('project/:project_id/column/:column_id/move/:direction', 'column', 'move', array('project_id', 'column_id', 'direction')); + + // Swimlane routes + $container['router']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index', array('project_id')); + $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/edit', 'swimlane', 'edit', array('project_id', 'swimlane_id')); + $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/confirm', 'swimlane', 'confirm', array('project_id', 'swimlane_id')); + $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/disable', 'swimlane', 'disable', array('project_id', 'swimlane_id')); + $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/enable', 'swimlane', 'enable', array('project_id', 'swimlane_id')); + $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/up', 'swimlane', 'moveup', array('project_id', 'swimlane_id')); + $container['router']->addRoute('project/:project_id/swimlane/:swimlane_id/down', 'swimlane', 'movedown', array('project_id', 'swimlane_id')); + + // Category routes + $container['router']->addRoute('project/:project_id/categories', 'category', 'index', array('project_id')); + $container['router']->addRoute('project/:project_id/category/:category_id/edit', 'category', 'edit', array('project_id', 'category_id')); + $container['router']->addRoute('project/:project_id/category/:category_id/confirm', 'category', 'confirm', array('project_id', 'category_id')); + + // Task routes + $container['router']->addRoute('project/:project_id/task/:task_id', 'task', 'show', array('project_id', 'task_id')); + $container['router']->addRoute('t/:task_id', 'task', 'show', array('task_id')); + $container['router']->addRoute('public/task/:task_id/:token', 'task', 'readonly', array('task_id', 'token')); + + $container['router']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task', array('project_id', 'task_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot', array('project_id', 'task_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create', array('project_id', 'task_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create', array('project_id', 'task_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create', array('project_id', 'task_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions', array('project_id', 'task_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics', array('project_id', 'task_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove', array('project_id', 'task_id')); + + $container['router']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit', array('project_id', 'task_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description', array('project_id', 'task_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence', array('project_id', 'task_id')); + + $container['router']->addRoute('project/:project_id/task/:task_id/close', 'taskstatus', 'close', array('task_id', 'project_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/open', 'taskstatus', 'open', array('task_id', 'project_id')); + + $container['router']->addRoute('project/:project_id/task/:task_id/duplicate', 'taskduplication', 'duplicate', array('task_id', 'project_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/copy', 'taskduplication', 'copy', array('task_id', 'project_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/copy/:dst_project_id', 'taskduplication', 'copy', array('task_id', 'project_id', 'dst_project_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/move', 'taskduplication', 'move', array('task_id', 'project_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/move/:dst_project_id', 'taskduplication', 'move', array('task_id', 'project_id', 'dst_project_id')); + + // Exports + $container['router']->addRoute('export/tasks/:project_id', 'export', 'tasks', array('project_id')); + $container['router']->addRoute('export/subtasks/:project_id', 'export', 'subtasks', array('project_id')); + $container['router']->addRoute('export/transitions/:project_id', 'export', 'transitions', array('project_id')); + $container['router']->addRoute('export/summary/:project_id', 'export', 'summary', array('project_id')); + + // Board routes + $container['router']->addRoute('board/:project_id', 'board', 'show', array('project_id')); + $container['router']->addRoute('b/:project_id', 'board', 'show', array('project_id')); + $container['router']->addRoute('public/board/:token', 'board', 'readonly', array('token')); + + // Calendar routes + $container['router']->addRoute('calendar/:project_id', 'calendar', 'show', array('project_id')); + $container['router']->addRoute('c/:project_id', 'calendar', 'show', array('project_id')); + + // Listing routes + $container['router']->addRoute('list/:project_id', 'listing', 'show', array('project_id')); + $container['router']->addRoute('l/:project_id', 'listing', 'show', array('project_id')); + + // Gantt routes + $container['router']->addRoute('gantt/:project_id', 'gantt', 'project', array('project_id')); + $container['router']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project', array('project_id', 'sorting')); + + // Subtask routes + $container['router']->addRoute('project/:project_id/task/:task_id/subtask/create', 'subtask', 'create', array('project_id', 'task_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/remove', 'subtask', 'confirm', array('project_id', 'task_id', 'subtask_id')); + $container['router']->addRoute('project/:project_id/task/:task_id/subtask/:subtask_id/edit', 'subtask', 'edit', array('project_id', 'task_id', 'subtask_id')); + + // Feed routes + $container['router']->addRoute('feed/project/:token', 'feed', 'project', array('token')); + $container['router']->addRoute('feed/user/:token', 'feed', 'user', array('token')); + + // Ical routes + $container['router']->addRoute('ical/project/:token', 'ical', 'project', array('token')); + $container['router']->addRoute('ical/user/:token', 'ical', 'user', array('token')); + + // Users + $container['router']->addRoute('users', 'user', 'index'); + $container['router']->addRoute('user/show/:user_id', 'user', 'show', array('user_id')); + $container['router']->addRoute('user/show/:user_id/timesheet', 'user', 'timesheet', array('user_id')); + $container['router']->addRoute('user/show/:user_id/last-logins', 'user', 'last', array('user_id')); + $container['router']->addRoute('user/show/:user_id/sessions', 'user', 'sessions', array('user_id')); + $container['router']->addRoute('user/:user_id/edit', 'user', 'edit', array('user_id')); + $container['router']->addRoute('user/:user_id/password', 'user', 'password', array('user_id')); + $container['router']->addRoute('user/:user_id/share', 'user', 'share', array('user_id')); + $container['router']->addRoute('user/:user_id/notifications', 'user', 'notifications', array('user_id')); + $container['router']->addRoute('user/:user_id/accounts', 'user', 'external', array('user_id')); + $container['router']->addRoute('user/:user_id/integrations', 'user', 'integrations', array('user_id')); + $container['router']->addRoute('user/:user_id/authentication', 'user', 'authentication', array('user_id')); + $container['router']->addRoute('user/:user_id/remove', 'user', 'remove', array('user_id')); + $container['router']->addRoute('user/:user_id/2fa', 'twofactor', 'index', array('user_id')); + + // Groups + $container['router']->addRoute('groups', 'group', 'index'); + $container['router']->addRoute('groups/create', 'group', 'create'); + $container['router']->addRoute('group/:group_id/associate', 'group', 'associate', array('group_id')); + $container['router']->addRoute('group/:group_id/dissociate/:user_id', 'group', 'dissociate', array('group_id', 'user_id')); + $container['router']->addRoute('group/:group_id/edit', 'group', 'edit', array('group_id')); + $container['router']->addRoute('group/:group_id/members', 'group', 'users', array('group_id')); + $container['router']->addRoute('group/:group_id/remove', 'group', 'confirm', array('group_id')); + + // Config + $container['router']->addRoute('settings', 'config', 'index'); + $container['router']->addRoute('settings/plugins', 'config', 'plugins'); + $container['router']->addRoute('settings/application', 'config', 'application'); + $container['router']->addRoute('settings/project', 'config', 'project'); + $container['router']->addRoute('settings/project', 'config', 'project'); + $container['router']->addRoute('settings/board', 'config', 'board'); + $container['router']->addRoute('settings/calendar', 'config', 'calendar'); + $container['router']->addRoute('settings/integrations', 'config', 'integrations'); + $container['router']->addRoute('settings/webhook', 'config', 'webhook'); + $container['router']->addRoute('settings/api', 'config', 'api'); + $container['router']->addRoute('settings/links', 'link', 'index'); + $container['router']->addRoute('settings/currencies', 'currency', 'index'); + + // Doc + $container['router']->addRoute('documentation/:file', 'doc', 'show', array('file')); + $container['router']->addRoute('documentation', 'doc', 'show'); + + // Auth routes + $container['router']->addRoute('oauth/google', 'oauth', 'google'); + $container['router']->addRoute('oauth/github', 'oauth', 'github'); + $container['router']->addRoute('oauth/gitlab', 'oauth', 'gitlab'); + $container['router']->addRoute('login', 'auth', 'login'); + $container['router']->addRoute('logout', 'auth', 'logout'); + } + + return $container; + } +} diff --git a/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..f834afe 100644 --- a/sources/app/Subscriber/AuthSubscriber.php +++ b/sources/app/Subscriber/AuthSubscriber.php @@ -2,26 +2,102 @@ namespace Kanboard\Subscriber; -use Kanboard\Core\Http\Request; -use Kanboard\Event\AuthEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Kanboard\Core\Base; +use Kanboard\Core\Security\AuthenticationManager; +use Kanboard\Core\Session\SessionManager; +use Kanboard\Event\AuthSuccessEvent; +use Kanboard\Event\AuthFailureEvent; -class AuthSubscriber extends \Kanboard\Core\Base implements EventSubscriberInterface +/** + * Authentication Subscriber + * + * @package subscriber + * @author Frederic Guillot + */ +class AuthSubscriber extends Base implements EventSubscriberInterface { + /** + * Get event listeners + * + * @static + * @access public + * @return array + */ public static function getSubscribedEvents() { return array( - 'auth.success' => array('onSuccess', 0), + AuthenticationManager::EVENT_SUCCESS => 'afterLogin', + AuthenticationManager::EVENT_FAILURE => 'onLoginFailure', + SessionManager::EVENT_DESTROY => 'afterLogout', ); } - public function onSuccess(AuthEvent $event) + /** + * After Login callback + * + * @access public + * @param AuthSuccessEvent $event + */ + public function afterLogin(AuthSuccessEvent $event) { + $userAgent = $this->request->getUserAgent(); + $ipAddress = $this->request->getIpAddress(); + + $this->userLocking->resetFailedLogin($this->userSession->getUsername()); + $this->lastLogin->create( $event->getAuthType(), - $event->getUserId(), - Request::getIpAddress(), - Request::getUserAgent() + $this->userSession->getId(), + $ipAddress, + $userAgent ); + + 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() + { + $credentials = $this->rememberMeCookie->read(); + + if ($credentials !== false) { + $session = $this->rememberMeSession->find($credentials['token'], $credentials['sequence']); + + if (! empty($session)) { + $this->rememberMeSession->remove($session['id']); + } + + $this->rememberMeCookie->remove(); + } + } + + /** + * Increment failed login counter + * + * @access public + */ + public function onLoginFailure(AuthFailureEvent $event) + { + $username = $event->getUsername(); + + if (! empty($username)) { + $this->userLocking->incrementFailedLogin($username); + + if ($this->userLocking->getFailedLogin($username) > BRUTEFORCE_LOCKDOWN) { + $this->userLocking->lock($username, BRUTEFORCE_LOCKDOWN_DURATION); + } + } } } diff --git a/sources/app/Subscriber/BootstrapSubscriber.php b/sources/app/Subscriber/BootstrapSubscriber.php index 25b919f..e399f68 100644 --- a/sources/app/Subscriber/BootstrapSubscriber.php +++ b/sources/app/Subscriber/BootstrapSubscriber.php @@ -9,9 +9,7 @@ class BootstrapSubscriber extends \Kanboard\Core\Base implements EventSubscriber public static function getSubscribedEvents() { return array( - 'session.bootstrap' => array('setup', 0), - 'api.bootstrap' => array('setup', 0), - 'console.bootstrap' => array('setup', 0), + 'app.bootstrap' => array('setup', 0), ); } @@ -19,5 +17,21 @@ class BootstrapSubscriber extends \Kanboard\Core\Base implements EventSubscriber { $this->config->setupTranslations(); $this->config->setupTimezone(); + $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/Template/activity/project.php b/sources/app/Template/activity/project.php index bc58521..34be06f 100644 --- a/sources/app/Template/activity/project.php +++ b/sources/app/Template/activity/project.php @@ -19,7 +19,7 @@ url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?> - user->isProjectManagementAllowed($project['id'])): ?> + user->hasProjectAccess('project', 'edit', $project['id'])): ?>
  • url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?> diff --git a/sources/app/Template/analytic/layout.php b/sources/app/Template/analytic/layout.php index fd2090a..3bb6ff6 100644 --- a/sources/app/Template/analytic/layout.php +++ b/sources/app/Template/analytic/layout.php @@ -19,7 +19,7 @@ url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
  • - user->isProjectManagementAllowed($project['id'])): ?> + user->hasProjectAccess('project', 'edit', $project['id'])): ?>
  • url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?> diff --git a/sources/app/Template/app/layout.php b/sources/app/Template/app/layout.php index 4f82121..ad1d5a9 100644 --- a/sources/app/Template/app/layout.php +++ b/sources/app/Template/app/layout.php @@ -1,7 +1,7 @@