summaryrefslogtreecommitdiff
path: root/app/Core
diff options
context:
space:
mode:
Diffstat (limited to 'app/Core')
-rw-r--r--app/Core/Action/ActionManager.php4
-rw-r--r--app/Core/Base.php260
-rw-r--r--app/Core/Controller/AccessForbiddenException.php14
-rw-r--r--app/Core/Controller/BaseException.php52
-rw-r--r--app/Core/Controller/BaseMiddleware.php58
-rw-r--r--app/Core/Controller/PageNotFoundException.php14
-rw-r--r--app/Core/Controller/Runner.php105
-rw-r--r--app/Core/Event/EventManager.php22
-rw-r--r--app/Core/Filter/Lexer.php24
-rw-r--r--app/Core/Helper.php3
-rw-r--r--app/Core/Http/Response.php365
-rw-r--r--app/Core/Http/Route.php4
-rw-r--r--app/Core/Http/Router.php62
-rw-r--r--app/Core/Ldap/Group.php7
-rw-r--r--app/Core/Ldap/Query.php4
-rw-r--r--app/Core/Ldap/User.php160
-rw-r--r--app/Core/Mail/Client.php31
-rw-r--r--app/Core/Mail/Transport/Mail.php2
-rw-r--r--app/Core/Markdown.php85
-rw-r--r--app/Core/Notification/NotificationInterface.php32
-rw-r--r--app/Core/Plugin/Base.php16
-rw-r--r--app/Core/Plugin/Directory.php56
-rw-r--r--app/Core/Plugin/Hook.php4
-rw-r--r--app/Core/Plugin/Installer.php162
-rw-r--r--app/Core/Plugin/Loader.php133
-rw-r--r--app/Core/Plugin/PluginInstallerException.php15
-rw-r--r--app/Core/Plugin/SchemaHandler.php122
-rw-r--r--app/Core/Queue/JobHandler.php50
-rw-r--r--app/Core/Queue/QueueManager.php71
-rw-r--r--app/Core/Session/SessionStorage.php1
-rw-r--r--app/Core/Tool.php20
-rw-r--r--app/Core/User/GroupSync.php50
-rw-r--r--app/Core/User/UserProfile.php10
-rw-r--r--app/Core/User/UserProperty.php6
-rw-r--r--app/Core/User/UserSession.php2
-rw-r--r--app/Core/User/UserSync.php10
36 files changed, 1488 insertions, 548 deletions
diff --git a/app/Core/Action/ActionManager.php b/app/Core/Action/ActionManager.php
index dfa5a140..1dfd820c 100644
--- a/app/Core/Action/ActionManager.php
+++ b/app/Core/Action/ActionManager.php
@@ -121,9 +121,9 @@ class ActionManager extends Base
public function attachEvents()
{
if ($this->userSession->isLogged()) {
- $actions = $this->action->getAllByUser($this->userSession->getId());
+ $actions = $this->actionModel->getAllByUser($this->userSession->getId());
} else {
- $actions = $this->action->getAll();
+ $actions = $this->actionModel->getAll();
}
foreach ($actions as $action) {
diff --git a/app/Core/Base.php b/app/Core/Base.php
index 2b619af5..7b4462e2 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -10,134 +10,138 @@ use Pimple\Container;
* @package core
* @author Frederic Guillot
*
- * @property \Kanboard\Analytic\TaskDistributionAnalytic $taskDistributionAnalytic
- * @property \Kanboard\Analytic\UserDistributionAnalytic $userDistributionAnalytic
- * @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic
- * @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic
- * @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic
- * @property \Kanboard\Core\Action\ActionManager $actionManager
- * @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager
- * @property \Kanboard\Core\Cache\MemoryCache $memoryCache
- * @property \Kanboard\Core\Event\EventManager $eventManager
- * @property \Kanboard\Core\Group\GroupManager $groupManager
- * @property \Kanboard\Core\Http\Client $httpClient
- * @property \Kanboard\Core\Http\OAuth2 $oauth
- * @property \Kanboard\Core\Http\RememberMeCookie $rememberMeCookie
- * @property \Kanboard\Core\Http\Request $request
- * @property \Kanboard\Core\Http\Response $response
- * @property \Kanboard\Core\Http\Router $router
- * @property \Kanboard\Core\Http\Route $route
- * @property \Kanboard\Core\Mail\Client $emailClient
- * @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage
- * @property \Kanboard\Core\Plugin\Hook $hook
- * @property \Kanboard\Core\Plugin\Loader $pluginLoader
- * @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\Avatar\AvatarManager $avatarManager
- * @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\Paginator $paginator
- * @property \Kanboard\Core\Template $template
- * @property \Kanboard\Model\Action $action
- * @property \Kanboard\Model\ActionParameter $actionParameter
- * @property \Kanboard\Model\AvatarFile $avatarFile
- * @property \Kanboard\Model\Board $board
- * @property \Kanboard\Model\Category $category
- * @property \Kanboard\Model\Color $color
- * @property \Kanboard\Model\Column $column
- * @property \Kanboard\Model\Comment $comment
- * @property \Kanboard\Model\Config $config
- * @property \Kanboard\Model\Currency $currency
- * @property \Kanboard\Model\CustomFilter $customFilter
- * @property \Kanboard\Model\TaskFile $taskFile
- * @property \Kanboard\Model\ProjectFile $projectFile
- * @property \Kanboard\Model\Group $group
- * @property \Kanboard\Model\GroupMember $groupMember
- * @property \Kanboard\Model\LastLogin $lastLogin
- * @property \Kanboard\Model\Link $link
- * @property \Kanboard\Model\Notification $notification
- * @property \Kanboard\Model\PasswordReset $passwordReset
- * @property \Kanboard\Model\Project $project
- * @property \Kanboard\Model\ProjectActivity $projectActivity
- * @property \Kanboard\Model\ProjectDuplication $projectDuplication
- * @property \Kanboard\Model\ProjectDailyColumnStats $projectDailyColumnStats
- * @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\SubtaskTimeTracking $subtaskTimeTracking
- * @property \Kanboard\Model\Swimlane $swimlane
- * @property \Kanboard\Model\Task $task
- * @property \Kanboard\Model\TaskAnalytic $taskAnalytic
- * @property \Kanboard\Model\TaskCreation $taskCreation
- * @property \Kanboard\Model\TaskDuplication $taskDuplication
- * @property \Kanboard\Model\TaskExternalLink $taskExternalLink
- * @property \Kanboard\Model\TaskFinder $taskFinder
- * @property \Kanboard\Model\TaskLink $taskLink
- * @property \Kanboard\Model\TaskModification $taskModification
- * @property \Kanboard\Model\TaskPermission $taskPermission
- * @property \Kanboard\Model\TaskPosition $taskPosition
- * @property \Kanboard\Model\TaskStatus $taskStatus
- * @property \Kanboard\Model\TaskMetadata $taskMetadata
- * @property \Kanboard\Model\Transition $transition
- * @property \Kanboard\Model\User $user
- * @property \Kanboard\Model\UserLocking $userLocking
- * @property \Kanboard\Model\UserMention $userMention
- * @property \Kanboard\Model\UserNotification $userNotification
- * @property \Kanboard\Model\UserNotificationType $userNotificationType
- * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter
- * @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification
- * @property \Kanboard\Model\UserMetadata $userMetadata
- * @property \Kanboard\Validator\ActionValidator $actionValidator
- * @property \Kanboard\Validator\AuthValidator $authValidator
- * @property \Kanboard\Validator\ColumnValidator $columnValidator
- * @property \Kanboard\Validator\CategoryValidator $categoryValidator
- * @property \Kanboard\Validator\CommentValidator $commentValidator
- * @property \Kanboard\Validator\CurrencyValidator $currencyValidator
- * @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator
- * @property \Kanboard\Validator\GroupValidator $groupValidator
- * @property \Kanboard\Validator\LinkValidator $linkValidator
- * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
- * @property \Kanboard\Validator\ProjectValidator $projectValidator
- * @property \Kanboard\Validator\SubtaskValidator $subtaskValidator
- * @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator
- * @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator
- * @property \Kanboard\Validator\ExternalLinkValidator $externalLinkValidator
- * @property \Kanboard\Validator\TaskValidator $taskValidator
- * @property \Kanboard\Validator\UserValidator $userValidator
- * @property \Kanboard\Import\TaskImport $taskImport
- * @property \Kanboard\Import\UserImport $userImport
- * @property \Kanboard\Export\SubtaskExport $subtaskExport
- * @property \Kanboard\Export\TaskExport $taskExport
- * @property \Kanboard\Export\TransitionExport $transitionExport
- * @property \Kanboard\Core\Filter\QueryBuilder $projectGroupRoleQuery
- * @property \Kanboard\Core\Filter\QueryBuilder $projectUserRoleQuery
- * @property \Kanboard\Core\Filter\QueryBuilder $projectActivityQuery
- * @property \Kanboard\Core\Filter\QueryBuilder $userQuery
- * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery
- * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery
- * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer
- * @property \Kanboard\Core\Filter\LexerBuilder $projectActivityLexer
- * @property \Psr\Log\LoggerInterface $logger
- * @property \PicoDb\Database $db
- * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
+ * @property \Kanboard\Analytic\TaskDistributionAnalytic $taskDistributionAnalytic
+ * @property \Kanboard\Analytic\UserDistributionAnalytic $userDistributionAnalytic
+ * @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic
+ * @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic
+ * @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic
+ * @property \Kanboard\Core\Action\ActionManager $actionManager
+ * @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager
+ * @property \Kanboard\Core\Cache\MemoryCache $memoryCache
+ * @property \Kanboard\Core\Event\EventManager $eventManager
+ * @property \Kanboard\Core\Group\GroupManager $groupManager
+ * @property \Kanboard\Core\Http\Client $httpClient
+ * @property \Kanboard\Core\Http\OAuth2 $oauth
+ * @property \Kanboard\Core\Http\RememberMeCookie $rememberMeCookie
+ * @property \Kanboard\Core\Http\Request $request
+ * @property \Kanboard\Core\Http\Response $response
+ * @property \Kanboard\Core\Http\Router $router
+ * @property \Kanboard\Core\Http\Route $route
+ * @property \Kanboard\Core\Queue\QueueManager $queueManager
+ * @property \Kanboard\Core\Mail\Client $emailClient
+ * @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage
+ * @property \Kanboard\Core\Plugin\Hook $hook
+ * @property \Kanboard\Core\Plugin\Loader $pluginLoader
+ * @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\Avatar\AvatarManager $avatarManager
+ * @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\Paginator $paginator
+ * @property \Kanboard\Core\Template $template
+ * @property \Kanboard\Model\ActionModel $actionModel
+ * @property \Kanboard\Model\ActionParameterModel $actionParameterModel
+ * @property \Kanboard\Model\AvatarFileModel $avatarFileModel
+ * @property \Kanboard\Model\BoardModel $boardModel
+ * @property \Kanboard\Model\CategoryModel $categoryModel
+ * @property \Kanboard\Model\ColorModel $colorModel
+ * @property \Kanboard\Model\ColumnModel $columnModel
+ * @property \Kanboard\Model\CommentModel $commentModel
+ * @property \Kanboard\Model\ConfigModel $configModel
+ * @property \Kanboard\Model\CurrencyModel $currencyModel
+ * @property \Kanboard\Model\CustomFilterModel $customFilterModel
+ * @property \Kanboard\Model\TaskFileModel $taskFileModel
+ * @property \Kanboard\Model\ProjectFileModel $projectFileModel
+ * @property \Kanboard\Model\GroupModel $groupModel
+ * @property \Kanboard\Model\GroupMemberModel $groupMemberModel
+ * @property \Kanboard\Model\LanguageModel $languageModel
+ * @property \Kanboard\Model\LastLoginModel $lastLoginModel
+ * @property \Kanboard\Model\LinkModel $linkModel
+ * @property \Kanboard\Model\NotificationModel $notificationModel
+ * @property \Kanboard\Model\PasswordResetModel $passwordResetModel
+ * @property \Kanboard\Model\ProjectModel $projectModel
+ * @property \Kanboard\Model\ProjectActivityModel $projectActivityModel
+ * @property \Kanboard\Model\ProjectDuplicationModel $projectDuplicationModel
+ * @property \Kanboard\Model\ProjectDailyColumnStatsModel $projectDailyColumnStatsModel
+ * @property \Kanboard\Model\ProjectDailyStatsModel $projectDailyStatsModel
+ * @property \Kanboard\Model\ProjectMetadataModel $projectMetadataModel
+ * @property \Kanboard\Model\ProjectPermissionModel $projectPermissionModel
+ * @property \Kanboard\Model\ProjectUserRoleModel $projectUserRoleModel
+ * @property \Kanboard\Model\ProjectGroupRoleModel $projectGroupRoleModel
+ * @property \Kanboard\Model\ProjectNotificationModel $projectNotificationModel
+ * @property \Kanboard\Model\ProjectNotificationTypeModel $projectNotificationTypeModel
+ * @property \Kanboard\Model\RememberMeSessionModel $rememberMeSessionModel
+ * @property \Kanboard\Model\SubtaskModel $subtaskModel
+ * @property \Kanboard\Model\SubtaskTimeTrackingModel $subtaskTimeTrackingModel
+ * @property \Kanboard\Model\SwimlaneModel $swimlaneModel
+ * @property \Kanboard\Model\TaskModel $taskModel
+ * @property \Kanboard\Model\TaskAnalyticModel $taskAnalyticModel
+ * @property \Kanboard\Model\TaskCreationModel $taskCreationModel
+ * @property \Kanboard\Model\TaskDuplicationModel $taskDuplicationModel
+ * @property \Kanboard\Model\TaskExternalLinkModel $taskExternalLinkModel
+ * @property \Kanboard\Model\TaskFinderModel $taskFinderModel
+ * @property \Kanboard\Model\TaskLinkModel $taskLinkModel
+ * @property \Kanboard\Model\TaskModificationModel $taskModificationModel
+ * @property \Kanboard\Model\TaskPositionModel $taskPositionModel
+ * @property \Kanboard\Model\TaskStatusModel $taskStatusModel
+ * @property \Kanboard\Model\TaskMetadataModel $taskMetadataModel
+ * @property \Kanboard\Model\TimezoneModel $timezoneModel
+ * @property \Kanboard\Model\TransitionModel $transitionModel
+ * @property \Kanboard\Model\UserModel $userModel
+ * @property \Kanboard\Model\UserLockingModel $userLockingModel
+ * @property \Kanboard\Model\UserMentionModel $userMentionModel
+ * @property \Kanboard\Model\UserNotificationModel $userNotificationModel
+ * @property \Kanboard\Model\UserNotificationTypeModel $userNotificationTypeModel
+ * @property \Kanboard\Model\UserNotificationFilterModel $userNotificationFilterModel
+ * @property \Kanboard\Model\UserUnreadNotificationModel $userUnreadNotificationModel
+ * @property \Kanboard\Model\UserMetadataModel $userMetadataModel
+ * @property \Kanboard\Validator\ActionValidator $actionValidator
+ * @property \Kanboard\Validator\AuthValidator $authValidator
+ * @property \Kanboard\Validator\ColumnValidator $columnValidator
+ * @property \Kanboard\Validator\CategoryValidator $categoryValidator
+ * @property \Kanboard\Validator\CommentValidator $commentValidator
+ * @property \Kanboard\Validator\CurrencyValidator $currencyValidator
+ * @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator
+ * @property \Kanboard\Validator\GroupValidator $groupValidator
+ * @property \Kanboard\Validator\LinkValidator $linkValidator
+ * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
+ * @property \Kanboard\Validator\ProjectValidator $projectValidator
+ * @property \Kanboard\Validator\SubtaskValidator $subtaskValidator
+ * @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator
+ * @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator
+ * @property \Kanboard\Validator\ExternalLinkValidator $externalLinkValidator
+ * @property \Kanboard\Validator\TaskValidator $taskValidator
+ * @property \Kanboard\Validator\UserValidator $userValidator
+ * @property \Kanboard\Import\TaskImport $taskImport
+ * @property \Kanboard\Import\UserImport $userImport
+ * @property \Kanboard\Export\SubtaskExport $subtaskExport
+ * @property \Kanboard\Export\TaskExport $taskExport
+ * @property \Kanboard\Export\TransitionExport $transitionExport
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectGroupRoleQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectUserRoleQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectActivityQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $userQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery
+ * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer
+ * @property \Kanboard\Core\Filter\LexerBuilder $projectActivityLexer
+ * @property \Psr\Log\LoggerInterface $logger
+ * @property \PicoDb\Database $db
+ * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
+ * @property \Symfony\Component\Console\Application $cli
+ * @property \JsonRPC\Server $api
*/
abstract class Base
{
diff --git a/app/Core/Controller/AccessForbiddenException.php b/app/Core/Controller/AccessForbiddenException.php
new file mode 100644
index 00000000..b5dccb78
--- /dev/null
+++ b/app/Core/Controller/AccessForbiddenException.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Kanboard\Core\Controller;
+
+/**
+ * Class AccessForbiddenException
+ *
+ * @package Kanboard\Core\Controller
+ * @author Frederic Guillot
+ */
+class AccessForbiddenException extends BaseException
+{
+
+}
diff --git a/app/Core/Controller/BaseException.php b/app/Core/Controller/BaseException.php
new file mode 100644
index 00000000..13836d2c
--- /dev/null
+++ b/app/Core/Controller/BaseException.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Kanboard\Core\Controller;
+
+use Exception;
+
+/**
+ * Class AccessForbiddenException
+ *
+ * @package Kanboard\Core\Controller
+ * @author Frederic Guillot
+ */
+class BaseException extends Exception
+{
+ protected $withoutLayout = false;
+
+ /**
+ * Get object instance
+ *
+ * @static
+ * @access public
+ * @param string $message
+ * @return static
+ */
+ public static function getInstance($message = '')
+ {
+ return new static($message);
+ }
+
+ /**
+ * There is no layout
+ *
+ * @access public
+ * @return BaseException
+ */
+ public function withoutLayout()
+ {
+ $this->withoutLayout = true;
+ return $this;
+ }
+
+ /**
+ * Return true if no layout
+ *
+ * @access public
+ * @return boolean
+ */
+ public function hasLayout()
+ {
+ return $this->withoutLayout;
+ }
+}
diff --git a/app/Core/Controller/BaseMiddleware.php b/app/Core/Controller/BaseMiddleware.php
new file mode 100644
index 00000000..e94ad95c
--- /dev/null
+++ b/app/Core/Controller/BaseMiddleware.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Kanboard\Core\Controller;
+
+use Kanboard\Core\Base;
+
+/**
+ * Class BaseMiddleware
+ *
+ * @package Kanboard\Core\Controller
+ * @author Frederic Guillot
+ */
+abstract class BaseMiddleware extends Base
+{
+ /**
+ * @var BaseMiddleware
+ */
+ protected $nextMiddleware = null;
+
+ /**
+ * Execute middleware
+ */
+ abstract public function execute();
+
+ /**
+ * Set next middleware
+ *
+ * @param BaseMiddleware $nextMiddleware
+ * @return BaseMiddleware
+ */
+ public function setNextMiddleware(BaseMiddleware $nextMiddleware)
+ {
+ $this->nextMiddleware = $nextMiddleware;
+ return $this;
+ }
+
+ /**
+ * @return BaseMiddleware
+ */
+ public function getNextMiddleware()
+ {
+ return $this->nextMiddleware;
+ }
+
+ /**
+ * Move to next middleware
+ */
+ public function next()
+ {
+ if ($this->nextMiddleware !== null) {
+ if (DEBUG) {
+ $this->logger->debug(__METHOD__.' => ' . get_class($this->nextMiddleware));
+ }
+
+ $this->nextMiddleware->execute();
+ }
+ }
+}
diff --git a/app/Core/Controller/PageNotFoundException.php b/app/Core/Controller/PageNotFoundException.php
new file mode 100644
index 00000000..e96a2057
--- /dev/null
+++ b/app/Core/Controller/PageNotFoundException.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Kanboard\Core\Controller;
+
+/**
+ * Class PageNotFoundException
+ *
+ * @package Kanboard\Core\Controller
+ * @author Frederic Guillot
+ */
+class PageNotFoundException extends BaseException
+{
+
+}
diff --git a/app/Core/Controller/Runner.php b/app/Core/Controller/Runner.php
new file mode 100644
index 00000000..8353cf69
--- /dev/null
+++ b/app/Core/Controller/Runner.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Kanboard\Core\Controller;
+
+use Kanboard\Controller\AppController;
+use Kanboard\Core\Base;
+use Kanboard\Middleware\ApplicationAuthorizationMiddleware;
+use Kanboard\Middleware\AuthenticationMiddleware;
+use Kanboard\Middleware\BootstrapMiddleware;
+use Kanboard\Middleware\PostAuthenticationMiddleware;
+use Kanboard\Middleware\ProjectAuthorizationMiddleware;
+use RuntimeException;
+
+/**
+ * Class Runner
+ *
+ * @package Kanboard\Core\Controller
+ * @author Frederic Guillot
+ */
+class Runner extends Base
+{
+ /**
+ * Execute middleware and controller
+ */
+ public function execute()
+ {
+ try {
+ $this->executeMiddleware();
+
+ if (!$this->response->isResponseAlreadySent()) {
+ $this->executeController();
+ }
+ } catch (PageNotFoundException $e) {
+ $controllerObject = new AppController($this->container);
+ $controllerObject->notFound($e->hasLayout());
+ } catch (AccessForbiddenException $e) {
+ $controllerObject = new AppController($this->container);
+ $controllerObject->accessForbidden($e->hasLayout());
+ }
+ }
+
+ /**
+ * Execute all middleware
+ */
+ protected function executeMiddleware()
+ {
+ if (DEBUG) {
+ $this->logger->debug(__METHOD__);
+ }
+
+ $bootstrapMiddleware = new BootstrapMiddleware($this->container);
+ $authenticationMiddleware = new AuthenticationMiddleware($this->container);
+ $postAuthenticationMiddleware = new PostAuthenticationMiddleware($this->container);
+ $appAuthorizationMiddleware = new ApplicationAuthorizationMiddleware($this->container);
+ $projectAuthorizationMiddleware = new ProjectAuthorizationMiddleware($this->container);
+
+ $bootstrapMiddleware->setNextMiddleware($authenticationMiddleware);
+ $authenticationMiddleware->setNextMiddleware($postAuthenticationMiddleware);
+ $postAuthenticationMiddleware->setNextMiddleware($appAuthorizationMiddleware);
+ $appAuthorizationMiddleware->setNextMiddleware($projectAuthorizationMiddleware);
+
+ $bootstrapMiddleware->execute();
+ }
+
+ /**
+ * Execute the controller
+ */
+ protected function executeController()
+ {
+ $className = $this->getControllerClassName();
+
+ if (DEBUG) {
+ $this->logger->debug(__METHOD__.' => '.$className.'::'.$this->router->getAction());
+ }
+
+ $controllerObject = new $className($this->container);
+ $controllerObject->{$this->router->getAction()}();
+ }
+
+ /**
+ * Get controller class name
+ *
+ * @access protected
+ * @return string
+ * @throws RuntimeException
+ */
+ protected function getControllerClassName()
+ {
+ if ($this->router->getPlugin() !== '') {
+ $className = '\Kanboard\Plugin\\'.$this->router->getPlugin().'\Controller\\'.$this->router->getController();
+ } else {
+ $className = '\Kanboard\Controller\\'.$this->router->getController();
+ }
+
+ if (! class_exists($className)) {
+ throw new RuntimeException('Controller not found');
+ }
+
+ if (! method_exists($className, $this->router->getAction())) {
+ throw new RuntimeException('Action not implemented');
+ }
+
+ return $className;
+ }
+}
diff --git a/app/Core/Event/EventManager.php b/app/Core/Event/EventManager.php
index 162d23e8..9ae43170 100644
--- a/app/Core/Event/EventManager.php
+++ b/app/Core/Event/EventManager.php
@@ -2,8 +2,8 @@
namespace Kanboard\Core\Event;
-use Kanboard\Model\Task;
-use Kanboard\Model\TaskLink;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskLinkModel;
/**
* Event Manager
@@ -44,15 +44,15 @@ class EventManager
public function getAll()
{
$events = array(
- TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'),
- Task::EVENT_MOVE_COLUMN => t('Move a task to another column'),
- Task::EVENT_UPDATE => t('Task modification'),
- Task::EVENT_CREATE => t('Task creation'),
- Task::EVENT_OPEN => t('Reopen a task'),
- Task::EVENT_CLOSE => t('Closing a task'),
- Task::EVENT_CREATE_UPDATE => t('Task creation or modification'),
- Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'),
- Task::EVENT_DAILY_CRONJOB => t('Daily background job for tasks'),
+ TaskLinkModel::EVENT_CREATE_UPDATE => t('Task link creation or modification'),
+ TaskModel::EVENT_MOVE_COLUMN => t('Move a task to another column'),
+ TaskModel::EVENT_UPDATE => t('Task modification'),
+ TaskModel::EVENT_CREATE => t('Task creation'),
+ TaskModel::EVENT_OPEN => t('Reopen a task'),
+ TaskModel::EVENT_CLOSE => t('Closing a task'),
+ TaskModel::EVENT_CREATE_UPDATE => t('Task creation or modification'),
+ TaskModel::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'),
+ TaskModel::EVENT_DAILY_CRONJOB => t('Daily background job for tasks'),
);
$events = array_merge($events, $this->events);
diff --git a/app/Core/Filter/Lexer.php b/app/Core/Filter/Lexer.php
index 041b58d3..fa5b8d2d 100644
--- a/app/Core/Filter/Lexer.php
+++ b/app/Core/Filter/Lexer.php
@@ -25,12 +25,13 @@ class Lexer
* @var array
*/
private $tokenMap = array(
- "/^(\s+)/" => 'T_WHITESPACE',
- '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
- '/^(yesterday|tomorrow|today)/' => 'T_DATE',
- '/^("(.*?)")/' => 'T_STRING',
- "/^(\w+)/" => 'T_STRING',
- "/^(#\d+)/" => 'T_STRING',
+ '/^(\s+)/' => 'T_WHITESPACE',
+ '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_STRING',
+ '/^([<=>]{1,2}\w+)/u' => 'T_STRING',
+ '/^([<=>]{1,2}".+")/' => 'T_STRING',
+ '/^("(.+)")/' => 'T_STRING',
+ '/^(\w+)/u' => 'T_STRING',
+ '/^(#\d+)/' => 'T_STRING',
);
/**
@@ -79,9 +80,10 @@ class Lexer
{
$tokens = array();
$this->offset = 0;
+ $input_length = mb_strlen($input, 'UTF-8');
- while (isset($input[$this->offset])) {
- $result = $this->match(substr($input, $this->offset));
+ while ($this->offset < $input_length) {
+ $result = $this->match(mb_substr($input, $this->offset, $input_length, 'UTF-8'));
if ($result === false) {
return array();
@@ -104,10 +106,10 @@ class Lexer
{
foreach ($this->tokenMap as $pattern => $name) {
if (preg_match($pattern, $string, $matches)) {
- $this->offset += strlen($matches[1]);
+ $this->offset += mb_strlen($matches[1], 'UTF-8');
return array(
- 'match' => trim($matches[1], '"'),
+ 'match' => str_replace('"', '', $matches[1]),
'token' => $name,
);
}
@@ -134,7 +136,7 @@ class Lexer
} else {
$next = next($tokens);
- if ($next !== false && in_array($next['token'], array('T_STRING', 'T_DATE'))) {
+ if ($next !== false && $next['token'] === 'T_STRING') {
$map[$token['token']][] = $next['match'];
}
}
diff --git a/app/Core/Helper.php b/app/Core/Helper.php
index 66f8d429..43151be8 100644
--- a/app/Core/Helper.php
+++ b/app/Core/Helper.php
@@ -27,6 +27,7 @@ use Pimple\Container;
* @property \Kanboard\Helper\LayoutHelper $layout
* @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader
* @property \Kanboard\Helper\ProjectActivityHelper $projectActivity
+ * @property \Kanboard\Helper\MailHelper $mail
*/
class Helper
{
@@ -94,7 +95,7 @@ class Helper
{
$container = $this->container;
- $this->helpers[$property] = function() use($className, $container) {
+ $this->helpers[$property] = function() use ($className, $container) {
return new $className($container);
};
diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php
index 996fc58d..0f16fb65 100644
--- a/app/Core/Http/Response.php
+++ b/app/Core/Http/Response.php
@@ -13,296 +13,373 @@ use Kanboard\Core\Csv;
*/
class Response extends Base
{
+ private $httpStatusCode = 200;
+ private $httpHeaders = array();
+ private $httpBody = '';
+ private $responseSent = false;
+
/**
- * Send headers to cache a resource
+ * Return true if the response have been sent to the user agent
*
* @access public
- * @param integer $duration
- * @param string $etag
+ * @return bool
*/
- public function cache($duration, $etag = '')
+ public function isResponseAlreadySent()
{
- header('Pragma: cache');
- header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT');
- header('Cache-Control: public, max-age=' . $duration);
-
- if ($etag) {
- header('ETag: "' . $etag . '"');
- }
+ return $this->responseSent;
}
/**
- * Send no cache headers
+ * Set HTTP status code
*
* @access public
+ * @param integer $statusCode
+ * @return $this
*/
- public function nocache()
+ public function withStatusCode($statusCode)
{
- header('Pragma: no-cache');
- header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
-
- // Use no-store due to a Chrome bug: https://code.google.com/p/chromium/issues/detail?id=28035
- header('Cache-Control: no-store, must-revalidate');
+ $this->httpStatusCode = $statusCode;
+ return $this;
}
/**
- * Send a custom Content-Type header
+ * Set HTTP header
*
* @access public
- * @param string $mimetype Mime-type
+ * @param string $header
+ * @param string $value
+ * @return $this
*/
- public function contentType($mimetype)
+ public function withHeader($header, $value)
{
- header('Content-Type: '.$mimetype);
+ $this->httpHeaders[$header] = $value;
+ return $this;
}
/**
- * Force the browser to download an attachment
+ * Set content type header
*
* @access public
- * @param string $filename File name
+ * @param string $value
+ * @return $this
*/
- public function forceDownload($filename)
+ public function withContentType($value)
{
- header('Content-Disposition: attachment; filename="'.$filename.'"');
- header('Content-Transfer-Encoding: binary');
- header('Content-Type: application/octet-stream');
+ $this->httpHeaders['Content-Type'] = $value;
+ return $this;
}
/**
- * Send a custom HTTP status code
+ * Set default security headers
*
* @access public
- * @param integer $status_code HTTP status code
+ * @return $this
*/
- public function status($status_code)
+ public function withSecurityHeaders()
{
- header('Status: '.$status_code);
- header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$status_code);
+ $this->httpHeaders['X-Content-Type-Options'] = 'nosniff';
+ $this->httpHeaders['X-XSS-Protection'] = '1; mode=block';
+ return $this;
}
/**
- * Redirect to another URL
+ * Set header Content-Security-Policy
*
* @access public
- * @param string $url Redirection URL
- * @param boolean $self If Ajax request and true: refresh the current page
+ * @param array $policies
+ * @return $this
*/
- public function redirect($url, $self = false)
+ public function withContentSecurityPolicy(array $policies = array())
{
- if ($this->request->isAjax()) {
- header('X-Ajax-Redirect: '.($self ? 'self' : $url));
- } else {
- header('Location: '.$url);
+ $values = '';
+
+ foreach ($policies as $policy => $acl) {
+ $values .= $policy.' '.trim($acl).'; ';
}
- exit;
+ $this->withHeader('Content-Security-Policy', $values);
+ return $this;
}
/**
- * Send a CSV response
+ * Set header X-Frame-Options
*
* @access public
- * @param array $data Data to serialize in csv
- * @param integer $status_code HTTP status code
+ * @return $this
*/
- public function csv(array $data, $status_code = 200)
+ public function withXframe()
{
- $this->status($status_code);
- $this->nocache();
+ $this->withHeader('X-Frame-Options', 'DENY');
+ return $this;
+ }
- header('Content-Type: text/csv');
- Csv::output($data);
- exit;
+ /**
+ * Set header Strict-Transport-Security (only if we use HTTPS)
+ *
+ * @access public
+ * @return $this
+ */
+ public function withStrictTransportSecurity()
+ {
+ if ($this->request->isHTTPS()) {
+ $this->withHeader('Strict-Transport-Security', 'max-age=31536000');
+ }
+
+ return $this;
}
/**
- * Send a Json response
+ * Set HTTP response body
*
* @access public
- * @param array $data Data to serialize in json
- * @param integer $status_code HTTP status code
+ * @param string $body
+ * @return $this
*/
- public function json(array $data, $status_code = 200)
+ public function withBody($body)
{
- $this->status($status_code);
- $this->nocache();
- header('Content-Type: application/json');
- echo json_encode($data);
- exit;
+ $this->httpBody = $body;
+ return $this;
}
/**
- * Send a text response
+ * Send headers to cache a resource
*
* @access public
- * @param string $data Raw data
- * @param integer $status_code HTTP status code
+ * @param integer $duration
+ * @param string $etag
+ * @return $this
*/
- public function text($data, $status_code = 200)
+ public function withCache($duration, $etag = '')
{
- $this->status($status_code);
- $this->nocache();
- header('Content-Type: text/plain; charset=utf-8');
- echo $data;
- exit;
+ $this
+ ->withHeader('Pragma', 'cache')
+ ->withHeader('Expires', gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT')
+ ->withHeader('Cache-Control', 'public, max-age=' . $duration)
+ ;
+
+ if ($etag) {
+ $this->withHeader('ETag', '"' . $etag . '"');
+ }
+
+ return $this;
}
/**
- * Send a HTML response
+ * Send no cache headers
*
* @access public
- * @param string $data Raw data
- * @param integer $status_code HTTP status code
+ * @return $this
*/
- public function html($data, $status_code = 200)
+ public function withoutCache()
{
- $this->status($status_code);
- $this->nocache();
- header('Content-Type: text/html; charset=utf-8');
- echo $data;
- exit;
+ $this->withHeader('Pragma', 'no-cache');
+ $this->withHeader('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT');
+ return $this;
}
/**
- * Send a XML response
+ * Force the browser to download an attachment
*
* @access public
- * @param string $data Raw data
- * @param integer $status_code HTTP status code
+ * @param string $filename
+ * @return $this
*/
- public function xml($data, $status_code = 200)
+ public function withFileDownload($filename)
{
- $this->status($status_code);
- $this->nocache();
- header('Content-Type: text/xml; charset=utf-8');
- echo $data;
- exit;
+ $this->withHeader('Content-Disposition', 'attachment; filename="'.$filename.'"');
+ $this->withHeader('Content-Transfer-Encoding', 'binary');
+ $this->withHeader('Content-Type', 'application/octet-stream');
+ return $this;
}
/**
- * Send a javascript response
+ * Send headers and body
*
* @access public
- * @param string $data Raw data
- * @param integer $status_code HTTP status code
*/
- public function js($data, $status_code = 200)
+ public function send()
{
- $this->status($status_code);
+ $this->responseSent = true;
- header('Content-Type: text/javascript; charset=utf-8');
- echo $data;
+ if ($this->httpStatusCode !== 200) {
+ header('Status: '.$this->httpStatusCode);
+ header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$this->httpStatusCode);
+ }
- exit;
+ foreach ($this->httpHeaders as $header => $value) {
+ header($header.': '.$value);
+ }
+
+ if (! empty($this->httpBody)) {
+ echo $this->httpBody;
+ }
}
/**
- * Send a css response
+ * Send a custom HTTP status code
*
* @access public
- * @param string $data Raw data
- * @param integer $status_code HTTP status code
+ * @param integer $statusCode
*/
- public function css($data, $status_code = 200)
+ public function status($statusCode)
{
- $this->status($status_code);
+ $this->withStatusCode($statusCode);
+ $this->send();
+ }
- header('Content-Type: text/css; charset=utf-8');
- echo $data;
+ /**
+ * Redirect to another URL
+ *
+ * @access public
+ * @param string $url Redirection URL
+ * @param boolean $self If Ajax request and true: refresh the current page
+ */
+ public function redirect($url, $self = false)
+ {
+ if ($this->request->isAjax()) {
+ $this->withHeader('X-Ajax-Redirect', $self ? 'self' : $url);
+ } else {
+ $this->withHeader('Location', $url);
+ }
- exit;
+ $this->send();
}
/**
- * Send a binary response
+ * Send a HTML response
*
* @access public
- * @param string $data Raw data
- * @param integer $status_code HTTP status code
+ * @param string $data
+ * @param integer $statusCode
*/
- public function binary($data, $status_code = 200)
+ public function html($data, $statusCode = 200)
{
- $this->status($status_code);
- $this->nocache();
- header('Content-Transfer-Encoding: binary');
- header('Content-Type: application/octet-stream');
- echo $data;
- exit;
+ $this->withStatusCode($statusCode);
+ $this->withContentType('text/html; charset=utf-8');
+ $this->withBody($data);
+ $this->send();
}
/**
- * Send a iCal response
+ * Send a text response
*
* @access public
- * @param string $data Raw data
- * @param integer $status_code HTTP status code
+ * @param string $data
+ * @param integer $statusCode
*/
- public function ical($data, $status_code = 200)
+ public function text($data, $statusCode = 200)
{
- $this->status($status_code);
- $this->contentType('text/calendar; charset=utf-8');
- echo $data;
+ $this->withStatusCode($statusCode);
+ $this->withContentType('text/plain; charset=utf-8');
+ $this->withBody($data);
+ $this->send();
}
/**
- * Send the security header: Content-Security-Policy
+ * Send a CSV response
*
* @access public
- * @param array $policies CSP rules
+ * @param array $data Data to serialize in csv
*/
- public function csp(array $policies = array())
+ public function csv(array $data)
{
- $values = '';
+ $this->withoutCache();
+ $this->withContentType('text/csv; charset=utf-8');
+ $this->send();
+ Csv::output($data);
+ }
- foreach ($policies as $policy => $acl) {
- $values .= $policy.' '.trim($acl).'; ';
- }
+ /**
+ * Send a Json response
+ *
+ * @access public
+ * @param array $data Data to serialize in json
+ * @param integer $statusCode HTTP status code
+ */
+ public function json(array $data, $statusCode = 200)
+ {
+ $this->withStatusCode($statusCode);
+ $this->withContentType('application/json');
+ $this->withoutCache();
+ $this->withBody(json_encode($data));
+ $this->send();
+ }
- header('Content-Security-Policy: '.$values);
+ /**
+ * Send a XML response
+ *
+ * @access public
+ * @param string $data
+ * @param integer $statusCode
+ */
+ public function xml($data, $statusCode = 200)
+ {
+ $this->withStatusCode($statusCode);
+ $this->withContentType('text/xml; charset=utf-8');
+ $this->withoutCache();
+ $this->withBody($data);
+ $this->send();
}
/**
- * Send the security header: X-Content-Type-Options
+ * Send a javascript response
*
* @access public
+ * @param string $data
+ * @param integer $statusCode
*/
- public function nosniff()
+ public function js($data, $statusCode = 200)
{
- header('X-Content-Type-Options: nosniff');
+ $this->withStatusCode($statusCode);
+ $this->withContentType('text/javascript; charset=utf-8');
+ $this->withBody($data);
+ $this->send();
}
/**
- * Send the security header: X-XSS-Protection
+ * Send a css response
*
* @access public
+ * @param string $data
+ * @param integer $statusCode
*/
- public function xss()
+ public function css($data, $statusCode = 200)
{
- header('X-XSS-Protection: 1; mode=block');
+ $this->withStatusCode($statusCode);
+ $this->withContentType('text/css; charset=utf-8');
+ $this->withBody($data);
+ $this->send();
}
/**
- * Send the security header: Strict-Transport-Security (only if we use HTTPS)
+ * Send a binary response
*
* @access public
+ * @param string $data
+ * @param integer $statusCode
*/
- public function hsts()
+ public function binary($data, $statusCode = 200)
{
- if ($this->request->isHTTPS()) {
- header('Strict-Transport-Security: max-age=31536000');
- }
+ $this->withStatusCode($statusCode);
+ $this->withoutCache();
+ $this->withHeader('Content-Transfer-Encoding', 'binary');
+ $this->withContentType('application/octet-stream');
+ $this->withBody($data);
+ $this->send();
}
/**
- * Send the security header: X-Frame-Options (deny by default)
+ * Send a iCal response
*
* @access public
- * @param string $mode Frame option mode
- * @param array $urls Allowed urls for the given mode
+ * @param string $data
+ * @param integer $statusCode
*/
- public function xframe($mode = 'DENY', array $urls = array())
+ public function ical($data, $statusCode = 200)
{
- header('X-Frame-Options: '.$mode.' '.implode(' ', $urls));
+ $this->withStatusCode($statusCode);
+ $this->withContentType('text/calendar; charset=utf-8');
+ $this->withBody($data);
+ $this->send();
}
}
diff --git a/app/Core/Http/Route.php b/app/Core/Http/Route.php
index 7836146d..9b45b725 100644
--- a/app/Core/Http/Route.php
+++ b/app/Core/Http/Route.php
@@ -119,8 +119,8 @@ class Route extends Base
}
return array(
- 'controller' => 'app',
- 'action' => 'index',
+ 'controller' => 'DashboardController',
+ 'action' => 'show',
'plugin' => '',
);
}
diff --git a/app/Core/Http/Router.php b/app/Core/Http/Router.php
index 0fe80ecc..4de276a0 100644
--- a/app/Core/Http/Router.php
+++ b/app/Core/Http/Router.php
@@ -2,7 +2,6 @@
namespace Kanboard\Core\Http;
-use RuntimeException;
use Kanboard\Core\Base;
/**
@@ -13,13 +12,16 @@ use Kanboard\Core\Base;
*/
class Router extends Base
{
+ const DEFAULT_CONTROLLER = 'DashboardController';
+ const DEFAULT_METHOD = 'show';
+
/**
* Plugin name
*
* @access private
* @var string
*/
- private $plugin = '';
+ private $currentPluginName = '';
/**
* Controller
@@ -27,7 +29,7 @@ class Router extends Base
* @access private
* @var string
*/
- private $controller = '';
+ private $currentControllerName = '';
/**
* Action
@@ -35,7 +37,7 @@ class Router extends Base
* @access private
* @var string
*/
- private $action = '';
+ private $currentActionName = '';
/**
* Get plugin name
@@ -45,7 +47,7 @@ class Router extends Base
*/
public function getPlugin()
{
- return $this->plugin;
+ return $this->currentPluginName;
}
/**
@@ -56,7 +58,7 @@ class Router extends Base
*/
public function getController()
{
- return $this->controller;
+ return $this->currentControllerName;
}
/**
@@ -67,7 +69,7 @@ class Router extends Base
*/
public function getAction()
{
- return $this->action;
+ return $this->currentActionName;
}
/**
@@ -109,11 +111,9 @@ class Router extends Base
$plugin = $route['plugin'];
}
- $this->controller = ucfirst($this->sanitize($controller, 'app'));
- $this->action = $this->sanitize($action, 'index');
- $this->plugin = ucfirst($this->sanitize($plugin));
-
- return $this->executeAction();
+ $this->currentControllerName = ucfirst($this->sanitize($controller, self::DEFAULT_CONTROLLER));
+ $this->currentActionName = $this->sanitize($action, self::DEFAULT_METHOD);
+ $this->currentPluginName = ucfirst($this->sanitize($plugin));
}
/**
@@ -128,42 +128,4 @@ class Router extends Base
{
return preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $value : $default;
}
-
- /**
- * Execute controller action
- *
- * @access private
- */
- private function executeAction()
- {
- $class = $this->getControllerClassName();
-
- if (! class_exists($class)) {
- throw new RuntimeException('Controller not found');
- }
-
- if (! method_exists($class, $this->action)) {
- throw new RuntimeException('Action not implemented');
- }
-
- $instance = new $class($this->container);
- $instance->beforeAction();
- $instance->{$this->action}();
- return $instance;
- }
-
- /**
- * Get controller class name
- *
- * @access private
- * @return string
- */
- private function getControllerClassName()
- {
- if ($this->plugin !== '') {
- return '\Kanboard\Plugin\\'.$this->plugin.'\Controller\\'.$this->controller;
- }
-
- return '\Kanboard\Controller\\'.$this->controller;
- }
}
diff --git a/app/Core/Ldap/Group.php b/app/Core/Ldap/Group.php
index 634d47ee..e1f60ab5 100644
--- a/app/Core/Ldap/Group.php
+++ b/app/Core/Ldap/Group.php
@@ -39,12 +39,11 @@ class Group
* @access public
* @param Client $client
* @param string $query
- * @return array
+ * @return LdapGroupProvider[]
*/
public static function getGroups(Client $client, $query)
{
- $className = get_called_class();
- $self = new $className(new Query($client));
+ $self = new static(new Query($client));
return $self->find($query);
}
@@ -111,7 +110,7 @@ class Group
throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_GROUP_ATTRIBUTE_NAME');
}
- return LDAP_GROUP_ATTRIBUTE_NAME;
+ return strtolower(LDAP_GROUP_ATTRIBUTE_NAME);
}
/**
diff --git a/app/Core/Ldap/Query.php b/app/Core/Ldap/Query.php
index 7c1524ca..0f9abb5c 100644
--- a/app/Core/Ldap/Query.php
+++ b/app/Core/Ldap/Query.php
@@ -66,6 +66,10 @@ class Query
$this->entries = $entries;
+ if (DEBUG && $this->client->hasLogger()) {
+ $this->client->getLogger()->debug('NbEntries='.$entries['count']);
+ }
+
return $this;
}
diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php
index d23ec07e..91b48530 100644
--- a/app/Core/Ldap/User.php
+++ b/app/Core/Ldap/User.php
@@ -23,14 +23,24 @@ class User
protected $query;
/**
+ * LDAP Group object
+ *
+ * @access protected
+ * @var Group
+ */
+ protected $group;
+
+ /**
* Constructor
*
* @access public
- * @param Query $query
+ * @param Query $query
+ * @param Group $group
*/
- public function __construct(Query $query)
+ public function __construct(Query $query, Group $group = null)
{
$this->query = $query;
+ $this->group = $group;
}
/**
@@ -44,7 +54,7 @@ class User
*/
public static function getUser(Client $client, $username)
{
- $self = new static(new Query($client));
+ $self = new static(new Query($client), new Group(new Query($client)));
return $self->find($self->getLdapUserPattern($username));
}
@@ -53,7 +63,7 @@ class User
*
* @access public
* @param string $query
- * @return null|LdapUserProvider
+ * @return LdapUserProvider
*/
public function find($query)
{
@@ -68,6 +78,62 @@ class User
}
/**
+ * Get user groupIds (DN)
+ *
+ * 1) If configured, use memberUid and posixGroup
+ * 2) Otherwise, use memberOf
+ *
+ * @access protected
+ * @param Entry $entry
+ * @param string $username
+ * @return string[]
+ */
+ protected function getGroups(Entry $entry, $username)
+ {
+ $groupIds = array();
+
+ if (! empty($username) && $this->group !== null && $this->hasGroupUserFilter()) {
+ $groups = $this->group->find(sprintf($this->getGroupUserFilter(), $username));
+
+ foreach ($groups as $group) {
+ $groupIds[] = $group->getExternalId();
+ }
+ } else {
+ $groupIds = $entry->getAll($this->getAttributeGroup());
+ }
+
+ return $groupIds;
+ }
+
+ /**
+ * Get role from LDAP groups
+ *
+ * Note: Do not touch the current role if groups are not configured
+ *
+ * @access protected
+ * @param string[] $groupIds
+ * @return string
+ */
+ protected function getRole(array $groupIds)
+ {
+ if ($this->hasGroupsNotConfigured()) {
+ return null;
+ }
+
+ foreach ($groupIds as $groupId) {
+ $groupId = strtolower($groupId);
+
+ if ($groupId === strtolower($this->getGroupAdminDn())) {
+ return Role::APP_ADMIN;
+ } elseif ($groupId === strtolower($this->getGroupManagerDn())) {
+ return Role::APP_MANAGER;
+ }
+ }
+
+ return Role::APP_USER;
+ }
+
+ /**
* Build user profile
*
* @access protected
@@ -76,21 +142,18 @@ class User
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;
- }
+ $username = $entry->getFirstValue($this->getAttributeUsername());
+ $groupIds = $this->getGroups($entry, $username);
return new LdapUserProvider(
$entry->getDn(),
- $entry->getFirstValue($this->getAttributeUsername()),
+ $username,
$entry->getFirstValue($this->getAttributeName()),
$entry->getFirstValue($this->getAttributeEmail()),
- $role,
- $entry->getAll($this->getAttributeGroup())
+ $this->getRole($groupIds),
+ $groupIds,
+ $entry->getFirstValue($this->getAttributePhoto()),
+ $entry->getFirstValue($this->getAttributeLanguage())
);
}
@@ -109,6 +172,8 @@ class User
$this->getAttributeName(),
$this->getAttributeEmail(),
$this->getAttributeGroup(),
+ $this->getAttributePhoto(),
+ $this->getAttributeLanguage(),
)));
}
@@ -124,7 +189,7 @@ class User
throw new LogicException('LDAP username attribute empty, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
}
- return LDAP_USER_ATTRIBUTE_USERNAME;
+ return strtolower(LDAP_USER_ATTRIBUTE_USERNAME);
}
/**
@@ -139,7 +204,7 @@ class User
throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_USER_ATTRIBUTE_FULLNAME');
}
- return LDAP_USER_ATTRIBUTE_FULLNAME;
+ return strtolower(LDAP_USER_ATTRIBUTE_FULLNAME);
}
/**
@@ -154,18 +219,73 @@ class User
throw new LogicException('LDAP email attribute empty, check the parameter LDAP_USER_ATTRIBUTE_EMAIL');
}
- return LDAP_USER_ATTRIBUTE_EMAIL;
+ return strtolower(LDAP_USER_ATTRIBUTE_EMAIL);
}
/**
- * Get LDAP account memberof attribute
+ * Get LDAP account memberOf attribute
*
* @access public
* @return string
*/
public function getAttributeGroup()
{
- return LDAP_USER_ATTRIBUTE_GROUPS;
+ return strtolower(LDAP_USER_ATTRIBUTE_GROUPS);
+ }
+
+ /**
+ * Get LDAP profile photo attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributePhoto()
+ {
+ return strtolower(LDAP_USER_ATTRIBUTE_PHOTO);
+ }
+
+ /**
+ * Get LDAP language attribute
+ *
+ * @access public
+ * @return string
+ */
+ public function getAttributeLanguage()
+ {
+ return strtolower(LDAP_USER_ATTRIBUTE_LANGUAGE);
+ }
+
+ /**
+ * Get LDAP Group User filter
+ *
+ * @access public
+ * @return string
+ */
+ public function getGroupUserFilter()
+ {
+ return LDAP_GROUP_USER_FILTER;
+ }
+
+ /**
+ * Return true if LDAP Group User filter is defined
+ *
+ * @access public
+ * @return string
+ */
+ public function hasGroupUserFilter()
+ {
+ return $this->getGroupUserFilter() !== '' && $this->getGroupUserFilter() !== null;
+ }
+
+ /**
+ * Return true if LDAP Group mapping is not configured
+ *
+ * @access public
+ * @return boolean
+ */
+ public function hasGroupsNotConfigured()
+ {
+ return !$this->getGroupAdminDn() && !$this->getGroupManagerDn();
}
/**
@@ -176,7 +296,7 @@ class User
*/
public function getGroupAdminDn()
{
- return LDAP_GROUP_ADMIN_DN;
+ return strtolower(LDAP_GROUP_ADMIN_DN);
}
/**
diff --git a/app/Core/Mail/Client.php b/app/Core/Mail/Client.php
index 641b6abe..44f4753a 100644
--- a/app/Core/Mail/Client.php
+++ b/app/Core/Mail/Client.php
@@ -2,6 +2,7 @@
namespace Kanboard\Core\Mail;
+use Kanboard\Job\EmailJob;
use Pimple\Container;
use Kanboard\Core\Base;
@@ -46,23 +47,29 @@ class Client extends Base
public function send($email, $name, $subject, $html)
{
if (! empty($email)) {
- $this->logger->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
-
- $start_time = microtime(true);
- $author = 'Kanboard';
+ $this->queueManager->push(EmailJob::getInstance($this->container)
+ ->withParams($email, $name, $subject, $html, $this->getAuthor())
+ );
+ }
- if ($this->userSession->isLogged()) {
- $author = e('%s via Kanboard', $this->helper->user->getFullname());
- }
+ return $this;
+ }
- $this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author);
+ /**
+ * Get email author
+ *
+ * @access public
+ * @return string
+ */
+ public function getAuthor()
+ {
+ $author = 'Kanboard';
- if (DEBUG) {
- $this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
- }
+ if ($this->userSession->isLogged()) {
+ $author = e('%s via Kanboard', $this->helper->user->getFullname());
}
- return $this;
+ return $author;
}
/**
diff --git a/app/Core/Mail/Transport/Mail.php b/app/Core/Mail/Transport/Mail.php
index aff3ee20..d27925f0 100644
--- a/app/Core/Mail/Transport/Mail.php
+++ b/app/Core/Mail/Transport/Mail.php
@@ -32,7 +32,7 @@ class Mail extends Base implements ClientInterface
try {
$message = Swift_Message::newInstance()
->setSubject($subject)
- ->setFrom(array(MAIL_FROM => $author))
+ ->setFrom(array($this->helper->mail->getMailSenderAddress() => $author))
->setBody($html, 'text/html')
->setTo(array($email => $name));
diff --git a/app/Core/Markdown.php b/app/Core/Markdown.php
index 827fd0df..b5abe5ed 100644
--- a/app/Core/Markdown.php
+++ b/app/Core/Markdown.php
@@ -15,12 +15,12 @@ use Pimple\Container;
class Markdown extends Parsedown
{
/**
- * Link params for tasks
+ * Task links generated will use the project token instead
*
* @access private
- * @var array
+ * @var boolean
*/
- private $link = array();
+ private $isPublicLink = false;
/**
* Container
@@ -35,11 +35,11 @@ class Markdown extends Parsedown
*
* @access public
* @param Container $container
- * @param array $link
+ * @param boolean $isPublicLink
*/
- public function __construct(Container $container, array $link)
+ public function __construct(Container $container, $isPublicLink)
{
- $this->link = $link;
+ $this->isPublicLink = $isPublicLink;
$this->container = $container;
$this->InlineTypes['#'][] = 'TaskLink';
$this->InlineTypes['@'][] = 'UserLink';
@@ -53,26 +53,26 @@ class Markdown extends Parsedown
*
* @access public
* @param array $Excerpt
- * @return array
+ * @return array|null
*/
protected function inlineTaskLink(array $Excerpt)
{
- if (! empty($this->link) && preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) {
- $url = $this->container['helper']->url->href(
- $this->link['controller'],
- $this->link['action'],
- $this->link['params'] + array('task_id' => $matches[1])
- );
+ if (preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) {
+ $link = $this->buildTaskLink($matches[1]);
- return array(
- 'extent' => strlen($matches[0]),
- 'element' => array(
- 'name' => 'a',
- 'text' => $matches[0],
- 'attributes' => array('href' => $url)
- ),
- );
+ if (! empty($link)) {
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $matches[0],
+ 'attributes' => array('href' => $link),
+ ),
+ );
+ }
}
+
+ return null;
}
/**
@@ -82,15 +82,15 @@ class Markdown extends Parsedown
*
* @access public
* @param array $Excerpt
- * @return array
+ * @return array|null
*/
protected function inlineUserLink(array $Excerpt)
{
- if (preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) {
- $user_id = $this->container['user']->getIdByUsername($matches[1]);
+ if (! $this->isPublicLink && preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) {
+ $user_id = $this->container['userModel']->getIdByUsername($matches[1]);
if (! empty($user_id)) {
- $url = $this->container['helper']->url->href('user', 'profile', array('user_id' => $user_id));
+ $url = $this->container['helper']->url->href('UserViewController', 'profile', array('user_id' => $user_id));
return array(
'extent' => strlen($matches[0]),
@@ -102,5 +102,40 @@ class Markdown extends Parsedown
);
}
}
+
+ return null;
+ }
+
+ /**
+ * Build task link
+ *
+ * @access private
+ * @param integer $task_id
+ * @return string
+ */
+ private function buildTaskLink($task_id)
+ {
+ if ($this->isPublicLink) {
+ $token = $this->container['memoryCache']->proxy($this->container['taskFinderModel'], 'getProjectToken', $task_id);
+
+ if (! empty($token)) {
+ return $this->container['helper']->url->href(
+ 'TaskViewController',
+ 'readonly',
+ array(
+ 'token' => $token,
+ 'task_id' => $task_id,
+ )
+ );
+ }
+
+ return '';
+ }
+
+ return $this->container['helper']->url->href(
+ 'TaskViewController',
+ 'show',
+ array('task_id' => $task_id)
+ );
}
}
diff --git a/app/Core/Notification/NotificationInterface.php b/app/Core/Notification/NotificationInterface.php
new file mode 100644
index 00000000..d336983a
--- /dev/null
+++ b/app/Core/Notification/NotificationInterface.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Kanboard\Core\Notification;
+
+/**
+ * Notification Interface
+ *
+ * @package Kanboard\Core\Notification
+ * @author Frederic Guillot
+ */
+interface NotificationInterface
+{
+ /**
+ * Send notification to a user
+ *
+ * @access public
+ * @param array $user
+ * @param string $event_name
+ * @param array $event_data
+ */
+ public function notifyUser(array $user, $event_name, array $event_data);
+
+ /**
+ * Send notification to a project
+ *
+ * @access public
+ * @param array $project
+ * @param string $event_name
+ * @param array $event_data
+ */
+ public function notifyProject(array $project, $event_name, array $event_data);
+}
diff --git a/app/Core/Plugin/Base.php b/app/Core/Plugin/Base.php
index 381b8bb3..9d8167a9 100644
--- a/app/Core/Plugin/Base.php
+++ b/app/Core/Plugin/Base.php
@@ -5,8 +5,8 @@ namespace Kanboard\Core\Plugin;
/**
* Plugin Base class
*
- * @package plugin
- * @author Frederic Guillot
+ * @package Kanboard\Core\Plugin
+ * @author Frederic Guillot
*/
abstract class Base extends \Kanboard\Core\Base
{
@@ -62,7 +62,7 @@ abstract class Base extends \Kanboard\Core\Base
{
$container = $this->container;
- $this->container['dispatcher']->addListener($event, function () use ($container, $callback) {
+ $this->dispatcher->addListener($event, function () use ($container, $callback) {
call_user_func($callback, $container);
});
}
@@ -70,7 +70,7 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Get plugin name
*
- * This method should be overrided by your Plugin class
+ * This method should be overridden by your Plugin class
*
* @access public
* @return string
@@ -83,7 +83,7 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Get plugin description
*
- * This method should be overrided by your Plugin class
+ * This method should be overridden by your Plugin class
*
* @access public
* @return string
@@ -96,7 +96,7 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Get plugin author
*
- * This method should be overrided by your Plugin class
+ * This method should be overridden by your Plugin class
*
* @access public
* @return string
@@ -109,7 +109,7 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Get plugin version
*
- * This method should be overrided by your Plugin class
+ * This method should be overridden by your Plugin class
*
* @access public
* @return string
@@ -122,7 +122,7 @@ abstract class Base extends \Kanboard\Core\Base
/**
* Get plugin homepage
*
- * This method should be overrided by your Plugin class
+ * This method should be overridden by your Plugin class
*
* @access public
* @return string
diff --git a/app/Core/Plugin/Directory.php b/app/Core/Plugin/Directory.php
new file mode 100644
index 00000000..21f11ca9
--- /dev/null
+++ b/app/Core/Plugin/Directory.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Kanboard\Core\Plugin;
+
+use Kanboard\Core\Base as BaseCore;
+
+/**
+ * Class Directory
+ *
+ * @package Kanboard\Core\Plugin
+ * @author Frederic Guillot
+ */
+class Directory extends BaseCore
+{
+ /**
+ * Get all plugins available
+ *
+ * @access public
+ * @param string $url
+ * @return array
+ */
+ public function getAvailablePlugins($url = PLUGIN_API_URL)
+ {
+ $plugins = $this->httpClient->getJson($url);
+ $plugins = array_filter($plugins, array($this, 'isCompatible'));
+ $plugins = array_filter($plugins, array($this, 'isInstallable'));
+ return $plugins;
+ }
+
+ /**
+ * Filter plugins
+ *
+ * @param array $plugin
+ * @param string $appVersion
+ * @return bool
+ */
+ public function isCompatible(array $plugin, $appVersion = APP_VERSION)
+ {
+ if (strpos($appVersion, 'master') !== false) {
+ return true;
+ }
+
+ return $plugin['compatible_version'] === $appVersion;
+ }
+
+ /**
+ * Filter plugins
+ *
+ * @param array $plugin
+ * @return bool
+ */
+ public function isInstallable(array $plugin)
+ {
+ return $plugin['remote_install'];
+ }
+}
diff --git a/app/Core/Plugin/Hook.php b/app/Core/Plugin/Hook.php
index a3bcd918..ade69150 100644
--- a/app/Core/Plugin/Hook.php
+++ b/app/Core/Plugin/Hook.php
@@ -5,8 +5,8 @@ namespace Kanboard\Core\Plugin;
/**
* Plugin Hooks Handler
*
- * @package plugin
- * @author Frederic Guillot
+ * @package Kanboard\Core\Plugin
+ * @author Frederic Guillot
*/
class Hook
{
diff --git a/app/Core/Plugin/Installer.php b/app/Core/Plugin/Installer.php
new file mode 100644
index 00000000..48c4d978
--- /dev/null
+++ b/app/Core/Plugin/Installer.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Kanboard\Core\Plugin;
+
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use ZipArchive;
+
+/**
+ * Class Installer
+ *
+ * @package Kanboard\Core\Plugin
+ * @author Frederic Guillot
+ */
+class Installer extends \Kanboard\Core\Base
+{
+ /**
+ * Return true if Kanboard is configured to install plugins
+ *
+ * @static
+ * @access public
+ * @return bool
+ */
+ public static function isConfigured()
+ {
+ return PLUGIN_INSTALLER && is_writable(PLUGINS_DIR) && extension_loaded('zip');
+ }
+
+ /**
+ * Install a plugin
+ *
+ * @access public
+ * @param string $archiveUrl
+ * @throws PluginInstallerException
+ */
+ public function install($archiveUrl)
+ {
+ $zip = $this->downloadPluginArchive($archiveUrl);
+
+ if (! $zip->extractTo(PLUGINS_DIR)) {
+ $this->cleanupArchive($zip);
+ throw new PluginInstallerException(t('Unable to extract plugin archive.'));
+ }
+
+ $this->cleanupArchive($zip);
+ }
+
+ /**
+ * Uninstall a plugin
+ *
+ * @access public
+ * @param string $pluginId
+ * @throws PluginInstallerException
+ */
+ public function uninstall($pluginId)
+ {
+ $pluginFolder = PLUGINS_DIR.DIRECTORY_SEPARATOR.basename($pluginId);
+
+ if (! file_exists($pluginFolder)) {
+ throw new PluginInstallerException(t('Plugin not found.'));
+ }
+
+ if (! is_writable($pluginFolder)) {
+ throw new PluginInstallerException(e('You don\'t have the permission to remove this plugin.'));
+ }
+
+ $this->removeAllDirectories($pluginFolder);
+ }
+
+ /**
+ * Update a plugin
+ *
+ * @access public
+ * @param string $archiveUrl
+ * @throws PluginInstallerException
+ */
+ public function update($archiveUrl)
+ {
+ $zip = $this->downloadPluginArchive($archiveUrl);
+
+ $firstEntry = $zip->statIndex(0);
+ $this->uninstall($firstEntry['name']);
+
+ if (! $zip->extractTo(PLUGINS_DIR)) {
+ $this->cleanupArchive($zip);
+ throw new PluginInstallerException(t('Unable to extract plugin archive.'));
+ }
+
+ $this->cleanupArchive($zip);
+ }
+
+ /**
+ * Download archive from URL
+ *
+ * @access protected
+ * @param string $archiveUrl
+ * @return ZipArchive
+ * @throws PluginInstallerException
+ */
+ protected function downloadPluginArchive($archiveUrl)
+ {
+ $zip = new ZipArchive();
+ $archiveData = $this->httpClient->get($archiveUrl);
+ $archiveFile = tempnam(sys_get_temp_dir(), 'kb_plugin');
+
+ if (empty($archiveData)) {
+ unlink($archiveFile);
+ throw new PluginInstallerException(t('Unable to download plugin archive.'));
+ }
+
+ if (file_put_contents($archiveFile, $archiveData) === false) {
+ unlink($archiveFile);
+ throw new PluginInstallerException(t('Unable to write temporary file for plugin.'));
+ }
+
+ if ($zip->open($archiveFile) !== true) {
+ unlink($archiveFile);
+ throw new PluginInstallerException(t('Unable to open plugin archive.'));
+ }
+
+ if ($zip->numFiles === 0) {
+ unlink($archiveFile);
+ throw new PluginInstallerException(t('There is no file in the plugin archive.'));
+ }
+
+ return $zip;
+ }
+
+ /**
+ * Remove archive file
+ *
+ * @access protected
+ * @param ZipArchive $zip
+ */
+ protected function cleanupArchive(ZipArchive $zip)
+ {
+ unlink($zip->filename);
+ $zip->close();
+ }
+
+ /**
+ * Remove recursively a directory
+ *
+ * @access protected
+ * @param string $directory
+ */
+ protected function removeAllDirectories($directory)
+ {
+ $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS);
+ $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
+
+ foreach ($files as $file) {
+ if ($file->isDir()) {
+ rmdir($file->getRealPath());
+ } else {
+ unlink($file->getRealPath());
+ }
+ }
+
+ rmdir($directory);
+ }
+}
diff --git a/app/Core/Plugin/Loader.php b/app/Core/Plugin/Loader.php
index 799024ad..f2f6add7 100644
--- a/app/Core/Plugin/Loader.php
+++ b/app/Core/Plugin/Loader.php
@@ -4,33 +4,35 @@ namespace Kanboard\Core\Plugin;
use Composer\Autoload\ClassLoader;
use DirectoryIterator;
-use PDOException;
use LogicException;
-use RuntimeException;
use Kanboard\Core\Tool;
/**
* Plugin Loader
*
- * @package plugin
- * @author Frederic Guillot
+ * @package Kanboard\Core\Plugin
+ * @author Frederic Guillot
*/
class Loader extends \Kanboard\Core\Base
{
/**
- * Schema version table for plugins
+ * Plugin instances
*
- * @var string
+ * @access protected
+ * @var array
*/
- const TABLE_SCHEMA = 'plugin_schema_versions';
+ protected $plugins = array();
/**
- * Plugin instances
+ * Get list of loaded plugins
*
* @access public
- * @var array
+ * @return Base[]
*/
- public $plugins = array();
+ public function getPlugins()
+ {
+ return $this->plugins;
+ }
/**
* Scan plugin folder and load plugins
@@ -46,115 +48,66 @@ class Loader extends \Kanboard\Core\Base
$dir = new DirectoryIterator(PLUGINS_DIR);
- foreach ($dir as $fileinfo) {
- if (! $fileinfo->isDot() && $fileinfo->isDir()) {
- $plugin = $fileinfo->getFilename();
- $this->loadSchema($plugin);
- $this->load($plugin);
+ foreach ($dir as $fileInfo) {
+ if ($fileInfo->isDir() && substr($fileInfo->getFilename(), 0, 1) !== '.') {
+ $pluginName = $fileInfo->getFilename();
+ $this->loadSchema($pluginName);
+ $this->initializePlugin($pluginName, $this->loadPlugin($pluginName));
}
}
}
}
/**
- * Load plugin
- *
- * @access public
- * @throws LogicException
- * @param string $plugin
- */
- public function load($plugin)
- {
- $class = '\Kanboard\Plugin\\'.$plugin.'\\Plugin';
-
- if (! class_exists($class)) {
- throw new LogicException('Unable to load this plugin class '.$class);
- }
-
- $instance = new $class($this->container);
-
- Tool::buildDIC($this->container, $instance->getClasses());
- Tool::buildDICHelpers($this->container, $instance->getHelpers());
-
- $instance->initialize();
- $this->plugins[] = $instance;
- }
-
- /**
* Load plugin schema
*
* @access public
- * @param string $plugin
+ * @param string $pluginName
*/
- public function loadSchema($plugin)
+ public function loadSchema($pluginName)
{
- $filename = PLUGINS_DIR.'/'.$plugin.'/Schema/'.ucfirst(DB_DRIVER).'.php';
-
- if (file_exists($filename)) {
- require_once($filename);
- $this->migrateSchema($plugin);
+ if (SchemaHandler::hasSchema($pluginName)) {
+ $schemaHandler = new SchemaHandler($this->container);
+ $schemaHandler->loadSchema($pluginName);
}
}
/**
- * Execute plugin schema migrations
+ * Load plugin
*
* @access public
- * @param string $plugin
+ * @throws LogicException
+ * @param string $pluginName
+ * @return Base
*/
- public function migrateSchema($plugin)
+ public function loadPlugin($pluginName)
{
- $last_version = constant('\Kanboard\Plugin\\'.$plugin.'\Schema\VERSION');
- $current_version = $this->getSchemaVersion($plugin);
-
- try {
- $this->db->startTransaction();
- $this->db->getDriver()->disableForeignKeys();
-
- for ($i = $current_version + 1; $i <= $last_version; $i++) {
- $function_name = '\Kanboard\Plugin\\'.$plugin.'\Schema\version_'.$i;
-
- if (function_exists($function_name)) {
- call_user_func($function_name, $this->db->getConnection());
- }
- }
+ $className = '\Kanboard\Plugin\\'.$pluginName.'\\Plugin';
- $this->db->getDriver()->enableForeignKeys();
- $this->db->closeTransaction();
- $this->setSchemaVersion($plugin, $i - 1);
- } catch (PDOException $e) {
- $this->db->cancelTransaction();
- $this->db->getDriver()->enableForeignKeys();
- throw new RuntimeException('Unable to migrate schema for the plugin: '.$plugin.' => '.$e->getMessage());
+ if (! class_exists($className)) {
+ throw new LogicException('Unable to load this plugin class '.$className);
}
- }
- /**
- * Get current plugin schema version
- *
- * @access public
- * @param string $plugin
- * @return integer
- */
- public function getSchemaVersion($plugin)
- {
- return (int) $this->db->table(self::TABLE_SCHEMA)->eq('plugin', strtolower($plugin))->findOneColumn('version');
+ return new $className($this->container);
}
/**
- * Save last plugin schema version
+ * Initialize plugin
*
* @access public
- * @param string $plugin
- * @param integer $version
- * @return boolean
+ * @param string $pluginName
+ * @param Base $plugin
*/
- public function setSchemaVersion($plugin, $version)
+ public function initializePlugin($pluginName, Base $plugin)
{
- $dictionary = array(
- strtolower($plugin) => $version
- );
+ if (method_exists($plugin, 'onStartup')) {
+ $this->dispatcher->addListener('app.bootstrap', array($plugin, 'onStartup'));
+ }
+
+ Tool::buildDIC($this->container, $plugin->getClasses());
+ Tool::buildDICHelpers($this->container, $plugin->getHelpers());
- return $this->db->getDriver()->upsert(self::TABLE_SCHEMA, 'plugin', 'version', $dictionary);
+ $plugin->initialize();
+ $this->plugins[$pluginName] = $plugin;
}
}
diff --git a/app/Core/Plugin/PluginInstallerException.php b/app/Core/Plugin/PluginInstallerException.php
new file mode 100644
index 00000000..7d356c9b
--- /dev/null
+++ b/app/Core/Plugin/PluginInstallerException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Kanboard\Core\Plugin;
+
+use Exception;
+
+/**
+ * Class PluginInstallerException
+ *
+ * @package Kanboard\Core\Plugin
+ * @author Frederic Guillot
+ */
+class PluginInstallerException extends Exception
+{
+}
diff --git a/app/Core/Plugin/SchemaHandler.php b/app/Core/Plugin/SchemaHandler.php
new file mode 100644
index 00000000..551141b8
--- /dev/null
+++ b/app/Core/Plugin/SchemaHandler.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Kanboard\Core\Plugin;
+
+use PDOException;
+use RuntimeException;
+
+/**
+ * Class SchemaHandler
+ *
+ * @package Kanboard\Core\Plugin
+ * @author Frederic Guillot
+ */
+class SchemaHandler extends \Kanboard\Core\Base
+{
+ /**
+ * Schema version table for plugins
+ *
+ * @var string
+ */
+ const TABLE_SCHEMA = 'plugin_schema_versions';
+
+ /**
+ * Get schema filename
+ *
+ * @static
+ * @access public
+ * @param string $pluginName
+ * @return string
+ */
+ public static function getSchemaFilename($pluginName)
+ {
+ return PLUGINS_DIR.'/'.$pluginName.'/Schema/'.ucfirst(DB_DRIVER).'.php';
+ }
+
+ /**
+ * Return true if the plugin has schema
+ *
+ * @static
+ * @access public
+ * @param string $pluginName
+ * @return boolean
+ */
+ public static function hasSchema($pluginName)
+ {
+ return file_exists(self::getSchemaFilename($pluginName));
+ }
+
+ /**
+ * Load plugin schema
+ *
+ * @access public
+ * @param string $pluginName
+ */
+ public function loadSchema($pluginName)
+ {
+ require_once self::getSchemaFilename($pluginName);
+ $this->migrateSchema($pluginName);
+ }
+
+ /**
+ * Execute plugin schema migrations
+ *
+ * @access public
+ * @param string $pluginName
+ */
+ public function migrateSchema($pluginName)
+ {
+ $lastVersion = constant('\Kanboard\Plugin\\'.$pluginName.'\Schema\VERSION');
+ $currentVersion = $this->getSchemaVersion($pluginName);
+
+ try {
+ $this->db->startTransaction();
+ $this->db->getDriver()->disableForeignKeys();
+
+ for ($i = $currentVersion + 1; $i <= $lastVersion; $i++) {
+ $functionName = '\Kanboard\Plugin\\'.$pluginName.'\Schema\version_'.$i;
+
+ if (function_exists($functionName)) {
+ call_user_func($functionName, $this->db->getConnection());
+ }
+ }
+
+ $this->db->getDriver()->enableForeignKeys();
+ $this->db->closeTransaction();
+ $this->setSchemaVersion($pluginName, $i - 1);
+ } catch (PDOException $e) {
+ $this->db->cancelTransaction();
+ $this->db->getDriver()->enableForeignKeys();
+ throw new RuntimeException('Unable to migrate schema for the plugin: '.$pluginName.' => '.$e->getMessage());
+ }
+ }
+
+ /**
+ * Get current plugin schema version
+ *
+ * @access public
+ * @param string $plugin
+ * @return integer
+ */
+ public function getSchemaVersion($plugin)
+ {
+ return (int) $this->db->table(self::TABLE_SCHEMA)->eq('plugin', strtolower($plugin))->findOneColumn('version');
+ }
+
+ /**
+ * Save last plugin schema version
+ *
+ * @access public
+ * @param string $plugin
+ * @param integer $version
+ * @return boolean
+ */
+ public function setSchemaVersion($plugin, $version)
+ {
+ $dictionary = array(
+ strtolower($plugin) => $version
+ );
+
+ return $this->db->getDriver()->upsert(self::TABLE_SCHEMA, 'plugin', 'version', $dictionary);
+ }
+}
diff --git a/app/Core/Queue/JobHandler.php b/app/Core/Queue/JobHandler.php
new file mode 100644
index 00000000..a2c4a2c7
--- /dev/null
+++ b/app/Core/Queue/JobHandler.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Kanboard\Core\Queue;
+
+use Kanboard\Core\Base;
+use Kanboard\Job\BaseJob;
+use SimpleQueue\Job;
+
+/**
+ * Class JobHandler
+ *
+ * @package Kanboard\Core\Queue
+ * @author Frederic Guillot
+ */
+class JobHandler extends Base
+{
+ /**
+ * Serialize a job
+ *
+ * @access public
+ * @param BaseJob $job
+ * @return Job
+ */
+ public function serializeJob(BaseJob $job)
+ {
+ return new Job(array(
+ 'class' => get_class($job),
+ 'params' => $job->getJobParams(),
+ ));
+ }
+
+ /**
+ * Execute a job
+ *
+ * @access public
+ * @param Job $job
+ */
+ public function executeJob(Job $job)
+ {
+ $payload = $job->getBody();
+ $className = $payload['class'];
+
+ if (DEBUG) {
+ $this->logger->debug(__METHOD__.' Received job => '.$className);
+ }
+
+ $worker = new $className($this->container);
+ call_user_func_array(array($worker, 'execute'), $payload['params']);
+ }
+}
diff --git a/app/Core/Queue/QueueManager.php b/app/Core/Queue/QueueManager.php
new file mode 100644
index 00000000..f34cb220
--- /dev/null
+++ b/app/Core/Queue/QueueManager.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Kanboard\Core\Queue;
+
+use Kanboard\Core\Base;
+use Kanboard\Job\BaseJob;
+use LogicException;
+use SimpleQueue\Queue;
+
+/**
+ * Class QueueManager
+ *
+ * @package Kanboard\Core\Queue
+ * @author Frederic Guillot
+ */
+class QueueManager extends Base
+{
+ /**
+ * @var Queue
+ */
+ protected $queue = null;
+
+ /**
+ * Set queue driver
+ *
+ * @access public
+ * @param Queue $queue
+ * @return $this
+ */
+ public function setQueue(Queue $queue)
+ {
+ $this->queue = $queue;
+ return $this;
+ }
+
+ /**
+ * Send a new job to the queue
+ *
+ * @access public
+ * @param BaseJob $job
+ * @return $this
+ */
+ public function push(BaseJob $job)
+ {
+ if ($this->queue !== null) {
+ $this->queue->push(JobHandler::getInstance($this->container)->serializeJob($job));
+ } else {
+ call_user_func_array(array($job, 'execute'), $job->getJobParams());
+ }
+
+ return $this;
+ }
+
+ /**
+ * Wait for new jobs
+ *
+ * @access public
+ * @throws LogicException
+ */
+ public function listen()
+ {
+ if ($this->queue === null) {
+ throw new LogicException('No Queue Driver defined!');
+ }
+
+ while ($job = $this->queue->pull()) {
+ JobHandler::getInstance($this->container)->executeJob($job);
+ $this->queue->completed($job);
+ }
+ }
+}
diff --git a/app/Core/Session/SessionStorage.php b/app/Core/Session/SessionStorage.php
index 6e2f9660..9e93602c 100644
--- a/app/Core/Session/SessionStorage.php
+++ b/app/Core/Session/SessionStorage.php
@@ -22,6 +22,7 @@ namespace Kanboard\Core\Session;
* @property bool $twoFactorBeforeCodeCalled
* @property string $twoFactorSecret
* @property string $oauthState
+ * @property int $smsTwoFactorSecret
*/
class SessionStorage
{
diff --git a/app/Core/Tool.php b/app/Core/Tool.php
index 3423998d..bfa6c955 100644
--- a/app/Core/Tool.php
+++ b/app/Core/Tool.php
@@ -13,26 +13,6 @@ use Pimple\Container;
class Tool
{
/**
- * Get the mailbox hash from an email address
- *
- * @static
- * @access public
- * @param string $email
- * @return string
- */
- public static function getMailboxHash($email)
- {
- if (! strpos($email, '@') || ! strpos($email, '+')) {
- return '';
- }
-
- list($local_part, ) = explode('@', $email);
- list(, $identifier) = explode('+', $local_part);
-
- return $identifier;
- }
-
- /**
* Build dependency injection container from an array
*
* @static
diff --git a/app/Core/User/GroupSync.php b/app/Core/User/GroupSync.php
index 573acd47..d0bb647b 100644
--- a/app/Core/User/GroupSync.php
+++ b/app/Core/User/GroupSync.php
@@ -16,16 +16,52 @@ class GroupSync extends Base
* Synchronize group membership
*
* @access public
- * @param integer $userId
- * @param array $groupIds
+ * @param integer $userId
+ * @param array $externalGroupIds
*/
- public function synchronize($userId, array $groupIds)
+ public function synchronize($userId, array $externalGroupIds)
{
- foreach ($groupIds as $groupId) {
- $group = $this->group->getByExternalId($groupId);
+ $userGroups = $this->groupMemberModel->getGroups($userId);
+ $this->addGroups($userId, $userGroups, $externalGroupIds);
+ $this->removeGroups($userId, $userGroups, $externalGroupIds);
+ }
+
+ /**
+ * Add missing groups to the user
+ *
+ * @access protected
+ * @param integer $userId
+ * @param array $userGroups
+ * @param array $externalGroupIds
+ */
+ protected function addGroups($userId, array $userGroups, array $externalGroupIds)
+ {
+ $userGroupIds = array_column($userGroups, 'external_id', 'external_id');
- if (! empty($group) && ! $this->groupMember->isMember($group['id'], $userId)) {
- $this->groupMember->addUser($group['id'], $userId);
+ foreach ($externalGroupIds as $externalGroupId) {
+ if (! isset($userGroupIds[$externalGroupId])) {
+ $group = $this->groupModel->getByExternalId($externalGroupId);
+
+ if (! empty($group)) {
+ $this->groupMemberModel->addUser($group['id'], $userId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Remove groups from the user
+ *
+ * @access protected
+ * @param integer $userId
+ * @param array $userGroups
+ * @param array $externalGroupIds
+ */
+ protected function removeGroups($userId, array $userGroups, array $externalGroupIds)
+ {
+ foreach ($userGroups as $userGroup) {
+ if (! empty($userGroup['external_id']) && ! in_array($userGroup['external_id'], $externalGroupIds)) {
+ $this->groupMemberModel->removeUser($userGroup['id'], $userId);
}
}
}
diff --git a/app/Core/User/UserProfile.php b/app/Core/User/UserProfile.php
index ef325801..8b9ebb71 100644
--- a/app/Core/User/UserProfile.php
+++ b/app/Core/User/UserProfile.php
@@ -3,6 +3,7 @@
namespace Kanboard\Core\User;
use Kanboard\Core\Base;
+use Kanboard\Event\UserProfileSyncEvent;
/**
* User Profile
@@ -12,6 +13,8 @@ use Kanboard\Core\Base;
*/
class UserProfile extends Base
{
+ const EVENT_USER_PROFILE_AFTER_SYNC = 'user_profile.after.sync';
+
/**
* Assign provider data to the local user
*
@@ -22,12 +25,12 @@ class UserProfile extends Base
*/
public function assign($userId, UserProviderInterface $user)
{
- $profile = $this->user->getById($userId);
+ $profile = $this->userModel->getById($userId);
$values = UserProperty::filterProperties($profile, UserProperty::getProperties($user));
$values['id'] = $userId;
- if ($this->user->update($values)) {
+ if ($this->userModel->update($values)) {
$profile = array_merge($profile, $values);
$this->userSession->initialize($profile);
return true;
@@ -46,7 +49,7 @@ class UserProfile extends Base
public function initialize(UserProviderInterface $user)
{
if ($user->getInternalId()) {
- $profile = $this->user->getById($user->getInternalId());
+ $profile = $this->userModel->getById($user->getInternalId());
} elseif ($user->getExternalIdColumn() && $user->getExternalId()) {
$profile = $this->userSync->synchronize($user);
$this->groupSync->synchronize($profile['id'], $user->getExternalGroupIds());
@@ -54,6 +57,7 @@ class UserProfile extends Base
if (! empty($profile) && $profile['is_active'] == 1) {
$this->userSession->initialize($profile);
+ $this->dispatcher->dispatch(self::EVENT_USER_PROFILE_AFTER_SYNC, new UserProfileSyncEvent($profile, $user));
return true;
}
diff --git a/app/Core/User/UserProperty.php b/app/Core/User/UserProperty.php
index f8b08a3d..348bd7f3 100644
--- a/app/Core/User/UserProperty.php
+++ b/app/Core/User/UserProperty.php
@@ -44,10 +44,14 @@ class UserProperty
*/
public static function filterProperties(array $profile, array $properties)
{
+ $excludedProperties = array('username');
$values = array();
foreach ($properties as $property => $value) {
- if (array_key_exists($property, $profile) && ! self::isNotEmptyValue($profile[$property])) {
+ if (self::isNotEmptyValue($value) &&
+ ! in_array($property, $excludedProperties) &&
+ array_key_exists($property, $profile) &&
+ $value !== $profile[$property]) {
$values[$property] = $value;
}
}
diff --git a/app/Core/User/UserSession.php b/app/Core/User/UserSession.php
index 0034c47a..9c63f07a 100644
--- a/app/Core/User/UserSession.php
+++ b/app/Core/User/UserSession.php
@@ -22,7 +22,7 @@ class UserSession extends Base
public function refresh($user_id)
{
if ($this->getId() == $user_id) {
- $this->initialize($this->user->getById($user_id));
+ $this->initialize($this->userModel->getById($user_id));
}
}
diff --git a/app/Core/User/UserSync.php b/app/Core/User/UserSync.php
index d450a0bd..c2f85498 100644
--- a/app/Core/User/UserSync.php
+++ b/app/Core/User/UserSync.php
@@ -21,7 +21,7 @@ class UserSync extends Base
*/
public function synchronize(UserProviderInterface $user)
{
- $profile = $this->user->getByExternalId($user->getExternalIdColumn(), $user->getExternalId());
+ $profile = $this->userModel->getByExternalId($user->getExternalIdColumn(), $user->getExternalId());
$properties = UserProperty::getProperties($user);
if (! empty($profile)) {
@@ -47,7 +47,7 @@ class UserSync extends Base
if (! empty($values)) {
$values['id'] = $profile['id'];
- $result = $this->user->update($values);
+ $result = $this->userModel->update($values);
return $result ? array_merge($profile, $properties) : $profile;
}
@@ -64,13 +64,13 @@ class UserSync extends Base
*/
private function createUser(UserProviderInterface $user, array $properties)
{
- $id = $this->user->create($properties);
+ $userId = $this->userModel->create($properties);
- if ($id === false) {
+ if ($userId === false) {
$this->logger->error('Unable to create user profile: '.$user->getExternalId());
return array();
}
- return $this->user->getById($id);
+ return $this->userModel->getById($userId);
}
}