summaryrefslogtreecommitdiff
path: root/app/Core
diff options
context:
space:
mode:
Diffstat (limited to 'app/Core')
-rw-r--r--app/Core/Action/ActionManager.php2
-rw-r--r--app/Core/Base.php33
-rw-r--r--app/Core/Cache/Base.php20
-rw-r--r--app/Core/ExternalLink/ExternalLinkManager.php2
-rw-r--r--app/Core/Filter/CriteriaInterface.php40
-rw-r--r--app/Core/Filter/FilterInterface.php56
-rw-r--r--app/Core/Filter/FormatterInterface.php31
-rw-r--r--app/Core/Filter/Lexer.php153
-rw-r--r--app/Core/Filter/LexerBuilder.php151
-rw-r--r--app/Core/Filter/OrCriteria.php68
-rw-r--r--app/Core/Filter/QueryBuilder.php103
-rw-r--r--app/Core/Helper.php29
-rw-r--r--app/Core/Http/OAuth2.php45
-rw-r--r--app/Core/Http/Response.php32
-rw-r--r--app/Core/Ldap/Client.php44
-rw-r--r--app/Core/Ldap/Query.php6
-rw-r--r--app/Core/Ldap/User.php3
-rw-r--r--app/Core/Lexer.php161
-rw-r--r--app/Core/Session/SessionStorage.php1
-rw-r--r--app/Core/Template.php40
-rw-r--r--app/Core/Thumbnail.php172
-rw-r--r--app/Core/Tool.php74
-rw-r--r--app/Core/User/Avatar/AvatarManager.php7
-rw-r--r--app/Core/User/UserSession.php13
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