Merge branch 'MDL-65547-master' of git://github.com/jleyva/moodle

This commit is contained in:
Jun Pataleta
2020-05-09 00:30:34 +08:00
10 changed files with 464 additions and 13 deletions
+56
View 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;
}
}
+99
View 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(),
]
);
}
}
+10 -1
View 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,
),
);
+10
View 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';
+51 -11
View 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);
}
}
}
/**
+12
View 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));
@@ -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);
}
}
+1 -1
View 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(
+46
View File
@@ -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 <[email protected]>
* @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');
}
}
+54
View File
@@ -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 <[email protected]>
* @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 <[email protected]>
* @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);
}
}