summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Locale/da_DK/translations.php2
-rw-r--r--app/Locale/de_DE/translations.php2
-rw-r--r--app/Locale/es_ES/translations.php2
-rw-r--r--app/Locale/fi_FI/translations.php2
-rw-r--r--app/Locale/fr_FR/translations.php2
-rw-r--r--app/Locale/hu_HU/translations.php2
-rw-r--r--app/Locale/it_IT/translations.php2
-rw-r--r--app/Locale/ja_JP/translations.php2
-rw-r--r--app/Locale/nl_NL/translations.php2
-rw-r--r--app/Locale/pl_PL/translations.php2
-rw-r--r--app/Locale/pt_BR/translations.php2
-rw-r--r--app/Locale/ru_RU/translations.php2
-rw-r--r--app/Locale/sr_Latn_RS/translations.php2
-rw-r--r--app/Locale/sv_SE/translations.php2
-rw-r--r--app/Locale/th_TH/translations.php2
-rw-r--r--app/Locale/tr_TR/translations.php2
-rw-r--r--app/Locale/zh_CN/translations.php2
-rw-r--r--app/Model/Comment.php4
-rw-r--r--app/Model/Webhook.php23
-rw-r--r--app/Schema/Mysql.php11
-rw-r--r--app/Schema/Postgres.php11
-rw-r--r--app/Schema/Sqlite.php11
-rw-r--r--app/Subscriber/WebhookSubscriber.php47
-rw-r--r--app/Template/comment/edit.php1
-rw-r--r--app/Template/config/webhook.php7
-rw-r--r--app/Template/user/show.php2
-rw-r--r--docs/webhooks.markdown323
-rw-r--r--tests/units/Base.php30
-rw-r--r--tests/units/TimetableTest.php39
-rw-r--r--tests/units/WebhookTest.php112
30 files changed, 501 insertions, 154 deletions
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index 1a080b4a..4e174ac6 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Skaber',
'Modification date' => 'Ændringsdato',
'Completion date' => 'Afslutningsdato',
- 'Webhook URL for task creation' => 'Webhook URL for opgave oprettelse',
- 'Webhook URL for task modification' => 'Webhook URL opgave redigering',
'Clone' => 'Kopier',
'Clone Project' => 'Kopier projekt',
'Project cloned successfully.' => 'Projektet er kopieret.',
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 3dff0aab..787974dd 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Erstellt von',
'Modification date' => 'Änderungsdatum',
'Completion date' => 'Abschlussdatum',
- 'Webhook URL for task creation' => 'Webhook URL zur Aufgabenerstellung',
- 'Webhook URL for task modification' => 'Webhook URL zur Aufgabenbearbeitung',
'Clone' => 'duplizieren',
'Clone Project' => 'Projekt duplizieren',
'Project cloned successfully.' => 'Projekt wurde dupliziert.',
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index e778d5a8..68bd73b6 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Creador',
'Modification date' => 'Fecha de modificación',
'Completion date' => 'Fecha de terminación',
- 'Webhook URL for task creation' => 'Disparador Web (Webhook) para la creación de tareas',
- 'Webhook URL for task modification' => 'Disparador Web (Webhook) para la modificación de tareas',
'Clone' => 'Clonar',
'Clone Project' => 'Clonar proyecto',
'Project cloned successfully.' => 'Proyecto clonado correctamente',
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index d41cd82f..2bb28719 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Luonut',
'Modification date' => 'Muokkauspäivä',
'Completion date' => 'Valmistumispäivä',
- 'Webhook URL for task creation' => 'Webhook URL tehtävän luomiselle',
- 'Webhook URL for task modification' => 'Webhook URL tehtävän muokkaamiselle',
'Clone' => 'Kahdenna',
'Clone Project' => 'Kahdenna projekti',
'Project cloned successfully.' => 'Projekti kahdennettu onnistuneesti',
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index 3621413a..64cb0999 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Créateur',
'Modification date' => 'Date de modification',
'Completion date' => 'Date de complétion',
- 'Webhook URL for task creation' => 'URL du webhook pour la création de tâche',
- 'Webhook URL for task modification' => 'URL du webhook pour la modification de tâche',
'Clone' => 'Clone',
'Clone Project' => 'Cloner le projet',
'Project cloned successfully.' => 'Projet cloné avec succès.',
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index 468b6a01..9831aa8a 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Készítette',
'Modification date' => 'Módosítás dátuma',
'Completion date' => 'Befejezés határideje',
- 'Webhook URL for task creation' => 'Webhook URL a feladat létrehozásakor',
- 'Webhook URL for task modification' => 'Webhook URL a feladatot módosításakor',
'Clone' => 'Másolat',
'Clone Project' => 'Projekt másolása',
'Project cloned successfully.' => 'A projekt sikeresen másolva.',
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index 2bbd8ba7..4674ddc4 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Creatore',
'Modification date' => 'Data di modifica',
'Completion date' => 'Data di termine',
- 'Webhook URL for task creation' => 'URL del Webhook per la creazione di compiti',
- 'Webhook URL for task modification' => 'URL del Webhook per la modifica di compiti',
'Clone' => 'Clona',
'Clone Project' => 'Clona il progetto',
'Project cloned successfully.' => 'Progetto clonato con successo.',
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index 1cc3818a..f4ea4d5f 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => '作成者',
'Modification date' => '変更日',
'Completion date' => '完了日',
- 'Webhook URL for task creation' => 'タスク作成の Webhook URL',
- 'Webhook URL for task modification' => 'タスク変更の Webhook URL',
'Clone' => '複製',
'Clone Project' => 'プロジェクトの複製',
'Project cloned successfully.' => 'プロジェクトを複製しました。',
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index c4c262e1..5d1f0695 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Aangemaakt door',
'Modification date' => 'Wijzigingsdatum',
'Completion date' => 'Afgerond op',
- 'Webhook URL for task creation' => 'Webhook URL voor aanmaken taak',
- 'Webhook URL for task modification' => 'Webhook URL voor wijzigen taak',
'Clone' => 'Kloon',
'Clone Project' => 'Project klonen',
'Project cloned successfully.' => 'Project succesvol gekloond.',
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index f1dade8f..8e0d8f58 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Autor',
'Modification date' => 'Data modyfyfikacji',
'Completion date' => 'Data ukończenia',
- 'Webhook URL for task creation' => 'Webhook URL do tworzenia zadań',
- 'Webhook URL for task modification' => 'Webhook URL do modyfikacji zadań',
'Clone' => 'Sklonuj',
'Clone Project' => 'Sklonuj projekt',
'Project cloned successfully.' => 'Projekt sklonowany pomyślnie.',
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index e8414330..85fc57f5 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Criado por',
'Modification date' => 'Data da modificação',
'Completion date' => 'Data da finalização',
- 'Webhook URL for task creation' => 'Webhook URL para criação de tarefas',
- 'Webhook URL for task modification' => 'Webhook URL para modificação de tarefa',
'Clone' => 'Clonar',
'Clone Project' => 'Clonar Projeto',
'Project cloned successfully.' => 'Projeto clonado com sucesso.',
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index d5df5e44..8bb5076c 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Автор',
'Modification date' => 'Дата изменения',
'Completion date' => 'Дата завершения',
- 'Webhook URL for task creation' => 'Webhook URL для создания задачи',
- 'Webhook URL for task modification' => 'Webhook URL для изменения задачи',
'Clone' => 'Клонировать',
'Clone Project' => 'Клонировать проект',
'Project cloned successfully.' => 'Проект клонирован.',
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index cf125c54..9b0e56be 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Autor',
'Modification date' => 'Datum izmene',
'Completion date' => 'Datum kompletiranja',
- 'Webhook URL for task creation' => 'Webhook URL zadatka za kreiranje',
- 'Webhook URL for task modification' => 'Webhook URL zadatka za izmenu',
'Clone' => 'Iskopiraj',
'Clone Project' => 'Iskopiraj projekat',
'Project cloned successfully.' => 'Projekat uspešno iskopiran.',
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index e2c46c9e..f144e9ad 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Skapare',
'Modification date' => 'Ändringsdatum',
'Completion date' => 'Slutfört datum',
- 'Webhook URL for task creation' => 'Webhook URL för att skapa uppgift',
- 'Webhook URL for task modification' => 'Webhook URL för att ändra uppgift',
'Clone' => 'Klona',
'Clone Project' => 'Klona projekt',
'Project cloned successfully.' => 'Projektet har klonats.',
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index 68b32c42..e6642a3d 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'ผู้สร้าง',
'Modification date' => 'วันที่แก้ไข',
'Completion date' => 'วันที่เสร็จสิ้น',
- 'Webhook URL for task creation' => 'Webhook URL for task creation',
- 'Webhook URL for task modification' => 'Webhook URL for task modification',
'Clone' => 'เลียนแบบ',
// 'Clone Project' => '',
'Project cloned successfully.' => 'เลียนแบบโปรเจคเรียบร้อยแล้ว',
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index 1368c358..a4327351 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => 'Oluşturan',
'Modification date' => 'Değişiklik tarihi',
'Completion date' => 'Tamamlanma tarihi',
- // 'Webhook URL for task creation' => '',
- // 'Webhook URL for task modification' => '',
'Clone' => 'Kopya oluştur',
'Clone Project' => 'Projenin kopyasını oluştur',
'Project cloned successfully.' => 'Proje kopyası başarıyla oluşturuldu.',
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index 7d492635..c9350c8b 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -389,8 +389,6 @@ return array(
'Creator' => '创建者',
'Modification date' => '修改日期',
'Completion date' => '完成日期',
- 'Webhook URL for task creation' => '创建任务的Webhook URL',
- 'Webhook URL for task modification' => '修改任务的Webhook URL',
'Clone' => '克隆',
'Clone Project' => '复制项目',
'Project cloned successfully.' => '成功复制项目。',
diff --git a/app/Model/Comment.php b/app/Model/Comment.php
index 844f0c89..3aa9c027 100644
--- a/app/Model/Comment.php
+++ b/app/Model/Comment.php
@@ -129,7 +129,9 @@ class Comment extends Base
->eq('id', $values['id'])
->update(array('comment' => $values['comment']));
- $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new CommentEvent($values));
+ if ($result) {
+ $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new CommentEvent($values));
+ }
return $result;
}
diff --git a/app/Model/Webhook.php b/app/Model/Webhook.php
index b3603818..8c270fb6 100644
--- a/app/Model/Webhook.php
+++ b/app/Model/Webhook.php
@@ -14,20 +14,23 @@ class Webhook extends Base
* Call the external URL
*
* @access public
- * @param string $url URL to call
- * @param array $task Task data
+ * @param array $values Event payload
*/
- public function notify($url, array $task)
+ public function notify(array $values)
{
+ $url = $this->config->get('webhook_url');
$token = $this->config->get('webhook_token');
- if (strpos($url, '?') !== false) {
- $url .= '&token='.$token;
- }
- else {
- $url .= '?token='.$token;
- }
+ if (! empty($url)) {
- return $this->httpClient->post($url, $task);
+ if (strpos($url, '?') !== false) {
+ $url .= '&token='.$token;
+ }
+ else {
+ $url .= '?token='.$token;
+ }
+
+ return $this->httpClient->post($url, $values);
+ }
}
}
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 593cca80..aa611a6e 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,16 @@ use PDO;
use Core\Security;
use Model\Link;
-const VERSION = 70;
+const VERSION = 71;
+
+function version_71($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO `settings` VALUES (?, ?)');
+ $rq->execute(array('webhook_url', ''));
+
+ $pdo->exec("DELETE FROM `settings` WHERE `option`='webhook_url_task_creation'");
+ $pdo->exec("DELETE FROM `settings` WHERE `option`='webhook_url_task_modification'");
+}
function version_70($pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index 4ec6965f..0afcd26a 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,16 @@ use PDO;
use Core\Security;
use Model\Link;
-const VERSION = 51;
+const VERSION = 52;
+
+function version_52($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('webhook_url', ''));
+
+ $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_creation'");
+ $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_modification'");
+}
function version_51($pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index 84892aaf..43fb136e 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,16 @@ use Core\Security;
use PDO;
use Model\Link;
-const VERSION = 69;
+const VERSION = 70;
+
+function version_70($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('webhook_url', ''));
+
+ $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_creation'");
+ $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_modification'");
+}
function version_69($pdo)
{
diff --git a/app/Subscriber/WebhookSubscriber.php b/app/Subscriber/WebhookSubscriber.php
index 6b5abf1b..e28675bd 100644
--- a/app/Subscriber/WebhookSubscriber.php
+++ b/app/Subscriber/WebhookSubscriber.php
@@ -2,8 +2,13 @@
namespace Subscriber;
+use Event\CommentEvent;
+use Event\GenericEvent;
use Event\TaskEvent;
+use Model\Comment;
use Model\Task;
+use Model\File;
+use Model\Subtask;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class WebhookSubscriber extends Base implements EventSubscriberInterface
@@ -11,32 +16,30 @@ class WebhookSubscriber extends Base implements EventSubscriberInterface
public static function getSubscribedEvents()
{
return array(
- Task::EVENT_CREATE => array('onTaskCreation', 0),
- Task::EVENT_UPDATE => array('onTaskModification', 0),
- Task::EVENT_CLOSE => array('onTaskModification', 0),
- Task::EVENT_OPEN => array('onTaskModification', 0),
- Task::EVENT_MOVE_COLUMN => array('onTaskModification', 0),
- Task::EVENT_MOVE_POSITION => array('onTaskModification', 0),
- Task::EVENT_ASSIGNEE_CHANGE => array('onTaskModification', 0),
+ Task::EVENT_CREATE => array('execute', 0),
+ Task::EVENT_UPDATE => array('execute', 0),
+ Task::EVENT_CLOSE => array('execute', 0),
+ Task::EVENT_OPEN => array('execute', 0),
+ Task::EVENT_MOVE_COLUMN => array('execute', 0),
+ Task::EVENT_MOVE_POSITION => array('execute', 0),
+ Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0),
+ Task::EVENT_MOVE_PROJECT => array('execute', 0),
+ Task::EVENT_MOVE_SWIMLANE => array('execute', 0),
+ Comment::EVENT_CREATE => array('execute', 0),
+ Comment::EVENT_UPDATE => array('execute', 0),
+ File::EVENT_CREATE => array('execute', 0),
+ Subtask::EVENT_CREATE => array('execute', 0),
+ Subtask::EVENT_UPDATE => array('execute', 0),
);
}
- public function onTaskCreation(TaskEvent $event)
+ public function execute(GenericEvent $event, $event_name)
{
- $this->executeRequest('webhook_url_task_creation', $event);
- }
-
- public function onTaskModification(TaskEvent $event)
- {
- $this->executeRequest('webhook_url_task_modification', $event);
- }
-
- public function executeRequest($parameter, TaskEvent $event)
- {
- $url = $this->config->get($parameter);
+ $payload = array(
+ 'event_name' => $event_name,
+ 'event_data' => $event->getAll(),
+ );
- if (! empty($url)) {
- $this->webhook->notify($url, $event->getAll());
- }
+ $this->webhook->notify($payload);
}
}
diff --git a/app/Template/comment/edit.php b/app/Template/comment/edit.php
index b4126a3e..2785fe62 100644
--- a/app/Template/comment/edit.php
+++ b/app/Template/comment/edit.php
@@ -7,6 +7,7 @@
<?= $this->formCsrf() ?>
<?= $this->formHidden('id', $values) ?>
<?= $this->formHidden('task_id', $values) ?>
+ <?= $this->formHidden('user_id', $values) ?>
<div class="form-tabs">
<ul class="form-tabs-nav">
diff --git a/app/Template/config/webhook.php b/app/Template/config/webhook.php
index d4f06d3f..06f7870b 100644
--- a/app/Template/config/webhook.php
+++ b/app/Template/config/webhook.php
@@ -6,11 +6,8 @@
<?= $this->formCsrf() ?>
- <?= $this->formLabel(t('Webhook URL for task creation'), 'webhook_url_task_creation') ?>
- <?= $this->formText('webhook_url_task_creation', $values, $errors) ?><br/>
-
- <?= $this->formLabel(t('Webhook URL for task modification'), 'webhook_url_task_modification') ?>
- <?= $this->formText('webhook_url_task_modification', $values, $errors) ?><br/>
+ <?= $this->formLabel(t('Webhook URL'), 'webhook_url') ?>
+ <?= $this->formText('webhook_url', $values, $errors) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
diff --git a/app/Template/user/show.php b/app/Template/user/show.php
index 9473b382..1be39cc3 100644
--- a/app/Template/user/show.php
+++ b/app/Template/user/show.php
@@ -33,7 +33,7 @@
<div class="listing">
<ul class="no-bullet">
- <li><strong><i class="fa fa-calendar"></i> <?= $this->a(t('iCalendar (iCal format, *.ics)'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?></strong></li>
+ <li><strong><i class="fa fa-calendar"></i> <?= $this->a(t('iCal feed'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?></strong></li>
</ul>
</div>
<?php endif ?>
diff --git a/docs/webhooks.markdown b/docs/webhooks.markdown
index 6627e3af..fa3e11d8 100644
--- a/docs/webhooks.markdown
+++ b/docs/webhooks.markdown
@@ -3,8 +3,269 @@ Webhooks
Webhooks are useful to perform actions with external applications.
-- Webhooks can be used to create a task by calling a simple URL (You can also do that by using the API)
-- An external URL can be called automatically when a task is created or modified
+- Webhooks can be used to create a task by calling a simple URL (You can also do that with the API)
+- An external URL can be called automatically when an event occurs in Kanboard (task creation, comment updated, etc)
+
+How to write a webhook receiver?
+--------------------------------
+
+All internal events of Kanboard can be sent to an external URL.
+
+- The webhook url have to be defined in **Settings > Webhooks > Webhook URL**.
+- When an event is triggered Kanboard call automatically the predefined URL
+- The data are encoded in JSON format and sent with a POST HTTP request
+- The webhook token is also sent as a query string parameter, so you can check if the request really come from Kanboard.
+- **Your custom URL must answer in less than 1 second**, those requests are synchronous (PHP limitation) and that can slow down the user interface if your script is too slow!
+
+### List of supported events
+
+- comment.create
+- comment.update
+- file.create
+- task.move.project
+- task.move.column
+- task.move.position
+- task.move.swimlane
+- task.update
+- task.create
+- task.close
+- task.open
+- task.assignee_change
+- subtask.update
+- subtask.create
+
+### Example of HTTP request
+
+```
+POST https://your_webhook_url/?token=WEBHOOK_TOKEN_HERE
+User-Agent: Kanboard Webhook
+Content-Type: application/json
+Connection: close
+
+{
+ "event_name": "task.move.column",
+ "event_data": {
+ "task_id": "1",
+ "project_id": "1",
+ "position": 1,
+ "column_id": "1",
+ "swimlane_id": "0",
+ "src_column_id": "2",
+ "dst_column_id": "1",
+ "date_moved": "1431991532",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0"
+ }
+}
+```
+
+All event payloads are in the following format:
+
+```json
+{
+ "event_name": "model.event_name",
+ "event_data": {
+ "key1": "value1",
+ "key2": "value2",
+ ...
+ }
+}
+```
+
+The `event_data` values are not necessary normalized across events.
+
+### Examples of event payloads
+
+Task creation:
+
+```json
+{
+ "event_name": "task.create",
+ "event_data": {
+ "title": "Demo",
+ "description": "",
+ "project_id": "1",
+ "owner_id": "1",
+ "category_id": 0,
+ "swimlane_id": 0,
+ "column_id": "2",
+ "color_id": "yellow",
+ "score": 0,
+ "time_estimated": 0,
+ "date_due": 0,
+ "creator_id": 1,
+ "date_creation": 1431991532,
+ "date_modification": 1431991532,
+ "date_moved": 1431991532,
+ "position": 1,
+ "task_id": 1
+ }
+}
+```
+
+Task modification:
+
+```json
+{
+ "event_name": "task.update",
+ "event_data": {
+ "id": "1",
+ "title": "Demo",
+ "description": "",
+ "date_creation": "1431991532",
+ "color_id": "yellow",
+ "project_id": "1",
+ "column_id": "1",
+ "owner_id": "1",
+ "position": "1",
+ "is_active": "1",
+ "date_completed": null,
+ "score": "0",
+ "date_due": "0",
+ "category_id": "0",
+ "creator_id": "1",
+ "date_modification": 1431991603,
+ "reference": "",
+ "date_started": 1431993600,
+ "time_spent": 0,
+ "time_estimated": 0,
+ "swimlane_id": "0",
+ "date_moved": "1431991572",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0",
+ "recurrence_factor": "0",
+ "recurrence_timeframe": "0",
+ "recurrence_basedate": "0",
+ "recurrence_parent": null,
+ "recurrence_child": null,
+ "task_id": "1"
+ }
+}
+```
+
+Move a task to another column:
+
+```json
+{
+ "event_name": "task.move.column",
+ "event_data": {
+ "task_id": "1",
+ "project_id": "1",
+ "position": 1,
+ "column_id": "1",
+ "swimlane_id": "0",
+ "src_column_id": "2",
+ "dst_column_id": "1",
+ "date_moved": "1431991532",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0"
+ }
+}
+```
+
+Move a task to another position:
+
+```json
+{
+ "event_name": "task.move.position",
+ "event_data": {
+ "task_id": "2",
+ "project_id": "1",
+ "position": 1,
+ "column_id": "1",
+ "swimlane_id": "0",
+ "src_column_id": "1",
+ "dst_column_id": "1",
+ "date_moved": "1431996905",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0"
+ }
+}
+```
+
+Comment creation:
+
+```json
+{
+ "event_name": "comment.create",
+ "event_data": {
+ "id": 1,
+ "task_id": "1",
+ "user_id": "1",
+ "comment": "test",
+ "date": 1431991615
+ }
+}
+```
+
+Comment modification:
+
+```
+{
+ "event_name": "comment.update",
+ "event_data": {
+ "id": "1",
+ "task_id": "1",
+ "user_id": "1",
+ "comment": "test edit"
+ }
+}
+```
+
+Subtask creation:
+
+```json
+{
+ "event_name": "subtask.create",
+ "event_data": {
+ "id": 3,
+ "task_id": "1",
+ "title": "Test",
+ "user_id": "1",
+ "time_estimated": "2",
+ "position": 3
+ }
+}
+```
+
+Subtask modification:
+
+```json
+{
+ "event_name": "subtask.update",
+ "event_data": {
+ "id": "1",
+ "status": 1,
+ "task_id": "1"
+ }
+}
+```
+
+File upload:
+
+```json
+{
+ "event_name": "file.create",
+ "event_data": {
+ "task_id": "1",
+ "name": "test.png"
+ }
+}
+```
+
+Screenshot created:
+
+```json
+{
+ "event_name": "file.create",
+ "event_data": {
+ "task_id": "2",
+ "name": "Screenshot taken May 19, 2015 at 10:56 AM"
+ }
+}
+```
+
+Note: Webhooks configuration and payload have changed since Kanboard >= 1.0.15
How to create a task with a webhook?
------------------------------------
@@ -38,61 +299,3 @@ Base URL: `http://YOUR_SERVER_HOSTNAME/?controller=webhook&action=task`
- `column_id`: Column on the board (Get the column id from the projects page, mouse over on the column name)
Only the token and the title parameters are mandatory. The different id can also be found in the database.
-
-How to call an external URL when a task is created or updated?
---------------------------------------------------------------
-
-- There is two events available: **task creation** and **task modification**
-- External URLs can be defined on the settings page
-- When an event is triggered Kanboard call automatically the predefined URL
-- The task data encoded in JSON is sent with a POST HTTP request
-- The webhook token is also sent as a query string parameter, so you can check if the request is not usurped, it's also better if you use HTTPS.
-- **Your custom URL must answer in less than 1 second**, those requests are synchronous (PHP limitation) and that can slow down the application if your script is too slow!
-
-### Quick example with PHP
-
-Start by creating a basic PHP script `index.php`:
-
-```php
-<?php
-
-$body = file_get_contents('php://input');
-file_put_contents('/tmp/webhook', $body);
-```
-
-This script dump the task data to the file `/tmp/webhook`.
-
-Now run a webserver from the command line:
-
-```bash
-php -S 127.0.0.1:8081
-```
-
-After that, go the settings page of Kanboard, enter the right URL here `http://127.0.0.1:8081/`.
-And finally, create a task and you should see the JSON payload in the file.
-
-```javascript
-{
- "task_id":"2",
- "title":"boo",
- "description":"",
- "project_id":"1",
- "owner_id":"0",
- "category_id":"0",
- "column_id":"2",
- "color_id":"yellow",
- "score":0,
- "date_due":0,
- "creator_id":1,
- "date_creation":1405981280,
- "position":0
-}
-```
-
-For our example, Kanboard use this request to call your program:
-
-```
-POST http:://127.0.0.1:8081/?token=RANDOM_TOKEN_HERE
-
-{... JSON payload ...}
-``` \ No newline at end of file
diff --git a/tests/units/Base.php b/tests/units/Base.php
index b6302942..20f4a8cc 100644
--- a/tests/units/Base.php
+++ b/tests/units/Base.php
@@ -11,6 +11,35 @@ use SimpleLogger\File;
date_default_timezone_set('UTC');
+class FakeHttpClient
+{
+ private $url = '';
+ private $data = array();
+
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ public function toPrettyJson()
+ {
+ return json_encode($this->data, JSON_PRETTY_PRINT);
+ }
+
+ public function post($url, array $data)
+ {
+ $this->url = $url;
+ $this->data = $data;
+ //echo $this->toPrettyJson();
+ return true;
+ }
+}
+
abstract class Base extends PHPUnit_Framework_TestCase
{
protected $container;
@@ -43,6 +72,7 @@ abstract class Base extends PHPUnit_Framework_TestCase
$this->container['logger'] = new Logger;
$this->container['logger']->setLogger(new File('/dev/null'));
+ $this->container['httpClient'] = new FakeHttpClient;
}
public function tearDown()
diff --git a/tests/units/TimetableTest.php b/tests/units/TimetableTest.php
index 2f38b534..5f698941 100644
--- a/tests/units/TimetableTest.php
+++ b/tests/units/TimetableTest.php
@@ -138,15 +138,15 @@ class TimetableTest extends Base
$this->assertNotFalse($w->create(1, 2, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 2, '13:00', '17:00'));
- $monday = new DateTime('Monday');
- $tuesday = new DateTime('Tuesday');
+ $monday = new DateTime('next Monday');
+ $tuesday = new DateTime('next Monday + 1 day');
$timetable = $t->calculate(1, new DateTime('Monday'), new DateTime('Monday + 6 days'));
$this->assertNotEmpty($timetable);
$this->assertCount(4, $timetable);
// Start to work before timetable
- $date = new DateTime('Monday');
+ $date = clone($monday);
$date->setTime(5, 02);
$slot = $t->findClosestTimeSlot($date, $timetable);
@@ -155,7 +155,7 @@ class TimetableTest extends Base
$this->assertEquals($monday->format('Y-m-d').' 12:00', $slot[1]->format('Y-m-d H:i'));
// Start to work at the end of the timeslot
- $date = new DateTime('Monday');
+ $date = clone($monday);
$date->setTime(12, 02);
$slot = $t->findClosestTimeSlot($date, $timetable);
@@ -164,7 +164,7 @@ class TimetableTest extends Base
$this->assertEquals($monday->format('Y-m-d').' 12:00', $slot[1]->format('Y-m-d H:i'));
// Start to work at lunch time
- $date = new DateTime('Monday');
+ $date = clone($monday);
$date->setTime(12, 32);
$slot = $t->findClosestTimeSlot($date, $timetable);
@@ -173,7 +173,7 @@ class TimetableTest extends Base
$this->assertEquals($monday->format('Y-m-d').' 17:00', $slot[1]->format('Y-m-d H:i'));
// Start to work early in the morning
- $date = new DateTime('Tuesday');
+ $date = clone($tuesday);
$date->setTime(8, 02);
$slot = $t->findClosestTimeSlot($date, $timetable);
@@ -192,47 +192,50 @@ class TimetableTest extends Base
$this->assertNotFalse($w->create(1, 2, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 2, '13:00', '17:00'));
+ $monday = new DateTime('next Monday');
+ $tuesday = new DateTime('next Monday + 1 day');
+
// Different day
- $start = new DateTime('Monday');
+ $start = clone($monday);
$start->setTime(16, 02);
- $end = new DateTime('Tuesday');
+ $end = clone($tuesday);
$end->setTime(10, 03);
$this->assertEquals(1.5, $t->calculateEffectiveDuration(1, $start, $end));
// Same time slot
- $start = new DateTime('Monday');
+ $start = clone($monday);
$start->setTime(16, 02);
- $end = new DateTime('Monday');
+ $end = clone($monday);
$end->setTime(17, 03);
$this->assertEquals(1, $t->calculateEffectiveDuration(1, $start, $end));
// Intermediate time slot
- $start = new DateTime('Monday');
+ $start = clone($monday);
$start->setTime(10, 02);
- $end = new DateTime('Tuesday');
+ $end = clone($tuesday);
$end->setTime(16, 03);
$this->assertEquals(11.5, $t->calculateEffectiveDuration(1, $start, $end));
// Different day
- $start = new DateTime('Monday');
+ $start = clone($monday);
$start->setTime(9, 02);
- $end = new DateTime('Tuesday');
+ $end = clone($tuesday);
$end->setTime(10, 03);
$this->assertEquals(7, $t->calculateEffectiveDuration(1, $start, $end));
// Start before first time slot
- $start = new DateTime('Monday');
+ $start = clone($monday);
$start->setTime(5, 32);
- $end = new DateTime('Tuesday');
+ $end = clone($tuesday);
$end->setTime(11, 17);
$this->assertEquals(8.25, $t->calculateEffectiveDuration(1, $start, $end));
@@ -242,10 +245,10 @@ class TimetableTest extends Base
{
$t = new Timetable($this->container);
- $start = new DateTime('Monday');
+ $start = new DateTime('next Monday');
$start->setTime(16, 02);
- $end = new DateTime('Monday');
+ $end = new DateTime('next Monday');
$end->setTime(17, 03);
$this->assertEquals(1, $t->calculateEffectiveDuration(1, $start, $end));
diff --git a/tests/units/WebhookTest.php b/tests/units/WebhookTest.php
new file mode 100644
index 00000000..946d744c
--- /dev/null
+++ b/tests/units/WebhookTest.php
@@ -0,0 +1,112 @@
+<?php
+
+require_once __DIR__.'/Base.php';
+
+use Model\Config;
+use Model\Task;
+use Model\TaskCreation;
+use Model\TaskModification;
+use Model\Project;
+use Model\Comment;
+use Subscriber\WebhookSubscriber;
+
+class WebhookTest extends Base
+{
+ public function testTaskCreation()
+ {
+ $c = new Config($this->container);
+ $p = new Project($this->container);
+ $tc = new TaskCreation($this->container);
+ $this->container['dispatcher']->addSubscriber(new WebhookSubscriber($this->container));
+
+ $c->save(array('webhook_url' => 'http://localhost/?task-creation'));
+
+ $this->assertEquals(1, $p->create(array('name' => 'test')));
+ $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
+
+ $this->assertStringStartsWith('http://localhost/?task-creation&token=', $this->container['httpClient']->getUrl());
+
+ $event = $this->container['httpClient']->getData();
+ $this->assertNotEmpty($event);
+ $this->assertArrayHasKey('event_name', $event);
+ $this->assertArrayHasKey('event_data', $event);
+ $this->assertEquals('task.create', $event['event_name']);
+ $this->assertNotEmpty($event['event_data']);
+
+ $this->assertArrayHasKey('project_id', $event['event_data']);
+ $this->assertArrayHasKey('task_id', $event['event_data']);
+ $this->assertArrayHasKey('title', $event['event_data']);
+ $this->assertArrayHasKey('column_id', $event['event_data']);
+ $this->assertArrayHasKey('color_id', $event['event_data']);
+ $this->assertArrayHasKey('swimlane_id', $event['event_data']);
+ $this->assertArrayHasKey('date_creation', $event['event_data']);
+ $this->assertArrayHasKey('date_modification', $event['event_data']);
+ $this->assertArrayHasKey('date_moved', $event['event_data']);
+ $this->assertArrayHasKey('position', $event['event_data']);
+ }
+
+ public function testTaskModification()
+ {
+ $c = new Config($this->container);
+ $p = new Project($this->container);
+ $tc = new TaskCreation($this->container);
+ $tm = new TaskModification($this->container);
+ $this->container['dispatcher']->addSubscriber(new WebhookSubscriber($this->container));
+
+ $c->save(array('webhook_url' => 'http://localhost/modif/'));
+
+ $this->assertEquals(1, $p->create(array('name' => 'test')));
+ $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
+ $this->assertTrue($tm->update(array('id' => 1, 'title' => 'test update')));
+
+ $this->assertStringStartsWith('http://localhost/modif/?token=', $this->container['httpClient']->getUrl());
+
+ $event = $this->container['httpClient']->getData();
+ $this->assertNotEmpty($event);
+ $this->assertArrayHasKey('event_name', $event);
+ $this->assertArrayHasKey('event_data', $event);
+ $this->assertEquals('task.update', $event['event_name']);
+ $this->assertNotEmpty($event['event_data']);
+
+ $this->assertArrayHasKey('project_id', $event['event_data']);
+ $this->assertArrayHasKey('task_id', $event['event_data']);
+ $this->assertArrayHasKey('title', $event['event_data']);
+ $this->assertArrayHasKey('column_id', $event['event_data']);
+ $this->assertArrayHasKey('color_id', $event['event_data']);
+ $this->assertArrayHasKey('swimlane_id', $event['event_data']);
+ $this->assertArrayHasKey('date_creation', $event['event_data']);
+ $this->assertArrayHasKey('date_modification', $event['event_data']);
+ $this->assertArrayHasKey('date_moved', $event['event_data']);
+ $this->assertArrayHasKey('position', $event['event_data']);
+ }
+
+ public function testCommentCreation()
+ {
+ $c = new Config($this->container);
+ $p = new Project($this->container);
+ $tc = new TaskCreation($this->container);
+ $cm = new Comment($this->container);
+ $this->container['dispatcher']->addSubscriber(new WebhookSubscriber($this->container));
+
+ $c->save(array('webhook_url' => 'http://localhost/comment'));
+
+ $this->assertEquals(1, $p->create(array('name' => 'test')));
+ $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
+ $this->assertEquals(1, $cm->create(array('task_id' => 1, 'comment' => 'test comment', 'user_id' => 1)));
+
+ $this->assertStringStartsWith('http://localhost/comment?token=', $this->container['httpClient']->getUrl());
+
+ $event = $this->container['httpClient']->getData();
+ $this->assertNotEmpty($event);
+ $this->assertArrayHasKey('event_name', $event);
+ $this->assertArrayHasKey('event_data', $event);
+ $this->assertEquals('comment.create', $event['event_name']);
+ $this->assertNotEmpty($event['event_data']);
+
+ $this->assertArrayHasKey('task_id', $event['event_data']);
+ $this->assertArrayHasKey('user_id', $event['event_data']);
+ $this->assertArrayHasKey('comment', $event['event_data']);
+ $this->assertArrayHasKey('id', $event['event_data']);
+ $this->assertEquals('test comment', $event['event_data']['comment']);
+ }
+}