diff options
Diffstat (limited to 'app/Core')
-rw-r--r-- | app/Core/Action/ActionManager.php | 2 | ||||
-rw-r--r-- | app/Core/Base.php | 33 | ||||
-rw-r--r-- | app/Core/Cache/Base.php | 20 | ||||
-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 | 29 | ||||
-rw-r--r-- | app/Core/Http/OAuth2.php | 45 | ||||
-rw-r--r-- | app/Core/Http/Response.php | 32 | ||||
-rw-r--r-- | app/Core/Ldap/Client.php | 44 | ||||
-rw-r--r-- | app/Core/Ldap/Query.php | 6 | ||||
-rw-r--r-- | app/Core/Ldap/User.php | 3 | ||||
-rw-r--r-- | app/Core/Lexer.php | 161 | ||||
-rw-r--r-- | app/Core/Session/SessionStorage.php | 1 | ||||
-rw-r--r-- | app/Core/Template.php | 40 | ||||
-rw-r--r-- | app/Core/Thumbnail.php | 172 | ||||
-rw-r--r-- | app/Core/Tool.php | 74 | ||||
-rw-r--r-- | app/Core/User/Avatar/AvatarManager.php | 7 | ||||
-rw-r--r-- | app/Core/User/UserSession.php | 13 |
24 files changed, 983 insertions, 303 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 f87f271a..2b619af5 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -48,18 +48,11 @@ 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 * @property \Kanboard\Model\Board $board * @property \Kanboard\Model\Category $category * @property \Kanboard\Model\Color $color @@ -84,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 @@ -98,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 @@ -136,6 +127,14 @@ 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 $projectActivityQuery + * @property \Kanboard\Core\Filter\QueryBuilder $userQuery + * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery + * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery + * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer + * @property \Kanboard\Core\Filter\LexerBuilder $projectActivityLexer * @property \Psr\Log\LoggerInterface $logger * @property \PicoDb\Database $db * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher @@ -172,4 +171,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/Cache/Base.php b/app/Core/Cache/Base.php index 2879f1f1..d62b8507 100644 --- a/app/Core/Cache/Base.php +++ b/app/Core/Cache/Base.php @@ -11,26 +11,6 @@ namespace Kanboard\Core\Cache; abstract class Base { /** - * Fetch value from cache - * - * @abstract - * @access public - * @param string $key - * @return mixed Null when not found, cached value otherwise - */ - abstract public function get($key); - - /** - * Save a new value in the cache - * - * @abstract - * @access public - * @param string $key - * @param mixed $value - */ - abstract public function set($key, $value); - - /** * Proxy cache * * Note: Arguments must be scalar types 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 3764a67c..66f8d429 100644 --- a/app/Core/Helper.php +++ b/app/Core/Helper.php @@ -10,18 +10,23 @@ use Pimple\Container; * @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\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\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 + * @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 + * @property \Kanboard\Helper\ProjectActivityHelper $projectActivity */ class Helper { diff --git a/app/Core/Http/OAuth2.php b/app/Core/Http/OAuth2.php index 6fa1fb0a..211ca5b4 100644 --- a/app/Core/Http/OAuth2.php +++ b/app/Core/Http/OAuth2.php @@ -12,14 +12,14 @@ use Kanboard\Core\Base; */ class OAuth2 extends Base { - private $clientId; - private $secret; - private $callbackUrl; - private $authUrl; - private $tokenUrl; - private $scopes; - private $tokenType; - private $accessToken; + protected $clientId; + protected $secret; + protected $callbackUrl; + protected $authUrl; + protected $tokenUrl; + protected $scopes; + protected $tokenType; + protected $accessToken; /** * Create OAuth2 service @@ -46,6 +46,33 @@ class OAuth2 extends Base } /** + * Generate OAuth2 state and return the token value + * + * @access public + * @return string + */ + public function getState() + { + if (! isset($this->sessionStorage->oauthState) || empty($this->sessionStorage->oauthState)) { + $this->sessionStorage->oauthState = $this->token->getToken(); + } + + return $this->sessionStorage->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 @@ -58,6 +85,7 @@ class OAuth2 extends Base 'client_id' => $this->clientId, 'redirect_uri' => $this->callbackUrl, 'scope' => implode(' ', $this->scopes), + 'state' => $this->getState(), ); return $this->authUrl.'?'.http_build_query($params); @@ -94,6 +122,7 @@ class OAuth2 extends Base '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); diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php index d098f519..996fc58d 100644 --- a/app/Core/Http/Response.php +++ b/app/Core/Http/Response.php @@ -14,6 +14,24 @@ use Kanboard\Core\Csv; class Response extends Base { /** + * Send headers to cache a resource + * + * @access public + * @param integer $duration + * @param string $etag + */ + public function cache($duration, $etag = '') + { + header('Pragma: cache'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT'); + header('Cache-Control: public, max-age=' . $duration); + + if ($etag) { + header('ETag: "' . $etag . '"'); + } + } + + /** * Send no cache headers * * @access public @@ -214,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/Ldap/Client.php b/app/Core/Ldap/Client.php index 05658190..867d67fe 100644 --- a/app/Core/Ldap/Client.php +++ b/app/Core/Ldap/Client.php @@ -3,6 +3,7 @@ namespace Kanboard\Core\Ldap; use LogicException; +use Psr\Log\LoggerInterface; /** * LDAP Client @@ -21,6 +22,14 @@ class Client protected $ldap; /** + * Logger instance + * + * @access private + * @var LoggerInterface + */ + private $logger; + + /** * Establish LDAP connection * * @static @@ -165,4 +174,39 @@ class Client { 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; + } } diff --git a/app/Core/Ldap/Query.php b/app/Core/Ldap/Query.php index 1779fa61..7c1524ca 100644 --- a/app/Core/Ldap/Query.php +++ b/app/Core/Ldap/Query.php @@ -48,6 +48,12 @@ class Query */ public function execute($baseDn, $filter, array $attributes) { + 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); if ($sr === false) { return $this; diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php index 52283434..d23ec07e 100644 --- a/app/Core/Ldap/User.php +++ b/app/Core/Ldap/User.php @@ -44,8 +44,7 @@ class User */ public static function getUser(Client $client, $username) { - $className = get_called_class(); - $self = new $className(new Query($client)); + $self = new static(new Query($client)); return $self->find($self->getLdapUserPattern($username)); } 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; - } -} diff --git a/app/Core/Session/SessionStorage.php b/app/Core/Session/SessionStorage.php index 667d9253..6e2f9660 100644 --- a/app/Core/Session/SessionStorage.php +++ b/app/Core/Session/SessionStorage.php @@ -21,6 +21,7 @@ namespace Kanboard\Core\Session; * @property bool $boardCollapsed * @property bool $twoFactorBeforeCodeCalled * @property string $twoFactorSecret + * @property string $oauthState */ class SessionStorage { diff --git a/app/Core/Template.php b/app/Core/Template.php index f85c7f28..1874d44a 100644 --- a/app/Core/Template.php +++ b/app/Core/Template.php @@ -7,6 +7,21 @@ namespace Kanboard\Core; * * @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 { @@ -84,25 +99,26 @@ class Template /** * Find template filename * - * Core template name: 'task/show' - * Plugin template name: 'myplugin:task/show' + * Core template: 'task/show' or 'kanboard:task/show' + * Plugin template: 'myplugin:task/show' * * @access public - * @param string $template_name + * @param string $template * @return string */ - public function getTemplateFile($template_name) + public function getTemplateFile($template) { - $template_name = isset($this->overrides[$template_name]) ? $this->overrides[$template_name] : $template_name; + $plugin = ''; + $template = isset($this->overrides[$template]) ? $this->overrides[$template] : $template; + + if (strpos($template, ':') !== false) { + list($plugin, $template) = explode(':', $template); + } - if (strpos($template_name, ':') !== false) { - list($plugin, $template) = explode(':', $template_name); - $path = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'plugins'; - $path .= DIRECTORY_SEPARATOR.ucfirst($plugin).DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.$template.'.php'; - } else { - $path = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.$template_name.'.php'; + if ($plugin !== 'kanboard' && $plugin !== '') { + return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', '..', 'plugins', ucfirst($plugin), 'Template', $template.'.php')); } - return $path; + return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', 'Template', $template.'.php')); } } diff --git a/app/Core/Thumbnail.php b/app/Core/Thumbnail.php new file mode 100644 index 00000000..733d3a3c --- /dev/null +++ b/app/Core/Thumbnail.php @@ -0,0 +1,172 @@ +<?php + +namespace Kanboard\Core; + +/** + * Thumbnail Generator + * + * @package core + * @author Frederic Guillot + */ +class Thumbnail +{ + 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); + } + + $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); + } + + imagecopyresampled($this->dstImage, $this->srcImage, $dstX, $dstY, 0, 0, $dstWidth, $dstHeight, $srcWidth, $srcHeight); + + return $this; + } + + /** + * Save the thumbnail to a local file + * + * @access public + * @param string $filename + * @return Thumbnail + */ + public function toFile($filename) + { + imagejpeg($this->dstImage, $filename); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + return $this; + } + + /** + * Return the thumbnail as a string + * + * @access public + * @return string + */ + public function toString() + { + ob_start(); + imagejpeg($this->dstImage, null); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + return ob_get_clean(); + } + + /** + * Output the thumbnail directly to the browser or stdout + * + * @access public + */ + public function toOutput() + { + imagejpeg($this->dstImage, null); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + } +} diff --git a/app/Core/Tool.php b/app/Core/Tool.php index db2445a1..3423998d 100644 --- a/app/Core/Tool.php +++ b/app/Core/Tool.php @@ -75,78 +75,4 @@ class Tool return $container; } - - /** - * Generate a jpeg thumbnail from an image - * - * @static - * @access public - * @param string $src_file Source file image - * @param string $dst_file Destination file image - * @param integer $resize_width Desired image width - * @param integer $resize_height Desired image height - */ - public static function generateThumbnail($src_file, $dst_file, $resize_width = 250, $resize_height = 100) - { - $metadata = getimagesize($src_file); - $src_width = $metadata[0]; - $src_height = $metadata[1]; - $dst_y = 0; - $dst_x = 0; - - if (empty($metadata['mime'])) { - return; - } - - if ($resize_width == 0 && $resize_height == 0) { - $resize_width = 100; - $resize_height = 100; - } - - if ($resize_width > 0 && $resize_height == 0) { - $dst_width = $resize_width; - $dst_height = floor($src_height * ($resize_width / $src_width)); - $dst_image = imagecreatetruecolor($dst_width, $dst_height); - } elseif ($resize_width == 0 && $resize_height > 0) { - $dst_width = floor($src_width * ($resize_height / $src_height)); - $dst_height = $resize_height; - $dst_image = imagecreatetruecolor($dst_width, $dst_height); - } else { - $src_ratio = $src_width / $src_height; - $resize_ratio = $resize_width / $resize_height; - - if ($src_ratio <= $resize_ratio) { - $dst_width = $resize_width; - $dst_height = floor($src_height * ($resize_width / $src_width)); - - $dst_y = ($dst_height - $resize_height) / 2 * (-1); - } else { - $dst_width = floor($src_width * ($resize_height / $src_height)); - $dst_height = $resize_height; - - $dst_x = ($dst_width - $resize_width) / 2 * (-1); - } - - $dst_image = imagecreatetruecolor($resize_width, $resize_height); - } - - switch ($metadata['mime']) { - case 'image/jpeg': - case 'image/jpg': - $src_image = imagecreatefromjpeg($src_file); - break; - case 'image/png': - $src_image = imagecreatefrompng($src_file); - break; - case 'image/gif': - $src_image = imagecreatefromgif($src_file); - break; - default: - return; - } - - imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, 0, 0, $dst_width, $dst_height, $src_width, $src_height); - imagejpeg($dst_image, $dst_file); - imagedestroy($dst_image); - } } diff --git a/app/Core/User/Avatar/AvatarManager.php b/app/Core/User/Avatar/AvatarManager.php index 71bd8aa5..5b61cbdb 100644 --- a/app/Core/User/Avatar/AvatarManager.php +++ b/app/Core/User/Avatar/AvatarManager.php @@ -32,23 +32,25 @@ class AvatarManager } /** - * Render avatar html element + * Render avatar HTML element * * @access public * @param string $user_id * @param string $username * @param string $name * @param string $email + * @param string $avatar_path * @param int $size * @return string */ - public function render($user_id, $username, $name, $email, $size) + public function render($user_id, $username, $name, $email, $avatar_path, $size) { $user = array( 'id' => $user_id, 'username' => $username, 'name' => $name, 'email' => $email, + 'avatar_path' => $avatar_path, ); krsort($this->providers); @@ -80,6 +82,7 @@ class AvatarManager 'username' => '', 'name' => '?', 'email' => '', + 'avatar_path' => '', ); return $provider->render($user, $size); diff --git a/app/Core/User/UserSession.php b/app/Core/User/UserSession.php index e494e7b4..0034c47a 100644 --- a/app/Core/User/UserSession.php +++ b/app/Core/User/UserSession.php @@ -14,6 +14,19 @@ use Kanboard\Core\Security\Role; class UserSession extends Base { /** + * Refresh current session if necessary + * + * @access public + * @param integer $user_id + */ + public function refresh($user_id) + { + if ($this->getId() == $user_id) { + $this->initialize($this->user->getById($user_id)); + } + } + + /** * Update user session * * @access public |