MDL-65983 airnotifier: Configuration check report
authorJuan Leyva <juanleyvadelgado@gmail.com>
Mon, 16 Nov 2020 15:01:40 +0000 (16:01 +0100)
committerJuan Leyva <juanleyvadelgado@gmail.com>
Mon, 12 Apr 2021 09:23:06 +0000 (11:23 +0200)
message/output/airnotifier/checkconfiguration.php [new file with mode: 0644]
message/output/airnotifier/classes/manager.php
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/airnotifier/settings.php
message/output/airnotifier/tests/manager_test.php [new file with mode: 0644]

diff --git a/message/output/airnotifier/checkconfiguration.php b/message/output/airnotifier/checkconfiguration.php
new file mode 100644 (file)
index 0000000..4aa8d34
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Check and test Push notifications configuration.
+ *
+ * @package    message_airnotifier
+ * @copyright  2020 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require('../../../config.php');
+require_once($CFG->libdir . '/filelib.php');
+
+$pageurl = new moodle_url('/message/output/airnotifier/checkconfiguration.php');
+$PAGE->set_url($pageurl);
+$PAGE->set_context(context_system::instance());
+
+require_login();
+require_capability('moodle/site:config', context_system::instance());
+
+// Build a path.
+$strheading = get_string('checkconfiguration', 'message_airnotifier');
+$PAGE->navbar->add(get_string('administrationsite'));
+$returl = new moodle_url('/admin/category.php', ['category' => 'messaging']);
+$PAGE->navbar->add(get_string('messagingcategory', 'admin'), $returl);
+$returl = new moodle_url('/admin/settings.php', ['section' => 'messagesettingairnotifier']);
+$PAGE->navbar->add(get_string('pluginname', 'message_airnotifier'), $returl);
+$PAGE->navbar->add($strheading);
+
+$PAGE->set_heading($SITE->fullname);
+$PAGE->set_title($strheading);
+
+$manager = new message_airnotifier_manager();
+
+// Sending a test Push notification.
+if (data_submitted()) {
+    require_sesskey();
+
+    if (optional_param('confirm', 0, PARAM_INT)) {
+        $manager->send_test_notification($USER);
+
+        redirect($pageurl, get_string('eventnotificationsent', 'message'), 5);
+    } else {
+
+        if (!$manager->has_enabled_devices($CFG->airnotifiermobileappname)) {
+            // The user has not connected to the site with the app yet.
+            redirect($pageurl, get_string('nodevices', 'message_airnotifier'), 5, \core\output\notification::NOTIFY_ERROR);
+        }
+
+        echo $OUTPUT->header();
+        $message = get_string('sendtestconfirmation', 'message_airnotifier');
+        $confirmurl = new moodle_url($pageurl->out(false), ['confirm' => 1]);
+        $continueb = new single_button($confirmurl, get_string('continue'), 'post');
+        $cancelb = new single_button($pageurl, get_string('cancel'), 'get');
+        echo $OUTPUT->confirm($message, $continueb, $cancelb);
+        echo $OUTPUT->footer();
+    }
+    die;
+}
+
+$checkresults = $manager->check_configuration();
+
+$table = new \html_table();
+$table->data = [];
+$table->head  = [
+    get_string('status'),
+    get_string('check'),
+    get_string('summary'),
+];
+$table->colclasses = [
+    'rightalign status',
+    'leftalign check',
+    'leftalign summary',
+];
+$table->id = 'message_airnotifier_checkconfiguration';
+$table->attributes = ['class' => 'admintable generaltable'];
+$table->data = [];
+
+$senddisabled = false;
+foreach ($checkresults as $result) {
+    if ($result->get_status() == core\check\result::CRITICAL || $result->get_status() == core\check\result::ERROR) {
+        $senddisabled = true;
+    }
+    $table->data[] = [$OUTPUT->check_result($result), $result->get_summary(), $result->get_details()];
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading($strheading);
+
+// Check table.
+echo \html_writer::table($table);
+
+// Test notification button.
+$button = $OUTPUT->single_button($PAGE->url, get_string('sendtest', 'message_airnotifier'), 'post', ['disabled' => $senddisabled]);
+echo $OUTPUT->box($button, 'clearfix mdl-align');
+
+echo $OUTPUT->footer();
index f75ee85..8cf0e8e 100644 (file)
@@ -217,4 +217,187 @@ class message_airnotifier_manager {
         return $DB->update_record('message_airnotifier_devices', $device);
     }
 
+    /**
+     * Check the system configuration to detect possible issues.
+     *
+     * @return array result checks
+     */
+    public function check_configuration(): array {
+        global $CFG, $DB;
+
+        $results = [];
+        // Check Mobile services enabled.
+        $summary = html_writer::link((new moodle_url('/admin/settings.php', ['section' => 'mobilesettings'])),
+                get_string('enablemobilewebservice', 'admin'));
+        if (empty($CFG->enablewebservices) || empty($CFG->enablemobilewebservice)) {
+            $results[] = new core\check\result(core\check\result::CRITICAL, $summary, get_string('enablewsdescription', 'webservice'));
+        } else {
+            $results[] = new core\check\result(core\check\result::OK, $summary, get_string('enabled', 'admin'));
+        }
+
+        // Check Mobile notifications enabled.
+        require_once($CFG->dirroot . '/message/lib.php');
+        $processors = get_message_processors();
+        $enabled = false;
+        foreach ($processors as $processor => $status) {
+            if ($processor == 'airnotifier' && $status->enabled) {
+                $enabled = true;
+            }
+        }
+
+        $summary = html_writer::link((new moodle_url('/admin/message.php')), get_string('enableprocessor', 'message_airnotifier'));
+        if ($enabled) {
+            $results[] = new core\check\result(core\check\result::OK, $summary, get_string('enabled', 'admin'));
+        } else {
+            $results[] = new core\check\result(core\check\result::CRITICAL, $summary,
+                get_string('mobilenotificationsdisabledwarning', 'tool_mobile'));
+        }
+
+        // Check Mobile notifications configuration is ok.
+        $summary = html_writer::link((new moodle_url('/admin/settings.php', ['section' => 'messagesettingairnotifier'])),
+            get_string('notificationsserverconfiguration', 'message_airnotifier'));
+        if ($this->is_system_configured()) {
+            $results[] = new core\check\result(core\check\result::OK, $summary, get_string('configured', 'message_airnotifier'));
+        } else {
+            $results[] = new core\check\result(core\check\result::ERROR, $summary, get_string('notconfigured', 'message_airnotifier'));
+        }
+
+        // Check settings properly formatted. Only display in case of errors.
+        $settingstocheck = ['airnotifierappname', 'airnotifiermobileappname'];
+        if ($this->is_system_configured()) {
+            foreach ($settingstocheck as $setting) {
+                if ($CFG->$setting != trim($CFG->$setting)) {
+                    $summary = html_writer::link((new moodle_url('/admin/settings.php', ['section' => 'messagesettingairnotifier'])),
+                        get_string('notificationsserverconfiguration', 'message_airnotifier'));
+
+                    $results[] = new core\check\result(core\check\result::ERROR, $summary,
+                        get_string('airnotifierfielderror', 'message_airnotifier', get_string($setting, 'message_airnotifier')));
+                }
+            }
+        }
+
+        // Check connectivity with Airnotifier.
+        $url = $CFG->airnotifierurl . ':' . $CFG->airnotifierport;
+        $curl = new \curl();
+        $curl->setopt(['CURLOPT_TIMEOUT' => 5, 'CURLOPT_CONNECTTIMEOUT' => 5]);
+        $curl->get($url);
+        $info = $curl->get_info();
+
+        $summary = html_writer::link($url, get_string('airnotifierurl', 'message_airnotifier'));
+        if (!empty($info['http_code']) && ($info['http_code'] == 200 || $info['http_code'] == 302)) {
+            $results[] = new core\check\result(core\check\result::OK, $summary, get_string('online', 'message'));
+        } else {
+            $details = get_string('serverconnectivityerror', 'message_airnotifier', $url);
+            $curlerrno = $curl->get_errno();
+            if (!empty($curlerrno)) {
+                $details .= ' CURL ERROR: ' . $curlerrno . ' - ' . $curl->error;
+            }
+            $results[] = new core\check\result(core\check\result::ERROR, $summary, $details);
+        }
+
+        // Check access key by trying to create an invalid token.
+        $settingsurl = new moodle_url('/admin/settings.php', ['section' => 'messagesettingairnotifier']);
+        $summary = html_writer::link($settingsurl, get_string('airnotifieraccesskey', 'message_airnotifier'));
+        if (!empty($CFG->airnotifieraccesskey)) {
+            $url = $CFG->airnotifierurl . ':' . $CFG->airnotifierport . '/tokens/testtoken';
+            $header = ['Accept: application/json', 'X-AN-APP-NAME: ' . $CFG->airnotifierappname,
+                'X-AN-APP-KEY: ' . $CFG->airnotifieraccesskey];
+            $curl->setHeader($header);
+            $response = $curl->post($url);
+            $info = $curl->get_info();
+
+            if ($curlerrno = $curl->get_errno()) {
+                $details = get_string('serverconnectivityerror', 'message_airnotifier', $url);
+                $details .= ' CURL ERROR: ' . $curlerrno . ' - ' . $curl->error;
+                $results[] = new core\check\result(core\check\result::ERROR, $summary, $details);
+            } else if (!empty($info['http_code']) && $info['http_code'] == 400 && $key = json_decode($response, true)) {
+                if ($key['error'] == 'Invalid access key') {
+                    $results[] = new core\check\result(core\check\result::ERROR, $summary, $key['error']);
+                } else {
+                    $results[] = new core\check\result(core\check\result::OK, $summary, get_string('enabled', 'admin'));
+                }
+            }
+        } else {
+            $results[] = new core\check\result(core\check\result::ERROR, $summary,
+                get_string('requestaccesskey', 'message_airnotifier'));
+        }
+
+        // Check default preferences.
+        $preferences = (array) get_message_output_default_preferences();
+        $providerscount = 0;
+        $providersconfigured = 0;
+        foreach ($preferences as $prefname => $prefval) {
+            if (strpos($prefname, 'message_provider') === 0) {
+                $providerscount++;
+                if (strpos($prefval, 'airnotifier') !== false) {
+                    $providersconfigured++;
+                }
+            }
+        }
+
+        $summary = html_writer::link((new moodle_url('/admin/message.php')), get_string('managemessageoutputs', 'message'));
+        if ($providersconfigured == 0) {
+            $results[] = new core\check\result(core\check\result::ERROR, $summary,
+                get_string('messageprovidersempty', 'message_airnotifier'));
+        } else if ($providersconfigured / $providerscount < 0.25) {
+            // Less than a 25% of the providers are enabled by default for users.
+            $results[] = new core\check\result(core\check\result::WARNING, $summary,
+                get_string('messageproviderslow', 'message_airnotifier'));
+        } else {
+            $results[] = new core\check\result(core\check\result::OK, $summary, get_string('configured', 'message_airnotifier'));
+        }
+
+        // Check user devices from last month.
+        $recentdevicescount = $DB->count_records_select('user_devices', 'appid = ? AND timemodified > ?',
+            [$CFG->airnotifiermobileappname, time() - (WEEKSECS * 4)]);
+
+        $summary = get_string('userdevices', 'message_airnotifier');
+        if (!empty($recentdevicescount)) {
+            $results[] = new core\check\result(core\check\result::OK, $summary, get_string('configured', 'message_airnotifier'));
+        } else {
+            $results[] = new core\check\result(core\check\result::ERROR, $summary, get_string('nodevices', 'message_airnotifier'));
+        }
+        return $results;
+    }
+
+    /**
+     * Send a test notification to the given user.
+     *
+     * @param  stdClass $user user object
+     */
+    public function send_test_notification(stdClass $user): void {
+        global $CFG;
+        require_once($CFG->dirroot . '/message/output/airnotifier/message_output_airnotifier.php');
+
+        $data = new stdClass;
+        $data->userto = clone $user;
+        $data->subject = 'Push Notification Test';
+        $data->fullmessage = 'This is a test message send at: ' . userdate(time());
+        $data->notification = 1;
+
+        // The send_message method always return true, so it does not make sense to return anything.
+        $airnotifier = new message_output_airnotifier();
+        $airnotifier->send_message($data);
+    }
+
+    /**
+     * Check whether the given user has enabled devices or not for the given app.
+     *
+     * @param  string $appname the app to check
+     * @param  int $userid the user to check the devices for (empty for current user)
+     * @return bool true when the user has enabled devices, false otherwise
+     */
+    public function has_enabled_devices(string $appname, int $userid = null): bool {
+        $enableddevices = false;
+        $devices = $this->get_user_devices($appname, $userid);
+
+        foreach ($devices as $device) {
+            if (!$device->enable) {
+                continue;
+            }
+            $enableddevices = true;
+            break;
+        }
+        return $enableddevices;
+    }
 }
index 58fcabc..bb41e73 100644 (file)
 
 $string['airnotifieraccesskey'] = 'Airnotifier access key';
 $string['airnotifierappname'] = 'Airnotifier app name';
+$string['airnotifierfielderror'] = 'Please remove any empty spaces or unnecessary characters from the following field: {$a}';
 $string['airnotifiermobileappname'] = 'Mobile app name';
 $string['airnotifierport'] = 'Airnotifier port';
 $string['airnotifierurl'] = 'Airnotifier URL';
+$string['checkconfiguration'] = 'Check and test push notification configuration';
 $string['configairnotifierurl'] = 'The server URL to connect to for sending push notifications.';
 $string['configairnotifierport'] = 'The port to use when connecting to the airnotifier server.';
 $string['configairnotifieraccesskey'] = 'The access key for connecting to the Airnotifier server. You can obtain an access key by clicking the "Request access key" link below (registered sites only) or by creating an account on the <a href="https://apps.moodle.com">Moodle Apps Portal</a>.';
 $string['configairnotifierappname'] = 'The app name identifier in Airnotifier.';
 $string['configairnotifiermobileappname'] = 'The Mobile app unique identifier (usually something like com.moodle.moodlemobile).';
+$string['configured'] = 'Configured';
 $string['deletecheckdevicename'] = 'Delete your device: {$a->name}';
 $string['deletedevice'] = 'Delete the device. Note that an app can register the device again. If the device keeps reappearing, disable it.';
 $string['devicetoken'] = 'Device token';
 $string['enableprocessor'] = 'Enable mobile notifications';
 $string['errorretrievingkey'] = 'An error occurred while retrieving the access key. Your site must be registered to use this service. If your site is already registered, please try updating your registration. Alternatively, you can obtain an access key by creating an account on the <a href="https://apps.moodle.com">Moodle Apps Portal</a>.';
 $string['keyretrievedsuccessfully'] = 'The access key was retrieved successfully. To access Moodle app usage statistics, please create an account on the <a href="https://apps.moodle.com">Moodle Apps Portal</a>.';
+$string['messageprovidersempty'] = 'There are no mobile notifications enabled in default notification preferences.';
+$string['messageproviderslow'] = 'Only a few mobile notifications are enabled in default notification preferences.';
 $string['moodleappsportallimitswarning'] = 'Please note that the number of user devices allowed to receive notifications depends on your Moodle app subscription. For details, visit the <a href="{$a}" target="_blank">Moodle Apps Portal</a>.';
 $string['nodevices'] = 'No registered devices. Devices will automatically appear after you install the Moodle app and add this site.';
 $string['nopermissiontomanagedevices'] = 'You don\'t have permission to manage devices.';
 $string['notconfigured'] = 'The Airnotifier server has not been configured so push notifications cannot be sent.';
+$string['notificationsserverconfiguration'] = 'Notifications server (Airnotifier) configuration';
 $string['pluginname'] = 'Mobile';
 $string['privacy:appiddescription'] = 'This is an identifier to the application being used.';
 $string['privacy:enableddescription'] = 'If this device is enabled for airnotifier.';
@@ -63,5 +69,9 @@ $string['privacy:subcontext'] = 'Message Airnotifier';
 $string['sitemustberegistered'] = 'In order to use the public Airnotifier instance, your site must be registered. Alternatively, you can obtain an access key by creating an account on the <a href="https://apps.moodle.com">Moodle Apps Portal</a>.';
 $string['showhide'] = 'Enable/disable the device.';
 $string['requestaccesskey'] = 'Request access key';
+$string['sendtest'] = 'Send test push notification to my devices';
+$string['sendtestconfirmation'] = 'A test push notification will be sent to the devices you use to connect to this site. Please ensure that your devices are connected to the Internet and that the mobile app is not open (since push notifications are only displayed when received in the background).';
+$string['serverconnectivityerror'] = 'This site is not able to connect to the notifications server {$a}';
 $string['unknowndevice'] = 'Unknown device';
+$string['userdevices'] = 'User devices';
 $string['airnotifier:managedevice'] = 'Manage devices';
index 91766bc..9169cab 100644 (file)
@@ -52,4 +52,8 @@ if ($ADMIN->fulltree) {
     $url = new moodle_url('/message/output/airnotifier/requestaccesskey.php', array('sesskey' => sesskey()));
     $link = html_writer::link($url, get_string('requestaccesskey', 'message_airnotifier'));
     $settings->add(new admin_setting_heading('requestaccesskey', '', $link));
+    // Check configuration.
+    $url = new moodle_url('/message/output/airnotifier/checkconfiguration.php');
+    $link = html_writer::link($url, get_string('checkconfiguration', 'message_airnotifier'));
+    $settings->add(new admin_setting_heading('checkconfiguration', '', $link));
 }
diff --git a/message/output/airnotifier/tests/manager_test.php b/message/output/airnotifier/tests/manager_test.php
new file mode 100644 (file)
index 0000000..9d524c3
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for message_airnotifier_manager.
+ *
+ * @package     message_airnotifier
+ * @category    test
+ * @copyright   2020 Juan Leyva <juan@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+class message_airnotifier_manager_testcase extends advanced_testcase {
+
+    /** Test check_configuration by default **/
+    public function test_check_configuration_default() {
+        global $CFG;
+        $this->resetAfterTest(true);
+
+        $manager = new message_airnotifier_manager();
+
+        // Mock server responses.
+        $CFG->airnotifierurl = 'localhost';
+        curl::mock_response(json_encode(['status' => 'ok']));   // Mock first request to check URL.
+        curl::mock_response(json_encode(['error' => 'Invalid access key']));  // Mock second request to check acces key.
+        $checks = $manager->check_configuration();
+
+        $this->assertEquals(core\check\result::OK, $checks[0]->get_status());   // Mobile service enabled.
+        $this->assertEquals(core\check\result::OK, $checks[1]->get_status());   // Mobile notifications enabled.
+        $this->assertEquals(core\check\result::ERROR, $checks[2]->get_status());    // Airnotifier NOT configured, missing key.
+        $this->assertEquals(core\check\result::OK, $checks[3]->get_status());   // Airnotifier URL available.
+        $this->assertEquals(core\check\result::ERROR, $checks[4]->get_status());    // Missing access key.
+        $this->assertEquals(core\check\result::WARNING, $checks[5]->get_status());  // Only a few of default mobile notifications.
+        $this->assertEquals(core\check\result::ERROR, $checks[6]->get_status());  // No registered devices yet.
+    }
+
+    /** Test check_configuration with token **/
+    public function test_check_configuration_with_token() {
+        global $CFG;
+        $this->resetAfterTest(true);
+
+        $manager = new message_airnotifier_manager();
+
+        // Mock server responses.
+        $CFG->airnotifierurl = 'localhost';
+        curl::mock_response(json_encode(['status' => 'ok']));   // Mock first request to check URL.
+        curl::mock_response(json_encode(['error' => 'Invalid access key']));  // Mock second request to check acces key.
+        $CFG->airnotifieraccesskey = 'test';    // For enabling Airnotifier.
+        $checks = $manager->check_configuration();
+
+        $this->assertEquals(core\check\result::OK, $checks[0]->get_status());   // Mobile service enabled.
+        $this->assertEquals(core\check\result::OK, $checks[1]->get_status());   // Mobile notifications enabled.
+        $this->assertEquals(core\check\result::OK, $checks[2]->get_status());    // Airnotifier configured.
+        $this->assertEquals(core\check\result::OK, $checks[3]->get_status());   // Airnotifier URL available.
+        // The original function fourth check (access key valid in the remote Airnotifier server) is not mockable.
+        $this->assertEquals(core\check\result::WARNING, $checks[4]->get_status());  // Only a few of default mobile notifications.
+        $this->assertEquals(core\check\result::ERROR, $checks[5]->get_status());  // No registered devices yet.
+    }
+
+    /** Test check_configuration bad settings **/
+    public function test_check_configuration_incorrect_settings() {
+        global $CFG;
+        $this->resetAfterTest(true);
+
+        $manager = new message_airnotifier_manager();
+
+        // Mock server responses.
+        $CFG->airnotifierurl = 'localhost';
+        curl::mock_response(json_encode(['status' => 'ok']));   // Mock first request to check URL.
+        curl::mock_response(json_encode(['error' => 'Invalid access key']));  // Mock second request to check acces key.
+        $CFG->airnotifieraccesskey = 'test';    // For enabling Airnotifier.
+        $CFG->airnotifierappname .= ' ';
+        $checks = $manager->check_configuration();
+
+        $this->assertEquals(core\check\result::OK, $checks[0]->get_status());   // Mobile service enabled.
+        $this->assertEquals(core\check\result::OK, $checks[1]->get_status());   // Mobile notifications enabled.
+        $this->assertEquals(core\check\result::OK, $checks[2]->get_status());    // Airnotifier configured.
+        $this->assertEquals(core\check\result::ERROR, $checks[3]->get_status());   // Airnotifier URL available.
+        $this->assertEquals(core\check\result::OK, $checks[4]->get_status());   // Invalid setting (empty space).
+        // The original function fifth check (access key valid in the remote Airnotifier server) is not mockable.
+        $this->assertEquals(core\check\result::WARNING, $checks[5]->get_status());  // Only a few of default mobile notifications.
+        $this->assertEquals(core\check\result::ERROR, $checks[6]->get_status());  // No registered devices yet.
+    }
+
+    /** Test has_enabled_devices **/
+    public function test_has_enabled_devices() {
+        global $CFG, $DB, $USER;
+        $this->resetAfterTest(true);
+
+        $CFG->airnotifieraccesskey = 'test';    // For mocking the request.
+        $manager = new message_airnotifier_manager();
+
+        // No devices yet for current user.
+        curl::mock_response(json_encode(['status' => 'ok']));
+        $this->assertFalse($manager->has_enabled_devices($CFG->airnotifiermobileappname));
+
+        // Add devices.
+        curl::mock_response(json_encode(['status' => 'ok']));
+        $DB->insert_record('user_devices',
+            ['userid' => $USER->id, 'appid' => $CFG->airnotifiermobileappname, 'platform' => 'ios',
+            'timecreated' => time(), 'timemodified' => time()]);
+        $this->assertTrue($manager->has_enabled_devices($CFG->airnotifiermobileappname));
+    }
+}