From 9e1dcf21dc5d65bcc4195f1ae4caedbe57835415 Mon Sep 17 00:00:00 2001 From: Frédéric Guillot Date: Mon, 21 Jul 2014 20:32:12 -0230 Subject: Improve webhooks to call external url on task creation/modification --- app/Controller/Base.php | 1 + app/Event/ProjectModificationDate.php | 53 ++++++++++++ app/Event/TaskModification.php | 51 ----------- app/Event/WebhookListener.php | 49 +++++++++++ app/Locales/de_DE/translations.php | 2 + app/Locales/es_ES/translations.php | 2 + app/Locales/fr_FR/translations.php | 2 + app/Locales/pl_PL/translations.php | 2 + app/Locales/pt_BR/translations.php | 2 + app/Locales/sv_SE/translations.php | 2 + app/Locales/zh_CN/translations.php | 2 + app/Model/Project.php | 4 +- app/Model/Webhook.php | 154 ++++++++++++++++++++++++++++++++++ app/Schema/Mysql.php | 8 +- app/Schema/Postgres.php | 8 +- app/Schema/Sqlite.php | 8 +- app/Templates/config_index.php | 6 ++ docs/webhooks.markdown | 71 ++++++++++++++-- jsonrpc.php | 3 + 19 files changed, 368 insertions(+), 62 deletions(-) create mode 100644 app/Event/ProjectModificationDate.php delete mode 100644 app/Event/TaskModification.php create mode 100644 app/Event/WebhookListener.php create mode 100644 app/Model/Webhook.php 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/ProjectModificationDate.php b/app/Event/ProjectModificationDate.php new file mode 100644 index 00000000..8fbaee73 --- /dev/null +++ b/app/Event/ProjectModificationDate.php @@ -0,0 +1,53 @@ +project = $project; + } + + /** + * 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) + { + if (isset($data['project_id'])) { + $this->project->updateModificationDate($data['project_id']); + return true; + } + + return false; + } +} diff --git a/app/Event/TaskModification.php b/app/Event/TaskModification.php deleted file mode 100644 index b1d412c7..00000000 --- a/app/Event/TaskModification.php +++ /dev/null @@ -1,51 +0,0 @@ -project = $project; - } - - /** - * 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) - { - if (isset($data['project_id'])) { - $this->project->updateModificationDate($data['project_id']); - return true; - } - - return false; - } -} 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 @@ +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 @@ +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 @@
+ +
+ + +
+
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 +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'))); -- cgit v1.2.3