diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/Controller/Ical.php | 99 | ||||
-rw-r--r-- | app/Controller/User.php | 29 | ||||
-rw-r--r-- | app/Model/Acl.php | 1 | ||||
-rw-r--r-- | app/Model/DateParser.php | 2 | ||||
-rw-r--r-- | app/Model/Project.php | 4 | ||||
-rw-r--r-- | app/Model/TaskFilter.php | 127 | ||||
-rw-r--r-- | app/Model/User.php | 55 | ||||
-rw-r--r-- | app/Schema/Mysql.php | 7 | ||||
-rw-r--r-- | app/Schema/Postgres.php | 7 | ||||
-rw-r--r-- | app/Schema/Sqlite.php | 7 | ||||
-rw-r--r-- | app/Template/project/share.php | 2 | ||||
-rw-r--r-- | app/Template/user/share.php | 17 | ||||
-rw-r--r-- | app/Template/user/show.php | 29 | ||||
-rw-r--r-- | app/Template/user/sidebar.php | 3 |
14 files changed, 376 insertions, 13 deletions
diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php new file mode 100644 index 00000000..52e10fa1 --- /dev/null +++ b/app/Controller/Ical.php @@ -0,0 +1,99 @@ +<?php + +namespace Controller; + +use Model\TaskFilter; +use Model\Task as TaskModel; +use Eluceo\iCal\Component\Calendar as iCalendar; + +/** + * iCalendar controller + * + * @package controller + * @author Frederic Guillot + */ +class Ical extends Base +{ + /** + * Get user iCalendar + * + * @access public + */ + public function user() + { + $token = $this->request->getStringParam('token'); + $user = $this->user->getByToken($token); + + // Token verification + if (empty($user)) { + $this->forbidden(true); + } + + // Common filter + $filter = $this->taskFilter + ->create() + ->filterByOwner($user['id']); + + // Calendar properties + $calendar = new iCalendar('Kanboard'); + $calendar->setName($user['name'] ?: $user['username']); + $calendar->setDescription($user['name'] ?: $user['username']); + $calendar->setPublishedTTL('PT1H'); + + $this->renderCalendar($filter, $calendar); + } + + /** + * Get project iCalendar + * + * @access public + */ + public function project() + { + $token = $this->request->getStringParam('token'); + $project = $this->project->getByToken($token); + + // Token verification + if (empty($project)) { + $this->forbidden(true); + } + + // Common filter + $filter = $this->taskFilter + ->create() + ->filterByProject($project['id']); + + // Calendar properties + $calendar = new iCalendar('Kanboard'); + $calendar->setName($project['name']); + $calendar->setDescription($project['name']); + $calendar->setPublishedTTL('PT1H'); + + $this->renderCalendar($filter, $calendar); + } + + /** + * Common method to render iCal events + * + * @access private + */ + private function renderCalendar(TaskFilter $filter, iCalendar $calendar) + { + $start = $this->request->getStringParam('start', strtotime('-1 month')); + $end = $this->request->getStringParam('end', strtotime('+2 months')); + + // Tasks + if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') { + $filter->copy()->filterByCreationDateRange($start, $end)->addDateTimeIcalEvents('date_creation', 'date_completed', $calendar); + } + else { + $filter->copy()->filterByStartDateRange($start, $end)->addDateTimeIcalEvents('date_started', 'date_completed', $calendar); + } + + // Tasks with due date + $filter->copy()->filterByDueDateRange($start, $end)->addAllDayIcalEvents('date_due', $calendar); + + $this->response->contentType('text/calendar; charset=utf-8'); + echo $calendar->render(); + } +} diff --git a/app/Controller/User.php b/app/Controller/User.php index 37f10969..c8496418 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -249,6 +249,35 @@ class User extends Base } /** + * Public access management + * + * @access public + */ + public function share() + { + $user = $this->getUser(); + $switch = $this->request->getStringParam('switch'); + + if ($switch === 'enable' || $switch === 'disable') { + + $this->checkCSRFParam(); + + if ($this->user->{$switch.'PublicAccess'}($user['id'])) { + $this->session->flash(t('User updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this user.')); + } + + $this->response->redirect($this->helper->url('user', 'share', array('user_id' => $user['id']))); + } + + $this->response->html($this->layout('user/share', array( + 'user' => $user, + 'title' => t('Public access'), + ))); + } + + /** * Password modification * * @access public diff --git a/app/Model/Acl.php b/app/Model/Acl.php index d7b96b06..6d5e6d50 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -24,6 +24,7 @@ class Acl extends Base 'project' => array('feed'), 'webhook' => '*', 'app' => array('colors'), + 'ical' => '*', ); /** diff --git a/app/Model/DateParser.php b/app/Model/DateParser.php index a0d10a36..be79a92e 100644 --- a/app/Model/DateParser.php +++ b/app/Model/DateParser.php @@ -148,7 +148,7 @@ class DateParser extends Base */ public function getTimestampFromIsoFormat($date) { - return $this->removeTimeFromTimestamp(strtotime($date)); + return $this->removeTimeFromTimestamp(ctype_digit($date) ? $date : strtotime($date)); } /** diff --git a/app/Model/Project.php b/app/Model/Project.php index 158de295..80e17437 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -84,6 +84,10 @@ class Project extends Base */ public function getByToken($token) { + if (empty($token)) { + return false; + } + return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_public', 1)->findOne(); } diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php index 21e77e63..c4c56800 100644 --- a/app/Model/TaskFilter.php +++ b/app/Model/TaskFilter.php @@ -2,6 +2,11 @@ namespace Model; +use DateTime; +use Eluceo\iCal\Component\Calendar; +use Eluceo\iCal\Component\Event; +use Eluceo\iCal\Property\Event\Attendees; + /** * Task Filter * @@ -27,6 +32,17 @@ class TaskFilter extends Base public function create() { $this->query = $this->db->table(Task::TABLE); + $this->query->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id'); + $this->query->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id'); + + $this->query->columns( + Task::TABLE.'.*', + 'ua.email AS assignee_email', + 'ua.username AS assignee_username', + 'uc.email AS creator_email', + 'uc.username AS creator_username' + ); + return $this; } @@ -214,8 +230,8 @@ class TaskFilter extends Base * Filter by due date (range) * * @access public - * @param integer $start - * @param integer $end + * @param string $start + * @param string $end * @return TaskFilter */ public function filterByDueDateRange($start, $end) @@ -230,8 +246,8 @@ class TaskFilter extends Base * Filter by start date (range) * * @access public - * @param integer $start - * @param integer $end + * @param string $start + * @param strings $end * @return TaskFilter */ public function filterByStartDateRange($start, $end) @@ -250,8 +266,8 @@ class TaskFilter extends Base * Filter by creation date * * @access public - * @param integer $start - * @param integer $end + * @param string $start + * @param string $end * @return TaskFilter */ public function filterByCreationDateRange($start, $end) @@ -349,4 +365,103 @@ class TaskFilter extends Base return $events; } + + /** + * Transform results to ical events + * + * @access public + * @param string $start_column Column name for the start date + * @param string $end_column Column name for the end date + * @param Eluceo\iCal\Component\Calendar $vCalendar Calendar object + * @return Eluceo\iCal\Component\Calendar + */ + public function addDateTimeIcalEvents($start_column, $end_column, Calendar $vCalendar = null) + { + if ($vCalendar === null) { + $vCalendar = new Calendar('Kanboard'); + } + + foreach ($this->query->findAll() as $task) { + + $start = new DateTime; + $start->setTimestamp($task[$start_column]); + + $end = new DateTime; + $end->setTimestamp($task[$end_column] ?: time()); + + $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$start_column.'-'.$end_column); + $vEvent->setDtStart($start); + $vEvent->setDtEnd($end); + + $vCalendar->addComponent($vEvent); + } + + return $vCalendar; + } + + /** + * Transform results to all day ical events + * + * @access public + * @param string $column Column name for the date + * @param Eluceo\iCal\Component\Calendar $vCalendar Calendar object + * @return Eluceo\iCal\Component\Calendar + */ + public function addAllDayIcalEvents($column = 'date_due', Calendar $vCalendar = null) + { + if ($vCalendar === null) { + $vCalendar = new Calendar('Kanboard'); + } + + foreach ($this->query->findAll() as $task) { + + $date = new DateTime; + $date->setTimestamp($task[$column]); + + $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$column); + $vEvent->setDtStart($date); + $vEvent->setDtEnd($date); + $vEvent->setNoTime(true); + + $vCalendar->addComponent($vEvent); + } + + return $vCalendar; + } + + /** + * Get common events for task ical events + * + * @access protected + * @param array $task + * @param string $uid + * @return Eluceo\iCal\Component\Event + */ + protected function getTaskIcalEvent(array &$task, $uid) + { + $dateCreation = new DateTime; + $dateCreation->setTimestamp($task['date_creation']); + + $dateModif = new DateTime; + $dateModif->setTimestamp($task['date_modification']); + + $vEvent = new Event($uid); + $vEvent->setCreated($dateCreation); + $vEvent->setModified($dateModif); + $vEvent->setUseTimezone(true); + $vEvent->setSummary(t('#%d', $task['id']).' '.$task['title']); + $vEvent->setUrl($this->helper->getCurrentBaseUrl().$this->helper->url('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + + if (! empty($task['creator_id'])) { + $vEvent->setOrganizer('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local')); + } + + if (! empty($task['owner_id'])) { + $attendees = new Attendees; + $attendees->add('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local')); + $vEvent->setAttendees($attendees); + } + + return $vEvent; + } } diff --git a/app/Model/User.php b/app/Model/User.php index be2b034b..845efac0 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -5,6 +5,7 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; use Core\Session; +use Core\Security; /** * User model @@ -114,6 +115,10 @@ class User extends Base */ public function getByGoogleId($google_id) { + if (empty($google_id)) { + return false; + } + return $this->db->table(self::TABLE)->eq('google_id', $google_id)->findOne(); } @@ -126,6 +131,10 @@ class User extends Base */ public function getByGitHubId($github_id) { + if (empty($github_id)) { + return false; + } + return $this->db->table(self::TABLE)->eq('github_id', $github_id)->findOne(); } @@ -158,6 +167,22 @@ class User extends Base } /** + * Fetch user by using the token + * + * @access public + * @param string $token Token + * @return array + */ + public function getByToken($token) + { + if (empty($token)) { + return false; + } + + return $this->db->table(self::TABLE)->eq('token', $token)->findOne(); + } + + /** * Get all users * * @access public @@ -301,6 +326,36 @@ class User extends Base } /** + * Enable public access for a user + * + * @access public + * @param integer $user_id User id + * @return bool + */ + public function enablePublicAccess($user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $user_id) + ->save(array('token' => Security::generateToken())); + } + + /** + * Disable public access for a user + * + * @access public + * @param integer $user_id User id + * @return bool + */ + public function disablePublicAccess($user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $user_id) + ->save(array('token' => '')); + } + + /** * Common validation rules * * @access private diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 7195c7fe..593cca80 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,12 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 69; +const VERSION = 70; + +function version_70($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN token VARCHAR(255) DEFAULT ''"); +} function version_69($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index 27efe8c4..4ec6965f 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,12 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 50; +const VERSION = 51; + +function version_51($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN token VARCHAR(255) DEFAULT ''"); +} function version_50($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 7af7bbe8..84892aaf 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,12 @@ use Core\Security; use PDO; use Model\Link; -const VERSION = 68; +const VERSION = 69; + +function version_69($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN token TEXT DEFAULT ''"); +} function version_68($pdo) { diff --git a/app/Template/project/share.php b/app/Template/project/share.php index 7c490008..f28ed6b2 100644 --- a/app/Template/project/share.php +++ b/app/Template/project/share.php @@ -8,8 +8,8 @@ <ul class="no-bullet"> <li><strong><i class="fa fa-share-alt"></i> <?= $this->a(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?></strong></li> <li><strong><i class="fa fa-rss-square"></i> <?= $this->a(t('RSS feed'), 'project', 'feed', array('token' => $project['token']), false, '', '', true) ?></strong></li> + <li><strong><i class="fa fa-calendar"></i> <?= $this->a(t('iCalendar (iCal format, *.ics)'), 'ical', 'project', array('token' => $project['token']), false, '', '', true) ?></strong></li> </ul> - <input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('board', 'readonly', array('token' => $project['token'])) ?>"/> </div> <?= $this->a(t('Disable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'disable'), true, 'btn btn-red') ?> diff --git a/app/Template/user/share.php b/app/Template/user/share.php new file mode 100644 index 00000000..98010d4f --- /dev/null +++ b/app/Template/user/share.php @@ -0,0 +1,17 @@ +<div class="page-header"> + <h2><?= t('Public access') ?></h2> +</div> + +<?php if (! empty($user['token'])): ?> + + <div class="listing"> + <ul class="no-bullet"> + <li><strong><i class="fa fa-calendar"></i> <?= $this->a(t('iCalendar (iCal format, *.ics)'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?></strong></li> + </ul> + </div> + + <?= $this->a(t('Disable public access'), 'user', 'share', array('user_id' => $user['id'], 'switch' => 'disable'), true, 'btn btn-red') ?> + +<?php else: ?> + <?= $this->a(t('Enable public access'), 'user', 'share', array('user_id' => $user['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?> +<?php endif ?> diff --git a/app/Template/user/show.php b/app/Template/user/show.php index 490d8fb3..9473b382 100644 --- a/app/Template/user/show.php +++ b/app/Template/user/show.php @@ -5,10 +5,35 @@ <li><?= t('Username:') ?> <strong><?= $this->e($user['username']) ?></strong></li> <li><?= t('Name:') ?> <strong><?= $this->e($user['name']) ?: t('None') ?></strong></li> <li><?= t('Email:') ?> <strong><?= $this->e($user['email']) ?: t('None') ?></strong></li> +</ul> + +<div class="page-header"> + <h2><?= t('Security') ?></h2> +</div> +<ul class="listing"> + <li><?= t('Group:') ?> <strong><?= $user['is_admin'] ? t('Administrator') : t('Regular user') ?></strong></li> + <li><?= t('Account type:') ?> <strong><?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?></strong></li> + <li><?= $user['twofactor_activated'] == 1 ? t('Two factor authentication enabled') : t('Two factor authentication disabled') ?></li> +</ul> + +<div class="page-header"> + <h2><?= t('Preferences') ?></h2> +</div> +<ul class="listing"> <li><?= t('Default project:') ?> <strong><?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? $this->e($projects[$user['default_project_id']]) : t('None') ?></strong></li> <li><?= t('Timezone:') ?> <strong><?= $this->inList($user['timezone'], $timezones) ?></strong></li> <li><?= t('Language:') ?> <strong><?= $this->inList($user['language'], $languages) ?></strong></li> <li><?= t('Notifications:') ?> <strong><?= $user['notifications_enabled'] == 1 ? t('Enabled') : t('Disabled') ?></strong></li> - <li><?= t('Group:') ?> <strong><?= $user['is_admin'] ? t('Administrator') : t('Regular user') ?></strong></li> - <li><?= t('Account type:') ?> <strong><?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?></strong></li> </ul> + +<?php if (! empty($user['token'])): ?> + <div class="page-header"> + <h2><?= t('Public access') ?></h2> + </div> + + <div class="listing"> + <ul class="no-bullet"> + <li><strong><i class="fa fa-calendar"></i> <?= $this->a(t('iCalendar (iCal format, *.ics)'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?></strong></li> + </ul> + </div> +<?php endif ?> diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php index ff0fb009..8c7e3cf8 100644 --- a/app/Template/user/sidebar.php +++ b/app/Template/user/sidebar.php @@ -49,6 +49,9 @@ <?php endif ?> <li> + <?= $this->a(t('Public access'), 'user', 'share', array('user_id' => $user['id'])) ?> + </li> + <li> <?= $this->a(t('Email notifications'), 'user', 'notifications', array('user_id' => $user['id'])) ?> </li> <li> |