Merge pull request #1 from fguillot/master

This commit is contained in:
Imbasaur 2016-04-13 17:05:59 +02:00
commit 99f275e5bb
347 changed files with 8752 additions and 4773 deletions

1
.gitattributes vendored
View File

@ -7,6 +7,7 @@ app/constants.php export-subst
.scrutinizer.yml export-ignore
.travis.yml export-ignore
Dockerfile export-ignore
docker-compose.yml export-ignore
Makefile export-ignore
README.md export-ignore
Vagrantfile export-ignore

View File

@ -23,7 +23,6 @@ before_script:
- phpenv config-rm xdebug.ini
- phpenv config-add tests/php.ini
- composer install
- php -i
script:
- phpunit -c tests/units.$DB.xml

View File

@ -29,7 +29,7 @@ Contributors:
- [Daniel Raknes](https://github.com/danielraknes)
- [David-Norris](https://github.com/David-Norris)
- [Dmitry](https://github.com/dmkcv)
- [Djpadz](https://github.com/djpadz)
- [Dj Padzensky](https://github.com/djpadz)
- [Draza (bdpsoft)](https://github.com/bdpsoft)
- [Eskiso](https://github.com/eSkiSo)
- [Esteban Monge](https://github.com/EstebanMonge)
@ -120,6 +120,7 @@ Contributors:
- [Vladimir Babin](https://github.com/Chiliec)
- [Yannick Ihmels](https://github.com/ihmels)
- [Ybarc](https://github.com/ybarc)
- [Yu Yongwoo](https://github.com/uyu423)
- [Yuichi Murata](https://github.com/yuichi1004)
There is also many people who have reported bugs or proposed awesome ideas.
There is also many people who have reported bugs or proposed awesome ideas.

View File

@ -1,14 +1,41 @@
Version 1.0.27 (unreleased)
Version 1.0.28 (unreleased)
--------------
New features:
* Search in activity stream
* Search in comments
* Search by task creator
* Added command line utility to reset user password and to disable 2FA
Improvements:
* Filter/Lexer/QueryBuilder refactoring
Bug fixes:
* Removed PHP notices in comment suppression view
Version 1.0.27
--------------
New features:
* Added Markdown editor
* Added letter avatar provider
* Added pluggable Avatar providers
* Added user avatars with pluggable system
- Default is a letter based avatar
- Gravatar
- Avatar Image upload
* Added Korean translation
Improvements:
* Added more logging for LDAP client
* Improve schema migration process
* Improve notification configuration form
* Handle state in OAuth2 client
* Allow to use the original template in overridden templates
* Unification of the project header
* Refactoring of Javascript code
* Improve comments design
* Improve task summary sections
@ -27,6 +54,7 @@ Improvements:
Bug fixes:
* Fix bad unique constraints in Mysql table user_has_notifications
* Force integer type for aggregated metrics (Burndown chart concat values instead of summing)
* Fixes cycle time calculation when the start date is defined in the future
* Access allowed to any tasks from the shared public board by changing the URL parameters

View File

@ -6,7 +6,7 @@ CSS_VENDOR = $(addprefix assets/css/vendor/, $(addsuffix .css, jquery-ui.min jqu
JS_APP = $(addprefix assets/js/src/, $(addsuffix .js, Namespace App Dropdown Popover Notification Accordion Session Calendar AvgTimeColumnChart BurndownChart CompareHoursColumnChart CumulativeFlowDiagram LeadCycleTimeChart UserRepartitionChart TaskTimeColumnChart TaskRepartitionChart Gantt Column Markdown ProjectPermission ProjectCreation Screenshot FileUpload Search Task Subtask Swimlane BoardColumnView BoardColumnScrolling BoardHorizontalScrolling BoardCollapsedMode BoardDragAndDrop BoardTask BoardPolling Tooltip Bootstrap))
JS_VENDOR = $(addprefix assets/js/vendor/, $(addsuffix .js, jquery-1.11.3.min jquery-ui.min jquery-ui-timepicker-addon.min jquery.ui.touch-punch.min chosen.jquery.min moment.min fullcalendar.min mousetrap.min mousetrap-global-bind.min simplemde.min))
JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es el fi fr hu id it ja nl nb pl pt pt-br ru sv sr th tr zh-cn))
JS_LANG = $(addprefix assets/js/vendor/lang/, $(addsuffix .js, cs da de es el fi fr hu id it ja ko nl nb pl pt pt-br ru sv sr th tr zh-cn))
all: css js
@ -56,6 +56,7 @@ archive:
@ rm -rf ${BUILD_DIR}/kanboard/Makefile
@ rm -rf ${BUILD_DIR}/kanboard/Vagrantfile
@ rm -rf ${BUILD_DIR}/kanboard/Dockerfile
@ rm -rf ${BUILD_DIR}/kanboard/docker-compose.yml
@ rm -rf ${BUILD_DIR}/kanboard/.*.yml
@ rm -rf ${BUILD_DIR}/kanboard/*.md
@ rm -rf ${BUILD_DIR}/kanboard/*.markdown
@ -71,40 +72,12 @@ archive:
@ find ${BUILD_DIR}/kanboard/vendor -name .travis.yml -delete
@ find ${BUILD_DIR}/kanboard/vendor -name README.* -delete
@ find ${BUILD_DIR}/kanboard/vendor -name .gitignore -delete
@ cd ${BUILD_DIR}/kanboard && sed -i.bak s/master/${version}/g app/constants.php && rm -f app/*.bak
@ cd ${BUILD_DIR}/kanboard && sed -i.bak 11s/.*/"define('APP_VERSION', '${version}');"/g app/constants.php && rm -f app/*.bak
@ cd ${BUILD_DIR} && zip -r kanboard-${version}.zip kanboard > /dev/null
@ cd ${BUILD_DIR} && mv kanboard-${version}.zip ${dst}
@ cd ${dst} && if [ -L kanboard-latest.zip ]; then unlink kanboard-latest.zip; ln -s kanboard-${version}.zip kanboard-latest.zip; fi
@ rm -rf ${BUILD_DIR}/kanboard
test-archive:
@ echo "Build archive with tests: version=${version}, destination=${dst}"
@ rm -rf ${BUILD_DIR}/kanboard ${BUILD_DIR}/kanboard-*.zip
@ cd ${BUILD_DIR} && git clone --depth 1 -q https://github.com/fguillot/kanboard.git
@ cd ${BUILD_DIR}/kanboard && composer --prefer-dist --optimize-autoloader --quiet install
@ rm -rf ${BUILD_DIR}/kanboard/data/*
@ rm -rf ${BUILD_DIR}/kanboard/.git*
@ rm -rf ${BUILD_DIR}/kanboard/.*.yml
@ rm -rf ${BUILD_DIR}/kanboard/*.md
@ rm -rf ${BUILD_DIR}/kanboard/*.markdown
@ rm -rf ${BUILD_DIR}/kanboard/Dockerfile
@ rm -rf ${BUILD_DIR}/kanboard/.docker
@ rm -rf ${BUILD_DIR}/kanboard/Vagrantfile
@ rm -rf ${BUILD_DIR}/kanboard/app.json
@ rm -rf ${BUILD_DIR}/kanboard/plugins/.gitignore
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name notes -type d -exec rm -rf {} +;
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name test -type d -exec rm -rf {} +;
@ cd ${BUILD_DIR}/kanboard && find ./vendor -name tests -type d -exec rm -rf {} +;
@ find ${BUILD_DIR}/kanboard/vendor -name composer.json -delete
@ find ${BUILD_DIR}/kanboard/vendor -name phpunit.xml -delete
@ find ${BUILD_DIR}/kanboard/vendor -name .travis.yml -delete
@ find ${BUILD_DIR}/kanboard/vendor -name README.* -delete
@ find ${BUILD_DIR}/kanboard/vendor -name .gitignore -delete
@ cd ${BUILD_DIR}/kanboard && sed -i.bak s/master/${version}/g app/constants.php && rm -f app/*.bak
@ cd ${BUILD_DIR} && zip -r kanboard-${version}.zip kanboard > /dev/null
@ cd ${BUILD_DIR} && mv kanboard-${version}.zip ${dst}
@ rm -rf ${BUILD_DIR}/kanboard
test-sqlite-coverage:
@ phpunit --coverage-html /tmp/coverage --whitelist app/ -c tests/units.sqlite.xml

View File

@ -20,7 +20,7 @@ Official website: <http://kanboard.net>
- [Change Log](https://github.com/fguillot/kanboard/blob/master/ChangeLog)
- [Documentation](https://github.com/fguillot/kanboard/blob/master/doc/index.markdown)
[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy)
[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/fguillot/kanboard)
Authors
-------

96
Vagrantfile vendored
View File

@ -1,54 +1,16 @@
$debian_script = <<SCRIPT
apt-get update
apt-get install -y apache2 php5 php5-gd php5-curl php5-sqlite php5-xdebug php5-ldap
apt-get clean -y
$script = <<SCRIPT
apt-get install -y apache2 php5 php5-sqlite php5-mysql php5-pgsql php5-gd curl unzip && \
apt-get clean && \
echo "ServerName localhost" >> /etc/apache2/apache2.conf && \
sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf && \
a2enmod rewrite
sudo sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf
if [ -f /etc/apache2/sites-enabled/000-default ]; then
sudo sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/apache2/sites-enabled/000-default
fi
sudo a2enmod rewrite
service apache2 restart
rm -f /var/www/html/index.html
date > /etc/vagrant_provisioned_at
wget -q https://getcomposer.org/composer.phar
chmod +x composer.phar
sudo mv composer.phar /usr/local/bin/composer
wget -q https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
SCRIPT
$centos_script = <<SCRIPT
sudo yum update -y
sudo yum install -y httpd php php-cli php-gd php-ldap php-mbstring php-mysql php-pdo php-pgsql php-xml wget
sudo sed -ri 's/AllowOverride None/AllowOverride All/g' /etc/httpd/conf/httpd.conf
if [ -x /usr/bin/systemctl ]; then
sudo systemctl restart httpd
sudo systemctl enable httpd
sudo chcon -R -t httpd_sys_content_rw_t /var/www/html/data
sudo setsebool -P httpd_can_network_connect=1
else
sudo service httpd restart
sudo chkconfig httpd on
fi
rm -f /var/www/html/index.html
date > /etc/vagrant_provisioned_at
wget -q https://getcomposer.org/composer.phar
chmod +x composer.phar
sudo mv composer.phar /usr/local/bin/composer
wget -q https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
@ -59,52 +21,8 @@ Vagrant.configure("2") do |config|
config.vm.define "ubuntu" do |m|
m.vm.box = "ubuntu/trusty64"
m.vm.provision "shell", inline: $debian_script
m.vm.provision "shell", inline: $script
m.vm.synced_folder ".", "/var/www/html", owner: "www-data", group: "www-data"
m.vm.network :forwarded_port, guest: 80, host: 8001
end
config.vm.define "debian8" do |m|
m.vm.box = "debian/jessie64"
m.vm.provision "shell", inline: $debian_script
m.vm.synced_folder ".", "/var/www/html", owner: "www-data", group: "www-data"
m.vm.network :forwarded_port, guest: 80, host: 8002
end
config.vm.define "debian7" do |m|
m.vm.box = "debian/wheezy64"
m.vm.provision "shell", inline: $debian_script
m.vm.synced_folder ".", "/var/www", owner: "www-data", group: "www-data"
m.vm.network :forwarded_port, guest: 80, host: 8003
end
config.vm.define "debian6" do |m|
m.vm.box = "bento/debian-6.0.10"
m.vm.provision "shell", inline: $debian_script
m.vm.synced_folder ".", "/var/www", owner: "www-data", group: "www-data"
m.vm.network :forwarded_port, guest: 80, host: 8004
end
config.vm.define "centos7" do |m|
m.vm.box = "centos/7"
m.vm.provision "shell", inline: $centos_script
m.vm.synced_folder ".", "/var/www/html", owner: "apache", group: "apache", type: "rsync",
rsync__exclude: ".git/", rsync__auto: true
m.vm.network :forwarded_port, guest: 80, host: 8005
end
config.vm.define "centos6" do |m|
m.vm.box = "bento/centos-6.7"
m.vm.provision "shell", inline: $centos_script
m.vm.synced_folder ".", "/var/www/html", owner: "apache", group: "apache", type: "rsync",
rsync__exclude: ".git/", rsync__auto: true
m.vm.network :forwarded_port, guest: 80, host: 8006
end
config.vm.define "freebsd10" do |m|
m.vm.box = "freebsd/FreeBSD-10.2-STABLE"
m.vm.base_mac = "080027D14C66"
m.ssh.shell = "sh"
m.vm.network :forwarded_port, guest: 80, host: 8007
end
end

View File

@ -33,7 +33,7 @@ class Me extends Base
public function getMyActivityStream()
{
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
return $this->projectActivity->getProjects($project_ids, 100);
return $this->helper->projectActivity->getProjectsEvents($project_ids, 100);
}
public function createMyPrivateProject($name, $description = null)

View File

@ -53,13 +53,13 @@ class Project extends Base
public function getProjectActivities(array $project_ids)
{
return $this->projectActivity->getProjects($project_ids);
return $this->helper->projectActivity->getProjectsEvents($project_ids);
}
public function getProjectActivity($project_id)
{
$this->checkProjectPermission($project_id);
return $this->projectActivity->getProject($project_id);
return $this->helper->projectActivity->getProjectEvents($project_id);
}
public function createProject($name, $description = null)

View File

@ -87,6 +87,7 @@ class User extends \Kanboard\Core\Base
try {
$ldap = LdapClient::connect();
$ldap->setLogger($this->logger);
$user = LdapUser::getUser($ldap, $username);
if ($user === null) {

View File

@ -63,10 +63,12 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
try {
$client = LdapClient::connect($this->getLdapUsername(), $this->getLdapPassword());
$client->setLogger($this->logger);
$user = LdapUser::getUser($client, $this->username);
if ($user === null) {
$this->logger->info('User not found in LDAP server');
$this->logger->info('User ('.$this->username.') not found in LDAP server');
return false;
}
@ -74,6 +76,8 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
}
$this->logger->info('Authenticate user: '.$user->getDn());
if ($client->authenticate($user->getDn(), $this->password)) {
$this->userInfo = $user;
return true;

View File

@ -11,6 +11,7 @@ use Symfony\Component\Console\Command\Command;
* @package console
* @author Frederic Guillot
*
* @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
* @property \Kanboard\Export\SubtaskExport $subtaskExport
* @property \Kanboard\Export\TaskExport $taskExport
* @property \Kanboard\Export\TransitionExport $transitionExport
@ -21,11 +22,12 @@ use Symfony\Component\Console\Command\Command;
* @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
* @property \Kanboard\Model\Task $task
* @property \Kanboard\Model\TaskFinder $taskFinder
* @property \Kanboard\Model\User $user
* @property \Kanboard\Model\UserNotification $userNotification
* @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/
abstract class Base extends Command
abstract class BaseCommand extends Command
{
/**
* Container instance

View File

@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\NullOutput;
class Cronjob extends Base
class CronjobCommand extends BaseCommand
{
private $commands = array(
'projects:daily-stats',

View File

@ -7,7 +7,7 @@ use RecursiveDirectoryIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LocaleComparator extends Base
class LocaleComparatorCommand extends BaseCommand
{
const REF_LOCALE = 'fr_FR';

View File

@ -6,7 +6,7 @@ use DirectoryIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LocaleSync extends Base
class LocaleSyncCommand extends BaseCommand
{
const REF_LOCALE = 'fr_FR';

View File

@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ProjectDailyColumnStatsExport extends Base
class ProjectDailyColumnStatsExportCommand extends BaseCommand
{
protected function configure()
{

View File

@ -6,7 +6,7 @@ use Kanboard\Model\Project;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ProjectDailyStatsCalculation extends Base
class ProjectDailyStatsCalculationCommand extends BaseCommand
{
protected function configure()
{

View File

@ -0,0 +1,79 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
class ResetPasswordCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('user:reset-password')
->setDescription('Change user password')
->addArgument('username', InputArgument::REQUIRED, 'Username')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$helper = $this->getHelper('question');
$username = $input->getArgument('username');
$passwordQuestion = new Question('What is the new password for '.$username.'? (characters are not printed)'.PHP_EOL);
$passwordQuestion->setHidden(true);
$passwordQuestion->setHiddenFallback(false);
$password = $helper->ask($input, $output, $passwordQuestion);
$confirmationQuestion = new Question('Confirmation:'.PHP_EOL);
$confirmationQuestion->setHidden(true);
$confirmationQuestion->setHiddenFallback(false);
$confirmation = $helper->ask($input, $output, $confirmationQuestion);
if ($this->validatePassword($output, $password, $confirmation)) {
$this->resetPassword($output, $username, $password);
}
}
private function validatePassword(OutputInterface $output, $password, $confirmation)
{
list($valid, $errors) = $this->passwordResetValidator->validateModification(array(
'password' => $password,
'confirmation' => $confirmation,
));
if (!$valid) {
foreach ($errors as $error_list) {
foreach ($error_list as $error) {
$output->writeln('<error>'.$error.'</error>');
}
}
}
return $valid;
}
private function resetPassword(OutputInterface $output, $username, $password)
{
$userId = $this->user->getIdByUsername($username);
if (empty($userId)) {
$output->writeln('<error>User not found</error>');
return false;
}
if (!$this->user->update(array('id' => $userId, 'password' => $password))) {
$output->writeln('<error>Unable to update password</error>');
return false;
}
$output->writeln('<info>Password updated successfully</info>');
return true;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Console;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ResetTwoFactorCommand extends BaseCommand
{
protected function configure()
{
$this
->setName('user:reset-2fa')
->setDescription('Remove two-factor authentication for a user')
->addArgument('username', InputArgument::REQUIRED, 'Username');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$username = $input->getArgument('username');
$userId = $this->user->getIdByUsername($username);
if (empty($userId)) {
$output->writeln('<error>User not found</error>');
return false;
}
if (!$this->user->update(array('id' => $userId, 'twofactor_activated' => 0, 'twofactor_secret' => ''))) {
$output->writeln('<error>Unable to update user profile</error>');
return false;
}
$output->writeln('<info>Two-factor authentication disabled</info>');
return true;
}
}

View File

@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SubtaskExport extends Base
class SubtaskExportCommand extends BaseCommand
{
protected function configure()
{

View File

@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class TaskExport extends Base
class TaskExportCommand extends BaseCommand
{
protected function configure()
{

View File

@ -8,7 +8,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class TaskOverdueNotification extends Base
class TaskOverdueNotificationCommand extends BaseCommand
{
protected function configure()
{

View File

@ -7,7 +7,7 @@ use Symfony\Component\Console\Output\OutputInterface;
use Kanboard\Model\Task;
use Kanboard\Event\TaskListEvent;
class TaskTrigger extends Base
class TaskTriggerCommand extends BaseCommand
{
protected function configure()
{

View File

@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class TransitionExport extends Base
class TransitionExportCommand extends BaseCommand
{
protected function configure()
{

View File

@ -20,7 +20,7 @@ class Activity extends Base
$project = $this->getProject();
$this->response->html($this->helper->layout->app('activity/project', array(
'events' => $this->projectActivity->getProject($project['id']),
'events' => $this->helper->projectActivity->getProjectEvents($project['id']),
'project' => $project,
'title' => t('%s\'s activity', $project['name'])
)));
@ -38,7 +38,8 @@ class Activity extends Base
$this->response->html($this->helper->layout->task('activity/task', array(
'title' => $task['title'],
'task' => $task,
'events' => $this->projectActivity->getTask($task['id']),
'project' => $this->project->getById($task['project_id']),
'events' => $this->helper->projectActivity->getTaskEvents($task['id']),
)));
}
}

View File

@ -2,6 +2,7 @@
namespace Kanboard\Controller;
use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Model\Task as TaskModel;
/**
@ -44,14 +45,15 @@ class Analytic extends Base
public function compareHours()
{
$project = $this->getProject();
$params = $this->getProjectFilters('analytic', 'compareHours');
$query = $this->taskFilter->create()->filterByProject($params['project']['id'])->getQuery();
$paginator = $this->paginator
->setUrl('analytic', 'compareHours', array('project_id' => $project['id']))
->setMax(30)
->setOrder(TaskModel::TABLE.'.id')
->setQuery($query)
->setQuery($this->taskQuery
->withFilter(new TaskProjectFilter($project['id']))
->getQuery()
)
->calculate();
$this->response->html($this->helper->layout->analytic('analytic/compare_hours', array(

View File

@ -157,7 +157,7 @@ class App extends Base
$this->response->html($this->helper->layout->dashboard('app/activity', array(
'title' => t('My activity stream'),
'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100),
'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermission->getActiveProjectIds($user['id']), 100),
'user' => $user,
)));
}

View File

@ -0,0 +1,92 @@
<?php
namespace Kanboard\Controller;
use Kanboard\Core\ObjectStorage\ObjectStorageException;
use Kanboard\Core\Thumbnail;
/**
* Avatar File Controller
*
* @package controller
* @author Frederic Guillot
*/
class AvatarFile extends Base
{
/**
* Display avatar page
*/
public function show()
{
$user = $this->getUser();
$this->response->html($this->helper->layout->user('avatar_file/show', array(
'user' => $user,
)));
}
/**
* Upload Avatar
*/
public function upload()
{
$user = $this->getUser();
if (! $this->avatarFile->uploadFile($user['id'], $this->request->getFileInfo('avatar'))) {
$this->flash->failure(t('Unable to upload the file.'));
}
$this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id'])));
}
/**
* Remove Avatar image
*/
public function remove()
{
$this->checkCSRFParam();
$user = $this->getUser();
$this->avatarFile->remove($user['id']);
$this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id'])));
}
/**
* Show Avatar image (public)
*/
public function image()
{
$user_id = $this->request->getIntegerParam('user_id');
$size = $this->request->getStringParam('size', 48);
$filename = $this->avatarFile->getFilename($user_id);
$etag = md5($filename.$size);
$this->response->cache(365 * 86400, $etag);
$this->response->contentType('image/jpeg');
if ($this->request->getHeader('If-None-Match') !== '"'.$etag.'"') {
$this->render($filename, $size);
} else {
$this->response->status(304);
}
}
/**
* Render thumbnail from object storage
*
* @access private
* @param string $filename
* @param integer $size
*/
private function render($filename, $size)
{
try {
$blob = $this->objectStorage->get($filename);
Thumbnail::createFromString($blob)
->resize($size, $size)
->toOutput();
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
}
}
}

View File

@ -287,60 +287,4 @@ abstract class Base extends \Kanboard\Core\Base
return $subtask;
}
/**
* Common method to get project filters
*
* @access protected
* @param string $controller
* @param string $action
* @return array
*/
protected function getProjectFilters($controller, $action)
{
$project = $this->getProject();
$search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id']));
$board_selector = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
unset($board_selector[$project['id']]);
$filters = array(
'controller' => $controller,
'action' => $action,
'project_id' => $project['id'],
'search' => urldecode($search),
);
$this->userSession->setFilters($project['id'], $filters['search']);
return array(
'project' => $project,
'board_selector' => $board_selector,
'filters' => $filters,
'title' => $project['name'],
'description' => $this->getProjectDescription($project),
);
}
/**
* Get project description
*
* @access protected
* @param array &$project
* @return string
*/
protected function getProjectDescription(array &$project)
{
if ($project['owner_id'] > 0) {
$description = t('Project owner: ').'**'.$this->helper->text->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL;
if (! empty($project['description'])) {
$description .= '***'.PHP_EOL.PHP_EOL;
$description .= $project['description'];
}
} else {
$description = $project['description'];
}
return $description;
}
}

View File

@ -2,6 +2,8 @@
namespace Kanboard\Controller;
use Kanboard\Formatter\BoardFormatter;
/**
* Board controller
*
@ -47,16 +49,19 @@ class Board extends Base
*/
public function show()
{
$params = $this->getProjectFilters('board', 'show');
$project = $this->getProject();
$search = $this->helper->projectHeader->getSearchQuery($project);
$this->response->html($this->helper->layout->app('board/view_private', array(
'categories_list' => $this->category->getList($params['project']['id'], false),
'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()),
'swimlanes' => $this->taskFilter->search($params['filters']['search'])->getBoard($params['project']['id']),
'project' => $project,
'title' => $project['name'],
'description' => $this->helper->projectHeader->getDescription($project),
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
'board_highlight_period' => $this->config->get('board_highlight_period'),
) + $params));
'swimlanes' => $this->taskLexer
->build($search)
->format(BoardFormatter::getInstance($this->container)->setProjectId($project['id']))
)));
}
/**
@ -177,9 +182,11 @@ class Board extends Base
{
return $this->template->render('board/table_container', array(
'project' => $this->project->getById($project_id),
'swimlanes' => $this->taskFilter->search($this->userSession->getFilters($project_id))->getBoard($project_id),
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
'board_highlight_period' => $this->config->get('board_highlight_period'),
'swimlanes' => $this->taskLexer
->build($this->userSession->getFilters($project_id))
->format(BoardFormatter::getInstance($this->container)->setProjectId($project_id))
));
}
}

View File

@ -2,6 +2,9 @@
namespace Kanboard\Controller;
use Kanboard\Filter\TaskAssigneeFilter;
use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Filter\TaskStatusFilter;
use Kanboard\Model\Task as TaskModel;
/**
@ -20,9 +23,14 @@ class Calendar extends Base
*/
public function show()
{
$project = $this->getProject();
$this->response->html($this->helper->layout->app('calendar/show', array(
'project' => $project,
'title' => $project['name'],
'description' => $this->helper->projectHeader->getDescription($project),
'check_interval' => $this->config->get('board_private_refresh_interval'),
) + $this->getProjectFilters('calendar', 'show')));
)));
}
/**
@ -35,21 +43,11 @@ class Calendar extends Base
$project_id = $this->request->getIntegerParam('project_id');
$start = $this->request->getStringParam('start');
$end = $this->request->getStringParam('end');
$search = $this->userSession->getFilters($project_id);
$queryBuilder = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project_id));
// Common filter
$filter = $this->taskFilterCalendarFormatter
->search($this->userSession->getFilters($project_id))
->filterByProject($project_id);
// Tasks
if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
$events = $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format();
} else {
$events = $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format();
}
// Tasks with due date
$events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format());
$events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end);
$events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end));
$events = $this->hook->merge('controller:calendar:project:events', $events, array(
'project_id' => $project_id,
@ -70,21 +68,15 @@ class Calendar extends Base
$user_id = $this->request->getIntegerParam('user_id');
$start = $this->request->getStringParam('start');
$end = $this->request->getStringParam('end');
$filter = $this->taskFilterCalendarFormatter->create()->filterByOwner($user_id)->filterByStatus(TaskModel::STATUS_OPEN);
$queryBuilder = $this->taskQuery
->withFilter(new TaskAssigneeFilter($user_id))
->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN));
// Task with due date
$events = $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format();
$events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end);
$events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end));
// Tasks
if ($this->config->get('calendar_user_tasks', 'date_started') === 'date_creation') {
$events = array_merge($events, $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format());
} else {
$events = array_merge($events, $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format());
}
// Subtasks time tracking
if ($this->config->get('calendar_user_subtasks_time_tracking') == 1) {
$events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end));
$events = array_merge($events, $this->helper->calendar->getSubtaskTimeTrackingEvents($user_id, $start, $end));
}
$events = $this->hook->merge('controller:calendar:user:events', $events, array(

View File

@ -5,54 +5,32 @@ namespace Kanboard\Controller;
use Parsedown;
/**
* Documentation controller
* Documentation Viewer
*
* @package controller
* @author Frederic Guillot
*/
class Doc extends Base
{
private function readFile($filename)
{
$url = $this->helper->url;
$data = file_get_contents($filename);
list($title, ) = explode("\n", $data, 2);
$replaceUrl = function (array $matches) use ($url) {
return '('.$url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')';
};
$content = preg_replace_callback('/\((.*.markdown)\)/', $replaceUrl, $data);
return array(
'content' => Parsedown::instance()->text($content),
'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title,
);
}
public function show()
{
$page = $this->request->getStringParam('file', 'index');
if (! preg_match('/^[a-z0-9\-]+/', $page)) {
if (!preg_match('/^[a-z0-9\-]+/', $page)) {
$page = 'index';
}
$filenames = array(__DIR__.'/../../doc/'.$page.'.markdown');
$filename = __DIR__.'/../../doc/index.markdown';
if ($this->config->getCurrentLanguage() === 'fr_FR') {
array_unshift($filenames, __DIR__.'/../../doc/fr/'.$page.'.markdown');
$filename = __DIR__.'/../../doc/fr/' . $page . '.markdown';
} else {
$filename = __DIR__ . '/../../doc/' . $page . '.markdown';
}
foreach ($filenames as $file) {
if (file_exists($file)) {
$filename = $file;
break;
}
if (!file_exists($filename)) {
$filename = __DIR__.'/../../doc/index.markdown';
}
$this->response->html($this->helper->layout->app('doc/show', $this->readFile($filename)));
$this->response->html($this->helper->layout->app('doc/show', $this->render($filename)));
}
/**
@ -62,4 +40,53 @@ class Doc extends Base
{
$this->response->html($this->template->render('config/keyboard_shortcuts'));
}
/**
* Prepare Markdown file
*
* @access private
* @param string $filename
* @return array
*/
private function render($filename)
{
$data = file_get_contents($filename);
$content = preg_replace_callback('/\((.*.markdown)\)/', array($this, 'replaceMarkdownUrl'), $data);
$content = preg_replace_callback('/\((screenshots.*\.png)\)/', array($this, 'replaceImageUrl'), $content);
list($title, ) = explode("\n", $data, 2);
return array(
'content' => Parsedown::instance()->text($content),
'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title,
);
}
/**
* Regex callback to replace Markdown links
*
* @access public
* @param array $matches
* @return string
*/
public function replaceMarkdownUrl(array $matches)
{
return '('.$this->helper->url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')';
}
/**
* Regex callback to replace image links
*
* @access public
* @param array $matches
* @return string
*/
public function replaceImageUrl(array $matches)
{
if ($this->config->getCurrentLanguage() === 'fr_FR') {
return '('.$this->helper->url->base().'doc/fr/'.$matches[1].')';
}
return '('.$this->helper->url->base().'doc/'.$matches[1].')';
}
}

View File

@ -26,7 +26,7 @@ class Feed extends Base
}
$this->response->xml($this->template->render('feed/user', array(
'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id'])),
'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermission->getActiveProjectIds($user['id'])),
'user' => $user,
)));
}
@ -47,7 +47,7 @@ class Feed extends Base
}
$this->response->xml($this->template->render('feed/project', array(
'events' => $this->projectActivity->getProject($project['id']),
'events' => $this->helper->projectActivity->getProjectEvents($project['id']),
'project' => $project,
)));
}

View File

@ -66,9 +66,16 @@ class FileViewer extends Base
*/
public function image()
{
$file = $this->getFile();
$etag = md5($file['path']);
$this->response->contentType($this->helper->file->getImageMimeType($file['name']));
$this->response->cache(5 * 86400, $etag);
if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') {
return $this->response->status(304);
}
try {
$file = $this->getFile();
$this->response->contentType($this->helper->file->getImageMimeType($file['name']));
$this->objectStorage->output($file['path']);
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
@ -82,12 +89,21 @@ class FileViewer extends Base
*/
public function thumbnail()
{
$file = $this->getFile();
$model = $file['model'];
$filename = $this->$model->getThumbnailPath($file['path']);
$etag = md5($filename);
$this->response->cache(5 * 86400, $etag);
$this->response->contentType('image/jpeg');
if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') {
return $this->response->status(304);
}
try {
$file = $this->getFile();
$model = $file['model'];
$this->objectStorage->output($this->$model->getThumbnailPath($file['path']));
$this->objectStorage->output($filename);
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());

View File

@ -2,7 +2,14 @@
namespace Kanboard\Controller;
use Kanboard\Filter\ProjectIdsFilter;
use Kanboard\Filter\ProjectStatusFilter;
use Kanboard\Filter\ProjectTypeFilter;
use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Formatter\ProjectGanttFormatter;
use Kanboard\Formatter\TaskGanttFormatter;
use Kanboard\Model\Task as TaskModel;
use Kanboard\Model\Project as ProjectModel;
/**
* Gantt controller
@ -17,14 +24,16 @@ class Gantt extends Base
*/
public function projects()
{
if ($this->userSession->isAdmin()) {
$project_ids = $this->project->getAllIds();
} else {
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
}
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
$filter = $this->projectQuery
->withFilter(new ProjectTypeFilter(ProjectModel::TYPE_TEAM))
->withFilter(new ProjectStatusFilter(ProjectModel::ACTIVE))
->withFilter(new ProjectIdsFilter($project_ids));
$filter->getQuery()->asc(ProjectModel::TABLE.'.start_date');
$this->response->html($this->helper->layout->app('gantt/projects', array(
'projects' => $this->projectGanttFormatter->filter($project_ids)->format(),
'projects' => $filter->format(new ProjectGanttFormatter($this->container)),
'title' => t('Gantt chart for all projects'),
)));
}
@ -54,9 +63,10 @@ class Gantt extends Base
*/
public function project()
{
$params = $this->getProjectFilters('gantt', 'project');
$filter = $this->taskFilterGanttFormatter->search($params['filters']['search'])->filterByProject($params['project']['id']);
$project = $this->getProject();
$search = $this->helper->projectHeader->getSearchQuery($project);
$sorting = $this->request->getStringParam('sorting', 'board');
$filter = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project['id']));
if ($sorting === 'date') {
$filter->getQuery()->asc(TaskModel::TABLE.'.date_started')->asc(TaskModel::TABLE.'.date_creation');
@ -64,10 +74,12 @@ class Gantt extends Base
$filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position');
}
$this->response->html($this->helper->layout->app('gantt/project', $params + array(
'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
$this->response->html($this->helper->layout->app('gantt/project', array(
'project' => $project,
'title' => $project['name'],
'description' => $this->helper->projectHeader->getDescription($project),
'sorting' => $sorting,
'tasks' => $filter->format(),
'tasks' => $filter->format(new TaskGanttFormatter($this->container)),
)));
}

View File

@ -2,6 +2,8 @@
namespace Kanboard\Controller;
use Kanboard\Formatter\GroupAutoCompleteFormatter;
/**
* Group Helper
*
@ -11,14 +13,14 @@ namespace Kanboard\Controller;
class GroupHelper extends Base
{
/**
* Group autocompletion (Ajax)
* Group auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
$groups = $this->groupManager->find($search);
$this->response->json($this->groupAutoCompleteFormatter->setGroups($groups)->format());
$formatter = new GroupAutoCompleteFormatter($this->groupManager->find($search));
$this->response->json($formatter->format());
}
}

View File

@ -2,7 +2,11 @@
namespace Kanboard\Controller;
use Kanboard\Model\TaskFilter;
use Kanboard\Core\Filter\QueryBuilder;
use Kanboard\Filter\TaskAssigneeFilter;
use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Filter\TaskStatusFilter;
use Kanboard\Formatter\TaskICalFormatter;
use Kanboard\Model\Task as TaskModel;
use Eluceo\iCal\Component\Calendar as iCalendar;
@ -30,10 +34,11 @@ class Ical extends Base
}
// Common filter
$filter = $this->taskFilterICalendarFormatter
->create()
->filterByStatus(TaskModel::STATUS_OPEN)
->filterByOwner($user['id']);
$queryBuilder = new QueryBuilder();
$queryBuilder
->withQuery($this->taskFinder->getICalQuery())
->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
->withFilter(new TaskAssigneeFilter($user['id']));
// Calendar properties
$calendar = new iCalendar('Kanboard');
@ -41,7 +46,7 @@ class Ical extends Base
$calendar->setDescription($user['name'] ?: $user['username']);
$calendar->setPublishedTTL('PT1H');
$this->renderCalendar($filter, $calendar);
$this->renderCalendar($queryBuilder, $calendar);
}
/**
@ -60,10 +65,11 @@ class Ical extends Base
}
// Common filter
$filter = $this->taskFilterICalendarFormatter
->create()
->filterByStatus(TaskModel::STATUS_OPEN)
->filterByProject($project['id']);
$queryBuilder = new QueryBuilder();
$queryBuilder
->withQuery($this->taskFinder->getICalQuery())
->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
->withFilter(new TaskProjectFilter($project['id']));
// Calendar properties
$calendar = new iCalendar('Kanboard');
@ -71,7 +77,7 @@ class Ical extends Base
$calendar->setDescription($project['name']);
$calendar->setPublishedTTL('PT1H');
$this->renderCalendar($filter, $calendar);
$this->renderCalendar($queryBuilder, $calendar);
}
/**
@ -79,37 +85,14 @@ class Ical extends Base
*
* @access private
*/
private function renderCalendar(TaskFilter $filter, iCalendar $calendar)
private function renderCalendar(QueryBuilder $queryBuilder, iCalendar $calendar)
{
$start = $this->request->getStringParam('start', strtotime('-2 month'));
$end = $this->request->getStringParam('end', strtotime('+6 months'));
// Tasks
if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
$filter
->copy()
->filterByCreationDateRange($start, $end)
->setColumns('date_creation', 'date_completed')
->setCalendar($calendar)
->addDateTimeEvents();
} else {
$filter
->copy()
->filterByStartDateRange($start, $end)
->setColumns('date_started', 'date_completed')
->setCalendar($calendar)
->addDateTimeEvents($calendar);
}
$this->helper->ical->addTaskDateDueEvents($queryBuilder, $calendar, $start, $end);
// Tasks with due date
$filter
->copy()
->filterByDueDateRange($start, $end)
->setColumns('date_due')
->setCalendar($calendar)
->addFullDayEvents($calendar);
$this->response->contentType('text/calendar; charset=utf-8');
echo $filter->setCalendar($calendar)->format();
$formatter = new TaskICalFormatter($this->container);
$this->response->ical($formatter->setCalendar($calendar)->format());
}
}

View File

@ -2,6 +2,7 @@
namespace Kanboard\Controller;
use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Model\Task as TaskModel;
/**
@ -19,22 +20,26 @@ class Listing extends Base
*/
public function show()
{
$params = $this->getProjectFilters('listing', 'show');
$query = $this->taskFilter->search($params['filters']['search'])->filterByProject($params['project']['id'])->getQuery();
$project = $this->getProject();
$search = $this->helper->projectHeader->getSearchQuery($project);
$paginator = $this->paginator
->setUrl('listing', 'show', array('project_id' => $params['project']['id']))
->setUrl('listing', 'show', array('project_id' => $project['id']))
->setMax(30)
->setOrder(TaskModel::TABLE.'.id')
->setDirection('DESC')
->setQuery($query)
->setQuery($this->taskLexer
->build($search)
->withFilter(new TaskProjectFilter($project['id']))
->getQuery()
)
->calculate();
$this->response->html($this->helper->layout->app('listing/show', $params + array(
$this->response->html($this->helper->layout->app('listing/show', array(
'project' => $project,
'title' => $project['name'],
'description' => $this->helper->projectHeader->getDescription($project),
'paginator' => $paginator,
'categories_list' => $this->category->getList($params['project']['id'], false),
'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()),
)));
}
}

View File

@ -2,6 +2,8 @@
namespace Kanboard\Controller;
use Kanboard\Core\Security\OAuthAuthenticationProviderInterface;
/**
* OAuth controller
*
@ -10,6 +12,72 @@ namespace Kanboard\Controller;
*/
class Oauth extends Base
{
/**
* Redirect to the provider if no code received
*
* @access private
* @param string $provider
*/
protected function step1($provider)
{
$code = $this->request->getStringParam('code');
$state = $this->request->getStringParam('state');
if (! empty($code)) {
$this->step2($provider, $code, $state);
} else {
$this->response->redirect($this->authenticationManager->getProvider($provider)->getService()->getAuthorizationUrl());
}
}
/**
* Link or authenticate the user
*
* @access protected
* @param string $providerName
* @param string $code
* @param string $state
*/
protected function step2($providerName, $code, $state)
{
$provider = $this->authenticationManager->getProvider($providerName);
$provider->setCode($code);
$hasValidState = $provider->getService()->isValidateState($state);
if ($this->userSession->isLogged()) {
if ($hasValidState) {
$this->link($provider);
} else {
$this->flash->failure(t('The OAuth2 state parameter is invalid'));
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
}
} else {
if ($hasValidState) {
$this->authenticate($providerName);
} else {
$this->authenticationFailure(t('The OAuth2 state parameter is invalid'));
}
}
}
/**
* Link the account
*
* @access protected
* @param OAuthAuthenticationProviderInterface $provider
*/
protected function link(OAuthAuthenticationProviderInterface $provider)
{
if (! $provider->authenticate()) {
$this->flash->failure(t('External authentication failed'));
} else {
$this->userProfile->assign($this->userSession->getId(), $provider->getUser());
$this->flash->success(t('Your external account is linked to your profile successfully.'));
}
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
}
/**
* Unlink external account
*
@ -29,78 +97,34 @@ class Oauth extends Base
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
}
/**
* Redirect to the provider if no code received
*
* @access private
* @param string $provider
*/
protected function step1($provider)
{
$code = $this->request->getStringParam('code');
if (! empty($code)) {
$this->step2($provider, $code);
} else {
$this->response->redirect($this->authenticationManager->getProvider($provider)->getService()->getAuthorizationUrl());
}
}
/**
* Link or authenticate the user
*
* @access protected
* @param string $provider
* @param string $code
*/
protected function step2($provider, $code)
{
$this->authenticationManager->getProvider($provider)->setCode($code);
if ($this->userSession->isLogged()) {
$this->link($provider);
}
$this->authenticate($provider);
}
/**
* Link the account
*
* @access protected
* @param string $provider
*/
protected function link($provider)
{
$authProvider = $this->authenticationManager->getProvider($provider);
if (! $authProvider->authenticate()) {
$this->flash->failure(t('External authentication failed'));
} else {
$this->userProfile->assign($this->userSession->getId(), $authProvider->getUser());
$this->flash->success(t('Your external account is linked to your profile successfully.'));
}
$this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
}
/**
* Authenticate the account
*
* @access protected
* @param string $provider
* @param string $providerName
*/
protected function authenticate($provider)
protected function authenticate($providerName)
{
if ($this->authenticationManager->oauthAuthentication($provider)) {
if ($this->authenticationManager->oauthAuthentication($providerName)) {
$this->response->redirect($this->helper->url->to('app', 'index'));
} else {
$this->response->html($this->helper->layout->app('auth/index', array(
'errors' => array('login' => t('External authentication failed')),
'values' => array(),
'no_layout' => true,
'title' => t('Login')
)));
$this->authenticationFailure(t('External authentication failed'));
}
}
/**
* Show login failure page
*
* @access protected
* @param string $message
*/
protected function authenticationFailure($message)
{
$this->response->html($this->helper->layout->app('auth/index', array(
'errors' => array('login' => $message),
'values' => array(),
'no_layout' => true,
'title' => t('Login')
)));
}
}

View File

@ -15,15 +15,18 @@ class ProjectOverview extends Base
*/
public function show()
{
$params = $this->getProjectFilters('ProjectOverview', 'show');
$params['users'] = $this->projectUserRole->getAllUsersGroupedByRole($params['project']['id']);
$params['roles'] = $this->role->getProjectRoles();
$params['events'] = $this->projectActivity->getProject($params['project']['id'], 10);
$params['images'] = $this->projectFile->getAllImages($params['project']['id']);
$params['files'] = $this->projectFile->getAllDocuments($params['project']['id']);
$project = $this->getProject();
$this->project->getColumnStats($project);
$this->project->getColumnStats($params['project']);
$this->response->html($this->helper->layout->app('project_overview/show', $params));
$this->response->html($this->helper->layout->app('project_overview/show', array(
'project' => $project,
'title' => $project['name'],
'description' => $this->helper->projectHeader->getDescription($project),
'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']),
'roles' => $this->role->getProjectRoles(),
'events' => $this->helper->projectActivity->getProjectEvents($project['id'], 10),
'images' => $this->projectFile->getAllImages($project['id']),
'files' => $this->projectFile->getAllDocuments($project['id']),
)));
}
}

View File

@ -83,7 +83,9 @@ class ProjectPermission extends Base
$project = $this->getProject();
$values = $this->request->getValues();
if ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) {
if (empty($values['user_id'])) {
$this->flash->failure(t('User not found.'));
} elseif ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));

View File

@ -2,6 +2,8 @@
namespace Kanboard\Controller;
use Kanboard\Filter\TaskProjectsFilter;
/**
* Search controller
*
@ -23,14 +25,12 @@ class Search extends Base
->setDirection('DESC');
if ($search !== '' && ! empty($projects)) {
$query = $this
->taskFilter
->search($search)
->filterByProjects(array_keys($projects))
->getQuery();
$paginator
->setQuery($query)
->setQuery($this->taskLexer
->build($search)
->withFilter(new TaskProjectsFilter(array_keys($projects)))
->getQuery()
)
->calculate();
$nb_tasks = $paginator->getTotal();
@ -46,4 +46,22 @@ class Search extends Base
'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
)));
}
public function activity()
{
$search = urldecode($this->request->getStringParam('search'));
$events = $this->helper->projectActivity->searchEvents($search);
$nb_events = count($events);
$this->response->html($this->helper->layout->app('search/activity', array(
'values' => array(
'search' => $search,
'controller' => 'search',
'action' => 'activity',
),
'title' => t('Search in activity stream').($nb_events > 0 ? ' ('.$nb_events.')' : ''),
'nb_events' => $nb_events,
'events' => $events,
)));
}
}

View File

@ -71,17 +71,16 @@ class Task extends Base
$values = $this->dateParser->format($values, array('date_started'), $this->config->get('application_datetime_format', DateParser::DATE_TIME_FORMAT));
$this->response->html($this->helper->layout->task('task/show', array(
'task' => $task,
'project' => $this->project->getById($task['project_id']),
'values' => $values,
'files' => $this->taskFile->getAllDocuments($task['id']),
'images' => $this->taskFile->getAllImages($task['id']),
'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting()),
'subtasks' => $subtasks,
'internal_links' => $this->taskLink->getAllGroupedByLabel($task['id']),
'external_links' => $this->taskExternalLink->getAll($task['id']),
'task' => $task,
'values' => $values,
'link_label_list' => $this->link->getList(0, false),
'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id'], true, false, false),
)));
}
@ -96,6 +95,7 @@ class Task extends Base
$this->response->html($this->helper->layout->task('task/analytics', array(
'task' => $task,
'project' => $this->project->getById($task['project_id']),
'lead_time' => $this->taskAnalytic->getLeadTime($task),
'cycle_time' => $this->taskAnalytic->getCycleTime($task),
'time_spent_columns' => $this->taskAnalytic->getTimeSpentByColumn($task),
@ -121,6 +121,7 @@ class Task extends Base
$this->response->html($this->helper->layout->task('task/time_tracking_details', array(
'task' => $task,
'project' => $this->project->getById($task['project_id']),
'subtask_paginator' => $subtask_paginator,
)));
}
@ -136,6 +137,7 @@ class Task extends Base
$this->response->html($this->helper->layout->task('task/transitions', array(
'task' => $task,
'project' => $this->project->getById($task['project_id']),
'transitions' => $this->transition->getAllByTask($task['id']),
)));
}

View File

@ -2,6 +2,12 @@
namespace Kanboard\Controller;
use Kanboard\Filter\TaskIdExclusionFilter;
use Kanboard\Filter\TaskIdFilter;
use Kanboard\Filter\TaskProjectsFilter;
use Kanboard\Filter\TaskTitleFilter;
use Kanboard\Formatter\TaskAutoCompleteFormatter;
/**
* Task Ajax Helper
*
@ -11,31 +17,33 @@ namespace Kanboard\Controller;
class TaskHelper extends Base
{
/**
* Task autocompletion (Ajax)
* Task auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
$projects = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
$exclude_task_id = $this->request->getIntegerParam('exclude_task_id');
if (empty($projects)) {
if (empty($project_ids)) {
$this->response->json(array());
}
$filter = $this->taskFilterAutoCompleteFormatter
->create()
->filterByProjects($projects)
->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
// Search by task id or by title
if (ctype_digit($search)) {
$filter->filterById($search);
} else {
$filter->filterByTitle($search);
}
$this->response->json($filter->format());
$filter = $this->taskQuery->withFilter(new TaskProjectsFilter($project_ids));
if (! empty($exclude_task_id)) {
$filter->withFilter(new TaskIdExclusionFilter(array($exclude_task_id)));
}
if (ctype_digit($search)) {
$filter->withFilter(new TaskIdFilter($search));
} else {
$filter->withFilter(new TaskTitleFilter($search));
}
$this->response->json($filter->format(new TaskAutoCompleteFormatter($this->container)));
}
}
}

View File

@ -2,6 +2,10 @@
namespace Kanboard\Controller;
use Kanboard\Filter\UserNameFilter;
use Kanboard\Formatter\UserAutoCompleteFormatter;
use Kanboard\Model\User as UserModel;
/**
* User Helper
*
@ -11,19 +15,20 @@ namespace Kanboard\Controller;
class UserHelper extends Base
{
/**
* User autocompletion (Ajax)
* User auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
$users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format();
$this->response->json($users);
$filter = $this->userQuery->withFilter(new UserNameFilter($search));
$filter->getQuery()->asc(UserModel::TABLE.'.name')->asc(UserModel::TABLE.'.username');
$this->response->json($filter->format(new UserAutoCompleteFormatter($this->container)));
}
/**
* User mention autocompletion (Ajax)
* User mention auto-completion (Ajax)
*
* @access public
*/

View File

@ -18,7 +18,7 @@ class ActionManager extends Base
* List of automatic actions
*
* @access private
* @var array
* @var ActionBase[]
*/
private $actions = array();

View File

@ -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;
}
}

View File

@ -10,26 +10,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
*

View File

@ -23,7 +23,7 @@ class ExternalLinkManager extends Base
* Registered providers
*
* @access private
* @var array
* @var ExternalLinkProviderInterface[]
*/
private $providers = array();

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

153
app/Core/Filter/Lexer.php Normal file
View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
{

View File

@ -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
@ -45,6 +45,33 @@ class OAuth2 extends Base
return $this;
}
/**
* 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
*
@ -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);

View File

@ -13,6 +13,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
*
@ -213,6 +231,20 @@ class Response extends Base
exit;
}
/**
* 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
*

View File

@ -3,6 +3,7 @@
namespace Kanboard\Core\Ldap;
use LogicException;
use Psr\Log\LoggerInterface;
/**
* LDAP Client
@ -20,6 +21,14 @@ class Client
*/
protected $ldap;
/**
* Logger instance
*
* @access private
* @var LoggerInterface
*/
private $logger;
/**
* Establish LDAP connection
*
@ -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;
}
}

View File

@ -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;

View File

@ -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));
}

View File

@ -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;
}
}

View File

@ -21,6 +21,7 @@ namespace Kanboard\Core\Session;
* @property bool $boardCollapsed
* @property bool $twoFactorBeforeCodeCalled
* @property string $twoFactorSecret
* @property string $oauthState
*/
class SessionStorage
{

View File

@ -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_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 (strpos($template, ':') !== false) {
list($plugin, $template) = explode(':', $template);
}
return $path;
if ($plugin !== 'kanboard' && $plugin !== '') {
return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', '..', 'plugins', ucfirst($plugin), 'Template', $template.'.php'));
}
return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', 'Template', $template.'.php'));
}
}

172
app/Core/Thumbnail.php Normal file
View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -13,6 +13,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
*

View File

@ -0,0 +1,103 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\DateParser;
/**
* Base date filter class
*
* @package filter
* @author Frederic Guillot
*/
abstract class BaseDateFilter extends BaseFilter
{
/**
* DateParser object
*
* @access protected
* @var DateParser
*/
protected $dateParser;
/**
* Set DateParser object
*
* @access public
* @param DateParser $dateParser
* @return $this
*/
public function setDateParser(DateParser $dateParser)
{
$this->dateParser = $dateParser;
return $this;
}
/**
* Parse operator in the input string
*
* @access protected
* @return string
*/
protected function parseOperator()
{
$operators = array(
'<=' => 'lte',
'>=' => 'gte',
'<' => 'lt',
'>' => 'gt',
);
foreach ($operators as $operator => $method) {
if (strpos($this->value, $operator) === 0) {
$this->value = substr($this->value, strlen($operator));
return $method;
}
}
return '';
}
/**
* Apply a date filter
*
* @access protected
* @param string $field
*/
protected function applyDateFilter($field)
{
$method = $this->parseOperator();
$timestamp = $this->dateParser->getTimestampFromIsoFormat($this->value);
if ($method !== '') {
$this->query->$method($field, $this->getTimestampFromOperator($method, $timestamp));
} else {
$this->query->gte($field, $timestamp);
$this->query->lte($field, $timestamp + 86399);
}
}
/**
* Get timestamp from the operator
*
* @access public
* @param string $method
* @param integer $timestamp
* @return integer
*/
protected function getTimestampFromOperator($method, $timestamp)
{
switch ($method) {
case 'lte':
return $timestamp + 86399;
case 'lt':
return $timestamp;
case 'gte':
return $timestamp;
case 'gt':
return $timestamp + 86400;
}
return $timestamp;
}
}

75
app/Filter/BaseFilter.php Normal file
View File

@ -0,0 +1,75 @@
<?php
namespace Kanboard\Filter;
use PicoDb\Table;
/**
* Base filter class
*
* @package filter
* @author Frederic Guillot
*/
abstract class BaseFilter
{
/**
* @var Table
*/
protected $query;
/**
* @var mixed
*/
protected $value;
/**
* BaseFilter constructor
*
* @access public
* @param mixed $value
*/
public function __construct($value = null)
{
$this->value = $value;
}
/**
* Get object instance
*
* @static
* @access public
* @param mixed $value
* @return static
*/
public static function getInstance($value = null)
{
$self = new static($value);
return $self;
}
/**
* Set query
*
* @access public
* @param Table $query
* @return \Kanboard\Core\Filter\FilterInterface
*/
public function withQuery(Table $query)
{
$this->query = $query;
return $this;
}
/**
* Set the value
*
* @access public
* @param string $value
* @return \Kanboard\Core\Filter\FilterInterface
*/
public function withValue($value)
{
$this->value = $value;
return $this;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\ProjectActivity;
/**
* Filter activity events by creation date
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityCreationDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('created');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->applyDateFilter(ProjectActivity::TABLE.'.date_creation');
return $this;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\ProjectActivity;
/**
* Filter activity events by creator
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityCreatorFilter extends BaseFilter implements FilterInterface
{
/**
* Current user id
*
* @access private
* @var int
*/
private $currentUserId = 0;
/**
* Set current user id
*
* @access public
* @param integer $userId
* @return TaskAssigneeFilter
*/
public function setCurrentUserId($userId)
{
$this->currentUserId = $userId;
return $this;
}
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('creator');
}
/**
* Apply filter
*
* @access public
* @return string
*/
public function apply()
{
if ($this->value === 'me') {
$this->query->eq(ProjectActivity::TABLE . '.creator_id', $this->currentUserId);
} else {
$this->query->beginOr();
$this->query->ilike('uc.username', '%'.$this->value.'%');
$this->query->ilike('uc.name', '%'.$this->value.'%');
$this->query->closeOr();
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\ProjectActivity;
/**
* Filter activity events by projectId
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityProjectIdFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('project_id');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->eq(ProjectActivity::TABLE.'.project_id', $this->value);
return $this;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\ProjectActivity;
/**
* Filter activity events by projectIds
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityProjectIdsFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('projects');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
if (empty($this->value)) {
$this->query->eq(ProjectActivity::TABLE.'.project_id', 0);
} else {
$this->query->in(ProjectActivity::TABLE.'.project_id', $this->value);
}
return $this;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Project;
/**
* Filter activity events by project name
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityProjectNameFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('project');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->ilike(Project::TABLE.'.name', '%'.$this->value.'%');
return $this;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\ProjectActivity;
/**
* Filter activity events by taskId
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityTaskIdFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('task_id');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->eq(ProjectActivity::TABLE.'.task_id', $this->value);
return $this;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Filter activity events by task status
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityTaskStatusFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('status');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
if ($this->value === 'open') {
$this->query->eq(Task::TABLE.'.is_active', Task::STATUS_OPEN);
} elseif ($this->value === 'closed') {
$this->query->eq(Task::TABLE.'.is_active', Task::STATUS_CLOSED);
}
return $this;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
/**
* Filter activity events by task title
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityTaskTitleFilter extends TaskTitleFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('title');
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\ProjectGroupRole;
/**
* Filter ProjectGroupRole users by project
*
* @package filter
* @author Frederic Guillot
*/
class ProjectGroupRoleProjectFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array();
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->eq(ProjectGroupRole::TABLE.'.project_id', $this->value);
return $this;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\GroupMember;
use Kanboard\Model\ProjectGroupRole;
use Kanboard\Model\User;
/**
* Filter ProjectGroupRole users by username
*
* @package filter
* @author Frederic Guillot
*/
class ProjectGroupRoleUsernameFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array();
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query
->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE)
->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
->ilike(User::TABLE.'.username', $this->value.'%');
return $this;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Project;
/**
* Filter project by ids
*
* @package filter
* @author Frederic Guillot
*/
class ProjectIdsFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('project_ids');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
if (empty($this->value)) {
$this->query->eq(Project::TABLE.'.id', 0);
} else {
$this->query->in(Project::TABLE.'.id', $this->value);
}
return $this;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Project;
/**
* Filter project by status
*
* @package filter
* @author Frederic Guillot
*/
class ProjectStatusFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('status');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
if (is_int($this->value) || ctype_digit($this->value)) {
$this->query->eq(Project::TABLE.'.is_active', $this->value);
} elseif ($this->value === 'inactive' || $this->value === 'closed' || $this->value === 'disabled') {
$this->query->eq(Project::TABLE.'.is_active', 0);
} else {
$this->query->eq(Project::TABLE.'.is_active', 1);
}
return $this;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Project;
/**
* Filter project by type
*
* @package filter
* @author Frederic Guillot
*/
class ProjectTypeFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('type');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
if (is_int($this->value) || ctype_digit($this->value)) {
$this->query->eq(Project::TABLE.'.is_private', $this->value);
} elseif ($this->value === 'private') {
$this->query->eq(Project::TABLE.'.is_private', Project::TYPE_PRIVATE);
} else {
$this->query->eq(Project::TABLE.'.is_private', Project::TYPE_TEAM);
}
return $this;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\ProjectUserRole;
/**
* Filter ProjectUserRole users by project
*
* @package filter
* @author Frederic Guillot
*/
class ProjectUserRoleProjectFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array();
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->eq(ProjectUserRole::TABLE.'.project_id', $this->value);
return $this;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\User;
/**
* Filter ProjectUserRole users by username
*
* @package filter
* @author Frederic Guillot
*/
class ProjectUserRoleUsernameFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array();
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query
->join(User::TABLE, 'id', 'user_id')
->ilike(User::TABLE.'.username', $this->value.'%');
return $this;
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
use Kanboard\Model\User;
/**
* Filter tasks by assignee
*
* @package filter
* @author Frederic Guillot
*/
class TaskAssigneeFilter extends BaseFilter implements FilterInterface
{
/**
* Current user id
*
* @access private
* @var int
*/
private $currentUserId = 0;
/**
* Set current user id
*
* @access public
* @param integer $userId
* @return TaskAssigneeFilter
*/
public function setCurrentUserId($userId)
{
$this->currentUserId = $userId;
return $this;
}
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('assignee');
}
/**
* Apply filter
*
* @access public
* @return string
*/
public function apply()
{
if (is_int($this->value) || ctype_digit($this->value)) {
$this->query->eq(Task::TABLE.'.owner_id', $this->value);
} else {
switch ($this->value) {
case 'me':
$this->query->eq(Task::TABLE.'.owner_id', $this->currentUserId);
break;
case 'nobody':
$this->query->eq(Task::TABLE.'.owner_id', 0);
break;
default:
$this->query->beginOr();
$this->query->ilike(User::TABLE.'.username', '%'.$this->value.'%');
$this->query->ilike(User::TABLE.'.name', '%'.$this->value.'%');
$this->query->closeOr();
}
}
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Category;
use Kanboard\Model\Task;
/**
* Filter tasks by category
*
* @package filter
* @author Frederic Guillot
*/
class TaskCategoryFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('category');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
if (is_int($this->value) || ctype_digit($this->value)) {
$this->query->eq(Task::TABLE.'.category_id', $this->value);
} elseif ($this->value === 'none') {
$this->query->eq(Task::TABLE.'.category_id', 0);
} else {
$this->query->eq(Category::TABLE.'.name', $this->value);
}
return $this;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Color;
use Kanboard\Model\Task;
/**
* Filter tasks by color
*
* @package filter
* @author Frederic Guillot
*/
class TaskColorFilter extends BaseFilter implements FilterInterface
{
/**
* Color object
*
* @access private
* @var Color
*/
private $colorModel;
/**
* Set color model object
*
* @access public
* @param Color $colorModel
* @return TaskColorFilter
*/
public function setColorModel(Color $colorModel)
{
$this->colorModel = $colorModel;
return $this;
}
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('color', 'colour');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->eq(Task::TABLE.'.color_id', $this->colorModel->find($this->value));
return $this;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Column;
use Kanboard\Model\Task;
/**
* Filter tasks by column
*
* @package filter
* @author Frederic Guillot
*/
class TaskColumnFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('column');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
if (is_int($this->value) || ctype_digit($this->value)) {
$this->query->eq(Task::TABLE.'.column_id', $this->value);
} else {
$this->query->eq(Column::TABLE.'.title', $this->value);
}
return $this;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Comment;
use Kanboard\Model\Task;
/**
* Filter tasks by comment
*
* @package filter
* @author Frederic Guillot
*/
class TaskCommentFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('comment');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->ilike(Comment::TABLE.'.comment', '%'.$this->value.'%');
$this->query->join(Comment::TABLE, 'task_id', 'id', Task::TABLE);
return $this;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Filter tasks by completion date
*
* @package filter
* @author Frederic Guillot
*/
class TaskCompletionDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('completed');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->applyDateFilter(Task::TABLE.'.date_completed');
return $this;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Filter tasks by creation date
*
* @package filter
* @author Frederic Guillot
*/
class TaskCreationDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('created');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->applyDateFilter(Task::TABLE.'.date_creation');
return $this;
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Filter tasks by creator
*
* @package filter
* @author Frederic Guillot
*/
class TaskCreatorFilter extends BaseFilter implements FilterInterface
{
/**
* Current user id
*
* @access private
* @var int
*/
private $currentUserId = 0;
/**
* Set current user id
*
* @access public
* @param integer $userId
* @return TaskAssigneeFilter
*/
public function setCurrentUserId($userId)
{
$this->currentUserId = $userId;
return $this;
}
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('creator');
}
/**
* Apply filter
*
* @access public
* @return string
*/
public function apply()
{
if (is_int($this->value) || ctype_digit($this->value)) {
$this->query->eq(Task::TABLE.'.creator_id', $this->value);
} else {
switch ($this->value) {
case 'me':
$this->query->eq(Task::TABLE.'.creator_id', $this->currentUserId);
break;
case 'nobody':
$this->query->eq(Task::TABLE.'.creator_id', 0);
break;
default:
$this->query->beginOr();
$this->query->ilike('uc.username', '%'.$this->value.'%');
$this->query->ilike('uc.name', '%'.$this->value.'%');
$this->query->closeOr();
}
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Filter tasks by description
*
* @package filter
* @author Frederic Guillot
*/
class TaskDescriptionFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('description', 'desc');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->ilike(Task::TABLE.'.description', '%'.$this->value.'%');
return $this;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Filter tasks by due date
*
* @package filter
* @author Frederic Guillot
*/
class TaskDueDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('due');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->neq(Task::TABLE.'.date_due', 0);
$this->query->notNull(Task::TABLE.'.date_due');
$this->applyDateFilter(Task::TABLE.'.date_due');
return $this;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Filter tasks by due date range
*
* @package filter
* @author Frederic Guillot
*/
class TaskDueDateRangeFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array();
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->gte(Task::TABLE.'.date_due', is_numeric($this->value[0]) ? $this->value[0] : strtotime($this->value[0]));
$this->query->lte(Task::TABLE.'.date_due', is_numeric($this->value[1]) ? $this->value[1] : strtotime($this->value[1]));
return $this;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Exclude task ids
*
* @package filter
* @author Frederic Guillot
*/
class TaskIdExclusionFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('exclude');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->notin(Task::TABLE.'.id', $this->value);
return $this;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Filter tasks by id
*
* @package filter
* @author Frederic Guillot
*/
class TaskIdFilter extends BaseFilter implements FilterInterface
{
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('id');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->eq(Task::TABLE.'.id', $this->value);
return $this;
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Link;
use Kanboard\Model\Task;
use Kanboard\Model\TaskLink;
use PicoDb\Database;
use PicoDb\Table;
/**
* Filter tasks by link name
*
* @package filter
* @author Frederic Guillot
*/
class TaskLinkFilter extends BaseFilter implements FilterInterface
{
/**
* Database object
*
* @access private
* @var Database
*/
private $db;
/**
* Set database object
*
* @access public
* @param Database $db
* @return TaskLinkFilter
*/
public function setDatabase(Database $db)
{
$this->db = $db;
return $this;
}
/**
* Get search attribute
*
* @access public
* @return string[]
*/
public function getAttributes()
{
return array('link');
}
/**
* Apply filter
*
* @access public
* @return string
*/
public function apply()
{
$task_ids = $this->getSubQuery()->findAllByColumn('task_id');
if (! empty($task_ids)) {
$this->query->in(Task::TABLE.'.id', $task_ids);
} else {
$this->query->eq(Task::TABLE.'.id', 0); // No match
}
}
/**
* Get subquery
*
* @access protected
* @return Table
*/
protected function getSubQuery()
{
return $this->db->table(TaskLink::TABLE)
->columns(
TaskLink::TABLE.'.task_id',
Link::TABLE.'.label'
)
->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE)
->ilike(Link::TABLE.'.label', $this->value);
}
}

Some files were not shown because too many files have changed in this diff Show More