diff options
Diffstat (limited to 'app/Core')
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); } } |
