Skip to content

Commit

Permalink
MDL-53777 tool_mobile: New script for launching the mobile app
Browse files Browse the repository at this point in the history
  • Loading branch information
jleyva committed Oct 3, 2016
1 parent c951f1f commit 0a90f62
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 101 deletions.
1 change: 1 addition & 0 deletions admin/tool/mobile/lang/en/tool_mobile.php
Expand Up @@ -22,6 +22,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

$string['clickheretolaunchtheapp'] = 'Please, click here if the app does not open automatically.';
$string['forcedurlscheme'] = 'The URL scheme allows to open the mobile app from other apps like the browser. Use this setting if you want to allow only your custom branded app to be opened by the browser.';
$string['forcedurlscheme_key'] = 'URL scheme';
$string['loginintheapp'] = 'Via the app';
Expand Down
102 changes: 102 additions & 0 deletions admin/tool/mobile/launch.php
@@ -0,0 +1,102 @@
<?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/>.

/**
* Launch page, launch the app using custom URL schemes.
*
* If the user is not logged when visiting this page, he will be redirected to the login page.
* Once he is logged, he will be redirected again to this page and the app launched via custom URL schemes.
*
* @package tool_mobile
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir . '/externallib.php');

$serviceshortname = required_param('service', PARAM_ALPHANUMEXT);
$passport = required_param('passport', PARAM_RAW); // Passport send from the app to validate the response URL.
$urlscheme = optional_param('urlscheme', 'moodlemobile', PARAM_NOTAGS); // The URL scheme the app supports.

// Check web services enabled.
if (!$CFG->enablewebservices) {
throw new moodle_exception('enablewsdescription', 'webservice');
}

// Check if the plugin is properly configured.
$typeoflogin = get_config('tool_mobile', 'typeoflogin');
if ($typeoflogin != tool_mobile\api::LOGIN_VIA_BROWSER and
$typeoflogin != tool_mobile\api::LOGIN_VIA_EMBEDDED_BROWSER) {
throw new moodle_exception('pluginnotenabledorconfigured', 'local_mobile');
}

// Check if the service exists and is enabled.
$service = $DB->get_record('external_services', array('shortname' => $serviceshortname, 'enabled' => 1));
if (empty($service)) {
throw new moodle_exception('servicenotavailable', 'webservice');
}

require_login(0, false);

// Require an active user: not guest, not suspended.
core_user::require_active_user($USER);

// Get an existing token or create a new one.
$token = external_generate_token_for_current_user($service);

// Log token access.
$DB->set_field('external_tokens', 'lastaccess', time(), array('id' => $token->id));

$params = array(
'objectid' => $token->id,
);
$event = \core\event\webservice_token_sent::create($params);
$event->add_record_snapshot('external_tokens', $token);
$event->trigger();

// Passport is generated in the mobile app, so the app opening can be validated using that variable.
// Passports are valid only one time, it's deleted in the app once used.
$siteid = md5($CFG->wwwroot . $passport);
$apptoken = base64_encode($siteid . ':::' . $token->token);

// Redirect using the custom URL scheme checking first if a URL scheme is forced in the site settings.
$forcedurlscheme = get_config('tool_mobile', 'forcedurlscheme');
if (!empty($forcedurlscheme)) {
$urlscheme = $forcedurlscheme;
}

$location = "$urlscheme://token=$apptoken";

// For iOS 10 onwards, we have to simulate a user click.
if (core_useragent::is_ios()) {
$PAGE->set_context(null);
$PAGE->set_url('/local/mobile/launch.php', array('service' => $serviceshortname, 'passport' => $passport, 'urlscheme' => $urlscheme));

echo $OUTPUT->header();
$notice = get_string('clickheretolaunchtheapp', 'tool_mobile');
echo html_writer::link($location, $notice, array('id' => 'launchapp'));
echo html_writer::script(
"window.onload = function() {
document.getElementById('launchapp').click();
};"
);
echo $OUTPUT->footer();
} else {
// For Android a http redirect will do fine.
header('Location: ' . $location);
die;
}
57 changes: 57 additions & 0 deletions lib/classes/useragent.php
Expand Up @@ -997,4 +997,61 @@ public static function is_web_crawler() {
$instance = self::instance();
return (bool) $instance->is_useragent_web_crawler();
}

/**
* Returns true if the client appears to be a device using iOS (iPhone, iPad, iPod).
*
* @param scalar $version The version if we need to find out if it is equal to or greater than that specified.
* @return bool true if the client is using iOS
* @since Moodle 3.2
*/
public static function is_ios($version = null) {
$useragent = self::get_user_agent_string();
if ($useragent === false) {
return false;
}
if (strpos($useragent, 'AppleWebKit') === false) {
return false;
}
if (strpos($useragent, 'Windows')) {
// Reject Windows Safari.
return false;
}
if (strpos($useragent, 'Macintosh')) {
// Reject MacOS Safari.
return false;
}
// Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices.
if (strpos($useragent, 'OmniWeb')) {
// Reject OmniWeb.
return false;
}
if (strpos($useragent, 'Shiira')) {
// Reject Shiira.
return false;
}
if (strpos($useragent, 'SymbianOS')) {
// Reject SymbianOS.
return false;
}
if (strpos($useragent, 'Android')) {
// Reject Androids too.
return false;
}
if (strpos($useragent, 'Chrome')) {
// Reject chrome browsers - it needs to be tested explicitly.
// This will also reject Edge, which pretends to be both Chrome, and Safari.
return false;
}

if (empty($version)) {
return true; // No version specified.
}
if (preg_match("/AppleWebKit\/([0-9.]+)/i", $useragent, $match)) {
if (version_compare($match[1], $version) >= 0) {
return true;
}
}
return false;
}
}
116 changes: 116 additions & 0 deletions lib/externallib.php
Expand Up @@ -954,6 +954,122 @@ function external_format_text($text, $textformat, $contextid, $component = null,
return array($text, $textformat);
}

/**
* Generate or return an existing token for the current authenticated user.
* This function is used for creating a valid token for users authenticathing via login/token.php or admin/tool/mobile/launch.php.
*
* @param stdClass $service external service object
* @return stdClass token object
* @since Moodle 3.2
* @throws moodle_exception
*/
function external_generate_token_for_current_user($service) {
global $DB, $USER;

core_user::require_active_user($USER, true, true);

// Check if there is any required system capability.
if ($service->requiredcapability and !has_capability($service->requiredcapability, context_system::instance())) {
throw new moodle_exception('missingrequiredcapability', 'webservice', '', $service->requiredcapability);
}

// Specific checks related to user restricted service.
if ($service->restrictedusers) {
$authoriseduser = $DB->get_record('external_services_users',
array('externalserviceid' => $service->id, 'userid' => $USER->id));

if (empty($authoriseduser)) {
throw new moodle_exception('usernotallowed', 'webservice', '', $service->shortname);
}

if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) {
throw new moodle_exception('invalidtimedtoken', 'webservice');
}

if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) {
throw new moodle_exception('invalidiptoken', 'webservice');
}
}

// Check if a token has already been created for this user and this service.
$conditions = array(
'userid' => $USER->id,
'externalserviceid' => $service->id,
'tokentype' => EXTERNAL_TOKEN_PERMANENT
);
$tokens = $DB->get_records('external_tokens', $conditions, 'timecreated ASC');

// A bit of sanity checks.
foreach ($tokens as $key => $token) {

// Checks related to a specific token. (script execution continue).
$unsettoken = false;
// If sid is set then there must be a valid associated session no matter the token type.
if (!empty($token->sid)) {
if (!\core\session\manager::session_exists($token->sid)) {
// This token will never be valid anymore, delete it.
$DB->delete_records('external_tokens', array('sid' => $token->sid));
$unsettoken = true;
}
}

// Remove token is not valid anymore.
if (!empty($token->validuntil) and $token->validuntil < time()) {
$DB->delete_records('external_tokens', array('token' => $token->token, 'tokentype' => EXTERNAL_TOKEN_PERMANENT));
$unsettoken = true;
}

// Remove token if its ip not in whitelist.
if (isset($token->iprestriction) and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
$unsettoken = true;
}

if ($unsettoken) {
unset($tokens[$key]);
}
}

// If some valid tokens exist then use the most recent.
if (count($tokens) > 0) {
$token = array_pop($tokens);
} else {
$context = context_system::instance();
$isofficialservice = $service->shortname == MOODLE_OFFICIAL_MOBILE_SERVICE;

if (($isofficialservice and has_capability('moodle/webservice:createmobiletoken', $context)) or
(!is_siteadmin($USER) && has_capability('moodle/webservice:createtoken', $context))) {

// Create a new token.
$token = new stdClass;
$token->token = md5(uniqid(rand(), 1));
$token->userid = $USER->id;
$token->tokentype = EXTERNAL_TOKEN_PERMANENT;
$token->contextid = context_system::instance()->id;
$token->creatorid = $USER->id;
$token->timecreated = time();
$token->externalserviceid = $service->id;
// MDL-43119 Token valid for 3 months (12 weeks).
$token->validuntil = $token->timecreated + 12 * WEEKSECS;
$token->id = $DB->insert_record('external_tokens', $token);

$params = array(
'objectid' => $token->id,
'relateduserid' => $USER->id,
'other' => array(
'auto' => true
)
);
$event = \core\event\webservice_token_created::create($params);
$event->add_record_snapshot('external_tokens', $token);
$event->trigger();
} else {
throw new moodle_exception('cannotcreatetoken', 'webservice', '', $service->shortname);
}
}
return $token;
}


/**
* Singleton to handle the external settings.
*
Expand Down
22 changes: 22 additions & 0 deletions lib/tests/useragent_test.php
Expand Up @@ -890,6 +890,7 @@ public function user_agents_providers() {
array(
// Note: We do *not* identify mobile Safari as Safari.
'is_safari_ios' => true,
'is_ios' => true,
'check_safari_ios_version' => array(
'527' => true,
),
Expand All @@ -911,6 +912,7 @@ public function user_agents_providers() {
array(
// Note: We do *not* identify mobile Safari as Safari.
'is_safari_ios' => true,
'is_ios' => true,
'check_safari_ios_version' => array(
'527' => true,
'590' => true,
Expand All @@ -934,6 +936,7 @@ public function user_agents_providers() {
array(
// Note: We do *not* identify mobile Safari as Safari.
'is_safari_ios' => true,
'is_ios' => true,
'check_safari_ios_version' => array(
'527' => true,
),
Expand Down Expand Up @@ -1222,6 +1225,7 @@ public function user_agents_providers() {
'is_web_crawler' => true,
'is_webkit' => true,
'is_safari_ios' => true,
'is_ios' => true,
'check_safari_ios_version' => array(
'527' => true,
),
Expand Down Expand Up @@ -1703,6 +1707,24 @@ public function test_useragent_ios_safari($useragent, $tests) {
}
}

/**
* @dataProvider user_agents_providers
*/
public function test_useragent_ios($useragent, $tests) {
// Setup the core_useragent instance.
core_useragent::instance(true, $useragent);

if (isset($tests['is_ios']) && $tests['is_ios']) {
$this->assertTrue(core_useragent::is_ios(),
"Browser was not identified as an iOS device browser");
$this->assertTrue(core_useragent::check_safari_ios_version());
} else {
$this->assertFalse(core_useragent::is_ios(),
"Browser was incorrectly identified as an iOS device browser");
$this->assertFalse(core_useragent::check_safari_ios_version());
}
}

/**
* @dataProvider user_agents_providers
*/
Expand Down

0 comments on commit 0a90f62

Please sign in to comment.