diff options
author | Frédéric Guillot <fred@kanboard.net> | 2014-07-21 20:32:12 -0230 |
---|---|---|
committer | Frédéric Guillot <fred@kanboard.net> | 2014-07-21 20:32:12 -0230 |
commit | 9e1dcf21dc5d65bcc4195f1ae4caedbe57835415 (patch) | |
tree | 7fa9a2a08e24de7d16c0196b61e7c1bf8540f486 | |
parent | 4ae655ced334bb48342274caaf15f2b3d8b444f6 (diff) |
Improve webhooks to call external url on task creation/modification
-rw-r--r-- | app/Controller/Base.php | 1 | ||||
-rw-r--r-- | app/Event/ProjectModificationDate.php (renamed from app/Event/TaskModification.php) | 8 | ||||
-rw-r--r-- | app/Event/WebhookListener.php | 49 | ||||
-rw-r--r-- | app/Locales/de_DE/translations.php | 2 | ||||
-rw-r--r-- | app/Locales/es_ES/translations.php | 2 | ||||
-rw-r--r-- | app/Locales/fr_FR/translations.php | 2 | ||||
-rw-r--r-- | app/Locales/pl_PL/translations.php | 2 | ||||
-rw-r--r-- | app/Locales/pt_BR/translations.php | 2 | ||||
-rw-r--r-- | app/Locales/sv_SE/translations.php | 2 | ||||
-rw-r--r-- | app/Locales/zh_CN/translations.php | 2 | ||||
-rw-r--r-- | app/Model/Project.php | 4 | ||||
-rw-r--r-- | app/Model/Webhook.php | 154 | ||||
-rw-r--r-- | app/Schema/Mysql.php | 8 | ||||
-rw-r--r-- | app/Schema/Postgres.php | 8 | ||||
-rw-r--r-- | app/Schema/Sqlite.php | 8 | ||||
-rw-r--r-- | app/Templates/config_index.php | 6 | ||||
-rw-r--r-- | docs/webhooks.markdown | 71 | ||||
-rw-r--r-- | jsonrpc.php | 3 |
18 files changed, 320 insertions, 14 deletions
diff --git a/app/Controller/Base.php b/app/Controller/Base.php index 462529b1..2739c5ac 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -151,6 +151,7 @@ abstract class Base // Attach events $this->action->attachEvents(); $this->project->attachEvents(); + $this->webhook->attachEvents(); } /** diff --git a/app/Event/TaskModification.php b/app/Event/ProjectModificationDate.php index b1d412c7..8fbaee73 100644 --- a/app/Event/TaskModification.php +++ b/app/Event/ProjectModificationDate.php @@ -6,12 +6,14 @@ use Core\Listener; use Model\Project; /** - * Task modification listener + * Project modification date listener * - * @package events + * Update the last modified field for a project + * + * @package event * @author Frederic Guillot */ -class TaskModification implements Listener +class ProjectModificationDate implements Listener { /** * Project model diff --git a/app/Event/WebhookListener.php b/app/Event/WebhookListener.php new file mode 100644 index 00000000..f9776653 --- /dev/null +++ b/app/Event/WebhookListener.php @@ -0,0 +1,49 @@ +<?php + +namespace Event; + +use Core\Listener; +use Model\Webhook; + +/** + * Webhook task events + * + * @package event + * @author Frederic Guillot + */ +class WebhookListener implements Listener +{ + /** + * Webhook model + * + * @accesss private + * @var \Model\Webhook + */ + private $webhook; + + /** + * Constructor + * + * @access public + * @param string $url URL to call + * @param \Model\Webhook $webhook Webhook model instance + */ + public function __construct($url, Webhook $webhook) + { + $this->url = $url; + $this->webhook = $webhook; + } + + /** + * Execute the action + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function execute(array $data) + { + $this->webhook->notify($this->url, $data); + return true; + } +} diff --git a/app/Locales/de_DE/translations.php b/app/Locales/de_DE/translations.php index 01be45c7..cc62bbe3 100644 --- a/app/Locales/de_DE/translations.php +++ b/app/Locales/de_DE/translations.php @@ -396,4 +396,6 @@ return array( // 'Creator' => '', // 'Modification date' => '', // 'Completion date' => '', + // 'Webhook URL for task creation' => '', + // 'Webhook URL for task modification' => '', ); diff --git a/app/Locales/es_ES/translations.php b/app/Locales/es_ES/translations.php index 2b7420d9..7306526f 100644 --- a/app/Locales/es_ES/translations.php +++ b/app/Locales/es_ES/translations.php @@ -395,4 +395,6 @@ return array( // 'Creator' => '', // 'Modification date' => '', // 'Completion date' => '', + // 'Webhook URL for task creation' => '', + // 'Webhook URL for task modification' => '', ); diff --git a/app/Locales/fr_FR/translations.php b/app/Locales/fr_FR/translations.php index 3d1d313b..9399fd14 100644 --- a/app/Locales/fr_FR/translations.php +++ b/app/Locales/fr_FR/translations.php @@ -393,4 +393,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', ); diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php index eaafe7c5..c961ac2e 100644 --- a/app/Locales/pl_PL/translations.php +++ b/app/Locales/pl_PL/translations.php @@ -396,4 +396,6 @@ return array( // 'Creator' => '', // 'Modification date' => '', // 'Completion date' => '', + // 'Webhook URL for task creation' => '', + // 'Webhook URL for task modification' => '', ); diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php index a422a660..bb7a3719 100644 --- a/app/Locales/pt_BR/translations.php +++ b/app/Locales/pt_BR/translations.php @@ -393,4 +393,6 @@ return array( // 'Creator' => '', // 'Modification date' => '', // 'Completion date' => '', + // 'Webhook URL for task creation' => '', + // 'Webhook URL for task modification' => '', ); diff --git a/app/Locales/sv_SE/translations.php b/app/Locales/sv_SE/translations.php index d69f6604..8113477c 100644 --- a/app/Locales/sv_SE/translations.php +++ b/app/Locales/sv_SE/translations.php @@ -395,4 +395,6 @@ return array( // 'Creator' => '', // 'Modification date' => '', // 'Completion date' => '', + // 'Webhook URL for task creation' => '', + // 'Webhook URL for task modification' => '', ); diff --git a/app/Locales/zh_CN/translations.php b/app/Locales/zh_CN/translations.php index de12c424..22678f19 100644 --- a/app/Locales/zh_CN/translations.php +++ b/app/Locales/zh_CN/translations.php @@ -401,4 +401,6 @@ return array( // 'Creator' => '', // 'Modification date' => '', // 'Completion date' => '', + // 'Webhook URL for task creation' => '', + // 'Webhook URL for task modification' => '', ); diff --git a/app/Model/Project.php b/app/Model/Project.php index 51a23967..5d3f01b9 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -4,7 +4,7 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; -use Event\TaskModification; +use Event\ProjectModificationDate; use Core\Security; /** @@ -575,7 +575,7 @@ class Project extends Base Task::EVENT_OPEN, ); - $listener = new TaskModification($this); + $listener = new ProjectModificationDate($this); foreach ($events as $event_name) { $this->event->attach($event_name, $listener); diff --git a/app/Model/Webhook.php b/app/Model/Webhook.php new file mode 100644 index 00000000..679d3edc --- /dev/null +++ b/app/Model/Webhook.php @@ -0,0 +1,154 @@ +<?php + +namespace Model; + +use Event\WebhookListener; + +/** + * Webhook model + * + * @package model + * @author Frederic Guillot + */ +class Webhook extends Base +{ + /** + * HTTP connection timeout in seconds + * + * @var integer + */ + const HTTP_TIMEOUT = 1; + + /** + * Number of maximum redirections for the HTTP client + * + * @var integer + */ + const HTTP_MAX_REDIRECTS = 3; + + /** + * HTTP client user agent + * + * @var string + */ + const HTTP_USER_AGENT = 'Kanboard Webhook'; + + /** + * URL to call for task creation + * + * @access private + * @var string + */ + private $url_task_creation = ''; + + /** + * URL to call for task modification + * + * @access private + * @var string + */ + private $url_task_modification = ''; + + /** + * Webook token + * + * @access private + * @var string + */ + private $token = ''; + + /** + * Attach events + * + * @access public + */ + public function attachEvents() + { + $config = new Config($this->db, $this->event); + + $this->url_task_creation = $config->get('webhooks_url_task_creation'); + $this->url_task_modification = $config->get('webhooks_url_task_modification'); + $this->token = $config->get('webhooks_token'); + + if ($this->url_task_creation) { + $this->attachCreateEvents(); + } + + if ($this->url_task_modification) { + $this->attachUpdateEvents(); + } + } + + /** + * Attach events for task modification + * + * @access public + */ + public function attachUpdateEvents() + { + $events = array( + Task::EVENT_UPDATE, + Task::EVENT_CLOSE, + Task::EVENT_OPEN, + ); + + $listener = new WebhookListener($this->url_task_modification, $this); + + foreach ($events as $event_name) { + $this->event->attach($event_name, $listener); + } + } + + /** + * Attach events for task creation + * + * @access public + */ + public function attachCreateEvents() + { + $events = array( + Task::EVENT_CREATE, + ); + + $listener = new WebhookListener($this->url_task_creation, $this); + + foreach ($events as $event_name) { + $this->event->attach($event_name, $listener); + } + } + + /** + * Call the external URL + * + * @access public + * @param string $url URL to call + * @param array $task Task data + */ + public function notify($url, array $task) + { + $headers = array( + 'Connection: close', + 'User-Agent: '.self::HTTP_USER_AGENT, + ); + + $context = stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'protocol_version' => 1.1, + 'timeout' => self::HTTP_TIMEOUT, + 'max_redirects' => self::HTTP_MAX_REDIRECTS, + 'header' => implode("\r\n", $headers), + 'content' => json_encode($task) + ) + )); + + if (strpos($url, '?') !== false) { + $url .= '&token='.$this->token; + } + else { + $url .= '?token='.$this->token; + } + + @file_get_contents($url, false, $context); + } +} diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index b9c35efc..46fc6d43 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -4,7 +4,13 @@ namespace Schema; use Core\Security; -const VERSION = 21; +const VERSION = 22; + +function version_22($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification VARCHAR(255)"); + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation VARCHAR(255)"); +} function version_21($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index bc18bdca..a9eea531 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -4,7 +4,13 @@ namespace Schema; use Core\Security; -const VERSION = 2; +const VERSION = 3; + +function version_3($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification VARCHAR(255)"); + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation VARCHAR(255)"); +} function version_2($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 5ab42a6e..4660251f 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -4,7 +4,13 @@ namespace Schema; use Core\Security; -const VERSION = 21; +const VERSION = 22; + +function version_22($pdo) +{ + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification TEXT"); + $pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation TEXT"); +} function version_21($pdo) { diff --git a/app/Templates/config_index.php b/app/Templates/config_index.php index b242d16f..91919776 100644 --- a/app/Templates/config_index.php +++ b/app/Templates/config_index.php @@ -15,6 +15,12 @@ <?= Helper\form_label(t('Timezone'), 'timezone') ?> <?= Helper\form_select('timezone', $timezones, $values, $errors) ?><br/> + <?= Helper\form_label(t('Webhook URL for task creation'), 'webhooks_url_task_creation') ?> + <?= Helper\form_text('webhooks_url_task_creation', $values, $errors) ?><br/> + + <?= Helper\form_label(t('Webhook URL for task modification'), 'webhooks_url_task_modification') ?> + <?= Helper\form_text('webhooks_url_task_modification', $values, $errors) ?><br/> + <div class="form-actions"> <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> </div> diff --git a/docs/webhooks.markdown b/docs/webhooks.markdown index fb5335f4..1d461746 100644 --- a/docs/webhooks.markdown +++ b/docs/webhooks.markdown @@ -1,7 +1,10 @@ Webhooks ======== -Webhooks are useful to perform actions from external applications (shell-scripts, git hooks...). +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 How to create a task with a webhook? ------------------------------------ @@ -16,17 +19,15 @@ curl "http://myserver/?controller=task&action=add&token=superSecretToken&title=m curl "http://myserver/?controller=task&action=add&token=superSecretToken&title=task123&project_id=3&column_id=7&color_id=red" ``` -Available responses -------------------- +### Available responses - When a task is created successfully, Kanboard return the message "OK" in plain text. - However if the task creation fail, you will got a "FAILED" message. - If the token is wrong, you got a "Not Authorized" message and a HTTP status code 401. -Available parameters --------------------- +### Available parameters -Base url: `http://YOUR_SERVER_HOSTNAME/?controller=task&action=add` +Base URL: `http://YOUR_SERVER_HOSTNAME/?controller=task&action=add` - `token`: Token displayed on the settings page (required) - `title`: Task title (required) @@ -37,3 +38,61 @@ Base url: `http://YOUR_SERVER_HOSTNAME/?controller=task&action=add` - `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/jsonrpc.php b/jsonrpc.php index 186e454d..9ec51c94 100644 --- a/jsonrpc.php +++ b/jsonrpc.php @@ -13,6 +13,7 @@ use Model\Comment; use Model\SubTask; use Model\Board; use Model\Action; +use Model\Webhook; $config = new Config($registry->shared('db'), $registry->shared('event')); $project = new Project($registry->shared('db'), $registry->shared('event')); @@ -23,9 +24,11 @@ $comment = new Comment($registry->shared('db'), $registry->shared('event')); $subtask = new SubTask($registry->shared('db'), $registry->shared('event')); $board = new Board($registry->shared('db'), $registry->shared('event')); $action = new Action($registry->shared('db'), $registry->shared('event')); +$webhook = new Webhook($registry->shared('db'), $registry->shared('event')); $action->attachEvents(); $project->attachEvents(); +$webhook->attachEvents(); $server = new Server; $server->authentication(array('jsonrpc' => $config->get('api_token'))); |