看板初始化提交
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Action;
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Action\Base as ActionBase;
|
||||
|
||||
/**
|
||||
* Action Manager
|
||||
*
|
||||
* @package action
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ActionManager extends Base
|
||||
{
|
||||
/**
|
||||
* List of automatic actions
|
||||
*
|
||||
* @access private
|
||||
* @var ActionBase[]
|
||||
*/
|
||||
private $actions = array();
|
||||
|
||||
/**
|
||||
* Register a new automatic action
|
||||
*
|
||||
* @access public
|
||||
* @param ActionBase $action
|
||||
* @return ActionManager
|
||||
*/
|
||||
public function register(ActionBase $action)
|
||||
{
|
||||
$this->actions[$action->getName()] = $action;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get automatic action instance
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Absolute class name with namespace
|
||||
* @return ActionBase
|
||||
*/
|
||||
public function getAction($name)
|
||||
{
|
||||
if (isset($this->actions[$name])) {
|
||||
return $this->actions[$name];
|
||||
}
|
||||
|
||||
throw new RuntimeException('Automatic Action Not Found: '.$name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available automatic actions
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableActions()
|
||||
{
|
||||
$actions = array();
|
||||
|
||||
foreach ($this->actions as $action) {
|
||||
if (count($action->getEvents()) > 0) {
|
||||
$actions[$action->getName()] = $action->getDescription();
|
||||
}
|
||||
}
|
||||
|
||||
asort($actions);
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available action parameters
|
||||
*
|
||||
* @access public
|
||||
* @param array $actions
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableParameters(array $actions)
|
||||
{
|
||||
$params = array();
|
||||
|
||||
foreach ($actions as $action) {
|
||||
try {
|
||||
$currentAction = $this->getAction($action['action_name']);
|
||||
$params[$currentAction->getName()] = $currentAction->getActionRequiredParameters();
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error(__METHOD__.': '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of compatible events for a given action
|
||||
*
|
||||
* @access public
|
||||
* @param string $name
|
||||
* @return array
|
||||
*/
|
||||
public function getCompatibleEvents($name)
|
||||
{
|
||||
$events = array();
|
||||
$actionEvents = $this->getAction($name)->getEvents();
|
||||
|
||||
foreach ($this->eventManager->getAll() as $event => $description) {
|
||||
if (in_array($event, $actionEvents)) {
|
||||
$events[$event] = $description;
|
||||
}
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind automatic actions to events
|
||||
*
|
||||
* @access public
|
||||
* @return ActionManager
|
||||
*/
|
||||
public function attachEvents()
|
||||
{
|
||||
if ($this->userSession->isLogged()) {
|
||||
$actions = $this->actionModel->getAllByUser($this->userSession->getId());
|
||||
} else {
|
||||
$actions = $this->actionModel->getAll();
|
||||
}
|
||||
|
||||
foreach ($actions as $action) {
|
||||
try {
|
||||
$listener = clone $this->getAction($action['action_name']);
|
||||
$listener->setProjectId($action['project_id']);
|
||||
|
||||
foreach ($action['params'] as $param_name => $param_value) {
|
||||
$listener->setParam($param_name, $param_value);
|
||||
}
|
||||
|
||||
$this->dispatcher->addListener($action['event_name'], array($listener, 'execute'));
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error(__METHOD__.': '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
use Pimple\Container;
|
||||
|
||||
/**
|
||||
* Base Class
|
||||
*
|
||||
* @package Kanboard\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\Analytic\EstimatedActualColumnAnalytic $estimatedActualColumnAnalytic
|
||||
* @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\User\UserManager $userManager
|
||||
* @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\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\ProjectApiFormatter $projectApiFormatter
|
||||
* @property \Kanboard\Formatter\ProjectsApiFormatter $projectsApiFormatter
|
||||
* @property \Kanboard\Formatter\SubtaskListFormatter $subtaskListFormatter
|
||||
* @property \Kanboard\Formatter\SubtaskTimeTrackingCalendarFormatter $subtaskTimeTrackingCalendarFormatter
|
||||
* @property \Kanboard\Formatter\TaskApiFormatter $taskApiFormatter
|
||||
* @property \Kanboard\Formatter\TasksApiFormatter $tasksApiFormatter
|
||||
* @property \Kanboard\Formatter\TaskAutoCompleteFormatter $taskAutoCompleteFormatter
|
||||
* @property \Kanboard\Formatter\TaskICalFormatter $taskICalFormatter
|
||||
* @property \Kanboard\Formatter\TaskListFormatter $taskListFormatter
|
||||
* @property \Kanboard\Formatter\TaskListSubtaskFormatter $taskListSubtaskFormatter
|
||||
* @property \Kanboard\Formatter\TaskListSubtaskAssigneeFormatter $taskListSubtaskAssigneeFormatter
|
||||
* @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\CaptchaModel $captchaModel
|
||||
* @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\PredefinedTaskDescriptionModel $predefinedTaskDescriptionModel
|
||||
* @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\TaskReorderModel $taskReorderModel
|
||||
* @property \Kanboard\Model\TaskStatusModel $taskStatusModel
|
||||
* @property \Kanboard\Model\TaskTagModel $taskTagModel
|
||||
* @property \Kanboard\Model\TaskMetadataModel $taskMetadataModel
|
||||
* @property \Kanboard\Model\ThemeModel $themeModel
|
||||
* @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\DashboardPagination $dashboardPagination
|
||||
* @property \Kanboard\Pagination\ProjectPagination $projectPagination
|
||||
* @property \Kanboard\Pagination\TaskPagination $taskPagination
|
||||
* @property \Kanboard\Pagination\SubtaskPagination $subtaskPagination
|
||||
* @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\ConfigValidator $configValidator
|
||||
* @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\Validator\PredefinedTaskDescriptionValidator $predefinedTaskDescriptionValidator
|
||||
* @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
|
||||
{
|
||||
/**
|
||||
* Container instance
|
||||
*
|
||||
* @access protected
|
||||
* @var \Pimple\Container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param \Pimple\Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load automatically dependencies
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Class name
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->container[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object instance
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @param Container $container
|
||||
* @return static
|
||||
*/
|
||||
public static function getInstance(Container $container)
|
||||
{
|
||||
return new static($container);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Cache;
|
||||
|
||||
/**
|
||||
* Base Class for Cache Drivers
|
||||
*
|
||||
* @package Kanboard\Core\Cache
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
abstract class BaseCache implements CacheInterface
|
||||
{
|
||||
/**
|
||||
* Proxy cache
|
||||
*
|
||||
* Note: Arguments must be scalar types
|
||||
*
|
||||
* @access public
|
||||
* @param string $class Class instance
|
||||
* @param string $method Container method
|
||||
* @return mixed
|
||||
*/
|
||||
public function proxy($class, $method)
|
||||
{
|
||||
$args = func_get_args();
|
||||
array_shift($args);
|
||||
|
||||
$key = 'proxy:'.get_class($class).':'.implode(':', $args);
|
||||
$result = $this->get($key);
|
||||
|
||||
if ($result === null) {
|
||||
$result = call_user_func_array(array($class, $method), array_splice($args, 1));
|
||||
$this->set($key, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Cache;
|
||||
|
||||
/**
|
||||
* Interface CacheInterface
|
||||
*
|
||||
* @package Kanboard\Core\Cache
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface CacheInterface
|
||||
{
|
||||
/**
|
||||
* Store an item in the cache
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function set($key, $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);
|
||||
|
||||
/**
|
||||
* Remove all items from the cache
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function flush();
|
||||
|
||||
/**
|
||||
* Remove an item from the cache
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
*/
|
||||
public function remove($key);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Cache;
|
||||
|
||||
/**
|
||||
* Memory Cache Driver
|
||||
*
|
||||
* @package Kanboard\Core\Cache
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class MemoryCache extends BaseCache
|
||||
{
|
||||
/**
|
||||
* Container
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $storage = array();
|
||||
|
||||
/**
|
||||
* Store an item in the cache
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function set($key, $value)
|
||||
{
|
||||
$this->storage[$key] = $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)
|
||||
{
|
||||
return isset($this->storage[$key]) ? $this->storage[$key] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
$this->storage = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cached value
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
*/
|
||||
public function remove($key)
|
||||
{
|
||||
unset($this->storage[$key]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Controller;
|
||||
|
||||
/**
|
||||
* Class AccessForbiddenException
|
||||
*
|
||||
* @package Kanboard\Core\Controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class AccessForbiddenException extends BaseException
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Controller;
|
||||
|
||||
/**
|
||||
* Class PageNotFoundException
|
||||
*
|
||||
* @package Kanboard\Core\Controller
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class PageNotFoundException extends BaseException
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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(), $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
use SplFileObject;
|
||||
|
||||
/**
|
||||
* CSV Writer/Reader
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Csv
|
||||
{
|
||||
/**
|
||||
* CSV delimiter
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $delimiter = ',';
|
||||
|
||||
/**
|
||||
* CSV enclosure
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $enclosure = '"';
|
||||
|
||||
/**
|
||||
* CSV/SQL columns
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $columns = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param string $delimiter
|
||||
* @param string $enclosure
|
||||
*/
|
||||
public function __construct($delimiter = ',', $enclosure = '"')
|
||||
{
|
||||
$this->delimiter = $delimiter;
|
||||
$this->enclosure = $enclosure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of delimiters
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public static function getDelimiters()
|
||||
{
|
||||
return array(
|
||||
',' => t('Comma'),
|
||||
';' => t('Semi-colon'),
|
||||
'\t' => t('Tab'),
|
||||
'|' => t('Vertical bar'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of enclosures
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public static function getEnclosures()
|
||||
{
|
||||
return array(
|
||||
'"' => t('Double Quote'),
|
||||
"'" => t('Single Quote'),
|
||||
'' => t('None'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check boolean field value
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @param mixed $value
|
||||
* @return int
|
||||
*/
|
||||
public static function getBooleanValue($value)
|
||||
{
|
||||
if (! empty($value)) {
|
||||
$value = trim(strtolower($value));
|
||||
return $value === '1' || $value[0] === 't' || $value[0] === 'y' ? 1 : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output CSV file to standard output
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @param array $rows
|
||||
* @param bool $addBOM
|
||||
*/
|
||||
public static function output(array $rows, $addBOM = false)
|
||||
{
|
||||
$csv = new static;
|
||||
$csv->write('php://output', $rows, $addBOM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define column mapping between CSV and SQL columns
|
||||
*
|
||||
* @access public
|
||||
* @param array $columns
|
||||
* @return Csv
|
||||
*/
|
||||
public function setColumnMapping(array $columns)
|
||||
{
|
||||
$this->columns = $columns;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read CSV file
|
||||
*
|
||||
* @access public
|
||||
* @param string $filename
|
||||
* @param callable $callback Example: function(array $row, $line_number)
|
||||
* @return Csv
|
||||
*/
|
||||
public function read($filename, $callback)
|
||||
{
|
||||
$file = new SplFileObject($filename);
|
||||
$file->setFlags(SplFileObject::READ_CSV);
|
||||
$file->setCsvControl($this->delimiter, $this->enclosure, '\\');
|
||||
$line_number = 0;
|
||||
|
||||
foreach ($file as $row) {
|
||||
$row = $this->filterRow($row);
|
||||
|
||||
if (! empty($row) && $line_number > 0) {
|
||||
call_user_func_array($callback, array($this->associateColumns($row), $line_number));
|
||||
}
|
||||
|
||||
$line_number++;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write CSV file
|
||||
*
|
||||
* @access public
|
||||
* @param string $filename
|
||||
* @param array $rows
|
||||
* @param bool $addBOM
|
||||
* @return Csv
|
||||
*/
|
||||
public function write($filename, array $rows, $addBOM = false)
|
||||
{
|
||||
$fp = fopen($filename, 'w');
|
||||
|
||||
if (is_resource($fp)) {
|
||||
if ($addBOM) {
|
||||
fwrite($fp, "\xEF\xBB\xBF");
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($fp, $row, $this->delimiter, $this->enclosure, '\\');
|
||||
}
|
||||
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate columns header with row values
|
||||
*
|
||||
* @access private
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
private function associateColumns(array $row)
|
||||
{
|
||||
$line = array();
|
||||
$index = 0;
|
||||
|
||||
foreach ($this->columns as $sql_name => $csv_name) {
|
||||
if (isset($row[$index])) {
|
||||
$line[$sql_name] = $row[$index];
|
||||
} else {
|
||||
$line[$sql_name] = '';
|
||||
}
|
||||
|
||||
$index++;
|
||||
}
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter empty rows
|
||||
*
|
||||
* @access private
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
private function filterRow(array $row)
|
||||
{
|
||||
return array_filter($row);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* Date Parser
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class DateParser extends Base
|
||||
{
|
||||
const DATE_FORMAT = 'm/d/Y';
|
||||
const TIME_FORMAT = 'H:i';
|
||||
|
||||
/**
|
||||
* Get date format from settings
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getUserDateFormat()
|
||||
{
|
||||
return $this->configModel->get('application_date_format', DateParser::DATE_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date time format from settings
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getUserDateTimeFormat()
|
||||
{
|
||||
return $this->getUserDateFormat().' '.$this->getUserTimeFormat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time format from settings
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getUserTimeFormat()
|
||||
{
|
||||
return $this->configModel->get('application_time_format', DateParser::TIME_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of time formats
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTimeFormats()
|
||||
{
|
||||
return array(
|
||||
'H:i',
|
||||
'g:i a',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of date formats
|
||||
*
|
||||
* @access public
|
||||
* @param boolean $iso
|
||||
* @return string[]
|
||||
*/
|
||||
public function getDateFormats($iso = false)
|
||||
{
|
||||
$formats = array(
|
||||
$this->getUserDateFormat(),
|
||||
);
|
||||
|
||||
$isoFormats = array(
|
||||
'Y-m-d',
|
||||
'Y_m_d',
|
||||
);
|
||||
|
||||
$userFormats = array(
|
||||
'm/d/Y',
|
||||
'd/m/Y',
|
||||
'Y/m/d',
|
||||
'd.m.Y',
|
||||
);
|
||||
|
||||
if ($iso) {
|
||||
$formats = array_merge($formats, $isoFormats, $userFormats);
|
||||
} else {
|
||||
$formats = array_merge($formats, $userFormats);
|
||||
}
|
||||
|
||||
return array_unique($formats);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of datetime formats
|
||||
*
|
||||
* @access public
|
||||
* @param boolean $iso
|
||||
* @return string[]
|
||||
*/
|
||||
public function getDateTimeFormats($iso = false)
|
||||
{
|
||||
$formats = array(
|
||||
$this->getUserDateTimeFormat(),
|
||||
);
|
||||
|
||||
foreach ($this->getDateFormats($iso) as $date) {
|
||||
foreach ($this->getTimeFormats() as $time) {
|
||||
$formats[] = $date.' '.$time;
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($formats);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of all date formats
|
||||
*
|
||||
* @access public
|
||||
* @param boolean $iso
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAllDateFormats($iso = false)
|
||||
{
|
||||
return array_merge($this->getDateFormats($iso), $this->getDateTimeFormats($iso));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available formats (visible in settings)
|
||||
*
|
||||
* @access public
|
||||
* @param array $formats
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableFormats(array $formats)
|
||||
{
|
||||
$values = array();
|
||||
|
||||
foreach ($formats as $format) {
|
||||
$values[$format] = date($format).' ('.$format.')';
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formats for date parsing
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getParserFormats()
|
||||
{
|
||||
return array(
|
||||
$this->getUserDateFormat(),
|
||||
'Y-m-d',
|
||||
'Y_m_d',
|
||||
$this->getUserDateTimeFormat(),
|
||||
'Y-m-d H:i',
|
||||
'Y_m_d H:i',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date and return a unix timestamp, try different date formats
|
||||
*
|
||||
* @access public
|
||||
* @param string $value Date to parse
|
||||
* @return integer
|
||||
*/
|
||||
public function getTimestamp($value)
|
||||
{
|
||||
if (ctype_digit((string) $value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
foreach ($this->getParserFormats() as $format) {
|
||||
$timestamp = $this->getValidDate($value, $format);
|
||||
|
||||
if ($timestamp !== 0) {
|
||||
return $timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a timestamp if the given date format is correct otherwise return 0
|
||||
*
|
||||
* @access private
|
||||
* @param string $value Date to parse
|
||||
* @param string $format Date format
|
||||
* @return integer
|
||||
*/
|
||||
private function getValidDate($value, $format)
|
||||
{
|
||||
$date = DateTime::createFromFormat($format, $value);
|
||||
|
||||
if ($date !== false) {
|
||||
$errors = DateTime::getLastErrors();
|
||||
if ($errors === false ||
|
||||
$errors['error_count'] === 0 && $errors['warning_count'] === 0) {
|
||||
$timestamp = $date->getTimestamp();
|
||||
return $timestamp > 0 ? $timestamp : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the date is within the date range
|
||||
*
|
||||
* @access public
|
||||
* @param DateTime $date
|
||||
* @param DateTime $start
|
||||
* @param DateTime $end
|
||||
* @return boolean
|
||||
*/
|
||||
public function withinDateRange(DateTime $date, DateTime $start, DateTime $end)
|
||||
{
|
||||
return $date >= $start && $date <= $end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of hours between 2 datetime objects
|
||||
* Minutes are rounded to the nearest quarter
|
||||
*
|
||||
* @access public
|
||||
* @param DateTime $d1
|
||||
* @param DateTime $d2
|
||||
* @return float
|
||||
*/
|
||||
public function getHours(DateTime $d1, DateTime $d2)
|
||||
{
|
||||
$seconds = abs($d1->getTimestamp() - $d2->getTimestamp());
|
||||
return round($seconds / 3600, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO-8601 date from user input
|
||||
*
|
||||
* @access public
|
||||
* @param string $value Date to parse
|
||||
* @return string
|
||||
*/
|
||||
public function getIsoDate($value)
|
||||
{
|
||||
return date('Y-m-d', $this->getTimestamp($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a timestamp from an ISO date format
|
||||
*
|
||||
* @access public
|
||||
* @param string $value
|
||||
* @return integer
|
||||
*/
|
||||
public function getTimestampFromIsoFormat($value)
|
||||
{
|
||||
return $this->removeTimeFromTimestamp(ctype_digit((string) $value) ? $value : strtotime($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the time from a timestamp
|
||||
*
|
||||
* @access public
|
||||
* @param integer $timestamp
|
||||
* @return integer
|
||||
*/
|
||||
public function removeTimeFromTimestamp($timestamp)
|
||||
{
|
||||
return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date (form display)
|
||||
*
|
||||
* @access public
|
||||
* @param array $values Database values
|
||||
* @param string[] $fields Date fields
|
||||
* @param string $format Date format
|
||||
* @return array
|
||||
*/
|
||||
public function format(array $values, array $fields, $format)
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (! empty($values[$field])) {
|
||||
if (ctype_digit((string) $values[$field])) {
|
||||
$values[$field] = date($format, $values[$field]);
|
||||
}
|
||||
} else {
|
||||
$values[$field] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert date to timestamp
|
||||
*
|
||||
* @access public
|
||||
* @param array $values Database values
|
||||
* @param string[] $fields Date fields
|
||||
* @param boolean $keep_time Keep time or not
|
||||
* @return array
|
||||
*/
|
||||
public function convert(array $values, array $fields, $keep_time = false)
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (! empty($values[$field])) {
|
||||
$timestamp = $this->getTimestamp($values[$field]);
|
||||
$values[$field] = $keep_time ? $timestamp : $this->removeTimeFromTimestamp($timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Event;
|
||||
|
||||
use Kanboard\Model\TaskModel;
|
||||
use Kanboard\Model\SubtaskModel;
|
||||
use Kanboard\Model\TaskLinkModel;
|
||||
|
||||
/**
|
||||
* Event Manager
|
||||
*
|
||||
* @package event
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class EventManager
|
||||
{
|
||||
/**
|
||||
* Extended events
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $events = array();
|
||||
|
||||
/**
|
||||
* Add new event
|
||||
*
|
||||
* @access public
|
||||
* @param string $event
|
||||
* @param string $description
|
||||
* @return EventManager
|
||||
*/
|
||||
public function register($event, $description)
|
||||
{
|
||||
$this->events[$event] = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of events and description that can be used from the user interface
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
$events = array(
|
||||
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'),
|
||||
TaskModel::EVENT_MOVE_SWIMLANE => t('Move a task to another swimlane'),
|
||||
SubtaskModel::EVENT_CREATE_UPDATE => t('Subtask creation or modification'),
|
||||
);
|
||||
|
||||
$events = array_merge($events, $this->events);
|
||||
asort($events);
|
||||
|
||||
return $events;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ExternalLink;
|
||||
|
||||
/**
|
||||
* External Link Interface
|
||||
*
|
||||
* @package externalLink
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface ExternalLinkInterface
|
||||
{
|
||||
/**
|
||||
* Get link title
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle();
|
||||
|
||||
/**
|
||||
* Get link URL
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl();
|
||||
|
||||
/**
|
||||
* Set link URL
|
||||
*
|
||||
* @access public
|
||||
* @param string $url
|
||||
*/
|
||||
public function setUrl($url);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ExternalLink;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* External Link Manager
|
||||
*
|
||||
* @package externalLink
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ExternalLinkManager extends Base
|
||||
{
|
||||
/**
|
||||
* Automatic type value
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TYPE_AUTO = 'auto';
|
||||
|
||||
/**
|
||||
* Registered providers
|
||||
*
|
||||
* @access private
|
||||
* @var ExternalLinkProviderInterface[]
|
||||
*/
|
||||
private $providers = array();
|
||||
|
||||
/**
|
||||
* Type chosen by the user
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $userInputType = '';
|
||||
|
||||
/**
|
||||
* Text entered by the user
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $userInputText = '';
|
||||
|
||||
/**
|
||||
* Register a new provider
|
||||
*
|
||||
* Providers are registered in a LIFO queue
|
||||
*
|
||||
* @access public
|
||||
* @param ExternalLinkProviderInterface $provider
|
||||
* @return ExternalLinkManager
|
||||
*/
|
||||
public function register(ExternalLinkProviderInterface $provider)
|
||||
{
|
||||
array_unshift($this->providers, $provider);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider
|
||||
*
|
||||
* @access public
|
||||
* @param string $type
|
||||
* @throws ExternalLinkProviderNotFound
|
||||
* @return ExternalLinkProviderInterface
|
||||
*/
|
||||
public function getProvider($type)
|
||||
{
|
||||
foreach ($this->providers as $provider) {
|
||||
if ($provider->getType() === $type) {
|
||||
return $provider;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ExternalLinkProviderNotFound('Unable to find link provider: '.$type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get link types
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getTypes()
|
||||
{
|
||||
$types = array();
|
||||
|
||||
foreach ($this->providers as $provider) {
|
||||
$types[$provider->getType()] = $provider->getName();
|
||||
}
|
||||
|
||||
asort($types);
|
||||
|
||||
return array(self::TYPE_AUTO => t('Auto')) + $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dependency label from a provider
|
||||
*
|
||||
* @access public
|
||||
* @param string $type
|
||||
* @param string $dependency
|
||||
* @return string
|
||||
*/
|
||||
public function getDependencyLabel($type, $dependency)
|
||||
{
|
||||
$provider = $this->getProvider($type);
|
||||
$dependencies = $provider->getDependencies();
|
||||
return isset($dependencies[$dependency]) ? $dependencies[$dependency] : $dependency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a provider that match
|
||||
*
|
||||
* @access public
|
||||
* @throws ExternalLinkProviderNotFound
|
||||
* @return ExternalLinkProviderInterface
|
||||
*/
|
||||
public function find()
|
||||
{
|
||||
if ($this->userInputType === self::TYPE_AUTO) {
|
||||
$provider = $this->findProvider();
|
||||
} else {
|
||||
$provider = $this->getProvider($this->userInputType);
|
||||
$provider->setUserTextInput($this->userInputText);
|
||||
|
||||
if (! $provider->match()) {
|
||||
throw new ExternalLinkProviderNotFound('Unable to parse URL with selected provider');
|
||||
}
|
||||
}
|
||||
|
||||
if ($provider === null) {
|
||||
throw new ExternalLinkProviderNotFound('Unable to find link information from provided information');
|
||||
}
|
||||
|
||||
return $provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set form values
|
||||
*
|
||||
* @access public
|
||||
* @param array $values
|
||||
* @return ExternalLinkManager
|
||||
*/
|
||||
public function setUserInput(array $values)
|
||||
{
|
||||
$this->userInputType = empty($values['type']) ? self::TYPE_AUTO : $values['type'];
|
||||
$this->userInputText = empty($values['text']) ? '' : trim($values['text']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set provider type
|
||||
*
|
||||
* @access public
|
||||
* @param string $userInputType
|
||||
* @return ExternalLinkManager
|
||||
*/
|
||||
public function setUserInputType($userInputType)
|
||||
{
|
||||
$this->userInputType = $userInputType;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set external link
|
||||
* @param string $userInputText
|
||||
* @return ExternalLinkManager
|
||||
*/
|
||||
public function setUserInputText($userInputText)
|
||||
{
|
||||
$this->userInputText = $userInputText;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a provider that user input
|
||||
*
|
||||
* @access private
|
||||
* @return ExternalLinkProviderInterface
|
||||
*/
|
||||
private function findProvider()
|
||||
{
|
||||
foreach ($this->providers as $provider) {
|
||||
$provider->setUserTextInput($this->userInputText);
|
||||
|
||||
if ($provider->match()) {
|
||||
return $provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ExternalLink;
|
||||
|
||||
/**
|
||||
* External Link Provider Interface
|
||||
*
|
||||
* @package externalLink
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface ExternalLinkProviderInterface
|
||||
{
|
||||
/**
|
||||
* Get provider name (label)
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
|
||||
/**
|
||||
* Get link type (will be saved in the database)
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getType();
|
||||
|
||||
/**
|
||||
* Get a dictionary of supported dependency types by the provider
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* [
|
||||
* 'related' => 'Related',
|
||||
* 'child' => 'Child',
|
||||
* 'parent' => 'Parent',
|
||||
* 'self' => 'Self',
|
||||
* ]
|
||||
*
|
||||
* The dictionary key is saved in the database.
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getDependencies();
|
||||
|
||||
/**
|
||||
* Set text entered by the user
|
||||
*
|
||||
* @access public
|
||||
* @param string $input
|
||||
*/
|
||||
public function setUserTextInput($input);
|
||||
|
||||
/**
|
||||
* Return true if the provider can parse correctly the user input
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function match();
|
||||
|
||||
/**
|
||||
* Get the link found with the properties
|
||||
*
|
||||
* @access public
|
||||
* @return ExternalLinkInterface
|
||||
*/
|
||||
public function getLink();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ExternalLink;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* External Link Provider Not Found Exception
|
||||
*
|
||||
* @package externalLink
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ExternalLinkProviderNotFound extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ExternalTask;
|
||||
|
||||
/**
|
||||
* Class AccessForbiddenException
|
||||
*
|
||||
* @package Kanboard\Core\ExternalTask
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class AccessForbiddenException extends ExternalTaskException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ExternalTask;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class NotFoundException
|
||||
*
|
||||
* @package Kanboard\Core\ExternalTask
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ExternalTaskException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all providers
|
||||
*
|
||||
* @return ExternalTaskProviderInterface[]
|
||||
*/
|
||||
public function getProviders()
|
||||
{
|
||||
return $this->providers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?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();
|
||||
|
||||
/**
|
||||
* Get provider icon
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon();
|
||||
|
||||
/**
|
||||
* Get label for adding a new task
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getMenuAddLabel();
|
||||
|
||||
/**
|
||||
* Retrieve task from external system or cache
|
||||
*
|
||||
* @access public
|
||||
* @throws \Kanboard\Core\ExternalTask\ExternalTaskException
|
||||
* @param string $uri
|
||||
* @param int $projectID
|
||||
* @return ExternalTaskInterface
|
||||
*/
|
||||
public function fetch($uri, $projectID);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ExternalTask;
|
||||
|
||||
/**
|
||||
* Class NotFoundException
|
||||
*
|
||||
* @package Kanboard\Core\ExternalTask
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class NotFoundException extends ExternalTaskException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ExternalTask;
|
||||
|
||||
/**
|
||||
* Class ProviderNotFoundException
|
||||
*
|
||||
* @package Kanboard\Core\ExternalTask
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProviderNotFoundException extends ExternalTaskException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Filter;
|
||||
|
||||
use PicoDb\Table;
|
||||
|
||||
/**
|
||||
* Criteria Interface
|
||||
*
|
||||
* @package filter
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface CriteriaInterface
|
||||
{
|
||||
/**
|
||||
* Set the Query
|
||||
*
|
||||
* @access public
|
||||
* @param Table $query
|
||||
* @return CriteriaInterface
|
||||
*/
|
||||
public function withQuery(Table $query);
|
||||
|
||||
/**
|
||||
* Set filter
|
||||
*
|
||||
* @access public
|
||||
* @param FilterInterface $filter
|
||||
* @return CriteriaInterface
|
||||
*/
|
||||
public function withFilter(FilterInterface $filter);
|
||||
|
||||
/**
|
||||
* Apply condition
|
||||
*
|
||||
* @access public
|
||||
* @return CriteriaInterface
|
||||
*/
|
||||
public function apply();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Filter;
|
||||
|
||||
use PicoDb\Table;
|
||||
|
||||
/**
|
||||
* Filter Interface
|
||||
*
|
||||
* @package filter
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface FilterInterface
|
||||
{
|
||||
/**
|
||||
* BaseFilter constructor
|
||||
*
|
||||
* @access public
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __construct($value = null);
|
||||
|
||||
/**
|
||||
* Set the value
|
||||
*
|
||||
* @access public
|
||||
* @param string $value
|
||||
* @return FilterInterface
|
||||
*/
|
||||
public function withValue($value);
|
||||
|
||||
/**
|
||||
* Set query
|
||||
*
|
||||
* @access public
|
||||
* @param Table $query
|
||||
* @return FilterInterface
|
||||
*/
|
||||
public function withQuery(Table $query);
|
||||
|
||||
/**
|
||||
* Get search attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAttributes();
|
||||
|
||||
/**
|
||||
* Apply filter
|
||||
*
|
||||
* @access public
|
||||
* @return FilterInterface
|
||||
*/
|
||||
public function apply();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Filter;
|
||||
|
||||
use PicoDb\Table;
|
||||
|
||||
/**
|
||||
* Formatter interface
|
||||
*
|
||||
* @package filter
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface FormatterInterface
|
||||
{
|
||||
/**
|
||||
* Set query
|
||||
*
|
||||
* @access public
|
||||
* @param Table $query
|
||||
* @return $this
|
||||
*/
|
||||
public function withQuery(Table $query);
|
||||
|
||||
/**
|
||||
* Apply formatter
|
||||
*
|
||||
* @access public
|
||||
* @return mixed
|
||||
*/
|
||||
public function format();
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Filter;
|
||||
|
||||
/**
|
||||
* Lexer
|
||||
*
|
||||
* @package filter
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Lexer
|
||||
{
|
||||
/**
|
||||
* Current position
|
||||
*
|
||||
* @access private
|
||||
* @var integer
|
||||
*/
|
||||
private $offset = 0;
|
||||
|
||||
/**
|
||||
* Token map
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $tokenMap = array(
|
||||
'/^(\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',
|
||||
'/^(\S+)/u' => 'T_STRING',
|
||||
'/^(#\d+)/' => 'T_STRING',
|
||||
);
|
||||
|
||||
/**
|
||||
* Default token
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $defaultToken = '';
|
||||
|
||||
/**
|
||||
* Add token
|
||||
*
|
||||
* @access public
|
||||
* @param string $regex
|
||||
* @param string $token
|
||||
* @return $this
|
||||
*/
|
||||
public function addToken($regex, $token)
|
||||
{
|
||||
$this->tokenMap = array($regex => $token) + $this->tokenMap;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default token
|
||||
*
|
||||
* @access public
|
||||
* @param string $token
|
||||
* @return $this
|
||||
*/
|
||||
public function setDefaultToken($token)
|
||||
{
|
||||
$this->defaultToken = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize input string
|
||||
*
|
||||
* @access public
|
||||
* @param string $input
|
||||
* @return array
|
||||
*/
|
||||
public function tokenize($input)
|
||||
{
|
||||
$tokens = array();
|
||||
$this->offset = 0;
|
||||
|
||||
if (is_null($input)) {
|
||||
$input = "";
|
||||
}
|
||||
$input_length = mb_strlen($input, 'UTF-8');
|
||||
|
||||
while ($this->offset < $input_length) {
|
||||
$result = $this->match(mb_substr($input, $this->offset, $input_length, 'UTF-8'));
|
||||
|
||||
if ($result === false) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$tokens[] = $result;
|
||||
}
|
||||
|
||||
return $this->map($tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a token that match and move the offset
|
||||
*
|
||||
* @access protected
|
||||
* @param string $string
|
||||
* @return array|boolean
|
||||
*/
|
||||
protected function match($string)
|
||||
{
|
||||
foreach ($this->tokenMap as $pattern => $name) {
|
||||
if (preg_match($pattern, $string, $matches)) {
|
||||
$this->offset += mb_strlen($matches[1], 'UTF-8');
|
||||
|
||||
return array(
|
||||
'match' => str_replace('"', '', $matches[1]),
|
||||
'token' => $name,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build map of tokens and matches
|
||||
*
|
||||
* @access protected
|
||||
* @param array $tokens
|
||||
* @return array
|
||||
*/
|
||||
protected function map(array $tokens)
|
||||
{
|
||||
$map = array();
|
||||
$leftOver = '';
|
||||
|
||||
while (false !== ($token = current($tokens))) {
|
||||
if ($token['token'] === 'T_STRING' || $token['token'] === 'T_WHITESPACE') {
|
||||
$leftOver .= $token['match'];
|
||||
} else {
|
||||
$next = next($tokens);
|
||||
|
||||
if ($next !== false && $next['token'] === 'T_STRING') {
|
||||
$map[$token['token']][] = $next['match'];
|
||||
}
|
||||
}
|
||||
|
||||
next($tokens);
|
||||
}
|
||||
|
||||
$leftOver = trim($leftOver);
|
||||
|
||||
if ($this->defaultToken !== '' && $leftOver !== '') {
|
||||
$map[$this->defaultToken] = array($leftOver);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Filter;
|
||||
|
||||
use PicoDb\Table;
|
||||
|
||||
/**
|
||||
* Lexer Builder
|
||||
*
|
||||
* @package filter
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class LexerBuilder
|
||||
{
|
||||
/**
|
||||
* Lexer object
|
||||
*
|
||||
* @access protected
|
||||
* @var Lexer
|
||||
*/
|
||||
protected $lexer;
|
||||
|
||||
/**
|
||||
* Query object
|
||||
*
|
||||
* @access protected
|
||||
* @var Table
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* List of filters
|
||||
*
|
||||
* @access protected
|
||||
* @var FilterInterface[]
|
||||
*/
|
||||
protected $filters;
|
||||
|
||||
/**
|
||||
* QueryBuilder object
|
||||
*
|
||||
* @access protected
|
||||
* @var QueryBuilder
|
||||
*/
|
||||
protected $queryBuilder;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->lexer = new Lexer();
|
||||
$this->queryBuilder = new QueryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a filter
|
||||
*
|
||||
* @access public
|
||||
* @param FilterInterface $filter
|
||||
* @param bool $default
|
||||
* @return LexerBuilder
|
||||
*/
|
||||
public function withFilter(FilterInterface $filter, $default = false)
|
||||
{
|
||||
$attributes = $filter->getAttributes();
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$this->filters[$attribute] = $filter;
|
||||
$this->lexer->addToken(sprintf("/^(%s:)/i", $attribute), $attribute);
|
||||
|
||||
if ($default) {
|
||||
$this->lexer->setDefaultToken($attribute);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query
|
||||
*
|
||||
* @access public
|
||||
* @param Table $query
|
||||
* @return LexerBuilder
|
||||
*/
|
||||
public function withQuery(Table $query)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->queryBuilder->withQuery($this->query);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the input and build the query
|
||||
*
|
||||
* @access public
|
||||
* @param string $input
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function build($input)
|
||||
{
|
||||
$tokens = $this->lexer->tokenize($input);
|
||||
|
||||
foreach ($tokens as $token => $values) {
|
||||
if (isset($this->filters[$token])) {
|
||||
$this->applyFilters($this->filters[$token], $values);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to the query
|
||||
*
|
||||
* @access protected
|
||||
* @param FilterInterface $filter
|
||||
* @param array $values
|
||||
*/
|
||||
protected function applyFilters(FilterInterface $filter, array $values)
|
||||
{
|
||||
$len = count($values);
|
||||
|
||||
if ($len > 1) {
|
||||
$criteria = new OrCriteria();
|
||||
$criteria->withQuery($this->query);
|
||||
|
||||
foreach ($values as $value) {
|
||||
$currentFilter = clone($filter);
|
||||
$criteria->withFilter($currentFilter->withValue($value));
|
||||
}
|
||||
|
||||
$this->queryBuilder->withCriteria($criteria);
|
||||
} elseif ($len === 1) {
|
||||
$this->queryBuilder->withFilter($filter->withValue($values[0]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone object with deep copy
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->lexer = clone $this->lexer;
|
||||
$this->query = clone $this->query;
|
||||
$this->queryBuilder = clone $this->queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Filter;
|
||||
|
||||
use PicoDb\Table;
|
||||
|
||||
/**
|
||||
* OR criteria
|
||||
*
|
||||
* @package filter
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class OrCriteria implements CriteriaInterface
|
||||
{
|
||||
/**
|
||||
* @var Table
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* @var FilterInterface[]
|
||||
*/
|
||||
protected $filters = array();
|
||||
|
||||
/**
|
||||
* Set the Query
|
||||
*
|
||||
* @access public
|
||||
* @param Table $query
|
||||
* @return CriteriaInterface
|
||||
*/
|
||||
public function withQuery(Table $query)
|
||||
{
|
||||
$this->query = $query;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set filter
|
||||
*
|
||||
* @access public
|
||||
* @param FilterInterface $filter
|
||||
* @return CriteriaInterface
|
||||
*/
|
||||
public function withFilter(FilterInterface $filter)
|
||||
{
|
||||
$this->filters[] = $filter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply condition
|
||||
*
|
||||
* @access public
|
||||
* @return CriteriaInterface
|
||||
*/
|
||||
public function apply()
|
||||
{
|
||||
$this->query->beginOr();
|
||||
|
||||
foreach ($this->filters as $filter) {
|
||||
$filter->withQuery($this->query)->apply();
|
||||
}
|
||||
|
||||
$this->query->closeOr();
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Filter;
|
||||
|
||||
use PicoDb\Table;
|
||||
|
||||
/**
|
||||
* Class QueryBuilder
|
||||
*
|
||||
* @package filter
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class QueryBuilder
|
||||
{
|
||||
/**
|
||||
* Query object
|
||||
*
|
||||
* @access protected
|
||||
* @var Table
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* Create a new class instance
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return static
|
||||
*/
|
||||
public static function create()
|
||||
{
|
||||
return new static();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query
|
||||
*
|
||||
* @access public
|
||||
* @param Table $query
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function withQuery(Table $query)
|
||||
{
|
||||
$this->query = $query;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a filter
|
||||
*
|
||||
* @access public
|
||||
* @param FilterInterface $filter
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function withFilter(FilterInterface $filter)
|
||||
{
|
||||
$filter->withQuery($this->query)->apply();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a criteria
|
||||
*
|
||||
* @access public
|
||||
* @param CriteriaInterface $criteria
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function withCriteria(CriteriaInterface $criteria)
|
||||
{
|
||||
$criteria->withQuery($this->query)->apply();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a formatter
|
||||
*
|
||||
* @access public
|
||||
* @param FormatterInterface $formatter
|
||||
* @return string|array
|
||||
*/
|
||||
public function format(FormatterInterface $formatter)
|
||||
{
|
||||
return $formatter->withQuery($this->query)->format();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query result as array
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->query->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Query object
|
||||
*
|
||||
* @access public
|
||||
* @return Table
|
||||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone object with deep copy
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->query = clone $this->query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Group;
|
||||
|
||||
/**
|
||||
* Group Backend Provider Interface
|
||||
*
|
||||
* @package Kanboard\Core\Group
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface GroupBackendProviderInterface
|
||||
{
|
||||
/**
|
||||
* Find a group from a search query
|
||||
*
|
||||
* @access public
|
||||
* @param string $input
|
||||
* @return GroupProviderInterface[]
|
||||
*/
|
||||
public function find($input);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Group;
|
||||
|
||||
/**
|
||||
* Group Manager
|
||||
*
|
||||
* @package Kanboard\Core\Group
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class GroupManager
|
||||
{
|
||||
/**
|
||||
* List of backend providers
|
||||
*
|
||||
* @access protected
|
||||
* @var array
|
||||
*/
|
||||
protected $providers = array();
|
||||
|
||||
/**
|
||||
* Register a new group backend provider
|
||||
*
|
||||
* @access public
|
||||
* @param GroupBackendProviderInterface $provider
|
||||
* @return GroupManager
|
||||
*/
|
||||
public function register(GroupBackendProviderInterface $provider)
|
||||
{
|
||||
$this->providers[] = $provider;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a group from a search query
|
||||
*
|
||||
* @access public
|
||||
* @param string $input
|
||||
* @return GroupProviderInterface[]
|
||||
*/
|
||||
public function find($input)
|
||||
{
|
||||
$groups = array();
|
||||
|
||||
foreach ($this->providers as $provider) {
|
||||
$groups = array_merge($groups, $provider->find($input));
|
||||
}
|
||||
|
||||
return $this->removeDuplicates($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicated groups
|
||||
*
|
||||
* @access protected
|
||||
* @param array $groups
|
||||
* @return GroupProviderInterface[]
|
||||
*/
|
||||
protected function removeDuplicates(array $groups)
|
||||
{
|
||||
$result = array();
|
||||
|
||||
foreach ($groups as $group) {
|
||||
if (! isset($result[$group->getName()])) {
|
||||
$result[$group->getName()] = $group;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Group;
|
||||
|
||||
/**
|
||||
* Group Provider Interface
|
||||
*
|
||||
* @package Kanboard\Core\Group
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface GroupProviderInterface
|
||||
{
|
||||
/**
|
||||
* Get internal id
|
||||
*
|
||||
* You must return 0 if the group come from an external backend
|
||||
*
|
||||
* @access public
|
||||
* @return integer
|
||||
*/
|
||||
public function getInternalId();
|
||||
|
||||
/**
|
||||
* Get external id
|
||||
*
|
||||
* You must return a unique id if the group come from an external provider
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getExternalId();
|
||||
|
||||
/**
|
||||
* Get group name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
use Pimple\Container;
|
||||
|
||||
/**
|
||||
* Helper base class
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*
|
||||
* @property \Kanboard\Helper\AppHelper $app
|
||||
* @property \Kanboard\Helper\AssetHelper $asset
|
||||
* @property \Kanboard\Helper\AvatarHelper $avatar
|
||||
* @property \Kanboard\Helper\BoardHelper $board
|
||||
* @property \Kanboard\Helper\CommentHelper $comment
|
||||
* @property \Kanboard\Helper\DateHelper $dt
|
||||
* @property \Kanboard\Helper\FileHelper $file
|
||||
* @property \Kanboard\Helper\FormHelper $form
|
||||
* @property \Kanboard\Helper\HookHelper $hook
|
||||
* @property \Kanboard\Helper\ModalHelper $modal
|
||||
* @property \Kanboard\Helper\ModelHelper $model
|
||||
* @property \Kanboard\Helper\SubtaskHelper $subtask
|
||||
* @property \Kanboard\Helper\TaskHelper $task
|
||||
* @property \Kanboard\Helper\TextHelper $text
|
||||
* @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
|
||||
*/
|
||||
class Helper
|
||||
{
|
||||
/**
|
||||
* Helper instances
|
||||
*
|
||||
* @access private
|
||||
* @var \Pimple\Container
|
||||
*/
|
||||
private $helpers;
|
||||
|
||||
/**
|
||||
* Container instance
|
||||
*
|
||||
* @access private
|
||||
* @var \Pimple\Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param \Pimple\Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->helpers = new Container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose helpers with magic getter
|
||||
*
|
||||
* @access public
|
||||
* @param string $helper
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($helper)
|
||||
{
|
||||
return $this->getHelper($helper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow overriding helpers through magic setter
|
||||
*
|
||||
* @access public
|
||||
* @param string $helper
|
||||
* @param mixed $instance
|
||||
*/
|
||||
public function __set($helper, $instance)
|
||||
{
|
||||
$this->helpers[$helper] = $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose helpers with method
|
||||
*
|
||||
* @access public
|
||||
* @param string $helper
|
||||
* @return mixed
|
||||
*/
|
||||
public function getHelper($helper)
|
||||
{
|
||||
return $this->helpers[$helper];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new Helper
|
||||
*
|
||||
* @access public
|
||||
* @param string $property
|
||||
* @param string $className
|
||||
* @return Helper
|
||||
*/
|
||||
public function register($property, $className)
|
||||
{
|
||||
$container = $this->container;
|
||||
|
||||
$this->helpers[$property] = function () use ($className, $container) {
|
||||
return new $className($container);
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Http;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Job\HttpAsyncJob;
|
||||
|
||||
/**
|
||||
* HTTP client
|
||||
*
|
||||
* @package Kanboard\Core\Http
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Client extends Base
|
||||
{
|
||||
/**
|
||||
* HTTP client user agent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const HTTP_USER_AGENT = 'Kanboard';
|
||||
|
||||
/**
|
||||
* Send a GET HTTP request
|
||||
*
|
||||
* @access public
|
||||
* @param string $url
|
||||
* @param string[] $headers
|
||||
* @param bool $raiseForErrors
|
||||
* @param bool $followRedirects
|
||||
* @return string
|
||||
*/
|
||||
public function get($url, array $headers = [], $raiseForErrors = false, $followRedirects = true)
|
||||
{
|
||||
return $this->doRequest('GET', $url, '', $headers, $raiseForErrors, $followRedirects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a GET HTTP request and parse JSON response
|
||||
*
|
||||
* @access public
|
||||
* @param string $url
|
||||
* @param string[] $headers
|
||||
* @param bool $raiseForErrors
|
||||
* @param bool $followRedirects
|
||||
* @return array
|
||||
*/
|
||||
public function getJson($url, array $headers = [], $raiseForErrors = false, $followRedirects = true)
|
||||
{
|
||||
$response = $this->doRequest('GET', $url, '', array_merge(['Accept: application/json'], $headers), $raiseForErrors, $followRedirects);
|
||||
return json_decode($response, true) ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST HTTP request encoded in JSON
|
||||
*
|
||||
* @access public
|
||||
* @param string $url
|
||||
* @param array $data
|
||||
* @param string[] $headers
|
||||
* @param bool $raiseForErrors
|
||||
* @param bool $followRedirects
|
||||
* @return string
|
||||
*/
|
||||
public function postJson($url, array $data, array $headers = [], $raiseForErrors = false, $followRedirects = true)
|
||||
{
|
||||
return $this->doRequest(
|
||||
'POST',
|
||||
$url,
|
||||
json_encode($data),
|
||||
array_merge(['Content-type: application/json'], $headers),
|
||||
$raiseForErrors,
|
||||
$followRedirects
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST HTTP request encoded in JSON (Fire and forget)
|
||||
*
|
||||
* @access public
|
||||
* @param string $url
|
||||
* @param array $data
|
||||
* @param string[] $headers
|
||||
* @param bool $raiseForErrors
|
||||
*/
|
||||
public function postJsonAsync($url, array $data, array $headers = [], $raiseForErrors = false)
|
||||
{
|
||||
$this->queueManager->push(HttpAsyncJob::getInstance($this->container)->withParams(
|
||||
'POST',
|
||||
$url,
|
||||
json_encode($data),
|
||||
array_merge(['Content-type: application/json'], $headers),
|
||||
$raiseForErrors
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST HTTP request encoded in www-form-urlencoded
|
||||
*
|
||||
* @access public
|
||||
* @param string $url
|
||||
* @param array $data
|
||||
* @param string[] $headers
|
||||
* @param bool $raiseForErrors
|
||||
* @return string
|
||||
*/
|
||||
public function postForm($url, array $data, array $headers = [], $raiseForErrors = false)
|
||||
{
|
||||
return $this->doRequest(
|
||||
'POST',
|
||||
$url,
|
||||
http_build_query($data),
|
||||
array_merge(['Content-type: application/x-www-form-urlencoded'], $headers),
|
||||
$raiseForErrors
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST HTTP request encoded in www-form-urlencoded (fire and forget)
|
||||
*
|
||||
* @access public
|
||||
* @param string $url
|
||||
* @param array $data
|
||||
* @param string[] $headers
|
||||
* @param bool $raiseForErrors
|
||||
*/
|
||||
public function postFormAsync($url, array $data, array $headers = [], $raiseForErrors = false)
|
||||
{
|
||||
$this->queueManager->push(HttpAsyncJob::getInstance($this->container)->withParams(
|
||||
'POST',
|
||||
$url,
|
||||
http_build_query($data),
|
||||
array_merge(['Content-type: application/x-www-form-urlencoded'], $headers),
|
||||
$raiseForErrors
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the HTTP request with cURL if detected, socket otherwise
|
||||
*
|
||||
* @access public
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param string $content
|
||||
* @param string[] $headers
|
||||
* @param bool $raiseForErrors
|
||||
* @param bool $followRedirects
|
||||
* @return string
|
||||
*/
|
||||
public function doRequest($method, $url, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
|
||||
{
|
||||
$requestBody = '';
|
||||
|
||||
if (! empty($url)) {
|
||||
if (function_exists('curl_version')) {
|
||||
if (DEBUG) {
|
||||
$this->logger->debug('HttpClient::doRequest: cURL detected');
|
||||
}
|
||||
$requestBody = $this->doRequestWithCurl($method, $url, $content, $headers, $raiseForErrors, $followRedirects);
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
$this->logger->debug('HttpClient::doRequest: using socket');
|
||||
}
|
||||
$requestBody = $this->doRequestWithSocket($method, $url, $content, $headers, $raiseForErrors, $followRedirects);
|
||||
}
|
||||
}
|
||||
|
||||
return $requestBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the HTTP request with socket
|
||||
*
|
||||
* @access private
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param string $content
|
||||
* @param string[] $headers
|
||||
* @param bool $raiseForErrors
|
||||
* @param bool $followRedirects
|
||||
* @return string
|
||||
*/
|
||||
private function doRequestWithSocket($method, $url, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$stream = @fopen(trim($url), 'r', false, stream_context_create($this->getContext($method, $content, $headers, $raiseForErrors, $followRedirects)));
|
||||
|
||||
if (! is_resource($stream)) {
|
||||
$this->logger->error('HttpClient: request failed ('.$url.')');
|
||||
|
||||
if ($raiseForErrors) {
|
||||
throw new ClientException('Unreachable URL: '.$url);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$body = stream_get_contents($stream);
|
||||
$metadata = stream_get_meta_data($stream);
|
||||
|
||||
if ($raiseForErrors && array_key_exists('wrapper_data', $metadata)) {
|
||||
$statusCode = $this->getStatusCode($metadata['wrapper_data']);
|
||||
|
||||
if ($statusCode >= 400) {
|
||||
throw new InvalidStatusException('Request failed with status code '.$statusCode, $statusCode, $body);
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
$this->logger->debug('HttpClient: url='.$url);
|
||||
$this->logger->debug('HttpClient: headers='.var_export($headers, true));
|
||||
$this->logger->debug('HttpClient: payload='.$content);
|
||||
$this->logger->debug('HttpClient: metadata='.var_export($metadata, true));
|
||||
$this->logger->debug('HttpClient: body='.$body);
|
||||
$this->logger->debug('HttpClient: executionTime='.(microtime(true) - $startTime));
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make the HTTP request with cURL
|
||||
*
|
||||
* @access private
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param string $content
|
||||
* @param string[] $headers
|
||||
* @param bool $raiseForErrors
|
||||
* @param bool $followRedirects
|
||||
* @return string
|
||||
*/
|
||||
private function doRequestWithCurl($method, $url, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$curlSession = @curl_init();
|
||||
|
||||
curl_setopt($curlSession, CURLOPT_URL, trim($url));
|
||||
curl_setopt($curlSession, CURLOPT_USERAGENT, self::HTTP_USER_AGENT);
|
||||
curl_setopt($curlSession, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
|
||||
curl_setopt($curlSession, CURLOPT_TIMEOUT, HTTP_TIMEOUT);
|
||||
curl_setopt($curlSession, CURLOPT_FORBID_REUSE, true);
|
||||
curl_setopt($curlSession, CURLOPT_MAXREDIRS, $followRedirects ? HTTP_MAX_REDIRECTS : 0);
|
||||
curl_setopt($curlSession, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curlSession, CURLOPT_FOLLOWLOCATION, $followRedirects);
|
||||
|
||||
if ('POST' === $method) {
|
||||
curl_setopt($curlSession, CURLOPT_POST, true);
|
||||
curl_setopt($curlSession, CURLOPT_POSTFIELDS, $content);
|
||||
} elseif ('PUT' === $method) {
|
||||
curl_setopt($curlSession, CURLOPT_CUSTOMREQUEST, 'PUT');
|
||||
curl_setopt($curlSession, CURLOPT_POST, true);
|
||||
curl_setopt($curlSession, CURLOPT_POSTFIELDS, $content);
|
||||
}
|
||||
|
||||
if (! empty($headers)) {
|
||||
curl_setopt($curlSession, CURLOPT_HTTPHEADER, $headers);
|
||||
}
|
||||
|
||||
if (HTTP_VERIFY_SSL_CERTIFICATE === false) {
|
||||
curl_setopt($curlSession, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_setopt($curlSession, CURLOPT_SSL_VERIFYPEER, false);
|
||||
}
|
||||
|
||||
if (HTTP_PROXY_HOSTNAME) {
|
||||
curl_setopt($curlSession, CURLOPT_PROXY, HTTP_PROXY_HOSTNAME);
|
||||
curl_setopt($curlSession, CURLOPT_PROXYPORT, HTTP_PROXY_PORT);
|
||||
curl_setopt($curlSession, CURLOPT_NOPROXY, HTTP_PROXY_EXCLUDE);
|
||||
}
|
||||
|
||||
if (HTTP_PROXY_USERNAME) {
|
||||
curl_setopt($curlSession, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
|
||||
curl_setopt($curlSession, CURLOPT_PROXYUSERPWD, HTTP_PROXY_USERNAME.':'.HTTP_PROXY_PASSWORD);
|
||||
}
|
||||
|
||||
$body = curl_exec($curlSession);
|
||||
|
||||
if ($body === false) {
|
||||
$errorMsg = curl_error($curlSession);
|
||||
curl_close($curlSession);
|
||||
|
||||
$this->logger->error('HttpClient: request failed ('.$url.' - '.$errorMsg.')');
|
||||
|
||||
if ($raiseForErrors) {
|
||||
throw new ClientException('Unreachable URL: '.$url.' ('.$errorMsg.')');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($raiseForErrors) {
|
||||
$statusCode = curl_getinfo($curlSession, CURLINFO_RESPONSE_CODE);
|
||||
|
||||
if ($statusCode >= 400) {
|
||||
curl_close($curlSession);
|
||||
throw new InvalidStatusException('Request failed with status code '.$statusCode, $statusCode, $body);
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
$this->logger->debug('HttpClient: url='.$url);
|
||||
$this->logger->debug('HttpClient: headers='.var_export($headers, true));
|
||||
$this->logger->debug('HttpClient: payload='.$content);
|
||||
$this->logger->debug('HttpClient: metadata='.var_export(curl_getinfo($curlSession), true));
|
||||
$this->logger->debug('HttpClient: body='.$body);
|
||||
$this->logger->debug('HttpClient: executionTime='.(microtime(true) - $startTime));
|
||||
}
|
||||
|
||||
curl_close($curlSession);
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stream context
|
||||
*
|
||||
* @access private
|
||||
* @param string $method
|
||||
* @param string $content
|
||||
* @param string[] $headers
|
||||
* @param bool $raiseForErrors
|
||||
* @param bool $followRedirects
|
||||
* @return array
|
||||
*/
|
||||
private function getContext($method, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
|
||||
{
|
||||
$default_headers = [
|
||||
'User-Agent: '.self::HTTP_USER_AGENT,
|
||||
'Connection: close',
|
||||
];
|
||||
|
||||
if (HTTP_PROXY_USERNAME) {
|
||||
$default_headers[] = 'Proxy-Authorization: Basic '.base64_encode(HTTP_PROXY_USERNAME.':'.HTTP_PROXY_PASSWORD);
|
||||
}
|
||||
|
||||
$headers = array_merge($default_headers, $headers);
|
||||
|
||||
$context = [
|
||||
'http' => [
|
||||
'method' => $method,
|
||||
'protocol_version' => 1.1,
|
||||
'timeout' => HTTP_TIMEOUT,
|
||||
'max_redirects' => $followRedirects ? HTTP_MAX_REDIRECTS : 0,
|
||||
'follow_location' => $followRedirects ? 1 : 0,
|
||||
'header' => implode("\r\n", $headers),
|
||||
'content' => $content,
|
||||
'ignore_errors' => $raiseForErrors,
|
||||
]
|
||||
];
|
||||
|
||||
if (HTTP_PROXY_HOSTNAME) {
|
||||
$context['http']['proxy'] = 'tcp://'.HTTP_PROXY_HOSTNAME.':'.HTTP_PROXY_PORT;
|
||||
$context['http']['request_fulluri'] = true;
|
||||
}
|
||||
|
||||
if (HTTP_VERIFY_SSL_CERTIFICATE === false) {
|
||||
$context['ssl'] = [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
private function getStatusCode(array $lines)
|
||||
{
|
||||
$status = 200;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (strpos($line, 'HTTP/1') === 0) {
|
||||
$status = (int) substr($line, 9, 3);
|
||||
}
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backend used for making HTTP connections
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public static function backend()
|
||||
{
|
||||
return function_exists('curl_version') ? 'cURL' : 'socket';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP address is private
|
||||
*
|
||||
* @access public
|
||||
* @param string $ip
|
||||
* @return bool
|
||||
*/
|
||||
public function isPrivateIpAddress($ip)
|
||||
{
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL is private (RFC1918, localhost, etc.)
|
||||
*
|
||||
* @access public
|
||||
* @param string $url
|
||||
* @return bool
|
||||
*/
|
||||
public function isPrivateURL($url)
|
||||
{
|
||||
$parsedUrl = parse_url($url);
|
||||
|
||||
if (!isset($parsedUrl['scheme']) || !in_array(strtolower($parsedUrl['scheme']), ['http', 'https'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($parsedUrl['host'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = trim($parsedUrl['host']);
|
||||
if ($host === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ipv4Address = gethostbyname($host);
|
||||
if ($this->isPrivateIpAddress($ipv4Address)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (function_exists('dns_get_record')) {
|
||||
$dnsRecords = @dns_get_record($host, DNS_AAAA);
|
||||
if (is_array($dnsRecords)) {
|
||||
foreach ($dnsRecords as $record) {
|
||||
if (isset($record['type']) && $record['type'] === 'AAAA' && isset($record['ipv6'])) {
|
||||
if ($this->isPrivateIpAddress($record['ipv6'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Http;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ClientException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Http;
|
||||
|
||||
class InvalidStatusException extends ClientException
|
||||
{
|
||||
protected $statusCode = 0;
|
||||
protected $body = '';
|
||||
|
||||
public function __construct($message, $statusCode, $body)
|
||||
{
|
||||
parent::__construct($message);
|
||||
$this->statusCode = $statusCode;
|
||||
$this->body = $body;
|
||||
}
|
||||
|
||||
public function getStatusCode()
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Http;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* OAuth2 Client
|
||||
*
|
||||
* @package http
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class OAuth2 extends Base
|
||||
{
|
||||
protected $clientId;
|
||||
protected $secret;
|
||||
protected $callbackUrl;
|
||||
protected $authUrl;
|
||||
protected $tokenUrl;
|
||||
protected $scopes;
|
||||
protected $tokenType;
|
||||
protected $accessToken;
|
||||
|
||||
/**
|
||||
* Create OAuth2 service
|
||||
*
|
||||
* @access public
|
||||
* @param string $clientId
|
||||
* @param string $secret
|
||||
* @param string $callbackUrl
|
||||
* @param string $authUrl
|
||||
* @param string $tokenUrl
|
||||
* @param array $scopes
|
||||
* @return OAuth2
|
||||
*/
|
||||
public function createService($clientId, $secret, $callbackUrl, $authUrl, $tokenUrl, array $scopes)
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
$this->secret = $secret;
|
||||
$this->callbackUrl = $callbackUrl;
|
||||
$this->authUrl = $authUrl;
|
||||
$this->tokenUrl = $tokenUrl;
|
||||
$this->scopes = $scopes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OAuth2 state and return the token value
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getState()
|
||||
{
|
||||
if (! session_exists('oauthState')) {
|
||||
session_set('oauthState', $this->token->getToken());
|
||||
}
|
||||
|
||||
return session_get('oauthState');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the validity of the state (CSRF token)
|
||||
*
|
||||
* @access public
|
||||
* @param string $state
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidateState($state)
|
||||
{
|
||||
return $state === $this->getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization url
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizationUrl()
|
||||
{
|
||||
$params = array(
|
||||
'response_type' => 'code',
|
||||
'client_id' => $this->clientId,
|
||||
'redirect_uri' => $this->callbackUrl,
|
||||
'scope' => implode(' ', $this->scopes),
|
||||
'state' => $this->getState(),
|
||||
);
|
||||
|
||||
return $this->authUrl.'?'.http_build_query($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization header
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizationHeader()
|
||||
{
|
||||
if (strtolower($this->tokenType) === 'bearer') {
|
||||
return 'Authorization: Bearer '.$this->accessToken;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token
|
||||
*
|
||||
* @access public
|
||||
* @param string $code
|
||||
* @return string
|
||||
*/
|
||||
public function getAccessToken($code)
|
||||
{
|
||||
if (empty($this->accessToken) && ! empty($code)) {
|
||||
$params = array(
|
||||
'code' => $code,
|
||||
'client_id' => $this->clientId,
|
||||
'client_secret' => $this->secret,
|
||||
'redirect_uri' => $this->callbackUrl,
|
||||
'grant_type' => 'authorization_code',
|
||||
'state' => $this->getState(),
|
||||
);
|
||||
|
||||
$response = json_decode($this->httpClient->postForm($this->tokenUrl, $params, array('Accept: application/json')), true);
|
||||
|
||||
$this->tokenType = isset($response['token_type']) ? $response['token_type'] : '';
|
||||
$this->accessToken = isset($response['access_token']) ? $response['access_token'] : '';
|
||||
}
|
||||
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set access token
|
||||
*
|
||||
* @access public
|
||||
* @param string $token
|
||||
* @param string $type
|
||||
* @return $this
|
||||
*/
|
||||
public function setAccessToken($token, $type = 'bearer')
|
||||
{
|
||||
$this->accessToken = $token;
|
||||
$this->tokenType = $type;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Http;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Remember Me Cookie
|
||||
*
|
||||
* @package http
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class RememberMeCookie extends Base
|
||||
{
|
||||
/**
|
||||
* Cookie name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const COOKIE_NAME = 'KB_RM';
|
||||
|
||||
/**
|
||||
* Encode the cookie
|
||||
*
|
||||
* @access public
|
||||
* @param string $token Session token
|
||||
* @param string $sequence Sequence token
|
||||
* @return string
|
||||
*/
|
||||
public function encode($token, $sequence)
|
||||
{
|
||||
return implode('|', array($token, $sequence));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the value of a cookie
|
||||
*
|
||||
* @access public
|
||||
* @param string $value Raw cookie data
|
||||
* @return array
|
||||
*/
|
||||
public function decode($value)
|
||||
{
|
||||
list($token, $sequence) = explode('|', $value);
|
||||
|
||||
return array(
|
||||
'token' => $token,
|
||||
'sequence' => $sequence,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the current user has a RememberMe cookie
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCookie()
|
||||
{
|
||||
return $this->request->getCookie(self::COOKIE_NAME) !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Write and encode the cookie
|
||||
*
|
||||
* @access public
|
||||
* @param string $token Session token
|
||||
* @param string $sequence Sequence token
|
||||
* @param string $expiration Cookie expiration
|
||||
* @return boolean
|
||||
*/
|
||||
public function write($token, $sequence, $expiration)
|
||||
{
|
||||
return setcookie(
|
||||
self::COOKIE_NAME,
|
||||
$this->encode($token, $sequence),
|
||||
$expiration,
|
||||
$this->helper->url->dir(),
|
||||
'',
|
||||
$this->request->isHTTPS(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and decode the cookie
|
||||
*
|
||||
* @access public
|
||||
* @return mixed
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
$cookie = $this->request->getCookie(self::COOKIE_NAME);
|
||||
|
||||
if (empty($cookie)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->decode($cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the cookie
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function remove()
|
||||
{
|
||||
return setcookie(
|
||||
self::COOKIE_NAME,
|
||||
'',
|
||||
time() - 3600,
|
||||
$this->helper->url->dir(),
|
||||
'',
|
||||
$this->request->isHTTPS(),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Http;
|
||||
|
||||
use Pimple\Container;
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Request class
|
||||
*
|
||||
* @package http
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Request extends Base
|
||||
{
|
||||
/**
|
||||
* Pointer to PHP environment variables
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $server;
|
||||
private $get;
|
||||
private $post;
|
||||
private $files;
|
||||
private $cookies;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param \Pimple\Container $container
|
||||
* @param array $server
|
||||
* @param array $get
|
||||
* @param array $post
|
||||
* @param array $files
|
||||
* @param array $cookies
|
||||
*/
|
||||
public function __construct(Container $container, array $server = array(), array $get = array(), array $post = array(), array $files = array(), array $cookies = array())
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->server = empty($server) ? $_SERVER : $server;
|
||||
$this->get = empty($get) ? $_GET : $get;
|
||||
$this->post = empty($post) ? $_POST : $post;
|
||||
$this->files = empty($files) ? $_FILES : $files;
|
||||
$this->cookies = empty($cookies) ? $_COOKIE : $cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set GET parameters
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function setParams(array $params)
|
||||
{
|
||||
$this->get = array_merge($this->get, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query string string parameter
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Parameter name
|
||||
* @param string $default_value Default value
|
||||
* @return string
|
||||
*/
|
||||
public function getStringParam($name, $default_value = '')
|
||||
{
|
||||
return isset($this->get[$name]) ? $this->get[$name] : $default_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query string integer parameter
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Parameter name
|
||||
* @param integer $default_value Default value
|
||||
* @return integer
|
||||
*/
|
||||
public function getIntegerParam($name, $default_value = 0)
|
||||
{
|
||||
return isset($this->get[$name]) && ctype_digit((string) $this->get[$name]) ? (int) $this->get[$name] : $default_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a form value
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Form field name
|
||||
* @return string|null
|
||||
*/
|
||||
public function getValue($name)
|
||||
{
|
||||
$values = $this->getValues();
|
||||
return isset($values[$name]) ? $values[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get form values and check for CSRF token
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getValues()
|
||||
{
|
||||
if (! empty($this->post) && ! empty($this->post['csrf_token']) && $this->token->validateCSRFToken($this->post['csrf_token'])) {
|
||||
unset($this->post['csrf_token']);
|
||||
return $this->filterValues($this->post);
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get POST values without modification
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRawFormValues()
|
||||
{
|
||||
return $this->post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get POST value without modification
|
||||
*
|
||||
* @param $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getRawValue($name)
|
||||
{
|
||||
return isset($this->post[$name]) ? $this->post[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw body of the HTTP request
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return file_get_contents('php://input');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Json request body
|
||||
*
|
||||
* @access public
|
||||
* @param bool $enforceContentType
|
||||
* @return array
|
||||
*/
|
||||
public function getJson($enforceContentType = true)
|
||||
{
|
||||
if ($enforceContentType && ! $this->isJsonContentType()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return json_decode($this->getBody(), true) ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of an uploaded file
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Form file name
|
||||
* @return string
|
||||
*/
|
||||
public function getFileContent($name)
|
||||
{
|
||||
if (isset($this->files[$name]['tmp_name'])) {
|
||||
return file_get_contents($this->files[$name]['tmp_name']);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of an uploaded file
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Form file name
|
||||
* @return string
|
||||
*/
|
||||
public function getFilePath($name)
|
||||
{
|
||||
return isset($this->files[$name]['tmp_name']) ? $this->files[$name]['tmp_name'] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info of an uploaded file
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Form file name
|
||||
* @return array
|
||||
*/
|
||||
public function getFileInfo($name)
|
||||
{
|
||||
return isset($this->files[$name]) ? $this->files[$name] : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return HTTP method
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return $this->getServerVariable('REQUEST_METHOD');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the HTTP request is sent with the POST method
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public function isPost()
|
||||
{
|
||||
return $this->getServerVariable('REQUEST_METHOD') === 'POST';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the HTTP request is an Ajax request
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public function isAjax()
|
||||
{
|
||||
return $this->getHeader('X-Requested-With') === 'XMLHttpRequest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request Content-Type is JSON
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public function isJsonContentType()
|
||||
{
|
||||
$contentType = $this->getServerVariable('CONTENT_TYPE');
|
||||
|
||||
if ($contentType === '') {
|
||||
$contentType = $this->getServerVariable('HTTP_CONTENT_TYPE');
|
||||
}
|
||||
|
||||
return stripos($contentType, 'application/json') === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the page is requested through HTTPS
|
||||
*
|
||||
* Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isHTTPS()
|
||||
{
|
||||
if ($this->getServerVariable('HTTP_X_FORWARDED_PROTO') === 'https') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->getServerVariable('HTTPS') !== '' && $this->server['HTTPS'] !== 'off';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cookie value
|
||||
*
|
||||
* @access public
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
public function getCookie($name)
|
||||
{
|
||||
return isset($this->cookies[$name]) ? $this->cookies[$name] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a HTTP header value
|
||||
*
|
||||
* @access public
|
||||
* @param string $name Header name
|
||||
* @return string
|
||||
*/
|
||||
public function getHeader($name)
|
||||
{
|
||||
$name = 'HTTP_'.str_replace('-', '_', strtoupper($name));
|
||||
return $this->getServerVariable($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote user
|
||||
*
|
||||
* @access public
|
||||
* @param array $trustedProxyNetworks
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteUser(array $trustedProxyNetworks = [])
|
||||
{
|
||||
if (! $this->isTrustedProxy($trustedProxyNetworks)) {
|
||||
return '';
|
||||
}
|
||||
return $this->getServerVariable(REVERSE_PROXY_USER_HEADER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote email
|
||||
*
|
||||
* @access public
|
||||
* @param array $trustedProxyNetworks
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteEmail(array $trustedProxyNetworks = [])
|
||||
{
|
||||
if (! $this->isTrustedProxy($trustedProxyNetworks)) {
|
||||
return '';
|
||||
}
|
||||
return $this->getServerVariable(REVERSE_PROXY_EMAIL_HEADER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote user full name
|
||||
*
|
||||
* @access public
|
||||
* @param array $trustedProxyNetworks
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteName(array $trustedProxyNetworks = [])
|
||||
{
|
||||
if (! $this->isTrustedProxy($trustedProxyNetworks)) {
|
||||
return '';
|
||||
}
|
||||
return $this->getServerVariable(REVERSE_PROXY_FULLNAME_HEADER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns query string
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getQueryString()
|
||||
{
|
||||
return $this->getServerVariable('QUERY_STRING');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return URI
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getUri()
|
||||
{
|
||||
return $this->getServerVariable('REQUEST_URI');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a redirect URI is safe (relative path)
|
||||
*
|
||||
* @access public
|
||||
* @param string $uri Redirect URI
|
||||
* @return bool
|
||||
*/
|
||||
public function isSafeRedirectUri($uri)
|
||||
{
|
||||
$uri = trim($uri);
|
||||
if ($uri === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject backslashes
|
||||
if (str_contains($uri, '\\')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject if it starts with // (protocol-relative)
|
||||
if (str_starts_with($uri, '//')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject if it does not start with a slash (relative path)
|
||||
if (! str_starts_with($uri, '/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parsedUrl = parse_url($uri);
|
||||
if ($parsedUrl === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject if it contains a scheme or host (partial or full URL)
|
||||
if (isset($parsedUrl['scheme']) || isset($parsedUrl['host'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user agent
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getUserAgent()
|
||||
{
|
||||
return empty($this->server['HTTP_USER_AGENT']) ? t('Unknown') : $this->server['HTTP_USER_AGENT'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the client IP address
|
||||
*
|
||||
* It returns the proxy IP address if the request is sent through a reverse proxy or the direct client IP address otherwise.
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getClientIpAddress()
|
||||
{
|
||||
return $this->getServerVariable('REMOTE_ADDR');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the real user IP address considering trusted proxy headers and networks
|
||||
*
|
||||
* @access public
|
||||
* @param array $trustedProxyHeaders List of trusted proxy headers (default: TRUSTED_PROXY_HEADERS constant)
|
||||
* @param array $trustedProxyNetworks List of trusted proxy networks (default: TRUSTED_PROXY_NETWORKS constant)
|
||||
* @return string
|
||||
*/
|
||||
public function getIpAddress(array $trustedProxyHeaders = [], array $trustedProxyNetworks = [])
|
||||
{
|
||||
$trustedProxyHeaders = array_filter(array_map('trim', $trustedProxyHeaders ?: explode(',', TRUSTED_PROXY_HEADERS)));
|
||||
$useProxyHeaders = ! empty($trustedProxyHeaders) && $this->isTrustedProxy($trustedProxyNetworks);
|
||||
$keys = $useProxyHeaders ? $trustedProxyHeaders : [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if ($this->getServerVariable($key) !== '') {
|
||||
foreach (explode(',', $this->server[$key]) as $ipAddress) {
|
||||
$ipAddress = trim($ipAddress);
|
||||
if (filter_var($ipAddress, FILTER_VALIDATE_IP)) {
|
||||
return $ipAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->getClientIpAddress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get start time
|
||||
*
|
||||
* @access public
|
||||
* @return float
|
||||
*/
|
||||
public function getStartTime()
|
||||
{
|
||||
return $this->getServerVariable('REQUEST_TIME_FLOAT') ?: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server variable
|
||||
*
|
||||
* @access public
|
||||
* @param string $variable
|
||||
* @return string
|
||||
*/
|
||||
public function getServerVariable($variable)
|
||||
{
|
||||
return isset($this->server[$variable]) ? $this->server[$variable] : '';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP address belongs to a trusted proxy network
|
||||
*
|
||||
* @access public
|
||||
* @param array $trustedProxyNetworks
|
||||
* @return bool
|
||||
*/
|
||||
public function isTrustedProxy(array $trustedProxyNetworks = [])
|
||||
{
|
||||
$ipAddress = $this->getClientIpAddress();
|
||||
if ($ipAddress === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$trustedProxyNetworks = array_filter(array_map('trim', $trustedProxyNetworks ?: explode(',', TRUSTED_PROXY_NETWORKS)));
|
||||
if (empty($trustedProxyNetworks)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->logger->debug('Checking if IP address {ip} belongs to trusted proxy networks: {networks}', ['ip' => $ipAddress, 'networks' => implode(', ', $trustedProxyNetworks)]);
|
||||
|
||||
return $this->isIpInNetworks($ipAddress, $trustedProxyNetworks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP belongs to any of the provided networks
|
||||
*
|
||||
* @access protected
|
||||
* @param string $ipAddress
|
||||
* @param array $networks
|
||||
* @return bool
|
||||
*/
|
||||
protected function isIpInNetworks($ipAddress, array $networks)
|
||||
{
|
||||
if (! filter_var($ipAddress, FILTER_VALIDATE_IP)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ipBinary = inet_pton($ipAddress);
|
||||
|
||||
foreach ($networks as $network) {
|
||||
if ($network === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mask = null;
|
||||
if (strpos($network, '/') !== false) {
|
||||
list($networkAddress, $mask) = explode('/', $network, 2);
|
||||
} else {
|
||||
$networkAddress = $network;
|
||||
}
|
||||
|
||||
if (! filter_var($networkAddress, FILTER_VALIDATE_IP)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$networkBinary = inet_pton($networkAddress);
|
||||
|
||||
if ($networkBinary === false || strlen($networkBinary) !== strlen($ipBinary)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$maxMask = strlen($networkBinary) * 8;
|
||||
$mask = ($mask === null || $mask === '') ? $maxMask : max(0, min((int) $mask, $maxMask));
|
||||
|
||||
if ($this->ipMatchesNetwork($ipBinary, $networkBinary, $mask)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a binary comparison between an IP and a network mask
|
||||
*
|
||||
* @access protected
|
||||
* @param string $ipBinary
|
||||
* @param string $networkBinary
|
||||
* @param int $mask
|
||||
* @return bool
|
||||
*/
|
||||
protected function ipMatchesNetwork($ipBinary, $networkBinary, $mask)
|
||||
{
|
||||
if ($mask === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$bytes = (int) floor($mask / 8);
|
||||
$bits = $mask % 8;
|
||||
|
||||
if ($bytes > 0 && strncmp($ipBinary, $networkBinary, $bytes) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($bits === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$maskByte = ~((1 << (8 - $bits)) - 1) & 0xFF;
|
||||
$ipByte = ord($ipBinary[$bytes]);
|
||||
$networkByte = ord($networkBinary[$bytes]);
|
||||
|
||||
return ($ipByte & $maskByte) === ($networkByte & $maskByte);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Http;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Core\Csv;
|
||||
|
||||
/**
|
||||
* Response class
|
||||
*
|
||||
* @package http
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Response extends Base
|
||||
{
|
||||
private $httpStatusCode = 200;
|
||||
private $httpHeaders = array();
|
||||
private $httpBody = '';
|
||||
private $responseSent = false;
|
||||
|
||||
/**
|
||||
* Return true if the response have been sent to the user agent
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
*/
|
||||
public function isResponseAlreadySent()
|
||||
{
|
||||
return $this->responseSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set HTTP status code
|
||||
*
|
||||
* @access public
|
||||
* @param integer $statusCode
|
||||
* @return $this
|
||||
*/
|
||||
public function withStatusCode($statusCode)
|
||||
{
|
||||
$this->httpStatusCode = $statusCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set HTTP header
|
||||
*
|
||||
* @access public
|
||||
* @param string $header
|
||||
* @param string $value
|
||||
* @return $this
|
||||
*/
|
||||
public function withHeader($header, $value)
|
||||
{
|
||||
$this->httpHeaders[$header] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content type header
|
||||
*
|
||||
* @access public
|
||||
* @param string $value
|
||||
* @return $this
|
||||
*/
|
||||
public function withContentType($value)
|
||||
{
|
||||
$this->httpHeaders['Content-Type'] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default security headers
|
||||
*
|
||||
* @access public
|
||||
* @return $this
|
||||
*/
|
||||
public function withSecurityHeaders()
|
||||
{
|
||||
$this->httpHeaders['X-Content-Type-Options'] = 'nosniff';
|
||||
$this->httpHeaders['X-XSS-Protection'] = '1; mode=block';
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set header Content-Security-Policy
|
||||
*
|
||||
* @access public
|
||||
* @param array $policies
|
||||
* @return $this
|
||||
*/
|
||||
public function withContentSecurityPolicy(array $policies = array())
|
||||
{
|
||||
$values = '';
|
||||
|
||||
foreach ($policies as $policy => $acl) {
|
||||
$values .= $policy.' '.trim($acl).'; ';
|
||||
}
|
||||
|
||||
$this->withHeader('Content-Security-Policy', $values);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set header X-Frame-Options
|
||||
*
|
||||
* @access public
|
||||
* @return $this
|
||||
*/
|
||||
public function withXframe()
|
||||
{
|
||||
$this->withHeader('X-Frame-Options', 'DENY');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param string $body
|
||||
* @return $this
|
||||
*/
|
||||
public function withBody($body)
|
||||
{
|
||||
$this->httpBody = $body;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send headers to cache a resource
|
||||
*
|
||||
* @access public
|
||||
* @param integer $duration
|
||||
* @param string $etag
|
||||
* @return $this
|
||||
*/
|
||||
public function withCache($duration, $etag = '')
|
||||
{
|
||||
$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 no cache headers
|
||||
*
|
||||
* @access public
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutCache()
|
||||
{
|
||||
$this->withHeader('Pragma', 'no-cache');
|
||||
$this->withHeader('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the browser to download an attachment
|
||||
*
|
||||
* @access public
|
||||
* @param string $filename
|
||||
* @return $this
|
||||
*/
|
||||
public function withFileDownload($filename)
|
||||
{
|
||||
$this->withHeader('Content-Disposition', 'attachment; filename="'.$filename.'"');
|
||||
$this->withHeader('Content-Transfer-Encoding', 'binary');
|
||||
$this->withHeader('Content-Type', 'application/octet-stream');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send headers and body
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
$this->responseSent = true;
|
||||
|
||||
if ($this->httpStatusCode !== 200) {
|
||||
header('Status: '.$this->httpStatusCode);
|
||||
header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$this->httpStatusCode);
|
||||
}
|
||||
|
||||
foreach ($this->httpHeaders as $header => $value) {
|
||||
header($header.': '.$value);
|
||||
}
|
||||
|
||||
if (! empty($this->httpBody)) {
|
||||
echo $this->httpBody;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a custom HTTP status code
|
||||
*
|
||||
* @access public
|
||||
* @param integer $statusCode
|
||||
*/
|
||||
public function status($statusCode)
|
||||
{
|
||||
$this->withStatusCode($statusCode);
|
||||
$this->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
$this->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTML response
|
||||
*
|
||||
* @access public
|
||||
* @param string $data
|
||||
* @param integer $statusCode
|
||||
*/
|
||||
public function html($data, $statusCode = 200)
|
||||
{
|
||||
$this->withStatusCode($statusCode);
|
||||
$this->withContentType('text/html; charset=utf-8');
|
||||
$this->withBody($data);
|
||||
$this->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a text response
|
||||
*
|
||||
* @access public
|
||||
* @param string $data
|
||||
* @param integer $statusCode
|
||||
*/
|
||||
public function text($data, $statusCode = 200)
|
||||
{
|
||||
$this->withStatusCode($statusCode);
|
||||
$this->withContentType('text/plain; charset=utf-8');
|
||||
$this->withBody($data);
|
||||
$this->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a CSV response
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Data to serialize in csv
|
||||
* @param bool $addBOM Add BOM header
|
||||
*/
|
||||
public function csv(array $data, $addBOM = false)
|
||||
{
|
||||
$this->withoutCache();
|
||||
$this->withContentType('text/csv; charset=utf-8');
|
||||
$this->send();
|
||||
Csv::output($data, $addBOM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 a javascript response
|
||||
*
|
||||
* @access public
|
||||
* @param string $data
|
||||
* @param integer $statusCode
|
||||
*/
|
||||
public function js($data, $statusCode = 200)
|
||||
{
|
||||
$this->withStatusCode($statusCode);
|
||||
$this->withContentType('text/javascript; charset=utf-8');
|
||||
$this->withBody($data);
|
||||
$this->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a css response
|
||||
*
|
||||
* @access public
|
||||
* @param string $data
|
||||
* @param integer $statusCode
|
||||
*/
|
||||
public function css($data, $statusCode = 200)
|
||||
{
|
||||
$this->withStatusCode($statusCode);
|
||||
$this->withContentType('text/css; charset=utf-8');
|
||||
$this->withBody($data);
|
||||
$this->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a binary response
|
||||
*
|
||||
* @access public
|
||||
* @param string $data
|
||||
* @param integer $statusCode
|
||||
*/
|
||||
public function binary($data, $statusCode = 200)
|
||||
{
|
||||
$this->withStatusCode($statusCode);
|
||||
$this->withoutCache();
|
||||
$this->withHeader('Content-Transfer-Encoding', 'binary');
|
||||
$this->withContentType('application/octet-stream');
|
||||
$this->withBody($data);
|
||||
$this->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a iCal response
|
||||
*
|
||||
* @access public
|
||||
* @param string $data
|
||||
* @param integer $statusCode
|
||||
*/
|
||||
public function ical($data, $statusCode = 200)
|
||||
{
|
||||
$this->withStatusCode($statusCode);
|
||||
$this->withContentType('text/calendar; charset=utf-8');
|
||||
$this->withBody($data);
|
||||
$this->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PDF response
|
||||
*
|
||||
* @access public
|
||||
* @param string $data
|
||||
* @param integer $statusCode
|
||||
* @param string $fileName
|
||||
*/
|
||||
public function pdf($data, int $statusCode = 200, string $fileName = "")
|
||||
{
|
||||
$this->withStatusCode($statusCode);
|
||||
$this->withContentType('application/pdf');
|
||||
|
||||
if (!empty($fileName)) {
|
||||
$this->httpHeaders["content-disposition"] = "attachment; filename=".$fileName;
|
||||
}
|
||||
|
||||
$this->withBody($data);
|
||||
$this->send();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Http;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Route Handler
|
||||
*
|
||||
* @package http
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Route extends Base
|
||||
{
|
||||
/**
|
||||
* Flag that enable the routing table
|
||||
*
|
||||
* @access private
|
||||
* @var boolean
|
||||
*/
|
||||
private $activated = false;
|
||||
|
||||
/**
|
||||
* Store routes for path lookup
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $paths = array();
|
||||
|
||||
/**
|
||||
* Store routes for url lookup
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $urls = array();
|
||||
|
||||
/**
|
||||
* Enable routing table
|
||||
*
|
||||
* @access public
|
||||
* @return Route
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
$this->activated = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add route
|
||||
*
|
||||
* @access public
|
||||
* @param string $path
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @param string $plugin
|
||||
* @return Route
|
||||
*/
|
||||
public function addRoute($path, $controller, $action, $plugin = '')
|
||||
{
|
||||
if ($this->activated) {
|
||||
$path = ltrim($path, '/');
|
||||
$items = explode('/', $path);
|
||||
$params = $this->findParams($items);
|
||||
|
||||
$this->paths[] = array(
|
||||
'items' => $items,
|
||||
'count' => count($items),
|
||||
'controller' => $controller,
|
||||
'action' => $action,
|
||||
'plugin' => $plugin,
|
||||
);
|
||||
|
||||
$this->urls[$plugin][$controller][$action][] = array(
|
||||
'path' => $path,
|
||||
'params' => $params,
|
||||
'count' => count($params),
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a route according to the given path
|
||||
*
|
||||
* @access public
|
||||
* @param string $path
|
||||
* @return array
|
||||
*/
|
||||
public function findRoute($path)
|
||||
{
|
||||
$items = explode('/', ltrim($path, '/'));
|
||||
$count = count($items);
|
||||
|
||||
foreach ($this->paths as $route) {
|
||||
if ($count === $route['count']) {
|
||||
$params = array();
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
if ($route['items'][$i][0] === ':') {
|
||||
$params[substr($route['items'][$i], 1)] = urldecode($items[$i]);
|
||||
} elseif ($route['items'][$i] !== $items[$i]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($i === $count) {
|
||||
$this->request->setParams($params);
|
||||
return array(
|
||||
'controller' => $route['controller'],
|
||||
'action' => $route['action'],
|
||||
'plugin' => $route['plugin'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'controller' => 'DashboardController',
|
||||
'action' => 'show',
|
||||
'plugin' => '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find route url
|
||||
*
|
||||
* @access public
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @param array $params
|
||||
* @param string $plugin
|
||||
* @return string
|
||||
*/
|
||||
public function findUrl($controller, $action, array $params = array(), $plugin = '')
|
||||
{
|
||||
if ($plugin === '' && isset($params['plugin'])) {
|
||||
$plugin = $params['plugin'];
|
||||
unset($params['plugin']);
|
||||
}
|
||||
|
||||
if (! isset($this->urls[$plugin][$controller][$action])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach ($this->urls[$plugin][$controller][$action] as $route) {
|
||||
if (array_diff_key($params, $route['params']) === array()) {
|
||||
$url = $route['path'];
|
||||
$i = 0;
|
||||
|
||||
foreach ($params as $variable => $value) {
|
||||
$value = urlencode($value);
|
||||
$url = str_replace(':'.$variable, $value, $url);
|
||||
$i++;
|
||||
}
|
||||
|
||||
if ($i === $route['count']) {
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Find url params
|
||||
*
|
||||
* @access public
|
||||
* @param array $items
|
||||
* @return array
|
||||
*/
|
||||
public function findParams(array $items)
|
||||
{
|
||||
$params = array();
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item !== '' && $item[0] === ':') {
|
||||
$params[substr($item, 1)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Http;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Route Dispatcher
|
||||
*
|
||||
* @package http
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Router extends Base
|
||||
{
|
||||
const DEFAULT_CONTROLLER = 'DashboardController';
|
||||
const DEFAULT_METHOD = 'show';
|
||||
|
||||
/**
|
||||
* Plugin name
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $currentPluginName = '';
|
||||
|
||||
/**
|
||||
* Controller
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $currentControllerName = '';
|
||||
|
||||
/**
|
||||
* Action
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $currentActionName = '';
|
||||
|
||||
/**
|
||||
* Get plugin name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPlugin()
|
||||
{
|
||||
return $this->currentPluginName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controller
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getController()
|
||||
{
|
||||
return $this->currentControllerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAction()
|
||||
{
|
||||
return $this->currentActionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to compare patterns
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
$path = substr($this->request->getUri(), strlen($this->helper->url->dir()));
|
||||
|
||||
if ($this->request->getQueryString() !== '') {
|
||||
$path = substr($path, 0, - strlen($this->request->getQueryString()) - 1);
|
||||
}
|
||||
|
||||
if ($path !== '' && $path[0] === '/') {
|
||||
$path = substr($path, 1);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find controller/action from the route table or from get arguments
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function dispatch()
|
||||
{
|
||||
$controller = $this->request->getStringParam('controller');
|
||||
$action = $this->request->getStringParam('action');
|
||||
$plugin = $this->request->getStringParam('plugin');
|
||||
|
||||
if ($controller === '') {
|
||||
$route = $this->route->findRoute($this->getPath());
|
||||
$controller = $route['controller'];
|
||||
$action = $route['action'];
|
||||
$plugin = $route['plugin'];
|
||||
}
|
||||
|
||||
$this->currentControllerName = ucfirst($this->sanitize($controller, self::DEFAULT_CONTROLLER));
|
||||
$this->currentActionName = $this->sanitize($action, self::DEFAULT_METHOD);
|
||||
$this->currentPluginName = ucfirst($this->sanitize($plugin));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check controller and action parameter
|
||||
*
|
||||
* @access public
|
||||
* @param string $value
|
||||
* @param string $default
|
||||
* @return string
|
||||
*/
|
||||
public function sanitize($value, $default = '')
|
||||
{
|
||||
return preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $value : $default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Ldap;
|
||||
|
||||
use LogicException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* LDAP Client
|
||||
*
|
||||
* @package ldap
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Client
|
||||
{
|
||||
/**
|
||||
* LDAP resource
|
||||
*
|
||||
* @access protected
|
||||
* @var resource
|
||||
*/
|
||||
protected $ldap;
|
||||
|
||||
/**
|
||||
* Logger instance
|
||||
*
|
||||
* @access private
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* Establish LDAP connection
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return Client
|
||||
*/
|
||||
public static function connect($username = null, $password = null)
|
||||
{
|
||||
$client = new static;
|
||||
$client->open($client->getLdapServer());
|
||||
$username = $username ?: $client->getLdapUsername();
|
||||
$password = $password ?: $client->getLdapPassword();
|
||||
|
||||
if (empty($username) && empty($password)) {
|
||||
$client->useAnonymousAuthentication();
|
||||
} else {
|
||||
$client->authenticate($username, $password);
|
||||
}
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server connection
|
||||
*
|
||||
* @access public
|
||||
* @return resource
|
||||
*/
|
||||
public function getConnection()
|
||||
{
|
||||
return $this->ldap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish server connection
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param string $server LDAP server URI (ldap[s]://hostname:port) or hostname (deprecated)
|
||||
* @param int $port LDAP port (deprecated)
|
||||
* @param bool $tls Start TLS
|
||||
* @param bool $verify Skip SSL certificate verification
|
||||
* @return Client
|
||||
* @throws ClientException
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function open($server, $port = LDAP_PORT, $tls = LDAP_START_TLS, $verify = LDAP_SSL_VERIFY)
|
||||
{
|
||||
if (! function_exists('ldap_connect')) {
|
||||
throw new ClientException('LDAP: The PHP LDAP extension is required');
|
||||
}
|
||||
|
||||
if (! $verify) {
|
||||
putenv('LDAPTLS_REQCERT=never');
|
||||
}
|
||||
|
||||
if (filter_var($server, FILTER_VALIDATE_URL) !== false) {
|
||||
$this->ldap = @ldap_connect($server);
|
||||
} else {
|
||||
$this->ldap = @ldap_connect($server, $port);
|
||||
}
|
||||
|
||||
if ($this->ldap === false) {
|
||||
throw new ConnectionException('Malformed LDAP server hostname or LDAP server port');
|
||||
}
|
||||
|
||||
ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
|
||||
ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, 0);
|
||||
ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, 1);
|
||||
ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, 1);
|
||||
|
||||
if ($tls && ! @ldap_start_tls($this->ldap)) {
|
||||
throw new ConnectionException('Unable to start LDAP TLS (' . $this->getLdapError() . ')');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymous authentication
|
||||
*
|
||||
* @access public
|
||||
* @throws ClientException
|
||||
* @return boolean
|
||||
*/
|
||||
public function useAnonymousAuthentication()
|
||||
{
|
||||
if (! @ldap_bind($this->ldap)) {
|
||||
$this->checkForServerConnectionError();
|
||||
throw new ClientException('Unable to perform anonymous binding => '.$this->getLdapError());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication with username/password
|
||||
*
|
||||
* @access public
|
||||
* @throws ClientException
|
||||
* @param string $bind_rdn
|
||||
* @param string $bind_password
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate($bind_rdn, $bind_password)
|
||||
{
|
||||
if (! @ldap_bind($this->ldap, $bind_rdn, $bind_password)) {
|
||||
$this->checkForServerConnectionError();
|
||||
throw new ClientException('LDAP authentication failure for "'.$bind_rdn.'" => '.$this->getLdapError());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP server name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapServer()
|
||||
{
|
||||
if (! LDAP_SERVER) {
|
||||
throw new LogicException('LDAP server not configured, check the parameter LDAP_SERVER');
|
||||
}
|
||||
|
||||
return LDAP_SERVER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP username (proxy auth)
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapUsername()
|
||||
{
|
||||
return LDAP_USERNAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP password (proxy auth)
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapPassword()
|
||||
{
|
||||
return LDAP_PASSWORD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set logger
|
||||
*
|
||||
* @access public
|
||||
* @param LoggerInterface $logger
|
||||
* @return Client
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logger
|
||||
*
|
||||
* @access public
|
||||
* @return LoggerInterface
|
||||
*/
|
||||
public function getLogger()
|
||||
{
|
||||
return $this->logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a logger is defined
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasLogger()
|
||||
{
|
||||
return $this->logger !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise ConnectionException if the application is not able to connect to LDAP server
|
||||
*
|
||||
* @access protected
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
protected function checkForServerConnectionError()
|
||||
{
|
||||
if (ldap_errno($this->ldap) === -1) {
|
||||
throw new ConnectionException('Unable to connect to LDAP server (' . $this->getLdapError() . ')');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extended LDAP error message
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getLdapError()
|
||||
{
|
||||
ldap_get_option($this->ldap, LDAP_OPT_ERROR_STRING, $extendedErrorMessage);
|
||||
$errorMessage = ldap_error($this->ldap);
|
||||
$errorCode = ldap_errno($this->ldap);
|
||||
|
||||
return 'Code="'.$errorCode.'"; Error="'.$errorMessage.'"; ExtendedError="'.$extendedErrorMessage.'"';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Ldap;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* LDAP Client Exception
|
||||
*
|
||||
* @package ldap
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ClientException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Ldap;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* LDAP Connection Exception
|
||||
*
|
||||
* @package ldap
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ConnectionException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Ldap;
|
||||
|
||||
/**
|
||||
* LDAP Entries
|
||||
*
|
||||
* @package ldap
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Entries
|
||||
{
|
||||
/**
|
||||
* LDAP entries
|
||||
*
|
||||
* @access protected
|
||||
* @var array
|
||||
*/
|
||||
protected $entries = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param array $entries
|
||||
*/
|
||||
public function __construct(array $entries)
|
||||
{
|
||||
$this->entries = $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entries
|
||||
*
|
||||
* @access public
|
||||
* @return Entry[]
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
$entities = array();
|
||||
|
||||
if (! isset($this->entries['count'])) {
|
||||
return $entities;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $this->entries['count']; $i++) {
|
||||
$entities[] = new Entry($this->entries[$i]);
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first entry
|
||||
*
|
||||
* @access public
|
||||
* @return Entry
|
||||
*/
|
||||
public function getFirstEntry()
|
||||
{
|
||||
return new Entry(isset($this->entries[0]) ? $this->entries[0] : array());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Ldap;
|
||||
|
||||
/**
|
||||
* LDAP Entry
|
||||
*
|
||||
* @package ldap
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Entry
|
||||
{
|
||||
/**
|
||||
* LDAP entry
|
||||
*
|
||||
* @access protected
|
||||
* @var array
|
||||
*/
|
||||
protected $entry = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param array $entry
|
||||
*/
|
||||
public function __construct(array $entry)
|
||||
{
|
||||
$this->entry = $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attribute values
|
||||
*
|
||||
* @access public
|
||||
* @param string $attribute
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAll($attribute)
|
||||
{
|
||||
$attributes = array();
|
||||
|
||||
if ($attribute === null) {
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
if (! isset($this->entry[$attribute]['count'])) {
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $this->entry[$attribute]['count']; $i++) {
|
||||
$attributes[] = $this->entry[$attribute][$i];
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first attribute value
|
||||
*
|
||||
* @access public
|
||||
* @param string $attribute
|
||||
* @param string $default
|
||||
* @return string
|
||||
*/
|
||||
public function getFirstValue($attribute, $default = '')
|
||||
{
|
||||
if ($attribute === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return isset($this->entry[$attribute][0]) ? $this->entry[$attribute][0] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entry distinguished name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getDn()
|
||||
{
|
||||
return isset($this->entry['dn']) ? $this->entry['dn'] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given value exists in attribute list
|
||||
*
|
||||
* @access public
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasValue($attribute, $value)
|
||||
{
|
||||
$attributes = $this->getAll($attribute);
|
||||
return in_array($value, $attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Ldap;
|
||||
|
||||
use LogicException;
|
||||
use Kanboard\Group\LdapGroupProvider;
|
||||
|
||||
/**
|
||||
* LDAP Group Finder
|
||||
*
|
||||
* @package ldap
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Group
|
||||
{
|
||||
/**
|
||||
* Query
|
||||
*
|
||||
* @access protected
|
||||
* @var Query
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param Query $query
|
||||
*/
|
||||
public function __construct(Query $query)
|
||||
{
|
||||
$this->query = $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get groups
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @param Client $client
|
||||
* @param string $query
|
||||
* @return LdapGroupProvider[]
|
||||
*/
|
||||
public static function getGroups(Client $client, $query)
|
||||
{
|
||||
$self = new static(new Query($client));
|
||||
return $self->find($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find groups
|
||||
*
|
||||
* @access public
|
||||
* @param string $query
|
||||
* @return array
|
||||
*/
|
||||
public function find($query)
|
||||
{
|
||||
$this->query->execute($this->getBaseDn(), $query, $this->getAttributes());
|
||||
$groups = array();
|
||||
|
||||
if ($this->query->hasResult()) {
|
||||
$groups = $this->build();
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build groups list
|
||||
*
|
||||
* @access protected
|
||||
* @return array
|
||||
*/
|
||||
protected function build()
|
||||
{
|
||||
$groups = array();
|
||||
|
||||
foreach ($this->query->getEntries()->getAll() as $entry) {
|
||||
$groups[] = new LdapGroupProvider($entry->getDn(), $entry->getFirstValue($this->getAttributeName()));
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ge the list of attributes to fetch when reading the LDAP group entry
|
||||
*
|
||||
* Must returns array with index that start at 0 otherwise ldap_search returns a warning "Array initialization wrong"
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getAttributes()
|
||||
{
|
||||
return array_values(array_filter(array(
|
||||
$this->getAttributeName(),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP group name attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAttributeName()
|
||||
{
|
||||
if (! LDAP_GROUP_ATTRIBUTE_NAME) {
|
||||
throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_GROUP_ATTRIBUTE_NAME');
|
||||
}
|
||||
|
||||
return strtolower(LDAP_GROUP_ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP group base DN
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getBaseDn()
|
||||
{
|
||||
if (! LDAP_GROUP_BASE_DN) {
|
||||
throw new LogicException('LDAP group base DN empty, check the parameter LDAP_GROUP_BASE_DN');
|
||||
}
|
||||
|
||||
return LDAP_GROUP_BASE_DN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Ldap;
|
||||
|
||||
/**
|
||||
* LDAP Query
|
||||
*
|
||||
* @package ldap
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Query
|
||||
{
|
||||
/**
|
||||
* LDAP client
|
||||
*
|
||||
* @access protected
|
||||
* @var Client
|
||||
*/
|
||||
protected $client = null;
|
||||
|
||||
/**
|
||||
* Query result
|
||||
*
|
||||
* @access protected
|
||||
* @var array
|
||||
*/
|
||||
protected $entries = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param Client $client
|
||||
*/
|
||||
public function __construct(Client $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute query
|
||||
*
|
||||
* @access public
|
||||
* @param string $baseDn
|
||||
* @param string $filter
|
||||
* @param array $attributes
|
||||
* @param integer $limit
|
||||
* @return $this
|
||||
*/
|
||||
public function execute($baseDn, $filter, array $attributes, $limit = 0)
|
||||
{
|
||||
if (DEBUG && $this->client->hasLogger()) {
|
||||
$this->client->getLogger()->debug('BaseDN='.$baseDn);
|
||||
$this->client->getLogger()->debug('Filter='.$filter);
|
||||
$this->client->getLogger()->debug('Attributes='.implode(', ', $attributes));
|
||||
}
|
||||
|
||||
$sr = @ldap_search($this->client->getConnection(), $baseDn, $filter, $attributes, null, $limit);
|
||||
if ($sr === false) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$entries = ldap_get_entries($this->client->getConnection(), $sr);
|
||||
if ($entries === false || count($entries) === 0 || $entries['count'] == 0) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->entries = $entries;
|
||||
|
||||
if (DEBUG && $this->client->hasLogger()) {
|
||||
$this->client->getLogger()->debug('NbEntries='.$entries['count']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the query returned a result
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasResult()
|
||||
{
|
||||
return ! empty($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP Entries
|
||||
*
|
||||
* @access public
|
||||
* @return Entries
|
||||
*/
|
||||
public function getEntries()
|
||||
{
|
||||
return new Entries($this->entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Ldap;
|
||||
|
||||
use LogicException;
|
||||
use Kanboard\Core\Security\Role;
|
||||
use Kanboard\User\LdapUserProvider;
|
||||
|
||||
/**
|
||||
* LDAP User Finder
|
||||
*
|
||||
* @package ldap
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class User
|
||||
{
|
||||
/**
|
||||
* Query
|
||||
*
|
||||
* @access protected
|
||||
* @var Query
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* LDAP Group object
|
||||
*
|
||||
* @access protected
|
||||
* @var Group
|
||||
*/
|
||||
protected $group;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param Query $query
|
||||
* @param Group $group
|
||||
*/
|
||||
public function __construct(Query $query, ?Group $group = null)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->group = $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @param Client $client
|
||||
* @param string $username
|
||||
* @return LdapUserProvider
|
||||
*/
|
||||
public static function getUser(Client $client, $username)
|
||||
{
|
||||
$self = new static(new Query($client), new Group(new Query($client)));
|
||||
return $self->find($self->getLdapUserPattern($username));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user
|
||||
*
|
||||
* @access public
|
||||
* @param string $query
|
||||
* @return LdapUserProvider
|
||||
*/
|
||||
public function find($query)
|
||||
{
|
||||
$this->query->execute($this->getBaseDn(), $query, $this->getAttributes());
|
||||
$user = null;
|
||||
|
||||
if ($this->query->hasResult()) {
|
||||
$user = $this->build();
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user groupIds (DN)
|
||||
*
|
||||
* 1) If configured, use memberUid and posixGroup
|
||||
* 2) Otherwise, use memberOf
|
||||
*
|
||||
* @access protected
|
||||
* @param Entry $entry
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getGroups(Entry $entry)
|
||||
{
|
||||
$userattr = '';
|
||||
if ('username' == $this->getGroupUserAttribute()) {
|
||||
$userattr = $entry->getFirstValue($this->getAttributeUsername());
|
||||
} elseif ('dn' == $this->getGroupUserAttribute()) {
|
||||
$userattr = $entry->getDn();
|
||||
}
|
||||
$groupIds = array();
|
||||
|
||||
if (! empty($userattr) && $this->group !== null && $this->hasGroupUserFilter()) {
|
||||
$escapedUserAttribute = ldap_escape($userattr, '', LDAP_ESCAPE_FILTER);
|
||||
$groups = $this->group->find(sprintf($this->getGroupUserFilter(), $escapedUserAttribute));
|
||||
|
||||
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->hasGroupsConfigured()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (LDAP_USER_DEFAULT_ROLE_MANAGER) {
|
||||
$role = Role::APP_MANAGER;
|
||||
} else {
|
||||
$role = Role::APP_USER;
|
||||
}
|
||||
|
||||
foreach ($groupIds as $groupId) {
|
||||
$groupId = strtolower($groupId);
|
||||
|
||||
if ($groupId === strtolower($this->getGroupAdminDn())) {
|
||||
$role = Role::APP_ADMIN;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($groupId === strtolower($this->getGroupManagerDn())) {
|
||||
$role = Role::APP_MANAGER;
|
||||
}
|
||||
}
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build user profile
|
||||
*
|
||||
* @access protected
|
||||
* @return LdapUserProvider
|
||||
*/
|
||||
protected function build()
|
||||
{
|
||||
$entry = $this->query->getEntries()->getFirstEntry();
|
||||
$groupIds = $this->getGroups($entry);
|
||||
|
||||
return new LdapUserProvider(
|
||||
$entry->getDn(),
|
||||
$entry->getFirstValue($this->getAttributeUsername()),
|
||||
$entry->getFirstValue($this->getAttributeName()),
|
||||
$entry->getFirstValue($this->getAttributeEmail()),
|
||||
$this->getRole($groupIds),
|
||||
$groupIds,
|
||||
$entry->getFirstValue($this->getAttributePhoto()),
|
||||
$entry->getFirstValue($this->getAttributeLanguage())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ge the list of attributes to fetch when reading the LDAP user entry
|
||||
*
|
||||
* Must returns array with index that start at 0 otherwise ldap_search returns a warning "Array initialization wrong"
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getAttributes()
|
||||
{
|
||||
return array_values(array_filter(array(
|
||||
$this->getAttributeUsername(),
|
||||
$this->getAttributeName(),
|
||||
$this->getAttributeEmail(),
|
||||
$this->getAttributeGroup(),
|
||||
$this->getAttributePhoto(),
|
||||
$this->getAttributeLanguage(),
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP account id attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAttributeUsername()
|
||||
{
|
||||
if (! LDAP_USER_ATTRIBUTE_USERNAME) {
|
||||
throw new LogicException('LDAP username attribute empty, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
|
||||
}
|
||||
|
||||
return strtolower(LDAP_USER_ATTRIBUTE_USERNAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP user name attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAttributeName()
|
||||
{
|
||||
if (! LDAP_USER_ATTRIBUTE_FULLNAME) {
|
||||
throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_USER_ATTRIBUTE_FULLNAME');
|
||||
}
|
||||
|
||||
return strtolower(LDAP_USER_ATTRIBUTE_FULLNAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP account email attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAttributeEmail()
|
||||
{
|
||||
if (! LDAP_USER_ATTRIBUTE_EMAIL) {
|
||||
throw new LogicException('LDAP email attribute empty, check the parameter LDAP_USER_ATTRIBUTE_EMAIL');
|
||||
}
|
||||
|
||||
return strtolower(LDAP_USER_ATTRIBUTE_EMAIL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP account memberOf attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAttributeGroup()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP Group User attribute
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getGroupUserAttribute()
|
||||
{
|
||||
return LDAP_GROUP_USER_ATTRIBUTE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 are configured
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasGroupsConfigured()
|
||||
{
|
||||
return $this->getGroupAdminDn() || $this->getGroupManagerDn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP admin group DN
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getGroupAdminDn(): string
|
||||
{
|
||||
return strtolower(LDAP_GROUP_ADMIN_DN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP application manager group DN
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getGroupManagerDn(): string
|
||||
{
|
||||
return LDAP_GROUP_MANAGER_DN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP user base DN
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getBaseDn()
|
||||
{
|
||||
return LDAP_USER_BASE_DN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LDAP user pattern
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @param string $filter
|
||||
* @return string
|
||||
*/
|
||||
public function getLdapUserPattern($username, $filter = LDAP_USER_FILTER)
|
||||
{
|
||||
if (! $filter) {
|
||||
throw new LogicException('LDAP user filter is not configured. Please set the LDAP_USER_FILTER parameter in your configuration file');
|
||||
}
|
||||
|
||||
$escapedUsername = ldap_escape($username, '', LDAP_ESCAPE_FILTER);
|
||||
return str_replace('%s', $escapedUsername, $filter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Log;
|
||||
|
||||
use Psr\Log\AbstractLogger;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Base class for loggers
|
||||
*
|
||||
* @package Kanboard\Core\Log
|
||||
* @author Frédéric Guillot
|
||||
*/
|
||||
abstract class Base extends AbstractLogger
|
||||
{
|
||||
/**
|
||||
* Minimum log level for the logger
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $level = LogLevel::DEBUG;
|
||||
|
||||
/**
|
||||
* Set minimum log level
|
||||
*
|
||||
* @access public
|
||||
* @param string $level
|
||||
*/
|
||||
public function setLevel($level)
|
||||
{
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum log level
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLevel()
|
||||
{
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump to log a variable (by example an array)
|
||||
*
|
||||
* @param mixed $variable
|
||||
*/
|
||||
public function dump($variable)
|
||||
{
|
||||
$this->log(LogLevel::DEBUG, var_export($variable, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates context values into the message placeholders.
|
||||
*
|
||||
* @access protected
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return string
|
||||
*/
|
||||
protected function interpolate($message, array $context = array())
|
||||
{
|
||||
// build a replacement array with braces around the context keys
|
||||
$replace = array();
|
||||
|
||||
foreach ($context as $key => $val) {
|
||||
$replace['{' . $key . '}'] = $val;
|
||||
}
|
||||
|
||||
// interpolate replacement values into the message and return
|
||||
return strtr($message, $replace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format log message
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return string
|
||||
*/
|
||||
protected function formatMessage($level, $message, array $context = array())
|
||||
{
|
||||
return '['.date('Y-m-d H:i:s').'] ['.$level.'] '.$this->interpolate($message, $context).PHP_EOL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Log;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* File Logger
|
||||
*
|
||||
* @package Kanboard\Core\Log
|
||||
* @author Frédéric Guillot
|
||||
*/
|
||||
class File extends Base
|
||||
{
|
||||
/**
|
||||
* Filename
|
||||
*
|
||||
* @access protected
|
||||
* @var string
|
||||
*/
|
||||
protected $filename = '';
|
||||
|
||||
/**
|
||||
* Setup logger configuration
|
||||
*
|
||||
* @param string $filename Output file
|
||||
*/
|
||||
public function __construct($filename)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function log($level, $message, array $context = array())
|
||||
{
|
||||
$line = $this->formatMessage($level, $message, $context);
|
||||
|
||||
if (file_put_contents($this->filename, $line, FILE_APPEND | LOCK_EX) === false) {
|
||||
throw new RuntimeException('Unable to write to the log file.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Log;
|
||||
|
||||
use Psr\Log\AbstractLogger;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Handler for multiple loggers
|
||||
*
|
||||
* @package Kanboard\Core\Log
|
||||
* @author Frédéric Guillot
|
||||
*/
|
||||
class Logger extends AbstractLogger implements LoggerAwareInterface
|
||||
{
|
||||
/**
|
||||
* Logger instances
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private $loggers = array();
|
||||
|
||||
/**
|
||||
* Get level priority
|
||||
*
|
||||
* @param mixed $level
|
||||
* @return integer
|
||||
*/
|
||||
public function getLevelPriority($level)
|
||||
{
|
||||
switch ($level) {
|
||||
case LogLevel::EMERGENCY:
|
||||
return 600;
|
||||
case LogLevel::ALERT:
|
||||
return 550;
|
||||
case LogLevel::CRITICAL:
|
||||
return 500;
|
||||
case LogLevel::ERROR:
|
||||
return 400;
|
||||
case LogLevel::WARNING:
|
||||
return 300;
|
||||
case LogLevel::NOTICE:
|
||||
return 250;
|
||||
case LogLevel::INFO:
|
||||
return 200;
|
||||
}
|
||||
|
||||
return 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a logger instance on the object
|
||||
*
|
||||
* @param LoggerInterface $logger
|
||||
* @return null
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger)
|
||||
{
|
||||
$this->loggers[] = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy method to the real loggers
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function log($level, $message, array $context = array())
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
if ($this->getLevelPriority($level) >= $this->getLevelPriority($logger->getLevel())) {
|
||||
$logger->log($level, $message, $context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump variables for debugging
|
||||
*
|
||||
* @param mixed $variable
|
||||
*/
|
||||
public function dump($variable)
|
||||
{
|
||||
foreach ($this->loggers as $logger) {
|
||||
if ($this->getLevelPriority(LogLevel::DEBUG) >= $this->getLevelPriority($logger->getLevel())) {
|
||||
$logger->dump($variable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Log;
|
||||
|
||||
/**
|
||||
* Stderr logger
|
||||
*
|
||||
* @package Kanboard\Core\Log
|
||||
* @author Frédéric Guillot
|
||||
*/
|
||||
class Stderr extends Base
|
||||
{
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function log($level, $message, array $context = array())
|
||||
{
|
||||
file_put_contents('php://stderr', $this->formatMessage($level, $message, $context), FILE_APPEND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Log;
|
||||
|
||||
/**
|
||||
* Stdout logger
|
||||
*
|
||||
* @package Kanboard\Core\Log
|
||||
* @author Frédéric Guillot
|
||||
*/
|
||||
class Stdout extends Base
|
||||
{
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function log($level, $message, array $context = array())
|
||||
{
|
||||
file_put_contents('php://stdout', $this->formatMessage($level, $message, $context), FILE_APPEND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Log;
|
||||
|
||||
use RuntimeException;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Syslog Logger
|
||||
*
|
||||
* @package Kanboard\Core\Log
|
||||
* @author Frédéric Guillot
|
||||
*/
|
||||
class Syslog extends Base
|
||||
{
|
||||
/**
|
||||
* Setup Syslog configuration
|
||||
*
|
||||
* @param string $ident Application name
|
||||
* @param int $facility See http://php.net/manual/en/function.openlog.php
|
||||
*/
|
||||
public function __construct($ident = 'PHP', $facility = LOG_USER)
|
||||
{
|
||||
if (! openlog($ident, LOG_ODELAY | LOG_PID, $facility)) {
|
||||
throw new RuntimeException('Unable to connect to syslog.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get syslog priority according to Psr\LogLevel
|
||||
*
|
||||
* @param mixed $level
|
||||
* @return integer
|
||||
*/
|
||||
public function getSyslogPriority($level)
|
||||
{
|
||||
switch ($level) {
|
||||
case LogLevel::EMERGENCY:
|
||||
return LOG_EMERG;
|
||||
case LogLevel::ALERT:
|
||||
return LOG_ALERT;
|
||||
case LogLevel::CRITICAL:
|
||||
return LOG_CRIT;
|
||||
case LogLevel::ERROR:
|
||||
return LOG_ERR;
|
||||
case LogLevel::WARNING:
|
||||
return LOG_WARNING;
|
||||
case LogLevel::NOTICE:
|
||||
return LOG_NOTICE;
|
||||
case LogLevel::INFO:
|
||||
return LOG_INFO;
|
||||
}
|
||||
|
||||
return LOG_DEBUG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function log($level, $message, array $context = array())
|
||||
{
|
||||
$syslogPriority = $this->getSyslogPriority($level);
|
||||
$syslogMessage = $this->interpolate($message, $context);
|
||||
|
||||
syslog($syslogPriority, $syslogMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Log;
|
||||
|
||||
/**
|
||||
* Built-in PHP Logger
|
||||
*
|
||||
* @package Kanboard\Core\Log
|
||||
* @author Frédéric Guillot
|
||||
*/
|
||||
class System extends Base
|
||||
{
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function log($level, $message, array $context = [])
|
||||
{
|
||||
error_log('['.$level.'] '.$this->interpolate($message, $context));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Mail;
|
||||
|
||||
use Kanboard\Job\EmailJob;
|
||||
use Pimple\Container;
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Mail Client
|
||||
*
|
||||
* @package Kanboard\Core\Mail
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Client extends Base
|
||||
{
|
||||
/**
|
||||
* Mail transport instances
|
||||
*
|
||||
* @access private
|
||||
* @var \Pimple\Container
|
||||
*/
|
||||
private $transports;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param \Pimple\Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->transports = new Container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $recipientEmail
|
||||
* @param string $recipientName
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @return Client
|
||||
*/
|
||||
public function send($recipientEmail, $recipientName, $subject, $html, $authorName = null, $authorEmail = null)
|
||||
{
|
||||
if (! empty($recipientEmail)) {
|
||||
$this->queueManager->push(EmailJob::getInstance($this->container)->withParams(
|
||||
$recipientEmail,
|
||||
$recipientName,
|
||||
$subject,
|
||||
$html,
|
||||
is_null($authorName) ? $this->getAuthorName() : $authorName,
|
||||
is_null($authorEmail) ? $this->getAuthorEmail() : $authorEmail
|
||||
));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get author name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorName()
|
||||
{
|
||||
$author = 'Kanboard';
|
||||
|
||||
if ($this->userSession->isLogged()) {
|
||||
$author = e('%s via Kanboard', $this->helper->user->getFullname());
|
||||
}
|
||||
|
||||
return $author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get author email
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorEmail()
|
||||
{
|
||||
if ($this->userSession->isLogged()) {
|
||||
$userData = $this->userSession->getAll();
|
||||
return ! empty($userData['email']) ? $userData['email'] : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mail transport instance
|
||||
*
|
||||
* @access public
|
||||
* @param string $transport
|
||||
* @return ClientInterface
|
||||
*/
|
||||
public function getTransport($transport)
|
||||
{
|
||||
return $this->transports[$transport];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new mail transport
|
||||
*
|
||||
* @access public
|
||||
* @param string $transport
|
||||
* @param string $class
|
||||
* @return Client
|
||||
*/
|
||||
public function setTransport($transport, $class)
|
||||
{
|
||||
$container = $this->container;
|
||||
|
||||
$this->transports[$transport] = function () use ($class, $container) {
|
||||
return new $class($container);
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of registered transports
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableTransports()
|
||||
{
|
||||
$availableTransports = $this->transports->keys();
|
||||
return array_combine($availableTransports, $availableTransports);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Mail;
|
||||
|
||||
/**
|
||||
* Mail Client Interface
|
||||
*
|
||||
* @package Kanboard\Core\Mail
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface ClientInterface
|
||||
{
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $recipientEmail
|
||||
* @param string $recipientName
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @param string $authorName
|
||||
* @param string $authorEmail
|
||||
*/
|
||||
public function sendEmail($recipientEmail, $recipientName, $subject, $html, $authorName, $authorEmail = '');
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Mail\Transport;
|
||||
|
||||
use Swift_Message;
|
||||
use Swift_Mailer;
|
||||
use Swift_MailTransport;
|
||||
use Swift_TransportException;
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Core\Mail\ClientInterface;
|
||||
|
||||
/**
|
||||
* PHP Mail Handler
|
||||
*
|
||||
* @package Kanboard\Core\Mail\Transport
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Mail extends Base implements ClientInterface
|
||||
{
|
||||
/**
|
||||
* Send a HTML email
|
||||
*
|
||||
* @access public
|
||||
* @param string $recipientEmail
|
||||
* @param string $recipientName
|
||||
* @param string $subject
|
||||
* @param string $html
|
||||
* @param string $authorName
|
||||
* @param string $authorEmail
|
||||
*/
|
||||
public function sendEmail($recipientEmail, $recipientName, $subject, $html, $authorName, $authorEmail = '')
|
||||
{
|
||||
try {
|
||||
$message = Swift_Message::newInstance()
|
||||
->setSubject($subject)
|
||||
->setFrom($this->helper->mail->getMailSenderAddress(), $authorName)
|
||||
->setTo(array($recipientEmail => $recipientName));
|
||||
|
||||
if (! empty(MAIL_BCC)) {
|
||||
$message->setBcc(MAIL_BCC);
|
||||
}
|
||||
|
||||
$headers = $message->getHeaders();
|
||||
|
||||
// See https://tools.ietf.org/html/rfc3834#section-5
|
||||
$headers->addTextHeader('Auto-Submitted', 'auto-generated');
|
||||
|
||||
if (! empty($authorEmail)) {
|
||||
$message->setReplyTo($authorEmail);
|
||||
}
|
||||
|
||||
$message->setBody($html, 'text/html');
|
||||
|
||||
Swift_Mailer::newInstance($this->getTransport())->send($message);
|
||||
} catch (Swift_TransportException $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SwiftMailer transport
|
||||
*
|
||||
* @access protected
|
||||
* @return \Swift_Transport
|
||||
*/
|
||||
protected function getTransport()
|
||||
{
|
||||
return Swift_MailTransport::newInstance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Mail\Transport;
|
||||
|
||||
use Swift_SendmailTransport;
|
||||
|
||||
/**
|
||||
* PHP Mail Handler
|
||||
*
|
||||
* @package Kanboard\Core\Mail\Transport
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Sendmail extends Mail
|
||||
{
|
||||
/**
|
||||
* Get SwiftMailer transport
|
||||
*
|
||||
* @access protected
|
||||
* @return \Swift_Transport
|
||||
*/
|
||||
protected function getTransport()
|
||||
{
|
||||
return Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Mail\Transport;
|
||||
|
||||
use Swift_SmtpTransport;
|
||||
|
||||
/**
|
||||
* PHP Mail Handler
|
||||
*
|
||||
* @package Kanboard\Core\Mail\Transport
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Smtp extends Mail
|
||||
{
|
||||
/**
|
||||
* Get SwiftMailer transport
|
||||
*
|
||||
* @access protected
|
||||
* @return \Swift_Transport
|
||||
*/
|
||||
protected function getTransport()
|
||||
{
|
||||
$transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT);
|
||||
$transport->setUsername(MAIL_SMTP_USERNAME);
|
||||
$transport->setPassword(MAIL_SMTP_PASSWORD);
|
||||
if (!is_null(MAIL_SMTP_HELO_NAME)) {
|
||||
$transport->setLocalDomain(MAIL_SMTP_HELO_NAME);
|
||||
}
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
use Parsedown;
|
||||
use Pimple\Container;
|
||||
|
||||
/**
|
||||
* Specific Markdown rules for Kanboard
|
||||
*
|
||||
* @package core
|
||||
* @author norcnorc
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Markdown extends Parsedown
|
||||
{
|
||||
/**
|
||||
* Task links generated will use the project token instead
|
||||
*
|
||||
* @access private
|
||||
* @var boolean
|
||||
*/
|
||||
private $isPublicLink = false;
|
||||
|
||||
/**
|
||||
* Container
|
||||
*
|
||||
* @access private
|
||||
* @var Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param Container $container
|
||||
* @param boolean $isPublicLink
|
||||
*/
|
||||
public function __construct(Container $container, $isPublicLink)
|
||||
{
|
||||
$this->isPublicLink = $isPublicLink;
|
||||
$this->container = $container;
|
||||
$this->BlockTypes['#'][0] = 'CustomHeader';
|
||||
$this->InlineTypes['#'][] = 'TaskLink';
|
||||
$this->InlineTypes['@'][] = 'UserLink';
|
||||
$this->inlineMarkerList .= '#@';
|
||||
}
|
||||
|
||||
protected function blockCustomHeader($Line)
|
||||
{
|
||||
if (preg_match('!#(\d+)!i', $Line['text'], $matches)) {
|
||||
$link = $this->buildTaskLink($matches[1]);
|
||||
|
||||
if (! empty($link)) {
|
||||
return [
|
||||
'extent' => strlen($matches[0]),
|
||||
'element' => [
|
||||
'name' => 'a',
|
||||
'text' => $matches[0],
|
||||
'attributes' => ['href' => $link],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->blockHeader($Line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Task Links
|
||||
*
|
||||
* Replace "#123" by a link to the task
|
||||
*
|
||||
* @access public
|
||||
* @param array $Excerpt
|
||||
* @return array|null
|
||||
*/
|
||||
protected function inlineTaskLink(array $Excerpt)
|
||||
{
|
||||
if (preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) {
|
||||
$link = $this->buildTaskLink($matches[1]);
|
||||
|
||||
if (! empty($link)) {
|
||||
return array(
|
||||
'extent' => strlen($matches[0]),
|
||||
'element' => array(
|
||||
'name' => 'a',
|
||||
'text' => $matches[0],
|
||||
'attributes' => array('href' => $link),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle User Mentions
|
||||
*
|
||||
* Replace "@username" by a link to the user
|
||||
*
|
||||
* @access public
|
||||
* @param array $Excerpt
|
||||
* @return array|null
|
||||
*/
|
||||
protected function inlineUserLink(array $Excerpt)
|
||||
{
|
||||
if (! $this->isPublicLink && preg_match('/^@([^\s,!:?]+)/', $Excerpt['text'], $matches)) {
|
||||
$username = rtrim($matches[1], '.');
|
||||
$user = $this->container['userCacheDecorator']->getByUsername($username);
|
||||
|
||||
if (! empty($user)) {
|
||||
$url = $this->container['helper']->url->to('UserViewController', 'profile', array('user_id' => $user['id']));
|
||||
$name = $user['name'] ?: $user['username'];
|
||||
|
||||
return array(
|
||||
'extent' => strlen($username) + 1,
|
||||
'element' => array(
|
||||
'name' => 'a',
|
||||
'text' => '@' . $username,
|
||||
'attributes' => array(
|
||||
'href' => $url,
|
||||
'class' => 'user-mention-link',
|
||||
'title' => $name,
|
||||
'aria-label' => $name,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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->to(
|
||||
'TaskViewController',
|
||||
'readonly',
|
||||
array(
|
||||
'token' => $token,
|
||||
'task_id' => $task_id,
|
||||
),
|
||||
'',
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->container['helper']->url->to(
|
||||
'TaskViewController',
|
||||
'show',
|
||||
array('task_id' => $task_id)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude from nesting task links and user mentions for links
|
||||
*
|
||||
* @param array $Excerpt
|
||||
* @return array|null
|
||||
*/
|
||||
protected function inlineLink($Excerpt)
|
||||
{
|
||||
$Inline = parent::inlineLink($Excerpt);
|
||||
if (is_array($Inline)) {
|
||||
array_push($Inline['element']['nonNestables'], 'TaskLink', 'UserLink');
|
||||
}
|
||||
return $Inline;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ObjectStorage;
|
||||
|
||||
/**
|
||||
* Local File Storage
|
||||
*
|
||||
* @package ObjectStorage
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class FileStorage implements ObjectStorageInterface
|
||||
{
|
||||
/**
|
||||
* Base directory
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $baseDir = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param string $baseDir
|
||||
*/
|
||||
public function __construct($baseDir)
|
||||
{
|
||||
$realBaseDir = realpath($baseDir);
|
||||
|
||||
if ($realBaseDir === false) {
|
||||
throw new ObjectStorageException('Invalid base folder: '.$baseDir);
|
||||
}
|
||||
|
||||
if (! is_dir($realBaseDir)) {
|
||||
throw new ObjectStorageException('Base folder is not a directory: '.$baseDir);
|
||||
}
|
||||
|
||||
$this->baseDir = $realBaseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch object contents
|
||||
*
|
||||
* @access public
|
||||
* @throws ObjectStorageException
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
public function get($key)
|
||||
{
|
||||
return file_get_contents($this->getRealFilePath($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save object
|
||||
*
|
||||
* @access public
|
||||
* @throws ObjectStorageException
|
||||
* @param string $key
|
||||
* @param string $blob
|
||||
*/
|
||||
public function put($key, &$blob)
|
||||
{
|
||||
$filename = $this->getSanitizedFilePath($key);
|
||||
$this->createFolder($key);
|
||||
|
||||
if (file_put_contents($filename, $blob) === false) {
|
||||
throw new ObjectStorageException('Unable to write the file: '.$filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output directly object content
|
||||
*
|
||||
* @access public
|
||||
* @throws ObjectStorageException
|
||||
* @param string $key
|
||||
*/
|
||||
public function output($key)
|
||||
{
|
||||
readfile($this->getRealFilePath($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move local file to object storage
|
||||
*
|
||||
* @access public
|
||||
* @throws ObjectStorageException
|
||||
* @param string $srcFilename
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function moveFile($srcFilename, $key)
|
||||
{
|
||||
if (! file_exists($srcFilename)) {
|
||||
throw new ObjectStorageException('Source file does not exist: '.$srcFilename);
|
||||
}
|
||||
|
||||
$dstFilename = $this->getSanitizedFilePath($key);
|
||||
$this->createFolder($key);
|
||||
|
||||
if (! rename($srcFilename, $dstFilename)) {
|
||||
throw new ObjectStorageException('Unable to move the file: '.$srcFilename.' to '.$dstFilename);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move uploaded file to object storage
|
||||
*
|
||||
* @access public
|
||||
* @param string $srcFilename
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function moveUploadedFile($srcFilename, $key)
|
||||
{
|
||||
if (! file_exists($srcFilename)) {
|
||||
throw new ObjectStorageException('Source file does not exist: '.$srcFilename);
|
||||
}
|
||||
|
||||
$dstFilename = $this->getSanitizedFilePath($key);
|
||||
$this->createFolder($key);
|
||||
|
||||
return move_uploaded_file($srcFilename, $dstFilename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove object
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function remove($key)
|
||||
{
|
||||
$filename = $this->getRealFilePath($key);
|
||||
$result = unlink($filename);
|
||||
|
||||
// Remove parent folder if empty
|
||||
$parentFolder = dirname($filename);
|
||||
$files = glob($parentFolder.DIRECTORY_SEPARATOR.'*');
|
||||
|
||||
if ($files !== false && is_dir($parentFolder) && count($files) === 0) {
|
||||
rmdir($parentFolder);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function createFolder(string $key)
|
||||
{
|
||||
$folder = strpos($key, DIRECTORY_SEPARATOR) !== false ? $this->baseDir.DIRECTORY_SEPARATOR.dirname($key) : $this->baseDir;
|
||||
|
||||
if (! is_dir($folder) && ! mkdir($folder, 0o755, true)) {
|
||||
throw new ObjectStorageException('Unable to create folder: '.$folder);
|
||||
}
|
||||
}
|
||||
|
||||
private function getRealFilePath(string $key): string
|
||||
{
|
||||
$filename = $this->baseDir.DIRECTORY_SEPARATOR.$key;
|
||||
|
||||
// Resolve the real path and make sure the file exists
|
||||
$realFilePath = realpath($filename);
|
||||
if ($realFilePath === false) {
|
||||
throw new ObjectStorageException('Invalid file path: '.$filename);
|
||||
}
|
||||
|
||||
$this->validateBasePath($realFilePath);
|
||||
|
||||
return $realFilePath;
|
||||
}
|
||||
|
||||
public function getSanitizedFilePath(string $key): string
|
||||
{
|
||||
$filename = $this->baseDir.DIRECTORY_SEPARATOR.$key;
|
||||
$sanitizedKey = sanitize_path($filename);
|
||||
|
||||
if ($sanitizedKey === false) {
|
||||
throw new ObjectStorageException('Invalid file path: '.$key);
|
||||
}
|
||||
|
||||
$this->validateBasePath($sanitizedKey);
|
||||
|
||||
return $sanitizedKey;
|
||||
}
|
||||
|
||||
private function validateBasePath(string $filePath)
|
||||
{
|
||||
if (strpos($filePath, $this->baseDir) !== 0) {
|
||||
throw new ObjectStorageException('File '.$filePath.' is not in base directory: '.$this->baseDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ObjectStorage;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ObjectStorageException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\ObjectStorage;
|
||||
|
||||
/**
|
||||
* Object Storage Interface
|
||||
*
|
||||
* @package ObjectStorage
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface ObjectStorageInterface
|
||||
{
|
||||
/**
|
||||
* Fetch object contents
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
public function get($key);
|
||||
|
||||
/**
|
||||
* Save object
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
* @param string $blob
|
||||
*/
|
||||
public function put($key, &$blob);
|
||||
|
||||
/**
|
||||
* Output directly object content
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
*/
|
||||
public function output($key);
|
||||
|
||||
/**
|
||||
* Move local file to object storage
|
||||
*
|
||||
* @access public
|
||||
* @param string $filename
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function moveFile($filename, $key);
|
||||
|
||||
/**
|
||||
* Move uploaded file to object storage
|
||||
*
|
||||
* @access public
|
||||
* @param string $filename
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function moveUploadedFile($filename, $key);
|
||||
|
||||
/**
|
||||
* Remove object
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function remove($key);
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
use Kanboard\Core\Filter\FormatterInterface;
|
||||
use Pimple\Container;
|
||||
use PicoDb\Table;
|
||||
|
||||
/**
|
||||
* Paginator Helper
|
||||
*
|
||||
* @package Kanboard\Core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Paginator
|
||||
{
|
||||
/**
|
||||
* Container instance
|
||||
*
|
||||
* @access private
|
||||
* @var \Pimple\Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* Total number of items
|
||||
*
|
||||
* @access private
|
||||
* @var integer
|
||||
*/
|
||||
private $total = 0;
|
||||
|
||||
/**
|
||||
* Page number
|
||||
*
|
||||
* @access private
|
||||
* @var integer
|
||||
*/
|
||||
private $page = 1;
|
||||
|
||||
/**
|
||||
* Offset
|
||||
*
|
||||
* @access private
|
||||
* @var integer
|
||||
*/
|
||||
private $offset = 0;
|
||||
|
||||
/**
|
||||
* Limit
|
||||
*
|
||||
* @access private
|
||||
* @var integer
|
||||
*/
|
||||
private $limit = 0;
|
||||
|
||||
/**
|
||||
* Sort by this column
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $order = '';
|
||||
|
||||
/**
|
||||
* Sorting direction
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $direction = 'ASC';
|
||||
|
||||
/**
|
||||
* Slice of items
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $items = array();
|
||||
|
||||
/**
|
||||
* PicoDb Table instance
|
||||
*
|
||||
* @access private
|
||||
* @var \Picodb\Table
|
||||
*/
|
||||
private $query = null;
|
||||
|
||||
/**
|
||||
* Controller name
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $controller = '';
|
||||
|
||||
/**
|
||||
* Action name
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $action = '';
|
||||
|
||||
/**
|
||||
* URL anchor
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $anchor = '';
|
||||
|
||||
/**
|
||||
* Url params
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $params = array();
|
||||
|
||||
/**
|
||||
* @var FormatterInterface
|
||||
*/
|
||||
protected $formatter = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param \Pimple\Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a PicoDb query
|
||||
*
|
||||
* @access public
|
||||
* @param \PicoDb\Table
|
||||
* @return $this
|
||||
*/
|
||||
public function setQuery(Table $query)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->total = $this->query->count();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Formatter
|
||||
*
|
||||
* @param FormatterInterface $formatter
|
||||
* @return $this
|
||||
*/
|
||||
public function setFormatter(FormatterInterface $formatter)
|
||||
{
|
||||
$this->formatter = $formatter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a PicoDb query
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function executeQuery()
|
||||
{
|
||||
if ($this->query !== null) {
|
||||
|
||||
$this->query
|
||||
->offset($this->offset)
|
||||
->limit($this->limit);
|
||||
|
||||
if (preg_match('/^[a-zA-Z0-9._]+$/', $this->order)) {
|
||||
$this->query->orderBy($this->order, $this->direction);
|
||||
} else {
|
||||
$this->order = '';
|
||||
}
|
||||
|
||||
if ($this->formatter !== null) {
|
||||
return $this->formatter->withQuery($this->query)->format();
|
||||
} else {
|
||||
return $this->query->findAll();
|
||||
}
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set url parameters
|
||||
*
|
||||
* @access public
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @param array $params
|
||||
* @param string $anchor
|
||||
* @return $this
|
||||
*/
|
||||
public function setUrl($controller, $action, array $params = array(), $anchor = '')
|
||||
{
|
||||
$this->controller = $controller;
|
||||
$this->action = $action;
|
||||
$this->params = $params;
|
||||
$this->anchor = $anchor;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add manually items
|
||||
*
|
||||
* @access public
|
||||
* @param array $items
|
||||
* @return $this
|
||||
*/
|
||||
public function setCollection(array $items)
|
||||
{
|
||||
$this->items = $items;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the items
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getCollection()
|
||||
{
|
||||
return $this->items ?: $this->executeQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the total number of items
|
||||
*
|
||||
* @access public
|
||||
* @param integer $total
|
||||
* @return $this
|
||||
*/
|
||||
public function setTotal($total)
|
||||
{
|
||||
$this->total = $total;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of items
|
||||
*
|
||||
* @access public
|
||||
* @return integer
|
||||
*/
|
||||
public function getTotal()
|
||||
{
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default page number
|
||||
*
|
||||
* @access public
|
||||
* @param integer $page
|
||||
* @return $this
|
||||
*/
|
||||
public function setPage($page)
|
||||
{
|
||||
$this->page = $page;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of current page
|
||||
*
|
||||
* @access public
|
||||
* @return integer
|
||||
*/
|
||||
public function getPage()
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default column order
|
||||
*
|
||||
* @access public
|
||||
* @param string $order
|
||||
* @return $this
|
||||
*/
|
||||
public function setOrder($order)
|
||||
{
|
||||
$this->order = $order;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default sorting direction
|
||||
*
|
||||
* @access public
|
||||
* @param string $direction
|
||||
* @return $this
|
||||
*/
|
||||
public function setDirection($direction)
|
||||
{
|
||||
$this->direction = $direction;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of items per page
|
||||
*
|
||||
* @access public
|
||||
* @param integer $limit
|
||||
* @return $this
|
||||
*/
|
||||
public function setMax($limit)
|
||||
{
|
||||
$this->limit = $limit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return boolean
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
return $this->total === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the offset calculation only if the $condition is true
|
||||
*
|
||||
* @access public
|
||||
* @param boolean $condition
|
||||
* @return $this
|
||||
*/
|
||||
public function calculateOnlyIf($condition)
|
||||
{
|
||||
if ($condition) {
|
||||
$this->calculate();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the offset value according to url params and the page number
|
||||
*
|
||||
* @access public
|
||||
* @return $this
|
||||
*/
|
||||
public function calculate()
|
||||
{
|
||||
$this->page = $this->container['request']->getIntegerParam('page', 1);
|
||||
$this->direction = $this->container['request']->getStringParam('direction', $this->direction);
|
||||
$this->order = $this->container['request']->getStringParam('order', $this->order);
|
||||
|
||||
if ($this->page < 1) {
|
||||
$this->page = 1;
|
||||
}
|
||||
|
||||
$this->offset = (int) (($this->page - 1) * $this->limit);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url params for link generation
|
||||
*
|
||||
* @access public
|
||||
* @param integer $page
|
||||
* @param string $order
|
||||
* @param string $direction
|
||||
* @return string
|
||||
*/
|
||||
public function getUrlParams($page, $order, $direction)
|
||||
{
|
||||
$params = array(
|
||||
'page' => $page,
|
||||
'order' => $order,
|
||||
'direction' => $direction,
|
||||
);
|
||||
|
||||
return array_merge($this->params, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the previous link
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function generatePreviousLink()
|
||||
{
|
||||
$html = '<span class="pagination-previous">';
|
||||
|
||||
if ($this->offset > 0) {
|
||||
$html .= $this->container['helper']->url->link(
|
||||
'← '.t('Previous'),
|
||||
$this->controller,
|
||||
$this->action,
|
||||
$this->getUrlParams($this->page - 1, $this->order, $this->direction),
|
||||
false,
|
||||
'js-modal-replace',
|
||||
t('Previous'),
|
||||
false,
|
||||
$this->anchor
|
||||
);
|
||||
} else {
|
||||
$html .= '← '.t('Previous');
|
||||
}
|
||||
|
||||
$html .= '</span>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the next link
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function generateNextLink()
|
||||
{
|
||||
$html = '<span class="pagination-next">';
|
||||
|
||||
if (($this->total - $this->offset) > $this->limit) {
|
||||
$html .= $this->container['helper']->url->link(
|
||||
t('Next').' →',
|
||||
$this->controller,
|
||||
$this->action,
|
||||
$this->getUrlParams($this->page + 1, $this->order, $this->direction),
|
||||
false,
|
||||
'js-modal-replace',
|
||||
t('Next'),
|
||||
false,
|
||||
$this->anchor
|
||||
);
|
||||
} else {
|
||||
$html .= t('Next').' →';
|
||||
}
|
||||
|
||||
$html .= '</span>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the page showing.
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function generatePageShowing()
|
||||
{
|
||||
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
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasNothingToShow()
|
||||
{
|
||||
return $this->offset === 0 && ($this->total - $this->offset) <= $this->limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generation pagination links
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function toHtml()
|
||||
{
|
||||
$html = '';
|
||||
|
||||
if (! $this->hasNothingToShow()) {
|
||||
$html .= '<div class="pagination">';
|
||||
$html .= $this->generatePageShowing();
|
||||
$html .= $this->generatePreviousLink();
|
||||
$html .= $this->generateNextLink();
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method to output pagination links
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->toHtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Column sorting
|
||||
*
|
||||
* @param string $label Column title
|
||||
* @param string $column SQL column name
|
||||
* @return string
|
||||
*/
|
||||
public function order($label, $column)
|
||||
{
|
||||
$prefix = '';
|
||||
$direction = 'ASC';
|
||||
|
||||
if ($this->order === $column) {
|
||||
$prefix = $this->direction === 'DESC' ? '▼ ' : '▲ ';
|
||||
$direction = $this->direction === 'DESC' ? 'ASC' : 'DESC';
|
||||
}
|
||||
|
||||
return $prefix.$this->container['helper']->url->link(
|
||||
$label,
|
||||
$this->controller,
|
||||
$this->action,
|
||||
$this->getUrlParams($this->page, $column, $direction),
|
||||
false,
|
||||
'js-modal-replace'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Plugin;
|
||||
|
||||
/**
|
||||
* Plugin Base class
|
||||
*
|
||||
* @package Kanboard\Core\Plugin
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
abstract class Base extends \Kanboard\Core\Base
|
||||
{
|
||||
/**
|
||||
* Method called for each request
|
||||
*
|
||||
* @abstract
|
||||
* @access public
|
||||
*/
|
||||
abstract public function initialize();
|
||||
|
||||
/**
|
||||
* Override default CSP rules
|
||||
*
|
||||
* @access public
|
||||
* @param array $rules
|
||||
*/
|
||||
public function setContentSecurityPolicy(array $rules)
|
||||
{
|
||||
$this->container['cspRules'] = $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all classes that needs to be stored in the DI container
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getClasses()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all helper classes that needs to be stored in the DI container
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getHelpers()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen on internal events
|
||||
*
|
||||
* @access public
|
||||
* @param string $event
|
||||
* @param callable $callback
|
||||
*/
|
||||
public function on($event, $callback)
|
||||
{
|
||||
$container = $this->container;
|
||||
|
||||
$this->dispatcher->addListener($event, function () use ($container, $callback) {
|
||||
call_user_func($callback, $container);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin name
|
||||
*
|
||||
* This method should be overridden by your Plugin class
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginName()
|
||||
{
|
||||
return ucfirst(substr(get_called_class(), 16, -7));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin description
|
||||
*
|
||||
* This method should be overridden by your Plugin class
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginDescription()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin author
|
||||
*
|
||||
* This method should be overridden by your Plugin class
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginAuthor()
|
||||
{
|
||||
return '?';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin version
|
||||
*
|
||||
* This method should be overridden by your Plugin class
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginVersion()
|
||||
{
|
||||
return '?';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin homepage
|
||||
*
|
||||
* This method should be overridden by your Plugin class
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginHomepage()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application compatibility version
|
||||
*
|
||||
* Examples: >=1.0.36, 1.0.37, APP_VERSION
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getCompatibleVersion()
|
||||
{
|
||||
return APP_VERSION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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)
|
||||
{
|
||||
return Version::isCompatible($plugin['compatible_version'], $appVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter plugins
|
||||
*
|
||||
* @param array $plugin
|
||||
* @return bool
|
||||
*/
|
||||
public function isInstallable(array $plugin)
|
||||
{
|
||||
return $plugin['remote_install'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Plugin;
|
||||
|
||||
/**
|
||||
* Plugin Hooks Handler
|
||||
*
|
||||
* @package Kanboard\Core\Plugin
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Hook
|
||||
{
|
||||
/**
|
||||
* List of hooks
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $hooks = array();
|
||||
|
||||
/**
|
||||
* Bind something on a hook
|
||||
*
|
||||
* @access public
|
||||
* @param string $hook
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function on($hook, $value)
|
||||
{
|
||||
if (! isset($this->hooks[$hook])) {
|
||||
$this->hooks[$hook] = array();
|
||||
}
|
||||
|
||||
$this->hooks[$hook][] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all bindings for a hook
|
||||
*
|
||||
* @access public
|
||||
* @param string $hook
|
||||
* @return array
|
||||
*/
|
||||
public function getListeners($hook)
|
||||
{
|
||||
return isset($this->hooks[$hook]) ? $this->hooks[$hook] : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the hook is used
|
||||
*
|
||||
* @access public
|
||||
* @param string $hook
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists($hook)
|
||||
{
|
||||
return isset($this->hooks[$hook]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge listener results with input array
|
||||
*
|
||||
* @access public
|
||||
* @param string $hook
|
||||
* @param array $values
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public function merge($hook, array &$values, array $params = array())
|
||||
{
|
||||
foreach ($this->getListeners($hook) as $listener) {
|
||||
$result = call_user_func_array($listener, $params);
|
||||
|
||||
if (is_array($result) && ! empty($result)) {
|
||||
$values = array_merge($values, $result);
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute only first listener
|
||||
*
|
||||
* @access public
|
||||
* @param string $hook
|
||||
* @param array $params
|
||||
* @return mixed
|
||||
*/
|
||||
public function first($hook, array $params = array())
|
||||
{
|
||||
foreach ($this->getListeners($hook) as $listener) {
|
||||
return call_user_func_array($listener, $params);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Plugin;
|
||||
|
||||
use ZipArchive;
|
||||
use Kanboard\Core\Tool;
|
||||
|
||||
/**
|
||||
* 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.'));
|
||||
}
|
||||
|
||||
Tool::removeAllFiles($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)
|
||||
{
|
||||
if (!preg_match('/^https?:\/\//', $archiveUrl) || !filter_var($archiveUrl, FILTER_VALIDATE_URL)) {
|
||||
throw new PluginInstallerException(t('This URL is invalid'));
|
||||
}
|
||||
|
||||
$archiveData = $this->httpClient->get($archiveUrl);
|
||||
$archiveFile = tempnam(ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : 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.'));
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
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)
|
||||
{
|
||||
$filename = $zip->filename;
|
||||
$zip->close();
|
||||
unlink($filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Plugin;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use DirectoryIterator;
|
||||
use Exception;
|
||||
use LogicException;
|
||||
use Kanboard\Core\Tool;
|
||||
|
||||
/**
|
||||
* Plugin Loader
|
||||
*
|
||||
* @package Kanboard\Core\Plugin
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Loader extends \Kanboard\Core\Base
|
||||
{
|
||||
/**
|
||||
* Plugin instances
|
||||
*
|
||||
* @access protected
|
||||
* @var array
|
||||
*/
|
||||
protected $plugins = array();
|
||||
protected $incompatiblePlugins = array();
|
||||
|
||||
/**
|
||||
* Get list of loaded plugins
|
||||
*
|
||||
* @access public
|
||||
* @return Base[]
|
||||
*/
|
||||
public function getPlugins()
|
||||
{
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of not compatible plugins
|
||||
*
|
||||
* @access public
|
||||
* @return Base[]
|
||||
*/
|
||||
public function getIncompatiblePlugins()
|
||||
{
|
||||
return $this->incompatiblePlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan plugin folder and load plugins
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function scan()
|
||||
{
|
||||
if (file_exists(PLUGINS_DIR)) {
|
||||
$loader = new ClassLoader();
|
||||
$loader->addPsr4('Kanboard\Plugin\\', PLUGINS_DIR);
|
||||
$loader->register();
|
||||
|
||||
$dir = new DirectoryIterator(PLUGINS_DIR);
|
||||
|
||||
foreach ($dir as $fileInfo) {
|
||||
if ($fileInfo->isDir() && substr($fileInfo->getFilename(), 0, 1) !== '.') {
|
||||
$pluginName = $fileInfo->getFilename();
|
||||
$this->initializePlugin($pluginName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin schema
|
||||
*
|
||||
* @access public
|
||||
* @param string $pluginName
|
||||
*/
|
||||
public function loadSchema($pluginName)
|
||||
{
|
||||
if (SchemaHandler::hasSchema($pluginName)) {
|
||||
$schemaHandler = new SchemaHandler($this->container);
|
||||
$schemaHandler->loadSchema($pluginName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin
|
||||
*
|
||||
* @access public
|
||||
* @throws LogicException
|
||||
* @param string $pluginName
|
||||
* @return Base
|
||||
*/
|
||||
public function loadPlugin($pluginName)
|
||||
{
|
||||
$className = '\Kanboard\Plugin\\'.$pluginName.'\\Plugin';
|
||||
|
||||
if (! class_exists($className)) {
|
||||
throw new LogicException('Unable to load this plugin class: '.$className);
|
||||
}
|
||||
|
||||
return new $className($this->container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize plugin
|
||||
*
|
||||
* @access public
|
||||
* @param string $pluginName
|
||||
*/
|
||||
public function initializePlugin($pluginName)
|
||||
{
|
||||
try {
|
||||
$plugin = $this->loadPlugin($pluginName);
|
||||
|
||||
if (Version::isCompatible($plugin->getCompatibleVersion(), APP_VERSION)) {
|
||||
$this->loadSchema($pluginName);
|
||||
|
||||
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());
|
||||
|
||||
$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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Plugin;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class PluginException
|
||||
*
|
||||
* @package Kanboard\Core\Plugin
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class PluginException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Plugin;
|
||||
|
||||
/**
|
||||
* Class PluginInstallerException
|
||||
*
|
||||
* @package Kanboard\Core\Plugin
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class PluginInstallerException extends PluginException
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 || strpos($appVersion, 'main') !== 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Queue;
|
||||
|
||||
use Exception;
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Job\BaseJob;
|
||||
use SimpleQueue\Job;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* 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(),
|
||||
'user_id' => $this->userSession->getId(),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a job
|
||||
*
|
||||
* @access public
|
||||
* @param Job $job
|
||||
*/
|
||||
public function executeJob(Job $job)
|
||||
{
|
||||
$payload = $job->getBody();
|
||||
|
||||
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']);
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error(__METHOD__.': Error during job execution: '.$e->getMessage());
|
||||
$this->logger->error(__METHOD__ .' => '.json_encode($payload));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the session for the job
|
||||
*
|
||||
* @access protected
|
||||
* @param integer $user_id
|
||||
*/
|
||||
protected function prepareJobSession($user_id)
|
||||
{
|
||||
session_flush();
|
||||
|
||||
if ($user_id > 0) {
|
||||
$user = $this->userModel->getById($user_id);
|
||||
$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(new Event(), 'app.bootstrap');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?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)
|
||||
{
|
||||
$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());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for new jobs
|
||||
*
|
||||
* @access public
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function listen()
|
||||
{
|
||||
if ($this->queue === null) {
|
||||
throw new LogicException('No queue driver defined or unable to connect to broker!');
|
||||
}
|
||||
|
||||
while ($job = $this->queue->pull()) {
|
||||
JobHandler::getInstance($this->container)->executeJob($job);
|
||||
$this->queue->completed($job);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* Access Map Definition
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class AccessMap
|
||||
{
|
||||
/**
|
||||
* Default role
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
private $defaultRole = '';
|
||||
|
||||
/**
|
||||
* Role hierarchy
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $hierarchy = array();
|
||||
|
||||
/**
|
||||
* Access map
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $map = array();
|
||||
|
||||
/**
|
||||
* Define the default role when nothing match
|
||||
*
|
||||
* @access public
|
||||
* @param string $role
|
||||
* @return AccessMap
|
||||
*/
|
||||
public function setDefaultRole($role)
|
||||
{
|
||||
$this->defaultRole = $role;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define role hierarchy
|
||||
*
|
||||
* @access public
|
||||
* @param string $role
|
||||
* @param array $subroles
|
||||
* @return AccessMap
|
||||
*/
|
||||
public function setRoleHierarchy($role, array $subroles)
|
||||
{
|
||||
foreach ($subroles as $subrole) {
|
||||
if (isset($this->hierarchy[$subrole])) {
|
||||
$this->hierarchy[$subrole][] = $role;
|
||||
} else {
|
||||
$this->hierarchy[$subrole] = array($role);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get computed role hierarchy
|
||||
*
|
||||
* @access public
|
||||
* @param string $role
|
||||
* @return array
|
||||
*/
|
||||
public function getRoleHierarchy($role)
|
||||
{
|
||||
$roles = array($role);
|
||||
|
||||
if (isset($this->hierarchy[$role])) {
|
||||
$roles = array_merge($roles, $this->hierarchy[$role]);
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the highest role from a list
|
||||
*
|
||||
* @access public
|
||||
* @param array $roles
|
||||
* @return string
|
||||
*/
|
||||
public function getHighestRole(array $roles)
|
||||
{
|
||||
$rank = array();
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$rank[$role] = count($this->getRoleHierarchy($role));
|
||||
}
|
||||
|
||||
asort($rank);
|
||||
|
||||
return key($rank);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new access rules
|
||||
*
|
||||
* @access public
|
||||
* @param string $controller Controller class name
|
||||
* @param mixed $methods List of method name or just one method
|
||||
* @param string $role Lowest role required
|
||||
* @return AccessMap
|
||||
*/
|
||||
public function add($controller, $methods, $role)
|
||||
{
|
||||
if (is_array($methods)) {
|
||||
foreach ($methods as $method) {
|
||||
$this->addRule($controller, $method, $role);
|
||||
}
|
||||
} else {
|
||||
$this->addRule($controller, $methods, $role);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new access rule
|
||||
*
|
||||
* @access private
|
||||
* @param string $controller
|
||||
* @param string $method
|
||||
* @param string $role
|
||||
* @return AccessMap
|
||||
*/
|
||||
private function addRule($controller, $method, $role)
|
||||
{
|
||||
$controller = strtolower($controller);
|
||||
$method = strtolower($method);
|
||||
|
||||
if (! isset($this->map[$controller])) {
|
||||
$this->map[$controller] = array();
|
||||
}
|
||||
|
||||
$this->map[$controller][$method] = $role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get roles that match the given controller/method
|
||||
*
|
||||
* @access public
|
||||
* @param string $controller
|
||||
* @param string $method
|
||||
* @return array
|
||||
*/
|
||||
public function getRoles($controller, $method)
|
||||
{
|
||||
$controller = strtolower($controller);
|
||||
$method = strtolower($method);
|
||||
|
||||
foreach (array($method, '*') as $key) {
|
||||
if (isset($this->map[$controller][$key])) {
|
||||
return $this->getRoleHierarchy($this->map[$controller][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->getRoleHierarchy($this->defaultRole);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
use LogicException;
|
||||
use Kanboard\Core\Base;
|
||||
use Kanboard\Event\AuthFailureEvent;
|
||||
use Kanboard\Event\AuthSuccessEvent;
|
||||
|
||||
/**
|
||||
* Authentication Manager
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class AuthenticationManager extends Base
|
||||
{
|
||||
/**
|
||||
* Event names
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const EVENT_SUCCESS = 'auth.success';
|
||||
const EVENT_FAILURE = 'auth.failure';
|
||||
|
||||
/**
|
||||
* List of authentication providers
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $providers = array();
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->providers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new authentication provider
|
||||
*
|
||||
* @access public
|
||||
* @param AuthenticationProviderInterface $provider
|
||||
* @return AuthenticationManager
|
||||
*/
|
||||
public function register(AuthenticationProviderInterface $provider)
|
||||
{
|
||||
$this->providers[$provider->getName()] = $provider;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new authentication provider
|
||||
*
|
||||
* @access public
|
||||
* @param string $name
|
||||
* @return AuthenticationProviderInterface|OAuthAuthenticationProviderInterface|PasswordAuthenticationProviderInterface|PreAuthenticationProviderInterface|OAuthAuthenticationProviderInterface
|
||||
*/
|
||||
public function getProvider($name)
|
||||
{
|
||||
if (! isset($this->providers[$name])) {
|
||||
throw new LogicException('Authentication provider not found: '.$name);
|
||||
}
|
||||
|
||||
return $this->providers[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute providers that are able to validate the current session
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function checkCurrentSession()
|
||||
{
|
||||
if ($this->userSession->isLogged()) {
|
||||
foreach ($this->filterProviders('SessionCheckProviderInterface') as $provider) {
|
||||
if ($provider instanceof OptionalAuthenticationProviderInterface && ! $provider->isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $provider->isValidSession()) {
|
||||
$this->logger->debug('Invalidate session for '.$this->userSession->getUsername());
|
||||
session_flush();
|
||||
$this->preAuthentication();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute pre-authentication providers
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function preAuthentication()
|
||||
{
|
||||
foreach ($this->filterProviders('PreAuthenticationProviderInterface') as $provider) {
|
||||
if ($provider instanceof OptionalAuthenticationProviderInterface && ! $provider->isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
|
||||
$this->dispatcher->dispatch(new AuthSuccessEvent($provider->getName()), self::EVENT_SUCCESS);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute username/password authentication providers
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param boolean $fireEvent
|
||||
* @return boolean
|
||||
*/
|
||||
public function passwordAuthentication($username, $password, $fireEvent = true)
|
||||
{
|
||||
foreach ($this->filterProviders('PasswordAuthenticationProviderInterface') as $provider) {
|
||||
$provider->setUsername($username);
|
||||
$provider->setPassword($password);
|
||||
|
||||
if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
|
||||
if ($fireEvent) {
|
||||
$this->dispatcher->dispatch(new AuthSuccessEvent($provider->getName()), self::EVENT_SUCCESS);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($fireEvent) {
|
||||
$this->dispatcher->dispatch(new AuthFailureEvent($username), self::EVENT_FAILURE);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OAuth2 authentication
|
||||
*
|
||||
* @access public
|
||||
* @param string $name
|
||||
* @return boolean
|
||||
*/
|
||||
public function oauthAuthentication($name)
|
||||
{
|
||||
$provider = $this->getProvider($name);
|
||||
|
||||
if ($provider->authenticate() && $this->userProfile->initialize($provider->getUser())) {
|
||||
$this->dispatcher->dispatch(new AuthSuccessEvent($provider->getName()), self::EVENT_SUCCESS);
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch(new AuthFailureEvent, self::EVENT_FAILURE);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last Post-Authentication provider
|
||||
*
|
||||
* @access public
|
||||
* @return PostAuthenticationProviderInterface
|
||||
*/
|
||||
public function getPostAuthenticationProvider()
|
||||
{
|
||||
$providers = $this->filterProviders('PostAuthenticationProviderInterface');
|
||||
|
||||
if (empty($providers)) {
|
||||
throw new LogicException('You must have at least one Post-Authentication Provider configured');
|
||||
}
|
||||
|
||||
return array_pop($providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter registered providers by interface type
|
||||
*
|
||||
* @access private
|
||||
* @param string $interface
|
||||
* @return array
|
||||
*/
|
||||
private function filterProviders($interface)
|
||||
{
|
||||
$interface = '\Kanboard\Core\Security\\'.$interface;
|
||||
|
||||
return array_filter($this->providers, function (AuthenticationProviderInterface $provider) use ($interface) {
|
||||
return is_a($provider, $interface);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* Authentication Provider Interface
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface AuthenticationProviderInterface
|
||||
{
|
||||
/**
|
||||
* Get authentication provider name
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
|
||||
/**
|
||||
* Authenticate the user
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function authenticate();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* Authorization Handler
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Authorization
|
||||
{
|
||||
/**
|
||||
* Access Map
|
||||
*
|
||||
* @access private
|
||||
* @var AccessMap
|
||||
*/
|
||||
private $accessMap;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param AccessMap $accessMap
|
||||
*/
|
||||
public function __construct(AccessMap $accessMap)
|
||||
{
|
||||
$this->accessMap = $accessMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given role is allowed to access to the specified resource
|
||||
*
|
||||
* @access public
|
||||
* @param string $controller
|
||||
* @param string $method
|
||||
* @param string $role
|
||||
* @return boolean
|
||||
*/
|
||||
public function isAllowed($controller, $method, $role)
|
||||
{
|
||||
$roles = $this->accessMap->getRoles($controller, $method);
|
||||
return in_array($role, $roles);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* OAuth2 Authentication Provider Interface
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface OAuthAuthenticationProviderInterface extends AuthenticationProviderInterface
|
||||
{
|
||||
/**
|
||||
* Get user object
|
||||
*
|
||||
* @access public
|
||||
* @return \Kanboard\Core\User\UserProviderInterface
|
||||
*/
|
||||
public function getUser();
|
||||
|
||||
/**
|
||||
* Unlink user
|
||||
*
|
||||
* @access public
|
||||
* @param integer $userId
|
||||
* @return bool
|
||||
*/
|
||||
public function unlink($userId);
|
||||
|
||||
/**
|
||||
* Get configured OAuth2 service
|
||||
*
|
||||
* @access public
|
||||
* @return \Kanboard\Core\Http\OAuth2
|
||||
*/
|
||||
public function getService();
|
||||
|
||||
/**
|
||||
* Set OAuth2 code
|
||||
*
|
||||
* @access public
|
||||
* @param string $code
|
||||
* @return OAuthAuthenticationProviderInterface
|
||||
*/
|
||||
public function setCode($code);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* Optional Authentication Provider Interface
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface OptionalAuthenticationProviderInterface
|
||||
{
|
||||
/**
|
||||
* Check if the authentication provider should be used
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isEnabled();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* Password Authentication Provider Interface
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface PasswordAuthenticationProviderInterface extends AuthenticationProviderInterface
|
||||
{
|
||||
/**
|
||||
* Get user object
|
||||
*
|
||||
* @access public
|
||||
* @return \Kanboard\Core\User\UserProviderInterface
|
||||
*/
|
||||
public function getUser();
|
||||
|
||||
/**
|
||||
* Set username
|
||||
*
|
||||
* @access public
|
||||
* @param string $username
|
||||
*/
|
||||
public function setUsername($username);
|
||||
|
||||
/**
|
||||
* Set password
|
||||
*
|
||||
* @access public
|
||||
* @param string $password
|
||||
*/
|
||||
public function setPassword($password);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* Post Authentication Provider Interface
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface PostAuthenticationProviderInterface extends AuthenticationProviderInterface
|
||||
{
|
||||
/**
|
||||
* Called only one time before to prompt the user for pin code
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function beforeCode();
|
||||
|
||||
/**
|
||||
* Set user pin-code
|
||||
*
|
||||
* @access public
|
||||
* @param string $code
|
||||
*/
|
||||
public function setCode($code);
|
||||
|
||||
/**
|
||||
* Generate secret if necessary
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function generateSecret();
|
||||
|
||||
/**
|
||||
* Set secret token (fetched from user profile)
|
||||
*
|
||||
* @access public
|
||||
* @param string $secret
|
||||
*/
|
||||
public function setSecret($secret);
|
||||
|
||||
/**
|
||||
* Get secret token (will be saved in user profile)
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getSecret();
|
||||
|
||||
/**
|
||||
* Get key url (empty if no url can be provided)
|
||||
*
|
||||
* @access public
|
||||
* @param string $label
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyUrl($label);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* Pre-Authentication Provider Interface
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface PreAuthenticationProviderInterface extends AuthenticationProviderInterface
|
||||
{
|
||||
/**
|
||||
* Get user object
|
||||
*
|
||||
* @access public
|
||||
* @return \Kanboard\Core\User\UserProviderInterface
|
||||
*/
|
||||
public function getUser();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* Role Definitions
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Role
|
||||
{
|
||||
const APP_ADMIN = 'app-admin';
|
||||
const APP_MANAGER = 'app-manager';
|
||||
const APP_USER = 'app-user';
|
||||
const APP_PUBLIC = 'app-public';
|
||||
|
||||
const PROJECT_MANAGER = 'project-manager';
|
||||
const PROJECT_MEMBER = 'project-member';
|
||||
const PROJECT_VIEWER = 'project-viewer';
|
||||
|
||||
/**
|
||||
* Get application roles
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getApplicationRoles()
|
||||
{
|
||||
return array(
|
||||
self::APP_ADMIN => t('Administrator'),
|
||||
self::APP_MANAGER => t('Manager'),
|
||||
self::APP_USER => t('User'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project roles
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getProjectRoles()
|
||||
{
|
||||
return array(
|
||||
self::PROJECT_MANAGER => t('Project Manager'),
|
||||
self::PROJECT_MEMBER => t('Project Member'),
|
||||
self::PROJECT_VIEWER => t('Project Viewer'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param string $role
|
||||
* @return string
|
||||
*/
|
||||
public function getRoleName($role)
|
||||
{
|
||||
$roles = $this->getApplicationRoles() + $this->getProjectRoles();
|
||||
return isset($roles[$role]) ? $roles[$role] : t('Unknown');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
/**
|
||||
* Session Check Provider Interface
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
interface SessionCheckProviderInterface
|
||||
{
|
||||
/**
|
||||
* Check if the user session is valid
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public function isValidSession();
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Security;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Token Handler
|
||||
*
|
||||
* @package security
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Token extends Base
|
||||
{
|
||||
protected static $KEY_LENGTH = 32;
|
||||
protected static $NONCE_LENGTH = 16;
|
||||
protected static $HMAC_ALGO = 'sha256';
|
||||
protected static $HMAC_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Generate a random token with different methods: openssl or /dev/urandom or fallback to uniqid()
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return string Random token
|
||||
*/
|
||||
public static function getToken($length = 30)
|
||||
{
|
||||
return bin2hex(random_bytes($length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and store a one-time CSRF token
|
||||
*
|
||||
* @access public
|
||||
* @return string Random token
|
||||
*/
|
||||
public function getCSRFToken()
|
||||
{
|
||||
return $this->createSessionToken('csrf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and store a reusable CSRF token
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getReusableCSRFToken()
|
||||
{
|
||||
return $this->createSessionToken('pcsrf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the token exists for the current session (a token can be used only one time)
|
||||
*
|
||||
* @access public
|
||||
* @param string $token CSRF token
|
||||
* @return bool
|
||||
*/
|
||||
public function validateCSRFToken($token)
|
||||
{
|
||||
return $this->validateSessionToken('csrf', $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the token exists as a reusable CSRF token
|
||||
*
|
||||
* @access public
|
||||
* @param string $token CSRF token
|
||||
* @return bool
|
||||
*/
|
||||
public function validateReusableCSRFToken($token)
|
||||
{
|
||||
return $this->validateSessionToken('pcsrf', $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a session token of the given type
|
||||
*
|
||||
* @access protected
|
||||
* @param string $type Token type
|
||||
* @return string Random token
|
||||
*/
|
||||
protected function createSessionToken($type)
|
||||
{
|
||||
$nonce = self::getToken(self::$NONCE_LENGTH);
|
||||
return $nonce . $this->signSessionToken($type, $nonce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a session token of the given type
|
||||
*
|
||||
* @access protected
|
||||
* @param string $type Token type
|
||||
* @param string $token Session token
|
||||
* @return bool
|
||||
*/
|
||||
protected function validateSessionToken($type, $token)
|
||||
{
|
||||
if (!is_string($token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($token) != (self::$NONCE_LENGTH + self::$HMAC_LENGTH) * 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$nonce = substr($token, 0, self::$NONCE_LENGTH * 2);
|
||||
$hmac = substr($token, self::$NONCE_LENGTH * 2, self::$HMAC_LENGTH * 2);
|
||||
|
||||
return hash_equals($this->signSessionToken($type, $nonce), $hmac);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a nonce with the key belonging to the given type
|
||||
*
|
||||
* @access protected
|
||||
* @param string $type Token type
|
||||
* @param string $nonce Nonce to sign
|
||||
* @return string
|
||||
*/
|
||||
protected function signSessionToken($type, $nonce)
|
||||
{
|
||||
if (!session_exists($type . '_key')) {
|
||||
session_set($type . '_key', self::getToken(self::$KEY_LENGTH));
|
||||
}
|
||||
|
||||
$data = $nonce . '-' . session_id();
|
||||
$key = session_get($type . '_key');
|
||||
$hmac = hash_hmac(self::$HMAC_ALGO, $data, $key, true);
|
||||
|
||||
return bin2hex(substr($hmac, 0, self::$HMAC_LENGTH));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Session;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
|
||||
/**
|
||||
* Session Flash Message
|
||||
*
|
||||
* @package Kanboard\Core\Session
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class FlashMessage extends Base
|
||||
{
|
||||
/**
|
||||
* Add success message
|
||||
*
|
||||
* @access public
|
||||
* @param string $message
|
||||
*/
|
||||
public function success($message)
|
||||
{
|
||||
$this->setMessage('success', $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add failure message
|
||||
*
|
||||
* @access public
|
||||
* @param string $message
|
||||
*/
|
||||
public function failure($message)
|
||||
{
|
||||
$this->setMessage('failure', $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new flash message
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
* @param string $message
|
||||
*/
|
||||
public function setMessage($key, $message)
|
||||
{
|
||||
if (! session_exists('flash')) {
|
||||
session_set('flash', []);
|
||||
}
|
||||
|
||||
session_merge('flash', [$key => $message]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get flash message
|
||||
*
|
||||
* @access public
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
public function getMessage($key)
|
||||
{
|
||||
$message = '';
|
||||
|
||||
if (session_exists('flash')) {
|
||||
$messages = session_get('flash');
|
||||
|
||||
if (isset($messages[$key])) {
|
||||
$message = $messages[$key];
|
||||
unset($messages[$key]);
|
||||
session_set('flash', $messages);
|
||||
}
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Session;
|
||||
|
||||
use PicoDb\Database;
|
||||
use SessionHandlerInterface;
|
||||
|
||||
/**
|
||||
* Class SessionHandler
|
||||
*
|
||||
* @package Kanboard\Core\Session
|
||||
*/
|
||||
class SessionHandler implements SessionHandlerInterface
|
||||
{
|
||||
const TABLE = 'sessions';
|
||||
|
||||
/**
|
||||
* @var Database
|
||||
*/
|
||||
private $db;
|
||||
|
||||
public function __construct(Database $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function close()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function destroy($sessionID)
|
||||
{
|
||||
return $this->db->table(self::TABLE)->eq('id', $sessionID)->remove();
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function gc($maxlifetime)
|
||||
{
|
||||
return $this->db->table(self::TABLE)->lt('expire_at', time())->remove();
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function open($savePath, $name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function read($sessionID)
|
||||
{
|
||||
$result = $this->db->table(self::TABLE)->eq('id', $sessionID)->gt('expire_at', time())->findOneColumn('data');
|
||||
|
||||
// Note: Returning false display an error message and write() is never called
|
||||
// preventing new sessions to be created when calling session_start()
|
||||
if (empty($result)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sanitize session data to prevent object deserialization attacks (CWE-502).
|
||||
// Using allowed_classes: false converts any serialized objects to harmless
|
||||
// __PHP_Incomplete_Class instances, preventing exploitation via gadget chains.
|
||||
$sanitized = @unserialize($result, ['allowed_classes' => false]);
|
||||
|
||||
// unserialize() returns false both on failure AND when the data legitimately
|
||||
// represents boolean false (serialized as 'b:0;'). Check the raw string to
|
||||
// distinguish a real deserialization error from a valid false value.
|
||||
if ($sanitized === false && $result !== 'b:0;') {
|
||||
// Data could not be unserialized (e.g. legacy format after handler change);
|
||||
// discard it so a fresh session is created.
|
||||
return '';
|
||||
}
|
||||
|
||||
return serialize($sanitized);
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function write($sessionID, $data)
|
||||
{
|
||||
if (SESSION_DURATION > 0) {
|
||||
$lifetime = time() + SESSION_DURATION;
|
||||
} else {
|
||||
$lifetime = time() + (ini_get('session.gc_maxlifetime') ?: 1440);
|
||||
}
|
||||
|
||||
$this->db->startTransaction();
|
||||
|
||||
if ($this->db->table(self::TABLE)->eq('id', $sessionID)->exists()) {
|
||||
$this->db->table(self::TABLE)->eq('id', $sessionID)->update([
|
||||
'expire_at' => $lifetime,
|
||||
'data' => $data,
|
||||
]);
|
||||
} else {
|
||||
$this->db->table(self::TABLE)->insert([
|
||||
'id' => $sessionID,
|
||||
'expire_at' => $lifetime,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->db->closeTransaction();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Session;
|
||||
|
||||
use Kanboard\Core\Base;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Session Manager
|
||||
*
|
||||
* @package Kanboard\Core\Session
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class SessionManager extends Base
|
||||
{
|
||||
/**
|
||||
* Event names
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const EVENT_DESTROY = 'session.destroy';
|
||||
|
||||
/**
|
||||
* Return true if the session is open
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isOpen()
|
||||
{
|
||||
return session_id() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function open()
|
||||
{
|
||||
if (SESSION_HANDLER === 'db') {
|
||||
session_set_save_handler(new SessionHandler($this->db), true);
|
||||
}
|
||||
|
||||
$this->configure();
|
||||
|
||||
if (ini_get('session.auto_start') == 1) {
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
session_name('KB_SID');
|
||||
session_start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the session
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
$this->dispatcher->dispatch(new Event(), self::EVENT_DESTROY);
|
||||
|
||||
// Destroy the session cookie
|
||||
$params = session_get_cookie_params();
|
||||
|
||||
setcookie(
|
||||
session_name(),
|
||||
'',
|
||||
time() - 42000,
|
||||
$params['path'],
|
||||
$params['domain'],
|
||||
$params['secure'],
|
||||
$params['httponly']
|
||||
);
|
||||
|
||||
session_unset();
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define session settings
|
||||
*
|
||||
* @access private
|
||||
*/
|
||||
private function configure()
|
||||
{
|
||||
// Session cookie: HttpOnly and secure flags
|
||||
session_set_cookie_params(
|
||||
SESSION_DURATION,
|
||||
$this->helper->url->dir() ?: '/',
|
||||
null,
|
||||
$this->request->isHTTPS(),
|
||||
true
|
||||
);
|
||||
|
||||
// Use php_serialize handler so session data is a single serialize() call,
|
||||
// which allows safe sanitization in SessionHandler::read() (CWE-502 mitigation).
|
||||
ini_set('session.serialize_handler', 'php_serialize');
|
||||
|
||||
// Avoid session id in the URL
|
||||
ini_set('session.use_only_cookies', '1');
|
||||
ini_set('session.use_trans_sid', '0');
|
||||
|
||||
// Enable strict mode
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
|
||||
// Better session hash
|
||||
ini_set('session.hash_function', '1'); // 'sha512' is not compatible with FreeBSD, only MD5 '0' and SHA-1 '1' seems to work
|
||||
ini_set('session.hash_bits_per_character', 6);
|
||||
|
||||
// Set an additional entropy
|
||||
ini_set('session.entropy_file', '/dev/urandom');
|
||||
ini_set('session.entropy_length', '256');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
/**
|
||||
* Template
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*
|
||||
* @property \Kanboard\Helper\AppHelper $app
|
||||
* @property \Kanboard\Helper\AssetHelper $asset
|
||||
* @property \Kanboard\Helper\DateHelper $dt
|
||||
* @property \Kanboard\Helper\FileHelper $file
|
||||
* @property \Kanboard\Helper\FormHelper $form
|
||||
* @property \Kanboard\Helper\HookHelper $hook
|
||||
* @property \Kanboard\Helper\ModelHelper $model
|
||||
* @property \Kanboard\Helper\SubtaskHelper $subtask
|
||||
* @property \Kanboard\Helper\TaskHelper $task
|
||||
* @property \Kanboard\Helper\TextHelper $text
|
||||
* @property \Kanboard\Helper\UrlHelper $url
|
||||
* @property \Kanboard\Helper\UserHelper $user
|
||||
* @property \Kanboard\Helper\LayoutHelper $layout
|
||||
* @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader
|
||||
*/
|
||||
class Template
|
||||
{
|
||||
/**
|
||||
* Helper object
|
||||
*
|
||||
* @access private
|
||||
* @var Helper
|
||||
*/
|
||||
private $helper;
|
||||
|
||||
/**
|
||||
* List of template overrides
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
private $overrides = array();
|
||||
|
||||
/**
|
||||
* Template constructor
|
||||
*
|
||||
* @access public
|
||||
* @param Helper $helper
|
||||
*/
|
||||
public function __construct(Helper $helper)
|
||||
{
|
||||
$this->helper = $helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose helpers with magic getter
|
||||
*
|
||||
* @access public
|
||||
* @param string $helper
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($helper)
|
||||
{
|
||||
return $this->helper->getHelper($helper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $template->render('template_name', ['bla' => 'value']);
|
||||
*
|
||||
* @access public
|
||||
* @param string $__template_name Template name
|
||||
* @param array $__template_args Key/Value map of template variables
|
||||
* @return string
|
||||
*/
|
||||
public function render($__template_name, array $__template_args = array())
|
||||
{
|
||||
extract($__template_args);
|
||||
ob_start();
|
||||
include $this->getTemplateFile($__template_name);
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a new template override
|
||||
*
|
||||
* @access public
|
||||
* @param string $original_template
|
||||
* @param string $new_template
|
||||
*/
|
||||
public function setTemplateOverride($original_template, $new_template)
|
||||
{
|
||||
$this->overrides[$original_template] = $new_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find template filename
|
||||
*
|
||||
* Core template: 'task/show' or 'kanboard:task/show'
|
||||
* Plugin template: 'myplugin:task/show'
|
||||
*
|
||||
* @access public
|
||||
* @param string $template
|
||||
* @return string
|
||||
*/
|
||||
public function getTemplateFile($template)
|
||||
{
|
||||
$plugin = '';
|
||||
$template = isset($this->overrides[$template]) ? $this->overrides[$template] : $template;
|
||||
|
||||
if (strpos($template, ':') !== false) {
|
||||
list($plugin, $template) = explode(':', $template);
|
||||
}
|
||||
|
||||
if ($plugin !== 'kanboard' && $plugin !== '') {
|
||||
return implode(DIRECTORY_SEPARATOR, array(PLUGINS_DIR, ucfirst($plugin), 'Template', $template.'.php'));
|
||||
}
|
||||
|
||||
return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', 'Template', $template.'.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
/**
|
||||
* Thumbnail Generator
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Thumbnail
|
||||
{
|
||||
protected $compression = -1;
|
||||
protected $metadata = array();
|
||||
protected $srcImage;
|
||||
protected $dstImage;
|
||||
|
||||
/**
|
||||
* Create a thumbnail from a local file
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @param string $filename
|
||||
* @return Thumbnail
|
||||
*/
|
||||
public static function createFromFile($filename)
|
||||
{
|
||||
$self = new static();
|
||||
$self->fromFile($filename);
|
||||
return $self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a thumbnail from a string
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @param string $blob
|
||||
* @return Thumbnail
|
||||
*/
|
||||
public static function createFromString($blob)
|
||||
{
|
||||
$self = new static();
|
||||
$self->fromString($blob);
|
||||
return $self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the local image file in memory with GD
|
||||
*
|
||||
* @access public
|
||||
* @param string $filename
|
||||
* @return Thumbnail
|
||||
*/
|
||||
public function fromFile($filename)
|
||||
{
|
||||
$this->metadata = getimagesize($filename);
|
||||
$this->srcImage = @imagecreatefromstring(file_get_contents($filename));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the image blob in memory with GD
|
||||
*
|
||||
* @access public
|
||||
* @param string $blob
|
||||
* @return Thumbnail
|
||||
*/
|
||||
public function fromString($blob)
|
||||
{
|
||||
if (!function_exists('getimagesizefromstring')) {
|
||||
$uri = 'data://application/octet-stream;base64,' . base64_encode($blob);
|
||||
$this->metadata = getimagesize($uri);
|
||||
} else {
|
||||
$this->metadata = getimagesizefromstring($blob);
|
||||
}
|
||||
|
||||
// Avoid warning from libpng when loading PNG image with obscure or incorrect iCCP profiles
|
||||
$this->srcImage = @imagecreatefromstring($blob);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the image
|
||||
*
|
||||
* @access public
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @return Thumbnail
|
||||
*/
|
||||
public function resize($width = 250, $height = 100)
|
||||
{
|
||||
$srcWidth = $this->metadata[0];
|
||||
$srcHeight = $this->metadata[1];
|
||||
$dstX = 0;
|
||||
$dstY = 0;
|
||||
|
||||
if ($width == 0 && $height == 0) {
|
||||
$width = 100;
|
||||
$height = 100;
|
||||
}
|
||||
|
||||
if ($width > 0 && $height == 0) {
|
||||
$dstWidth = $width;
|
||||
$dstHeight = floor($srcHeight * ($width / $srcWidth));
|
||||
$this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight);
|
||||
} elseif ($width == 0 && $height > 0) {
|
||||
$dstWidth = floor($srcWidth * ($height / $srcHeight));
|
||||
$dstHeight = $height;
|
||||
$this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight);
|
||||
} else {
|
||||
$srcRatio = $srcWidth / $srcHeight;
|
||||
$resizeRatio = $width / $height;
|
||||
|
||||
if ($srcRatio <= $resizeRatio) {
|
||||
$dstWidth = $width;
|
||||
$dstHeight = floor($srcHeight * ($width / $srcWidth));
|
||||
$dstY = ($dstHeight - $height) / 2 * (-1);
|
||||
} else {
|
||||
$dstWidth = floor($srcWidth * ($height / $srcHeight));
|
||||
$dstHeight = $height;
|
||||
$dstX = ($dstWidth - $width) / 2 * (-1);
|
||||
}
|
||||
|
||||
$this->dstImage = imagecreatetruecolor($width, $height);
|
||||
}
|
||||
|
||||
imagealphablending($this->dstImage, false);
|
||||
imagesavealpha($this->dstImage, true);
|
||||
|
||||
imagecopyresampled($this->dstImage, $this->srcImage, (int) $dstX, (int) $dstY, 0, 0, (int) $dstWidth, (int) $dstHeight, (int) $srcWidth, (int) $srcHeight);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the thumbnail to a local file
|
||||
*
|
||||
* @access public
|
||||
* @param string $filename
|
||||
* @return Thumbnail
|
||||
*/
|
||||
public function toFile($filename)
|
||||
{
|
||||
imagepng($this->dstImage, $filename, $this->compression);
|
||||
imagedestroy($this->dstImage);
|
||||
imagedestroy($this->srcImage);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the thumbnail as a string
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
ob_start();
|
||||
imagepng($this->dstImage, null, $this->compression);
|
||||
imagedestroy($this->dstImage);
|
||||
imagedestroy($this->srcImage);
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the thumbnail directly to the browser or stdout
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function toOutput()
|
||||
{
|
||||
imagepng($this->dstImage, null, $this->compression);
|
||||
imagedestroy($this->dstImage);
|
||||
imagedestroy($this->srcImage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
use Pimple\Container;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
/**
|
||||
* Tool class
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Tool
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
* @param Container $container
|
||||
* @param array $namespaces
|
||||
* @return Container
|
||||
*/
|
||||
public static function buildDIC(Container $container, array $namespaces)
|
||||
{
|
||||
foreach ($namespaces as $namespace => $classes) {
|
||||
foreach ($classes as $name) {
|
||||
$class = '\\Kanboard\\'.$namespace.'\\'.$name;
|
||||
$container[lcfirst($name)] = function ($c) use ($class) {
|
||||
return new $class($c);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @access public
|
||||
* @param Container $container
|
||||
* @param array $namespaces
|
||||
* @return Container
|
||||
*/
|
||||
public static function buildDICHelpers(Container $container, array $namespaces)
|
||||
{
|
||||
foreach ($namespaces as $namespace => $classes) {
|
||||
foreach ($classes as $name) {
|
||||
$class = '\\Kanboard\\'.$namespace.'\\'.$name;
|
||||
$container['helper']->register($name, $class);
|
||||
}
|
||||
}
|
||||
|
||||
return $container;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core;
|
||||
|
||||
/**
|
||||
* Translator class
|
||||
*
|
||||
* @package core
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class Translator
|
||||
{
|
||||
/**
|
||||
* Locale
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @var array
|
||||
*/
|
||||
public static $locales = array();
|
||||
|
||||
/**
|
||||
* Instance
|
||||
*
|
||||
* @static
|
||||
* @access private
|
||||
* @var Translator
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return Translator
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self;
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a translation
|
||||
*
|
||||
* $translator->translate('I have %d kids', 5);
|
||||
*
|
||||
* @access public
|
||||
* @param string $identifier Default string
|
||||
* @return string
|
||||
*/
|
||||
public function translate($identifier)
|
||||
{
|
||||
$args = func_get_args();
|
||||
|
||||
array_shift($args);
|
||||
array_unshift($args, $this->get($identifier, $identifier));
|
||||
|
||||
foreach ($args as &$arg) {
|
||||
$arg = htmlspecialchars((string) $arg, ENT_QUOTES, 'UTF-8', false);
|
||||
}
|
||||
|
||||
return call_user_func_array(
|
||||
'sprintf',
|
||||
$args
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a translation with no HTML escaping
|
||||
*
|
||||
* $translator->translateNoEscaping('I have %d kids', 5);
|
||||
*
|
||||
* @access public
|
||||
* @param string $identifier Default string
|
||||
* @return string
|
||||
*/
|
||||
public function translateNoEscaping($identifier)
|
||||
{
|
||||
$args = func_get_args();
|
||||
|
||||
array_shift($args);
|
||||
array_unshift($args, $this->get($identifier, $identifier));
|
||||
|
||||
return call_user_func_array(
|
||||
'sprintf',
|
||||
$args
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a formatted number
|
||||
*
|
||||
* $translator->number(1234.56);
|
||||
*
|
||||
* @access public
|
||||
* @param float $number Number to format
|
||||
* @return string
|
||||
*/
|
||||
public function number($number)
|
||||
{
|
||||
return number_format(
|
||||
$number,
|
||||
$this->get('number.decimals', 2),
|
||||
$this->get('number.decimals_separator', '.'),
|
||||
$this->get('number.thousands_separator', ',')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a formatted currency number
|
||||
*
|
||||
* $translator->currency(1234.56);
|
||||
*
|
||||
* @access public
|
||||
* @param float $amount Number to format
|
||||
* @return string
|
||||
*/
|
||||
public function currency($amount)
|
||||
{
|
||||
$position = $this->get('currency.position', 'before');
|
||||
$symbol = $this->get('currency.symbol', '$');
|
||||
$str = '';
|
||||
|
||||
if ($position === 'before') {
|
||||
$str .= $symbol;
|
||||
}
|
||||
|
||||
$str .= $this->number($amount);
|
||||
|
||||
if ($position === 'after') {
|
||||
$str .= ' '.$symbol;
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an identifier from the translations or return the default
|
||||
*
|
||||
* @access public
|
||||
* @param string $identifier Locale identifier
|
||||
* @param string $default Default value
|
||||
* @return string
|
||||
*/
|
||||
public function get($identifier, $default = '')
|
||||
{
|
||||
if (isset(self::$locales[$identifier])) {
|
||||
return self::$locales[$identifier];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translations
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @param string $languageCode Locale code: fr_FR
|
||||
* @param string $localeDir Locale folder
|
||||
* @return boolean True if the translation file has been loaded
|
||||
*/
|
||||
public static function load($languageCode, $localeDir = '')
|
||||
{
|
||||
if (! preg_match('/^[a-z]{2}(_[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?)?$/', $languageCode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($localeDir === '') {
|
||||
$localeDir = self::getDefaultFolder();
|
||||
}
|
||||
|
||||
$baseDir = realpath($localeDir);
|
||||
if ($baseDir === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$realFilePath = realpath(implode(DIRECTORY_SEPARATOR, [$localeDir, $languageCode, 'translations.php']));
|
||||
|
||||
if ($realFilePath !== false && strpos($realFilePath, $baseDir) === 0) {
|
||||
self::$locales = array_merge(self::$locales, require($realFilePath));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear locales stored in memory
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
*/
|
||||
public static function unload()
|
||||
{
|
||||
self::$locales = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default locales folder
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public static function getDefaultFolder()
|
||||
{
|
||||
return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', 'Locale'));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user