diff options
Diffstat (limited to 'app/Core')
-rw-r--r-- | app/Core/Action/ActionManager.php | 2 | ||||
-rw-r--r-- | app/Core/Base.php | 30 | ||||
-rw-r--r-- | app/Core/ExternalLink/ExternalLinkManager.php | 2 | ||||
-rw-r--r-- | app/Core/Filter/CriteriaInterface.php | 40 | ||||
-rw-r--r-- | app/Core/Filter/FilterInterface.php | 56 | ||||
-rw-r--r-- | app/Core/Filter/FormatterInterface.php | 31 | ||||
-rw-r--r-- | app/Core/Filter/Lexer.php | 153 | ||||
-rw-r--r-- | app/Core/Filter/LexerBuilder.php | 151 | ||||
-rw-r--r-- | app/Core/Filter/OrCriteria.php | 68 | ||||
-rw-r--r-- | app/Core/Filter/QueryBuilder.php | 103 | ||||
-rw-r--r-- | app/Core/Helper.php | 2 | ||||
-rw-r--r-- | app/Core/Http/Response.php | 14 | ||||
-rw-r--r-- | app/Core/Lexer.php | 161 |
13 files changed, 640 insertions, 173 deletions
diff --git a/app/Core/Action/ActionManager.php b/app/Core/Action/ActionManager.php index f1ea8abe..dfa5a140 100644 --- a/app/Core/Action/ActionManager.php +++ b/app/Core/Action/ActionManager.php @@ -18,7 +18,7 @@ class ActionManager extends Base * List of automatic actions * * @access private - * @var array + * @var ActionBase[] */ private $actions = array(); diff --git a/app/Core/Base.php b/app/Core/Base.php index 74573e94..8c6b7620 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -48,16 +48,8 @@ use Pimple\Container; * @property \Kanboard\Core\User\UserSession $userSession * @property \Kanboard\Core\DateParser $dateParser * @property \Kanboard\Core\Helper $helper - * @property \Kanboard\Core\Lexer $lexer * @property \Kanboard\Core\Paginator $paginator * @property \Kanboard\Core\Template $template - * @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter - * @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter - * @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter - * @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter - * @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter - * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter - * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter * @property \Kanboard\Model\Action $action * @property \Kanboard\Model\ActionParameter $actionParameter * @property \Kanboard\Model\AvatarFile $avatarFile @@ -85,7 +77,6 @@ use Pimple\Container; * @property \Kanboard\Model\ProjectMetadata $projectMetadata * @property \Kanboard\Model\ProjectPermission $projectPermission * @property \Kanboard\Model\ProjectUserRole $projectUserRole - * @property \Kanboard\Model\projectUserRoleFilter $projectUserRoleFilter * @property \Kanboard\Model\ProjectGroupRole $projectGroupRole * @property \Kanboard\Model\ProjectNotification $projectNotification * @property \Kanboard\Model\ProjectNotificationType $projectNotificationType @@ -99,7 +90,6 @@ use Pimple\Container; * @property \Kanboard\Model\TaskDuplication $taskDuplication * @property \Kanboard\Model\TaskExternalLink $taskExternalLink * @property \Kanboard\Model\TaskFinder $taskFinder - * @property \Kanboard\Model\TaskFilter $taskFilter * @property \Kanboard\Model\TaskLink $taskLink * @property \Kanboard\Model\TaskModification $taskModification * @property \Kanboard\Model\TaskPermission $taskPermission @@ -137,6 +127,12 @@ use Pimple\Container; * @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 $userQuery + * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery + * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery + * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer * @property \Psr\Log\LoggerInterface $logger * @property \PicoDb\Database $db * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher @@ -173,4 +169,18 @@ abstract class Base { return $this->container[$name]; } + + /** + * Get object instance + * + * @static + * @access public + * @param Container $container + * @return static + */ + public static function getInstance(Container $container) + { + $self = new static($container); + return $self; + } } diff --git a/app/Core/ExternalLink/ExternalLinkManager.php b/app/Core/ExternalLink/ExternalLinkManager.php index 1fa423c2..804e6b34 100644 --- a/app/Core/ExternalLink/ExternalLinkManager.php +++ b/app/Core/ExternalLink/ExternalLinkManager.php @@ -23,7 +23,7 @@ class ExternalLinkManager extends Base * Registered providers * * @access private - * @var array + * @var ExternalLinkProviderInterface[] */ private $providers = array(); diff --git a/app/Core/Filter/CriteriaInterface.php b/app/Core/Filter/CriteriaInterface.php new file mode 100644 index 00000000..009c4bd3 --- /dev/null +++ b/app/Core/Filter/CriteriaInterface.php @@ -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(); +} diff --git a/app/Core/Filter/FilterInterface.php b/app/Core/Filter/FilterInterface.php new file mode 100644 index 00000000..7b66ec28 --- /dev/null +++ b/app/Core/Filter/FilterInterface.php @@ -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(); +} diff --git a/app/Core/Filter/FormatterInterface.php b/app/Core/Filter/FormatterInterface.php new file mode 100644 index 00000000..b7c04c51 --- /dev/null +++ b/app/Core/Filter/FormatterInterface.php @@ -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 FormatterInterface + */ + public function withQuery(Table $query); + + /** + * Apply formatter + * + * @access public + * @return mixed + */ + public function format(); +} diff --git a/app/Core/Filter/Lexer.php b/app/Core/Filter/Lexer.php new file mode 100644 index 00000000..041b58d3 --- /dev/null +++ b/app/Core/Filter/Lexer.php @@ -0,0 +1,153 @@ +<?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_DATE', + '/^(yesterday|tomorrow|today)/' => 'T_DATE', + '/^("(.*?)")/' => 'T_STRING', + "/^(\w+)/" => '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; + + while (isset($input[$this->offset])) { + $result = $this->match(substr($input, $this->offset)); + + 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 += strlen($matches[1]); + + return array( + 'match' => trim($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 && in_array($next['token'], array('T_STRING', 'T_DATE'))) { + $map[$token['token']][] = $next['match']; + } + } + + next($tokens); + } + + $leftOver = trim($leftOver); + + if ($this->defaultToken !== '' && $leftOver !== '') { + $map[$this->defaultToken] = array($leftOver); + } + + return $map; + } +} diff --git a/app/Core/Filter/LexerBuilder.php b/app/Core/Filter/LexerBuilder.php new file mode 100644 index 00000000..7a9a714f --- /dev/null +++ b/app/Core/Filter/LexerBuilder.php @@ -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:)/", $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; + } +} diff --git a/app/Core/Filter/OrCriteria.php b/app/Core/Filter/OrCriteria.php new file mode 100644 index 00000000..174b8458 --- /dev/null +++ b/app/Core/Filter/OrCriteria.php @@ -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; + } +} diff --git a/app/Core/Filter/QueryBuilder.php b/app/Core/Filter/QueryBuilder.php new file mode 100644 index 00000000..3de82b63 --- /dev/null +++ b/app/Core/Filter/QueryBuilder.php @@ -0,0 +1,103 @@ +<?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; + + /** + * 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; + } +} diff --git a/app/Core/Helper.php b/app/Core/Helper.php index 3a66fbd0..ab1f8f76 100644 --- a/app/Core/Helper.php +++ b/app/Core/Helper.php @@ -12,10 +12,12 @@ use Pimple\Container; * * @property \Kanboard\Helper\AppHelper $app * @property \Kanboard\Helper\AssetHelper $asset + * @property \Kanboard\Helper\CalendarHelper $calendar * @property \Kanboard\Helper\DateHelper $dt * @property \Kanboard\Helper\FileHelper $file * @property \Kanboard\Helper\FormHelper $form * @property \Kanboard\Helper\HookHelper $hook + * @property \Kanboard\Helper\ICalHelper $ical * @property \Kanboard\Helper\ModelHelper $model * @property \Kanboard\Helper\SubtaskHelper $subtask * @property \Kanboard\Helper\TaskHelper $task diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php index 37349ca5..996fc58d 100644 --- a/app/Core/Http/Response.php +++ b/app/Core/Http/Response.php @@ -232,6 +232,20 @@ class Response extends Base } /** + * Send a iCal response + * + * @access public + * @param string $data Raw data + * @param integer $status_code HTTP status code + */ + public function ical($data, $status_code = 200) + { + $this->status($status_code); + $this->contentType('text/calendar; charset=utf-8'); + echo $data; + } + + /** * Send the security header: Content-Security-Policy * * @access public diff --git a/app/Core/Lexer.php b/app/Core/Lexer.php deleted file mode 100644 index df2d90ae..00000000 --- a/app/Core/Lexer.php +++ /dev/null @@ -1,161 +0,0 @@ -<?php - -namespace Kanboard\Core; - -/** - * Lexer - * - * @package core - * @author Frederic Guillot - */ -class Lexer -{ - /** - * Current position - * - * @access private - * @var integer - */ - private $offset = 0; - - /** - * Token map - * - * @access private - * @var array - */ - private $tokenMap = array( - "/^(assignee:)/" => 'T_ASSIGNEE', - "/^(color:)/" => 'T_COLOR', - "/^(due:)/" => 'T_DUE', - "/^(updated:)/" => 'T_UPDATED', - "/^(modified:)/" => 'T_UPDATED', - "/^(created:)/" => 'T_CREATED', - "/^(status:)/" => 'T_STATUS', - "/^(description:)/" => 'T_DESCRIPTION', - "/^(category:)/" => 'T_CATEGORY', - "/^(column:)/" => 'T_COLUMN', - "/^(project:)/" => 'T_PROJECT', - "/^(swimlane:)/" => 'T_SWIMLANE', - "/^(ref:)/" => 'T_REFERENCE', - "/^(reference:)/" => 'T_REFERENCE', - "/^(link:)/" => 'T_LINK', - "/^(\s+)/" => 'T_WHITESPACE', - '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE', - '/^(yesterday|tomorrow|today)/' => 'T_DATE', - '/^("(.*?)")/' => 'T_STRING', - "/^(\w+)/" => 'T_STRING', - "/^(#\d+)/" => 'T_STRING', - ); - - /** - * Tokenize input string - * - * @access public - * @param string $input - * @return array - */ - public function tokenize($input) - { - $tokens = array(); - $this->offset = 0; - - while (isset($input[$this->offset])) { - $result = $this->match(substr($input, $this->offset)); - - if ($result === false) { - return array(); - } - - $tokens[] = $result; - } - - return $tokens; - } - - /** - * Find a token that match and move the offset - * - * @access public - * @param string $string - * @return array|boolean - */ - public function match($string) - { - foreach ($this->tokenMap as $pattern => $name) { - if (preg_match($pattern, $string, $matches)) { - $this->offset += strlen($matches[1]); - - return array( - 'match' => trim($matches[1], '"'), - 'token' => $name, - ); - } - } - - return false; - } - - /** - * Change the output of tokenizer to be easily parsed by the database filter - * - * Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title'] - * - * @access public - * @param array $tokens - * @return array - */ - public function map(array $tokens) - { - $map = array( - 'T_TITLE' => '', - ); - - while (false !== ($token = current($tokens))) { - switch ($token['token']) { - case 'T_ASSIGNEE': - case 'T_COLOR': - case 'T_CATEGORY': - case 'T_COLUMN': - case 'T_PROJECT': - case 'T_SWIMLANE': - case 'T_LINK': - $next = next($tokens); - - if ($next !== false && $next['token'] === 'T_STRING') { - $map[$token['token']][] = $next['match']; - } - - break; - - case 'T_STATUS': - case 'T_DUE': - case 'T_UPDATED': - case 'T_CREATED': - case 'T_DESCRIPTION': - case 'T_REFERENCE': - $next = next($tokens); - - if ($next !== false && ($next['token'] === 'T_DATE' || $next['token'] === 'T_STRING')) { - $map[$token['token']] = $next['match']; - } - - break; - - default: - $map['T_TITLE'] .= $token['match']; - break; - } - - next($tokens); - } - - $map['T_TITLE'] = trim($map['T_TITLE']); - - if (empty($map['T_TITLE'])) { - unset($map['T_TITLE']); - } - - return $map; - } -} |