Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68ee022ee8 | ||
|
|
6a87749efd | ||
|
|
01fa57e17a | ||
|
|
43d1e8b58c | ||
|
|
94a9f89416 | ||
|
|
cce89ba4a2 | ||
|
|
5da0d865e0 | ||
|
|
bb891e182e | ||
|
|
392b2d737b | ||
|
|
23bbaa8c60 |
No files matched your search
@@ -0,0 +1,13 @@
|
|||||||
|
# Contributions to kanboard/kanboard
|
||||||
|
|
||||||
|
Kanban project management software
|
||||||
|
|
||||||
|
This repository holds only my commits to the upstream project, with their original author, date and diff.
|
||||||
|
Each of my commits is preceded by a snapshot of the upstream files it touched, so every diff shows exactly what I changed.
|
||||||
|
|
||||||
|
## Merged pull requests
|
||||||
|
|
||||||
|
- [fix(ical): only accept known columns as the calendar start date](https://github.com/kanboard/kanboard/pull/5914) (2026-10-11)
|
||||||
|
- [fix(security): escape the project name in the analytic modal title](https://github.com/kanboard/kanboard/pull/5913) (2026-10-11)
|
||||||
|
- [fix(session): scope the session cookie to the directory without a trailing slash](https://github.com/kanboard/kanboard/pull/5900) (2026-10-02)
|
||||||
|
- [fix(api): return a full batch response when one request is forbidden](https://github.com/kanboard/kanboard/pull/5899) (2026-09-11)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Api\Procedure;
|
||||||
|
|
||||||
|
use Kanboard\Api\Authorization\ProcedureAuthorization;
|
||||||
|
use Kanboard\Api\Authorization\UserAuthorization;
|
||||||
|
use Kanboard\Core\Base;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class
|
||||||
|
*
|
||||||
|
* @package Kanboard\Api\Procedure
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
abstract class BaseProcedure extends Base
|
||||||
|
{
|
||||||
|
public function beforeProcedure($procedure)
|
||||||
|
{
|
||||||
|
ProcedureAuthorization::getInstance($this->container)->check($procedure);
|
||||||
|
UserAuthorization::getInstance($this->container)->check($this->getClassName(), $procedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterValues(array $values)
|
||||||
|
{
|
||||||
|
foreach ($values as $key => $value) {
|
||||||
|
if (is_null($value)) {
|
||||||
|
unset($values[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterUser($user)
|
||||||
|
{
|
||||||
|
return is_array($user) ? $this->userModel->removePrivateColumns($user) : $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterUsers($users)
|
||||||
|
{
|
||||||
|
return is_array($users) ? array_map(array($this, 'filterUser'), $users) : $users;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterComment($comment)
|
||||||
|
{
|
||||||
|
if (is_array($comment)) {
|
||||||
|
unset($comment['email']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterComments($comments)
|
||||||
|
{
|
||||||
|
return is_array($comments) ? array_map(array($this, 'filterComment'), $comments) : $comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterEvent(array $event)
|
||||||
|
{
|
||||||
|
unset($event['email']);
|
||||||
|
|
||||||
|
if (isset($event['comment'])) {
|
||||||
|
$event['comment'] = $this->filterComment($event['comment']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $event;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterEvents(array $events)
|
||||||
|
{
|
||||||
|
return array_map(array($this, 'filterEvent'), $events);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getClassName()
|
||||||
|
{
|
||||||
|
$reflection = new ReflectionClass(get_called_class());
|
||||||
|
return $reflection->getShortName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Api\Procedure;
|
||||||
|
|
||||||
|
use Kanboard\Api\Authorization\CommentAuthorization;
|
||||||
|
use Kanboard\Api\Authorization\TaskAuthorization;
|
||||||
|
use Kanboard\Core\Security\Role;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comment API controller
|
||||||
|
*
|
||||||
|
* @package Kanboard\Api\Procedure
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class CommentProcedure extends BaseProcedure
|
||||||
|
{
|
||||||
|
public function getComment($comment_id)
|
||||||
|
{
|
||||||
|
CommentAuthorization::getInstance($this->container)->check($this->getClassName(), 'getComment', $comment_id);
|
||||||
|
return $this->filterComment($this->commentModel->getById($comment_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllComments($task_id)
|
||||||
|
{
|
||||||
|
TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllComments', $task_id);
|
||||||
|
$comments = $this->commentModel->getAll($task_id);
|
||||||
|
$filteredComments = [];
|
||||||
|
$userRole = $this->userSession->getRole();
|
||||||
|
|
||||||
|
foreach ($comments as $comment) {
|
||||||
|
if ($userRole === Role::APP_MANAGER && $comment['visibility'] === Role::APP_ADMIN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userRole === Role::APP_USER && $comment['visibility'] !== Role::APP_USER) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filteredComments[] = $comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->filterComments($filteredComments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeComment($comment_id)
|
||||||
|
{
|
||||||
|
CommentAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeComment', $comment_id);
|
||||||
|
|
||||||
|
if ($this->userSession->isLogged()) {
|
||||||
|
$loggedUserID = $this->userSession->getId();
|
||||||
|
$comment = $this->commentModel->getById($comment_id);
|
||||||
|
if ($comment['user_id'] != $loggedUserID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->commentModel->remove($comment_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createComment($task_id, $user_id, $content, $reference = '', $visibility = Role::APP_USER)
|
||||||
|
{
|
||||||
|
TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'createComment', $task_id);
|
||||||
|
|
||||||
|
if ($this->userSession->isLogged()) {
|
||||||
|
$loggedUserID = $this->userSession->getId();
|
||||||
|
if ($user_id != $loggedUserID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userRole = $this->userSession->getRole();
|
||||||
|
if ($userRole === Role::APP_MANAGER && $visibility === Role::APP_ADMIN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userRole === Role::APP_USER && $visibility !== Role::APP_USER) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = array(
|
||||||
|
'task_id' => $task_id,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'comment' => $content,
|
||||||
|
'reference' => $reference,
|
||||||
|
'visibility' => $visibility,
|
||||||
|
);
|
||||||
|
|
||||||
|
list($valid, ) = $this->commentValidator->validateCreation($values);
|
||||||
|
|
||||||
|
return $valid ? $this->commentModel->create($values) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateComment($id, $content)
|
||||||
|
{
|
||||||
|
CommentAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateComment', $id);
|
||||||
|
|
||||||
|
if ($this->userSession->isLogged()) {
|
||||||
|
$loggedUserID = $this->userSession->getId();
|
||||||
|
$comment = $this->commentModel->getById($id);
|
||||||
|
if ($comment['user_id'] != $loggedUserID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = array(
|
||||||
|
'id' => $id,
|
||||||
|
'comment' => $content,
|
||||||
|
);
|
||||||
|
|
||||||
|
list($valid, ) = $this->commentValidator->validateModification($values);
|
||||||
|
return $valid && $this->commentModel->update($values);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Api\Procedure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Me API controller
|
||||||
|
*
|
||||||
|
* @package Kanboard\Api\Procedure
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class MeProcedure extends BaseProcedure
|
||||||
|
{
|
||||||
|
public function getMe()
|
||||||
|
{
|
||||||
|
return session_get('user');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMyDashboard()
|
||||||
|
{
|
||||||
|
$userId = $this->userSession->getId();
|
||||||
|
|
||||||
|
return $this->taskListSubtaskAssigneeFormatter
|
||||||
|
->withQuery($this->taskFinderModel->getUserQuery($userId))
|
||||||
|
->withUserId($userId)
|
||||||
|
->format();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMyActivityStream()
|
||||||
|
{
|
||||||
|
$project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId());
|
||||||
|
return $this->filterEvents($this->helper->projectActivity->getProjectsEvents($project_ids, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createMyPrivateProject($name, $description = null)
|
||||||
|
{
|
||||||
|
if ($this->configModel->get('disable_private_project', 0) == 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = array(
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'is_private' => 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
list($valid, ) = $this->projectValidator->validateCreation($values);
|
||||||
|
return $valid ? $this->projectModel->create($values, $this->userSession->getId(), true) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMyProjectsList()
|
||||||
|
{
|
||||||
|
return (object) $this->projectUserRoleModel->getProjectsByUser($this->userSession->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMyOverdueTasks()
|
||||||
|
{
|
||||||
|
return $this->taskFinderModel->getOverdueTasksByUser($this->userSession->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMyProjects()
|
||||||
|
{
|
||||||
|
$project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId());
|
||||||
|
$projects = $this->projectModel->getAllByIds($project_ids);
|
||||||
|
|
||||||
|
return $this->projectsApiFormatter->withProjects($projects)->format();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Api\Procedure;
|
||||||
|
|
||||||
|
use Kanboard\Api\Authorization\ProjectAuthorization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project API controller
|
||||||
|
*
|
||||||
|
* @package Kanboard\Api\Procedure
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class ProjectProcedure extends BaseProcedure
|
||||||
|
{
|
||||||
|
public function getProjectById($project_id)
|
||||||
|
{
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectById', $project_id);
|
||||||
|
$project = $this->projectModel->getById($project_id);
|
||||||
|
return $this->projectApiFormatter->withProject($project)->format();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProjectByName($name)
|
||||||
|
{
|
||||||
|
$project = $this->projectModel->getByName($name);
|
||||||
|
|
||||||
|
if (empty($project)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectByName', $project['id']);
|
||||||
|
return $this->projectApiFormatter->withProject($project)->format();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProjectByIdentifier($identifier)
|
||||||
|
{
|
||||||
|
$project = $this->projectModel->getByIdentifier($identifier);
|
||||||
|
|
||||||
|
if (empty($project)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectByIdentifier', $project['id']);
|
||||||
|
return $this->projectApiFormatter->withProject($project)->format();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProjectByEmail($email)
|
||||||
|
{
|
||||||
|
$project = $this->projectModel->getByEmail($email);
|
||||||
|
|
||||||
|
if (empty($project)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectByEmail', $project['id']);
|
||||||
|
return $this->projectApiFormatter->withProject($project)->format();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllProjects()
|
||||||
|
{
|
||||||
|
return $this->projectsApiFormatter->withProjects($this->projectModel->getAll())->format();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeProject($project_id)
|
||||||
|
{
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeProject', $project_id);
|
||||||
|
return $this->projectModel->remove($project_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enableProject($project_id)
|
||||||
|
{
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'enableProject', $project_id);
|
||||||
|
return $this->projectModel->enable($project_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disableProject($project_id)
|
||||||
|
{
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'disableProject', $project_id);
|
||||||
|
return $this->projectModel->disable($project_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enableProjectPublicAccess($project_id)
|
||||||
|
{
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'enableProjectPublicAccess', $project_id);
|
||||||
|
return $this->projectModel->enablePublicAccess($project_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disableProjectPublicAccess($project_id)
|
||||||
|
{
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'disableProjectPublicAccess', $project_id);
|
||||||
|
return $this->projectModel->disablePublicAccess($project_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProjectActivities(array $project_ids)
|
||||||
|
{
|
||||||
|
foreach ($project_ids as $project_id) {
|
||||||
|
ProjectAuthorization::getInstance($this->container)
|
||||||
|
->check($this->getClassName(), 'getProjectActivities', $project_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->filterEvents($this->helper->projectActivity->getProjectsEvents($project_ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProjectActivity($project_id)
|
||||||
|
{
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectActivity', $project_id);
|
||||||
|
return $this->filterEvents($this->helper->projectActivity->getProjectEvents($project_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createProject($name, $description = null, $owner_id = 0, $identifier = null, $start_date = null, $end_date = null, $priority_default = null, $priority_start = null, $priority_end = null, $email = null)
|
||||||
|
{
|
||||||
|
if ($owner_id === 0 && $this->userSession->isLogged()) {
|
||||||
|
$owner_id = $this->userSession->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = $this->filterValues(array(
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'identifier' => $identifier,
|
||||||
|
'start_date' => $start_date,
|
||||||
|
'end_date' => $end_date,
|
||||||
|
'priority_default' => $priority_default,
|
||||||
|
'priority_start' => $priority_start,
|
||||||
|
'priority_end' => $priority_end,
|
||||||
|
'email' => $email,
|
||||||
|
));
|
||||||
|
|
||||||
|
list($valid, ) = $this->projectValidator->validateCreation($values);
|
||||||
|
return $valid ? $this->projectModel->create($values, $owner_id, $this->userSession->isLogged()) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateProject($project_id, $name = null, $description = null, $owner_id = null, $identifier = null, $start_date = null, $end_date = null, $priority_default = null, $priority_start = null, $priority_end = null, $email = null)
|
||||||
|
{
|
||||||
|
ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateProject', $project_id);
|
||||||
|
|
||||||
|
$values = $this->filterValues(array(
|
||||||
|
'id' => $project_id,
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'owner_id' => $owner_id,
|
||||||
|
'identifier' => $identifier,
|
||||||
|
'start_date' => $start_date,
|
||||||
|
'end_date' => $end_date,
|
||||||
|
'priority_default' => $priority_default,
|
||||||
|
'priority_start' => $priority_start,
|
||||||
|
'priority_end' => $priority_end,
|
||||||
|
'email' => $email,
|
||||||
|
));
|
||||||
|
|
||||||
|
list($valid, ) = $this->projectValidator->validateModification($values);
|
||||||
|
return $valid && $this->projectModel->update($values);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,7 +91,7 @@ class CurrencyController extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function update()
|
public function update()
|
||||||
{
|
{
|
||||||
$values = $this->request->getValues();
|
$values = array_intersect_key($this->request->getValues(), array('application_currency' => ''));
|
||||||
|
|
||||||
if ($this->configModel->save($values)) {
|
if ($this->configModel->save($values)) {
|
||||||
$this->flash->success(t('Settings saved successfully.'));
|
$this->flash->success(t('Settings saved successfully.'));
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ICalendarController extends BaseController
|
|||||||
$startRange = strtotime('-2 months');
|
$startRange = strtotime('-2 months');
|
||||||
$endRange = strtotime('+6 months');
|
$endRange = strtotime('+6 months');
|
||||||
|
|
||||||
$startColumn = $this->configModel->get('calendar_user_tasks', 'date_started');
|
$startColumn = $this->getStartColumn('calendar_user_tasks');
|
||||||
|
|
||||||
$calendar = new iCalendar('Kanboard');
|
$calendar = new iCalendar('Kanboard');
|
||||||
$calendar->setName($user['name'] ?: $user['username']);
|
$calendar->setName($user['name'] ?: $user['username']);
|
||||||
@@ -71,7 +71,7 @@ class ICalendarController extends BaseController
|
|||||||
$startRange = strtotime('-2 months');
|
$startRange = strtotime('-2 months');
|
||||||
$endRange = strtotime('+6 months');
|
$endRange = strtotime('+6 months');
|
||||||
|
|
||||||
$startColumn = $this->configModel->get('calendar_project_tasks', 'date_started');
|
$startColumn = $this->getStartColumn('calendar_project_tasks');
|
||||||
|
|
||||||
$calendar = new iCalendar('Kanboard');
|
$calendar = new iCalendar('Kanboard');
|
||||||
$calendar->setName($project['name']);
|
$calendar->setName($project['name']);
|
||||||
@@ -99,6 +99,12 @@ class ICalendarController extends BaseController
|
|||||||
->format());
|
->format());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getStartColumn($option)
|
||||||
|
{
|
||||||
|
$column = $this->configModel->get($option, 'date_started');
|
||||||
|
return in_array($column, array('date_creation', 'date_started'), true) ? $column : 'date_started';
|
||||||
|
}
|
||||||
|
|
||||||
protected function getConditionForTasksWithStartAndDueDate($start_time, $end_time, $start_column, $end_column)
|
protected function getConditionForTasksWithStartAndDueDate($start_time, $end_time, $start_column, $end_column)
|
||||||
{
|
{
|
||||||
$start_time = (int) $start_time;
|
$start_time = (int) $start_time;
|
||||||
|
|||||||
@@ -0,0 +1,468 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Core\Http;
|
||||||
|
|
||||||
|
use Kanboard\Core\Base;
|
||||||
|
use Kanboard\Job\HttpAsyncJob;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP client
|
||||||
|
*
|
||||||
|
* @package Kanboard\Core\Http
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class Client extends Base
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* HTTP client user agent
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const HTTP_USER_AGENT = 'Kanboard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a GET HTTP request
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $url
|
||||||
|
* @param string[] $headers
|
||||||
|
* @param bool $raiseForErrors
|
||||||
|
* @param bool $followRedirects
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get($url, array $headers = [], $raiseForErrors = false, $followRedirects = true)
|
||||||
|
{
|
||||||
|
return $this->doRequest('GET', $url, '', $headers, $raiseForErrors, $followRedirects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a GET HTTP request and parse JSON response
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $url
|
||||||
|
* @param string[] $headers
|
||||||
|
* @param bool $raiseForErrors
|
||||||
|
* @param bool $followRedirects
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getJson($url, array $headers = [], $raiseForErrors = false, $followRedirects = true)
|
||||||
|
{
|
||||||
|
$response = $this->doRequest('GET', $url, '', array_merge(['Accept: application/json'], $headers), $raiseForErrors, $followRedirects);
|
||||||
|
return json_decode($response, true) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a POST HTTP request encoded in JSON
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $url
|
||||||
|
* @param array $data
|
||||||
|
* @param string[] $headers
|
||||||
|
* @param bool $raiseForErrors
|
||||||
|
* @param bool $followRedirects
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function postJson($url, array $data, array $headers = [], $raiseForErrors = false, $followRedirects = true)
|
||||||
|
{
|
||||||
|
return $this->doRequest(
|
||||||
|
'POST',
|
||||||
|
$url,
|
||||||
|
json_encode($data),
|
||||||
|
array_merge(['Content-type: application/json'], $headers),
|
||||||
|
$raiseForErrors,
|
||||||
|
$followRedirects
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a POST HTTP request encoded in JSON (Fire and forget)
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $url
|
||||||
|
* @param array $data
|
||||||
|
* @param string[] $headers
|
||||||
|
* @param bool $raiseForErrors
|
||||||
|
*/
|
||||||
|
public function postJsonAsync($url, array $data, array $headers = [], $raiseForErrors = false)
|
||||||
|
{
|
||||||
|
$this->queueManager->push(HttpAsyncJob::getInstance($this->container)->withParams(
|
||||||
|
'POST',
|
||||||
|
$url,
|
||||||
|
json_encode($data),
|
||||||
|
array_merge(['Content-type: application/json'], $headers),
|
||||||
|
$raiseForErrors
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a POST HTTP request encoded in www-form-urlencoded
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $url
|
||||||
|
* @param array $data
|
||||||
|
* @param string[] $headers
|
||||||
|
* @param bool $raiseForErrors
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function postForm($url, array $data, array $headers = [], $raiseForErrors = false)
|
||||||
|
{
|
||||||
|
return $this->doRequest(
|
||||||
|
'POST',
|
||||||
|
$url,
|
||||||
|
http_build_query($data),
|
||||||
|
array_merge(['Content-type: application/x-www-form-urlencoded'], $headers),
|
||||||
|
$raiseForErrors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a POST HTTP request encoded in www-form-urlencoded (fire and forget)
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $url
|
||||||
|
* @param array $data
|
||||||
|
* @param string[] $headers
|
||||||
|
* @param bool $raiseForErrors
|
||||||
|
*/
|
||||||
|
public function postFormAsync($url, array $data, array $headers = [], $raiseForErrors = false)
|
||||||
|
{
|
||||||
|
$this->queueManager->push(HttpAsyncJob::getInstance($this->container)->withParams(
|
||||||
|
'POST',
|
||||||
|
$url,
|
||||||
|
http_build_query($data),
|
||||||
|
array_merge(['Content-type: application/x-www-form-urlencoded'], $headers),
|
||||||
|
$raiseForErrors
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the HTTP request with cURL if detected, socket otherwise
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $method
|
||||||
|
* @param string $url
|
||||||
|
* @param string $content
|
||||||
|
* @param string[] $headers
|
||||||
|
* @param bool $raiseForErrors
|
||||||
|
* @param bool $followRedirects
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function doRequest($method, $url, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
|
||||||
|
{
|
||||||
|
$requestBody = '';
|
||||||
|
|
||||||
|
if (! empty($url)) {
|
||||||
|
if (function_exists('curl_version')) {
|
||||||
|
if (DEBUG) {
|
||||||
|
$this->logger->debug('HttpClient::doRequest: cURL detected');
|
||||||
|
}
|
||||||
|
$requestBody = $this->doRequestWithCurl($method, $url, $content, $headers, $raiseForErrors, $followRedirects);
|
||||||
|
} else {
|
||||||
|
if (DEBUG) {
|
||||||
|
$this->logger->debug('HttpClient::doRequest: using socket');
|
||||||
|
}
|
||||||
|
$requestBody = $this->doRequestWithSocket($method, $url, $content, $headers, $raiseForErrors, $followRedirects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $requestBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the HTTP request with socket
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $method
|
||||||
|
* @param string $url
|
||||||
|
* @param string $content
|
||||||
|
* @param string[] $headers
|
||||||
|
* @param bool $raiseForErrors
|
||||||
|
* @param bool $followRedirects
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function doRequestWithSocket($method, $url, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
|
||||||
|
{
|
||||||
|
$startTime = microtime(true);
|
||||||
|
$stream = @fopen(trim($url), 'r', false, stream_context_create($this->getContext($method, $content, $headers, $raiseForErrors, $followRedirects)));
|
||||||
|
|
||||||
|
if (! is_resource($stream)) {
|
||||||
|
$this->logger->error('HttpClient: request failed ('.$url.')');
|
||||||
|
|
||||||
|
if ($raiseForErrors) {
|
||||||
|
throw new ClientException('Unreachable URL: '.$url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = stream_get_contents($stream);
|
||||||
|
$metadata = stream_get_meta_data($stream);
|
||||||
|
|
||||||
|
if ($raiseForErrors && array_key_exists('wrapper_data', $metadata)) {
|
||||||
|
$statusCode = $this->getStatusCode($metadata['wrapper_data']);
|
||||||
|
|
||||||
|
if ($statusCode >= 400) {
|
||||||
|
throw new InvalidStatusException('Request failed with status code '.$statusCode, $statusCode, $body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
$this->logger->debug('HttpClient: url='.$url);
|
||||||
|
$this->logger->debug('HttpClient: headers='.var_export($headers, true));
|
||||||
|
$this->logger->debug('HttpClient: payload='.$content);
|
||||||
|
$this->logger->debug('HttpClient: metadata='.var_export($metadata, true));
|
||||||
|
$this->logger->debug('HttpClient: body='.$body);
|
||||||
|
$this->logger->debug('HttpClient: executionTime='.(microtime(true) - $startTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the HTTP request with cURL
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $method
|
||||||
|
* @param string $url
|
||||||
|
* @param string $content
|
||||||
|
* @param string[] $headers
|
||||||
|
* @param bool $raiseForErrors
|
||||||
|
* @param bool $followRedirects
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function doRequestWithCurl($method, $url, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
|
||||||
|
{
|
||||||
|
$startTime = microtime(true);
|
||||||
|
$curlSession = @curl_init();
|
||||||
|
|
||||||
|
curl_setopt($curlSession, CURLOPT_URL, trim($url));
|
||||||
|
curl_setopt($curlSession, CURLOPT_USERAGENT, self::HTTP_USER_AGENT);
|
||||||
|
curl_setopt($curlSession, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
|
||||||
|
curl_setopt($curlSession, CURLOPT_TIMEOUT, HTTP_TIMEOUT);
|
||||||
|
curl_setopt($curlSession, CURLOPT_FORBID_REUSE, true);
|
||||||
|
curl_setopt($curlSession, CURLOPT_MAXREDIRS, $followRedirects ? HTTP_MAX_REDIRECTS : 0);
|
||||||
|
curl_setopt($curlSession, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($curlSession, CURLOPT_FOLLOWLOCATION, $followRedirects);
|
||||||
|
|
||||||
|
if ('POST' === $method) {
|
||||||
|
curl_setopt($curlSession, CURLOPT_POST, true);
|
||||||
|
curl_setopt($curlSession, CURLOPT_POSTFIELDS, $content);
|
||||||
|
} elseif ('PUT' === $method) {
|
||||||
|
curl_setopt($curlSession, CURLOPT_CUSTOMREQUEST, 'PUT');
|
||||||
|
curl_setopt($curlSession, CURLOPT_POST, true);
|
||||||
|
curl_setopt($curlSession, CURLOPT_POSTFIELDS, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($headers)) {
|
||||||
|
curl_setopt($curlSession, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HTTP_VERIFY_SSL_CERTIFICATE === false) {
|
||||||
|
curl_setopt($curlSession, CURLOPT_SSL_VERIFYHOST, 0);
|
||||||
|
curl_setopt($curlSession, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HTTP_PROXY_HOSTNAME) {
|
||||||
|
curl_setopt($curlSession, CURLOPT_PROXY, HTTP_PROXY_HOSTNAME);
|
||||||
|
curl_setopt($curlSession, CURLOPT_PROXYPORT, HTTP_PROXY_PORT);
|
||||||
|
curl_setopt($curlSession, CURLOPT_NOPROXY, HTTP_PROXY_EXCLUDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HTTP_PROXY_USERNAME) {
|
||||||
|
curl_setopt($curlSession, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
|
||||||
|
curl_setopt($curlSession, CURLOPT_PROXYUSERPWD, HTTP_PROXY_USERNAME.':'.HTTP_PROXY_PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = curl_exec($curlSession);
|
||||||
|
|
||||||
|
if ($body === false) {
|
||||||
|
$errorMsg = curl_error($curlSession);
|
||||||
|
curl_close($curlSession);
|
||||||
|
|
||||||
|
$this->logger->error('HttpClient: request failed ('.$url.' - '.$errorMsg.')');
|
||||||
|
|
||||||
|
if ($raiseForErrors) {
|
||||||
|
throw new ClientException('Unreachable URL: '.$url.' ('.$errorMsg.')');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($raiseForErrors) {
|
||||||
|
$statusCode = curl_getinfo($curlSession, CURLINFO_RESPONSE_CODE);
|
||||||
|
|
||||||
|
if ($statusCode >= 400) {
|
||||||
|
curl_close($curlSession);
|
||||||
|
throw new InvalidStatusException('Request failed with status code '.$statusCode, $statusCode, $body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
$this->logger->debug('HttpClient: url='.$url);
|
||||||
|
$this->logger->debug('HttpClient: headers='.var_export($headers, true));
|
||||||
|
$this->logger->debug('HttpClient: payload='.$content);
|
||||||
|
$this->logger->debug('HttpClient: metadata='.var_export(curl_getinfo($curlSession), true));
|
||||||
|
$this->logger->debug('HttpClient: body='.$body);
|
||||||
|
$this->logger->debug('HttpClient: executionTime='.(microtime(true) - $startTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_close($curlSession);
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stream context
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $method
|
||||||
|
* @param string $content
|
||||||
|
* @param string[] $headers
|
||||||
|
* @param bool $raiseForErrors
|
||||||
|
* @param bool $followRedirects
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getContext($method, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
|
||||||
|
{
|
||||||
|
$default_headers = [
|
||||||
|
'User-Agent: '.self::HTTP_USER_AGENT,
|
||||||
|
'Connection: close',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (HTTP_PROXY_USERNAME) {
|
||||||
|
$default_headers[] = 'Proxy-Authorization: Basic '.base64_encode(HTTP_PROXY_USERNAME.':'.HTTP_PROXY_PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = array_merge($default_headers, $headers);
|
||||||
|
|
||||||
|
$context = [
|
||||||
|
'http' => [
|
||||||
|
'method' => $method,
|
||||||
|
'protocol_version' => 1.1,
|
||||||
|
'timeout' => HTTP_TIMEOUT,
|
||||||
|
'max_redirects' => $followRedirects ? HTTP_MAX_REDIRECTS : 0,
|
||||||
|
'follow_location' => $followRedirects ? 1 : 0,
|
||||||
|
'header' => implode("\r\n", $headers),
|
||||||
|
'content' => $content,
|
||||||
|
'ignore_errors' => $raiseForErrors,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
if (HTTP_PROXY_HOSTNAME) {
|
||||||
|
$context['http']['proxy'] = 'tcp://'.HTTP_PROXY_HOSTNAME.':'.HTTP_PROXY_PORT;
|
||||||
|
$context['http']['request_fulluri'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HTTP_VERIFY_SSL_CERTIFICATE === false) {
|
||||||
|
$context['ssl'] = [
|
||||||
|
'verify_peer' => false,
|
||||||
|
'verify_peer_name' => false,
|
||||||
|
'allow_self_signed' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getStatusCode(array $lines)
|
||||||
|
{
|
||||||
|
$status = 200;
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (strpos($line, 'HTTP/1') === 0) {
|
||||||
|
$status = (int) substr($line, 9, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get backend used for making HTTP connections
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function backend()
|
||||||
|
{
|
||||||
|
return function_exists('curl_version') ? 'cURL' : 'socket';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an IP address is private
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $ip
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isPrivateIpAddress($ip)
|
||||||
|
{
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP) === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv4-mapped IPv6 address (::ffff:a.b.c.d), check the IPv4 part
|
||||||
|
$address = inet_pton($ip);
|
||||||
|
if (strlen($address) === 16 && substr($address, 0, 12) === str_repeat("\0", 10)."\xff\xff") {
|
||||||
|
$ip = inet_ntop(substr($address, 12));
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a URL is private (RFC1918, localhost, etc.)
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $url
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isPrivateURL($url)
|
||||||
|
{
|
||||||
|
$parsedUrl = parse_url($url);
|
||||||
|
|
||||||
|
if (!isset($parsedUrl['scheme']) || !in_array(strtolower($parsedUrl['scheme']), ['http', 'https'], true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($parsedUrl['host'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = rtrim(trim($parsedUrl['host'], " \t\n\r\0\x0B[]"), '.');
|
||||||
|
if ($host === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($host, FILTER_VALIDATE_IP) !== false) {
|
||||||
|
return $this->isPrivateIpAddress($host);
|
||||||
|
}
|
||||||
|
|
||||||
|
$addresses = gethostbynamel($host) ?: [];
|
||||||
|
|
||||||
|
if (function_exists('dns_get_record')) {
|
||||||
|
$dnsRecords = @dns_get_record($host, DNS_AAAA);
|
||||||
|
if (is_array($dnsRecords)) {
|
||||||
|
foreach ($dnsRecords as $record) {
|
||||||
|
if (isset($record['type']) && $record['type'] === 'AAAA' && isset($record['ipv6'])) {
|
||||||
|
$addresses[] = $record['ipv6'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refuse hosts we cannot resolve: the HTTP client may still connect to them,
|
||||||
|
// for example cURL reads "0x7f000001" as 127.0.0.1.
|
||||||
|
if (empty($addresses)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($addresses as $address) {
|
||||||
|
if ($this->isPrivateIpAddress($address)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Formatter;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use Eluceo\iCal\Component\Calendar;
|
||||||
|
use Eluceo\iCal\Component\Event;
|
||||||
|
use Eluceo\iCal\Property\Event\Attendees;
|
||||||
|
use Eluceo\iCal\Property\Event\Organizer;
|
||||||
|
use Kanboard\Core\Filter\FormatterInterface;
|
||||||
|
use PicoDb\Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iCal event formatter for tasks
|
||||||
|
*
|
||||||
|
* @package formatter
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class TaskICalFormatter extends BaseFormatter implements FormatterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Calendar object
|
||||||
|
*
|
||||||
|
* @access protected
|
||||||
|
* @var \Eluceo\iCal\Component\Calendar
|
||||||
|
*/
|
||||||
|
protected $vCalendar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Ical events
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function format()
|
||||||
|
{
|
||||||
|
return $this->vCalendar->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set calendar object
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param \Eluceo\iCal\Component\Calendar $vCalendar
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setCalendar(Calendar $vCalendar)
|
||||||
|
{
|
||||||
|
$this->vCalendar = $vCalendar;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform results to iCal events
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param Table $query
|
||||||
|
* @param string $startColumn
|
||||||
|
* @param string $endColumn
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function addTasksWithStartAndDueDate(Table $query, $startColumn, $endColumn)
|
||||||
|
{
|
||||||
|
foreach ($query->findAll() as $task) {
|
||||||
|
$start = new DateTime;
|
||||||
|
$start->setTimestamp($task[$startColumn]);
|
||||||
|
|
||||||
|
$end = new DateTime;
|
||||||
|
$end->setTimestamp($task[$endColumn] ?: time());
|
||||||
|
|
||||||
|
$vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$startColumn.'-'.$endColumn);
|
||||||
|
$vEvent->setDtStart($start);
|
||||||
|
$vEvent->setDtEnd($end);
|
||||||
|
|
||||||
|
$this->vCalendar->addComponent($vEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform results to all day iCal events
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param Table $query
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function addTasksWithDueDateOnly(Table $query)
|
||||||
|
{
|
||||||
|
foreach ($query->findAll() as $task) {
|
||||||
|
$date = new DateTime;
|
||||||
|
$date->setTimestamp($task['date_due']);
|
||||||
|
|
||||||
|
$vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-date_due');
|
||||||
|
$vEvent->setDtStart($date);
|
||||||
|
$vEvent->setDtEnd($date);
|
||||||
|
|
||||||
|
if ($date->format('Hi') === '0000') {
|
||||||
|
$vEvent->setNoTime(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->vCalendar->addComponent($vEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common events for task iCal events
|
||||||
|
*
|
||||||
|
* @access protected
|
||||||
|
* @param array $task
|
||||||
|
* @param string $uid
|
||||||
|
* @return 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->setDescription($task['description']);
|
||||||
|
$vEvent->setDescriptionHTML($this->helper->text->markdown($task['description']));
|
||||||
|
$vEvent->setUrl($this->helper->url->base().$this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'])));
|
||||||
|
|
||||||
|
if (! empty($task['owner_id'])) {
|
||||||
|
$attendees = new Attendees;
|
||||||
|
$attendees->add(
|
||||||
|
'MAILTO:'.$task['assignee_username'].'@kanboard.local',
|
||||||
|
array('CN' => $task['assignee_name'] ?: $task['assignee_username'])
|
||||||
|
);
|
||||||
|
$vEvent->setAttendees($attendees);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($task['creator_id'])) {
|
||||||
|
$vEvent->setOrganizer(new Organizer(
|
||||||
|
'MAILTO:'.$task['creator_username'].'@kanboard.local',
|
||||||
|
array('CN' => $task['creator_name'] ?: $task['creator_username'])
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $vEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Model;
|
||||||
|
|
||||||
|
use Kanboard\Core\Base;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task Finder model
|
||||||
|
*
|
||||||
|
* @package Kanboard\Model
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class TaskFinderModel extends Base
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get query for project user overview
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param array $project_ids
|
||||||
|
* @param integer $is_active
|
||||||
|
* @return \PicoDb\Table
|
||||||
|
*/
|
||||||
|
public function getProjectUserOverviewQuery(array $project_ids, $is_active)
|
||||||
|
{
|
||||||
|
if (empty($project_ids)) {
|
||||||
|
$project_ids = array(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->db
|
||||||
|
->table(TaskModel::TABLE)
|
||||||
|
->columns(
|
||||||
|
TaskModel::TABLE.'.id',
|
||||||
|
TaskModel::TABLE.'.title',
|
||||||
|
TaskModel::TABLE.'.date_due',
|
||||||
|
TaskModel::TABLE.'.date_started',
|
||||||
|
TaskModel::TABLE.'.project_id',
|
||||||
|
TaskModel::TABLE.'.color_id',
|
||||||
|
TaskModel::TABLE.'.priority',
|
||||||
|
TaskModel::TABLE.'.time_spent',
|
||||||
|
TaskModel::TABLE.'.time_estimated',
|
||||||
|
ProjectModel::TABLE.'.name AS project_name',
|
||||||
|
ColumnModel::TABLE.'.title AS column_name',
|
||||||
|
UserModel::TABLE.'.username AS assignee_username',
|
||||||
|
UserModel::TABLE.'.name AS assignee_name'
|
||||||
|
)
|
||||||
|
->eq(TaskModel::TABLE.'.is_active', $is_active)
|
||||||
|
->in(ProjectModel::TABLE.'.id', $project_ids)
|
||||||
|
->join(ProjectModel::TABLE, 'id', 'project_id')
|
||||||
|
->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
|
||||||
|
->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get query for assigned user tasks
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $user_id User id
|
||||||
|
* @return \PicoDb\Table
|
||||||
|
*/
|
||||||
|
public function getUserQuery($user_id)
|
||||||
|
{
|
||||||
|
return $this->getExtendedQuery()
|
||||||
|
->beginOr()
|
||||||
|
->eq(TaskModel::TABLE.'.owner_id', $user_id)
|
||||||
|
->inSubquery(TaskModel::TABLE.'.id', $this->db->table(SubtaskModel::TABLE)->columns('task_id')->eq('user_id', $user_id))
|
||||||
|
->closeOr()
|
||||||
|
->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN)
|
||||||
|
->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE)
|
||||||
|
->eq(ColumnModel::TABLE.'.hide_in_dashboard', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended query
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return \PicoDb\Table
|
||||||
|
*/
|
||||||
|
public function getExtendedQuery()
|
||||||
|
{
|
||||||
|
return $this->db
|
||||||
|
->table(TaskModel::TABLE)
|
||||||
|
->columns(
|
||||||
|
'(SELECT COUNT(*) FROM '.CommentModel::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
|
||||||
|
'(SELECT COUNT(*) FROM '.TaskFileModel::TABLE.' WHERE task_id=tasks.id) AS nb_files',
|
||||||
|
'(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id) AS nb_subtasks',
|
||||||
|
'(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
|
||||||
|
'(SELECT COUNT(*) FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id) AS nb_links',
|
||||||
|
'(SELECT COUNT(*) FROM '.TaskExternalLinkModel::TABLE.' WHERE '.TaskExternalLinkModel::TABLE.'.task_id = tasks.id) AS nb_external_links',
|
||||||
|
'(SELECT DISTINCT 1 FROM '.TaskLinkModel::TABLE.' tl JOIN '.LinkModel::TABLE.' l ON tl.link_id = l.id WHERE tl.task_id = tasks.id AND l.label = '."'is a milestone of') AS is_milestone",
|
||||||
|
TaskModel::TABLE.'.id',
|
||||||
|
TaskModel::TABLE.'.reference',
|
||||||
|
TaskModel::TABLE.'.title',
|
||||||
|
TaskModel::TABLE.'.description',
|
||||||
|
TaskModel::TABLE.'.date_creation',
|
||||||
|
TaskModel::TABLE.'.date_modification',
|
||||||
|
TaskModel::TABLE.'.date_completed',
|
||||||
|
TaskModel::TABLE.'.date_started',
|
||||||
|
TaskModel::TABLE.'.date_due',
|
||||||
|
TaskModel::TABLE.'.color_id',
|
||||||
|
TaskModel::TABLE.'.project_id',
|
||||||
|
TaskModel::TABLE.'.column_id',
|
||||||
|
TaskModel::TABLE.'.swimlane_id',
|
||||||
|
TaskModel::TABLE.'.owner_id',
|
||||||
|
TaskModel::TABLE.'.creator_id',
|
||||||
|
TaskModel::TABLE.'.position',
|
||||||
|
TaskModel::TABLE.'.is_active',
|
||||||
|
TaskModel::TABLE.'.score',
|
||||||
|
TaskModel::TABLE.'.category_id',
|
||||||
|
TaskModel::TABLE.'.priority',
|
||||||
|
TaskModel::TABLE.'.date_moved',
|
||||||
|
TaskModel::TABLE.'.recurrence_status',
|
||||||
|
TaskModel::TABLE.'.recurrence_trigger',
|
||||||
|
TaskModel::TABLE.'.recurrence_factor',
|
||||||
|
TaskModel::TABLE.'.recurrence_timeframe',
|
||||||
|
TaskModel::TABLE.'.recurrence_basedate',
|
||||||
|
TaskModel::TABLE.'.recurrence_parent',
|
||||||
|
TaskModel::TABLE.'.recurrence_child',
|
||||||
|
TaskModel::TABLE.'.time_estimated',
|
||||||
|
TaskModel::TABLE.'.time_spent',
|
||||||
|
TaskModel::TABLE.'.reference',
|
||||||
|
UserModel::TABLE.'.username AS assignee_username',
|
||||||
|
UserModel::TABLE.'.name AS assignee_name',
|
||||||
|
UserModel::TABLE.'.email AS assignee_email',
|
||||||
|
UserModel::TABLE.'.avatar_path AS assignee_avatar_path',
|
||||||
|
CategoryModel::TABLE.'.name AS category_name',
|
||||||
|
CategoryModel::TABLE.'.description AS category_description',
|
||||||
|
CategoryModel::TABLE.'.color_id AS category_color_id',
|
||||||
|
ColumnModel::TABLE.'.title AS column_name',
|
||||||
|
ColumnModel::TABLE.'.position AS column_position',
|
||||||
|
SwimlaneModel::TABLE.'.name AS swimlane_name',
|
||||||
|
ProjectModel::TABLE.'.name AS project_name'
|
||||||
|
)
|
||||||
|
->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
|
||||||
|
->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
|
||||||
|
->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
|
||||||
|
->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
|
||||||
|
->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
|
||||||
|
->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tasks for a given project and status
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id Project id
|
||||||
|
* @param integer $status_id Status id
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getAll($project_id, $status_id = TaskModel::STATUS_OPEN)
|
||||||
|
{
|
||||||
|
return $this->db
|
||||||
|
->table(TaskModel::TABLE)
|
||||||
|
->eq(TaskModel::TABLE.'.project_id', $project_id)
|
||||||
|
->eq(TaskModel::TABLE.'.is_active', $status_id)
|
||||||
|
->asc(TaskModel::TABLE.'.id')
|
||||||
|
->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tasks for a given project and status
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id
|
||||||
|
* @param array $status
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getAllIds($project_id, array $status = array(TaskModel::STATUS_OPEN))
|
||||||
|
{
|
||||||
|
return $this->db
|
||||||
|
->table(TaskModel::TABLE)
|
||||||
|
->eq(TaskModel::TABLE.'.project_id', $project_id)
|
||||||
|
->in(TaskModel::TABLE.'.is_active', $status)
|
||||||
|
->asc(TaskModel::TABLE.'.id')
|
||||||
|
->findAllByColumn(TaskModel::TABLE.'.id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get overdue tasks query
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return \PicoDb\Table
|
||||||
|
*/
|
||||||
|
public function getOverdueTasksQuery()
|
||||||
|
{
|
||||||
|
return $this->db->table(TaskModel::TABLE)
|
||||||
|
->columns(
|
||||||
|
TaskModel::TABLE.'.id',
|
||||||
|
TaskModel::TABLE.'.title',
|
||||||
|
TaskModel::TABLE.'.date_due',
|
||||||
|
TaskModel::TABLE.'.project_id',
|
||||||
|
TaskModel::TABLE.'.creator_id',
|
||||||
|
TaskModel::TABLE.'.owner_id',
|
||||||
|
ProjectModel::TABLE.'.name AS project_name',
|
||||||
|
UserModel::TABLE.'.username AS assignee_username',
|
||||||
|
UserModel::TABLE.'.name AS assignee_name'
|
||||||
|
)
|
||||||
|
->join(ProjectModel::TABLE, 'id', 'project_id')
|
||||||
|
->join(UserModel::TABLE, 'id', 'owner_id')
|
||||||
|
->eq(ProjectModel::TABLE.'.is_active', 1)
|
||||||
|
->eq(TaskModel::TABLE.'.is_active', 1)
|
||||||
|
->neq(TaskModel::TABLE.'.date_due', 0)
|
||||||
|
->lte(TaskModel::TABLE.'.date_due', time());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of overdue tasks for all projects
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getOverdueTasks()
|
||||||
|
{
|
||||||
|
return $this->getOverdueTasksQuery()->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of overdue tasks by project
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getOverdueTasksByProject($project_id)
|
||||||
|
{
|
||||||
|
return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.project_id', $project_id)->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of overdue tasks by user
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $user_id
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getOverdueTasksByUser($user_id)
|
||||||
|
{
|
||||||
|
return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.owner_id', $user_id)->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get project id for a given task
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $task_id Task id
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
public function getProjectId($task_id)
|
||||||
|
{
|
||||||
|
return (int) $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a task by the id
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $task_id Task id
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getById($task_id)
|
||||||
|
{
|
||||||
|
return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a task by the reference (external id)
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id Project id
|
||||||
|
* @param string $reference Task reference
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getByReference($project_id, $reference)
|
||||||
|
{
|
||||||
|
return $this->db->table(TaskModel::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get task details (fetch more information from other tables)
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $task_id Task id
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getDetails($task_id)
|
||||||
|
{
|
||||||
|
return $this->db->table(TaskModel::TABLE)
|
||||||
|
->columns(
|
||||||
|
TaskModel::TABLE.'.*',
|
||||||
|
CategoryModel::TABLE.'.name AS category_name',
|
||||||
|
SwimlaneModel::TABLE.'.name AS swimlane_name',
|
||||||
|
ProjectModel::TABLE.'.name AS project_name',
|
||||||
|
ColumnModel::TABLE.'.title AS column_title',
|
||||||
|
UserModel::TABLE.'.username AS assignee_username',
|
||||||
|
UserModel::TABLE.'.name AS assignee_name',
|
||||||
|
'uc.username AS creator_username',
|
||||||
|
'uc.name AS creator_name',
|
||||||
|
CategoryModel::TABLE.'.description AS category_description',
|
||||||
|
ColumnModel::TABLE.'.position AS column_position'
|
||||||
|
)
|
||||||
|
->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
|
||||||
|
->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
|
||||||
|
->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
|
||||||
|
->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
|
||||||
|
->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
|
||||||
|
->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE)
|
||||||
|
->eq(TaskModel::TABLE.'.id', $task_id)
|
||||||
|
->findOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get iCal query
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return \PicoDb\Table
|
||||||
|
*/
|
||||||
|
public function getICalQuery()
|
||||||
|
{
|
||||||
|
return $this->db->table(TaskModel::TABLE)
|
||||||
|
->left(UserModel::TABLE, 'ua', 'id', TaskModel::TABLE, 'owner_id')
|
||||||
|
->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
|
||||||
|
->columns(
|
||||||
|
TaskModel::TABLE.'.*',
|
||||||
|
'ua.name AS assignee_name',
|
||||||
|
'ua.username AS assignee_username',
|
||||||
|
'uc.name AS creator_name',
|
||||||
|
'uc.username AS creator_username'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count all tasks for a given project and status
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id Project id
|
||||||
|
* @param array $status List of status id
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
public function countByProjectId($project_id, array $status = array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED))
|
||||||
|
{
|
||||||
|
return $this->db
|
||||||
|
->table(TaskModel::TABLE)
|
||||||
|
->eq('project_id', $project_id)
|
||||||
|
->in('is_active', $status)
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of tasks for a given column and status
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id Project id
|
||||||
|
* @param integer $column_id Column id
|
||||||
|
* @param array $status
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function countByColumnId($project_id, $column_id, array $status = array(TaskModel::STATUS_OPEN))
|
||||||
|
{
|
||||||
|
return $this->db
|
||||||
|
->table(TaskModel::TABLE)
|
||||||
|
->eq('project_id', $project_id)
|
||||||
|
->eq('column_id', $column_id)
|
||||||
|
->in('is_active', $status)
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of tasks for a given column and swimlane
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id Project id
|
||||||
|
* @param integer $column_id Column id
|
||||||
|
* @param integer $swimlane_id Swimlane id
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id)
|
||||||
|
{
|
||||||
|
return $this->db
|
||||||
|
->table(TaskModel::TABLE)
|
||||||
|
->eq('project_id', $project_id)
|
||||||
|
->eq('column_id', $column_id)
|
||||||
|
->eq('swimlane_id', $swimlane_id)
|
||||||
|
->eq('is_active', 1)
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the task exists
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $task_id Task id
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function exists($task_id)
|
||||||
|
{
|
||||||
|
return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get project token
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $task_id
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getProjectToken($task_id)
|
||||||
|
{
|
||||||
|
return $this->db
|
||||||
|
->table(TaskModel::TABLE)
|
||||||
|
->eq(TaskModel::TABLE.'.id', $task_id)
|
||||||
|
->join(ProjectModel::TABLE, 'id', 'project_id')
|
||||||
|
->findOneColumn(ProjectModel::TABLE.'.token');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php if ($is_ajax): ?>
|
||||||
|
<div class="page-header">
|
||||||
|
<h2><?= $this->text->e($title) ?></h2>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= $this->projectHeader->render($project, 'TaskListController', 'show') ?>
|
||||||
|
<?php endif ?>
|
||||||
|
<section class="sidebar-container">
|
||||||
|
<?= $this->render($sidebar_template, array('project' => $project)) ?>
|
||||||
|
|
||||||
|
<div class="sidebar-content">
|
||||||
|
<?= $content_for_sublayout ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KanboardTests\units\Api\Procedure;
|
||||||
|
|
||||||
|
use Kanboard\Api\Procedure\CommentProcedure;
|
||||||
|
use Kanboard\Core\Security\Role;
|
||||||
|
use KanboardTests\units\Base;
|
||||||
|
|
||||||
|
class CommentProcedureTest extends Base
|
||||||
|
{
|
||||||
|
public function testCommentsDoNotExposeTheAuthorEmail()
|
||||||
|
{
|
||||||
|
$this->assertEquals(2, $this->container['userModel']->create(array('username' => 'owner', 'email' => 'owner@example.org')));
|
||||||
|
$this->assertEquals(3, $this->container['userModel']->create(array('username' => 'peer')));
|
||||||
|
$this->assertEquals(1, $this->container['projectModel']->create(array('name' => 'P1')));
|
||||||
|
$this->assertTrue($this->container['projectUserRoleModel']->addUser(1, 2, Role::PROJECT_MANAGER));
|
||||||
|
$this->assertTrue($this->container['projectUserRoleModel']->addUser(1, 3, Role::PROJECT_MEMBER));
|
||||||
|
$this->assertEquals(1, $this->container['taskCreationModel']->create(array('title' => 'Task', 'project_id' => 1)));
|
||||||
|
$this->assertEquals(1, $this->container['commentModel']->create(array('task_id' => 1, 'user_id' => 2, 'comment' => 'hello')));
|
||||||
|
|
||||||
|
$this->container['userSession']->initialize($this->container['userModel']->getById(3));
|
||||||
|
$procedure = new CommentProcedure($this->container);
|
||||||
|
|
||||||
|
$comment = $procedure->getComment(1);
|
||||||
|
$this->assertEquals('owner', $comment['username']);
|
||||||
|
$this->assertArrayNotHasKey('email', $comment);
|
||||||
|
|
||||||
|
$comments = $procedure->getAllComments(1);
|
||||||
|
$this->assertCount(1, $comments);
|
||||||
|
$this->assertEquals('owner', $comments[0]['username']);
|
||||||
|
$this->assertArrayNotHasKey('email', $comments[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KanboardTests\units\Api\Procedure;
|
||||||
|
|
||||||
|
use Kanboard\Api\Procedure\MeProcedure;
|
||||||
|
use Kanboard\Api\Procedure\ProjectProcedure;
|
||||||
|
use Kanboard\Core\Security\Role;
|
||||||
|
use Kanboard\EventBuilder\CommentEventBuilder;
|
||||||
|
use Kanboard\Model\CommentModel;
|
||||||
|
use KanboardTests\units\Base;
|
||||||
|
|
||||||
|
class ProjectActivityProcedureTest extends Base
|
||||||
|
{
|
||||||
|
public function testProjectActivityDoesNotExposeTheAuthorEmail()
|
||||||
|
{
|
||||||
|
$this->createCommentEvent();
|
||||||
|
$procedure = new ProjectProcedure($this->container);
|
||||||
|
|
||||||
|
$this->assertEventsWithoutEmail($procedure->getProjectActivity(1));
|
||||||
|
$this->assertEventsWithoutEmail($procedure->getProjectActivities(array(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMyActivityStreamDoesNotExposeTheAuthorEmail()
|
||||||
|
{
|
||||||
|
$this->createCommentEvent();
|
||||||
|
$procedure = new MeProcedure($this->container);
|
||||||
|
|
||||||
|
$this->assertEventsWithoutEmail($procedure->getMyActivityStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertEventsWithoutEmail(array $events)
|
||||||
|
{
|
||||||
|
$this->assertCount(1, $events);
|
||||||
|
$this->assertEquals('owner', $events[0]['author_username']);
|
||||||
|
$this->assertEquals('hello', $events[0]['comment']['comment']);
|
||||||
|
$this->assertStringNotContainsString('owner@example.org', json_encode($events));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createCommentEvent()
|
||||||
|
{
|
||||||
|
$this->assertEquals(2, $this->container['userModel']->create(array('username' => 'owner', 'email' => 'owner@example.org')));
|
||||||
|
$this->assertEquals(3, $this->container['userModel']->create(array('username' => 'peer')));
|
||||||
|
$this->assertEquals(1, $this->container['projectModel']->create(array('name' => 'P1')));
|
||||||
|
$this->assertTrue($this->container['projectUserRoleModel']->addUser(1, 2, Role::PROJECT_MANAGER));
|
||||||
|
$this->assertTrue($this->container['projectUserRoleModel']->addUser(1, 3, Role::PROJECT_MEMBER));
|
||||||
|
$this->assertEquals(1, $this->container['taskCreationModel']->create(array('title' => 'Task', 'project_id' => 1)));
|
||||||
|
$this->assertEquals(1, $this->container['commentModel']->create(array('task_id' => 1, 'user_id' => 2, 'comment' => 'hello')));
|
||||||
|
|
||||||
|
$event = (new CommentEventBuilder($this->container))->withCommentId(1)->buildEvent();
|
||||||
|
$this->assertNotFalse($this->container['projectActivityModel']->createEvent(1, 1, 2, CommentModel::EVENT_CREATE, $event->getAll()));
|
||||||
|
|
||||||
|
$this->container['userSession']->initialize($this->container['userModel']->getById(3));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KanboardTests\units\Controller;
|
||||||
|
|
||||||
|
use Kanboard\Controller\CurrencyController;
|
||||||
|
use Kanboard\Core\Http\Request;
|
||||||
|
use Kanboard\Core\Http\Response;
|
||||||
|
use KanboardTests\units\Base;
|
||||||
|
|
||||||
|
class CurrencyControllerTest extends Base
|
||||||
|
{
|
||||||
|
public function testChangingTheCurrencyDoesNotSaveOtherSettings()
|
||||||
|
{
|
||||||
|
$this->container['request'] = new Request(
|
||||||
|
$this->container,
|
||||||
|
array('REQUEST_METHOD' => 'POST'),
|
||||||
|
array(),
|
||||||
|
array(
|
||||||
|
'csrf_token' => $this->container['token']->getCSRFToken(),
|
||||||
|
'application_currency' => 'EUR',
|
||||||
|
'calendar_project_tasks' => '1 OR 1=1 OR 1',
|
||||||
|
),
|
||||||
|
array(),
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->container['response'] = $this->getMockBuilder(Response::class)
|
||||||
|
->setConstructorArgs(array($this->container))
|
||||||
|
->onlyMethods(array('redirect'))
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$this->container['response']
|
||||||
|
->expects($this->once())
|
||||||
|
->method('redirect');
|
||||||
|
|
||||||
|
(new CurrencyController($this->container))->update();
|
||||||
|
|
||||||
|
$this->assertEquals('EUR', $this->container['configModel']->getOption('application_currency'));
|
||||||
|
$this->assertEquals('date_started', $this->container['configModel']->getOption('calendar_project_tasks'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KanboardTests\units\Controller;
|
||||||
|
|
||||||
|
use Kanboard\Controller\ICalendarController;
|
||||||
|
use Kanboard\Core\Http\Request;
|
||||||
|
use Kanboard\Core\Http\Response;
|
||||||
|
use KanboardTests\units\Base;
|
||||||
|
|
||||||
|
class ICalendarControllerTest extends Base
|
||||||
|
{
|
||||||
|
private $calendar = '';
|
||||||
|
|
||||||
|
public function testProjectCalendarIgnoresUnknownStartColumn()
|
||||||
|
{
|
||||||
|
$this->container['configModel']->save(array('calendar_project_tasks' => '1 OR 1=1 OR 1'));
|
||||||
|
$this->createFixtures();
|
||||||
|
$project = $this->container['projectModel']->getById(1);
|
||||||
|
|
||||||
|
$this->renderCalendar(array('token' => $project['token']))->project();
|
||||||
|
|
||||||
|
$this->assertStringNotContainsString('Private task', $this->calendar);
|
||||||
|
$this->assertStringContainsString('Public task', $this->calendar);
|
||||||
|
$this->assertStringContainsString('task-#1-date_started-date_due', $this->calendar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUserCalendarIgnoresUnknownStartColumn()
|
||||||
|
{
|
||||||
|
$this->container['configModel']->save(array('calendar_user_tasks' => '1 OR 1=1 OR 1'));
|
||||||
|
$this->createFixtures();
|
||||||
|
$user = $this->container['userModel']->getById(2);
|
||||||
|
|
||||||
|
$this->renderCalendar(array('token' => $user['token']))->user();
|
||||||
|
|
||||||
|
$this->assertStringNotContainsString('Private task', $this->calendar);
|
||||||
|
$this->assertStringContainsString('Public task', $this->calendar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProjectCalendarUsesCreationDateWhenConfigured()
|
||||||
|
{
|
||||||
|
$this->container['configModel']->save(array('calendar_project_tasks' => 'date_creation'));
|
||||||
|
$this->createFixtures();
|
||||||
|
$project = $this->container['projectModel']->getById(1);
|
||||||
|
|
||||||
|
$this->renderCalendar(array('token' => $project['token']))->project();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('task-#1-date_creation-date_due', $this->calendar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createFixtures()
|
||||||
|
{
|
||||||
|
$this->assertEquals(2, $this->container['userModel']->create(array('username' => 'user1')));
|
||||||
|
$this->assertTrue($this->container['userModel']->enablePublicAccess(2));
|
||||||
|
|
||||||
|
$this->assertEquals(1, $this->container['projectModel']->create(array('name' => 'Public project')));
|
||||||
|
$this->assertTrue($this->container['projectModel']->enablePublicAccess(1));
|
||||||
|
$this->assertEquals(2, $this->container['projectModel']->create(array('name' => 'Private project')));
|
||||||
|
|
||||||
|
$this->assertEquals(1, $this->createTask(1, 'Public task', 2));
|
||||||
|
$this->assertEquals(2, $this->createTask(2, 'Private task', 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createTask($projectId, $title, $ownerId)
|
||||||
|
{
|
||||||
|
return $this->container['taskCreationModel']->create(array(
|
||||||
|
'project_id' => $projectId,
|
||||||
|
'title' => $title,
|
||||||
|
'owner_id' => $ownerId,
|
||||||
|
'date_started' => strtotime('-1 day'),
|
||||||
|
'date_due' => strtotime('+1 day'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderCalendar(array $params)
|
||||||
|
{
|
||||||
|
$this->container['request'] = new Request($this->container, array('REQUEST_METHOD' => 'GET'), $params);
|
||||||
|
|
||||||
|
$this->container['response'] = $this->getMockBuilder(Response::class)
|
||||||
|
->setConstructorArgs(array($this->container))
|
||||||
|
->onlyMethods(array('ical'))
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$this->container['response']
|
||||||
|
->expects($this->once())
|
||||||
|
->method('ical')
|
||||||
|
->willReturnCallback(function ($calendar) {
|
||||||
|
$this->calendar = $calendar;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ICalendarController($this->container);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KanboardTests\units\Core\Http;
|
||||||
|
|
||||||
|
use KanboardTests\units\Base;
|
||||||
|
use Kanboard\Core\Http\Client;
|
||||||
|
|
||||||
|
class ClientTest extends Base
|
||||||
|
{
|
||||||
|
public function testGetCanDisableRedirects()
|
||||||
|
{
|
||||||
|
$client = $this
|
||||||
|
->getMockBuilder('\Kanboard\Core\Http\Client')
|
||||||
|
->setConstructorArgs(array($this->container))
|
||||||
|
->onlyMethods(array('doRequest'))
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$client
|
||||||
|
->expects($this->once())
|
||||||
|
->method('doRequest')
|
||||||
|
->with('GET', 'https://example.org', '', array('X-Test: 1'), true, false)
|
||||||
|
->willReturn('response-body');
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'response-body',
|
||||||
|
$client->get('https://example.org', array('X-Test: 1'), true, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateIpAddressWithPrivateIPv4()
|
||||||
|
{
|
||||||
|
$client = new Client($this->container);
|
||||||
|
|
||||||
|
$this->assertTrue($client->isPrivateIpAddress('10.0.0.1'));
|
||||||
|
$this->assertTrue($client->isPrivateIpAddress('172.16.5.10'));
|
||||||
|
$this->assertTrue($client->isPrivateIpAddress('192.168.1.20'));
|
||||||
|
$this->assertTrue($client->isPrivateIpAddress('127.0.0.1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateIpAddressWithPrivateIPv6()
|
||||||
|
{
|
||||||
|
$client = new Client($this->container);
|
||||||
|
|
||||||
|
$this->assertTrue($client->isPrivateIpAddress('::1'));
|
||||||
|
$this->assertTrue($client->isPrivateIpAddress('fd12:3456:789a:1::1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateIpAddressWithIPv4MappedIPv6()
|
||||||
|
{
|
||||||
|
$client = new Client($this->container);
|
||||||
|
|
||||||
|
$this->assertTrue($client->isPrivateIpAddress('::ffff:127.0.0.1'));
|
||||||
|
$this->assertTrue($client->isPrivateIpAddress('::ffff:7f00:1'));
|
||||||
|
$this->assertTrue($client->isPrivateIpAddress('::ffff:169.254.169.254'));
|
||||||
|
$this->assertTrue($client->isPrivateIpAddress('::ffff:10.0.0.1'));
|
||||||
|
$this->assertFalse($client->isPrivateIpAddress('::ffff:8.8.8.8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateIpAddressWithPublicAndInvalidValues()
|
||||||
|
{
|
||||||
|
$client = new Client($this->container);
|
||||||
|
|
||||||
|
$this->assertFalse($client->isPrivateIpAddress('8.8.8.8'));
|
||||||
|
$this->assertFalse($client->isPrivateIpAddress('2607:f8b0:4005:805::200e'));
|
||||||
|
$this->assertFalse($client->isPrivateIpAddress('not-an-ip'));
|
||||||
|
$this->assertFalse($client->isPrivateIpAddress(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateUrlWithPrivateHostnames()
|
||||||
|
{
|
||||||
|
$client = new Client($this->container);
|
||||||
|
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://localhost'));
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://127.0.0.1/path'));
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://10.0.0.15/api'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateUrlWithBracketedIPv6Literals()
|
||||||
|
{
|
||||||
|
$client = new Client($this->container);
|
||||||
|
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://[::1]/'));
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://[::ffff:127.0.0.1]:9099/'));
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://[0:0:0:0:0:ffff:127.0.0.1]/'));
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://[::ffff:169.254.169.254]/'));
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://[fd00::1]/'));
|
||||||
|
$this->assertFalse($client->isPrivateURL('http://[2607:f8b0:4005:805::200e]/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateUrlWithOtherIPv4Notations()
|
||||||
|
{
|
||||||
|
$client = new Client($this->container);
|
||||||
|
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://2130706433:9000/'));
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://0x7f000001/'));
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://0177.0.0.1/'));
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://2852039166/'));
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://127.0.0.1./'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateUrlWithUnresolvableHost()
|
||||||
|
{
|
||||||
|
$client = new Client($this->container);
|
||||||
|
|
||||||
|
$this->assertTrue($client->isPrivateURL('http://kanboard.invalid/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateUrlWithPublicAddresses()
|
||||||
|
{
|
||||||
|
$client = new Client($this->container);
|
||||||
|
|
||||||
|
$this->assertFalse($client->isPrivateURL('http://8.8.8.8/data'));
|
||||||
|
$this->assertFalse($client->isPrivateURL('https://ipv6.google.com/test'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateUrlWithUnsupportedScheme()
|
||||||
|
{
|
||||||
|
$client = new Client($this->container);
|
||||||
|
|
||||||
|
$this->assertFalse($client->isPrivateURL('ftp://127.0.0.1'));
|
||||||
|
$this->assertFalse($client->isPrivateURL('mailto:test@example.com'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KanboardTests\units\Core;
|
||||||
|
|
||||||
|
use KanboardTests\units\Base;
|
||||||
|
use Kanboard\Core\Http\Request;
|
||||||
|
use Kanboard\Core\Security\Role;
|
||||||
|
use Kanboard\Core\Template;
|
||||||
|
|
||||||
|
class TemplateTest extends Base
|
||||||
|
{
|
||||||
|
public function testGetTemplateFile()
|
||||||
|
{
|
||||||
|
$template = new Template($this->container['helper']);
|
||||||
|
|
||||||
|
$this->assertStringEndsWith(
|
||||||
|
implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', 'Template', 'a', 'b.php')),
|
||||||
|
$template->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertStringEndsWith(
|
||||||
|
implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', 'Template', 'a', 'b.php')),
|
||||||
|
$template->getTemplateFile('kanboard:a'.DIRECTORY_SEPARATOR.'b')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetPluginTemplateFile()
|
||||||
|
{
|
||||||
|
$template = new Template($this->container['helper']);
|
||||||
|
$this->assertStringEndsWith(
|
||||||
|
implode(DIRECTORY_SEPARATOR, array(PLUGINS_DIR, 'Myplugin', 'Template', 'a', 'b.php')),
|
||||||
|
$template->getTemplateFile('myplugin:a'.DIRECTORY_SEPARATOR.'b')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetOverridedTemplateFile()
|
||||||
|
{
|
||||||
|
$template = new Template($this->container['helper']);
|
||||||
|
$template->setTemplateOverride('a'.DIRECTORY_SEPARATOR.'b', 'myplugin:c');
|
||||||
|
|
||||||
|
$this->assertStringEndsWith(
|
||||||
|
implode(DIRECTORY_SEPARATOR, array(PLUGINS_DIR, 'Myplugin', 'Template', 'c.php')),
|
||||||
|
$template->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertStringEndsWith(
|
||||||
|
implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', 'Template', 'd.php')),
|
||||||
|
$template->getTemplateFile('d')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCommentReplyEscapesTemplateClosingTags()
|
||||||
|
{
|
||||||
|
$_SESSION['user'] = array(
|
||||||
|
'id' => 2,
|
||||||
|
'role' => Role::APP_USER,
|
||||||
|
);
|
||||||
|
|
||||||
|
$html = $this->container['template']->render('comment/show', array(
|
||||||
|
'comment' => array(
|
||||||
|
'id' => 1,
|
||||||
|
'user_id' => 1,
|
||||||
|
'username' => 'alice',
|
||||||
|
'name' => 'Alice',
|
||||||
|
'email' => '',
|
||||||
|
'avatar_path' => '',
|
||||||
|
'date_creation' => 0,
|
||||||
|
'date_modification' => 0,
|
||||||
|
'visibility' => Role::APP_USER,
|
||||||
|
'comment' => '</textarea></template><base href="http://127.0.0.1:8899/">',
|
||||||
|
),
|
||||||
|
'task' => array('id' => 1),
|
||||||
|
'editable' => false,
|
||||||
|
'hide_actions' => true,
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->assertStringNotContainsString(
|
||||||
|
'<base href="http://127.0.0.1:8899/">',
|
||||||
|
$html
|
||||||
|
);
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'</textarea></template><base href="http://127.0.0.1:8899/">',
|
||||||
|
$html
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnalyticAjaxLayoutEscapesProjectNameInTitle()
|
||||||
|
{
|
||||||
|
$this->container['request'] = new Request($this->container, array(
|
||||||
|
'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
|
||||||
|
));
|
||||||
|
|
||||||
|
$html = $this->container['helper']->layout->analytic('analytic/task_distribution', array(
|
||||||
|
'project' => array('id' => 1, 'name' => '<img src=x onerror=alert(document.domain)>'),
|
||||||
|
'metrics' => array(),
|
||||||
|
'title' => 'Task distribution',
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->assertStringNotContainsString(
|
||||||
|
'<img src=x onerror=alert(document.domain)>',
|
||||||
|
$html
|
||||||
|
);
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'<h2><img src=x onerror=alert(document.domain)> > Task distribution</h2>',
|
||||||
|
$html
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KanboardTests\units\Formatter;
|
||||||
|
|
||||||
|
use Eluceo\iCal\Component\Calendar;
|
||||||
|
use Kanboard\Formatter\TaskICalFormatter;
|
||||||
|
use KanboardTests\units\Base;
|
||||||
|
|
||||||
|
class TaskICalFormatterTest extends Base
|
||||||
|
{
|
||||||
|
public function testAttendeeAndOrganizerDoNotExposeEmailAddresses()
|
||||||
|
{
|
||||||
|
$this->assertEquals(2, $this->container['userModel']->create(array('username' => 'creator', 'name' => 'Task Creator', 'email' => 'creator@example.org')));
|
||||||
|
$this->assertEquals(3, $this->container['userModel']->create(array('username' => 'assignee', 'name' => 'Task Assignee', 'email' => 'assignee@example.org')));
|
||||||
|
$this->assertEquals(1, $this->container['projectModel']->create(array('name' => 'P1')));
|
||||||
|
$this->assertEquals(1, $this->container['taskCreationModel']->create(array(
|
||||||
|
'project_id' => 1,
|
||||||
|
'title' => 'Task',
|
||||||
|
'creator_id' => 2,
|
||||||
|
'owner_id' => 3,
|
||||||
|
'date_due' => strtotime('+1 day'),
|
||||||
|
)));
|
||||||
|
|
||||||
|
$calendar = TaskICalFormatter::getInstance($this->container)
|
||||||
|
->setCalendar(new Calendar('Kanboard'))
|
||||||
|
->addTasksWithDueDateOnly($this->container['taskFinderModel']->getICalQuery())
|
||||||
|
->format();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('ATTENDEE;CN=Task Assignee:MAILTO:assignee@kanboard.local', $calendar);
|
||||||
|
$this->assertStringContainsString('ORGANIZER;CN=Task Creator:MAILTO:creator@kanboard.local', $calendar);
|
||||||
|
$this->assertStringNotContainsString('@example.org', $calendar);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user