Merge branch 'MDL-68271-master' of git://github.com/sarjona/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 11 May 2020 01:10:40 +0000 (09:10 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 11 May 2020 01:10:40 +0000 (09:10 +0800)
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/db/services.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/lib.php
admin/tool/mobile/settings.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/version.php
lib/classes/qrcode.php [new file with mode: 0644]
lib/tests/qrcode_test.php [new file with mode: 0644]

index 1136bd0..914ef5f 100644 (file)
@@ -31,6 +31,8 @@ use moodle_url;
 use moodle_exception;
 use lang_string;
 use curl;
+use core_qrcode;
+use stdClass;
 
 /**
  * API exposed by tool_mobile, to be used mostly by external functions and the plugin settings.
@@ -51,6 +53,14 @@ class api {
     const LOGIN_KEY_TTL = 60;
     /** @var string URL of the Moodle Apps Portal */
     const MOODLE_APPS_PORTAL_URL = 'https://apps.moodle.com';
+    /** @var int seconds a QR login key will expire. */
+    const LOGIN_QR_KEY_TTL = 600;
+    /** @var int QR code disabled value */
+    const QR_CODE_DISABLED = 0;
+    /** @var int QR code type URL value */
+    const QR_CODE_URL = 1;
+    /** @var int QR code type login value */
+    const QR_CODE_LOGIN = 2;
 
     /**
      * Returns a list of Moodle plugins supporting the mobile app.
@@ -336,6 +346,7 @@ class api {
 
     /**
      * Creates an auto-login key for the current user, this key is restricted by time and ip address.
+     * This key is used for automatically login the user in the site when the Moodle app opens the site in a mobile browser.
      *
      * @return string the key
      * @since Moodle 3.2
@@ -351,6 +362,24 @@ class api {
         return create_user_key('tool_mobile', $USER->id, null, $iprestriction, $validuntil);
     }
 
+    /**
+     * Creates a QR login key for the current user, this key is restricted by time and ip address.
+     * This key is used for automatically login the user in the site when the user scans a QR code in the Moodle app.
+     *
+     * @return string the key
+     * @since Moodle 3.9
+     */
+    public static function get_qrlogin_key() {
+        global $USER;
+        // Delete previous keys.
+        delete_user_key('tool_mobile', $USER->id);
+
+        // Create a new key.
+        $iprestriction = getremoteaddr(null);
+        $validuntil = time() + self::LOGIN_QR_KEY_TTL;
+        return create_user_key('tool_mobile', $USER->id, null, $iprestriction, $validuntil);
+    }
+
     /**
      * Get a list of the Mobile app features.
      *
@@ -601,4 +630,31 @@ class api {
 
         return $warnings;
     }
+
+    /**
+     * Generates a QR code with the site URL or for automatic login from the mobile app.
+     *
+     * @param  stdClass $mobilesettings tool_mobile settings
+     * @return string base64 data image contents, null if qr disabled
+     */
+    public static function generate_login_qrcode(stdClass $mobilesettings) {
+        global $CFG, $USER;
+
+        if ($mobilesettings->qrcodetype == static::QR_CODE_DISABLED) {
+            return null;
+        }
+
+        $urlscheme = !empty($mobilesettings->forcedurlscheme) ? $mobilesettings->forcedurlscheme : 'moodlemobile';
+        $data = $urlscheme . '://' . $CFG->wwwroot;
+
+        if ($mobilesettings->qrcodetype == static::QR_CODE_LOGIN) {
+            $qrloginkey = static::get_qrlogin_key();
+            $data .= '?qrlogin=' . $qrloginkey . '&userid=' . $USER->id;
+        }
+
+        $qrcode = new core_qrcode($data);
+        $imagedata = 'data:image/png;base64,' . base64_encode($qrcode->getBarcodePngData(5, 5));
+
+        return $imagedata;
+    }
 }
index fe1dad0..fa13085 100644 (file)
@@ -39,6 +39,7 @@ use context_system;
 use moodle_exception;
 use moodle_url;
 use core_text;
+use core_user;
 use coding_exception;
 
 /**
@@ -593,4 +594,102 @@ class external extends external_api {
              )
         ]);
     }
+
+    /**
+     * Returns description of get_tokens_for_qr_login() parameters.
+     *
+     * @return external_function_parameters
+     * @since  Moodle 3.9
+     */
+    public static function get_tokens_for_qr_login_parameters() {
+        return new external_function_parameters (
+            [
+                'qrloginkey' => new external_value(PARAM_ALPHANUMEXT, 'The user key for validating the request.'),
+                'userid' => new external_value(PARAM_INT, 'The user the key belongs to.'),
+            ]
+        );
+    }
+
+    /**
+     * Returns a WebService token (and private token) for QR login
+     *
+     * @param string $qrloginkey the user key generated and embedded into the QR code for validating the request
+     * @param int $userid the user the key belongs to
+     * @return array with the tokens and warnings
+     * @since  Moodle 3.9
+     */
+    public static function get_tokens_for_qr_login($qrloginkey, $userid) {
+        global $PAGE, $DB;
+
+        $params = self::validate_parameters(self::get_tokens_for_qr_login_parameters(),
+            ['qrloginkey' => $qrloginkey, 'userid' => $userid]);
+
+        $context = context_system::instance();
+        // We need this to make work the format text functions.
+        $PAGE->set_context($context);
+
+        $qrcodetype = get_config('tool_mobile', 'qrcodetype');
+        if ($qrcodetype != api::QR_CODE_LOGIN) {
+            throw new moodle_exception('qrcodedisabled', 'tool_mobile');
+        }
+
+        // Only requests from the Moodle mobile or desktop app. This enhances security to avoid any type of XSS attack.
+        // This code goes intentionally here and not inside the check_autologin_prerequisites() function because it
+        // is used by other PHP scripts that can be opened in any browser.
+        if (!\core_useragent::is_moodle_app()) {
+            throw new moodle_exception('apprequired', 'tool_mobile');
+        }
+        api::check_autologin_prerequisites($params['userid']);  // Checks https, avoid site admins using this...
+
+        // Validate and delete the key.
+        $key = validate_user_key($params['qrloginkey'], 'tool_mobile', null);
+        delete_user_key('tool_mobile', $params['userid']);
+
+        // Double check key belong to user.
+        if ($key->userid != $params['userid']) {
+            throw new moodle_exception('invalidkey');
+        }
+
+        // Key validated, check user.
+        $user = core_user::get_user($key->userid, '*', MUST_EXIST);
+        core_user::require_active_user($user, true, true);
+
+        // Generate WS tokens.
+        \core\session\manager::set_user($user);
+
+        // Check if the service exists and is enabled.
+        $service = $DB->get_record('external_services', ['shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE, 'enabled' => 1]);
+        if (empty($service)) {
+            // will throw exception if no token found
+            throw new moodle_exception('servicenotavailable', 'webservice');
+        }
+
+        // Get an existing token or create a new one.
+        $token = external_generate_token_for_current_user($service);
+        $privatetoken = $token->privatetoken; // Save it here, the next function removes it.
+        external_log_token_request($token);
+
+        $result = [
+            'token' => $token->token,
+            'privatetoken' => $privatetoken ?: '',
+            'warnings' => [],
+        ];
+        return $result;
+    }
+
+    /**
+     * Returns description of get_tokens_for_qr_login() result value.
+     *
+     * @return external_description
+     * @since  Moodle 3.9
+     */
+    public static function get_tokens_for_qr_login_returns() {
+        return new external_single_structure(
+            [
+                'token' => new external_value(PARAM_ALPHANUM, 'A valid WebService token for the official mobile app service.'),
+                'privatetoken' => new external_value(PARAM_ALPHANUM, 'Private token used for auto-login processes.'),
+                'warnings' => new external_warnings(),
+            ]
+        );
+    }
 }
index d53f7b4..530267a 100644 (file)
@@ -78,5 +78,14 @@ $functions = array(
         'type'        => 'write',
         'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
-);
 
+    'tool_mobile_get_tokens_for_qr_login' => array(
+        'classname'   => 'tool_mobile\external',
+        'methodname'  => 'get_tokens_for_qr_login',
+        'description' => 'Returns a WebService token (and private token) for QR login.',
+        'type'        => 'read',
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'ajax'          => true,
+        'loginrequired' => false,
+    ),
+);
index ccc6867..789cfd2 100644 (file)
@@ -96,6 +96,15 @@ $string['oauth2identityproviders'] = 'OAuth 2 identity providers';
 $string['offlineuse'] = 'Offline use';
 $string['pluginname'] = 'Moodle app tools';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
+$string['qrcodedisabled'] = 'Access via QR code disabled';
+$string['qrcodeformobileappaccess'] = 'QR code for mobile app access';
+$string['qrcodeformobileapploginabout'] = 'Scan the QR code with your mobile app and you will be automatically logged in. The QR code will expire in {$a} minutes.';
+$string['qrcodeformobileappurlabout'] = 'Scan the QR code with your mobile app to fill in the site URL in your app.';
+$string['qrsiteadminsnotallowed'] = 'For security reasons login via QR code is not allowed for site administrators or if you are logged in as another user.';
+$string['qrcodetype'] = 'QR code access';
+$string['qrcodetype_desc'] = 'A QR code can be provided for mobile app users to scan and either have the site URL filled in or be automatically logged in without having to enter their credentials.';
+$string['qrcodetypeurl'] = 'QR code with site URL';
+$string['qrcodetypelogin'] = 'QR code with automatic login';
 $string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
@@ -108,3 +117,4 @@ $string['getmoodleonyourmobile'] = 'Get the mobile app';
 $string['privacy:metadata:preference:tool_mobile_autologin_request_last'] = 'The date of the last auto-login key request. Between each request 6 minutes are required.';
 $string['privacy:metadata:core_userkey'] = 'User\'s keys used to create auto-login key for the current user.';
 $string['responsivemainmenuitems'] = 'Responsive menu items';
+$string['viewqrcode'] = 'View QR code';
index 74b3c0b..43d6cc2 100644 (file)
@@ -126,24 +126,64 @@ function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree
         return;
     }
 
-    if (!$url = tool_mobile_create_app_download_url()) {
-        return;
+    $newnodes = [];
+    $mobilesettings = get_config('tool_mobile');
+
+    // Check if we should display a QR code.
+    if (!empty($mobilesettings->qrcodetype)) {
+        $mobileqr = null;
+        $qrcodeforappstr = get_string('qrcodeformobileappaccess', 'tool_mobile');
+
+        if ($mobilesettings->qrcodetype == tool_mobile\api::QR_CODE_LOGIN && is_https()) {
+
+            if (is_siteadmin() || \core\session\manager::is_loggedinas()) {
+                $mobileqr = get_string('qrsiteadminsnotallowed', 'tool_mobile');
+            } else {
+                $qrcodeimg = tool_mobile\api::generate_login_qrcode($mobilesettings);
+
+                $minutes = tool_mobile\api::LOGIN_QR_KEY_TTL / MINSECS;
+                $mobileqr = html_writer::tag('p', get_string('qrcodeformobileapploginabout', 'tool_mobile', $minutes));
+                $mobileqr .= html_writer::link('#qrcode', get_string('viewqrcode', 'tool_mobile'),
+                    ['class' => 'btn btn-primary mt-2', 'data-toggle' => 'collapse',
+                    'role' => 'button', 'aria-expanded' => 'false']);
+                $mobileqr .= html_writer::div(html_writer::img($qrcodeimg, $qrcodeforappstr), 'collapse mt-4', ['id' => 'qrcode']);
+            }
+
+        } else if ($mobilesettings->qrcodetype == tool_mobile\api::QR_CODE_URL) {
+            $qrcodeimg = tool_mobile\api::generate_login_qrcode($mobilesettings);
+
+            $mobileqr = get_string('qrcodeformobileappurlabout', 'tool_mobile');
+            $mobileqr .= html_writer::div(html_writer::img($qrcodeimg, $qrcodeforappstr));
+        }
+
+        if ($mobileqr) {
+            $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappqr', $qrcodeforappstr, null, null, $mobileqr);
+        }
     }
 
+    // Check if the user is using the app, encouraging him to use it otherwise.
     $userhastoken = tool_mobile_user_has_token($user->id);
-
-    $mobilecategory = new core_user\output\myprofile\category('mobile', get_string('mobileapp', 'tool_mobile'),
-            'loginactivity');
-    $tree->add_category($mobilecategory);
+    $mobilestrconnected = null;
 
     if ($userhastoken) {
-        $mobilestr = get_string('mobileappconnected', 'tool_mobile');
-    } else {
-        $mobilestr = get_string('mobileappenabled', 'tool_mobile', $url->out());
+        $mobilestrconnected = get_string('mobileappconnected', 'tool_mobile');
+    } else if ($url = tool_mobile_create_app_download_url()) {
+         $mobilestrconnected = get_string('mobileappenabled', 'tool_mobile', $url->out());
     }
 
-    $node = new  core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestr, null);
-    $tree->add_node($node);
+    if ($mobilestrconnected) {
+        $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestrconnected, null);
+    }
+
+    // Add nodes, if any.
+    if (!empty($newnodes)) {
+        $mobilecat = new core_user\output\myprofile\category('mobile', get_string('mobileapp', 'tool_mobile'), 'loginactivity');
+        $tree->add_category($mobilecat);
+
+        foreach ($newnodes as $node) {
+            $tree->add_node($node);
+        }
+    }
 }
 
 /**
index 05ed4aa..732bd71 100644 (file)
@@ -58,6 +58,9 @@ if ($hassiteconfig) {
 
         // Type of login.
         $temp = new admin_settingpage('mobileauthentication', new lang_string('mobileauthentication', 'tool_mobile'));
+
+        $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesauth', '', $featuresnotice));
+
         $options = array(
             tool_mobile\api::LOGIN_VIA_APP => new lang_string('loginintheapp', 'tool_mobile'),
             tool_mobile\api::LOGIN_VIA_BROWSER => new lang_string('logininthebrowser', 'tool_mobile'),
@@ -67,6 +70,15 @@ if ($hassiteconfig) {
                     new lang_string('typeoflogin', 'tool_mobile'),
                     new lang_string('typeoflogin_desc', 'tool_mobile'), 1, $options));
 
+        $options = [
+            tool_mobile\api::QR_CODE_DISABLED => new lang_string('qrcodedisabled', 'tool_mobile'),
+            tool_mobile\api::QR_CODE_URL => new lang_string('qrcodetypeurl', 'tool_mobile'),
+            tool_mobile\api::QR_CODE_LOGIN => new lang_string('qrcodetypelogin', 'tool_mobile'),
+        ];
+        $temp->add(new admin_setting_configselect('tool_mobile/qrcodetype',
+                    new lang_string('qrcodetype', 'tool_mobile'),
+                    new lang_string('qrcodetype_desc', 'tool_mobile'), tool_mobile\api::QR_CODE_LOGIN, $options));
+
         $temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
                     new lang_string('forcedurlscheme_key', 'tool_mobile'),
                     new lang_string('forcedurlscheme', 'tool_mobile'), 'moodlemobile', PARAM_NOTAGS));
index 9534f05..7b05175 100644 (file)
@@ -600,4 +600,129 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => true]);
         $this->assertEquals($expected, $data->courses[0]->summary);
     }
+
+    /*
+     * Test get_tokens_for_qr_login.
+     */
+    public function test_get_tokens_for_qr_login() {
+        global $DB, $CFG, $USER;
+
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $qrloginkey = api::get_qrlogin_key();
+
+        // Generate new tokens, the ones we expect to receive.
+        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
+        $token = external_generate_token_for_current_user($service);
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+                'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        $result = external::get_tokens_for_qr_login($qrloginkey, $USER->id);
+        $result = external_api::clean_returnvalue(external::get_tokens_for_qr_login_returns(), $result);
+
+        $this->assertEmpty($result['warnings']);
+        $this->assertEquals($token->token, $result['token']);
+        $this->assertEquals($token->privatetoken, $result['privatetoken']);
+
+        // Now, try with an invalid key.
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('invalidkey', 'error'));
+        $result = external::get_tokens_for_qr_login(random_string('64'), $user->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing QR code enabled.
+     */
+    public function test_get_tokens_for_qr_login_missing_enableqr() {
+        global $CFG, $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        set_config('qrcodetype', tool_mobile\api::QR_CODE_DISABLED, 'tool_mobile');
+
+        $this->expectExceptionMessage(get_string('qrcodedisabled', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing ws.
+     */
+    public function test_get_tokens_for_qr_login_missing_ws() {
+        global $CFG;
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        // Need to disable webservices to verify that's checked.
+        $CFG->enablewebservices = 0;
+        $CFG->enablemobilewebservice = 0;
+
+        $this->setAdminUser();
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
+        $result = external::get_tokens_for_qr_login('', $user->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing https.
+     */
+    public function test_get_tokens_for_qr_login_missing_https() {
+        global $CFG, $USER;
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        // Need to simulate a non HTTPS site here.
+        $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing admin.
+     */
+    public function test_get_tokens_for_qr_login_missing_admin() {
+        global $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing app_request.
+     */
+    public function test_get_tokens_for_qr_login_missing_app_request() {
+        global $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('apprequired', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
 }
index 392d5b2..afba9ff 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2019111800; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019111801; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019111200; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
 $plugin->dependencies = array(
diff --git a/lib/classes/qrcode.php b/lib/classes/qrcode.php
new file mode 100644 (file)
index 0000000..2f24a43
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Class for generating QR codes. Wrapper class that extends TCPDF.
+ *
+ * @package    core
+ * @copyright  2020 Moodle Pty Ltd.
+ * @author     Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tcpdf/tcpdf_barcodes_2d.php');
+
+/**
+ * Class for generating QR codes. Wrapper class that extends TCPDF.
+ *
+ * @copyright  2020 Moodle Pty Ltd.
+ */
+class core_qrcode extends TCPDF2DBarcode {
+
+    /**
+     * Overrided constructor to force QR codes.
+     *
+     * @param string $data the data to generate the code
+     */
+    public function __construct($data) {
+
+        parent::__construct($data, 'QRCODE');
+    }
+}
diff --git a/lib/tests/qrcode_test.php b/lib/tests/qrcode_test.php
new file mode 100644 (file)
index 0000000..95152c4
--- /dev/null
@@ -0,0 +1,54 @@
+<?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/>.
+
+/**
+ * Test QR code functionality.
+ *
+ * @package    core
+ * @copyright  Moodle Pty Ltd
+ * @author     <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A set of tests for some of the QR code functionality within Moodle.
+ *
+ * @package    core
+ * @copyright  Moodle Pty Ltd
+ * @author     <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_qrcode_testcase extends basic_testcase {
+
+    /**
+     * Basic test to generate a QR code and check that the library is not broken.
+     */
+    public function test_generate_basic_qr() {
+        // The QR code generator library apply masks by random order, this is why everytime a QR code is generated the resultant
+        // binary file can be different. This is why tests are limited.
+
+        $text = 'abc';
+        $color = 'black';
+        $qrcode = new core_qrcode($text, $color);
+        $svgdata = $qrcode->getBarcodeSVGcode(1, 1);
+
+        // Just check the SVG was generated.
+        $this->assertContains('<desc>' . $text . '</desc>', $svgdata);
+        $this->assertContains('fill="' . $color . '"', $svgdata);
+    }
+}