diff --git a/app/Controller/Base.php b/app/Controller/Base.php index 462529b17..2739c5ac0 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 similarity index 85% rename from app/Event/TaskModification.php rename to app/Event/ProjectModificationDate.php index b1d412c74..8fbaee73c 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 000000000..f97766538 --- /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 01be45c77..cc62bbe3a 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 2b7420d98..7306526f3 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 3d1d313b6..9399fd14b 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 eaafe7c5e..c961ac2ea 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 a422a6602..bb7a37190 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 d69f66046..8113477cd 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 de12c4248..22678f197 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 51a23967f..5d3f01b9d 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 000000000..679d3edc7 --- /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 b9c35efc6..46fc6d430 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 bc18bdcab..a9eea531c 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 5ab42a6ed..4660251f6 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 b242d16ff..919197768 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 fb5335f49..1d4617469 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')));