diff options
Diffstat (limited to 'app/Core')
40 files changed, 888 insertions, 310 deletions
diff --git a/app/Core/Action/ActionManager.php b/app/Core/Action/ActionManager.php index 1dfd820c..aec9ef02 100644 --- a/app/Core/Action/ActionManager.php +++ b/app/Core/Action/ActionManager.php @@ -139,4 +139,20 @@ class ActionManager extends Base return $this; } + + /** + * Remove all listeners for automated actions + * + * @access public + */ + public function removeEvents() + { + foreach ($this->dispatcher->getListeners() as $eventName => $listeners) { + foreach ($listeners as $listener) { + if (is_array($listener) && $listener[0] instanceof ActionBase) { + $this->dispatcher->removeListener($eventName, $listener); + } + } + } + } } diff --git a/app/Core/Base.php b/app/Core/Base.php index 8103ec14..17ed5b33 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -10,151 +10,196 @@ 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\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\AccessMap $apiAccessMap - * @property \Kanboard\Core\Security\AccessMap $apiProjectAccessMap - * @property \Kanboard\Core\Security\Authorization $applicationAuthorization - * @property \Kanboard\Core\Security\Authorization $projectAuthorization - * @property \Kanboard\Core\Security\Authorization $apiAuthorization - * @property \Kanboard\Core\Security\Authorization $apiProjectAuthorization - * @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\ProjectTaskDuplicationModel $projectTaskDuplicationModel - * @property \Kanboard\Model\ProjectTaskPriorityModel $projectTaskPriorityModel - * @property \Kanboard\Model\RememberMeSessionModel $rememberMeSessionModel - * @property \Kanboard\Model\SubtaskModel $subtaskModel - * @property \Kanboard\Model\SubtaskTimeTrackingModel $subtaskTimeTrackingModel - * @property \Kanboard\Model\SwimlaneModel $swimlaneModel - * @property \Kanboard\Model\TagDuplicationModel $tagDuplicationModel - * @property \Kanboard\Model\TagModel $tagModel - * @property \Kanboard\Model\TaskModel $taskModel - * @property \Kanboard\Model\TaskAnalyticModel $taskAnalyticModel - * @property \Kanboard\Model\TaskCreationModel $taskCreationModel - * @property \Kanboard\Model\TaskDuplicationModel $taskDuplicationModel - * @property \Kanboard\Model\TaskProjectDuplicationModel $taskProjectDuplicationModel - * @property \Kanboard\Model\TaskProjectMoveModel $taskProjectMoveModel - * @property \Kanboard\Model\TaskRecurrenceModel $taskRecurrenceModel - * @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\TaskTagModel $taskTagModel - * @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\ExternalLinkValidator $externalLinkValidator - * @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\TagValidator $tagValidator - * @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator - * @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 + * @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\ExternalTask\ExternalTaskManager $externalTaskManager + * @property \Kanboard\Core\Cache\MemoryCache $memoryCache + * @property \Kanboard\Core\Cache\BaseCache $cacheDriver + * @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\AccessMap $apiAccessMap + * @property \Kanboard\Core\Security\AccessMap $apiProjectAccessMap + * @property \Kanboard\Core\Security\Authorization $applicationAuthorization + * @property \Kanboard\Core\Security\Authorization $projectAuthorization + * @property \Kanboard\Core\Security\Authorization $apiAuthorization + * @property \Kanboard\Core\Security\Authorization $apiProjectAuthorization + * @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\Decorator\MetadataCacheDecorator $userMetadataCacheDecorator + * @property \Kanboard\Decorator\UserCacheDecorator $userCacheDecorator + * @property \Kanboard\Decorator\ColumnRestrictionCacheDecorator $columnRestrictionCacheDecorator + * @property \Kanboard\Decorator\ColumnMoveRestrictionCacheDecorator $columnMoveRestrictionCacheDecorator + * @property \Kanboard\Decorator\ProjectRoleRestrictionCacheDecorator $projectRoleRestrictionCacheDecorator + * @property \Kanboard\Formatter\BoardColumnFormatter $boardColumnFormatter + * @property \Kanboard\Formatter\BoardFormatter $boardFormatter + * @property \Kanboard\Formatter\BoardSwimlaneFormatter $boardSwimlaneFormatter + * @property \Kanboard\Formatter\BoardTaskFormatter $boardTaskFormatter + * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter + * @property \Kanboard\Formatter\ProjectActivityEventFormatter $projectActivityEventFormatter + * @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter + * @property \Kanboard\Formatter\SubtaskTimeTrackingCalendarFormatter $subtaskTimeTrackingCalendarFormatter + * @property \Kanboard\Formatter\TaskAutoCompleteFormatter $taskAutoCompleteFormatter + * @property \Kanboard\Formatter\TaskCalendarFormatter $taskCalendarFormatter + * @property \Kanboard\Formatter\TaskGanttFormatter $taskGanttFormatter + * @property \Kanboard\Formatter\TaskICalFormatter $taskICalFormatter + * @property \Kanboard\Formatter\TaskSuggestMenuFormatter $taskSuggestMenuFormatter + * @property \Kanboard\Formatter\UserAutoCompleteFormatter $userAutoCompleteFormatter + * @property \Kanboard\Formatter\UserMentionFormatter $userMentionFormatter + * @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\ColumnRestrictionModel $columnRestrictionModel + * @property \Kanboard\Model\ColumnMoveRestrictionModel $columnMoveRestrictionModel + * @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\InviteModel $inviteModel + * @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\ProjectRoleModel $projectRoleModel + * @property \Kanboard\Model\ProjectRoleRestrictionModel $projectRoleRestrictionModel + * @property \Kanboard\Model\ProjectTaskDuplicationModel $projectTaskDuplicationModel + * @property \Kanboard\Model\ProjectTaskPriorityModel $projectTaskPriorityModel + * @property \Kanboard\Model\RememberMeSessionModel $rememberMeSessionModel + * @property \Kanboard\Model\SubtaskModel $subtaskModel + * @property \Kanboard\Model\SubtaskPositionModel $subtaskPositionModel + * @property \Kanboard\Model\SubtaskStatusModel $subtaskStatusModel + * @property \Kanboard\Model\SubtaskTaskConversionModel $subtaskTaskConversionModel + * @property \Kanboard\Model\SubtaskTimeTrackingModel $subtaskTimeTrackingModel + * @property \Kanboard\Model\SwimlaneModel $swimlaneModel + * @property \Kanboard\Model\TagDuplicationModel $tagDuplicationModel + * @property \Kanboard\Model\TagModel $tagModel + * @property \Kanboard\Model\TaskModel $taskModel + * @property \Kanboard\Model\TaskAnalyticModel $taskAnalyticModel + * @property \Kanboard\Model\TaskCreationModel $taskCreationModel + * @property \Kanboard\Model\TaskDuplicationModel $taskDuplicationModel + * @property \Kanboard\Model\TaskProjectDuplicationModel $taskProjectDuplicationModel + * @property \Kanboard\Model\TaskProjectMoveModel $taskProjectMoveModel + * @property \Kanboard\Model\TaskRecurrenceModel $taskRecurrenceModel + * @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\TaskTagModel $taskTagModel + * @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\UserNotificationModel $userNotificationModel + * @property \Kanboard\Model\UserNotificationTypeModel $userNotificationTypeModel + * @property \Kanboard\Model\UserNotificationFilterModel $userNotificationFilterModel + * @property \Kanboard\Model\UserUnreadNotificationModel $userUnreadNotificationModel + * @property \Kanboard\Model\UserMetadataModel $userMetadataModel + * @property \Kanboard\Pagination\TaskPagination $taskPagination + * @property \Kanboard\Pagination\SubtaskPagination $subtaskPagination + * @property \Kanboard\Pagination\ProjectPagination $projectPagination + * @property \Kanboard\Pagination\UserPagination $userPagination + * @property \Kanboard\Validator\ActionValidator $actionValidator + * @property \Kanboard\Validator\AuthValidator $authValidator + * @property \Kanboard\Validator\ColumnValidator $columnValidator + * @property \Kanboard\Validator\CategoryValidator $categoryValidator + * @property \Kanboard\Validator\ColumnRestrictionValidator $columnRestrictionValidator + * @property \Kanboard\Validator\ColumnMoveRestrictionValidator $columnMoveRestrictionValidator + * @property \Kanboard\Validator\CommentValidator $commentValidator + * @property \Kanboard\Validator\CurrencyValidator $currencyValidator + * @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator + * @property \Kanboard\Validator\ExternalLinkValidator $externalLinkValidator + * @property \Kanboard\Validator\GroupValidator $groupValidator + * @property \Kanboard\Validator\LinkValidator $linkValidator + * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator + * @property \Kanboard\Validator\ProjectValidator $projectValidator + * @property \Kanboard\Validator\ProjectRoleValidator $projectRoleValidator + * @property \Kanboard\Validator\SubtaskValidator $subtaskValidator + * @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator + * @property \Kanboard\Validator\TagValidator $tagValidator + * @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator + * @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 \Kanboard\Job\CommentEventJob $commentEventJob + * @property \Kanboard\Job\SubtaskEventJob $subtaskEventJob + * @property \Kanboard\Job\TaskEventJob $taskEventJob + * @property \Kanboard\Job\TaskFileEventJob $taskFileEventJob + * @property \Kanboard\Job\TaskLinkEventJob $taskLinkEventJob + * @property \Kanboard\Job\ProjectFileEventJob $projectFileEventJob + * @property \Kanboard\Job\NotificationJob $notificationJob + * @property \Kanboard\Job\ProjectMetricJob $projectMetricJob + * @property \Kanboard\Job\UserMentionJob $userMentionJob + * @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 { @@ -178,10 +223,10 @@ abstract class Base } /** - * Load automatically models + * Load automatically dependencies * * @access public - * @param string $name Model name + * @param string $name Class name * @return mixed */ public function __get($name) @@ -199,7 +244,6 @@ abstract class Base */ public static function getInstance(Container $container) { - $self = new static($container); - return $self; + return new static($container); } } diff --git a/app/Core/Cache/Base.php b/app/Core/Cache/BaseCache.php index d62b8507..b51c4c0c 100644 --- a/app/Core/Cache/Base.php +++ b/app/Core/Cache/BaseCache.php @@ -3,12 +3,12 @@ namespace Kanboard\Core\Cache; /** - * Base class for cache drivers + * Base Class for Cache Drivers * - * @package cache + * @package Kanboard\Core\Cache * @author Frederic Guillot */ -abstract class Base +abstract class BaseCache implements CacheInterface { /** * Proxy cache diff --git a/app/Core/Cache/CacheInterface.php b/app/Core/Cache/CacheInterface.php index d9e9747a..033732cf 100644 --- a/app/Core/Cache/CacheInterface.php +++ b/app/Core/Cache/CacheInterface.php @@ -3,24 +3,24 @@ namespace Kanboard\Core\Cache; /** - * Cache Interface + * Interface CacheInterface * - * @package cache - * @author Frederic Guillot + * @package Kanboard\Core\Cache + * @author Frederic Guillot */ interface CacheInterface { /** - * Save a new value in the cache + * Store an item in the cache * * @access public * @param string $key - * @param string $value + * @param mixed $value */ public function set($key, $value); /** - * Fetch value from cache + * Retrieve an item from the cache by key * * @access public * @param string $key @@ -29,14 +29,14 @@ interface CacheInterface public function get($key); /** - * Clear all cache + * Remove all items from the cache * * @access public */ public function flush(); /** - * Remove cached value + * Remove an item from the cache * * @access public * @param string $key diff --git a/app/Core/Cache/FileCache.php b/app/Core/Cache/FileCache.php new file mode 100644 index 00000000..1e0a60ef --- /dev/null +++ b/app/Core/Cache/FileCache.php @@ -0,0 +1,98 @@ +<?php + +namespace Kanboard\Core\Cache; + +use Kanboard\Core\Tool; +use LogicException; + +/** + * Class FileCache + * + * @package Kanboard\Core\Cache + */ +class FileCache extends BaseCache +{ + /** + * Store an item in the cache + * + * @access public + * @param string $key + * @param mixed $value + */ + public function set($key, $value) + { + $this->createCacheFolder(); + file_put_contents($this->getFilenameFromKey($key), serialize($value)); + } + + /** + * Retrieve an item from the cache by key + * + * @access public + * @param string $key + * @return mixed Null when not found, cached value otherwise + */ + public function get($key) + { + $filename = $this->getFilenameFromKey($key); + + if (file_exists($filename)) { + return unserialize(file_get_contents($filename)); + } + + return null; + } + + /** + * Remove all items from the cache + * + * @access public + */ + public function flush() + { + $this->createCacheFolder(); + Tool::removeAllFiles(CACHE_DIR, false); + } + + /** + * Remove an item from the cache + * + * @access public + * @param string $key + */ + public function remove($key) + { + $filename = $this->getFilenameFromKey($key); + + if (file_exists($filename)) { + unlink($filename); + } + } + + /** + * Get absolute filename from the key + * + * @access protected + * @param string $key + * @return string + */ + protected function getFilenameFromKey($key) + { + return CACHE_DIR.DIRECTORY_SEPARATOR.$key; + } + + /** + * Create cache folder if missing + * + * @access protected + * @throws LogicException + */ + protected function createCacheFolder() + { + if (! is_dir(CACHE_DIR)) { + if (! mkdir(CACHE_DIR, 0755)) { + throw new LogicException('Unable to create cache directory: '.CACHE_DIR); + } + } + } +} diff --git a/app/Core/Cache/MemoryCache.php b/app/Core/Cache/MemoryCache.php index 39e3947b..4fb94728 100644 --- a/app/Core/Cache/MemoryCache.php +++ b/app/Core/Cache/MemoryCache.php @@ -3,12 +3,12 @@ namespace Kanboard\Core\Cache; /** - * Memory Cache + * Memory Cache Driver * - * @package cache + * @package Kanboard\Core\Cache * @author Frederic Guillot */ -class MemoryCache extends Base implements CacheInterface +class MemoryCache extends BaseCache { /** * Container @@ -19,7 +19,7 @@ class MemoryCache extends Base implements CacheInterface private $storage = array(); /** - * Save a new value in the cache + * Store an item in the cache * * @access public * @param string $key @@ -31,7 +31,7 @@ class MemoryCache extends Base implements CacheInterface } /** - * Fetch value from cache + * Retrieve an item from the cache by key * * @access public * @param string $key diff --git a/app/Core/Controller/Runner.php b/app/Core/Controller/Runner.php index 8353cf69..48346390 100644 --- a/app/Core/Controller/Runner.php +++ b/app/Core/Controller/Runner.php @@ -35,7 +35,7 @@ class Runner extends Base $controllerObject->notFound($e->hasLayout()); } catch (AccessForbiddenException $e) { $controllerObject = new AppController($this->container); - $controllerObject->accessForbidden($e->hasLayout()); + $controllerObject->accessForbidden($e->hasLayout(), $e->getMessage()); } } diff --git a/app/Core/DateParser.php b/app/Core/DateParser.php index a7b10a7a..9d012d12 100644 --- a/app/Core/DateParser.php +++ b/app/Core/DateParser.php @@ -13,7 +13,6 @@ use DateTime; class DateParser extends Base { const DATE_FORMAT = 'm/d/Y'; - const DATE_TIME_FORMAT = 'm/d/Y H:i'; const TIME_FORMAT = 'H:i'; /** @@ -35,7 +34,7 @@ class DateParser extends Base */ public function getUserDateTimeFormat() { - return $this->configModel->get('application_datetime_format', DateParser::DATE_TIME_FORMAT); + return $this->getUserDateFormat().' '.$this->getUserTimeFormat(); } /** @@ -239,23 +238,11 @@ class DateParser extends Base */ public function getHours(DateTime $d1, DateTime $d2) { - $seconds = $this->getRoundedSeconds(abs($d1->getTimestamp() - $d2->getTimestamp())); + $seconds = abs($d1->getTimestamp() - $d2->getTimestamp()); return round($seconds / 3600, 2); } /** - * Round the timestamp to the nearest quarter - * - * @access public - * @param integer $seconds Timestamp - * @return integer - */ - public function getRoundedSeconds($seconds) - { - return (int) round($seconds / (15 * 60)) * (15 * 60); - } - - /** * Get ISO-8601 date from user input * * @access public @@ -304,7 +291,9 @@ class DateParser extends Base { foreach ($fields as $field) { if (! empty($values[$field])) { - $values[$field] = date($format, $values[$field]); + if (ctype_digit($values[$field])) { + $values[$field] = date($format, $values[$field]); + } } else { $values[$field] = ''; } diff --git a/app/Core/Event/EventManager.php b/app/Core/Event/EventManager.php index 9ae43170..48a9d299 100644 --- a/app/Core/Event/EventManager.php +++ b/app/Core/Event/EventManager.php @@ -53,6 +53,7 @@ class EventManager 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'), + TaskModel::EVENT_MOVE_SWIMLANE => t('Move a task to another swimlane'), ); $events = array_merge($events, $this->events); diff --git a/app/Core/ExternalTask/AccessForbiddenException.php b/app/Core/ExternalTask/AccessForbiddenException.php new file mode 100644 index 00000000..2b5ebd33 --- /dev/null +++ b/app/Core/ExternalTask/AccessForbiddenException.php @@ -0,0 +1,13 @@ +<?php + +namespace Kanboard\Core\ExternalTask; + +/** + * Class AccessForbiddenException + * + * @package Kanboard\Core\ExternalTask + * @author Frederic Guillot + */ +class AccessForbiddenException extends ExternalTaskException +{ +} diff --git a/app/Core/ExternalTask/ExternalTaskException.php b/app/Core/ExternalTask/ExternalTaskException.php new file mode 100644 index 00000000..07e5665d --- /dev/null +++ b/app/Core/ExternalTask/ExternalTaskException.php @@ -0,0 +1,15 @@ +<?php + +namespace Kanboard\Core\ExternalTask; + +use Exception; + +/** + * Class NotFoundException + * + * @package Kanboard\Core\ExternalTask + * @author Frederic Guillot + */ +class ExternalTaskException extends Exception +{ +} diff --git a/app/Core/ExternalTask/ExternalTaskInterface.php b/app/Core/ExternalTask/ExternalTaskInterface.php new file mode 100644 index 00000000..084af509 --- /dev/null +++ b/app/Core/ExternalTask/ExternalTaskInterface.php @@ -0,0 +1,26 @@ +<?php + +namespace Kanboard\Core\ExternalTask; + +/** + * Interface ExternalTaskInterface + * + * @package Kanboard\Core\ExternalTask + * @author Frederic Guillot + */ +interface ExternalTaskInterface +{ + /** + * Return Uniform Resource Identifier for the task + * + * @return string + */ + public function getUri(); + + /** + * Return a dict to populate the task form + * + * @return array + */ + public function getFormValues(); +} diff --git a/app/Core/ExternalTask/ExternalTaskManager.php b/app/Core/ExternalTask/ExternalTaskManager.php new file mode 100644 index 00000000..102ec459 --- /dev/null +++ b/app/Core/ExternalTask/ExternalTaskManager.php @@ -0,0 +1,58 @@ +<?php + +namespace Kanboard\Core\ExternalTask; + +/** + * Class ExternalTaskManager + * + * @package Kanboard\Core\ExternalTask + * @author Frederic Guillot + */ +class ExternalTaskManager +{ + protected $providers = array(); + + /** + * Register a new task provider + * + * @param ExternalTaskProviderInterface $externalTaskProvider + * @return $this + */ + public function register(ExternalTaskProviderInterface $externalTaskProvider) + { + $this->providers[$externalTaskProvider->getName()] = $externalTaskProvider; + return $this; + } + + /** + * Get task provider + * + * @param string $name + * @return ExternalTaskProviderInterface|null + * @throws ProviderNotFoundException + */ + public function getProvider($name) + { + if (isset($this->providers[$name])) { + return $this->providers[$name]; + } + + throw new ProviderNotFoundException('Unable to load this provider: '.$name); + } + + /** + * Get list of task providers + * + * @return array + */ + public function getProvidersList() + { + $providers = array_keys($this->providers); + + if (count($providers)) { + return array_combine($providers, $providers); + } + + return array(); + } +} diff --git a/app/Core/ExternalTask/ExternalTaskProviderInterface.php b/app/Core/ExternalTask/ExternalTaskProviderInterface.php new file mode 100644 index 00000000..f67f7552 --- /dev/null +++ b/app/Core/ExternalTask/ExternalTaskProviderInterface.php @@ -0,0 +1,77 @@ +<?php + +namespace Kanboard\Core\ExternalTask; + +/** + * Interface ExternalTaskProviderInterface + * + * @package Kanboard\Core\ExternalTask + * @author Frederic Guillot + */ +interface ExternalTaskProviderInterface +{ + /** + * Get provider name (visible in the user interface) + * + * @access public + * @return string + */ + public function getName(); + + /** + * Retrieve task from external system or cache + * + * @access public + * @throws \Kanboard\Core\ExternalTask\ExternalTaskException + * @param string $uri + * @return ExternalTaskInterface + */ + public function fetch($uri); + + /** + * Save external task to another system + * + * @throws \Kanboard\Core\ExternalTask\ExternalTaskException + * @param string $uri + * @param array $formValues + * @param array $formErrors + * @return bool + */ + public function save($uri, array $formValues, array &$formErrors); + + /** + * Get task import template name + * + * @return string + */ + public function getImportFormTemplate(); + + /** + * Get creation form template + * + * @return string + */ + public function getCreationFormTemplate(); + + /** + * Get modification form template + * + * @return string + */ + public function getModificationFormTemplate(); + + /** + * Get task view template name + * + * @return string + */ + public function getViewTemplate(); + + /** + * Build external task URI based on import form values + * + * @param array $formValues + * @return string + */ + public function buildTaskUri(array $formValues); +} diff --git a/app/Core/ExternalTask/NotFoundException.php b/app/Core/ExternalTask/NotFoundException.php new file mode 100644 index 00000000..34eff8ea --- /dev/null +++ b/app/Core/ExternalTask/NotFoundException.php @@ -0,0 +1,13 @@ +<?php + +namespace Kanboard\Core\ExternalTask; + +/** + * Class NotFoundException + * + * @package Kanboard\Core\ExternalTask + * @author Frederic Guillot + */ +class NotFoundException extends ExternalTaskException +{ +} diff --git a/app/Core/ExternalTask/ProviderNotFoundException.php b/app/Core/ExternalTask/ProviderNotFoundException.php new file mode 100644 index 00000000..6ad1fae1 --- /dev/null +++ b/app/Core/ExternalTask/ProviderNotFoundException.php @@ -0,0 +1,13 @@ +<?php + +namespace Kanboard\Core\ExternalTask; + +/** + * Class ProviderNotFoundException + * + * @package Kanboard\Core\ExternalTask + * @author Frederic Guillot + */ +class ProviderNotFoundException extends ExternalTaskException +{ +} diff --git a/app/Core/Filter/FormatterInterface.php b/app/Core/Filter/FormatterInterface.php index b7c04c51..0ff84976 100644 --- a/app/Core/Filter/FormatterInterface.php +++ b/app/Core/Filter/FormatterInterface.php @@ -17,7 +17,7 @@ interface FormatterInterface * * @access public * @param Table $query - * @return FormatterInterface + * @return $this */ public function withQuery(Table $query); diff --git a/app/Core/Filter/LexerBuilder.php b/app/Core/Filter/LexerBuilder.php index 7a9a714f..e3ab725b 100644 --- a/app/Core/Filter/LexerBuilder.php +++ b/app/Core/Filter/LexerBuilder.php @@ -51,7 +51,7 @@ class LexerBuilder */ public function __construct() { - $this->lexer = new Lexer; + $this->lexer = new Lexer(); $this->queryBuilder = new QueryBuilder(); } @@ -69,7 +69,7 @@ class LexerBuilder foreach ($attributes as $attribute) { $this->filters[$attribute] = $filter; - $this->lexer->addToken(sprintf("/^(%s:)/", $attribute), $attribute); + $this->lexer->addToken(sprintf("/^(%s:)/i", $attribute), $attribute); if ($default) { $this->lexer->setDefaultToken($attribute); diff --git a/app/Core/Helper.php b/app/Core/Helper.php index 43151be8..ab7c3b7b 100644 --- a/app/Core/Helper.php +++ b/app/Core/Helper.php @@ -12,12 +12,15 @@ use Pimple\Container; * * @property \Kanboard\Helper\AppHelper $app * @property \Kanboard\Helper\AssetHelper $asset + * @property \Kanboard\Helper\AvatarHelper $avatar + * @property \Kanboard\Helper\BoardHelper $board * @property \Kanboard\Helper\CalendarHelper $calendar * @property \Kanboard\Helper\DateHelper $dt * @property \Kanboard\Helper\FileHelper $file * @property \Kanboard\Helper\FormHelper $form * @property \Kanboard\Helper\HookHelper $hook * @property \Kanboard\Helper\ICalHelper $ical + * @property \Kanboard\Helper\ModalHelper $modal * @property \Kanboard\Helper\ModelHelper $model * @property \Kanboard\Helper\SubtaskHelper $subtask * @property \Kanboard\Helper\TaskHelper $task @@ -25,6 +28,7 @@ use Pimple\Container; * @property \Kanboard\Helper\UrlHelper $url * @property \Kanboard\Helper\UserHelper $user * @property \Kanboard\Helper\LayoutHelper $layout + * @property \Kanboard\Helper\ProjectRoleHelper $projectRole * @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader * @property \Kanboard\Helper\ProjectActivityHelper $projectActivity * @property \Kanboard\Helper\MailHelper $mail diff --git a/app/Core/Http/Request.php b/app/Core/Http/Request.php index e0df2d3c..44bfdbe6 100644 --- a/app/Core/Http/Request.php +++ b/app/Core/Http/Request.php @@ -105,7 +105,7 @@ class Request extends Base { if (! empty($this->post) && ! empty($this->post['csrf_token']) && $this->token->validateCSRFToken($this->post['csrf_token'])) { unset($this->post['csrf_token']); - return $this->post; + return $this->filterValues($this->post); } return array(); @@ -301,6 +301,7 @@ class Request extends Base public function getIpAddress() { $keys = array( + 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', @@ -343,4 +344,17 @@ class Request extends Base { return isset($this->server[$variable]) ? $this->server[$variable] : ''; } + + protected function filterValues(array $values) + { + foreach ($values as $key => $value) { + + // IE11 Workaround when submitting multipart/form-data + if (strpos($key, '-----------------------------') === 0) { + unset($values[$key]); + } + } + + return $values; + } } diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php index 0f16fb65..0af763a6 100644 --- a/app/Core/Http/Response.php +++ b/app/Core/Http/Response.php @@ -129,6 +129,18 @@ class Response extends Base } /** + * Add P3P headers for Internet Explorer + * + * @access public + * @return $this + */ + public function withP3P() + { + $this->withHeader('P3P', 'CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"'); + return $this; + } + + /** * Set HTTP response body * * @access public diff --git a/app/Core/Mail/Transport/Mail.php b/app/Core/Mail/Transport/Mail.php index d27925f0..c99cc8ba 100644 --- a/app/Core/Mail/Transport/Mail.php +++ b/app/Core/Mail/Transport/Mail.php @@ -33,8 +33,8 @@ class Mail extends Base implements ClientInterface $message = Swift_Message::newInstance() ->setSubject($subject) ->setFrom(array($this->helper->mail->getMailSenderAddress() => $author)) - ->setBody($html, 'text/html') - ->setTo(array($email => $name)); + ->setTo(array($email => $name)) + ->setBody($html, 'text/html'); Swift_Mailer::newInstance($this->getTransport())->send($message); } catch (Swift_TransportException $e) { diff --git a/app/Core/Mail/Transport/Smtp.php b/app/Core/Mail/Transport/Smtp.php index 66f0a3aa..815dde5d 100644 --- a/app/Core/Mail/Transport/Smtp.php +++ b/app/Core/Mail/Transport/Smtp.php @@ -25,6 +25,16 @@ class Smtp extends Mail $transport->setPassword(MAIL_SMTP_PASSWORD); $transport->setEncryption(MAIL_SMTP_ENCRYPTION); + if (HTTP_VERIFY_SSL_CERTIFICATE === false) { + $transport->setStreamOptions(array( + 'ssl' => array( + 'allow_self_signed' => true, + 'verify_peer' => false, + 'verify_peer_name' => false, + ) + )); + } + return $transport; } } diff --git a/app/Core/Markdown.php b/app/Core/Markdown.php index b5abe5ed..4487bf2a 100644 --- a/app/Core/Markdown.php +++ b/app/Core/Markdown.php @@ -86,18 +86,23 @@ class Markdown extends Parsedown */ protected function inlineUserLink(array $Excerpt) { - if (! $this->isPublicLink && preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) { - $user_id = $this->container['userModel']->getIdByUsername($matches[1]); + if (! $this->isPublicLink && preg_match('/^@([^\s,!:?]+)/', $Excerpt['text'], $matches)) { + $username = rtrim($matches[1], '.'); + $user = $this->container['userCacheDecorator']->getByUsername($username); - if (! empty($user_id)) { - $url = $this->container['helper']->url->href('UserViewController', 'profile', array('user_id' => $user_id)); + if (! empty($user)) { + $url = $this->container['helper']->url->href('UserViewController', 'profile', array('user_id' => $user['id'])); return array( - 'extent' => strlen($matches[0]), + 'extent' => strlen($username) + 1, 'element' => array( - 'name' => 'a', - 'text' => $matches[0], - 'attributes' => array('href' => $url, 'class' => 'user-mention-link'), + 'name' => 'a', + 'text' => '@' . $username, + 'attributes' => array( + 'href' => $url, + 'class' => 'user-mention-link', + 'title' => $user['name'] ?: $user['username'], + ), ), ); } @@ -125,7 +130,10 @@ class Markdown extends Parsedown array( 'token' => $token, 'task_id' => $task_id, - ) + ), + false, + '', + true ); } diff --git a/app/Core/Paginator.php b/app/Core/Paginator.php index cfe89938..9075a713 100644 --- a/app/Core/Paginator.php +++ b/app/Core/Paginator.php @@ -232,6 +232,17 @@ class Paginator } /** + * Get the number of current page + * + * @access public + * @return integer + */ + public function getPage() + { + return $this->page; + } + + /** * Set the default column order * * @access public @@ -271,6 +282,16 @@ class Paginator } /** + * Get the maximum number of items per page. + * + * @return int + */ + public function getMax() + { + return $this->limit; + } + + /** * Return true if the collection is empty * * @access public @@ -353,7 +374,9 @@ class Paginator '← '.t('Previous'), $this->controller, $this->action, - $this->getUrlParams($this->page - 1, $this->order, $this->direction) + $this->getUrlParams($this->page - 1, $this->order, $this->direction), + false, + 'js-modal-replace' ); } else { $html .= '← '.t('Previous'); @@ -379,7 +402,9 @@ class Paginator t('Next').' →', $this->controller, $this->action, - $this->getUrlParams($this->page + 1, $this->order, $this->direction) + $this->getUrlParams($this->page + 1, $this->order, $this->direction), + false, + 'js-modal-replace' ); } else { $html .= t('Next').' →'; @@ -391,6 +416,17 @@ class Paginator } /** + * Generate the page showing. + * + * @access public + * @return string + */ + public function generatPageShowing() + { + return '<span class="pagination-showing">'.t('Showing %d-%d of %d', (($this->getPage() - 1) * $this->getMax() + 1), min($this->getTotal(), $this->getPage() * $this->getMax()), $this->getTotal()).'</span>'; + } + + /** * Return true if there is no pagination to show * * @access public @@ -413,6 +449,7 @@ class Paginator if (! $this->hasNothingtoShow()) { $html .= '<div class="pagination">'; + $html .= $this->generatPageShowing(); $html .= $this->generatePreviousLink(); $html .= $this->generateNextLink(); $html .= '</div>'; @@ -453,7 +490,9 @@ class Paginator $label, $this->controller, $this->action, - $this->getUrlParams($this->page, $column, $direction) + $this->getUrlParams($this->page, $column, $direction), + false, + 'js-modal-replace' ); } } diff --git a/app/Core/Plugin/Base.php b/app/Core/Plugin/Base.php index 9d8167a9..e0b5954a 100644 --- a/app/Core/Plugin/Base.php +++ b/app/Core/Plugin/Base.php @@ -131,4 +131,17 @@ abstract class Base extends \Kanboard\Core\Base { return ''; } + + /** + * Get application compatibility version + * + * Examples: >=1.0.36, 1.0.37, APP_VERSION + * + * @access public + * @return string + */ + public function getCompatibleVersion() + { + return APP_VERSION; + } } diff --git a/app/Core/Plugin/Directory.php b/app/Core/Plugin/Directory.php index 21f11ca9..dc32e655 100644 --- a/app/Core/Plugin/Directory.php +++ b/app/Core/Plugin/Directory.php @@ -36,11 +36,7 @@ class Directory extends BaseCore */ public function isCompatible(array $plugin, $appVersion = APP_VERSION) { - if (strpos($appVersion, 'master') !== false) { - return true; - } - - return $plugin['compatible_version'] === $appVersion; + return Version::isCompatible($plugin['compatible_version'], $appVersion); } /** diff --git a/app/Core/Plugin/Hook.php b/app/Core/Plugin/Hook.php index ade69150..ca197937 100644 --- a/app/Core/Plugin/Hook.php +++ b/app/Core/Plugin/Hook.php @@ -96,4 +96,21 @@ class Hook return null; } + + /** + * Hook with reference + * + * @access public + * @param string $hook + * @param mixed $param + * @return mixed + */ + public function reference($hook, &$param) + { + foreach ($this->getListeners($hook) as $listener) { + $listener($param); + } + + return $param; + } } diff --git a/app/Core/Plugin/Installer.php b/app/Core/Plugin/Installer.php index 48c4d978..b3618aeb 100644 --- a/app/Core/Plugin/Installer.php +++ b/app/Core/Plugin/Installer.php @@ -2,9 +2,8 @@ namespace Kanboard\Core\Plugin; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; use ZipArchive; +use Kanboard\Core\Tool; /** * Class Installer @@ -64,7 +63,7 @@ class Installer extends \Kanboard\Core\Base throw new PluginInstallerException(e('You don\'t have the permission to remove this plugin.')); } - $this->removeAllDirectories($pluginFolder); + Tool::removeAllFiles($pluginFolder); } /** @@ -137,26 +136,4 @@ class Installer extends \Kanboard\Core\Base 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 f2f6add7..38f41d39 100644 --- a/app/Core/Plugin/Loader.php +++ b/app/Core/Plugin/Loader.php @@ -4,6 +4,7 @@ namespace Kanboard\Core\Plugin; use Composer\Autoload\ClassLoader; use DirectoryIterator; +use Exception; use LogicException; use Kanboard\Core\Tool; @@ -22,6 +23,7 @@ class Loader extends \Kanboard\Core\Base * @var array */ protected $plugins = array(); + protected $incompatiblePlugins = array(); /** * Get list of loaded plugins @@ -35,6 +37,17 @@ class Loader extends \Kanboard\Core\Base } /** + * Get list of not compatible plugins + * + * @access public + * @return Base[] + */ + public function getIncompatiblePlugins() + { + return $this->incompatiblePlugins; + } + + /** * Scan plugin folder and load plugins * * @access public @@ -51,8 +64,7 @@ class Loader extends \Kanboard\Core\Base foreach ($dir as $fileInfo) { if ($fileInfo->isDir() && substr($fileInfo->getFilename(), 0, 1) !== '.') { $pluginName = $fileInfo->getFilename(); - $this->loadSchema($pluginName); - $this->initializePlugin($pluginName, $this->loadPlugin($pluginName)); + $this->initializePlugin($pluginName); } } } @@ -85,7 +97,7 @@ class Loader extends \Kanboard\Core\Base $className = '\Kanboard\Plugin\\'.$pluginName.'\\Plugin'; if (! class_exists($className)) { - throw new LogicException('Unable to load this plugin class '.$className); + throw new LogicException('Unable to load this plugin class: '.$className); } return new $className($this->container); @@ -96,18 +108,30 @@ class Loader extends \Kanboard\Core\Base * * @access public * @param string $pluginName - * @param Base $plugin */ - public function initializePlugin($pluginName, Base $plugin) + public function initializePlugin($pluginName) { - if (method_exists($plugin, 'onStartup')) { - $this->dispatcher->addListener('app.bootstrap', array($plugin, 'onStartup')); - } + try { + $plugin = $this->loadPlugin($pluginName); - Tool::buildDIC($this->container, $plugin->getClasses()); - Tool::buildDICHelpers($this->container, $plugin->getHelpers()); + if (Version::isCompatible($plugin->getCompatibleVersion(), APP_VERSION)) { + $this->loadSchema($pluginName); + + if (method_exists($plugin, 'onStartup')) { + $this->dispatcher->addListener('app.bootstrap', array($plugin, 'onStartup')); + } - $plugin->initialize(); - $this->plugins[$pluginName] = $plugin; + Tool::buildDIC($this->container, $plugin->getClasses()); + Tool::buildDICHelpers($this->container, $plugin->getHelpers()); + + $plugin->initialize(); + $this->plugins[$pluginName] = $plugin; + } else { + $this->incompatiblePlugins[$pluginName] = $plugin; + $this->logger->error($pluginName.' is not compatible with this version'); + } + } catch (Exception $e) { + $this->logger->critical($pluginName.': '.$e->getMessage()); + } } } diff --git a/app/Core/Plugin/PluginException.php b/app/Core/Plugin/PluginException.php new file mode 100644 index 00000000..fae7de35 --- /dev/null +++ b/app/Core/Plugin/PluginException.php @@ -0,0 +1,15 @@ +<?php + +namespace Kanboard\Core\Plugin; + +use Exception; + +/** + * Class PluginException + * + * @package Kanboard\Core\Plugin + * @author Frederic Guillot + */ +class PluginException extends Exception +{ +} diff --git a/app/Core/Plugin/PluginInstallerException.php b/app/Core/Plugin/PluginInstallerException.php index 7d356c9b..31745f22 100644 --- a/app/Core/Plugin/PluginInstallerException.php +++ b/app/Core/Plugin/PluginInstallerException.php @@ -2,14 +2,12 @@ namespace Kanboard\Core\Plugin; -use Exception; - /** * Class PluginInstallerException * * @package Kanboard\Core\Plugin * @author Frederic Guillot */ -class PluginInstallerException extends Exception +class PluginInstallerException extends PluginException { } diff --git a/app/Core/Plugin/Version.php b/app/Core/Plugin/Version.php new file mode 100644 index 00000000..ba5e0443 --- /dev/null +++ b/app/Core/Plugin/Version.php @@ -0,0 +1,38 @@ +<?php + +namespace Kanboard\Core\Plugin; + +/** + * Class Version + * + * @package Kanboard\Core\Plugin + * @author Frederic Guillot + */ +class Version +{ + /** + * Check plugin version compatibility with application version + * + * @param string $pluginCompatibleVersion + * @param string $appVersion + * @return bool + */ + public static function isCompatible($pluginCompatibleVersion, $appVersion = APP_VERSION) + { + if (strpos($appVersion, 'master') !== false) { + return true; + } + + $appVersion = str_replace('v', '', $appVersion); + $pluginCompatibleVersion = str_replace('v', '', $pluginCompatibleVersion); + + foreach (array('>=', '>', '<=', '<') as $operator) { + if (strpos($pluginCompatibleVersion, $operator) === 0) { + $pluginVersion = substr($pluginCompatibleVersion, strlen($operator)); + return version_compare($appVersion, $pluginVersion, $operator); + } + } + + return $pluginCompatibleVersion === $appVersion; + } +} diff --git a/app/Core/Queue/JobHandler.php b/app/Core/Queue/JobHandler.php index 7ca36328..11c1fb69 100644 --- a/app/Core/Queue/JobHandler.php +++ b/app/Core/Queue/JobHandler.php @@ -2,6 +2,7 @@ namespace Kanboard\Core\Queue; +use Exception; use Kanboard\Core\Base; use Kanboard\Job\BaseJob; use SimpleQueue\Job; @@ -39,16 +40,23 @@ class JobHandler extends Base public function executeJob(Job $job) { $payload = $job->getBody(); - $className = $payload['class']; - $this->memoryCache->flush(); - $this->prepareJobSession($payload['user_id']); - if (DEBUG) { - $this->logger->debug(__METHOD__.' Received job => '.$className.' ('.getmypid().')'); - } + try { + $className = $payload['class']; + $this->prepareJobSession($payload['user_id']); + $this->prepareJobEnvironment(); + + if (DEBUG) { + $this->logger->debug(__METHOD__.' Received job => '.$className.' ('.getmypid().')'); + $this->logger->debug(__METHOD__.' => '.json_encode($payload)); + } - $worker = new $className($this->container); - call_user_func_array(array($worker, 'execute'), $payload['params']); + $worker = new $className($this->container); + call_user_func_array(array($worker, 'execute'), $payload['params']); + } catch (Exception $e) { + $this->logger->error(__METHOD__.': Error during job execution: '.$e->getMessage()); + $this->logger->error(__METHOD__ .' => '.json_encode($payload)); + } } /** @@ -67,4 +75,16 @@ class JobHandler extends Base $this->userSession->initialize($user); } } + + /** + * Flush in-memory caching and specific events + * + * @access protected + */ + protected function prepareJobEnvironment() + { + $this->memoryCache->flush(); + $this->actionManager->removeEvents(); + $this->dispatcher->dispatch('app.bootstrap'); + } } diff --git a/app/Core/Queue/QueueManager.php b/app/Core/Queue/QueueManager.php index f34cb220..1d7c2d1e 100644 --- a/app/Core/Queue/QueueManager.php +++ b/app/Core/Queue/QueueManager.php @@ -42,9 +42,13 @@ class QueueManager extends Base */ public function push(BaseJob $job) { + $jobClassName = get_class($job); + if ($this->queue !== null) { + $this->logger->debug(__METHOD__.': Job pushed in queue: '.$jobClassName); $this->queue->push(JobHandler::getInstance($this->container)->serializeJob($job)); } else { + $this->logger->debug(__METHOD__.': Job executed synchronously: '.$jobClassName); call_user_func_array(array($job, 'execute'), $job->getJobParams()); } @@ -60,7 +64,7 @@ class QueueManager extends Base public function listen() { if ($this->queue === null) { - throw new LogicException('No Queue Driver defined!'); + throw new LogicException('No queue driver defined or unable to connect to broker!'); } while ($job = $this->queue->pull()) { diff --git a/app/Core/Security/Role.php b/app/Core/Security/Role.php index cb45a8af..c16d4094 100644 --- a/app/Core/Security/Role.php +++ b/app/Core/Security/Role.php @@ -50,6 +50,18 @@ class Role } /** + * Check if the given role is custom or not + * + * @access public + * @param string $role + * @return bool + */ + public function isCustomProjectRole($role) + { + return ! empty($role) && $role !== self::PROJECT_MANAGER && $role !== self::PROJECT_MEMBER && $role !== self::PROJECT_VIEWER; + } + + /** * Get role name * * @access public diff --git a/app/Core/Session/SessionStorage.php b/app/Core/Session/SessionStorage.php index 9e93602c..e6478d8d 100644 --- a/app/Core/Session/SessionStorage.php +++ b/app/Core/Session/SessionStorage.php @@ -19,6 +19,7 @@ namespace Kanboard\Core\Session; * @property bool $hasSubtaskInProgress * @property bool $hasRememberMe * @property bool $boardCollapsed + * @property string $scope * @property bool $twoFactorBeforeCodeCalled * @property string $twoFactorSecret * @property string $oauthState diff --git a/app/Core/Tool.php b/app/Core/Tool.php index bfa6c955..6e457641 100644 --- a/app/Core/Tool.php +++ b/app/Core/Tool.php @@ -3,6 +3,8 @@ namespace Kanboard\Core; use Pimple\Container; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; /** * Tool class @@ -13,7 +15,33 @@ use Pimple\Container; class Tool { /** - * Build dependency injection container from an array + * Remove recursively a directory + * + * @static + * @access public + * @param string $directory + * @param bool $removeDirectory + */ + public static function removeAllFiles($directory, $removeDirectory = true) + { + $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()); + } + } + + if ($removeDirectory) { + rmdir($directory); + } + } + + /** + * Build dependency injection containers from an array * * @static * @access public @@ -36,6 +64,29 @@ class Tool } /** + * Build dependency injection container from an array + * + * @static + * @access public + * @param Container $container + * @param array $namespaces + * @return Container + */ + public static function buildFactories(Container $container, array $namespaces) + { + foreach ($namespaces as $namespace => $classes) { + foreach ($classes as $name) { + $class = '\\Kanboard\\'.$namespace.'\\'.$name; + $container[lcfirst($name)] = $container->factory(function ($c) use ($class) { + return new $class($c); + }); + } + } + + return $container; + } + + /** * Build dependency injection container for custom helpers from an array * * @static diff --git a/app/Core/Translator.php b/app/Core/Translator.php index 113c0dc6..ac2e2aae 100644 --- a/app/Core/Translator.php +++ b/app/Core/Translator.php @@ -11,13 +11,6 @@ namespace Kanboard\Core; class Translator { /** - * Locale path - * - * @var string - */ - const PATH = 'app/Locale'; - - /** * Locale * * @static @@ -171,9 +164,13 @@ class Translator * @param string $language Locale code: fr_FR * @param string $path Locale folder */ - public static function load($language, $path = self::PATH) + public static function load($language, $path = '') { - $filename = $path.DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR.'translations.php'; + if ($path === '') { + $path = self::getDefaultFolder(); + } + + $filename = implode(DIRECTORY_SEPARATOR, array($path, $language, 'translations.php')); if (file_exists($filename)) { self::$locales = array_merge(self::$locales, require($filename)); @@ -190,4 +187,15 @@ class Translator { self::$locales = array(); } + + /** + * Get default locales folder + * + * @access public + * @return string + */ + public static function getDefaultFolder() + { + return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', 'Locale')); + } } diff --git a/app/Core/User/UserSession.php b/app/Core/User/UserSession.php index 9c63f07a..7917b223 100644 --- a/app/Core/User/UserSession.php +++ b/app/Core/User/UserSession.php @@ -179,50 +179,4 @@ class UserSession extends Base { $this->sessionStorage->filters[$project_id] = $filters; } - - /** - * Is board collapsed or expanded - * - * @access public - * @param integer $project_id - * @return boolean - */ - public function isBoardCollapsed($project_id) - { - return ! empty($this->sessionStorage->boardCollapsed[$project_id]) ? $this->sessionStorage->boardCollapsed[$project_id] : false; - } - - /** - * Set board display mode - * - * @access public - * @param integer $project_id - * @param boolean $is_collapsed - */ - public function setBoardDisplayMode($project_id, $is_collapsed) - { - $this->sessionStorage->boardCollapsed[$project_id] = $is_collapsed; - } - - /** - * Set comments sorting - * - * @access public - * @param string $order - */ - public function setCommentSorting($order) - { - $this->sessionStorage->commentSorting = $order; - } - - /** - * Get comments sorting direction - * - * @access public - * @return string - */ - public function getCommentSorting() - { - return empty($this->sessionStorage->commentSorting) ? 'ASC' : $this->sessionStorage->commentSorting; - } } |