From f24b0795effd4d794c25f19f020360c3f4f045d5 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Mon, 13 Apr 2020 19:41:23 +0200 Subject: [PATCH 1/4] MDL-65547 qr: Wrapper for core_qrcode --- lib/classes/qrcode.php | 46 +++++++++++++++++++++++++++++++++ lib/tests/qrcode_test.php | 54 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 lib/classes/qrcode.php create mode 100644 lib/tests/qrcode_test.php diff --git a/lib/classes/qrcode.php b/lib/classes/qrcode.php new file mode 100644 index 00000000000..2f24a433f3e --- /dev/null +++ b/lib/classes/qrcode.php @@ -0,0 +1,46 @@ +. + +/** + * Class for generating QR codes. Wrapper class that extends TCPDF. + * + * @package core + * @copyright 2020 Moodle Pty Ltd. + * @author Juan Leyva + * @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 index 00000000000..95152c4ef24 --- /dev/null +++ b/lib/tests/qrcode_test.php @@ -0,0 +1,54 @@ +. + +/** + * Test QR code functionality. + * + * @package core + * @copyright Moodle Pty Ltd + * @author + * @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 + * @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('' . $text . '', $svgdata); + $this->assertContains('fill="' . $color . '"', $svgdata); + } +} From 9df51510138290f73c6de0e8d8d3c3e0e884b172 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Tue, 14 Apr 2020 00:20:30 +0200 Subject: [PATCH 2/4] MDL-65547 tool_mobile: Display QR in user profile --- admin/tool/mobile/classes/api.php | 42 +++++++++++++++++++ admin/tool/mobile/lang/en/tool_mobile.php | 6 +++ admin/tool/mobile/lib.php | 51 ++++++++++++++++++----- admin/tool/mobile/settings.php | 7 ++++ 4 files changed, 95 insertions(+), 11 deletions(-) diff --git a/admin/tool/mobile/classes/api.php b/admin/tool/mobile/classes/api.php index 1136bd0af73..9c078e1aa34 100644 --- a/admin/tool/mobile/classes/api.php +++ b/admin/tool/mobile/classes/api.php @@ -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,8 @@ 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; /** * Returns a list of Moodle plugins supporting the mobile app. @@ -336,6 +340,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 +356,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 +624,23 @@ class api { return $warnings; } + + /** + * Generates a QR code for automatic login from the mobile app. + * + * @param stdClass $mobilesettings tool_mobile settings + * @return string base64 data image contents + */ + public static function generate_login_qrcode(stdClass $mobilesettings) { + global $CFG, $USER; + + $urlscheme = !empty($mobilesettings->forcedurlscheme) ? $mobilesettings->forcedurlscheme : 'moodlemobile'; + $qrloginkey = static::get_qrlogin_key(); + $data = $urlscheme . '://' . $CFG->wwwroot . '?qrlogin=' . $qrloginkey . '&userid=' . $USER->id; + + $qrcode = new core_qrcode($data); + $imagedata = 'data:image/png;base64,' . base64_encode($qrcode->getBarcodePngData(5, 5)); + + return $imagedata; + } } diff --git a/admin/tool/mobile/lang/en/tool_mobile.php b/admin/tool/mobile/lang/en/tool_mobile.php index ccc68672217..462b474fdf4 100644 --- a/admin/tool/mobile/lang/en/tool_mobile.php +++ b/admin/tool/mobile/lang/en/tool_mobile.php @@ -60,6 +60,8 @@ $string['disabledfeatures_desc'] = 'Select here the features you want to disable $string['displayerrorswarning'] = 'Display debug messages (debugdisplay) is enabled. It should be disabled.'; $string['downloadcourse'] = 'Download course'; $string['downloadcourses'] = 'Download courses'; +$string['enableqrlogin'] = 'Enable QR login'; +$string['enableqrlogin_desc'] = 'If enabled, a QR code that will log users in into the site will be displayed in the users profile page. Only works in sites with https enabled.'; $string['enablesmartappbanners'] = 'Enable App Banners'; $string['enablesmartappbanners_desc'] = 'If enabled, a banner promoting the mobile app will be displayed when accessing the site using a mobile browser.'; $string['forcedurlscheme'] = 'If you want to allow only your custom branded app to be opened via a browser window, then specify its URL scheme here. If you want to allow only the official app, then set the default value. Leave the field empty if you want to allow any app.'; @@ -96,6 +98,9 @@ $string['oauth2identityproviders'] = 'OAuth 2 identity providers'; $string['offlineuse'] = 'Offline use'; $string['pluginname'] = 'Moodle app tools'; $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.'; +$string['qrcodeformobileapplogin'] = 'QR code for mobile app login'; +$string['qrcodeformobileapploginabout'] = 'Scan this code in your mobile app and you will be automatically logged in into this site. The code expires in {$a} minutes.'; +$string['qrsiteadminsnotallowed'] = 'For security reasons login via QR code is not allowed to site administrators.'; $string['readingthisemailgettheapp'] = 'Reading this in an email? Download the mobile app and receive notifications on your mobile device.'; $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 +113,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'; diff --git a/admin/tool/mobile/lib.php b/admin/tool/mobile/lib.php index 74b3c0b3671..c40d7aa980e 100644 --- a/admin/tool/mobile/lib.php +++ b/admin/tool/mobile/lib.php @@ -126,24 +126,53 @@ 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 for quick login. + if (is_https() && !empty($mobilesettings->enableqrlogin)) { + $qrcodeforappstr = get_string('qrcodeformobileapplogin', 'tool_mobile'); + + if (is_siteadmin()) { + $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 = 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']); + } + + $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); + } + } } /** diff --git a/admin/tool/mobile/settings.php b/admin/tool/mobile/settings.php index 05ed4aad1b0..2655ad86276 100644 --- a/admin/tool/mobile/settings.php +++ b/admin/tool/mobile/settings.php @@ -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,10 @@ if ($hassiteconfig) { new lang_string('typeoflogin', 'tool_mobile'), new lang_string('typeoflogin_desc', 'tool_mobile'), 1, $options)); + $temp->add(new admin_setting_configcheckbox('tool_mobile/enableqrlogin', + new lang_string('enableqrlogin', 'tool_mobile'), + new lang_string('enableqrlogin_desc', 'tool_mobile'), 1)); + $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)); From 118852a710481a95653eaa75ffe313083485ef6f Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Tue, 14 Apr 2020 21:17:43 +0200 Subject: [PATCH 3/4] MDL-65547 tool_mobile: New WS for generating tokens with qr login keys --- admin/tool/mobile/classes/external.php | 99 +++++++++++++++ admin/tool/mobile/db/services.php | 11 +- admin/tool/mobile/tests/externallib_test.php | 125 +++++++++++++++++++ admin/tool/mobile/version.php | 2 +- 4 files changed, 235 insertions(+), 2 deletions(-) diff --git a/admin/tool/mobile/classes/external.php b/admin/tool/mobile/classes/external.php index fe1dad0e5a6..55dc950b73a 100644 --- a/admin/tool/mobile/classes/external.php +++ b/admin/tool/mobile/classes/external.php @@ -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); + + $enableqrlogin = get_config('tool_mobile', 'enableqrlogin'); + if (empty($enableqrlogin)) { + throw new moodle_exception('QR login not enabled'); + } + + // 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(), + ] + ); + } } diff --git a/admin/tool/mobile/db/services.php b/admin/tool/mobile/db/services.php index d53f7b41a7f..530267aaf38 100644 --- a/admin/tool/mobile/db/services.php +++ b/admin/tool/mobile/db/services.php @@ -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, + ), +); diff --git a/admin/tool/mobile/tests/externallib_test.php b/admin/tool/mobile/tests/externallib_test.php index 9534f05f4f4..7d024ec8801 100644 --- a/admin/tool/mobile/tests/externallib_test.php +++ b/admin/tool/mobile/tests/externallib_test.php @@ -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('enableqrlogin', 1, 'tool_mobile'); + + $this->expectException('moodle_exception'); + $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); + } } diff --git a/admin/tool/mobile/version.php b/admin/tool/mobile/version.php index 392d5b2b067..afba9ff99ea 100644 --- a/admin/tool/mobile/version.php +++ b/admin/tool/mobile/version.php @@ -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( From af59fe585a5fc56457a880dca9339f65d13bf061 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Wed, 22 Apr 2020 13:01:57 +0200 Subject: [PATCH 4/4] MDL-65547 tool_mobile: Allow to generate site QR codes only --- admin/tool/mobile/classes/api.php | 22 +++++++++--- admin/tool/mobile/classes/external.php | 6 ++-- admin/tool/mobile/lang/en/tool_mobile.php | 14 +++++--- admin/tool/mobile/lib.php | 37 +++++++++++++------- admin/tool/mobile/settings.php | 11 ++++-- admin/tool/mobile/tests/externallib_test.php | 4 +-- 6 files changed, 64 insertions(+), 30 deletions(-) diff --git a/admin/tool/mobile/classes/api.php b/admin/tool/mobile/classes/api.php index 9c078e1aa34..914ef5f4bd1 100644 --- a/admin/tool/mobile/classes/api.php +++ b/admin/tool/mobile/classes/api.php @@ -55,6 +55,12 @@ class api { 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. @@ -626,17 +632,25 @@ class api { } /** - * Generates a QR code for automatic login from the mobile app. + * 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 + * @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'; - $qrloginkey = static::get_qrlogin_key(); - $data = $urlscheme . '://' . $CFG->wwwroot . '?qrlogin=' . $qrloginkey . '&userid=' . $USER->id; + $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)); diff --git a/admin/tool/mobile/classes/external.php b/admin/tool/mobile/classes/external.php index 55dc950b73a..fa130854e8c 100644 --- a/admin/tool/mobile/classes/external.php +++ b/admin/tool/mobile/classes/external.php @@ -628,9 +628,9 @@ class external extends external_api { // We need this to make work the format text functions. $PAGE->set_context($context); - $enableqrlogin = get_config('tool_mobile', 'enableqrlogin'); - if (empty($enableqrlogin)) { - throw new moodle_exception('QR login not enabled'); + $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. diff --git a/admin/tool/mobile/lang/en/tool_mobile.php b/admin/tool/mobile/lang/en/tool_mobile.php index 462b474fdf4..789cfd25348 100644 --- a/admin/tool/mobile/lang/en/tool_mobile.php +++ b/admin/tool/mobile/lang/en/tool_mobile.php @@ -60,8 +60,6 @@ $string['disabledfeatures_desc'] = 'Select here the features you want to disable $string['displayerrorswarning'] = 'Display debug messages (debugdisplay) is enabled. It should be disabled.'; $string['downloadcourse'] = 'Download course'; $string['downloadcourses'] = 'Download courses'; -$string['enableqrlogin'] = 'Enable QR login'; -$string['enableqrlogin_desc'] = 'If enabled, a QR code that will log users in into the site will be displayed in the users profile page. Only works in sites with https enabled.'; $string['enablesmartappbanners'] = 'Enable App Banners'; $string['enablesmartappbanners_desc'] = 'If enabled, a banner promoting the mobile app will be displayed when accessing the site using a mobile browser.'; $string['forcedurlscheme'] = 'If you want to allow only your custom branded app to be opened via a browser window, then specify its URL scheme here. If you want to allow only the official app, then set the default value. Leave the field empty if you want to allow any app.'; @@ -98,9 +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['qrcodeformobileapplogin'] = 'QR code for mobile app login'; -$string['qrcodeformobileapploginabout'] = 'Scan this code in your mobile app and you will be automatically logged in into this site. The code expires in {$a} minutes.'; -$string['qrsiteadminsnotallowed'] = 'For security reasons login via QR code is not allowed to site administrators.'; +$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? Download the mobile app and receive notifications on your mobile device.'; $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.'; diff --git a/admin/tool/mobile/lib.php b/admin/tool/mobile/lib.php index c40d7aa980e..43d6cc23e92 100644 --- a/admin/tool/mobile/lib.php +++ b/admin/tool/mobile/lib.php @@ -129,25 +129,36 @@ function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree $newnodes = []; $mobilesettings = get_config('tool_mobile'); - // Check if we should display a QR code for quick login. - if (is_https() && !empty($mobilesettings->enableqrlogin)) { - $qrcodeforappstr = get_string('qrcodeformobileapplogin', 'tool_mobile'); + // Check if we should display a QR code. + if (!empty($mobilesettings->qrcodetype)) { + $mobileqr = null; + $qrcodeforappstr = get_string('qrcodeformobileappaccess', 'tool_mobile'); - if (is_siteadmin()) { - $mobileqr = get_string('qrsiteadminsnotallowed', 'tool_mobile'); - } else { + 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); - $minutes = tool_mobile\api::LOGIN_QR_KEY_TTL / MINSECS; - - $mobileqr = 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']); + $mobileqr = get_string('qrcodeformobileappurlabout', 'tool_mobile'); + $mobileqr .= html_writer::div(html_writer::img($qrcodeimg, $qrcodeforappstr)); } - $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappqr', $qrcodeforappstr, null, null, $mobileqr); + 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. diff --git a/admin/tool/mobile/settings.php b/admin/tool/mobile/settings.php index 2655ad86276..732bd71c347 100644 --- a/admin/tool/mobile/settings.php +++ b/admin/tool/mobile/settings.php @@ -70,9 +70,14 @@ if ($hassiteconfig) { new lang_string('typeoflogin', 'tool_mobile'), new lang_string('typeoflogin_desc', 'tool_mobile'), 1, $options)); - $temp->add(new admin_setting_configcheckbox('tool_mobile/enableqrlogin', - new lang_string('enableqrlogin', 'tool_mobile'), - new lang_string('enableqrlogin_desc', 'tool_mobile'), 1)); + $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'), diff --git a/admin/tool/mobile/tests/externallib_test.php b/admin/tool/mobile/tests/externallib_test.php index 7d024ec8801..7b051753088 100644 --- a/admin/tool/mobile/tests/externallib_test.php +++ b/admin/tool/mobile/tests/externallib_test.php @@ -643,9 +643,9 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase { $this->resetAfterTest(true); $this->setAdminUser(); - set_config('enableqrlogin', 1, 'tool_mobile'); + set_config('qrcodetype', tool_mobile\api::QR_CODE_DISABLED, 'tool_mobile'); - $this->expectException('moodle_exception'); + $this->expectExceptionMessage(get_string('qrcodedisabled', 'tool_mobile')); $result = external::get_tokens_for_qr_login('', $USER->id); }