first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+867
View File
@@ -0,0 +1,867 @@
<?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 Moodle Mobile tools.
*
* @package tool_mobile
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.1
*/
namespace tool_mobile;
use core_component;
use core_plugin_manager;
use context_system;
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.
*
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.1
*/
class api {
/** @var int to identify the login via app. */
const LOGIN_VIA_APP = 1;
/** @var int to identify the login via browser. */
const LOGIN_VIA_BROWSER = 2;
/** @var int to identify the login via an embedded browser. */
const LOGIN_VIA_EMBEDDED_BROWSER = 3;
/** @var int seconds an auto-login key will expire. */
const LOGIN_KEY_TTL = 60;
/** @var string URL of the Moodle Apps Portal */
const MOODLE_APPS_PORTAL_URL = 'https://apps.moodle.com';
/** @var int default value in 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;
/** @var string Default Android app id */
const DEFAULT_ANDROID_APP_ID = 'com.moodle.moodlemobile';
/** @var string Default iOS app id */
const DEFAULT_IOS_APP_ID = '633359593';
/** @var int AUTOLOGOUT disabled value */
const AUTOLOGOUT_DISABLED = 0;
/** @var int AUTOLOGOUT type inmediate value */
const AUTOLOGOUT_INMEDIATE = 1;
/** @var int AUTOLOGOUT type custom value */
const AUTOLOGOUT_CUSTOM = 2;
/**
* Returns a list of Moodle plugins supporting the mobile app.
*
* @return array an array of objects containing the plugin information
*/
public static function get_plugins_supporting_mobile() {
global $CFG;
require_once($CFG->libdir . '/adminlib.php');
$cachekey = 'mobileplugins';
if (!isloggedin()) {
$cachekey = 'authmobileplugins'; // Use a different cache for not logged users.
}
// Check if we can return this from cache.
$cache = \cache::make('tool_mobile', 'plugininfo');
$pluginsinfo = $cache->get($cachekey);
if ($pluginsinfo !== false) {
return (array)$pluginsinfo;
}
$pluginsinfo = [];
// For not logged users return only auth plugins.
// This is to avoid anyone (not being a registered user) to obtain and download all the site remote add-ons.
if (!isloggedin()) {
$plugintypes = array('auth' => $CFG->dirroot.'/auth');
} else {
$plugintypes = core_component::get_plugin_types();
}
foreach ($plugintypes as $plugintype => $unused) {
// We need to include files here.
$pluginswithfile = core_component::get_plugin_list_with_file($plugintype, 'db' . DIRECTORY_SEPARATOR . 'mobile.php');
foreach ($pluginswithfile as $plugin => $notused) {
$path = core_component::get_plugin_directory($plugintype, $plugin);
$component = $plugintype . '_' . $plugin;
$version = get_component_version($component);
require("$path/db/mobile.php");
foreach ($addons as $addonname => $addoninfo) {
// Add handlers (for site add-ons).
$handlers = !empty($addoninfo['handlers']) ? $addoninfo['handlers'] : array();
$handlers = json_encode($handlers); // JSON formatted, since it is a complex structure that may vary over time.
// Now language strings used by the app.
$lang = array();
if (!empty($addoninfo['lang'])) {
$stringmanager = get_string_manager();
$langs = $stringmanager->get_list_of_translations(true);
foreach ($langs as $langid => $langname) {
foreach ($addoninfo['lang'] as $stringinfo) {
$lang[$langid][$stringinfo[0]] = $stringmanager->get_string(
$stringinfo[0],
$stringinfo[1] ?? '',
null,
$langid,
);
}
}
}
$lang = json_encode($lang);
$plugininfo = array(
'component' => $component,
'version' => $version,
'addon' => $addonname,
'dependencies' => !empty($addoninfo['dependencies']) ? $addoninfo['dependencies'] : array(),
'fileurl' => '',
'filehash' => '',
'filesize' => 0,
'handlers' => $handlers,
'lang' => $lang,
);
// All the mobile packages must be under the plugin mobile directory.
$package = $path . '/mobile/' . $addonname . '.zip';
if (file_exists($package)) {
$plugininfo['fileurl'] = $CFG->wwwroot . '' . str_replace($CFG->dirroot, '', $package);
$plugininfo['filehash'] = sha1_file($package);
$plugininfo['filesize'] = filesize($package);
}
$pluginsinfo[] = $plugininfo;
}
}
}
$cache->set($cachekey, $pluginsinfo);
return $pluginsinfo;
}
/**
* Returns a list of the site public settings, those not requiring authentication.
*
* @return array with the settings and warnings
*/
public static function get_public_config() {
global $CFG, $SITE, $PAGE, $OUTPUT;
require_once($CFG->libdir . '/authlib.php');
$context = context_system::instance();
// We need this to make work the format text functions.
$PAGE->set_context($context);
// Check if contacting site support is available to all visitors.
$sitesupportavailable = (isset($CFG->supportavailability) && $CFG->supportavailability == CONTACT_SUPPORT_ANYONE);
[$authinstructions] = \core_external\util::format_text($CFG->auth_instructions, FORMAT_MOODLE, $context->id);
[$maintenancemessage] = \core_external\util::format_text($CFG->maintenance_message, FORMAT_MOODLE, $context->id);
$settings = array(
'wwwroot' => $CFG->wwwroot,
'httpswwwroot' => $CFG->wwwroot,
'sitename' => \core_external\util::format_string($SITE->fullname, $context->id, true),
'guestlogin' => $CFG->guestloginbutton,
'rememberusername' => $CFG->rememberusername,
'authloginviaemail' => $CFG->authloginviaemail,
'registerauth' => $CFG->registerauth,
'forgottenpasswordurl' => clean_param($CFG->forgottenpasswordurl, PARAM_URL), // We may expect a mailto: here.
'authinstructions' => $authinstructions,
'authnoneenabled' => (int) is_enabled_auth('none'),
'enablewebservices' => $CFG->enablewebservices,
'enablemobilewebservice' => $CFG->enablemobilewebservice,
'maintenanceenabled' => $CFG->maintenance_enabled,
'maintenancemessage' => $maintenancemessage,
'mobilecssurl' => !empty($CFG->mobilecssurl) ? $CFG->mobilecssurl : '',
'tool_mobile_disabledfeatures' => get_config('tool_mobile', 'disabledfeatures'),
'country' => clean_param($CFG->country, PARAM_NOTAGS),
'agedigitalconsentverification' => \core_auth\digital_consent::is_age_digital_consent_verification_enabled(),
'autolang' => $CFG->autolang,
'lang' => clean_param($CFG->lang, PARAM_LANG), // Avoid breaking WS because of incorrect package langs.
'langmenu' => $CFG->langmenu,
'langlist' => $CFG->langlist,
'locale' => $CFG->locale,
'tool_mobile_minimumversion' => get_config('tool_mobile', 'minimumversion'),
'tool_mobile_iosappid' => get_config('tool_mobile', 'iosappid'),
'tool_mobile_androidappid' => get_config('tool_mobile', 'androidappid'),
'tool_mobile_setuplink' => clean_param(get_config('tool_mobile', 'setuplink'), PARAM_URL),
'tool_mobile_qrcodetype' => clean_param(get_config('tool_mobile', 'qrcodetype'), PARAM_INT),
'supportpage' => $sitesupportavailable ? clean_param($CFG->supportpage, PARAM_URL) : '',
'supportavailability' => clean_param($CFG->supportavailability, PARAM_INT),
);
$typeoflogin = get_config('tool_mobile', 'typeoflogin');
// Not found, edge case.
if ($typeoflogin === false) {
$typeoflogin = self::LOGIN_VIA_APP; // Defaults to via app.
}
$settings['typeoflogin'] = $typeoflogin;
// Check if the user can sign-up to return the launch URL in that case.
$cansignup = signup_is_enabled();
$url = new moodle_url("/$CFG->admin/tool/mobile/launch.php");
$settings['launchurl'] = $url->out(false);
// Check that we are receiving a moodle_url object, themes can override get_logo_url and may return incorrect values.
if (($logourl = $OUTPUT->get_logo_url()) && $logourl instanceof moodle_url) {
$settings['logourl'] = clean_param($logourl->out(false), PARAM_URL);
}
if (($compactlogourl = $OUTPUT->get_compact_logo_url()) && $compactlogourl instanceof moodle_url) {
$settings['compactlogourl'] = clean_param($compactlogourl->out(false), PARAM_URL);
}
// Identity providers.
$authsequence = get_enabled_auth_plugins();
$identityproviders = \auth_plugin_base::get_identity_providers($authsequence);
$identityprovidersdata = \auth_plugin_base::prepare_identity_providers_for_output($identityproviders, $OUTPUT);
if (!empty($identityprovidersdata)) {
$settings['identityproviders'] = $identityprovidersdata;
// Clean URLs to avoid breaking Web Services.
// We can't do it in prepare_identity_providers_for_output() because it may break the web output.
foreach ($settings['identityproviders'] as &$ip) {
$ip['url'] = (!empty($ip['url'])) ? clean_param($ip['url'], PARAM_URL) : '';
$ip['iconurl'] = (!empty($ip['iconurl'])) ? clean_param($ip['iconurl'], PARAM_URL) : '';
}
}
// If age is verified or support is available to all visitors, also return the admin contact details.
if ($settings['agedigitalconsentverification'] || $sitesupportavailable) {
$settings['supportname'] = clean_param($CFG->supportname, PARAM_NOTAGS);
$settings['supportemail'] = clean_param($CFG->supportemail, PARAM_EMAIL);
}
return $settings;
}
/**
* Returns a list of site configurations, filtering by section.
*
* @param string $section section name
* @return stdClass object containing the settings
*/
public static function get_config($section) {
global $CFG, $SITE;
$settings = new \stdClass;
$context = context_system::instance();
$isadmin = has_capability('moodle/site:config', $context);
if (empty($section) or $section == 'frontpagesettings') {
require_once($CFG->dirroot . '/course/format/lib.php');
// First settings that anyone can deduce.
$settings->fullname = \core_external\util::format_string($SITE->fullname, $context->id);
$settings->shortname = \core_external\util::format_string($SITE->shortname, $context->id);
// Return to a var instead of directly to $settings object because of differences between
// list() in php5 and php7. {@link http://php.net/manual/en/function.list.php}
$formattedsummary = \core_external\util::format_text($SITE->summary, $SITE->summaryformat,
$context->id);
$settings->summary = $formattedsummary[0];
$settings->summaryformat = $formattedsummary[1];
$settings->frontpage = $CFG->frontpage;
$settings->frontpageloggedin = $CFG->frontpageloggedin;
$settings->maxcategorydepth = $CFG->maxcategorydepth;
$settings->frontpagecourselimit = $CFG->frontpagecourselimit;
$settings->numsections = course_get_format($SITE)->get_last_section_number();
$settings->newsitems = $SITE->newsitems;
$settings->commentsperpage = $CFG->commentsperpage;
// Now, admin settings.
if ($isadmin) {
$settings->defaultfrontpageroleid = $CFG->defaultfrontpageroleid;
}
}
if (empty($section) or $section == 'sitepolicies') {
$manager = new \core_privacy\local\sitepolicy\manager();
$settings->sitepolicy = ($sitepolicy = $manager->get_embed_url()) ? $sitepolicy->out(false) : '';
$settings->sitepolicyhandler = $CFG->sitepolicyhandler;
$settings->disableuserimages = $CFG->disableuserimages;
}
if (empty($section) or $section == 'gradessettings') {
require_once($CFG->dirroot . '/user/lib.php');
$settings->mygradesurl = user_mygrades_url();
// The previous function may return moodle_url instances or plain string URLs.
if ($settings->mygradesurl instanceof moodle_url) {
$settings->mygradesurl = $settings->mygradesurl->out(false);
}
}
if (empty($section) or $section == 'mobileapp') {
$settings->tool_mobile_forcelogout = get_config('tool_mobile', 'forcelogout');
$settings->tool_mobile_customlangstrings = get_config('tool_mobile', 'customlangstrings');
$settings->tool_mobile_disabledfeatures = get_config('tool_mobile', 'disabledfeatures');
$settings->tool_mobile_filetypeexclusionlist = get_config('tool_mobile', 'filetypeexclusionlist');
$settings->tool_mobile_custommenuitems = get_config('tool_mobile', 'custommenuitems');
$settings->tool_mobile_apppolicy = get_config('tool_mobile', 'apppolicy');
// This setting could be not set in some edge cases such as bad upgrade.
$mintimereq = get_config('tool_mobile', 'autologinmintimebetweenreq');
$mintimereq = empty($mintimereq) ? 6 * MINSECS : $mintimereq;
$settings->tool_mobile_autologinmintimebetweenreq = $mintimereq;
$settings->tool_mobile_autologout = get_config('tool_mobile', 'autologout');
$settings->tool_mobile_autologouttime = get_config('tool_mobile', 'autologouttime');
}
if (empty($section) or $section == 'calendar') {
$settings->calendartype = $CFG->calendartype;
$settings->calendar_site_timeformat = $CFG->calendar_site_timeformat;
$settings->calendar_startwday = $CFG->calendar_startwday;
$settings->calendar_adminseesall = $CFG->calendar_adminseesall;
$settings->calendar_lookahead = $CFG->calendar_lookahead;
$settings->calendar_maxevents = $CFG->calendar_maxevents;
}
if (empty($section) or $section == 'coursecolors') {
$colornumbers = range(1, 10);
foreach ($colornumbers as $number) {
$settings->{'core_admin_coursecolor' . $number} = get_config('core_admin', 'coursecolor' . $number);
}
}
if (empty($section) or $section == 'supportcontact') {
$settings->supportavailability = $CFG->supportavailability;
if ($CFG->supportavailability == CONTACT_SUPPORT_DISABLED) {
$settings->supportname = null;
$settings->supportemail = null;
$settings->supportpage = null;
} else {
$settings->supportname = $CFG->supportname;
$settings->supportemail = $CFG->supportemail ?? null;
$settings->supportpage = $CFG->supportpage;
}
}
if (empty($section) || $section === 'graceperiodsettings') {
$settings->coursegraceperiodafter = $CFG->coursegraceperiodafter;
$settings->coursegraceperiodbefore = $CFG->coursegraceperiodbefore;
}
if (empty($section) || $section === 'navigation') {
$settings->enabledashboard = $CFG->enabledashboard;
}
if (empty($section) || ($section === 'themesettings' || $section === 'themesettingsadvanced')) {
$settings->customusermenuitems = $CFG->customusermenuitems;
}
if (empty($section) || $section === 'locationsettings') {
$settings->timezone = $CFG->timezone;
$settings->forcetimezone = $CFG->forcetimezone;
}
if (empty($section) || $section === 'manageglobalsearch') {
$settings->searchengine = $CFG->searchengine;
$settings->searchenablecategories = $CFG->searchenablecategories;
$settings->searchdefaultcategory = $CFG->searchdefaultcategory;
$settings->searchhideallcategory = $CFG->searchhideallcategory;
$settings->searchmaxtopresults = $CFG->searchmaxtopresults;
$settings->searchbannerenable = $CFG->searchbannerenable;
$settings->searchbanner = \core_external\util::format_text(
$CFG->searchbanner, FORMAT_HTML, $context)[0];
}
if (empty($section) || $section === 'privacysettings') {
$settings->tool_dataprivacy_contactdataprotectionofficer = get_config('tool_dataprivacy', 'contactdataprotectionofficer');
$settings->tool_dataprivacy_showdataretentionsummary = get_config('tool_dataprivacy', 'showdataretentionsummary');
}
if (empty($section) || $section === 'blog') {
$settings->useblogassociations = $CFG->useblogassociations;
$settings->bloglevel = $CFG->bloglevel;
$settings->blogusecomments = $CFG->blogusecomments;
}
if (empty($section) || $section === 'h5psettings') {
\core_h5p\local\library\autoloader::register();
$customcss = \core_h5p\file_storage::get_custom_styles();
if (!empty($customcss)) {
$settings->h5pcustomcssurl = $customcss['cssurl']->out() . '?ver=' . $customcss['cssversion'];
}
}
return $settings;
}
/*
* Check if all the required conditions are met to allow the auto-login process continue.
*
* @param int $userid current user id
* @since Moodle 3.2
* @throws moodle_exception
*/
public static function check_autologin_prerequisites($userid) {
global $CFG;
if (!$CFG->enablewebservices or !$CFG->enablemobilewebservice) {
throw new moodle_exception('enablewsdescription', 'webservice');
}
if (!is_https()) {
throw new moodle_exception('httpsrequired', 'tool_mobile');
}
if (has_capability('moodle/site:config', context_system::instance(), $userid) or is_siteadmin($userid)) {
throw new moodle_exception('autologinnotallowedtoadmins', 'tool_mobile');
}
}
/**
* 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
*/
public static function get_autologin_key() {
global $USER;
// Delete previous keys.
delete_user_key('tool_mobile', $USER->id);
// Create a new key.
$iprestriction = getremoteaddr();
$validuntil = time() + self::LOGIN_KEY_TTL;
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.
*
* @param stdClass $mobilesettings mobile app plugin settings
* @return string the key
* @since Moodle 3.9
*/
public static function get_qrlogin_key(stdClass $mobilesettings) {
global $USER;
// Delete previous keys.
delete_user_key('tool_mobile/qrlogin', $USER->id);
// Create a new key.
$iprestriction = !empty($mobilesettings->qrsameipcheck) ? getremoteaddr(null) : null;
$qrkeyttl = !empty($mobilesettings->qrkeyttl) ? $mobilesettings->qrkeyttl : self::LOGIN_QR_KEY_TTL;
$validuntil = time() + $qrkeyttl;
return create_user_key('tool_mobile/qrlogin', $USER->id, null, $iprestriction, $validuntil);
}
/**
* Get a list of the Mobile app features.
*
* @return array array with the features grouped by theirs ubication in the app.
* @since Moodle 3.3
*/
public static function get_features_list() {
global $CFG;
require_once($CFG->libdir . '/authlib.php');
$general = new lang_string('general');
$mainmenu = new lang_string('mainmenu', 'tool_mobile');
$course = new lang_string('course');
$modules = new lang_string('managemodules');
$blocks = new lang_string('blocks');
$useraccount = new lang_string('useraccount');
$participants = new lang_string('participants');
$files = new lang_string('files');
$remoteaddons = new lang_string('remoteaddons', 'tool_mobile');
$identityproviders = new lang_string('oauth2identityproviders', 'tool_mobile');
$availablemods = core_plugin_manager::instance()->get_plugins_of_type('mod');
$coursemodules = array();
$appsupportedmodules = array(
'assign', 'bigbluebuttonbn', 'book', 'chat', 'choice', 'data', 'feedback', 'folder', 'forum', 'glossary', 'h5pactivity',
'imscp', 'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop');
foreach ($availablemods as $mod) {
if (in_array($mod->name, $appsupportedmodules)) {
$coursemodules['$mmCourseDelegate_mmaMod' . ucfirst($mod->name)] = $mod->displayname;
}
}
asort($coursemodules);
$remoteaddonslist = array();
$mobileplugins = self::get_plugins_supporting_mobile();
foreach ($mobileplugins as $plugin) {
$displayname = core_plugin_manager::instance()->plugin_name($plugin['component']) . " - " . $plugin['addon'];
$remoteaddonslist['sitePlugin_' . $plugin['component'] . '_' . $plugin['addon']] = $displayname;
}
// Display blocks.
$availableblocks = core_plugin_manager::instance()->get_plugins_of_type('block');
$courseblocks = array();
$appsupportedblocks = array(
'activity_modules' => 'CoreBlockDelegate_AddonBlockActivityModules',
'activity_results' => 'CoreBlockDelegate_AddonBlockActivityResults',
'site_main_menu' => 'CoreBlockDelegate_AddonBlockSiteMainMenu',
'myoverview' => 'CoreBlockDelegate_AddonBlockMyOverview',
'course_list' => 'CoreBlockDelegate_AddonBlockCourseList',
'timeline' => 'CoreBlockDelegate_AddonBlockTimeline',
'recentlyaccessedcourses' => 'CoreBlockDelegate_AddonBlockRecentlyAccessedCourses',
'starredcourses' => 'CoreBlockDelegate_AddonBlockStarredCourses',
'recentlyaccesseditems' => 'CoreBlockDelegate_AddonBlockRecentlyAccessedItems',
'badges' => 'CoreBlockDelegate_AddonBlockBadges',
'blog_menu' => 'CoreBlockDelegate_AddonBlockBlogMenu',
'blog_recent' => 'CoreBlockDelegate_AddonBlockBlogRecent',
'blog_tags' => 'CoreBlockDelegate_AddonBlockBlogTags',
'calendar_month' => 'CoreBlockDelegate_AddonBlockCalendarMonth',
'calendar_upcoming' => 'CoreBlockDelegate_AddonBlockCalendarUpcoming',
'comments' => 'CoreBlockDelegate_AddonBlockComments',
'completionstatus' => 'CoreBlockDelegate_AddonBlockCompletionStatus',
'feedback' => 'CoreBlockDelegate_AddonBlockFeedback',
'globalsearch' => 'CoreBlockDelegate_AddonBlockGlobalSearch',
'glossary_random' => 'CoreBlockDelegate_AddonBlockGlossaryRandom',
'html' => 'CoreBlockDelegate_AddonBlockHtml',
'lp' => 'CoreBlockDelegate_AddonBlockLp',
'news_items' => 'CoreBlockDelegate_AddonBlockNewsItems',
'online_users' => 'CoreBlockDelegate_AddonBlockOnlineUsers',
'private_files' => 'CoreBlockDelegate_AddonBlockPrivateFiles',
'recent_activity' => 'CoreBlockDelegate_AddonBlockRecentActivity',
'rss_client' => 'CoreBlockDelegate_AddonBlockRssClient',
'search_forums' => 'CoreBlockDelegate_AddonBlockSearchForums',
'selfcompletion' => 'CoreBlockDelegate_AddonBlockSelfCompletion',
'tags' => 'CoreBlockDelegate_AddonBlockTags',
);
foreach ($availableblocks as $block) {
if (isset($appsupportedblocks[$block->name])) {
$courseblocks[$appsupportedblocks[$block->name]] = $block->displayname;
}
}
asort($courseblocks);
$features = array(
"$general" => array(
'NoDelegate_CoreOffline' => new lang_string('offlineuse', 'tool_mobile'),
'NoDelegate_SiteBlocks' => new lang_string('blocks'),
'NoDelegate_CoreComments' => new lang_string('comments'),
'NoDelegate_CoreRating' => new lang_string('ratings', 'rating'),
'NoDelegate_CoreTag' => new lang_string('tags'),
'$mmLoginEmailSignup' => new lang_string('startsignup'),
'NoDelegate_ForgottenPassword' => new lang_string('forgotten'),
'NoDelegate_ResponsiveMainMenuItems' => new lang_string('responsivemainmenuitems', 'tool_mobile'),
'NoDelegate_H5POffline' => new lang_string('h5poffline', 'tool_mobile'),
'NoDelegate_DarkMode' => new lang_string('darkmode', 'tool_mobile'),
'CoreFilterDelegate' => new lang_string('type_filter_plural', 'plugin'),
'CoreReportBuilderDelegate' => new lang_string('reportbuilder', 'core_reportbuilder'),
'NoDelegate_CoreUserSupport' => new lang_string('contactsitesupport', 'admin'),
'NoDelegate_GlobalSearch' => new lang_string('globalsearch', 'search'),
),
"$mainmenu" => array(
'$mmSideMenuDelegate_mmaFrontpage' => new lang_string('sitehome'),
'CoreMainMenuDelegate_CoreCoursesDashboard' => new lang_string('myhome'),
'$mmSideMenuDelegate_mmCourses' => new lang_string('mycourses'),
'$mmSideMenuDelegate_mmaMessages' => new lang_string('messages', 'message'),
'$mmSideMenuDelegate_mmaNotifications' => new lang_string('notifications', 'message'),
'$mmSideMenuDelegate_mmaCalendar' => new lang_string('calendar', 'calendar'),
'CoreMainMenuDelegate_AddonBlog' => new lang_string('blog', 'blog'),
'CoreMainMenuDelegate_CoreTag' => new lang_string('tags'),
'CoreMainMenuDelegate_QrReader' => new lang_string('scanqrcode', 'tool_mobile'),
),
"$useraccount" => array(
'$mmSideMenuDelegate_mmaGrades' => new lang_string('grades', 'grades'),
'$mmSideMenuDelegate_mmaFiles' => new lang_string('files'),
'CoreUserDelegate_AddonBadges:account' => new lang_string('badges', 'badges'),
'CoreUserDelegate_AddonBlog:account' => new lang_string('blog', 'blog'),
'$mmSideMenuDelegate_mmaCompetency' => new lang_string('myplans', 'tool_lp'),
'CoreUserDelegate_CorePolicy' => new lang_string('policiesagreements', 'tool_policy'),
'CoreUserDelegate_CoreDataPrivacy' => new lang_string('pluginname', 'tool_dataprivacy'),
'NoDelegate_SwitchAccount' => new lang_string('switchaccount', 'tool_mobile'),
),
"$course" => array(
'$mmCoursesDelegate_mmaParticipants' => new lang_string('participants'),
'$mmCoursesDelegate_mmaGrades' => new lang_string('grades', 'grades'),
'$mmCoursesDelegate_mmaCompetency' => new lang_string('competencies', 'competency'),
'$mmCoursesDelegate_mmaNotes' => new lang_string('notes', 'notes'),
'$mmCoursesDelegate_mmaCourseCompletion' => new lang_string('coursecompletion', 'completion'),
'NoDelegate_CourseBlocks' => new lang_string('blocks'),
'CoreCourseOptionsDelegate_AddonBlog' => new lang_string('blog', 'blog'),
'$mmCoursesDelegate_search' => new lang_string('search'),
'NoDelegate_CoreCourseDownload' => new lang_string('downloadcourse', 'tool_mobile'),
'NoDelegate_CoreCoursesDownload' => new lang_string('downloadcourses', 'tool_mobile'),
),
"$participants" => array(
'$mmUserDelegate_mmaGrades:viewGrades' => new lang_string('grades', 'grades'),
'$mmUserDelegate_mmaCourseCompletion:viewCompletion' => new lang_string('coursecompletion', 'completion'),
'$mmUserDelegate_mmaBadges' => new lang_string('badges', 'badges'),
'$mmUserDelegate_mmaNotes:addNote' => new lang_string('notes', 'notes'),
'CoreUserDelegate_AddonBlog:blogs' => new lang_string('blog', 'blog'),
'$mmUserDelegate_mmaCompetency:learningPlan' => new lang_string('competencies', 'competency'),
'$mmUserDelegate_mmaMessages:sendMessage' => new lang_string('sendmessage', 'message'),
'$mmUserDelegate_picture' => new lang_string('userpic'),
),
"$files" => array(
'files_privatefiles' => new lang_string('privatefiles'),
'files_sitefiles' => new lang_string('sitefiles'),
'files_upload' => new lang_string('upload'),
),
"$modules" => $coursemodules,
"$blocks" => $courseblocks,
);
if (!empty($remoteaddonslist)) {
$features["$remoteaddons"] = $remoteaddonslist;
}
if (!empty($availablemods['lti'])) {
$ltidisplayname = $availablemods['lti']->displayname;
$features["$ltidisplayname"]['CoreCourseModuleDelegate_AddonModLti:launchViaSite'] =
new lang_string('launchviasiteinbrowser', 'tool_mobile');
}
// Display OAuth 2 identity providers.
if (is_enabled_auth('oauth2')) {
$identityproviderslist = array();
$idps = \auth_plugin_base::get_identity_providers(['oauth2']);
foreach ($idps as $idp) {
// Only add identity providers that have an ID.
$id = isset($idp['url']) ? $idp['url']->get_param('id') : null;
if ($id != null) {
$identityproviderslist['NoDelegate_IdentityProvider_' . $id] = $idp['name'];
}
}
if (!empty($identityproviderslist)) {
$features["$identityproviders"] = array();
if (count($identityproviderslist) > 1) {
// Include an option to disable them all.
$features["$identityproviders"]['NoDelegate_IdentityProviders'] = new lang_string('all');
}
$features["$identityproviders"] = array_merge($features["$identityproviders"], $identityproviderslist);
}
}
return $features;
}
/**
* This function check the current site for potential configuration issues that may prevent the mobile app to work.
*
* @return array list of potential issues
* @since Moodle 3.4
*/
public static function get_potential_config_issues() {
global $CFG;
require_once($CFG->dirroot . "/lib/filelib.php");
require_once($CFG->dirroot . '/message/lib.php');
$warnings = array();
if (is_https()) {
$curl = new curl();
// Return certificate information and verify the certificate.
$curl->setopt(array('CURLOPT_CERTINFO' => 1, 'CURLOPT_SSL_VERIFYPEER' => true));
// Check https using a page not redirecting or returning exceptions.
$curl->head("$CFG->wwwroot/$CFG->admin/tool/mobile/mobile.webmanifest.php");
$info = $curl->get_info();
// Check the certificate is not self-signed or has an untrusted-root.
// This may be weak in some scenarios (when the curl SSL verifier is outdated).
if (empty($info['http_code']) || empty($info['certinfo'])) {
$warnings[] = ['selfsignedoruntrustedcertificatewarning', 'tool_mobile'];
} else {
$timenow = time();
$infokeys = array_keys($info['certinfo']);
$lastkey = end($infokeys);
if (count($info['certinfo']) == 1) {
// This will work in a normal browser because it will complete the chain, but not in a mobile app.
$warnings[] = ['invalidcertificatechainwarning', 'tool_mobile'];
}
foreach ($info['certinfo'] as $key => $cert) {
// Convert to lower case the keys, some OS/curl implementations differ.
$cert = array_change_key_case($cert, CASE_LOWER);
// Due to a bug in certain curl/openssl versions the signature algorithm isn't always correctly parsed.
// See https://github.com/curl/curl/issues/3706 for reference.
if (!array_key_exists('signature algorithm', $cert)) {
// The malformed field that does contain the algorithm we're looking for looks like the following:
// <WHITESPACE>Signature Algorithm: <ALGORITHM><CRLF><ALGORITHM>.
preg_match('/\s+Signature Algorithm: (?<algorithm>[^\s]+)/', $cert['public key algorithm'], $matches);
$signaturealgorithm = $matches['algorithm'] ?? '';
} else {
$signaturealgorithm = $cert['signature algorithm'];
}
// Check if the signature algorithm is weak (Android won't work with SHA-1).
if ($key != $lastkey &&
($signaturealgorithm == 'sha1WithRSAEncryption' || $signaturealgorithm == 'sha1WithRSA')) {
$warnings['insecurealgorithmwarning'] = ['insecurealgorithmwarning', 'tool_mobile'];
}
// Check certificate start date.
if (strtotime($cert['start date']) > $timenow) {
$warnings['invalidcertificatestartdatewarning'] = ['invalidcertificatestartdatewarning', 'tool_mobile'];
}
// Check certificate end date.
if (strtotime($cert['expire date']) < $timenow) {
$warnings['invalidcertificateexpiredatewarning'] = ['invalidcertificateexpiredatewarning', 'tool_mobile'];
}
}
}
} else {
// Warning for non https sites.
$warnings[] = ['nohttpsformobilewarning', 'admin'];
}
// Check ADOdb debug enabled.
if (get_config('auth_db', 'debugauthdb') || get_config('enrol_database', 'debugdb')) {
$warnings[] = ['adodbdebugwarning', 'tool_mobile'];
}
// Check display errors on.
if (!empty($CFG->debugdisplay)) {
$warnings[] = ['displayerrorswarning', 'tool_mobile'];
}
// Check mobile notifications.
$processors = get_message_processors();
$enabled = false;
foreach ($processors as $processor => $status) {
if ($processor == 'airnotifier' && $status->enabled) {
$enabled = true;
}
}
if (!$enabled) {
$warnings[] = ['mobilenotificationsdisabledwarning', 'tool_mobile'];
}
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($mobilesettings);
$data .= '?qrlogin=' . $qrloginkey . '&userid=' . $USER->id;
}
$qrcode = new core_qrcode($data);
$imagedata = 'data:image/png;base64,' . base64_encode($qrcode->getBarcodePngData(5, 5));
return $imagedata;
}
/**
* Gets Moodle app plan subscription information for the current site as it is returned by the Apps Portal.
*
* @return array Subscription information
*/
public static function get_subscription_information(): ?array {
global $CFG;
// Use session cache to prevent multiple requests.
$cache = \cache::make('tool_mobile', 'subscriptiondata');
$subscriptiondata = $cache->get(0);
if ($subscriptiondata !== false) {
return $subscriptiondata;
}
$mobilesettings = get_config('tool_mobile');
// To validate that the requests come from this site we need to send some private information that only is known by the
// Moodle Apps portal or the Sites registration database.
$credentials = [];
if (!empty($CFG->airnotifieraccesskey)) {
$credentials[] = ['type' => 'airnotifieraccesskey', 'value' => $CFG->airnotifieraccesskey];
}
if (\core\hub\registration::is_registered()) {
$credentials[] = ['type' => 'siteid', 'value' => $CFG->siteidentifier];
}
// Generate a hash key for validating that the request is coming from this site via WS.
$key = complex_random_string(32);
$sitesubscriptionkey = json_encode(['validuntil' => time() + 10 * MINSECS, 'key' => $key]);
set_config('sitesubscriptionkey', $sitesubscriptionkey, 'tool_mobile');
$credentials[] = ['type' => 'sitesubscriptionkey', 'value' => $key];
// Parameters for the WebService returning site information.
$androidappid = empty($mobilesettings->androidappid) ? static::DEFAULT_ANDROID_APP_ID : $mobilesettings->androidappid;
$iosappid = empty($mobilesettings->iosappid) ? static::DEFAULT_IOS_APP_ID : $mobilesettings->iosappid;
$fnparams = (object) [
'siteurl' => $CFG->wwwroot,
'appids' => [$androidappid, $iosappid],
'credentials' => $credentials,
];
// Prepare the arguments for a request to the AJAX nologin endpoint.
$args = [
(object) [
'index' => 0,
'methodname' => 'local_apps_get_site_info',
'args' => $fnparams,
]
];
// Ask the Moodle Apps Portal for the subscription information.
$curl = new curl();
$curl->setopt(array('CURLOPT_TIMEOUT' => 10, 'CURLOPT_CONNECTTIMEOUT' => 10));
$serverurl = static::MOODLE_APPS_PORTAL_URL . "/lib/ajax/service-nologin.php";
$query = 'args=' . urlencode(json_encode($args));
$wsresponse = @json_decode($curl->post($serverurl, $query), true);
$info = $curl->get_info();
if ($curlerrno = $curl->get_errno()) {
// CURL connection error.
debugging("Unexpected response from the Moodle Apps Portal server, CURL error number: $curlerrno");
return null;
} else if ($info['http_code'] != 200) {
// Unexpected error from server.
debugging('Unexpected response from the Moodle Apps Portal server, HTTP code:' . $info['httpcode']);
return null;
} else if (!empty($wsresponse[0]['error'])) {
// Unexpected error from Moodle Apps Portal.
debugging('Unexpected response from the Moodle Apps Portal server:' . json_encode($wsresponse[0]));
return null;
} else if (empty($wsresponse[0]['data'])) {
debugging('Unexpected response from the Moodle Apps Portal server:' . json_encode($wsresponse));
return null;
}
$cache->set(0, $wsresponse[0]['data']);
return $wsresponse[0]['data'];
}
}
+760
View File
@@ -0,0 +1,760 @@
<?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/>.
/**
* This is the external API for this tool.
*
* @package tool_mobile
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_mobile;
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->dirroot/webservice/lib.php");
use core_external\external_api;
use core_external\external_files;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_settings;
use core_external\external_value;
use core_external\external_warnings;
use context_system;
use moodle_exception;
use moodle_url;
use core_user;
use coding_exception;
/**
* This is the external API for this tool.
*
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external extends external_api {
/**
* Returns description of get_plugins_supporting_mobile() parameters.
*
* @return external_function_parameters
* @since Moodle 3.1
*/
public static function get_plugins_supporting_mobile_parameters() {
return new external_function_parameters(array());
}
/**
* Returns a list of Moodle plugins supporting the mobile app.
*
* @return array an array of warnings and objects containing the plugin information
* @since Moodle 3.1
*/
public static function get_plugins_supporting_mobile() {
return array(
'plugins' => api::get_plugins_supporting_mobile(),
'warnings' => array(),
);
}
/**
* Returns description of get_plugins_supporting_mobile() result value.
*
* @return \core_external\external_description
* @since Moodle 3.1
*/
public static function get_plugins_supporting_mobile_returns() {
return new external_single_structure(
array(
'plugins' => new external_multiple_structure(
new external_single_structure(
array(
'component' => new external_value(PARAM_COMPONENT, 'The plugin component name.'),
'version' => new external_value(PARAM_NOTAGS, 'The plugin version number.'),
'addon' => new external_value(PARAM_COMPONENT, 'The Mobile addon (package) name.'),
'dependencies' => new external_multiple_structure(
new external_value(PARAM_COMPONENT, 'Mobile addon name.'),
'The list of Mobile addons this addon depends on.'
),
'fileurl' => new external_value(PARAM_URL, 'The addon package url for download
or empty if it doesn\'t exist.'),
'filehash' => new external_value(PARAM_RAW, 'The addon package hash or empty if it doesn\'t exist.'),
'filesize' => new external_value(PARAM_INT, 'The addon package size or empty if it doesn\'t exist.'),
'handlers' => new external_value(PARAM_RAW, 'Handlers definition (JSON)', VALUE_OPTIONAL),
'lang' => new external_value(PARAM_RAW, 'Language strings used by the handlers (JSON)', VALUE_OPTIONAL),
)
)
),
'warnings' => new external_warnings(),
)
);
}
/**
* Returns description of get_public_config() parameters.
*
* @return external_function_parameters
* @since Moodle 3.2
*/
public static function get_public_config_parameters() {
return new external_function_parameters(array());
}
/**
* Returns a list of the site public settings, those not requiring authentication.
*
* @return array with the settings and warnings
* @since Moodle 3.2
*/
public static function get_public_config() {
$result = api::get_public_config();
$result['warnings'] = array();
return $result;
}
/**
* Returns description of get_public_config() result value.
*
* @return \core_external\external_description
* @since Moodle 3.2
*/
public static function get_public_config_returns() {
return new external_single_structure(
array(
'wwwroot' => new external_value(PARAM_RAW, 'Site URL.'),
'httpswwwroot' => new external_value(PARAM_RAW, 'Site https URL (if httpslogin is enabled).'),
'sitename' => new external_value(PARAM_RAW, 'Site name.'),
'guestlogin' => new external_value(PARAM_INT, 'Whether guest login is enabled.'),
'rememberusername' => new external_value(PARAM_INT, 'Values: 0 for No, 1 for Yes, 2 for optional.'),
'authloginviaemail' => new external_value(PARAM_INT, 'Whether log in via email is enabled.'),
'registerauth' => new external_value(PARAM_PLUGIN, 'Authentication method for user registration.'),
'forgottenpasswordurl' => new external_value(PARAM_URL, 'Forgotten password URL.'),
'authinstructions' => new external_value(PARAM_RAW, 'Authentication instructions.'),
'authnoneenabled' => new external_value(PARAM_INT, 'Whether auth none is enabled.'),
'enablewebservices' => new external_value(PARAM_INT, 'Whether Web Services are enabled.'),
'enablemobilewebservice' => new external_value(PARAM_INT, 'Whether the Mobile service is enabled.'),
'maintenanceenabled' => new external_value(PARAM_INT, 'Whether site maintenance is enabled.'),
'maintenancemessage' => new external_value(PARAM_RAW, 'Maintenance message.'),
'logourl' => new external_value(PARAM_URL, 'The site logo URL', VALUE_OPTIONAL),
'compactlogourl' => new external_value(PARAM_URL, 'The site compact logo URL', VALUE_OPTIONAL),
'typeoflogin' => new external_value(PARAM_INT, 'The type of login. 1 for app, 2 for browser, 3 for embedded.'),
'launchurl' => new external_value(PARAM_URL, 'SSO login launch URL.', VALUE_OPTIONAL),
'mobilecssurl' => new external_value(PARAM_URL, 'Mobile custom CSS theme', VALUE_OPTIONAL),
'tool_mobile_disabledfeatures' => new external_value(PARAM_RAW, 'Disabled features in the app', VALUE_OPTIONAL),
'identityproviders' => new external_multiple_structure(
new external_single_structure(
array(
'name' => new external_value(PARAM_TEXT, 'The identity provider name.'),
'iconurl' => new external_value(PARAM_URL, 'The icon URL for the provider.'),
'url' => new external_value(PARAM_URL, 'The URL of the provider.'),
)
),
'Identity providers', VALUE_OPTIONAL
),
'country' => new external_value(PARAM_NOTAGS, 'Default site country', VALUE_OPTIONAL),
'agedigitalconsentverification' => new external_value(PARAM_BOOL, 'Whether age digital consent verification
is enabled.', VALUE_OPTIONAL),
'supportname' => new external_value(PARAM_NOTAGS, 'Site support contact name
(only if age verification is enabled).', VALUE_OPTIONAL),
'supportemail' => new external_value(PARAM_EMAIL, 'Site support contact email
(only if age verification is enabled).', VALUE_OPTIONAL),
'supportpage' => new external_value(PARAM_URL, 'Site support page link.', VALUE_OPTIONAL),
'supportavailability' => new external_value(PARAM_INT, 'Determines who has access to contact site support.',
VALUE_OPTIONAL),
'autolang' => new external_value(PARAM_INT, 'Whether to detect default language
from browser setting.', VALUE_OPTIONAL),
'lang' => new external_value(PARAM_LANG, 'Default language for the site.', VALUE_OPTIONAL),
'langmenu' => new external_value(PARAM_INT, 'Whether the language menu should be displayed.', VALUE_OPTIONAL),
'langlist' => new external_value(PARAM_RAW, 'Languages on language menu.', VALUE_OPTIONAL),
'locale' => new external_value(PARAM_RAW, 'Sitewide locale.', VALUE_OPTIONAL),
'tool_mobile_minimumversion' => new external_value(PARAM_NOTAGS, 'Minimum required version to access.',
VALUE_OPTIONAL),
'tool_mobile_iosappid' => new external_value(PARAM_ALPHANUM, 'iOS app\'s unique identifier.',
VALUE_OPTIONAL),
'tool_mobile_androidappid' => new external_value(PARAM_NOTAGS, 'Android app\'s unique identifier.',
VALUE_OPTIONAL),
'tool_mobile_setuplink' => new external_value(PARAM_URL, 'App download page.', VALUE_OPTIONAL),
'tool_mobile_qrcodetype' => new external_value(PARAM_INT, 'QR login configuration.', VALUE_OPTIONAL),
'warnings' => new external_warnings(),
)
);
}
/**
* Returns description of get_config() parameters.
*
* @return external_function_parameters
* @since Moodle 3.2
*/
public static function get_config_parameters() {
return new external_function_parameters(
array(
'section' => new external_value(PARAM_ALPHANUMEXT, 'Settings section name.', VALUE_DEFAULT, ''),
)
);
}
/**
* Returns a list of site settings, filtering by section.
*
* @param string $section settings section name
* @return array with the settings and warnings
* @since Moodle 3.2
*/
public static function get_config($section = '') {
$params = self::validate_parameters(self::get_config_parameters(), array('section' => $section));
$settings = api::get_config($params['section']);
$result['settings'] = array();
foreach ($settings as $name => $value) {
$result['settings'][] = array(
'name' => $name,
'value' => $value,
);
}
$result['warnings'] = array();
return $result;
}
/**
* Returns description of get_config() result value.
*
* @return \core_external\external_description
* @since Moodle 3.2
*/
public static function get_config_returns() {
return new external_single_structure(
array(
'settings' => new external_multiple_structure(
new external_single_structure(
array(
'name' => new external_value(PARAM_RAW, 'The name of the setting'),
'value' => new external_value(PARAM_RAW, 'The value of the setting'),
)
),
'Settings'
),
'warnings' => new external_warnings(),
)
);
}
/**
* Returns description of get_autologin_key() parameters.
*
* @return external_function_parameters
* @since Moodle 3.2
*/
public static function get_autologin_key_parameters() {
return new external_function_parameters (
array(
'privatetoken' => new external_value(PARAM_ALPHANUM, 'Private token, usually generated by login/token.php'),
)
);
}
/**
* Creates an auto-login key for the current user. Is created only in https sites and is restricted by time and ip address.
*
* Please note that it only works if the request comes from the Moodle mobile or desktop app.
*
* @param string $privatetoken the user private token for validating the request
* @return array with the settings and warnings
* @since Moodle 3.2
*/
public static function get_autologin_key($privatetoken) {
global $CFG, $DB, $USER;
$params = self::validate_parameters(self::get_autologin_key_parameters(), array('privatetoken' => $privatetoken));
$privatetoken = $params['privatetoken'];
$context = context_system::instance();
// We must toletare these two exceptions: forcepasswordchangenotice and usernotfullysetup.
try {
self::validate_context($context);
} catch (moodle_exception $e) {
if ($e->errorcode != 'usernotfullysetup' && $e->errorcode != 'forcepasswordchangenotice') {
// In case we receive a different exception, throw it.
throw $e;
}
}
// 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($USER->id);
if (isset($_GET['privatetoken']) or empty($privatetoken)) {
throw new moodle_exception('invalidprivatetoken', 'tool_mobile');
}
// Check the request counter, we must limit the number of times the privatetoken is sent.
// Between each request 6 minutes are required.
$last = get_user_preferences('tool_mobile_autologin_request_last', 0, $USER);
// Check if we must reset the count.
$mintimereq = get_config('tool_mobile', 'autologinmintimebetweenreq');
$mintimereq = empty($mintimereq) ? 6 * MINSECS : $mintimereq;
$timenow = time();
if ($timenow - $last < $mintimereq) {
$minutes = $mintimereq / MINSECS;
throw new moodle_exception('autologinkeygenerationlockout', 'tool_mobile', '', $minutes);
}
set_user_preference('tool_mobile_autologin_request_last', $timenow, $USER);
// We are expecting a privatetoken linked to the current token being used.
// This WS is only valid when using mobile services via REST (this is intended).
$currenttoken = required_param('wstoken', PARAM_ALPHANUM);
$conditions = array(
'userid' => $USER->id,
'token' => $currenttoken,
'privatetoken' => $privatetoken,
);
if (!$token = $DB->get_record('external_tokens', $conditions)) {
throw new moodle_exception('invalidprivatetoken', 'tool_mobile');
}
$result = array();
$result['key'] = api::get_autologin_key();
$autologinurl = new moodle_url("/$CFG->admin/tool/mobile/autologin.php");
$result['autologinurl'] = $autologinurl->out(false);
$result['warnings'] = array();
return $result;
}
/**
* Returns description of get_autologin_key() result value.
*
* @return \core_external\external_description
* @since Moodle 3.2
*/
public static function get_autologin_key_returns() {
return new external_single_structure(
array(
'key' => new external_value(PARAM_ALPHANUMEXT, 'Auto-login key for a single usage with time expiration.'),
'autologinurl' => new external_value(PARAM_URL, 'Auto-login URL.'),
'warnings' => new external_warnings(),
)
);
}
/**
* Returns description of get_content() parameters
*
* @return external_function_parameters
* @since Moodle 3.5
*/
public static function get_content_parameters() {
return new external_function_parameters(
array(
'component' => new external_value(PARAM_COMPONENT, 'Component where the class is e.g. mod_assign.'),
'method' => new external_value(PARAM_ALPHANUMEXT, 'Method to execute in class \$component\output\mobile.'),
'args' => new external_multiple_structure(
new external_single_structure(
array(
'name' => new external_value(PARAM_ALPHANUMEXT, 'Param name.'),
'value' => new external_value(PARAM_RAW, 'Param value.')
)
), 'Args for the method are optional.', VALUE_OPTIONAL
)
)
);
}
/**
* Returns a piece of content to be displayed in the Mobile app, it usually returns a template, javascript and
* other structured data that will be used to render a view in the Mobile app.
*
* Callbacks (placed in \$component\output\mobile) that are called by this web service are responsible for doing the
* appropriate security checks to access the information to be returned.
*
* @param string $component name of the component.
* @param string $method function method name in class \$component\output\mobile.
* @param array $args optional arguments for the method.
* @return array HTML, JavaScript and other required data and information to create a view in the app.
* @since Moodle 3.5
* @throws coding_exception
*/
public static function get_content($component, $method, $args = array()) {
global $OUTPUT, $PAGE, $USER;
$params = self::validate_parameters(self::get_content_parameters(),
array(
'component' => $component,
'method' => $method,
'args' => $args
)
);
// Reformat arguments into something less unwieldy.
$arguments = array();
foreach ($params['args'] as $paramargument) {
$arguments[$paramargument['name']] = $paramargument['value'];
}
// The component was validated via the PARAM_COMPONENT parameter type.
$classname = '\\' . $params['component'] .'\output\mobile';
if (!method_exists($classname, $params['method'])) {
throw new coding_exception("Missing method in $classname");
}
$result = call_user_func_array(array($classname, $params['method']), array($arguments));
// Populate otherdata.
$otherdata = array();
if (!empty($result['otherdata'])) {
$result['otherdata'] = (array) $result['otherdata'];
foreach ($result['otherdata'] as $name => $value) {
$otherdata[] = array(
'name' => $name,
'value' => $value
);
}
}
return array(
'templates' => !empty($result['templates']) ? $result['templates'] : array(),
'javascript' => !empty($result['javascript']) ? $result['javascript'] : '',
'otherdata' => $otherdata,
'files' => !empty($result['files']) ? $result['files'] : array(),
'restrict' => !empty($result['restrict']) ? $result['restrict'] : array(),
'disabled' => !empty($result['disabled']) ? true : false,
);
}
/**
* Returns description of get_content() result value
*
* @return array
* @since Moodle 3.5
*/
public static function get_content_returns() {
return new external_single_structure(
array(
'templates' => new external_multiple_structure(
new external_single_structure(
array(
'id' => new external_value(PARAM_TEXT, 'ID of the template.'),
'html' => new external_value(PARAM_RAW, 'HTML code.'),
)
),
'Templates required by the generated content.'
),
'javascript' => new external_value(PARAM_RAW, 'JavaScript code.'),
'otherdata' => new external_multiple_structure(
new external_single_structure(
array(
'name' => new external_value(PARAM_RAW, 'Field name.'),
'value' => new external_value(PARAM_RAW, 'Field value.')
)
),
'Other data that can be used or manipulated by the template via 2-way data-binding.'
),
'files' => new external_files('Files in the content.'),
'restrict' => new external_single_structure(
array(
'users' => new external_multiple_structure(
new external_value(PARAM_INT, 'user id'), 'List of allowed users.', VALUE_OPTIONAL
),
'courses' => new external_multiple_structure(
new external_value(PARAM_INT, 'course id'), 'List of allowed courses.', VALUE_OPTIONAL
),
),
'Restrict this content to certain users or courses.'
),
'disabled' => new external_value(PARAM_BOOL, 'Whether we consider this disabled or not.', VALUE_OPTIONAL),
)
);
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.7
*/
public static function call_external_functions_parameters() {
return new external_function_parameters([
'requests' => new external_multiple_structure(
new external_single_structure([
'function' => new external_value(PARAM_ALPHANUMEXT, 'Function name'),
'arguments' => new external_value(PARAM_RAW, 'JSON-encoded object with named arguments', VALUE_DEFAULT, '{}'),
'settingraw' => new external_value(PARAM_BOOL, 'Return raw text', VALUE_DEFAULT, false),
'settingfilter' => new external_value(PARAM_BOOL, 'Filter text', VALUE_DEFAULT, false),
'settingfileurl' => new external_value(PARAM_BOOL, 'Rewrite plugin file URLs', VALUE_DEFAULT, true),
'settinglang' => new external_value(PARAM_LANG, 'Session language', VALUE_DEFAULT, ''),
])
)
]);
}
/**
* Call multiple external functions and return all responses.
*
* @param array $requests List of requests.
* @return array Responses.
* @since Moodle 3.7
*/
public static function call_external_functions($requests) {
global $SESSION;
$params = self::validate_parameters(self::call_external_functions_parameters(), ['requests' => $requests]);
// We need to check if the functions being called are included in the service of the current token.
// This function only works when using mobile services via REST (this is intended).
$webservicemanager = new \webservice;
$token = $webservicemanager->get_user_ws_token(required_param('wstoken', PARAM_ALPHANUM));
$settings = external_settings::get_instance();
$defaultlang = current_language();
$responses = [];
foreach ($params['requests'] as $request) {
// Some external functions modify _GET or $_POST data, we need to restore the original data after each call.
$originalget = fullclone($_GET);
$originalpost = fullclone($_POST);
// Set external settings and language.
$settings->set_raw($request['settingraw']);
$settings->set_filter($request['settingfilter']);
$settings->set_fileurl($request['settingfileurl']);
$settings->set_lang($request['settinglang']);
$SESSION->lang = $request['settinglang'] ?: $defaultlang;
// Parse arguments to an array, validation is done in external_api::call_external_function.
$args = @json_decode($request['arguments'], true);
if (!is_array($args)) {
$args = [];
}
if ($webservicemanager->service_function_exists($request['function'], $token->externalserviceid)) {
$response = external_api::call_external_function($request['function'], $args, false);
} else {
// Function not included in the service, return an access exception.
$response = [
'error' => true,
'exception' => [
'errorcode' => 'accessexception',
'module' => 'webservice'
]
];
if (debugging('', DEBUG_DEVELOPER)) {
$response['exception']['debuginfo'] = 'Access to the function is not allowed.';
}
}
if (isset($response['data'])) {
$response['data'] = json_encode($response['data']);
}
if (isset($response['exception'])) {
$response['exception'] = json_encode($response['exception']);
}
$responses[] = $response;
// Restore original $_GET and $_POST.
$_GET = $originalget;
$_POST = $originalpost;
if ($response['error']) {
// Do not process the remaining requests.
break;
}
}
return ['responses' => $responses];
}
/**
* Returns description of method result value
*
* @return external_single_structure
* @since Moodle 3.7
*/
public static function call_external_functions_returns() {
return new external_function_parameters([
'responses' => new external_multiple_structure(
new external_single_structure([
'error' => new external_value(PARAM_BOOL, 'Whether an exception was thrown.'),
'data' => new external_value(PARAM_RAW, 'JSON-encoded response data', VALUE_OPTIONAL),
'exception' => new external_value(PARAM_RAW, 'JSON-encoed exception info', VALUE_OPTIONAL),
])
)
]);
}
/**
* 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/qrlogin', null);
delete_user_key('tool_mobile/qrlogin', $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 = \core_external\util::generate_token_for_current_user($service);
$privatetoken = $token->privatetoken; // Save it here, the next function removes it.
\core_external\util::log_token_request($token);
$result = [
'token' => $token->token,
'privatetoken' => $privatetoken ?: '',
'warnings' => [],
];
return $result;
}
/**
* Returns description of get_tokens_for_qr_login() result value.
*
* @return \core_external\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(),
]
);
}
/**
* Returns description of validate_subscription_key() parameters.
*
* @return external_function_parameters
* @since Moodle 3.9
*/
public static function validate_subscription_key_parameters() {
return new external_function_parameters(
[
'key' => new external_value(PARAM_RAW, 'Site subscription temporary key.'),
]
);
}
/**
* Check if the given site subscription key is valid
*
* @param string $key subscriptiion temporary key
* @return array with the settings and warnings
* @since Moodle 3.9
*/
public static function validate_subscription_key(string $key): array {
global $CFG, $PAGE;
$params = self::validate_parameters(self::validate_subscription_key_parameters(), ['key' => $key]);
$context = context_system::instance();
$PAGE->set_context($context);
$validated = false;
$sitesubscriptionkey = get_config('tool_mobile', 'sitesubscriptionkey');
if (!empty($sitesubscriptionkey) && $CFG->enablemobilewebservice && empty($CFG->disablemobileappsubscription)) {
$sitesubscriptionkey = json_decode($sitesubscriptionkey);
$validated = time() < $sitesubscriptionkey->validuntil && $params['key'] === $sitesubscriptionkey->key;
// Delete existing, even if not validated to enforce security and attacks prevention.
unset_config('sitesubscriptionkey', 'tool_mobile');
}
return [
'validated' => $validated,
'warnings' => [],
];
}
/**
* Returns description of validate_subscription_key() result value.
*
* @return \core_external\external_description
* @since Moodle 3.9
*/
public static function validate_subscription_key_returns() {
return new external_single_structure(
[
'validated' => new external_value(PARAM_BOOL, 'Whether the key is validated or not.'),
'warnings' => new external_warnings(),
]
);
}
}
@@ -0,0 +1,111 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_mobile;
use core\session\utility\cookie_helper;
use html_writer;
/**
* Allows plugins to add any elements to the footer.
*
* @package tool_mobile
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class hook_callbacks {
/**
* Callback to add head elements.
*
* @param \core\hook\output\before_standard_head_html_generation $hook
*/
public static function before_standard_head_html_generation(
\core\hook\output\before_standard_head_html_generation $hook,
): void {
global $CFG, $PAGE;
// Smart App Banners meta tag is only displayed if mobile services are enabled and configured.
if (!empty($CFG->enablemobilewebservice)) {
$mobilesettings = get_config('tool_mobile');
if (!empty($mobilesettings->enablesmartappbanners)) {
if (!empty($mobilesettings->iosappid)) {
$hook->add_html(
'<meta name="apple-itunes-app" content="app-id=' . s($mobilesettings->iosappid) . ', ' .
'app-argument=' . $PAGE->url->out() . '"/>'
);
}
if (!empty($mobilesettings->androidappid)) {
$mobilemanifesturl = "$CFG->wwwroot/$CFG->admin/tool/mobile/mobile.webmanifest.php";
$hook->add_html('<link rel="manifest" href="' . $mobilemanifesturl . '" />');
}
}
}
}
/**
* Callback to add head elements.
*
* @param \core\hook\output\before_standard_footer_html_generation $hook
*/
public static function before_standard_footer_html_generation(
\core\hook\output\before_standard_footer_html_generation $hook,
): void {
global $CFG;
require_once(__DIR__ . '/../lib.php');
if (empty($CFG->enablemobilewebservice)) {
return;
}
$url = tool_mobile_create_app_download_url();
if (empty($url)) {
return;
}
$hook->add_html(
html_writer::div(
html_writer::link($url, get_string('getmoodleonyourmobile', 'tool_mobile'), ['class' => 'mobilelink']),
),
);
}
/**
* Callback to recover $SESSION->wantsurl.
*
* @param \core_user\hook\after_login_completed $hook
*/
public static function after_login_completed(
\core_user\hook\after_login_completed $hook,
): void {
global $SESSION, $CFG;
// Check if the user is doing a mobile app launch, if that's the case, ensure $SESSION->wantsurl is correctly set.
if (!NO_MOODLE_COOKIES && !empty($_COOKIE['tool_mobile_launch'])) {
if (empty($SESSION->wantsurl) || strpos($SESSION->wantsurl, '/tool/mobile/launch.php') === false) {
$params = json_decode($_COOKIE['tool_mobile_launch'], true);
$SESSION->wantsurl = (new \moodle_url("/$CFG->admin/tool/mobile/launch.php", $params))->out(false);
}
}
// Set Partitioned and Secure attributes to the MoodleSession cookie if the user is using the Moodle app.
if (\core_useragent::is_moodle_app()) {
cookie_helper::add_attributes_to_cookie_response_header(
'MoodleSession' . $CFG->sessioncookie,
['Secure', 'Partitioned'],
);
}
}
}
@@ -0,0 +1,41 @@
<?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/>.
namespace tool_mobile\local\hooks\output;
use core\session\utility\cookie_helper;
/**
* Allows plugins to modify headers.
*
* @package tool_mobile
* @copyright 2024 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class before_http_headers {
/**
* Callback to allow modify headers.
*
* @param \core\hook\output\before_http_headers $hook
*/
public static function callback(\core\hook\output\before_http_headers $hook): void {
global $CFG;
// Set Partitioned and Secure attributes to the MoodleSession cookie if the user is using the Moodle app.
if (\core_useragent::is_moodle_app()) {
cookie_helper::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, ['Secure', 'Partitioned']);
}
}
}
@@ -0,0 +1,51 @@
<?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/>.
namespace tool_mobile\local\hooks\user;
/**
* Handles mobile app launches when third-party auth plugins are put in front of MFA.
*
* @package tool_mobile
* @copyright 2024 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class after_user_passed_mfa {
/**
* Callback to recover $SESSION->wantsurl.
*
* @param \tool_mfa\hook\after_user_passed_mfa $hook
*/
public static function callback(\tool_mfa\hook\after_user_passed_mfa $hook): void {
global $SESSION, $CFG;
// Check if the user is doing a mobile app launch, if that's the case, ensure $SESSION->wantsurl is correctly set.
if (!NO_MOODLE_COOKIES && !empty($_COOKIE['tool_mobile_launch'])) {
if (empty($SESSION->wantsurl) || strpos($SESSION->wantsurl, '/tool/mobile/launch.php') === false) {
$params = json_decode($_COOKIE['tool_mobile_launch'], true);
$SESSION->wantsurl = (new \moodle_url("/$CFG->admin/tool/mobile/launch.php", $params))->out(false);
$SESSION->tool_mfa_has_been_redirected = true; // Indicate MFA that they need to follow $SESSION->wantsurl.
}
// Invalidate cookie as we won't be needing it anymore.
unset($_COOKIE['tool_mobile_launch']);
if (!headers_sent()) { // Just be very cautios as this is a critical code.
setcookie('tool_mobile_launch', '', -1, $CFG->sessioncookiepath);
}
}
}
}
@@ -0,0 +1,50 @@
<?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/>.
/**
* Renderer.
*
* @package tool_mobile
* @copyright 2020 Moodle Pty Ltd
* @author <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_mobile\output;
use plugin_renderer_base;
/**
* Renderer class.
*
* @package tool_mobile
* @copyright 2020 Moodle Pty Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends plugin_renderer_base {
/**
* Defer to template.
*
* @param \tool_mobile\output\subscription $subscription Subscription
* @return string HTML
*/
protected function render_subscription(\tool_mobile\output\subscription $subscription): string {
$data = $subscription->export_for_template($this);
return parent::render_from_template('tool_mobile/subscription', $data);
}
}
@@ -0,0 +1,213 @@
<?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/>.
/**
* Subscription page.
*
* @package tool_mobile
* @copyright 2020 Moodle Pty Ltd
* @author <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_mobile\output;
/**
* Subscription page.
*
* @package tool_mobile
* @copyright 2020 Moodle Pty Ltd
* @author <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class subscription implements \renderable, \templatable {
/**
* Subscription data.
*
* @var array subscription data
*/
protected $subscriptiondata;
/**
* Constructor for the class, sets the subscription data.
*
* @param array $subscriptiondata subscription data
* @return void
*/
public function __construct(array $subscriptiondata) {
$this->subscriptiondata = $subscriptiondata;
}
/**
* Exports the data.
*
* @param \renderer_base $output
* @return array with the subscription information
*/
public function export_for_template(\renderer_base $output): array {
global $CFG;
$ms = get_config('tool_mobile'); // Get mobile settings.
$data = $this->subscriptiondata;
$data['appsportalurl'] = \tool_mobile\api::MOODLE_APPS_PORTAL_URL;
// First prepare messages that may come from the WS.
if (!empty($data['messages'])) {
foreach ($data['messages'] as $msg) {
$data['messages' . $msg['type']][] = ['message' => $msg['message']];
}
}
unset($data['messages']);
// Now prepare statistics information.
if (isset($data['statistics']['notifications'])) {
$data['notifications'] = $data['statistics']['notifications'];
unset($data['statistics']['notifications']);
// Find current month data.
$data['notifications']['currentactivedevices'] = 0;
if (isset($data['notifications']['monthly'][0])) {
$currentmonth = $data['notifications']['monthly'][0];
$data['notifications']['currentactivedevices'] = $currentmonth['activedevices'];
if (!empty($currentmonth['limitreachedtime'])) {
$data['notifications']['limitreachedtime'] = $currentmonth['limitreachedtime'];
$data['notifications']['ignorednotificationswarning'] = [
'message' => get_string('notificationslimitreached', 'tool_mobile', $data['appsportalurl'])
];
}
}
}
// Review features.
foreach ($data['subscription']['features'] as &$feature) {
// Check the type of features, if it is a limitation or functionality feature.
if (array_key_exists('limit', $feature)) {
if (empty($feature['limit'])) { // Unlimited, no need to calculate current values.
$feature['humanstatus'] = get_string('unlimited');
$feature['showbar'] = 0;
continue;
}
switch ($feature['name']) {
// Check active devices.
case 'pushnotificationsdevices':
if (isset($data['notifications']['currentactivedevices'])) {
$feature['status'] = $data['notifications']['currentactivedevices'];
}
break;
// Check menu items.
case 'custommenuitems':
$custommenuitems = [];
$els = rtrim($ms->custommenuitems, "\n");
if (!empty($els)) {
$custommenuitems = explode("\n", $els);
// Get unique custom menu urls.
$custommenuitems = array_flip(
array_map(function($val) {
return explode('|', $val)[1];
}, $custommenuitems)
);
}
$feature['status'] = count($custommenuitems);
break;
// Check language strings.
case 'customlanguagestrings':
$langstrings = [];
$els = rtrim($ms->customlangstrings, "\n");
if (!empty($els)) {
$langstrings = explode("\n", $els);
// Get unique language string ids.
$langstrings = array_flip(
array_map(function($val) {
return explode('|', $val)[0];
}, $langstrings)
);
}
$feature['status'] = count($langstrings);
break;
// Check disabled features strings.
case 'disabledfeatures':
$feature['status'] = empty($ms->disabledfeatures) ? 0 : count(explode(',', $ms->disabledfeatures));
break;
}
$feature['humanstatus'] = '?/' . $feature['limit'];
// Check if we should display the bar and how.
if (isset($feature['status']) && is_int($feature['status'])) {
$feature['humanstatus'] = $feature['status'] . '/' . $feature['limit'];
$feature['showbar'] = 1;
if ($feature['status'] == $feature['limit']) {
$feature['barclass'] = 'bg-warning';
}
if ($feature['status'] > $feature['limit']) {
$feature['barclass'] = 'bg-danger';
$feature['humanstatus'] .= ' - ' . get_string('subscriptionlimitsurpassed', 'tool_mobile');
}
}
} else {
$feature['humanstatus'] = empty($feature['enabled']) ? get_string('notincluded') : get_string('included');
if (empty($feature['enabled'])) {
switch ($feature['name']) {
// Check remote themes.
case 'remotethemes':
if (!empty($CFG->mobilecssurl)) {
$feature['message'] = [
'type' => 'danger', 'message' => get_string('subscriptionfeaturenotapplied', 'tool_mobile')];
}
break;
// Check site logo.
case 'sitelogo':
if ($output->get_logo_url() || $output->get_compact_logo_url()) {
$feature['message'] = [
'type' => 'danger', 'message' => get_string('subscriptionfeaturenotapplied', 'tool_mobile')];
}
break;
// Check QR automatic login.
case 'qrautomaticlogin':
if ($ms->qrcodetype == \tool_mobile\api::QR_CODE_LOGIN) {
$feature['message'] = [
'type' => 'danger', 'message' => get_string('subscriptionfeaturenotapplied', 'tool_mobile')];
}
break;
}
}
}
}
usort($data['subscription']['features'],
function (array $featurea, array $featureb) {
$isfeaturea = !array_key_exists('limit', $featurea);
$isfeatureb = !array_key_exists('limit', $featureb);
if (!$isfeaturea && $isfeatureb) {
return 1;
}
return 0;
}
);
return $data;
}
}
@@ -0,0 +1,187 @@
<?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/>.
/**
* Privacy Subsystem implementation for tool_mobile.
*
* @package tool_mobile
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_mobile\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\local\request\writer;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\userlist;
/**
* Privacy provider for tool_mobile.
*
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\user_preference_provider,
\core_privacy\local\request\plugin\provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised item collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
// There is a one user preference.
$collection->add_user_preference('tool_mobile_autologin_request_last',
'privacy:metadata:preference:tool_mobile_autologin_request_last');
$collection->add_subsystem_link('core_userkey', [], 'privacy:metadata:core_userkey');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
$sql = "SELECT ctx.id
FROM {user_private_key} k
JOIN {user} u ON k.userid = u.id
JOIN {context} ctx ON ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel
WHERE k.userid = :userid AND (k.script = 'tool_mobile' OR k.script = 'tool_mobile/qrlogin')";
$params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER];
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, $params);
return $contextlist;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
if (!is_a($context, \context_user::class)) {
return;
}
// Add users based on userkey.
\core_userkey\privacy\provider::get_user_contexts_with_script($userlist, $context, 'tool_mobile');
\core_userkey\privacy\provider::get_user_contexts_with_script($userlist, $context, 'tool_mobile/qrlogin');
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
// If the user has data, then only the CONTEXT_USER should be present so get the first context.
$contexts = $contextlist->get_contexts();
if (count($contexts) == 0) {
return;
}
$context = reset($contexts);
// Sanity check that context is at the user context level, then get the userid.
if ($context->contextlevel !== CONTEXT_USER) {
return;
}
// Export associated userkeys.
\core_userkey\privacy\provider::export_userkeys($context, [], 'tool_mobile');
\core_userkey\privacy\provider::export_userkeys($context, [], 'tool_mobile/qrlogin');
}
/**
* Export all user preferences for the plugin.
*
* @param int $userid The userid of the user whose data is to be exported.
*/
public static function export_user_preferences(int $userid) {
$autologinrequestlast = get_user_preferences('tool_mobile_autologin_request_last', null, $userid);
if ($autologinrequestlast !== null) {
$time = transform::datetime($autologinrequestlast);
writer::export_user_preference('tool_mobile',
'tool_mobile_autologin_request_last',
$time,
get_string('privacy:metadata:preference:tool_mobile_autologin_request_last', 'tool_mobile')
);
}
}
/**
* Delete all use data which matches the specified deletion_criteria.
*
* @param context $context A user context.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
// Sanity check that context is at the user context level, then get the userid.
if ($context->contextlevel !== CONTEXT_USER) {
return;
}
$userid = $context->instanceid;
// Delete all the userkeys.
\core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
\core_userkey\privacy\provider::delete_userkeys('tool_mobile/qrlogin', $userid);
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
// If the user has data, then only the user context should be present so get the first context.
$contexts = $contextlist->get_contexts();
if (count($contexts) == 0) {
return;
}
$context = reset($contexts);
// Sanity check that context is at the user context level, then get the userid.
if ($context->contextlevel !== CONTEXT_USER) {
return;
}
$userid = $context->instanceid;
// Delete all the userkeys.
\core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
\core_userkey\privacy\provider::delete_userkeys('tool_mobile/qrlogin', $userid);
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
$context = $userlist->get_context();
$userids = $userlist->get_userids();
$userid = reset($userids);
// Only deleting data for the user ID in that user's user context should be valid.
if ($context->contextlevel !== CONTEXT_USER || count($userids) != 1 || $userid != $context->instanceid) {
return;
}
// Delete all the userkeys.
\core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
\core_userkey\privacy\provider::delete_userkeys('tool_mobile/qrlogin', $userid);
}
}