diff options
55 files changed, 455 insertions, 125 deletions
diff --git a/.travis.yml b/.travis.yml index 1596ec9a..398dda1b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,20 @@ language: php php: - - "5.6" - - "5.5" - - "5.4" - - "5.3" + - 7.0 + - 5.6 + - 5.5 + - 5.4 + - 5.3 -before_script: wget https://phar.phpunit.de/phpunit.phar -script: +matrix: + fast_finish: true + allow_failures: + - php: 7.0 + +before_script: - composer install - - php phpunit.phar -c tests/units.sqlite.xml
\ No newline at end of file + +script: + - phpunit -c tests/units.sqlite.xml + diff --git a/README.markdown b/README.markdown index fd34c6fb..32a67c48 100644 --- a/README.markdown +++ b/README.markdown @@ -97,6 +97,7 @@ Documentation - [Bitbucket webhooks](docs/bitbucket-webhooks.markdown) - [Github webhooks](docs/github-webhooks.markdown) - [Gitlab webhooks](docs/gitlab-webhooks.markdown) +- [Hipchat](docs/hipchat.markdown) - [Slack](docs/slack.markdown) #### More diff --git a/app/Controller/Config.php b/app/Controller/Config.php index 57f586ae..3c884191 100644 --- a/app/Controller/Config.php +++ b/app/Controller/Config.php @@ -44,7 +44,7 @@ class Config extends Base $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'subtask_forecast' => 0); } else if ($redirect === 'integrations') { - $values += array('integration_slack_webhook' => 0); + $values += array('integration_slack_webhook' => 0, 'integration_hipchat' => 0, 'integration_gravatar' => 0); } if ($this->config->save($values)) { diff --git a/app/Core/Helper.php b/app/Core/Helper.php index 34a5e6ab..29003416 100644 --- a/app/Core/Helper.php +++ b/app/Core/Helper.php @@ -502,7 +502,7 @@ class Helper public function markdown($text, array $link = array()) { $parser = new Markdown($link, $this); - $parser->setMarkupEscaped(true); + $parser->setMarkupEscaped(MARKDOWN_ESCAPE_HTML); return $parser->text($text); } @@ -770,4 +770,21 @@ class Helper return 'fa-file-o'; } + + /** + * Display gravatar image + * + * @access public + * @param string $email + * @param string $alt + * @return string + */ + public function avatar($email, $alt = '') + { + if (! empty($email) && $this->config->get('integration_gravatar') == 1) { + return '<img class="avatar" src="https://www.gravatar.com/avatar/'.md5(strtolower($email)).'?s=25" alt="'.$this->e($alt).'" title="'.$this->e($alt).'">'; + } + + return ''; + } } diff --git a/app/Core/HttpClient.php b/app/Core/HttpClient.php index 96860152..e1d90858 100644 --- a/app/Core/HttpClient.php +++ b/app/Core/HttpClient.php @@ -47,8 +47,9 @@ class HttpClient } $headers = array( - 'Connection: close', 'User-Agent: '.self::HTTP_USER_AGENT, + 'Content-Type: application/json', + 'Connection: close', ); $context = stream_context_create(array( diff --git a/app/Integration/Hipchat.php b/app/Integration/Hipchat.php new file mode 100644 index 00000000..036925f7 --- /dev/null +++ b/app/Integration/Hipchat.php @@ -0,0 +1,53 @@ +<?php + +namespace Integration; + +/** + * Hipchat Webhook + * + * @package integration + * @author Frederic Guillot + */ +class Hipchat extends Base +{ + /** + * Send message to the Hipchat room + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $event_name Event name + * @param array $data Event data + */ + public function notify($project_id, $task_id, $event_name, array $event) + { + $project = $this->project->getbyId($project_id); + + $event['event_name'] = $event_name; + $event['author'] = $this->user->getFullname($this->session['user']); + + $html = '<img src="http://kanboard.net/assets/img/favicon-32x32.png"/>'; + $html .= '<strong>'.$project['name'].'</strong><br/>'; + $html .= $this->projectActivity->getTitle($event); + + if ($this->config->get('application_url')) { + $html .= '<br/><a href="'.$this->config->get('application_url'); + $html .= $this->helper->u('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id)).'">'; + $html .= t('view the task on Kanboard').'</a>'; + } + + $payload = array( + 'message' => $html, + 'color' => 'yellow', + ); + + $url = sprintf( + '%s/v2/room/%s/notification?auth_token=%s', + $this->config->get('integration_hipchat_api_url'), + $this->config->get('integration_hipchat_room_id'), + $this->config->get('integration_hipchat_room_token') + ); + + $this->httpClient->post($url, $payload); + } +} diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index 9af5dcb8..a13aa3af 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index dda991c7..2043215b 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -1,8 +1,8 @@ <?php return array( - // 'number.decimals_separator' => '', - // 'number.thousands_separator' => '', + 'number.decimals_separator' => ',', + 'number.thousands_separator' => '.', 'None' => 'Keines', 'edit' => 'Bearbeiten', 'Edit' => 'Bearbeiten', @@ -410,13 +410,13 @@ return array( 'Comment updated' => 'Kommentar wurde aktualisiert', 'New comment posted by %s' => 'Neuer Kommentar verfasst durch %s', 'List of due tasks for the project "%s"' => 'Liste der fälligen Aufgaben für das Projekt "%s"', - // 'New attachment' => '', - // 'New comment' => '', - // 'New subtask' => '', - // 'Subtask updated' => '', - // 'Task updated' => '', - // 'Task closed' => '', - // 'Task opened' => '', + 'New attachment' => 'Neuer Anhang', + 'New comment' => 'Neuer Kommentar', + 'New subtask' => 'Neue Teilaufgabe', + 'Subtask updated' => 'Teilaufgabe aktualisiert', + 'Task updated' => 'Aufgabe aktualisiert', + 'Task closed' => 'Aufgabe geschlossen', + 'Task opened' => 'Aufgabe geöffnet', '[%s][Due tasks]' => '[%s][Fällige Aufgaben]', '[Kanboard] Notification' => '[Kanboard] Benachrichtigung', 'I want to receive notifications only for those projects:' => 'Ich möchte nur für diese Projekte Benachrichtigungen erhalten:', @@ -500,9 +500,9 @@ return array( 'Task assignee change' => 'Zuständigkeit geändert', '%s change the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s', '%s changed the assignee of the task %s to %s' => '%s hat die Zuständigkeit der Aufgabe %s geändert um %s', - // 'Column Change' => '', - // 'Position Change' => '', - // 'Assignee Change' => '', + 'Column Change' => 'Spalte ändern', + 'Position Change' => 'Position ändern', + 'Assignee Change' => 'Zuordnung ändern', 'New password for the user "%s"' => 'Neues Passwort des Benutzers "%s"', 'Choose an event' => 'Aktion wählen', 'Github commit received' => 'Github commit empfangen', @@ -736,95 +736,100 @@ return array( 'Filter recently updated' => 'Zuletzt geänderte anzeigen', 'since %B %e, %Y at %k:%M %p' => 'seit %B %e, %Y um %k:%M %p', 'More filters' => 'Mehr Filter', - // 'Compact view' => '', - // 'Horizontal scrolling' => '', - // 'Compact/wide view' => '', - // 'No results match:' => '', - // 'Remove hourly rate' => '', - // 'Do you really want to remove this hourly rate?' => '', - // 'Hourly rates' => '', - // 'Hourly rate' => '', - // 'Currency' => '', - // 'Effective date' => '', - // 'Add new rate' => '', - // 'Rate removed successfully.' => '', - // 'Unable to remove this rate.' => '', - // 'Unable to save the hourly rate.' => '', - // 'Hourly rate created successfully.' => '', - // 'Start time' => '', - // 'End time' => '', - // 'Comment' => '', - // 'All day' => '', - // 'Day' => '', - // 'Manage timetable' => '', - // 'Overtime timetable' => '', - // 'Time off timetable' => '', - // 'Timetable' => '', - // 'Work timetable' => '', - // 'Week timetable' => '', - // 'Day timetable' => '', - // 'From' => '', - // 'To' => '', - // 'Time slot created successfully.' => '', - // 'Unable to save this time slot.' => '', - // 'Time slot removed successfully.' => '', - // 'Unable to remove this time slot.' => '', - // 'Do you really want to remove this time slot?' => '', - // 'Remove time slot' => '', - // 'Add new time slot' => '', - // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', - // 'Files' => '', - // 'Images' => '', - // 'Private project' => '', - // 'Amount' => '', + 'Compact view' => 'Kompaktansicht', + 'Horizontal scrolling' => 'Horizontales Scrollen', + 'Compact/wide view' => 'Kompakt/Breite-Ansicht', + 'No results match:' => 'Keine Ergebnisse:', + 'Remove hourly rate' => 'Stundensatz entfernen', + 'Do you really want to remove this hourly rate?' => 'Diesen Stundensatz wirklich entfernen?', + 'Hourly rates' => 'Stundensätze', + 'Hourly rate' => 'Stundensatz', + 'Currency' => 'Währung', + 'Effective date' => 'Inkraftsetzung', + 'Add new rate' => 'Neue Rate hinzufügen', + 'Rate removed successfully.' => 'Rate erfolgreich entfernt', + 'Unable to remove this rate.' => 'Nicht in der Lage, diese Rate zu entfernen.', + 'Unable to save the hourly rate.' => 'Nicht in der Lage, diese Rate zu speichern', + 'Hourly rate created successfully.' => 'Stundensatz erfolgreich angelegt.', + 'Start time' => 'Startzeit', + 'End time' => 'Endzeit', + 'Comment' => 'Kommentar', + 'All day' => 'ganztägig', + 'Day' => 'Tag', + 'Manage timetable' => 'Zeitplan verwalten', + 'Overtime timetable' => 'Überstunden Zeitplan', + 'Time off timetable' => 'Freizeit Zeitplan', + 'Timetable' => 'Zeitplan', + 'Work timetable' => 'Arbeitszeitplan', + 'Week timetable' => 'Wochenzeitplan', + 'Day timetable' => 'Tageszeitplan', + 'From' => 'von', + 'To' => 'bis', + 'Time slot created successfully.' => 'Zeitfenster erfolgreich erstellt.', + 'Unable to save this time slot.' => 'Nicht in der Lage, dieses Zeitfenster zu speichern.', + 'Time slot removed successfully.' => 'Zeitfenster erfolgreich entfernt.', + 'Unable to remove this time slot.' => 'Nicht in der Lage, dieses Zeitfenster zu entfernen', + 'Do you really want to remove this time slot?' => 'Soll diese Zeitfenster wirklich gelöscht werden?', + 'Remove time slot' => 'Zeitfenster entfernen', + 'Add new time slot' => 'Neues Zeitfenster hinzufügen', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Dieses Zeitfenster wird verwendet, wenn die Checkbox "gantägig" für Freizeit und Überstunden angeklickt ist.', + 'Files' => 'Dateien', + 'Images' => 'Bilder', + 'Private project' => 'privates Projekt', + 'Amount' => 'Betrag', // 'AUD - Australian Dollar' => '', - // 'Budget' => '', - // 'Budget line' => '', - // 'Budget line removed successfully.' => '', - // 'Budget lines' => '', + 'Budget' => 'Budget', + 'Budget line' => 'Budgetlinie', + 'Budget line removed successfully.' => 'Budgetlinie erfolgreich entfernt', + 'Budget lines' => 'Budgetlinien', // 'CAD - Canadian Dollar' => '', // 'CHF - Swiss Francs' => '', - // 'Cost' => '', - // 'Cost breakdown' => '', - // 'Custom Stylesheet' => '', - // 'download' => '', - // 'Do you really want to remove this budget line?' => '', + 'Cost' => 'Kosten', + 'Cost breakdown' => 'Kostenaufschlüsselung', + 'Custom Stylesheet' => 'benutzerdefiniertes Stylesheet', + 'download' => 'Download', + 'Do you really want to remove this budget line?' => 'Soll diese Budgetlinie wirklich entfernt werden?', // 'EUR - Euro' => '', - // 'Expenses' => '', + 'Expenses' => 'Kosten', // 'GBP - British Pound' => '', // 'INR - Indian Rupee' => '', // 'JPY - Japanese Yen' => '', - // 'New budget line' => '', + 'New budget line' => 'Neue Budgetlinie', // 'NZD - New Zealand Dollar' => '', - // 'Remove a budget line' => '', - // 'Remove budget line' => '', + 'Remove a budget line' => 'Budgetlinie entfernen', + 'Remove budget line' => 'Budgetlinie entfernen', // 'RSD - Serbian dinar' => '', - // 'The budget line have been created successfully.' => '', - // 'Unable to create the budget line.' => '', - // 'Unable to remove this budget line.' => '', + 'The budget line have been created successfully.' => 'Die Budgetlinie wurde erfolgreich angelegt.', + 'Unable to create the budget line.' => 'Budgetlinie konnte nicht erstellt werden.', + 'Unable to remove this budget line.' => 'Budgetlinie konnte nicht gelöscht werden.', // 'USD - US Dollar' => '', - // 'Remaining' => '', - // 'Destination column' => '', - // 'Move the task to another column when assigned to a user' => '', - // 'Move the task to another column when assignee is cleared' => '', - // 'Source column' => '', - // 'Show subtask estimates in the user calendar' => '', - // 'Transitions' => '', - // 'Executer' => '', - // 'Time spent in the column' => '', - // 'Task transitions' => '', - // 'Task transitions export' => '', - // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', - // 'Currency rates' => '', - // 'Rate' => '', - // 'Change reference currency' => '', - // 'Add a new currency rate' => '', - // 'Currency rates are used to calculate project budget.' => '', - // 'Reference currency' => '', - // 'The currency rate have been added successfully.' => '', - // 'Unable to add this currency rate.' => '', - // 'Send notifications to a Slack channel' => '', - // 'Webhook URL' => '', - // 'Help on Slack integration' => '', - // '%s remove the assignee of the task %s' => '', + 'Remaining' => 'Verbleibend', + 'Destination column' => 'Zielspalte', + 'Move the task to another column when assigned to a user' => 'Aufgabe in eine andere Spalte verschieben, wenn ein User zugeordnet wurde.', + 'Move the task to another column when assignee is cleared' => 'Aufgabe in eine andere Spalte verschieben, wenn die Zuordnung gelöscht wurde.', + 'Source column' => 'Quellspalte', + 'Show subtask estimates in the user calendar' => 'Teilaufgabenschätzung in Benutzerkalender anzeigen.', + 'Transitions' => 'Übergänge', + 'Executer' => 'Ausführender', + 'Time spent in the column' => 'Zeit in Spalte verbracht', + 'Task transitions' => 'Aufgaben Übergänge', + 'Task transitions export' => 'Aufgaben Übergänge exportieren', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Diese Auswertung enthält alle Spaltenbewegungen für jede Aufgabe mit Datum, Benutzer und Zeit vor jedem Wechsel.', + 'Currency rates' => 'Währungskurse', + 'Rate' => 'Kurse', + 'Change reference currency' => 'Referenzwährung ändern', + 'Add a new currency rate' => 'Neuen Währungskurs hinzufügen', + 'Currency rates are used to calculate project budget.' => 'Währungskurse werden verwendet um das Projektbudget zu berechnen.', + 'Reference currency' => 'Referenzwährung', + 'The currency rate have been added successfully.' => 'Der Währungskurs wurde erfolgreich hinzugefügt.', + 'Unable to add this currency rate.' => 'Währungskurs konnte nicht hinzugefügt werden', + 'Send notifications to a Slack channel' => 'Benachrichtigung an einen Slack-Kanal senden', + 'Webhook URL' => 'Webhook URL', + 'Help on Slack integration' => 'Hilfe für Slack integration.', + '%s remove the assignee of the task %s' => '%s Zuordnung für die Aufgabe %s entfernen', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index a857493f..33c062c6 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index ae238224..856914ad 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index e2425892..ac21f387 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -825,8 +825,14 @@ return array( 'Reference currency' => 'Devise de référence', 'The currency rate have been added successfully.' => 'Le taux de change a été ajouté avec succès.', 'Unable to add this currency rate.' => 'Impossible d\'ajouter ce taux de change', - 'Send notifications to a Slack channel' => 'Envoyer des notifications sur un channel Slack', + 'Send notifications to a Slack channel' => 'Envoyer les notifications sur un salon de discussion Slack', 'Webhook URL' => 'URL du webhook', 'Help on Slack integration' => 'Aide sur l\'intégration avec Slack', '%s remove the assignee of the task %s' => '%s a enlevé la personne assignée à la tâche %s', + 'Send notifications to Hipchat' => 'Envoyer les notifications vers Hipchat', + 'API URL' => 'URL de l\'api', + 'Room API ID or name' => 'Nom ou identifiant du salon de discussion', + 'Room notification token' => 'Jeton de sécurité du salon de discussion', + 'Help on Hipchat integration' => 'Aide sur l\'intégration avec Hipchat', + 'Enable Gravatar images' => 'Activer les images Gravatar', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index 70ab8898..29792417 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 5924ed0c..e8d4c4d1 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index f7d225af..52d5413d 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 0efbc7f3..b53f9c83 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 97c185c3..42a3dff7 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index bca27142..b18f2143 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index 8b84fc1f..b77960d3 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index a04a31af..0514faa4 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index b5e6a629..dede7364 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index f45b24cb..86a4f79c 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index d46e71a4..eb971798 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index b6be113d..70175e5f 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -827,4 +827,9 @@ return array( // 'Webhook URL' => '', // 'Help on Slack integration' => '', // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Model/Comment.php b/app/Model/Comment.php index a36f2b45..844f0c89 100644 --- a/app/Model/Comment.php +++ b/app/Model/Comment.php @@ -47,7 +47,8 @@ class Comment extends Base self::TABLE.'.user_id', self::TABLE.'.comment', User::TABLE.'.username', - User::TABLE.'.name' + User::TABLE.'.name', + User::TABLE.'.email' ) ->join(User::TABLE, 'id', 'user_id') ->orderBy(self::TABLE.'.date', 'ASC') diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 46d71fc7..05081a47 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -131,18 +131,19 @@ class ProjectActivity extends Base ->columns( self::TABLE.'.*', User::TABLE.'.username AS author_username', - User::TABLE.'.name AS author_name' + User::TABLE.'.name AS author_name', + User::TABLE.'.email' ) ->in('project_id', $project_ids) ->join(User::TABLE, 'id', 'creator_id') ->desc(self::TABLE.'.id') ->limit($limit); - if(!is_null($start)){ + if (!is_null($start)){ $query->gte('date_creation', $start); } - if(!is_null($end)){ + if (!is_null($end)){ $query->lte('date_creation', $end); } diff --git a/app/Model/Subtask.php b/app/Model/Subtask.php index e33373e0..492f3a77 100644 --- a/app/Model/Subtask.php +++ b/app/Model/Subtask.php @@ -98,6 +98,7 @@ class Subtask extends Base Subtask::TABLE.'.*', Task::TABLE.'.project_id', Task::TABLE.'.color_id', + Task::TABLE.'.title AS task_name', Project::TABLE.'.name AS project_name' ) ->eq('user_id', $user_id) diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index dc0b8b42..4ea7b041 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,22 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 58; +const VERSION = 60; + +function version_60($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_gravatar', '0')); +} + +function version_59($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_hipchat', '0')); + $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com')); + $rq->execute(array('integration_hipchat_room_id', '')); + $rq->execute(array('integration_hipchat_room_token', '')); +} function version_58($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index ea7b84d1..93d8e869 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,22 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 39; +const VERSION = 40; + +function version_41($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_gravatar', '0')); +} + +function version_40($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_hipchat', '0')); + $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com')); + $rq->execute(array('integration_hipchat_room_id', '')); + $rq->execute(array('integration_hipchat_room_token', '')); +} function version_39($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 1ffd9405..3683acb6 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,22 @@ use Core\Security; use PDO; use Model\Link; -const VERSION = 57; +const VERSION = 59; + +function version_59($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_gravatar', '0')); +} + +function version_58($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_hipchat', '0')); + $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com')); + $rq->execute(array('integration_hipchat_room_id', '')); + $rq->execute(array('integration_hipchat_room_token', '')); +} function version_57($pdo) { diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 5f6298aa..6a12ea5a 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -75,6 +75,7 @@ class ClassProvider implements ServiceProviderInterface 'GitlabWebhook', 'GithubWebhook', 'BitbucketWebhook', + 'Hipchat', 'SlackWebhook', ) ); diff --git a/app/Subscriber/ProjectActivitySubscriber.php b/app/Subscriber/ProjectActivitySubscriber.php index d2e85166..42314637 100644 --- a/app/Subscriber/ProjectActivitySubscriber.php +++ b/app/Subscriber/ProjectActivitySubscriber.php @@ -42,14 +42,32 @@ class ProjectActivitySubscriber extends Base implements EventSubscriberInterface $values ); - if ($this->config->get('integration_slack_webhook') == 1) { - $this->slackWebhook->notify( - $values['task']['project_id'], - $values['task']['id'], - $event_name, - $values - ); - } + $this->sendSlackNotification($event_name, $values); + $this->sendHipchatNotification($event_name, $values); + } + } + + private function sendSlackNotification($event_name, array $values) + { + if ($this->config->get('integration_slack_webhook') == 1) { + $this->slackWebhook->notify( + $values['task']['project_id'], + $values['task']['id'], + $event_name, + $values + ); + } + } + + private function sendHipchatNotification($event_name, array $values) + { + if ($this->config->get('integration_hipchat') == 1) { + $this->hipchat->notify( + $values['task']['project_id'], + $values['task']['id'], + $event_name, + $values + ); } } diff --git a/app/Template/app/subtasks.php b/app/Template/app/subtasks.php index fdfbdf2f..487b66fc 100644 --- a/app/Template/app/subtasks.php +++ b/app/Template/app/subtasks.php @@ -6,6 +6,7 @@ <tr> <th class="column-10"><?= $paginator->order('Id', 'tasks.id') ?></th> <th class="column-20"><?= $paginator->order(t('Project'), 'project_name') ?></th> + <th><?= $paginator->order(t('Task'), 'task_name') ?></th> <th><?= $paginator->order(t('Subtask'), 'title') ?></th> <th class="column-20"><?= t('Time tracking') ?></th> </tr> @@ -18,6 +19,9 @@ <?= $this->a($this->e($subtask['project_name']), 'board', 'show', array('project_id' => $subtask['project_id'])) ?> </td> <td> + <?= $this->a($this->e($subtask['task_name']), 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?> + </td> + <td> <?= $this->toggleSubtaskStatus($subtask, 'dashboard') ?> </td> <td> diff --git a/app/Template/comment/show.php b/app/Template/comment/show.php index 23389c06..98c29441 100644 --- a/app/Template/comment/show.php +++ b/app/Template/comment/show.php @@ -1,9 +1,11 @@ <div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>"> <p class="comment-title"> + <?php if (! empty($comment['email'])): ?> + <?= $this->avatar($comment['email'], $comment['name'] ?: $comment['username']) ?> + <?php endif ?> <span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @ <span class="comment-date"><?= dt('%B %e, %Y at %k:%M %p', $comment['date']) ?></span> </p> - <div class="comment-inner"> <?php if (! isset($preview)): ?> diff --git a/app/Template/config/integrations.php b/app/Template/config/integrations.php index 104ebc16..e11b62f8 100644 --- a/app/Template/config/integrations.php +++ b/app/Template/config/integrations.php @@ -6,6 +6,27 @@ <?= $this->formCsrf() ?> + <h3><?= t('Gravatar') ?></h3> + <div class="listing"> + <?= $this->formCheckbox('integration_gravatar', t('Enable Gravatar images'), 1, $values['integration_gravatar'] == 1) ?> + </div> + + <h3><img src="assets/img/hipchat-icon.png"/> <?= t('Hipchat') ?></h3> + <div class="listing"> + <?= $this->formCheckbox('integration_hipchat', t('Send notifications to Hipchat'), 1, $values['integration_hipchat'] == 1) ?> + + <?= $this->formLabel(t('API URL'), 'integration_hipchat_api_url') ?> + <?= $this->formText('integration_hipchat_api_url', $values, $errors) ?> + + <?= $this->formLabel(t('Room API ID or name'), 'integration_hipchat_room_id') ?> + <?= $this->formText('integration_hipchat_room_id', $values, $errors) ?> + + <?= $this->formLabel(t('Room notification token'), 'integration_hipchat_room_token') ?> + <?= $this->formText('integration_hipchat_room_token', $values, $errors) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/hipchat" target="_blank"><?= t('Help on Hipchat integration') ?></a></p> + </div> + <h3><i class="fa fa-slack fa-fw"></i> <?= t('Slack') ?></h3> <div class="listing"> <?= $this->formCheckbox('integration_slack_webhook', t('Send notifications to a Slack channel'), 1, $values['integration_slack_webhook'] == 1) ?> diff --git a/app/Template/event/comment_create.php b/app/Template/event/comment_create.php index fd046fd1..79238aba 100644 --- a/app/Template/event/comment_create.php +++ b/app/Template/event/comment_create.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?= e('%s commented the task %s', $this->e($author), diff --git a/app/Template/event/comment_update.php b/app/Template/event/comment_update.php index 7149bacf..5d22a2ce 100644 --- a/app/Template/event/comment_update.php +++ b/app/Template/event/comment_update.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?= e('%s updated a comment on the task %s', $this->e($author), diff --git a/app/Template/event/subtask_create.php b/app/Template/event/subtask_create.php index 1cc7585c..4f33069a 100644 --- a/app/Template/event/subtask_create.php +++ b/app/Template/event/subtask_create.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?= e('%s created a subtask for the task %s', $this->e($author), diff --git a/app/Template/event/subtask_update.php b/app/Template/event/subtask_update.php index be06f7f5..19fe2e56 100644 --- a/app/Template/event/subtask_update.php +++ b/app/Template/event/subtask_update.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?= e('%s updated a subtask for the task %s', $this->e($author), diff --git a/app/Template/event/task_assignee_change.php b/app/Template/event/task_assignee_change.php index 22ed936b..38e2bca7 100644 --- a/app/Template/event/task_assignee_change.php +++ b/app/Template/event/task_assignee_change.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?php $assignee = $task['assignee_name'] ?: $task['assignee_username'] ?> diff --git a/app/Template/event/task_close.php b/app/Template/event/task_close.php index b5ad4d1d..afedbef3 100644 --- a/app/Template/event/task_close.php +++ b/app/Template/event/task_close.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?= e('%s closed the task %s', $this->e($author), diff --git a/app/Template/event/task_create.php b/app/Template/event/task_create.php index de9a7e0d..4b920234 100644 --- a/app/Template/event/task_create.php +++ b/app/Template/event/task_create.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?= e('%s created the task %s', $this->e($author), diff --git a/app/Template/event/task_move_column.php b/app/Template/event/task_move_column.php index e56e92d7..e97a3ab7 100644 --- a/app/Template/event/task_move_column.php +++ b/app/Template/event/task_move_column.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?= e('%s moved the task %s to the column "%s"', $this->e($author), diff --git a/app/Template/event/task_move_position.php b/app/Template/event/task_move_position.php index 412a9401..2ed4ffe8 100644 --- a/app/Template/event/task_move_position.php +++ b/app/Template/event/task_move_position.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?= e('%s moved the task %s to the position #%d in the column "%s"', $this->e($author), diff --git a/app/Template/event/task_open.php b/app/Template/event/task_open.php index 353f8dac..9a408449 100644 --- a/app/Template/event/task_open.php +++ b/app/Template/event/task_open.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?= e('%s opened the task %s', $this->e($author), diff --git a/app/Template/event/task_update.php b/app/Template/event/task_update.php index 24b17446..0f81870b 100644 --- a/app/Template/event/task_update.php +++ b/app/Template/event/task_update.php @@ -1,3 +1,5 @@ +<?= $this->avatar($email, $author) ?> + <p class="activity-title"> <?= e('%s updated the task %s', $this->e($author), diff --git a/app/Template/project/integrations.php b/app/Template/project/integrations.php index 194bd672..4f6553ad 100644 --- a/app/Template/project/integrations.php +++ b/app/Template/project/integrations.php @@ -8,7 +8,7 @@ <p class="form-help"><a href="http://kanboard.net/documentation/github-webhooks" target="_blank"><?= t('Help on Github webhooks') ?></a></p> </div> -<h3><i class="fa fa-git fa-fw"></i> <?= t('Gitlab webhooks') ?></h3> +<h3><img src="assets/img/gitlab-icon.png"/> <?= t('Gitlab webhooks') ?></h3> <div class="listing"> <input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'gitlab', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> <p class="form-help"><a href="http://kanboard.net/documentation/gitlab-webhooks" target="_blank"><?= t('Help on Gitlab webhooks') ?></a></p> diff --git a/app/constants.php b/app/constants.php index 82d26f2c..08759cbd 100644 --- a/app/constants.php +++ b/app/constants.php @@ -74,3 +74,5 @@ defined('ENABLE_XFRAME') or define('ENABLE_XFRAME', true); // Default files directory defined('FILES_DIR') or define('FILES_DIR', 'data/files/'); +// Escape html inside markdown text +defined('MARKDOWN_ESCAPE_HTML') or define('MARKDOWN_ESCAPE_HTML', true); diff --git a/assets/css/app.css b/assets/css/app.css index 79d61c6b..e3797b1d 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -62,7 +62,11 @@ hr { .chosen-select { min-height: 27px; /* Reserve some space to avoid re-layout due to chosen */ } -/* links */ + +.avatar { + float: left; + margin-right: 10px; +}/* links */ a { color: #3366CC; border: none; diff --git a/assets/css/src/base.css b/assets/css/src/base.css index d92df612..10a3ee8e 100644 --- a/assets/css/src/base.css +++ b/assets/css/src/base.css @@ -46,3 +46,8 @@ hr { .chosen-select { min-height: 27px; /* Reserve some space to avoid re-layout due to chosen */ } + +.avatar { + float: left; + margin-right: 10px; +}
\ No newline at end of file diff --git a/assets/img/gitlab-icon.png b/assets/img/gitlab-icon.png Binary files differnew file mode 100644 index 00000000..7e1eaa5c --- /dev/null +++ b/assets/img/gitlab-icon.png diff --git a/assets/img/hipchat-icon.png b/assets/img/hipchat-icon.png Binary files differnew file mode 100644 index 00000000..1b0a825f --- /dev/null +++ b/assets/img/hipchat-icon.png diff --git a/composer.json b/composer.json index 0ab8f511..8e78ab39 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "require" : { + "php": ">=5.3", "ext-mbstring" : "*", "fguillot/simple-validator" : "dev-master", "swiftmailer/swiftmailer" : "@stable", @@ -24,4 +25,4 @@ "require-dev" : { "symfony/stopwatch" : "~2.6" } -}
\ No newline at end of file +} diff --git a/config.default.php b/config.default.php index eb9ad1b8..92b61bde 100644 --- a/config.default.php +++ b/config.default.php @@ -1,5 +1,7 @@ <?php +// Rename this file to config.php if you want to change the values + // Enable/Disable debug define('DEBUG', false); @@ -127,3 +129,6 @@ define('ENABLE_HSTS', true); // Enable or disable "X-Frame-Options: DENY" HTTP header define('ENABLE_XFRAME', true); + +// Escape html inside markdown text +define('MARKDOWN_ESCAPE_HTML', true); diff --git a/docs/hipchat.markdown b/docs/hipchat.markdown new file mode 100644 index 00000000..45d93eb2 --- /dev/null +++ b/docs/hipchat.markdown @@ -0,0 +1,31 @@ +Hipchat integration +=================== + +Send notifications to a room +----------------------------- + +Example of notifications: + +![Hipchat notification](http://kanboard.net/screenshots/documentation/hipchat-notification.png) + +This feature use the room notification token system of Hipchat. + +### Hipchat configuration + +![Hipchat room token](http://kanboard.net/screenshots/documentation/hipchat-room-token.png) + +1. Go to to **My account** +2. Click on the tab **Rooms** and select the room you want to send the notifications +3. On the left, choose **Tokens** +4. Enter a label, by example "Kanboard" and save + +### Kanboard configuration + +![Hipchat settings](http://kanboard.net/screenshots/documentation/hipchat-settings.png) + +1. Go to **Settings > Integrations > Hipchat** +2. Replace the API url if you use the self-hosted version of Hipchat +3. Set the room name or the room API ID +4. Copy and paste the token generated previously + +Now, all Kanboard events will be sent to the Hipchat room. diff --git a/docs/slack.markdown b/docs/slack.markdown index 7d7777eb..89e3006b 100644 --- a/docs/slack.markdown +++ b/docs/slack.markdown @@ -10,7 +10,7 @@ Example of notifications: This feature use the [Incoming webhook](https://api.slack.com/incoming-webhooks) system of Slack. -### Configure Slack +### Slack configuration ![Slack webhook creation](http://kanboard.net/screenshots/documentation/slack-add-incoming-webhook.png) @@ -18,4 +18,4 @@ This feature use the [Incoming webhook](https://api.slack.com/incoming-webhooks) 2. On the list of services, scroll-down and choose **DIY Integrations & Customizations > Incoming WebHooks** 3. Copy the webhook url to the Kanboard settings page: **Settings > Integrations > Slack** - +Now, all Kanboard events will be sent to the Slack channel. |