summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/Controller/Base.php11
-rw-r--r--app/Controller/BoardTooltip.php14
-rw-r--r--app/Controller/Comment.php10
-rw-r--r--app/Controller/TaskExternalLink.php185
-rw-r--r--app/Controller/Tasklink.php37
-rw-r--r--app/Controller/Taskmodification.php7
-rw-r--r--app/Core/Base.php3
-rw-r--r--app/Core/ExternalLink/ExternalLinkInterface.php36
-rw-r--r--app/Core/ExternalLink/ExternalLinkManager.php171
-rw-r--r--app/Core/ExternalLink/ExternalLinkProviderInterface.php71
-rw-r--r--app/Core/ExternalLink/ExternalLinkProviderNotFound.php15
-rw-r--r--app/Core/Http/Client.php13
-rw-r--r--app/Core/Http/Response.php5
-rw-r--r--app/ExternalLink/AttachmentLink.php26
-rw-r--r--app/ExternalLink/AttachmentLinkProvider.php117
-rw-r--r--app/ExternalLink/BaseLink.php44
-rw-r--r--app/ExternalLink/BaseLinkProvider.php33
-rw-r--r--app/ExternalLink/WebLink.php37
-rw-r--r--app/ExternalLink/WebLinkProvider.php77
-rw-r--r--app/Locale/bs_BA/translations.php26
-rw-r--r--app/Locale/cs_CZ/translations.php26
-rw-r--r--app/Locale/da_DK/translations.php26
-rw-r--r--app/Locale/de_DE/translations.php26
-rw-r--r--app/Locale/el_GR/translations.php26
-rw-r--r--app/Locale/es_ES/translations.php26
-rw-r--r--app/Locale/fi_FI/translations.php26
-rw-r--r--app/Locale/fr_FR/translations.php26
-rw-r--r--app/Locale/hu_HU/translations.php26
-rw-r--r--app/Locale/id_ID/translations.php26
-rw-r--r--app/Locale/it_IT/translations.php26
-rw-r--r--app/Locale/ja_JP/translations.php26
-rw-r--r--app/Locale/my_MY/translations.php26
-rw-r--r--app/Locale/nb_NO/translations.php26
-rw-r--r--app/Locale/nl_NL/translations.php26
-rw-r--r--app/Locale/pl_PL/translations.php26
-rw-r--r--app/Locale/pt_BR/translations.php26
-rw-r--r--app/Locale/pt_PT/translations.php26
-rw-r--r--app/Locale/ru_RU/translations.php26
-rw-r--r--app/Locale/sr_Latn_RS/translations.php26
-rw-r--r--app/Locale/sv_SE/translations.php26
-rw-r--r--app/Locale/th_TH/translations.php26
-rw-r--r--app/Locale/tr_TR/translations.php26
-rw-r--r--app/Locale/zh_CN/translations.php26
-rw-r--r--app/Model/TaskExternalLink.php99
-rw-r--r--app/Model/TaskFinder.php11
-rw-r--r--app/Model/User.php2
-rw-r--r--app/Schema/Mysql.php20
-rw-r--r--app/Schema/Postgres.php20
-rw-r--r--app/Schema/Sqlite.php20
-rw-r--r--app/ServiceProvider/AuthenticationProvider.php3
-rw-r--r--app/ServiceProvider/ClassProvider.php2
-rw-r--r--app/ServiceProvider/ExternalLinkProvider.php34
-rw-r--r--app/ServiceProvider/RouteProvider.php7
-rw-r--r--app/Template/board/task_footer.php6
-rw-r--r--app/Template/board/task_menu.php1
-rw-r--r--app/Template/board/tooltip_external_links.php20
-rw-r--r--app/Template/comment/create.php4
-rw-r--r--app/Template/task/comments.php3
-rw-r--r--app/Template/task/show.php17
-rw-r--r--app/Template/task/sidebar.php22
-rw-r--r--app/Template/task_creation/form.php2
-rw-r--r--app/Template/task_external_link/create.php13
-rw-r--r--app/Template/task_external_link/edit.php13
-rw-r--r--app/Template/task_external_link/find.php32
-rw-r--r--app/Template/task_external_link/form.php13
-rw-r--r--app/Template/task_external_link/remove.php15
-rw-r--r--app/Template/task_external_link/show.php50
-rw-r--r--app/Template/tasklink/create.php4
-rw-r--r--app/Template/tasklink/show.php12
-rw-r--r--app/Validator/ExternalLinkValidator.php76
-rw-r--r--app/common.php1
71 files changed, 1991 insertions, 67 deletions
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index efeab31e..fb64bcf3 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -189,9 +189,15 @@ abstract class Base extends \Kanboard\Core\Base
*/
protected function taskLayout($template, array $params)
{
+ $params['ajax'] = $this->request->isAjax() || $this->request->getIntegerParam('ajax') === 1;
$content = $this->template->render($template, $params);
- $params['task_content_for_layout'] = $content;
+
+ if ($params['ajax']) {
+ return $content;
+ }
+
$params['title'] = $params['task']['project_name'].' > '.$params['task']['title'];
+ $params['task_content_for_layout'] = $content;
$params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
return $this->template->layout('task/layout', $params);
@@ -319,7 +325,8 @@ abstract class Base extends \Kanboard\Core\Base
* @param array &$project
* @return string
*/
- protected function getProjectDescription(array &$project) {
+ protected function getProjectDescription(array &$project)
+ {
if ($project['owner_id'] > 0) {
$description = t('Project owner: ').'**'.$this->template->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL;
diff --git a/app/Controller/BoardTooltip.php b/app/Controller/BoardTooltip.php
index bcf7de81..06f4d729 100644
--- a/app/Controller/BoardTooltip.php
+++ b/app/Controller/BoardTooltip.php
@@ -25,6 +25,20 @@ class BoardTooltip extends Base
}
/**
+ * Get links on mouseover
+ *
+ * @access public
+ */
+ public function externallinks()
+ {
+ $task = $this->getTask();
+ $this->response->html($this->template->render('board/tooltip_external_links', array(
+ 'links' => $this->taskExternalLink->getAll($task['id']),
+ 'task' => $task,
+ )));
+ }
+
+ /**
* Get subtasks on mouseover
*
* @access public
diff --git a/app/Controller/Comment.php b/app/Controller/Comment.php
index a608dd1c..c77a4712 100644
--- a/app/Controller/Comment.php
+++ b/app/Controller/Comment.php
@@ -41,7 +41,6 @@ class Comment extends Base
public function create(array $values = array(), array $errors = array())
{
$task = $this->getTask();
- $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
if (empty($values)) {
$values = array(
@@ -50,15 +49,6 @@ class Comment extends Base
);
}
- if ($ajax) {
- $this->response->html($this->template->render('comment/create', array(
- 'values' => $values,
- 'errors' => $errors,
- 'task' => $task,
- 'ajax' => $ajax,
- )));
- }
-
$this->response->html($this->taskLayout('comment/create', array(
'values' => $values,
'errors' => $errors,
diff --git a/app/Controller/TaskExternalLink.php b/app/Controller/TaskExternalLink.php
new file mode 100644
index 00000000..3209751b
--- /dev/null
+++ b/app/Controller/TaskExternalLink.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Kanboard\Core\ExternalLink\ExternalLinkProviderNotFound;
+
+/**
+ * Task External Link Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class TaskExternalLink extends Base
+{
+ /**
+ * Creation form
+ *
+ * @access public
+ */
+ public function show()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->taskLayout('task_external_link/show', array(
+ 'links' => $this->taskExternalLink->getAll($task['id']),
+ 'task' => $task,
+ 'title' => t('List of external links'),
+ )));
+ }
+
+ /**
+ * First creation form
+ *
+ * @access public
+ */
+ public function find(array $values = array(), array $errors = array())
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->taskLayout('task_external_link/find', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'types' => $this->externalLinkManager->getTypes(),
+ )));
+ }
+
+ /**
+ * Second creation form
+ *
+ * @access public
+ */
+ public function create()
+ {
+ try {
+
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+
+ $provider = $this->externalLinkManager->setUserInput($values)->find();
+ $link = $provider->getLink();
+
+ $this->response->html($this->taskLayout('task_external_link/create', array(
+ 'values' => array(
+ 'title' => $link->getTitle(),
+ 'url' => $link->getUrl(),
+ 'link_type' => $provider->getType(),
+ ),
+ 'dependencies' => $provider->getDependencies(),
+ 'errors' => array(),
+ 'task' => $task,
+ )));
+
+ } catch (ExternalLinkProviderNotFound $e) {
+ $errors = array('text' => array(t('Unable to fetch link information.')));
+ $this->find($values, $errors);
+ }
+ }
+
+ /**
+ * Save link
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->externalLinkValidator->validateCreation($values);
+
+ if ($valid && $this->taskExternalLink->create($values)) {
+ $this->flash->success(t('Link added successfully.'));
+ return $this->response->redirect($this->helper->url->to('TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true);
+ }
+
+ $this->edit($values, $errors);
+ }
+
+ /**
+ * Edit form
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $task = $this->getTask();
+ $link_id = $this->request->getIntegerParam('link_id');
+
+ if ($link_id > 0) {
+ $values = $this->taskExternalLink->getById($link_id);
+ }
+
+ if (empty($values)) {
+ return $this->notfound();
+ }
+
+ $provider = $this->externalLinkManager->getProvider($values['link_type']);
+
+ $this->response->html($this->taskLayout('task_external_link/edit', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'dependencies' => $provider->getDependencies(),
+ )));
+ }
+
+ /**
+ * Update link
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->externalLinkValidator->validateModification($values);
+
+ if ($valid && $this->taskExternalLink->update($values)) {
+ $this->flash->success(t('Link updated successfully.'));
+ return $this->response->redirect($this->helper->url->to('TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
+ }
+
+ $this->edit($values, $errors);
+ }
+
+ /**
+ * Confirmation dialog before removing a link
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $task = $this->getTask();
+ $link_id = $this->request->getIntegerParam('link_id');
+ $link = $this->taskExternalLink->getById($link_id);
+
+ if (empty($link)) {
+ return $this->notfound();
+ }
+
+ $this->response->html($this->taskLayout('task_external_link/remove', array(
+ 'link' => $link,
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $task = $this->getTask();
+
+ if ($this->taskExternalLink->remove($this->request->getIntegerParam('link_id'))) {
+ $this->flash->success(t('Link removed successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to remove this link.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
+ }
+}
diff --git a/app/Controller/Tasklink.php b/app/Controller/Tasklink.php
index a81d3ee5..4338e7bf 100644
--- a/app/Controller/Tasklink.php
+++ b/app/Controller/Tasklink.php
@@ -29,6 +29,25 @@ class Tasklink extends Base
}
/**
+ * Show links
+ *
+ * @access public
+ */
+ public function show()
+ {
+ $task = $this->getTask();
+ $project = $this->project->getById($task['project_id']);
+
+ $this->response->html($this->taskLayout('tasklink/show', array(
+ 'links' => $this->taskLink->getAllGroupedByLabel($task['id']),
+ 'task' => $task,
+ 'project' => $project,
+ 'editable' => true,
+ 'is_public' => false,
+ )));
+ }
+
+ /**
* Creation form
*
* @access public
@@ -36,18 +55,6 @@ class Tasklink extends Base
public function create(array $values = array(), array $errors = array())
{
$task = $this->getTask();
- $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
-
- if ($ajax && empty($errors)) {
- $this->response->html($this->template->render('tasklink/create', array(
- 'values' => $values,
- 'errors' => $errors,
- 'task' => $task,
- 'labels' => $this->link->getList(0, false),
- 'title' => t('Add a new link'),
- 'ajax' => $ajax,
- )));
- }
$this->response->html($this->taskLayout('tasklink/create', array(
'values' => $values,
@@ -76,10 +83,10 @@ class Tasklink extends Base
$this->flash->success(t('Link added successfully.'));
if ($ajax) {
- $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
+ return $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id'])));
}
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links');
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links');
}
$errors = array('title' => array(t('The exact same link already exists')));
@@ -130,7 +137,7 @@ class Tasklink extends Base
if ($valid) {
if ($this->taskLink->update($values['id'], $values['task_id'], $values['opposite_task_id'], $values['link_id'])) {
$this->flash->success(t('Link updated successfully.'));
- $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links');
+ return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links');
}
$this->flash->failure(t('Unable to update your link.'));
diff --git a/app/Controller/Taskmodification.php b/app/Controller/Taskmodification.php
index 2c97970b..1fcc416b 100644
--- a/app/Controller/Taskmodification.php
+++ b/app/Controller/Taskmodification.php
@@ -80,14 +80,9 @@ class Taskmodification extends Base
'values' => $values,
'errors' => $errors,
'task' => $task,
- 'ajax' => $ajax,
);
- if ($ajax) {
- $this->response->html($this->template->render('task_modification/edit_description', $params));
- } else {
- $this->response->html($this->taskLayout('task_modification/edit_description', $params));
- }
+ $this->response->html($this->taskLayout('task_modification/edit_description', $params));
}
/**
diff --git a/app/Core/Base.php b/app/Core/Base.php
index 2821e5ae..ab99fcea 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -16,6 +16,7 @@ use Pimple\Container;
* @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic
* @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic
* @property \Kanboard\Core\Action\ActionManager $actionManager
+ * @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager
* @property \Kanboard\Core\Cache\MemoryCache $memoryCache
* @property \Kanboard\Core\Event\EventManager $eventManager
* @property \Kanboard\Core\Group\GroupManager $groupManager
@@ -97,6 +98,7 @@ use Pimple\Container;
* @property \Kanboard\Model\TaskCreation $taskCreation
* @property \Kanboard\Model\TaskDuplication $taskDuplication
* @property \Kanboard\Model\TaskExport $taskExport
+ * @property \Kanboard\Model\TaskExternalLink $taskExternalLink
* @property \Kanboard\Model\TaskImport $taskImport
* @property \Kanboard\Model\TaskFinder $taskFinder
* @property \Kanboard\Model\TaskFilter $taskFilter
@@ -132,6 +134,7 @@ use Pimple\Container;
* @property \Kanboard\Validator\SubtaskValidator $subtaskValidator
* @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator
* @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator
+ * @property \Kanboard\Validator\TaskExternalLinkValidator $taskExternalLinkValidator
* @property \Kanboard\Validator\TaskValidator $taskValidator
* @property \Kanboard\Validator\UserValidator $userValidator
* @property \Psr\Log\LoggerInterface $logger
diff --git a/app/Core/ExternalLink/ExternalLinkInterface.php b/app/Core/ExternalLink/ExternalLinkInterface.php
new file mode 100644
index 00000000..2dbc0a19
--- /dev/null
+++ b/app/Core/ExternalLink/ExternalLinkInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Kanboard\Core\ExternalLink;
+
+/**
+ * External Link Interface
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+interface ExternalLinkInterface
+{
+ /**
+ * Get link title
+ *
+ * @access public
+ * @return string
+ */
+ public function getTitle();
+
+ /**
+ * Get link URL
+ *
+ * @access public
+ * @return string
+ */
+ public function getUrl();
+
+ /**
+ * Set link URL
+ *
+ * @access public
+ * @param string $url
+ */
+ public function setUrl($url);
+}
diff --git a/app/Core/ExternalLink/ExternalLinkManager.php b/app/Core/ExternalLink/ExternalLinkManager.php
new file mode 100644
index 00000000..cd3476ca
--- /dev/null
+++ b/app/Core/ExternalLink/ExternalLinkManager.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace Kanboard\Core\ExternalLink;
+
+use Kanboard\Core\Base;
+
+/**
+ * External Link Manager
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class ExternalLinkManager extends Base
+{
+ /**
+ * Automatic type value
+ *
+ * @var string
+ */
+ const TYPE_AUTO = 'auto';
+
+ /**
+ * Registered providers
+ *
+ * @access private
+ * @var array
+ */
+ private $providers = array();
+
+ /**
+ * Type chosen by the user
+ *
+ * @access private
+ * @var string
+ */
+ private $userInputType = '';
+
+ /**
+ * Text entered by the user
+ *
+ * @access private
+ * @var string
+ */
+ private $userInputText = '';
+
+ /**
+ * Register a new provider
+ *
+ * Providers are registered in a LIFO queue
+ *
+ * @access public
+ * @param ExternalLinkProviderInterface $provider
+ * @return ExternalLinkManager
+ */
+ public function register(ExternalLinkProviderInterface $provider)
+ {
+ array_unshift($this->providers, $provider);
+ return $this;
+ }
+
+ /**
+ * Get provider
+ *
+ * @access public
+ * @param string $type
+ * @throws ExternalLinkProviderNotFound
+ * @return ExternalLinkProviderInterface
+ */
+ public function getProvider($type)
+ {
+ foreach ($this->providers as $provider) {
+ if ($provider->getType() === $type) {
+ return $provider;
+ }
+ }
+
+ throw new ExternalLinkProviderNotFound('Unable to find link provider: '.$type);
+ }
+
+ /**
+ * Get link types
+ *
+ * @access public
+ * @return array
+ */
+ public function getTypes()
+ {
+ $types = array();
+
+ foreach ($this->providers as $provider) {
+ $types[$provider->getType()] = $provider->getName();
+ }
+
+ asort($types);
+
+ return array(self::TYPE_AUTO => t('Auto')) + $types;
+ }
+
+ /**
+ * Get dependency label from a provider
+ *
+ * @access public
+ * @param string $type
+ * @param string $dependency
+ * @return string
+ */
+ public function getDependencyLabel($type, $dependency)
+ {
+ $provider = $this->getProvider($type);
+ $dependencies = $provider->getDependencies();
+ return isset($dependencies[$dependency]) ? $dependencies[$dependency] : $dependency;
+ }
+
+ /**
+ * Find a provider that match
+ *
+ * @access public
+ * @throws ExternalLinkProviderNotFound
+ * @return ExternalLinkProviderInterface
+ */
+ public function find()
+ {
+ $provider = null;
+
+ if ($this->userInputType === self::TYPE_AUTO) {
+ $provider = $this->findProvider();
+ } else {
+ $provider = $this->getProvider($this->userInputType);
+ $provider->setUserTextInput($this->userInputText);
+ }
+
+ if ($provider === null) {
+ throw new ExternalLinkProviderNotFound('Unable to find link information from provided information');
+ }
+
+ return $provider;
+ }
+
+ /**
+ * Set form values
+ *
+ * @access public
+ * @param array $values
+ * @return ExternalLinkManager
+ */
+ public function setUserInput(array $values)
+ {
+ $this->userInputType = empty($values['type']) ? self::TYPE_AUTO : $values['type'];
+ $this->userInputText = empty($values['text']) ? '' : trim($values['text']);
+ return $this;
+ }
+
+ /**
+ * Find a provider that user input
+ *
+ * @access private
+ * @return ExternalLinkProviderInterface
+ */
+ private function findProvider()
+ {
+ foreach ($this->providers as $provider) {
+ $provider->setUserTextInput($this->userInputText);
+
+ if ($provider->match()) {
+ return $provider;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/app/Core/ExternalLink/ExternalLinkProviderInterface.php b/app/Core/ExternalLink/ExternalLinkProviderInterface.php
new file mode 100644
index 00000000..c908e1eb
--- /dev/null
+++ b/app/Core/ExternalLink/ExternalLinkProviderInterface.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Kanboard\Core\ExternalLink;
+
+/**
+ * External Link Provider Interface
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+interface ExternalLinkProviderInterface
+{
+ /**
+ * Get provider name (label)
+ *
+ * @access public
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Get link type (will be saved in the database)
+ *
+ * @access public
+ * @return string
+ */
+ public function getType();
+
+ /**
+ * Get a dictionary of supported dependency types by the provider
+ *
+ * Example:
+ *
+ * [
+ * 'related' => t('Related'),
+ * 'child' => t('Child'),
+ * 'parent' => t('Parent'),
+ * 'self' => t('Self'),
+ * ]
+ *
+ * The dictionary key is saved in the database.
+ *
+ * @access public
+ * @return array
+ */
+ public function getDependencies();
+
+ /**
+ * Set text entered by the user
+ *
+ * @access public
+ * @param string $input
+ */
+ public function setUserTextInput($input);
+
+ /**
+ * Return true if the provider can parse correctly the user input
+ *
+ * @access public
+ * @return boolean
+ */
+ public function match();
+
+ /**
+ * Get the link found with the properties
+ *
+ * @access public
+ * @return ExternalLinkInterface
+ */
+ public function getLink();
+}
diff --git a/app/Core/ExternalLink/ExternalLinkProviderNotFound.php b/app/Core/ExternalLink/ExternalLinkProviderNotFound.php
new file mode 100644
index 00000000..4fd05202
--- /dev/null
+++ b/app/Core/ExternalLink/ExternalLinkProviderNotFound.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Kanboard\Core\ExternalLink;
+
+use Exception;
+
+/**
+ * External Link Provider Not Found Exception
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class ExternalLinkProviderNotFound extends Exception
+{
+}
diff --git a/app/Core/Http/Client.php b/app/Core/Http/Client.php
index c6bf36a6..12b0a1cb 100644
--- a/app/Core/Http/Client.php
+++ b/app/Core/Http/Client.php
@@ -34,6 +34,19 @@ class Client extends Base
const HTTP_USER_AGENT = 'Kanboard';
/**
+ * Send a GET HTTP request
+ *
+ * @access public
+ * @param string $url
+ * @param string[] $headers
+ * @return string
+ */
+ public function get($url, array $headers = array())
+ {
+ return $this->doRequest('GET', $url, '', $headers);
+ }
+
+ /**
* Send a GET HTTP request and parse JSON response
*
* @access public
diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php
index 7fefddeb..a0d8137b 100644
--- a/app/Core/Http/Response.php
+++ b/app/Core/Http/Response.php
@@ -68,11 +68,12 @@ class Response extends Base
*
* @access public
* @param string $url Redirection URL
+ * @param boolean $self If Ajax request and true: refresh the current page
*/
- public function redirect($url)
+ public function redirect($url, $self = false)
{
if ($this->request->getServerVariable('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest') {
- header('X-Ajax-Redirect: '.$url);
+ header('X-Ajax-Redirect: '.($self ? 'self' : $url));
} else {
header('Location: '.$url);
}
diff --git a/app/ExternalLink/AttachmentLink.php b/app/ExternalLink/AttachmentLink.php
new file mode 100644
index 00000000..5a0d1344
--- /dev/null
+++ b/app/ExternalLink/AttachmentLink.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\ExternalLink\ExternalLinkInterface;
+
+/**
+ * Attachment Link
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class AttachmentLink extends BaseLink implements ExternalLinkInterface
+{
+ /**
+ * Get link title
+ *
+ * @access public
+ * @return string
+ */
+ public function getTitle()
+ {
+ $path = parse_url($this->url, PHP_URL_PATH);
+ return basename($path);
+ }
+}
diff --git a/app/ExternalLink/AttachmentLinkProvider.php b/app/ExternalLink/AttachmentLinkProvider.php
new file mode 100644
index 00000000..df27284f
--- /dev/null
+++ b/app/ExternalLink/AttachmentLinkProvider.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\ExternalLink\ExternalLinkProviderInterface;
+
+/**
+ * Attachment Link Provider
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class AttachmentLinkProvider extends BaseLinkProvider implements ExternalLinkProviderInterface
+{
+ /**
+ * File extensions that are not attachments
+ *
+ * @access protected
+ * @var array
+ */
+ protected $extensions = array(
+ 'html',
+ 'htm',
+ 'xhtml',
+ 'php',
+ 'jsp',
+ 'do',
+ 'action',
+ 'asp',
+ 'aspx',
+ 'cgi',
+ );
+
+ /**
+ * Get provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return t('Attachment');
+ }
+
+ /**
+ * Get link type
+ *
+ * @access public
+ * @return string
+ */
+ public function getType()
+ {
+ return 'attachment';
+ }
+
+ /**
+ * Get a dictionary of supported dependency types by the provider
+ *
+ * @access public
+ * @return array
+ */
+ public function getDependencies()
+ {
+ return array(
+ 'related' => t('Related'),
+ );
+ }
+
+ /**
+ * Return true if the provider can parse correctly the user input
+ *
+ * @access public
+ * @return boolean
+ */
+ public function match()
+ {
+ if (preg_match('/^https?:\/\/.*\.([^\/]+)$/', $this->userInput, $matches)) {
+ return $this->isValidExtension($matches[1]);
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the link found with the properties
+ *
+ * @access public
+ * @return ExternalLinkInterface
+ */
+ public function getLink()
+ {
+ $link = new AttachmentLink($this->container);
+ $link->setUrl($this->userInput);
+
+ return $link;
+ }
+
+ /**
+ * Check file extension
+ *
+ * @access protected
+ * @param string $extension
+ * @return boolean
+ */
+ protected function isValidExtension($extension)
+ {
+ $extension = strtolower($extension);
+
+ foreach ($this->extensions as $ext) {
+ if ($extension === $ext) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/app/ExternalLink/BaseLink.php b/app/ExternalLink/BaseLink.php
new file mode 100644
index 00000000..08693ae7
--- /dev/null
+++ b/app/ExternalLink/BaseLink.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\Base;
+
+/**
+ * Base Link
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+abstract class BaseLink extends Base
+{
+ /**
+ * URL
+ *
+ * @access protected
+ * @var string
+ */
+ protected $url = '';
+
+ /**
+ * Get link URL
+ *
+ * @access public
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * Set link URL
+ *
+ * @access public
+ * @param string $url
+ */
+ public function setUrl($url)
+ {
+ $this->url = $url;
+ }
+}
diff --git a/app/ExternalLink/BaseLinkProvider.php b/app/ExternalLink/BaseLinkProvider.php
new file mode 100644
index 00000000..749cda94
--- /dev/null
+++ b/app/ExternalLink/BaseLinkProvider.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\Base;
+
+/**
+ * Base Link Provider
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+abstract class BaseLinkProvider extends Base
+{
+ /**
+ * User input
+ *
+ * @access protected
+ * @var string
+ */
+ protected $userInput = '';
+
+ /**
+ * Set text entered by the user
+ *
+ * @access public
+ * @param string $input
+ */
+ public function setUserTextInput($input)
+ {
+ $this->userInput = trim($input);
+ }
+}
diff --git a/app/ExternalLink/WebLink.php b/app/ExternalLink/WebLink.php
new file mode 100644
index 00000000..9338ca42
--- /dev/null
+++ b/app/ExternalLink/WebLink.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\ExternalLink\ExternalLinkInterface;
+
+/**
+ * Web Link
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class WebLink extends BaseLink implements ExternalLinkInterface
+{
+ /**
+ * Get link title
+ *
+ * @access public
+ * @return string
+ */
+ public function getTitle()
+ {
+ $html = $this->httpClient->get($this->url);
+
+ if (preg_match('/<title>(.*)<\/title>/siU', $html, $matches)) {
+ return trim($matches[1]);
+ }
+
+ $components = parse_url($this->url);
+
+ if (! empty($components['host']) && ! empty($components['path'])) {
+ return $components['host'].$components['path'];
+ }
+
+ return t('Title not found');
+ }
+}
diff --git a/app/ExternalLink/WebLinkProvider.php b/app/ExternalLink/WebLinkProvider.php
new file mode 100644
index 00000000..ea6dc132
--- /dev/null
+++ b/app/ExternalLink/WebLinkProvider.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Kanboard\ExternalLink;
+
+use Kanboard\Core\ExternalLink\ExternalLinkProviderInterface;
+
+/**
+ * Web Link Provider
+ *
+ * @package externalLink
+ * @author Frederic Guillot
+ */
+class WebLinkProvider extends BaseLinkProvider implements ExternalLinkProviderInterface
+{
+ /**
+ * Get provider name
+ *
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return t('Web Link');
+ }
+
+ /**
+ * Get link type
+ *
+ * @access public
+ * @return string
+ */
+ public function getType()
+ {
+ return 'weblink';
+ }
+
+ /**
+ * Get a dictionary of supported dependency types by the provider
+ *
+ * @access public
+ * @return array
+ */
+ public function getDependencies()
+ {
+ return array(
+ 'related' => t('Related'),
+ );
+ }
+
+ /**
+ * Return true if the provider can parse correctly the user input
+ *
+ * @access public
+ * @return boolean
+ */
+ public function match()
+ {
+ $startWithHttp = strpos($this->userInput, 'http://') === 0 || strpos($this->userInput, 'https://') === 0;
+ $validUrl = filter_var($this->userInput, FILTER_VALIDATE_URL);
+
+ return $startWithHttp && $validUrl;
+ }
+
+ /**
+ * Get the link found with the properties
+ *
+ * @access public
+ * @return ExternalLinkInterface
+ */
+ public function getLink()
+ {
+ $link = new WebLink($this->container);
+ $link->setUrl($this->userInput);
+
+ return $link;
+ }
+}
diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php
index 208f7d77..af090d23 100644
--- a/app/Locale/bs_BA/translations.php
+++ b/app/Locale/bs_BA/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php
index 70d2815b..6dacadcb 100644
--- a/app/Locale/cs_CZ/translations.php
+++ b/app/Locale/cs_CZ/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index db32e047..e30eac46 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 55d144d6..77835aa2 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -1097,4 +1097,30 @@ return array(
'Highest priority' => 'Höchste Priorität',
'If you put zero to the low and high priority, this feature will be disabled.' => 'Wenn Sie Null bei höchster und niedrigster Priorität eintragen, wird diese Funktion deaktiviert.',
'Priority: %d' => 'Priorität: %d',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php
index e87d1daa..d75c25d1 100644
--- a/app/Locale/el_GR/translations.php
+++ b/app/Locale/el_GR/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index c8bc0d7b..02dd6790 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index 6ae4a373..df20259a 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index f04f95ba..df74e5b4 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -1100,4 +1100,30 @@ return array(
'Highest priority' => 'Priorité haute',
'If you put zero to the low and high priority, this feature will be disabled.' => 'Si vous mettez zéro pour la priorité basse et haute, cette fonctionnalité sera désactivée.',
'Priority: %d' => 'Priorité : %d',
+ 'Close a task when there is no activity' => 'Fermer une tâche sans activité',
+ 'Duration in days' => 'Durée en jours',
+ 'Send email when there is no activity on a task' => 'Envoyer un email lorsqu\'il n\'y a pas d\'activité sur une tâche',
+ 'List of external links' => 'Liste des liens externes',
+ 'Unable to fetch link information.' => 'Impossible de récupérer les informations sur le lien.',
+ 'Daily background job for tasks' => 'Tâche planifée quotidienne pour les tâches',
+ 'Auto' => 'Auto',
+ 'Related' => 'Relié',
+ 'Attachment' => 'Pièce-jointe',
+ 'Title not found' => 'Titre non trouvé',
+ 'Web Link' => 'Lien web',
+ 'External links' => 'Liens externes',
+ 'Add external link' => 'Ajouter un lien externe',
+ 'Type' => 'Type',
+ 'Dependency' => 'Dépendance',
+ 'View internal links' => 'Voir les liens internes',
+ 'View external links' => 'Voir les liens externes',
+ 'Add internal link' => 'Ajouter un lien interne',
+ 'Add a new external link' => 'Ajouter un nouveau lien externe',
+ 'Edit external link' => 'Modifier un lien externe',
+ 'External link' => 'Lien externe',
+ 'Copy and paste your link here...' => 'Copier-coller vôtre lien ici...',
+ 'URL' => 'URL',
+ 'There is no external link for the moment.' => 'Il n\'y a pas de lien externe pour le moment.',
+ 'Internal links' => 'Liens internes',
+ 'There is no internal link for the moment.' => 'Il n\'y a pas de lien interne pour le moment.',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index 5f34ac56..b8d6d1b6 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php
index 5c9be946..02ee727c 100644
--- a/app/Locale/id_ID/translations.php
+++ b/app/Locale/id_ID/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index 4ff71c7b..b9c45642 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -1097,4 +1097,30 @@ return array(
'Highest priority' => 'Priorità massima',
'If you put zero to the low and high priority, this feature will be disabled.' => 'Se imposti a zero la priorità massima e minima, questa funzionalità sarà disabilitata.',
'Priority: %d' => 'Priorità: %d',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index 21961f3b..a16c44e8 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php
index 643f5b4a..93185888 100644
--- a/app/Locale/my_MY/translations.php
+++ b/app/Locale/my_MY/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php
index 66ccd125..b35dce3d 100644
--- a/app/Locale/nb_NO/translations.php
+++ b/app/Locale/nb_NO/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index d7268df3..0117a978 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index 222f1fe2..4f4f840f 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index 155d4e83..1b5b35a6 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php
index 99bb096f..baa8eab0 100644
--- a/app/Locale/pt_PT/translations.php
+++ b/app/Locale/pt_PT/translations.php
@@ -1097,4 +1097,30 @@ return array(
'Highest priority' => 'Prioridade mais alta',
'If you put zero to the low and high priority, this feature will be disabled.' => 'Se colocar zero na prioridade baixa ou alta, essa funcionalidade será desactivada.',
'Priority: %d' => 'Prioridade: %d',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index 93419c5b..3aa717b9 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -1097,4 +1097,30 @@ return array(
'Highest priority' => 'Нивысший приоритет',
'If you put zero to the low and high priority, this feature will be disabled.' => 'Если Вы введете 0 для наименьшего и наивысшего приоритета, этот функционал будет отключен.',
'Priority: %d' => 'Приоритет: %d',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index 430a299f..4b320fe7 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index a4d61922..c225cc05 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index e6411efc..7fa53c6f 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index a628d6f8..35613345 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index 8df2c01b..9de31cdd 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -1097,4 +1097,30 @@ return array(
// 'Highest priority' => '',
// 'If you put zero to the low and high priority, this feature will be disabled.' => '',
// 'Priority: %d' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'List of external links' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'View internal links' => '',
+ // 'View external links' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'There is no external link for the moment.' => '',
+ // 'Internal links' => '',
+ // 'There is no internal link for the moment.' => '',
);
diff --git a/app/Model/TaskExternalLink.php b/app/Model/TaskExternalLink.php
new file mode 100644
index 00000000..f2c756b4
--- /dev/null
+++ b/app/Model/TaskExternalLink.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Kanboard\Model;
+
+/**
+ * Task External Link Model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskExternalLink extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'task_has_external_links';
+
+ /**
+ * Get all links
+ *
+ * @access public
+ * @param integer $task_id
+ * @return array
+ */
+ public function getAll($task_id)
+ {
+ $types = $this->externalLinkManager->getTypes();
+
+ $links = $this->db->table(self::TABLE)
+ ->columns(self::TABLE.'.*', User::TABLE.'.name AS creator_name', User::TABLE.'.username AS creator_username')
+ ->eq('task_id', $task_id)
+ ->asc('title')
+ ->join(User::TABLE, 'id', 'creator_id')
+ ->findAll();
+
+ foreach ($links as &$link) {
+ $link['dependency_label'] = $this->externalLinkManager->getDependencyLabel($link['link_type'], $link['dependency']);
+ $link['type'] = isset($types[$link['link_type']]) ? $types[$link['link_type']] : t('Unknown');
+ }
+
+ return $links;
+ }
+
+ /**
+ * Get link
+ *
+ * @access public
+ * @param integer $link_id
+ * @return array
+ */
+ public function getById($link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $link_id)->findOne();
+ }
+
+ /**
+ * Add a new link in the database
+ *
+ * @access public
+ * @param array $values Form values
+ * @return boolean|integer
+ */
+ public function create(array $values)
+ {
+ unset($values['id']);
+ $values['creator_id'] = $this->userSession->getId();
+ $values['date_creation'] = time();
+ $values['date_modification'] = $values['date_creation'];
+
+ return $this->persist(self::TABLE, $values);
+ }
+
+ /**
+ * Modify external link
+ *
+ * @access public
+ * @param array $values Form values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ $values['date_modification'] = time();
+ return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ * @param integer $link_id
+ * @return boolean
+ */
+ public function remove($link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $link_id)->remove();
+ }
+}
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index 1c83136b..ab290bce 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -88,11 +88,12 @@ class TaskFinder extends Base
return $this->db
->table(Task::TABLE)
->columns(
- '(SELECT count(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
- '(SELECT count(*) FROM '.File::TABLE.' WHERE task_id=tasks.id) AS nb_files',
- '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks',
- '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
- '(SELECT count(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links',
+ '(SELECT COUNT(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
+ '(SELECT COUNT(*) FROM '.File::TABLE.' WHERE task_id=tasks.id) AS nb_files',
+ '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks',
+ '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
+ '(SELECT COUNT(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links',
+ '(SELECT COUNT(*) FROM '.TaskExternalLink::TABLE.' WHERE '.TaskExternalLink::TABLE.'.task_id = tasks.id) AS nb_external_links',
'(SELECT DISTINCT 1 FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id AND '.TaskLink::TABLE.'.link_id = 9) AS is_milestone',
'tasks.id',
'tasks.reference',
diff --git a/app/Model/User.php b/app/Model/User.php
index 0174a040..dd622207 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -265,7 +265,7 @@ class User extends Base
*
* @access public
* @param array $values Form values
- * @return array
+ * @return boolean
*/
public function update(array $values)
{
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 8f1db510..5433c751 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,25 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 103;
+const VERSION = 104;
+
+function version_104(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE task_has_external_links (
+ id INT NOT NULL AUTO_INCREMENT,
+ link_type VARCHAR(100) NOT NULL,
+ dependency VARCHAR(100) NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ url VARCHAR(255) NOT NULL,
+ date_creation INT NOT NULL,
+ date_modification INT NOT NULL,
+ task_id INT NOT NULL,
+ creator_id INT DEFAULT 0,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+}
function version_103(PDO $pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index a7bf8054..363b633b 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,25 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 83;
+const VERSION = 84;
+
+function version_84(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE task_has_external_links (
+ id SERIAL,
+ link_type VARCHAR(100) NOT NULL,
+ dependency VARCHAR(100) NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ url VARCHAR(255) NOT NULL,
+ date_creation INT NOT NULL,
+ date_modification INT NOT NULL,
+ task_id INT NOT NULL,
+ creator_id INT DEFAULT 0,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )
+ ");
+}
function version_83(PDO $pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index 08689749..bc701341 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,25 @@ use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
use PDO;
-const VERSION = 95;
+const VERSION = 96;
+
+function version_96(PDO $pdo)
+{
+ $pdo->exec("
+ CREATE TABLE task_has_external_links (
+ id INTEGER PRIMARY KEY,
+ link_type TEXT NOT NULL,
+ dependency TEXT NOT NULL,
+ title TEXT NOT NULL,
+ url TEXT NOT NULL,
+ date_creation INTEGER NOT NULL,
+ date_modification INTEGER NOT NULL,
+ task_id INTEGER NOT NULL,
+ creator_id INTEGER DEFAULT 0,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )
+ ");
+}
function version_95(PDO $pdo)
{
diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php
index ed0962b6..144abb0e 100644
--- a/app/ServiceProvider/AuthenticationProvider.php
+++ b/app/ServiceProvider/AuthenticationProvider.php
@@ -89,6 +89,9 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->add('Taskduplication', '*', Role::PROJECT_MEMBER);
$acl->add('TaskImport', '*', Role::PROJECT_MANAGER);
$acl->add('Tasklink', '*', Role::PROJECT_MEMBER);
+ $acl->add('Tasklink', array('show'), Role::PROJECT_VIEWER);
+ $acl->add('TaskExternalLink', '*', Role::PROJECT_MEMBER);
+ $acl->add('TaskExternalLink', array('show'), Role::PROJECT_VIEWER);
$acl->add('Taskmodification', '*', Role::PROJECT_MEMBER);
$acl->add('Taskstatus', '*', Role::PROJECT_MEMBER);
$acl->add('Timer', '*', Role::PROJECT_MEMBER);
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index df4e183b..61a4c512 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -61,6 +61,7 @@ class ClassProvider implements ServiceProviderInterface
'TaskCreation',
'TaskDuplication',
'TaskExport',
+ 'TaskExternalLink',
'TaskFinder',
'TaskFilter',
'TaskLink',
@@ -97,6 +98,7 @@ class ClassProvider implements ServiceProviderInterface
'CommentValidator',
'CurrencyValidator',
'CustomFilterValidator',
+ 'ExternalLinkValidator',
'GroupValidator',
'LinkValidator',
'PasswordResetValidator',
diff --git a/app/ServiceProvider/ExternalLinkProvider.php b/app/ServiceProvider/ExternalLinkProvider.php
new file mode 100644
index 00000000..c4bbc4cf
--- /dev/null
+++ b/app/ServiceProvider/ExternalLinkProvider.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Kanboard\Core\ExternalLink\ExternalLinkManager;
+use Kanboard\ExternalLink\WebLinkProvider;
+use Kanboard\ExternalLink\AttachmentLinkProvider;
+
+/**
+ * External Link Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class ExternalLinkProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['externalLinkManager'] = new ExternalLinkManager($container);
+ $container['externalLinkManager']->register(new WebLinkProvider($container));
+ $container['externalLinkManager']->register(new AttachmentLinkProvider($container));
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php
index dd9ee23b..ebe087ae 100644
--- a/app/ServiceProvider/RouteProvider.php
+++ b/app/ServiceProvider/RouteProvider.php
@@ -106,11 +106,18 @@ class RouteProvider implements ServiceProviderInterface
$container['route']->addRoute('project/:project_id/task/:task_id/screenshot', 'file', 'screenshot');
$container['route']->addRoute('project/:project_id/task/:task_id/upload', 'file', 'create');
$container['route']->addRoute('project/:project_id/task/:task_id/comment', 'comment', 'create');
+ $container['route']->addRoute('project/:project_id/task/:task_id/links', 'tasklink', 'show');
$container['route']->addRoute('project/:project_id/task/:task_id/link', 'tasklink', 'create');
$container['route']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions');
$container['route']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics');
$container['route']->addRoute('project/:project_id/task/:task_id/remove', 'task', 'remove');
+ $container['route']->addRoute('project/:project_id/task/:task_id/links/external', 'TaskExternalLink', 'show');
+ $container['route']->addRoute('project/:project_id/task/:task_id/link/external/new', 'TaskExternalLink', 'find');
+ $container['route']->addRoute('project/:project_id/task/:task_id/link/external/save', 'TaskExternalLink', 'create');
+ $container['route']->addRoute('project/:project_id/task/:task_id/link/external/:link_id', 'TaskExternalLink', 'edit');
+ $container['route']->addRoute('project/:project_id/task/:task_id/link/external/:link_id/remove', 'TaskExternalLink', 'confirm');
+
$container['route']->addRoute('project/:project_id/task/:task_id/edit', 'taskmodification', 'edit');
$container['route']->addRoute('project/:project_id/task/:task_id/description', 'taskmodification', 'description');
$container['route']->addRoute('project/:project_id/task/:task_id/recurrence', 'taskmodification', 'recurrence');
diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php
index 26f3b1d4..1912dd83 100644
--- a/app/Template/board/task_footer.php
+++ b/app/Template/board/task_footer.php
@@ -35,7 +35,11 @@
<?php endif ?>
<?php if (! empty($task['nb_links'])): ?>
- <span title="<?= t('Links') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-code-fork"></i>&nbsp;<?= $task['nb_links'] ?></span>
+ <span title="<?= t('Links') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-code-fork fa-fw"></i><?= $task['nb_links'] ?></span>
+ <?php endif ?>
+
+ <?php if (! empty($task['nb_external_links'])): ?>
+ <span title="<?= t('External links') ?>" class="tooltip" data-href="<?= $this->url->href('BoardTooltip', 'externallinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-external-link fa-fw"></i><?= $task['nb_external_links'] ?></span>
<?php endif ?>
<?php if (! empty($task['nb_subtasks'])): ?>
diff --git a/app/Template/board/task_menu.php b/app/Template/board/task_menu.php
index b5ed125d..9e26e15b 100644
--- a/app/Template/board/task_menu.php
+++ b/app/Template/board/task_menu.php
@@ -7,6 +7,7 @@
<li><i class="fa fa-pencil-square-o fa-fw"></i>&nbsp;<?= $this->url->link(t('Edit this task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<li><i class="fa fa-comment-o fa-fw"></i>&nbsp;<?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<li><i class="fa fa-code-fork fa-fw"></i>&nbsp;<?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
+ <li><i class="fa fa-external-link fa-fw"></i>&nbsp;<?= $this->url->link(t('Add external link'), 'TaskExternalLink', 'find', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<li><i class="fa fa-camera fa-fw"></i>&nbsp;<?= $this->url->link(t('Add a screenshot'), 'BoardPopover', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?></li>
<?php if ($task['is_active'] == 1): ?>
<li><i class="fa fa-close fa-fw"></i>&nbsp;<?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'popover') ?></li>
diff --git a/app/Template/board/tooltip_external_links.php b/app/Template/board/tooltip_external_links.php
new file mode 100644
index 00000000..7681c06c
--- /dev/null
+++ b/app/Template/board/tooltip_external_links.php
@@ -0,0 +1,20 @@
+<table class="table-striped table-small">
+ <tr>
+ <th class="column-20"><?= t('Type') ?></th>
+ <th class="column-80"><?= t('Title') ?></th>
+ <th class="column-10"><?= t('Dependency') ?></th>
+ </tr>
+ <?php foreach ($links as $link): ?>
+ <tr>
+ <td>
+ <?= $link['type'] ?>
+ </td>
+ <td>
+ <a href="<?= $link['url'] ?>" target="_blank"><?= $this->e($link['title']) ?></a>
+ </td>
+ <td>
+ <?= $this->e($link['dependency_label']) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table> \ No newline at end of file
diff --git a/app/Template/comment/create.php b/app/Template/comment/create.php
index e9a6404d..8ce9aac3 100644
--- a/app/Template/comment/create.php
+++ b/app/Template/comment/create.php
@@ -2,7 +2,7 @@
<h2><?= t('Add a comment') ?></h2>
</div>
-<form method="post" action="<?= $this->url->href('comment', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => isset($ajax))) ?>" autocomplete="off" class="form-comment">
+<form method="post" action="<?= $this->url->href('comment', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" autocomplete="off" class="form-comment">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('task_id', $values) ?>
<?= $this->form->hidden('user_id', $values) ?>
@@ -41,7 +41,7 @@
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?php if (! isset($skip_cancel)): ?>
<?= t('or') ?>
- <?php if (isset($ajax)): ?>
+ <?php if ($ajax): ?>
<?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?>
<?php else: ?>
<?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
diff --git a/app/Template/task/comments.php b/app/Template/task/comments.php
index 57fb305f..ef85287b 100644
--- a/app/Template/task/comments.php
+++ b/app/Template/task/comments.php
@@ -28,7 +28,8 @@
'task_id' => $task['id'],
),
'errors' => array(),
- 'task' => $task
+ 'task' => $task,
+ 'ajax' => $ajax,
)) ?>
<?php endif ?>
</div>
diff --git a/app/Template/task/show.php b/app/Template/task/show.php
index f6d47e53..b848d49e 100644
--- a/app/Template/task/show.php
+++ b/app/Template/task/show.php
@@ -13,14 +13,6 @@
<?= $this->render('task/description', array('task' => $task)) ?>
-<?= $this->render('tasklink/show', array(
- 'task' => $task,
- 'links' => $links,
- 'link_label_list' => $link_label_list,
- 'editable' => $this->user->hasProjectAccess('tasklink', 'edit', $project['id']),
- 'is_public' => false,
-)) ?>
-
<?= $this->render('subtask/show', array(
'task' => $task,
'subtasks' => $subtasks,
@@ -29,6 +21,14 @@
'editable' => $this->user->hasProjectAccess('subtask', 'edit', $project['id']),
)) ?>
+<?= $this->render('tasklink/show', array(
+ 'task' => $task,
+ 'links' => $links,
+ 'link_label_list' => $link_label_list,
+ 'editable' => $this->user->hasProjectAccess('tasklink', 'edit', $project['id']),
+ 'is_public' => false,
+)) ?>
+
<?= $this->render('task/time_tracking_summary', array('task' => $task)) ?>
<?= $this->render('file/show', array(
@@ -42,4 +42,5 @@
'comments' => $comments,
'project' => $project,
'editable' => $this->user->hasProjectAccess('comment', 'edit', $project['id']),
+ 'ajax' => $ajax,
)) ?>
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index f522c1c4..b5a2c4b4 100644
--- a/app/Template/task/sidebar.php
+++ b/app/Template/task/sidebar.php
@@ -21,6 +21,25 @@
<?= $this->hook->render('template:task:sidebar:information') ?>
</ul>
+
+ <h2><?= t('Links') ?></h2>
+ <ul>
+ <li <?= $this->app->checkMenuSelection('tasklink', 'show') ?>>
+ <?= $this->url->link(t('View internal links'), 'tasklink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li <?= $this->app->checkMenuSelection('TaskExternalLink', 'show') ?>>
+ <?= $this->url->link(t('View external links'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <?php if ($this->user->hasProjectAccess('tasklink', 'create', $task['project_id'])): ?>
+ <li <?= $this->app->checkMenuSelection('tasklink', 'create') ?>>
+ <?= $this->url->link(t('Add internal link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li <?= $this->app->checkMenuSelection('TaskExternalLink', 'find') ?>>
+ <?= $this->url->link(t('Add external link'), 'TaskExternalLink', 'find', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <?php endif ?>
+ </ul>
+
<?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
<h2><?= t('Actions') ?></h2>
<ul>
@@ -36,9 +55,6 @@
<li <?= $this->app->checkMenuSelection('subtask', 'create') ?>>
<?= $this->url->link(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
- <li <?= $this->app->checkMenuSelection('tasklink', 'create') ?>>
- <?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
<li <?= $this->app->checkMenuSelection('comment', 'create') ?>>
<?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
</li>
diff --git a/app/Template/task_creation/form.php b/app/Template/task_creation/form.php
index eaf9024d..01b000f2 100644
--- a/app/Template/task_creation/form.php
+++ b/app/Template/task_creation/form.php
@@ -10,7 +10,7 @@
</div>
<?php endif ?>
-<form id="task-form" method="post" action="<?= $this->url->href('taskcreation', 'save', array('project_id' => $values['project_id'])) ?>" autocomplete="off">
+<form id="task-form" class="popover-form" method="post" action="<?= $this->url->href('taskcreation', 'save', array('project_id' => $values['project_id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
diff --git a/app/Template/task_external_link/create.php b/app/Template/task_external_link/create.php
new file mode 100644
index 00000000..3179d6af
--- /dev/null
+++ b/app/Template/task_external_link/create.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Add a new external link') ?></h2>
+</div>
+
+<form class="popover-form" action="<?= $this->url->href('TaskExternalLink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" method="post" autocomplete="off">
+ <?= $this->render('task_external_link/form', array('task' => $task, 'dependencies' => $dependencies, 'values' => $values, 'errors' => $errors)) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/task_external_link/edit.php b/app/Template/task_external_link/edit.php
new file mode 100644
index 00000000..cf9ddfed
--- /dev/null
+++ b/app/Template/task_external_link/edit.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Edit external link') ?></h2>
+</div>
+
+<form class="popover-form" action="<?= $this->url->href('TaskExternalLink', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" method="post" autocomplete="off">
+ <?= $this->render('task_external_link/form', array('task' => $task, 'dependencies' => $dependencies, 'values' => $values, 'errors' => $errors)) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue">
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/task_external_link/find.php b/app/Template/task_external_link/find.php
new file mode 100644
index 00000000..a2304014
--- /dev/null
+++ b/app/Template/task_external_link/find.php
@@ -0,0 +1,32 @@
+<div class="page-header">
+ <h2><?= t('Add a new external link') ?></h2>
+</div>
+
+<form class="popover-form" action="<?= $this->url->href('TaskExternalLink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" method="post" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?>
+
+ <?= $this->form->label(t('External link'), 'text') ?>
+ <?= $this->form->text(
+ 'text',
+ $values,
+ $errors,
+ array(
+ 'required',
+ 'autofocus',
+ 'placeholder="'.t('Copy and paste your link here...').'"',
+ )) ?>
+
+ <?= $this->form->label(t('Link type'), 'type') ?>
+ <?= $this->form->select('type', $types, $values) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Next') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?php if ($ajax): ?>
+ <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id']), false, 'close-popover') ?>
+ <?php else: ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?php endif ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/task_external_link/form.php b/app/Template/task_external_link/form.php
new file mode 100644
index 00000000..932ca521
--- /dev/null
+++ b/app/Template/task_external_link/form.php
@@ -0,0 +1,13 @@
+<?= $this->form->csrf() ?>
+<?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?>
+<?= $this->form->hidden('id', $values) ?>
+<?= $this->form->hidden('link_type', $values) ?>
+
+<?= $this->form->label(t('URL'), 'url') ?>
+<?= $this->form->text('url', $values, $errors, array('required')) ?>
+
+<?= $this->form->label(t('Title'), 'title') ?>
+<?= $this->form->text('title', $values, $errors, array('required')) ?>
+
+<?= $this->form->label(t('Dependency'), 'dependency') ?>
+<?= $this->form->select('dependency', $dependencies, $values, $errors) ?>
diff --git a/app/Template/task_external_link/remove.php b/app/Template/task_external_link/remove.php
new file mode 100644
index 00000000..f55e751c
--- /dev/null
+++ b/app/Template/task_external_link/remove.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Remove a link') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this link: "%s"?', $link['title']) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'TaskExternalLink', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/task_external_link/show.php b/app/Template/task_external_link/show.php
new file mode 100644
index 00000000..7e8b1948
--- /dev/null
+++ b/app/Template/task_external_link/show.php
@@ -0,0 +1,50 @@
+<div class="page-header">
+ <h2><?= t('External links') ?></h2>
+</div>
+
+<?php if (empty($links)): ?>
+ <p class="alert"><?= t('There is no external link for the moment.') ?></p>
+<?php else: ?>
+ <table class="table-stripped table-small">
+ <tr>
+ <th class="column-10"><?= t('Type') ?></th>
+ <th><?= t('Title') ?></th>
+ <th class="column-10"><?= t('Dependency') ?></th>
+ <th class="column-15"><?= t('Creator') ?></th>
+ <th class="column-15"><?= t('Date') ?></th>
+ <?php if ($this->user->hasProjectAccess('TaskExternalLink', 'edit', $task['project_id'])): ?>
+ <th class="column-5"><?= t('Action') ?></th>
+ <?php endif ?>
+ </tr>
+ <?php foreach ($links as $link): ?>
+ <tr>
+ <td>
+ <?= $link['type'] ?>
+ </td>
+ <td>
+ <a href="<?= $link['url'] ?>" target="_blank"><?= $this->e($link['title']) ?></a>
+ </td>
+ <td>
+ <?= $this->e($link['dependency_label']) ?>
+ </td>
+ <td>
+ <?= $this->e($link['creator_name'] ?: $link['creator_username']) ?>
+ </td>
+ <td>
+ <?= dt('%B %e, %Y', $link['date_creation']) ?>
+ </td>
+ <?php if ($this->user->hasProjectAccess('TaskExternalLink', 'edit', $task['project_id'])): ?>
+ <td>
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li><?= $this->url->link(t('Edit'), 'TaskExternalLink', 'edit', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?></li>
+ <li><?= $this->url->link(t('Remove'), 'TaskExternalLink', 'confirm', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?></li>
+ </ul>
+ </div>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </table>
+<?php endif ?>
diff --git a/app/Template/tasklink/create.php b/app/Template/tasklink/create.php
index 2832bdc7..f4c3cdae 100644
--- a/app/Template/tasklink/create.php
+++ b/app/Template/tasklink/create.php
@@ -2,7 +2,7 @@
<h2><?= t('Add a new link') ?></h2>
</div>
-<form action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => isset($ajax))) ?>" method="post" autocomplete="off">
+<form action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" method="post" autocomplete="off">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?>
@@ -28,7 +28,7 @@
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
<?= t('or') ?>
- <?php if (isset($ajax)): ?>
+ <?php if ($ajax): ?>
<?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id']), false, 'close-popover') ?>
<?php else: ?>
<?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
diff --git a/app/Template/tasklink/show.php b/app/Template/tasklink/show.php
index 5843da17..11b12b73 100644
--- a/app/Template/tasklink/show.php
+++ b/app/Template/tasklink/show.php
@@ -1,15 +1,17 @@
-<?php if (! empty($links)): ?>
<div class="page-header">
- <h2><?= t('Links') ?></h2>
+ <h2><?= t('Internal links') ?></h2>
</div>
-<table id="links">
+<?php if (empty($links)): ?>
+ <p class="alert"><?= t('There is no internal link for the moment.') ?></p>
+<?php else: ?>
+<table id="links" class="table-small table-stripped">
<tr>
<th class="column-20"><?= t('Label') ?></th>
<th class="column-30"><?= t('Task') ?></th>
<th class="column-20"><?= t('Project') ?></th>
<th><?= t('Column') ?></th>
<th><?= t('Assignee') ?></th>
- <?php if ($editable): ?>
+ <?php if ($editable && $this->user->hasProjectAccess('Tasklink', 'edit', $task['project_id'])): ?>
<th class="column-5"><?= t('Action') ?></th>
<?php endif ?>
</tr>
@@ -64,7 +66,7 @@
<?php endif ?>
<?php endif ?>
</td>
- <?php if ($editable): ?>
+ <?php if ($editable && $this->user->hasProjectAccess('Tasklink', 'edit', $task['project_id'])): ?>
<td>
<div class="dropdown">
<a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-cog fa-fw"></i><i class="fa fa-caret-down"></i></a>
diff --git a/app/Validator/ExternalLinkValidator.php b/app/Validator/ExternalLinkValidator.php
new file mode 100644
index 00000000..fff4133b
--- /dev/null
+++ b/app/Validator/ExternalLinkValidator.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Kanboard\Validator;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * External Link Validator
+ *
+ * @package validator
+ * @author Frederic Guillot
+ */
+class ExternalLinkValidator extends Base
+{
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, $this->commonValidationRules());
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Validate modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateModification(array $values)
+ {
+ $rules = array(
+ new Validators\Required('id', t('The id is required')),
+ );
+
+ $v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+
+ /**
+ * Common validation rules
+ *
+ * @access private
+ * @return array
+ */
+ private function commonValidationRules()
+ {
+ return array(
+ new Validators\Required('url', t('Field required')),
+ new Validators\MaxLength('url', t('The maximum length is %d characters', 255), 255),
+ new Validators\Required('title', t('Field required')),
+ new Validators\MaxLength('title', t('The maximum length is %d characters', 255), 255),
+ new Validators\Required('link_type', t('Field required')),
+ new Validators\MaxLength('link_type', t('The maximum length is %d characters', 100), 100),
+ new Validators\Required('dependency', t('Field required')),
+ new Validators\MaxLength('dependency', t('The maximum length is %d characters', 100), 100),
+ new Validators\Integer('id', t('This value must be an integer')),
+ new Validators\Required('task_id', t('Field required')),
+ new Validators\Integer('task_id', t('This value must be an integer')),
+ );
+ }
+}
diff --git a/app/common.php b/app/common.php
index fe287811..399fcb86 100644
--- a/app/common.php
+++ b/app/common.php
@@ -34,4 +34,5 @@ $container->register(new Kanboard\ServiceProvider\EventDispatcherProvider);
$container->register(new Kanboard\ServiceProvider\GroupProvider);
$container->register(new Kanboard\ServiceProvider\RouteProvider);
$container->register(new Kanboard\ServiceProvider\ActionProvider);
+$container->register(new Kanboard\ServiceProvider\ExternalLinkProvider);
$container->register(new Kanboard\ServiceProvider\PluginProvider);