diff --git a/Slack.php b/Slack.php index b35d80b..a428055 100644 --- a/Slack.php +++ b/Slack.php @@ -58,7 +58,7 @@ public function registerEvents() 'ScheduledReports.sendReport' => 'sendReport', 'Template.reportParametersScheduledReports' => 'templateReportParametersScheduledReports', 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', - 'CustomAlerts.validateReportParameters' => 'validateCustomAlertReportParameters', + 'CustomAlerts.validateReportParameters' => 'validateCustomAlertReportParameters', 'CustomAlerts.sendNewAlerts' => 'sendNewAlerts', ]; } @@ -76,6 +76,16 @@ public function getClientSideTranslationKeys(&$translationKeys) $translationKeys[] = 'Slack_SlackEnterYourSlackChannelIdHelpText'; } + /** + * + * Validates the Schedule Report for Slack reportType + * + * @param $parameters + * @param $reportType + * @return void + * @throws \Piwik\Exception\DI\DependencyException + * @throws \Piwik\Exception\DI\NotFoundException + */ public function validateReportParameters(&$parameters, $reportType) { if (!self::isSlackEvent($reportType)) { @@ -106,8 +116,23 @@ public function validateReportParameters(&$parameters, $reportType) } elseif (empty($parameters[self::SLACK_CHANNEL_ID_PARAMETER])) { throw new \Exception(Piwik::translate('Slack_SlackChannelIdRequiredErrorMessage')); } + $slackChannels = explode(',', (string) $parameters[self::SLACK_CHANNEL_ID_PARAMETER]); + foreach ($slackChannels as $slackChannel) { + if (!ctype_alnum($slackChannel)) { + throw new \Exception(Piwik::translate('Slack_SlackChannelIdInvalidErrorMessage')); + } + } } + /** + * + * Get report metadata for Slack scheduled report + * + * @param $availableReportMetadata + * @param $reportType + * @param $idSite + * @return void + */ public function getReportMetadata(&$availableReportMetadata, $reportType, $idSite) { if (!self::isSlackEvent($reportType)) { @@ -121,11 +146,26 @@ public function getReportMetadata(&$availableReportMetadata, $reportType, $idSit ); } + /** + * + * Adds Slack as a reportType in Schedule Reports + * + * @param $reportTypes + * @return void + */ public function getReportTypes(&$reportTypes) { $reportTypes = array_merge($reportTypes, self::$managedReportTypes); } + /** + * + * Adds allowed reportTypes for Slack, e.g. PDF, CSV and TSV + * + * @param $reportFormats + * @param $reportType + * @return void + */ public function getReportFormats(&$reportFormats, $reportType) { if (self::isSlackEvent($reportType)) { @@ -133,6 +173,14 @@ public function getReportFormats(&$reportFormats, $reportType) } } + /** + * + * Adds report parameter for Slack, e.g. SlackChannelID + * + * @param $availableParameters + * @param $reportType + * @return void + */ public function getReportParameters(&$availableParameters, $reportType) { if (self::isSlackEvent($reportType)) { @@ -140,6 +188,16 @@ public function getReportParameters(&$availableParameters, $reportType) } } + /** + * + * Process the Schedule report for reportType Slack + * + * @param $processedReports + * @param $reportType + * @param $outputType + * @param $report + * @return void + */ public function processReports(&$processedReports, $reportType, $outputType, $report) { if (!self::isSlackEvent($reportType)) { @@ -153,6 +211,17 @@ public function processReports(&$processedReports, $reportType, $outputType, $re ); } + /** + * + * Sets the rendered instance based on reportFormat for Slack + * + * @param $reportRenderer + * @param $reportType + * @param $outputType + * @param $report + * @return void + * @throws \Exception + */ public function getRendererInstance(&$reportRenderer, $reportType, $outputType, $report) { if (!self::isSlackEvent($reportType)) { @@ -164,6 +233,14 @@ public function getRendererInstance(&$reportRenderer, $reportType, $outputType, $reportRenderer = ReportRenderer::factory($reportFormat); } + /** + * + * To allow multiple reports in a single file + * + * @param $allowMultipleReports + * @param $reportType + * @return void + */ public function allowMultipleReports(&$allowMultipleReports, $reportType) { if (self::isSlackEvent($reportType)) { @@ -171,6 +248,15 @@ public function allowMultipleReports(&$allowMultipleReports, $reportType) } } + /** + * + * Displays the recipients in the list of Schedule Reports + * + * @param $recipients + * @param $reportType + * @param $report + * @return void + */ public function getReportRecipients(&$recipients, $reportType, $report) { if (!self::isSlackEvent($reportType) || empty($report['parameters'][self::SLACK_CHANNEL_ID_PARAMETER])) { @@ -181,6 +267,9 @@ public function getReportRecipients(&$recipients, $reportType, $report) } /** + * + * Code to send a Schedule Report via Slack + * * @param $reportType * @param $report * @param $contents @@ -189,8 +278,11 @@ public function getReportRecipients(&$recipients, $reportType, $report) * @param $reportSubject * @param $reportTitle * @param $additionalFiles - * @param Period|null $period + * @param $period * @param $force + * @return void + * @throws \Piwik\Exception\DI\DependencyException + * @throws \Piwik\Exception\DI\NotFoundException */ public function sendReport( $reportType, @@ -232,6 +324,16 @@ public function sendReport( $scheduleReportSlack->send(); } + /** + * + * Add the view template for Slack report parameters + * + * @param $out + * @param $context + * @return void + * @throws \Piwik\Exception\DI\DependencyException + * @throws \Piwik\Exception\DI\NotFoundException + */ public function templateReportParametersScheduledReports(&$out, $context = '') { if (Piwik::isUserIsAnonymous()) { @@ -277,17 +379,42 @@ public function uninstall() return; } + /** + * + * Validation check for CustomAlert report parameters + * + * @param $parameters + * @param $alertMedium + * @return void + * @throws \Exception + */ public function validateCustomAlertReportParameters($parameters, $alertMedium) { - if ($alertMedium === self::SLACK_TYPE && empty($parameters[self::SLACK_CHANNEL_ID_PARAMETER])) { - throw new \Exception(Piwik::translate('Slack_SlackChannelIdRequiredErrorMessage')); + if ($alertMedium === self::SLACK_TYPE) { + $settings = StaticContainer::get(SystemSettings::class); + if (empty($settings->slackOauthToken->getValue())) { + throw new \Exception(Piwik::translate('Slack_OauthTokenRequiredErrorMessage')); + } elseif (empty($parameters[self::SLACK_CHANNEL_ID_PARAMETER])) { + throw new \Exception(Piwik::translate('Slack_SlackChannelIdRequiredErrorMessage')); + } elseif (!ctype_alnum($parameters[self::SLACK_CHANNEL_ID_PARAMETER])) { + throw new \Exception(Piwik::translate('Slack_SlackChannelIdInvalidErrorMessage')); + } } } + /** + * + * Code to send CustomAlerts via Slack + * + * @param $triggeredAlerts + * @return void + * @throws \Piwik\Exception\DI\DependencyException + * @throws \Piwik\Exception\DI\NotFoundException + */ public function sendNewAlerts($triggeredAlerts): void { if (!empty($triggeredAlerts)) { - $enrichTriggerAlerts = new EnrichTriggeredAlerts(); + $enrichTriggerAlerts = StaticContainer::get(EnrichTriggeredAlerts::class); $triggeredAlerts = $enrichTriggerAlerts->enrichTriggeredAlerts($triggeredAlerts); $settings = StaticContainer::get(SystemSettings::class); $token = $settings->slackOauthToken->getValue(); @@ -305,6 +432,13 @@ public function sendNewAlerts($triggeredAlerts): void } } + /** + * + * Group alerts by slackChannelID to reduce number of network calls for multiple alerts + * + * @param array $alerts + * @return array + */ private function groupAlertsByChannelId(array $alerts): array { $groupedAlerts = []; @@ -321,11 +455,27 @@ private function groupAlertsByChannelId(array $alerts): array return $groupedAlerts; } + /** + * + * Returns the alert message to send via Slack + * + * @param array $alert + * @param string $metric + * @param string $reportName + * @return string + */ public function getAlertMessage(array $alert, string $metric, string $reportName): string { return Piwik::translate('Slack_SlackAlertContent', [$alert['name'], $alert['siteName'], $metric, $reportName, $this->transformAlertCondition($alert)]); } + /** + * + * Transform the alert condition to text + * + * @param array $alert + * @return string + */ private function transformAlertCondition(array $alert): string { switch ($alert['metric_condition']) { diff --git a/SlackApi.php b/SlackApi.php index ce7d520..02e6c3c 100644 --- a/SlackApi.php +++ b/SlackApi.php @@ -65,6 +65,14 @@ public function uploadFile(string $subject, string $fileName, string $fileConten return false; } + /** + * + * Get the URL to upload the file + * + * @param string $fileName + * @param int $fileLength + * @return string + */ public function getUploadURLExternal(string $fileName, int $fileLength): string { try { @@ -93,6 +101,14 @@ public function getUploadURLExternal(string $fileName, int $fileLength): string return ''; } + /** + * + * Upload the file contents to the URL received from getUploadURLExternal method + * + * @param string $uploadURL + * @param string $fileContents + * @return bool + */ public function sendFile(string $uploadURL, string $fileContents): bool { try { @@ -111,6 +127,15 @@ public function sendFile(string $uploadURL, string $fileContents): bool return strtolower($response) === ('ok - ' . strlen($fileContents)); } + + /** + * + * Post the uploaded file to a channel + * + * @param string $channel + * @param string $subject + * @return bool + */ public function completeUploadExternal(string $channel, string $subject): bool { try { @@ -135,8 +160,21 @@ public function completeUploadExternal(string $channel, string $subject): bool return !empty($data['ok']); } + /** + * + * Send a text message to a Slack channel + * + * @param string $message + * @param string $channel + * @return bool + */ public function sendMessage(string $message, string $channel): bool { + if (empty($message) || empty($channel)) { + $this->logger->debug('Empty message or channel for sending message'); + return false; + } + try { $response = $this->sendHttpRequest( self::SLACK_POST_MESSAGE_URL, @@ -158,6 +196,18 @@ public function sendMessage(string $message, string $channel): bool return !empty($data['ok']); } + /** + * + * Wrapper to send HTTP request + * + * @param string $url + * @param int $timeout + * @param array $requestBody + * @param array $additionalHeaders + * @param $requestBodyAsString + * @return array|bool|int[]|string + * @throws \Exception + */ public function sendHttpRequest(string $url, int $timeout, array $requestBody, array $additionalHeaders = [], $requestBodyAsString = false) { if ($requestBodyAsString && !empty($requestBody[0])) { diff --git a/lang/en.json b/lang/en.json index f3e19b0..5d1c6dc 100644 --- a/lang/en.json +++ b/lang/en.json @@ -7,6 +7,7 @@ "OauthTokenSettingDescription": "Enter your Slack OAuth Token generated from your Slack. %1$sLearn more%2$s.", "PleaseFindYourReport": "Here is your %1$s report for %2$s", "SlackChannelIdRequiredErrorMessage": "Slack Channel ID cannot be empty.", + "SlackChannelIdInvalidErrorMessage": "Invalid Slack Channel ID, only alphanumeric values are allowed.", "SlackChannel": "Slack Channel", "SlackEnterYourSlackChannelIdHelpText": "Enter the Slack Channel ID of the channel that will receive these reports. To find the ID, go to Slack and open the channel details > About tab. %1$sLearn more%2$s", "SlackAlertContent": "%1$s has been triggered for website %2$s as the metric %3$s in report %4$s %5$s." diff --git a/tests/Integration/CustomAlertsApiTest.php b/tests/Integration/CustomAlertsApiTest.php index 19acdec..312c72f 100644 --- a/tests/Integration/CustomAlertsApiTest.php +++ b/tests/Integration/CustomAlertsApiTest.php @@ -9,6 +9,8 @@ namespace Piwik\Plugins\Slack\tests; +use Piwik\Container\StaticContainer; +use Piwik\Plugins\Slack\SystemSettings; use Piwik\Tests\Framework\Fixture; use Piwik\Tests\Framework\TestCase\IntegrationTestCase; use Piwik\Tests\Framework\TestingEnvironmentManipulator; @@ -43,6 +45,20 @@ public function setUp(): void $this->idSite = Fixture::createWebsite('2012-08-09 11:22:33'); } + public function testAddAlertShouldThrowExceptionIfSlackOauthTokenNotSet() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_OauthTokenRequiredErrorMessage'); + $this->addAlert('', false); + } + + public function testAddAlertShouldThrowExceptionIfSlackChannelIdNotEmptyButOauthTokenNotSet() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_OauthTokenRequiredErrorMessage'); + $this->addAlert('channelID', false); + } + public function testAddAlertShouldThrowExceptionIfEmptySlackChannelId() { $this->expectException(\Exception::class); @@ -50,6 +66,20 @@ public function testAddAlertShouldThrowExceptionIfEmptySlackChannelId() $this->addAlert(); } + public function testAddAlertShouldThrowExceptionIfInvalidSlackChannelId() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_SlackChannelIdInvalidErrorMessage'); + $this->addAlert('ChanneldId1@123'); + } + + public function testAddAlertShouldThrowExceptionIfInvalidSlackChannelId2() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_SlackChannelIdInvalidErrorMessage'); + $this->addAlert('ChannelID1,ChannelID2'); + } + public function testAddAlertSuccess() { $id = $this->addAlert($slackChannelId = 'channelID'); @@ -76,8 +106,13 @@ public function testUpdateAlertSuccess() $this->assertEquals('channelIDNew', $alert['slack_channel_id']); } - private function addAlert($slackChannelId = '') + private function addAlert($slackChannelId = '', $createToken = true) { + if ($createToken) { + $settings = StaticContainer::get(SystemSettings::class); + $settings->slackOauthToken->setValue('test123'); + } + return $this->api->addAlert( 'Test Slack and Email', $this->idSite, diff --git a/tests/Integration/SlackTest.php b/tests/Integration/SlackTest.php index 95258c3..c949d43 100644 --- a/tests/Integration/SlackTest.php +++ b/tests/Integration/SlackTest.php @@ -10,6 +10,7 @@ namespace Piwik\Plugins\Slack\tests; use Piwik\Container\StaticContainer; +use Piwik\Piwik; use Piwik\Plugins\ScheduledReports\API; use Piwik\Plugins\ScheduledReports\ScheduledReports; use Piwik\Plugins\Slack\Slack; @@ -113,6 +114,72 @@ public function getAlertData() ]; } + public function testValidateReportParametersSlackTokenNotSetShouldThrowOauthException() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_OauthTokenRequiredErrorMessage'); + $parameters = [ScheduledReports::DISPLAY_FORMAT_PARAMETER => ScheduledReports::DISPLAY_FORMAT_GRAPHS_ONLY]; + Piwik::postEvent('ScheduledReports.validateReportParameters', [&$parameters, 'slack']); + } + + public function testValidateReportParametersSlackTokenNotSetShouldThrowSlackChannelIdEmptyException() + { + $settings = StaticContainer::get(SystemSettings::class); + $settings->slackOauthToken->setValue('test123'); + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_SlackChannelIdRequiredErrorMessage'); + $parameters = [ScheduledReports::DISPLAY_FORMAT_PARAMETER => ScheduledReports::DISPLAY_FORMAT_GRAPHS_ONLY]; + Piwik::postEvent('ScheduledReports.validateReportParameters', [&$parameters, 'slack']); + } + + public function testValidateReportParametersSlackTokenNotSetShouldThrowSlackChannelIdEmptyException2() + { + $settings = StaticContainer::get(SystemSettings::class); + $settings->slackOauthToken->setValue('test123'); + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_SlackChannelIdRequiredErrorMessage'); + $parameters = [ScheduledReports::DISPLAY_FORMAT_PARAMETER => ScheduledReports::DISPLAY_FORMAT_GRAPHS_ONLY, Slack::SLACK_CHANNEL_ID_PARAMETER => '']; + Piwik::postEvent('ScheduledReports.validateReportParameters', [&$parameters, 'slack']); + } + + public function testValidateReportParametersSlackTokenNotSetShouldThrowSlackChannelIdInvalidException() + { + $settings = StaticContainer::get(SystemSettings::class); + $settings->slackOauthToken->setValue('test123'); + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_SlackChannelIdInvalidErrorMessage'); + $parameters = [ScheduledReports::DISPLAY_FORMAT_PARAMETER => ScheduledReports::DISPLAY_FORMAT_GRAPHS_ONLY, Slack::SLACK_CHANNEL_ID_PARAMETER => 'test123@11']; + Piwik::postEvent('ScheduledReports.validateReportParameters', [&$parameters, 'slack']); + } + + public function testValidateReportParametersSlackTokenNotSetShouldThrowSlackChannelIdInvalidException2() + { + $settings = StaticContainer::get(SystemSettings::class); + $settings->slackOauthToken->setValue('test123'); + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Slack_SlackChannelIdInvalidErrorMessage'); + $parameters = [ScheduledReports::DISPLAY_FORMAT_PARAMETER => ScheduledReports::DISPLAY_FORMAT_GRAPHS_ONLY, Slack::SLACK_CHANNEL_ID_PARAMETER => 'test123,Demo@11']; + Piwik::postEvent('ScheduledReports.validateReportParameters', [&$parameters, 'slack']); + } + + public function testValidateReportParametersSlackTokenShouldNotThrowAnyException() + { + $settings = StaticContainer::get(SystemSettings::class); + $settings->slackOauthToken->setValue('test123'); + $parameters = [ScheduledReports::DISPLAY_FORMAT_PARAMETER => ScheduledReports::DISPLAY_FORMAT_GRAPHS_ONLY, Slack::SLACK_CHANNEL_ID_PARAMETER => 'test123']; + Piwik::postEvent('ScheduledReports.validateReportParameters', [&$parameters, 'slack']); + $this->assertNotEmpty($parameters); + } + + public function testValidateReportParametersSlackTokenShouldNotThrowAnyException2() + { + $settings = StaticContainer::get(SystemSettings::class); + $settings->slackOauthToken->setValue('test123'); + $parameters = [ScheduledReports::DISPLAY_FORMAT_PARAMETER => ScheduledReports::DISPLAY_FORMAT_GRAPHS_ONLY, Slack::SLACK_CHANNEL_ID_PARAMETER => 'test123,dEmoA,abcd123']; + Piwik::postEvent('ScheduledReports.validateReportParameters', [&$parameters, 'slack']); + $this->assertNotEmpty($parameters); + } + private function assertHasReport($login, $idSite) { $report = $this->getReport($login, $idSite);