Merge branch 'MDL-68571-master' of git://github.com/cescobedo/moodle
authorSara Arjona <sara@moodle.com>
Mon, 11 May 2020 14:52:48 +0000 (16:52 +0200)
committerSara Arjona <sara@moodle.com>
Mon, 11 May 2020 14:52:48 +0000 (16:52 +0200)
40 files changed:
admin/cli/adhoc_task.php [new file with mode: 0644]
admin/cli/scheduled_task.php [new file with mode: 0644]
admin/settings/appearance.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/db/services.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/lib.php
admin/tool/mobile/settings.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/version.php
admin/tool/task/classes/run_from_cli.php
admin/tool/task/cli/adhoc_task.php
admin/tool/task/cli/schedule_task.php
admin/tool/task/renderer.php
admin/tool/task/schedule_task.php
h5p/classes/core.php
h5p/classes/editor_ajax.php
h5p/classes/editor_framework.php
h5p/classes/helper.php
h5p/tests/editor_ajax_test.php
h5p/tests/editor_framework_test.php
h5p/tests/generator/lib.php
lang/en/admin.php
lang/en/cache.php
lib/classes/qrcode.php [new file with mode: 0644]
lib/classes/task/manager.php
lib/cronlib.php
lib/db/caches.php
lib/deprecatedlib.php
lib/outputrenderers.php
lib/tests/behat/securelayout.feature
lib/tests/qrcode_test.php [new file with mode: 0644]
lib/upgrade.txt
repository/filepicker.js
repository/tests/behat/edit_file.feature [new file with mode: 0644]
repository/tests/behat/select_file.feature [new file with mode: 0644]
theme/boost/templates/navbar-secure.mustache
theme/classic/templates/navbar-secure.mustache
theme/upgrade.txt

diff --git a/admin/cli/adhoc_task.php b/admin/cli/adhoc_task.php
new file mode 100644 (file)
index 0000000..b0ed21d
--- /dev/null
@@ -0,0 +1,122 @@
+<?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/>.
+
+/**
+ * Task executor for adhoc tasks.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+require_once("{$CFG->libdir}/clilib.php");
+require_once("{$CFG->libdir}/cronlib.php");
+
+list($options, $unrecognized) = cli_get_params(
+    [
+        'execute' => false,
+        'help' => false,
+        'keep-alive' => 0,
+        'showsql' => false,
+        'showdebugging' => false,
+        'ignorelimits' => false,
+    ], [
+        'h' => 'help',
+        'e' => 'execute',
+        'k' => 'keep-alive',
+        'i' => 'ignorelimits',
+    ]
+);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help'] or empty($options['execute'])) {
+    $help = <<<EOT
+Ad hoc cron tasks.
+
+Options:
+ -h, --help                Print out this help
+     --showsql             Show sql queries before they are executed
+     --showdebugging       Show developer level debugging information
+ -e, --execute             Run all queued adhoc tasks
+ -k, --keep-alive=N        Keep this script alive for N seconds and poll for new adhoc tasks
+ -i  --ignorelimits        Ignore task_adhoc_concurrency_limit and task_adhoc_max_runtime limits
+
+Example:
+\$sudo -u www-data /usr/bin/php admin/cli/adhoc_task.php --execute
+
+EOT;
+
+    echo $help;
+    die;
+}
+
+if ($options['showdebugging']) {
+    set_debugging(DEBUG_DEVELOPER, true);
+}
+
+if ($options['showsql']) {
+    $DB->set_debug(true);
+}
+
+if (CLI_MAINTENANCE) {
+    echo "CLI maintenance mode active, cron execution suspended.\n";
+    exit(1);
+}
+
+if (moodle_needs_upgrading()) {
+    echo "Moodle upgrade pending, cron execution suspended.\n";
+    exit(1);
+}
+
+if (empty($options['execute'])) {
+    exit(0);
+}
+if (empty($options['keep-alive'])) {
+    $options['keep-alive'] = 0;
+}
+
+if (!empty($CFG->showcronsql)) {
+    $DB->set_debug(true);
+}
+if (!empty($CFG->showcrondebugging)) {
+    set_debugging(DEBUG_DEVELOPER, true);
+}
+
+$checklimits = empty($options['ignorelimits']);
+
+core_php_time_limit::raise();
+
+// Increase memory limit.
+raise_memory_limit(MEMORY_EXTRA);
+
+// Emulate normal session - we use admin account by default.
+cron_setup_user();
+
+$humantimenow = date('r', time());
+$keepalive = (int)$options['keep-alive'];
+
+\core\local\cli\shutdown::script_supports_graceful_exit();
+
+mtrace("Server Time: {$humantimenow}\n");
+cron_run_adhoc_tasks(time(), $keepalive, $checklimits);
diff --git a/admin/cli/scheduled_task.php b/admin/cli/scheduled_task.php
new file mode 100644 (file)
index 0000000..f825f46
--- /dev/null
@@ -0,0 +1,151 @@
+<?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/>.
+
+/**
+ * CLI task execution.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2014 Petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+require_once("$CFG->libdir/clilib.php");
+require_once("$CFG->libdir/cronlib.php");
+
+list($options, $unrecognized) = cli_get_params(
+    array('help' => false, 'list' => false, 'execute' => false, 'showsql' => false, 'showdebugging' => false),
+    array('h' => 'help')
+);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help'] or (!$options['list'] and !$options['execute'])) {
+    $help =
+    "Scheduled cron tasks.
+
+    Options:
+    --execute=\\some\\task  Execute scheduled task manually
+    --list                List all scheduled tasks
+    --showsql             Show sql queries before they are executed
+    --showdebugging       Show developer level debugging information
+    -h, --help            Print out this help
+
+    Example:
+    \$sudo -u www-data /usr/bin/php admin/cli/scheduled_task.php --execute=\\core\\task\\session_cleanup_task
+
+    ";
+
+    echo $help;
+    die;
+}
+
+if ($options['showdebugging']) {
+    set_debugging(DEBUG_DEVELOPER, true);
+}
+
+if ($options['showsql']) {
+    $DB->set_debug(true);
+}
+if ($options['list']) {
+    cli_heading("List of scheduled tasks ($CFG->wwwroot)");
+
+    $shorttime = get_string('strftimedatetimeshort');
+
+    $tasks = \core\task\manager::get_all_scheduled_tasks();
+    echo str_pad(get_string('scheduledtasks', 'tool_task'), 50, ' ') . ' ' . str_pad(get_string('runpattern', 'tool_task'), 17, ' ')
+        . ' ' . str_pad(get_string('lastruntime', 'tool_task'), 40, ' ') . get_string('nextruntime', 'tool_task') . "\n";
+    foreach ($tasks as $task) {
+        $class = '\\' . get_class($task);
+        $schedule = $task->get_minute() . ' '
+            . $task->get_hour() . ' '
+            . $task->get_day() . ' '
+            . $task->get_day_of_week() . ' '
+            . $task->get_month() . ' '
+            . $task->get_day_of_week();
+        $nextrun = $task->get_next_run_time();
+        $lastrun = $task->get_last_run_time();
+
+        $plugininfo = core_plugin_manager::instance()->get_plugin_info($task->get_component());
+        $plugindisabled = $plugininfo && $plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled();
+
+        if ($plugindisabled) {
+            $nextrun = get_string('plugindisabled', 'tool_task');
+        } else if ($task->get_disabled()) {
+            $nextrun = get_string('taskdisabled', 'tool_task');
+        } else if ($nextrun > time()) {
+            $nextrun = userdate($nextrun);
+        } else {
+            $nextrun = get_string('asap', 'tool_task');
+        }
+
+        if ($lastrun) {
+            $lastrun = userdate($lastrun);
+        } else {
+            $lastrun = get_string('never');
+        }
+
+        echo str_pad($class, 50, ' ') . ' ' . str_pad($schedule, 17, ' ') .
+            ' ' . str_pad($lastrun, 40, ' ') . ' ' . $nextrun . "\n";
+    }
+    exit(0);
+}
+
+if ($execute = $options['execute']) {
+    if (!$task = \core\task\manager::get_scheduled_task($execute)) {
+        mtrace("Task '$execute' not found");
+        exit(1);
+    }
+
+    if (moodle_needs_upgrading()) {
+        mtrace("Moodle upgrade pending, cannot execute tasks.");
+        exit(1);
+    }
+
+    // Increase memory limit.
+    raise_memory_limit(MEMORY_EXTRA);
+
+    // Emulate normal session - we use admin account by default.
+    cron_setup_user();
+
+    // Execute the task.
+    \core\local\cli\shutdown::script_supports_graceful_exit();
+    $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
+    if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
+        mtrace('Cannot obtain cron lock');
+        exit(129);
+    }
+    if (!$lock = $cronlockfactory->get_lock('\\' . get_class($task), 10)) {
+        $cronlock->release();
+        mtrace('Cannot obtain task lock');
+        exit(130);
+    }
+
+    $task->set_lock($lock);
+    if (!$task->is_blocking()) {
+        $cronlock->release();
+    } else {
+        $task->set_cron_lock($cronlock);
+    }
+
+    cron_run_inner_scheduled_task($task);
+}
index c9d406a..4aa50b4 100644 (file)
@@ -25,6 +25,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // sp
     $temp->add(new admin_setting_configcheckbox('allowcohortthemes',  new lang_string('allowcohortthemes', 'admin'), new lang_string('configallowcohortthemes', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowthemechangeonurl',  new lang_string('allowthemechangeonurl', 'admin'), new lang_string('configallowthemechangeonurl', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowuserblockhiding', new lang_string('allowuserblockhiding', 'admin'), new lang_string('configallowuserblockhiding', 'admin'), 1));
+    $temp->add(new admin_setting_configcheckbox('langmenuinsecurelayout',
+        new lang_string('langmenuinsecurelayout', 'admin'),
+        new lang_string('langmenuinsecurelayout_desc', 'admin'), 0));
+    $temp->add(new admin_setting_configcheckbox('logininfoinsecurelayout',
+        new lang_string('logininfoinsecurelayout', 'admin'),
+        new lang_string('logininfoinsecurelayout_desc', 'admin'), 0));
     $temp->add(new admin_setting_configtextarea('custommenuitems', new lang_string('custommenuitems', 'admin'),
         new lang_string('configcustommenuitems', 'admin'), '', PARAM_RAW, '50', '10'));
     $temp->add(new admin_setting_configtextarea(
index 1136bd0..914ef5f 100644 (file)
@@ -31,6 +31,8 @@ use moodle_url;
 use moodle_exception;
 use lang_string;
 use curl;
+use core_qrcode;
+use stdClass;
 
 /**
  * API exposed by tool_mobile, to be used mostly by external functions and the plugin settings.
@@ -51,6 +53,14 @@ class api {
     const LOGIN_KEY_TTL = 60;
     /** @var string URL of the Moodle Apps Portal */
     const MOODLE_APPS_PORTAL_URL = 'https://apps.moodle.com';
+    /** @var int seconds a QR login key will expire. */
+    const LOGIN_QR_KEY_TTL = 600;
+    /** @var int QR code disabled value */
+    const QR_CODE_DISABLED = 0;
+    /** @var int QR code type URL value */
+    const QR_CODE_URL = 1;
+    /** @var int QR code type login value */
+    const QR_CODE_LOGIN = 2;
 
     /**
      * Returns a list of Moodle plugins supporting the mobile app.
@@ -336,6 +346,7 @@ class api {
 
     /**
      * Creates an auto-login key for the current user, this key is restricted by time and ip address.
+     * This key is used for automatically login the user in the site when the Moodle app opens the site in a mobile browser.
      *
      * @return string the key
      * @since Moodle 3.2
@@ -351,6 +362,24 @@ class api {
         return create_user_key('tool_mobile', $USER->id, null, $iprestriction, $validuntil);
     }
 
+    /**
+     * Creates a QR login key for the current user, this key is restricted by time and ip address.
+     * This key is used for automatically login the user in the site when the user scans a QR code in the Moodle app.
+     *
+     * @return string the key
+     * @since Moodle 3.9
+     */
+    public static function get_qrlogin_key() {
+        global $USER;
+        // Delete previous keys.
+        delete_user_key('tool_mobile', $USER->id);
+
+        // Create a new key.
+        $iprestriction = getremoteaddr(null);
+        $validuntil = time() + self::LOGIN_QR_KEY_TTL;
+        return create_user_key('tool_mobile', $USER->id, null, $iprestriction, $validuntil);
+    }
+
     /**
      * Get a list of the Mobile app features.
      *
@@ -601,4 +630,31 @@ class api {
 
         return $warnings;
     }
+
+    /**
+     * Generates a QR code with the site URL or for automatic login from the mobile app.
+     *
+     * @param  stdClass $mobilesettings tool_mobile settings
+     * @return string base64 data image contents, null if qr disabled
+     */
+    public static function generate_login_qrcode(stdClass $mobilesettings) {
+        global $CFG, $USER;
+
+        if ($mobilesettings->qrcodetype == static::QR_CODE_DISABLED) {
+            return null;
+        }
+
+        $urlscheme = !empty($mobilesettings->forcedurlscheme) ? $mobilesettings->forcedurlscheme : 'moodlemobile';
+        $data = $urlscheme . '://' . $CFG->wwwroot;
+
+        if ($mobilesettings->qrcodetype == static::QR_CODE_LOGIN) {
+            $qrloginkey = static::get_qrlogin_key();
+            $data .= '?qrlogin=' . $qrloginkey . '&userid=' . $USER->id;
+        }
+
+        $qrcode = new core_qrcode($data);
+        $imagedata = 'data:image/png;base64,' . base64_encode($qrcode->getBarcodePngData(5, 5));
+
+        return $imagedata;
+    }
 }
index fe1dad0..fa13085 100644 (file)
@@ -39,6 +39,7 @@ use context_system;
 use moodle_exception;
 use moodle_url;
 use core_text;
+use core_user;
 use coding_exception;
 
 /**
@@ -593,4 +594,102 @@ class external extends external_api {
              )
         ]);
     }
+
+    /**
+     * Returns description of get_tokens_for_qr_login() parameters.
+     *
+     * @return external_function_parameters
+     * @since  Moodle 3.9
+     */
+    public static function get_tokens_for_qr_login_parameters() {
+        return new external_function_parameters (
+            [
+                'qrloginkey' => new external_value(PARAM_ALPHANUMEXT, 'The user key for validating the request.'),
+                'userid' => new external_value(PARAM_INT, 'The user the key belongs to.'),
+            ]
+        );
+    }
+
+    /**
+     * Returns a WebService token (and private token) for QR login
+     *
+     * @param string $qrloginkey the user key generated and embedded into the QR code for validating the request
+     * @param int $userid the user the key belongs to
+     * @return array with the tokens and warnings
+     * @since  Moodle 3.9
+     */
+    public static function get_tokens_for_qr_login($qrloginkey, $userid) {
+        global $PAGE, $DB;
+
+        $params = self::validate_parameters(self::get_tokens_for_qr_login_parameters(),
+            ['qrloginkey' => $qrloginkey, 'userid' => $userid]);
+
+        $context = context_system::instance();
+        // We need this to make work the format text functions.
+        $PAGE->set_context($context);
+
+        $qrcodetype = get_config('tool_mobile', 'qrcodetype');
+        if ($qrcodetype != api::QR_CODE_LOGIN) {
+            throw new moodle_exception('qrcodedisabled', 'tool_mobile');
+        }
+
+        // Only requests from the Moodle mobile or desktop app. This enhances security to avoid any type of XSS attack.
+        // This code goes intentionally here and not inside the check_autologin_prerequisites() function because it
+        // is used by other PHP scripts that can be opened in any browser.
+        if (!\core_useragent::is_moodle_app()) {
+            throw new moodle_exception('apprequired', 'tool_mobile');
+        }
+        api::check_autologin_prerequisites($params['userid']);  // Checks https, avoid site admins using this...
+
+        // Validate and delete the key.
+        $key = validate_user_key($params['qrloginkey'], 'tool_mobile', null);
+        delete_user_key('tool_mobile', $params['userid']);
+
+        // Double check key belong to user.
+        if ($key->userid != $params['userid']) {
+            throw new moodle_exception('invalidkey');
+        }
+
+        // Key validated, check user.
+        $user = core_user::get_user($key->userid, '*', MUST_EXIST);
+        core_user::require_active_user($user, true, true);
+
+        // Generate WS tokens.
+        \core\session\manager::set_user($user);
+
+        // Check if the service exists and is enabled.
+        $service = $DB->get_record('external_services', ['shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE, 'enabled' => 1]);
+        if (empty($service)) {
+            // will throw exception if no token found
+            throw new moodle_exception('servicenotavailable', 'webservice');
+        }
+
+        // Get an existing token or create a new one.
+        $token = external_generate_token_for_current_user($service);
+        $privatetoken = $token->privatetoken; // Save it here, the next function removes it.
+        external_log_token_request($token);
+
+        $result = [
+            'token' => $token->token,
+            'privatetoken' => $privatetoken ?: '',
+            'warnings' => [],
+        ];
+        return $result;
+    }
+
+    /**
+     * Returns description of get_tokens_for_qr_login() result value.
+     *
+     * @return external_description
+     * @since  Moodle 3.9
+     */
+    public static function get_tokens_for_qr_login_returns() {
+        return new external_single_structure(
+            [
+                'token' => new external_value(PARAM_ALPHANUM, 'A valid WebService token for the official mobile app service.'),
+                'privatetoken' => new external_value(PARAM_ALPHANUM, 'Private token used for auto-login processes.'),
+                'warnings' => new external_warnings(),
+            ]
+        );
+    }
 }
index d53f7b4..530267a 100644 (file)
@@ -78,5 +78,14 @@ $functions = array(
         'type'        => 'write',
         'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
-);
 
+    'tool_mobile_get_tokens_for_qr_login' => array(
+        'classname'   => 'tool_mobile\external',
+        'methodname'  => 'get_tokens_for_qr_login',
+        'description' => 'Returns a WebService token (and private token) for QR login.',
+        'type'        => 'read',
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'ajax'          => true,
+        'loginrequired' => false,
+    ),
+);
index ccc6867..789cfd2 100644 (file)
@@ -96,6 +96,15 @@ $string['oauth2identityproviders'] = 'OAuth 2 identity providers';
 $string['offlineuse'] = 'Offline use';
 $string['pluginname'] = 'Moodle app tools';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
+$string['qrcodedisabled'] = 'Access via QR code disabled';
+$string['qrcodeformobileappaccess'] = 'QR code for mobile app access';
+$string['qrcodeformobileapploginabout'] = 'Scan the QR code with your mobile app and you will be automatically logged in. The QR code will expire in {$a} minutes.';
+$string['qrcodeformobileappurlabout'] = 'Scan the QR code with your mobile app to fill in the site URL in your app.';
+$string['qrsiteadminsnotallowed'] = 'For security reasons login via QR code is not allowed for site administrators or if you are logged in as another user.';
+$string['qrcodetype'] = 'QR code access';
+$string['qrcodetype_desc'] = 'A QR code can be provided for mobile app users to scan and either have the site URL filled in or be automatically logged in without having to enter their credentials.';
+$string['qrcodetypeurl'] = 'QR code with site URL';
+$string['qrcodetypelogin'] = 'QR code with automatic login';
 $string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
@@ -108,3 +117,4 @@ $string['getmoodleonyourmobile'] = 'Get the mobile app';
 $string['privacy:metadata:preference:tool_mobile_autologin_request_last'] = 'The date of the last auto-login key request. Between each request 6 minutes are required.';
 $string['privacy:metadata:core_userkey'] = 'User\'s keys used to create auto-login key for the current user.';
 $string['responsivemainmenuitems'] = 'Responsive menu items';
+$string['viewqrcode'] = 'View QR code';
index 74b3c0b..43d6cc2 100644 (file)
@@ -126,24 +126,64 @@ function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree
         return;
     }
 
-    if (!$url = tool_mobile_create_app_download_url()) {
-        return;
+    $newnodes = [];
+    $mobilesettings = get_config('tool_mobile');
+
+    // Check if we should display a QR code.
+    if (!empty($mobilesettings->qrcodetype)) {
+        $mobileqr = null;
+        $qrcodeforappstr = get_string('qrcodeformobileappaccess', 'tool_mobile');
+
+        if ($mobilesettings->qrcodetype == tool_mobile\api::QR_CODE_LOGIN && is_https()) {
+
+            if (is_siteadmin() || \core\session\manager::is_loggedinas()) {
+                $mobileqr = get_string('qrsiteadminsnotallowed', 'tool_mobile');
+            } else {
+                $qrcodeimg = tool_mobile\api::generate_login_qrcode($mobilesettings);
+
+                $minutes = tool_mobile\api::LOGIN_QR_KEY_TTL / MINSECS;
+                $mobileqr = html_writer::tag('p', get_string('qrcodeformobileapploginabout', 'tool_mobile', $minutes));
+                $mobileqr .= html_writer::link('#qrcode', get_string('viewqrcode', 'tool_mobile'),
+                    ['class' => 'btn btn-primary mt-2', 'data-toggle' => 'collapse',
+                    'role' => 'button', 'aria-expanded' => 'false']);
+                $mobileqr .= html_writer::div(html_writer::img($qrcodeimg, $qrcodeforappstr), 'collapse mt-4', ['id' => 'qrcode']);
+            }
+
+        } else if ($mobilesettings->qrcodetype == tool_mobile\api::QR_CODE_URL) {
+            $qrcodeimg = tool_mobile\api::generate_login_qrcode($mobilesettings);
+
+            $mobileqr = get_string('qrcodeformobileappurlabout', 'tool_mobile');
+            $mobileqr .= html_writer::div(html_writer::img($qrcodeimg, $qrcodeforappstr));
+        }
+
+        if ($mobileqr) {
+            $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappqr', $qrcodeforappstr, null, null, $mobileqr);
+        }
     }
 
+    // Check if the user is using the app, encouraging him to use it otherwise.
     $userhastoken = tool_mobile_user_has_token($user->id);
-
-    $mobilecategory = new core_user\output\myprofile\category('mobile', get_string('mobileapp', 'tool_mobile'),
-            'loginactivity');
-    $tree->add_category($mobilecategory);
+    $mobilestrconnected = null;
 
     if ($userhastoken) {
-        $mobilestr = get_string('mobileappconnected', 'tool_mobile');
-    } else {
-        $mobilestr = get_string('mobileappenabled', 'tool_mobile', $url->out());
+        $mobilestrconnected = get_string('mobileappconnected', 'tool_mobile');
+    } else if ($url = tool_mobile_create_app_download_url()) {
+         $mobilestrconnected = get_string('mobileappenabled', 'tool_mobile', $url->out());
     }
 
-    $node = new  core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestr, null);
-    $tree->add_node($node);
+    if ($mobilestrconnected) {
+        $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestrconnected, null);
+    }
+
+    // Add nodes, if any.
+    if (!empty($newnodes)) {
+        $mobilecat = new core_user\output\myprofile\category('mobile', get_string('mobileapp', 'tool_mobile'), 'loginactivity');
+        $tree->add_category($mobilecat);
+
+        foreach ($newnodes as $node) {
+            $tree->add_node($node);
+        }
+    }
 }
 
 /**
index 05ed4aa..732bd71 100644 (file)
@@ -58,6 +58,9 @@ if ($hassiteconfig) {
 
         // Type of login.
         $temp = new admin_settingpage('mobileauthentication', new lang_string('mobileauthentication', 'tool_mobile'));
+
+        $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesauth', '', $featuresnotice));
+
         $options = array(
             tool_mobile\api::LOGIN_VIA_APP => new lang_string('loginintheapp', 'tool_mobile'),
             tool_mobile\api::LOGIN_VIA_BROWSER => new lang_string('logininthebrowser', 'tool_mobile'),
@@ -67,6 +70,15 @@ if ($hassiteconfig) {
                     new lang_string('typeoflogin', 'tool_mobile'),
                     new lang_string('typeoflogin_desc', 'tool_mobile'), 1, $options));
 
+        $options = [
+            tool_mobile\api::QR_CODE_DISABLED => new lang_string('qrcodedisabled', 'tool_mobile'),
+            tool_mobile\api::QR_CODE_URL => new lang_string('qrcodetypeurl', 'tool_mobile'),
+            tool_mobile\api::QR_CODE_LOGIN => new lang_string('qrcodetypelogin', 'tool_mobile'),
+        ];
+        $temp->add(new admin_setting_configselect('tool_mobile/qrcodetype',
+                    new lang_string('qrcodetype', 'tool_mobile'),
+                    new lang_string('qrcodetype_desc', 'tool_mobile'), tool_mobile\api::QR_CODE_LOGIN, $options));
+
         $temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
                     new lang_string('forcedurlscheme_key', 'tool_mobile'),
                     new lang_string('forcedurlscheme', 'tool_mobile'), 'moodlemobile', PARAM_NOTAGS));
index 9534f05..7b05175 100644 (file)
@@ -600,4 +600,129 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => true]);
         $this->assertEquals($expected, $data->courses[0]->summary);
     }
+
+    /*
+     * Test get_tokens_for_qr_login.
+     */
+    public function test_get_tokens_for_qr_login() {
+        global $DB, $CFG, $USER;
+
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        $qrloginkey = api::get_qrlogin_key();
+
+        // Generate new tokens, the ones we expect to receive.
+        $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
+        $token = external_generate_token_for_current_user($service);
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+                'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        $result = external::get_tokens_for_qr_login($qrloginkey, $USER->id);
+        $result = external_api::clean_returnvalue(external::get_tokens_for_qr_login_returns(), $result);
+
+        $this->assertEmpty($result['warnings']);
+        $this->assertEquals($token->token, $result['token']);
+        $this->assertEquals($token->privatetoken, $result['privatetoken']);
+
+        // Now, try with an invalid key.
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('invalidkey', 'error'));
+        $result = external::get_tokens_for_qr_login(random_string('64'), $user->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing QR code enabled.
+     */
+    public function test_get_tokens_for_qr_login_missing_enableqr() {
+        global $CFG, $USER;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        set_config('qrcodetype', tool_mobile\api::QR_CODE_DISABLED, 'tool_mobile');
+
+        $this->expectExceptionMessage(get_string('qrcodedisabled', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing ws.
+     */
+    public function test_get_tokens_for_qr_login_missing_ws() {
+        global $CFG;
+        $this->resetAfterTest(true);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        // Need to disable webservices to verify that's checked.
+        $CFG->enablewebservices = 0;
+        $CFG->enablemobilewebservice = 0;
+
+        $this->setAdminUser();
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
+        $result = external::get_tokens_for_qr_login('', $user->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing https.
+     */
+    public function test_get_tokens_for_qr_login_missing_https() {
+        global $CFG, $USER;
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        // Need to simulate a non HTTPS site here.
+        $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing admin.
+     */
+    public function test_get_tokens_for_qr_login_missing_admin() {
+        global $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // Fake the app.
+        core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
+            'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
+
+    /**
+     * Test get_tokens_for_qr_login missing app_request.
+     */
+    public function test_get_tokens_for_qr_login_missing_app_request() {
+        global $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('apprequired', 'tool_mobile'));
+        $result = external::get_tokens_for_qr_login('', $USER->id);
+    }
 }
index 392d5b2..afba9ff 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2019111800; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019111801; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019111200; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
 $plugin->dependencies = array(
index 06e24ef..9b91a14 100644 (file)
@@ -17,6 +17,9 @@
 /**
  * Form for scheduled tasks admin pages.
  *
+ * @deprecated since Moodle 3.9 MDL-63580. Please use the \core\task\manager.
+ * @todo final deprecation. To be removed in Moodle 4.3 MDL-63594.
+ *
  * @package    tool_task
  * @copyright  2018 Toni Barbera <toni@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -55,7 +58,9 @@ class run_from_cli {
      * @return bool
      */
     public static function is_runnable():bool {
-        return self::find_php_cli_path() !== false;
+        debugging('run_from_cli class is deprecated. Please use \core\task\manager::run_from_cli() instead.',
+            DEBUG_DEVELOPER);
+        return \core\task\manager::is_runnable();
     }
 
     /**
@@ -66,30 +71,8 @@ class run_from_cli {
      * @throws \moodle_exception
      */
     public static function execute(\core\task\task_base $task):bool {
-        global $CFG;
-
-        if (!self::is_runnable()) {
-            $redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
-            throw new \moodle_exception('cannotfindthepathtothecli', 'tool_task', $redirecturl->out());
-        } else {
-            // Shell-escaped path to the PHP binary.
-            $phpbinary = escapeshellarg(self::find_php_cli_path());
-
-            // Shell-escaped path CLI script.
-            $pathcomponents = [$CFG->dirroot, $CFG->admin, 'tool', 'task', 'cli', 'schedule_task.php'];
-            $scriptpath     = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents));
-
-            // Shell-escaped task name.
-            $classname = get_class($task);
-            $taskarg   = escapeshellarg("--execute={$classname}");
-
-            // Build the CLI command.
-            $command = "{$phpbinary} {$scriptpath} {$taskarg}";
-
-            // Execute it.
-            passthru($command);
-        }
-
-        return true;
+        debugging('run_from_cli class is deprecated. Please use \core\task\manager::run_from_cli() instead.',
+            DEBUG_DEVELOPER);
+        return \core\task\manager::run_from_cli($task);
     }
 }
index 1a04ba8..07cded8 100644 (file)
@@ -17,6 +17,9 @@
 /**
  * Task executor for adhoc tasks.
  *
+ * @deprecated since Moodle 3.9 MDL-63580. Please use the admin/cli/adhoc_task.php.
+ * @todo final deprecation. To be removed in Moodle 4.3 MDL-63594.
+ *
  * @package    tool_task
  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -44,6 +47,8 @@ list($options, $unrecognized) = cli_get_params(
     ]
 );
 
+debugging('admin/tool/task/cli/adhoc_task.php is deprecated. Please use admin/cli/adhoc_task.php instead.', DEBUG_DEVELOPER);
+
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
     cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
index 85d7a4f..073075e 100644 (file)
@@ -17,6 +17,9 @@
 /**
  * CLI task execution.
  *
+ * @deprecated since Moodle 3.9 MDL-63580. Please use the admin/cli/schedule_task.php.
+ * @todo final deprecation. To be removed in Moodle 4.3 MDL-63594.
+ *
  * @package    tool_task
  * @copyright  2014 Petr Skoda
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -33,6 +36,8 @@ list($options, $unrecognized) = cli_get_params(
     array('h' => 'help')
 );
 
+debugging('admin/tool/task/cli/schedule_task.php is deprecated. Please use admin/cli/scheduled_task.php instead.', DEBUG_DEVELOPER);
+
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
     cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
@@ -65,6 +70,7 @@ if ($options['showdebugging']) {
 if ($options['showsql']) {
     $DB->set_debug(true);
 }
+
 if ($options['list']) {
     cli_heading("List of scheduled tasks ($CFG->wwwroot)");
 
index 0a01d0a..a003eab 100644 (file)
@@ -75,7 +75,7 @@ class tool_task_renderer extends plugin_renderer_base {
         $data = [];
         $yes = get_string('yes');
         $no = get_string('no');
-        $canruntasks = tool_task\run_from_cli::is_runnable();
+        $canruntasks = \core\task\manager::is_runnable();
         foreach ($tasks as $task) {
             $classname = get_class($task);
             $defaulttask = \core\task\manager::get_default_scheduled_task($classname, false);
index b404a82..1b0de98 100644 (file)
@@ -88,7 +88,7 @@ echo html_writer::start_tag('pre');
 $CFG->mtrace_wrapper = 'tool_task_mtrace_wrapper';
 
 // Run the specified task (this will output an error if it doesn't exist).
-\tool_task\run_from_cli::execute($task);
+\core\task\manager::run_from_cli($task);
 
 echo html_writer::end_tag('pre');
 
index e76d6b2..4fb007b 100644 (file)
@@ -377,4 +377,19 @@ class core extends \H5PCore {
     public static function validToken($action, $token) {
         return confirm_sesskey($token);
     }
+
+    /**
+     * Get the library string from a DB library record.
+     *
+     * @param  stdClass $record The DB library record.
+     * @param  bool $foldername If true, use hyphen instead of space in returned string.
+     * @return string The string name on the form {machineName} {majorVersion}.{minorVersion}.
+     */
+    public static function record_to_string(stdClass $record, bool $foldername = false): string {
+        return static::libraryToString([
+            'machineName' => $record->machinename,
+            'majorVersion' => $record->majorversion,
+            'minorVersion' => $record->minorversion,
+        ], $foldername);
+    }
 }
index dc1291b..e1090eb 100644 (file)
@@ -25,6 +25,7 @@
 namespace core_h5p;
 
 use H5PEditorAjaxInterface;
+use core\dml\table as dml_table;
 
 /**
  * Moodle's implementation of the H5P Editor Ajax interface.
@@ -110,7 +111,95 @@ class editor_ajax implements H5PEditorAjaxInterface {
      * @return array Translations in $languagecode available for libraries $libraries
      */
     public function getTranslations($libraries, $languagecode): array {
-        // To be implemented when translations are introduced.
-        return [];
+        $translations = [];
+        $langcache = \cache::make('core', 'h5p_content_type_translations');
+
+        $missing = [];
+        foreach ($libraries as $libstring) {
+            // Check if this library has been saved previously into the cache.
+            $librarykey = helper::get_cache_librarykey($libstring);
+            $cachekey = "{$librarykey}/{$languagecode}";
+            $libtranslation = $langcache->get($cachekey);
+            if ($libtranslation) {
+                // The library has this language stored into the cache.
+                $translations[$libstring] = $libtranslation;
+            } else {
+                // This language for the library hasn't been stored previously into the cache, so we need to get it from DB.
+                $missing[] = $libstring;
+            }
+        }
+
+        // Get all language files for libraries which aren't stored into the cache and merge them with the cache ones.
+        return array_merge(
+            $translations,
+            $this->get_missing_translations($missing, $languagecode)
+        );
+    }
+
+    /**
+     * Get translation for $language for libraries in $missing.
+     *
+     * @param  array  $missing  An array of libraries, in the form "<machineName> <majorVersion>.<minorVersion>
+     * @param  string $language Language code
+     * @return array  Translations in $language available for libraries $missing
+     */
+    protected function get_missing_translations(array $missing, string $language): array {
+        global $DB;
+
+        if (empty($missing)) {
+            return [];
+        }
+
+        $wheres = [];
+        $params = [
+            file_storage::COMPONENT,
+            file_storage::LIBRARY_FILEAREA,
+        ];
+        $sqllike = $DB->sql_like('f.filepath', '?');
+        $params[] = '%language%';
+
+        foreach ($missing as $library) {
+            $librarydata = core::libraryFromString($library);
+            $wheres[] = '(h.machinename = ? AND h.majorversion = ? AND h.minorversion = ?)';
+            $params[] = $librarydata['machineName'];
+            $params[] = $librarydata['majorVersion'];
+            $params[] = $librarydata['minorVersion'];
+        }
+        $params[] = "{$language}.json";
+        $wheresql = implode(' OR ', $wheres);
+
+        $filestable = new dml_table('files', 'f', 'f_');
+        $filestableselect = $filestable->get_field_select();
+
+        $libtable = new dml_table('h5p_libraries', 'h', 'h_');
+        $libtableselect = $libtable->get_field_select();
+
+        $sql = "SELECT {$filestableselect}, {$libtableselect}
+                  FROM {h5p_libraries} h
+             LEFT JOIN {files} f
+                    ON h.id = f.itemid AND f.component = ?
+                   AND f.filearea = ? AND $sqllike
+                 WHERE ($wheresql) AND f.filename = ?";
+
+        // Get the content of all these language files and put them into the translations array.
+        $langcache = \cache::make('core', 'h5p_content_type_translations');
+        $fs = get_file_storage();
+        $translations = [];
+        $results = $DB->get_recordset_sql($sql, $params);
+        $toset = [];
+        foreach ($results as $result) {
+            $file = $fs->get_file_instance($filestable->extract_from_result($result));
+            $library = $libtable->extract_from_result($result);
+            $libstring = core::record_to_string($library);
+            $librarykey = helper::get_cache_librarykey($libstring);
+            $translations[$libstring] = $file->get_content();
+            $cachekey = "{$librarykey}/{$language}";
+            $toset[$cachekey] = $translations[$libstring];
+        }
+        $langcache->set_many($toset);
+
+        $results->close();
+
+        return $translations;
     }
 }
index c3eb18c..bac7950 100644 (file)
@@ -51,8 +51,54 @@ class editor_framework implements H5peditorStorage {
      * @return string|boolean Translation in JSON format if available, false otherwise
      */
     public function getLanguage($name, $major, $minor, $lang) {
-        // To be implemented when translations are introduced.
-        return false;
+        global $DB;
+
+        // Check if this information has been saved previously into the cache.
+        $langcache = \cache::make('core', 'h5p_content_type_translations');
+        $library = new stdClass();
+        $library->machinename = $name;
+        $library->majorversion = $major;
+        $library->minorversion = $minor;
+        $librarykey = helper::get_cache_librarykey(core::record_to_string($library));
+        $cachekey = "{$librarykey}/{$lang}";
+        $translation = $langcache->get($cachekey);
+        if ($translation) {
+            return $translation;
+        }
+
+        // Get the language file for this library.
+        $params = [
+            file_storage::COMPONENT,
+            file_storage::LIBRARY_FILEAREA,
+        ];
+        $sqllike = $DB->sql_like('f.filepath', '?');
+        $params[] = '%language%';
+
+        $sql = "SELECT hl.id, f.pathnamehash
+                  FROM {h5p_libraries} hl
+             LEFT JOIN {files} f
+                    ON hl.id = f.itemid AND f.component = ? AND f.filearea = ? AND $sqllike
+                 WHERE ((hl.machinename = ? AND hl.majorversion = ? AND hl.minorversion = ?)
+                   AND f.filename = ?)
+              ORDER BY hl.patchversion DESC";
+        $params[] = $name;
+        $params[] = $major;
+        $params[] = $minor;
+        $params[] = $lang.'.json';
+
+        $result = $DB->get_record_sql($sql, $params);
+
+        if (!empty($result)) {
+            // If the JS language file exists, its content should be returned.
+            $fs = get_file_storage();
+            $file = $fs->get_file_by_hash($result->pathnamehash);
+            $translation = $file->get_content();
+        }
+
+        // Save translation into the cache (even if there is no translation for this language).
+        $langcache->set($cachekey, $translation);
+
+        return $translation;
     }
 
     /**
@@ -67,13 +113,77 @@ class editor_framework implements H5peditorStorage {
      * @return array List of possible language codes
      */
     public function getAvailableLanguages($machinename, $major, $minor): array {
+        global $DB;
+
+        // Check if this information has been saved previously into the cache.
+        $langcache = \cache::make('core', 'h5p_content_type_translations');
+        $library = new stdClass();
+        $library->machinename = $machinename;
+        $library->majorversion = $major;
+        $library->minorversion = $minor;
+        $librarykey = helper::get_cache_librarykey(core::record_to_string($library));
+        $languages = $langcache->get($librarykey);
+        if ($languages) {
+            // This contains a list of all of the available languages for the library.
+            return $languages;
+        }
+
+        // Get the language files for this library.
+        $params = [
+            file_storage::COMPONENT,
+            file_storage::LIBRARY_FILEAREA,
+        ];
+        $filepathsqllike = $DB->sql_like('f.filepath', '?');
+        $params[] = '%language%';
+        $filenamesqllike = $DB->sql_like('f.filename', '?');
+        $params[] = '%.json';
+
+        $sql = "SELECT DISTINCT f.filename
+                           FROM {h5p_libraries} hl
+                      LEFT JOIN {files} f
+                             ON hl.id = f.itemid AND f.component = ? AND f.filearea = ?
+                            AND $filepathsqllike AND $filenamesqllike
+                          WHERE hl.machinename = ? AND hl.majorversion = ? AND hl.minorversion = ?";
+        $params[] = $machinename;
+        $params[] = $major;
+        $params[] = $minor;
+
         $defaultcode = 'en';
-        $codes = [];
+        $languages = [];
+
+        $results = $DB->get_recordset_sql($sql, $params);
+        if ($results->valid()) {
+            // Extract the code language from the JS language files.
+            foreach ($results as $result) {
+                if (!empty($result->filename)) {
+                    $lang = substr($result->filename, 0, -5);
+                    $languages[$lang] = $languages;
+                }
+            }
+            $results->close();
+
+            // Semantics is 'en' by default. It has to be added always.
+            if (!array_key_exists($defaultcode, $languages)) {
+                $languages = array_keys($languages);
+                array_unshift($languages, $defaultcode);
+            }
+        } else {
+            $results->close();
+            $params = [
+                'machinename' => $machinename,
+                'majorversion' => $major,
+                'minorversion' => $minor,
+            ];
+            if ($DB->record_exists('h5p_libraries', $params)) {
+                // If the library exists (but it doesn't contain any language file), at least defaultcode should be returned.
+                $languages[] = $defaultcode;
+            }
+        }
 
-        // Semantics is 'en' by default.
-        array_unshift($codes, $defaultcode);
+        // Save available languages into the cache.
+        $langcache->set($librarykey, $languages);
 
-        return $codes;
+        return $languages;
     }
 
     /**
index f34236e..41b1c9d 100644 (file)
@@ -259,7 +259,7 @@ class helper {
                 '/admin/tool/task/scheduledtasks.php',
                 array('action' => 'edit', 'task' => get_class($task))
             );
-            if ($status && \tool_task\run_from_cli::is_runnable() && get_config('tool_task', 'enablerunnow')) {
+            if ($status && \core\task\manager::is_runnable() && get_config('tool_task', 'enablerunnow')) {
                 $statusaction = \html_writer::link(
                     new \moodle_url('/admin/tool/task/schedule_task.php',
                         array('task' => get_class($task))),
@@ -399,4 +399,15 @@ class helper {
 
         return $settings;
     }
+
+    /**
+     * Prepare the library name to be used as a cache key (remove whitespaces and replace dots to underscores).
+     *
+     * @param  string $library Library name.
+     * @return string Library name in a cache simple key format (a-zA-Z0-9_).
+     */
+    public static function get_cache_librarykey(string $library): string {
+        // Remove whitespaces and replace '.' to '_'.
+        return str_replace('.', '_', str_replace(' ', '', $library));
+    }
 }
index 573289a..9ae0ae2 100644 (file)
@@ -96,4 +96,168 @@ class editor_ajax_testcase extends \advanced_testcase {
         $this->assertTrue($invalidaction);
         $this->assertFalse($invalidtoken);
     }
+
+    /**
+     * Test that the method getTranslations retrieves the translations of several libraries.
+     *
+     * @dataProvider  get_translations_provider
+     *
+     * @param  array  $datalibs      Libraries to create
+     * @param  string $lang          Language to get the translations
+     * @param  bool   $emptyexpected True if empty translations are expected; false otherwise
+     * @param  array  $altstringlibs When defined, libraries are no created and the content here is used to call the method
+     */
+    public function test_get_translations(array $datalibs, string $lang, bool $emptyexpected, ?array $altstringlibs = []): void {
+        $this->resetAfterTest();
+
+        // Fetch generator.
+        $generator = \testing_util::get_data_generator();
+        $h5pgenerator = $generator->get_plugin_generator('core_h5p');
+
+        $h5pfilestorage = new file_storage();
+        $h5ptempath = $h5pfilestorage->getTmpPath();
+
+        if (!empty($altstringlibs)) {
+            // Libraries won't be created and the getTranslation method will be called with this $altstringlibs.
+            $stringlibs = $altstringlibs;
+        } else {
+            $stringlibs = [];
+            foreach ($datalibs as $datalib) {
+                // Create DB entry for this library.
+                $tmplib = $h5pgenerator->create_library_record($datalib['machinename'], $datalib['title'], $datalib['majorversion'],
+                    $datalib['minorversion']);
+                // Create the files for this libray.
+                [$library, $files] = $h5pgenerator->create_library($h5ptempath, $tmplib->id, $datalib['machinename'],
+                    $datalib['majorversion'], $datalib['minorversion'], $datalib['translation']);
+                $h5pfilestorage->saveLibrary($library);
+                $stringlibs[] = \H5PCore::libraryToString($library);
+            }
+        }
+
+        $translations = $this->editorajax->getTranslations($stringlibs, $lang);
+
+        if ($emptyexpected) {
+            $this->assertEmpty($translations);
+        } else {
+            foreach ($translations as $stringlib => $translation) {
+                $this->assertEquals($datalibs[$stringlib]['translation'][$lang], $translation);
+            }
+        }
+    }
+
+    /**
+     * Data provider for test_get_translations().
+     *
+     * @return array
+     */
+    public function get_translations_provider(): array {
+        return [
+            'No library' => [
+                [],
+                'es',
+                true,
+                ['Library1 1.2']
+            ],
+            'One library with existing translation (es)' => [
+                [
+                    'Library1 1.2' => [
+                        'machinename' => 'Library1',
+                        'title' => 'Lib1',
+                        'majorversion' => 1,
+                        'minorversion' => 2,
+                        'translation' => [
+                            'es' => '{"libraryStrings": {"key": "valor"}}',
+                            'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                        ],
+                    ]
+                ],
+                'es',
+                false
+            ],
+            'One library with existing translation (fr)' => [
+                [
+                    'Library1 1.2' => [
+                        'machinename' => 'Library1',
+                        'title' => 'Lib1',
+                        'majorversion' => 1,
+                        'minorversion' => 2,
+                        'translation' => [
+                            'es' => '{"libraryStrings": {"key": "valor"}}',
+                            'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                        ],
+                    ]
+                ],
+                'fr',
+                false
+            ],
+            'One library with unexisting translation (de)' => [
+                [
+                    'Library1 1.2' => [
+                        'machinename' => 'Library1',
+                        'title' => 'Lib1',
+                        'majorversion' => 1,
+                        'minorversion' => 2,
+                        'translation' => [
+                            'es' => '{"libraryStrings": {"key": "valor"}}',
+                            'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                        ],
+                    ]
+                ],
+                'de',
+                true
+            ],
+            'Two libraries with existing translation (es)' => [
+                [
+                    'Library1 1.2' => [
+                        'machinename' => 'Library1',
+                        'title' => 'Lib1',
+                        'majorversion' => 1,
+                        'minorversion' => 2,
+                        'translation' => [
+                            'es' => '{"libraryStrings": {"key": "valor"}}',
+                            'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                        ],
+                    ],
+                    'Library2 3.4' => [
+                        'machinename' => 'Library2',
+                        'title' => 'Lib1',
+                        'majorversion' => 3,
+                        'minorversion' => 4,
+                        'translation' => [
+                            'es' => '{"libraryStrings": {"key": "valor"}}',
+                            'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                        ],
+                    ]
+                ],
+                'es',
+                false
+            ],
+            'Two libraries with unexisting translation (de)' => [
+                [
+                    'Library1 1.2' => [
+                        'machinename' => 'Library1',
+                        'title' => 'Lib1',
+                        'majorversion' => 1,
+                        'minorversion' => 2,
+                        'translation' => [
+                            'es' => '{"libraryStrings": {"key": "valor"}}',
+                            'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                        ],
+                    ],
+                    'Library2 3.4' => [
+                        'machinename' => 'Library2',
+                        'title' => 'Lib1',
+                        'majorversion' => 3,
+                        'minorversion' => 4,
+                        'translation' => [
+                            'es' => '{"libraryStrings": {"key": "valor"}}',
+                            'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                        ],
+                    ]
+                ],
+                'de',
+                true
+            ],
+        ];
+    }
 }
index 27f485e..1dd6b8c 100644 (file)
@@ -53,6 +53,301 @@ class editor_framework_testcase extends \advanced_testcase {
         $this->editorframework = new editor_framework();
     }
 
+    /**
+     * Test that the method getLanguage retrieves the translation of a library in the requested language.
+     *
+     * @dataProvider  get_language_provider
+     *
+     * @param  array  $datalib        Library data to create
+     * @param  string $lang           Language to retrieve the translation
+     * @param  bool   $emptyexpected  True when false value is expected; false, otherwise
+     * @param  string $machinename    The machine readable name of the library(content type)
+     * @param  int    $majorversion   Major part of version number
+     * @param  int    $minorversion   Minor part of version number
+     */
+    public function test_get_language(array $datalib, string $lang, ?bool $emptyexpected = false, ?string $machinename = '',
+            ?int $majorversion = 1, ?int $minorversion = 0): void {
+        $this->resetAfterTest(true);
+
+        // Fetch generator.
+        $generator = \testing_util::get_data_generator();
+        $h5pgenerator = $generator->get_plugin_generator('core_h5p');
+
+        $h5pfilestorage = new file_storage();
+        $h5ptempath = $h5pfilestorage->getTmpPath();
+
+        $expectedresult = '';
+        if ($datalib) {
+            $translations = [];
+            if (array_key_exists('translation', $datalib)) {
+                $translations = $datalib['translation'];
+            }
+            // Create DB entry for this library.
+            $tmplib = $h5pgenerator->create_library_record($datalib['machinename'], $datalib['title'], $datalib['majorversion'],
+                $datalib['minorversion']);
+            // Create the files for this libray.
+            [$library, $files] = $h5pgenerator->create_library($h5ptempath, $tmplib->id, $datalib['machinename'],
+                $datalib['majorversion'], $datalib['minorversion'], $translations);
+            $h5pfilestorage->saveLibrary($library);
+
+            // If machinename, majorversion or minorversion are empty, use the value in datalib.
+            if (empty($machinename)) {
+                $machinename = $datalib['machinename'];
+            }
+            if (empty($majorversion)) {
+                $majorversion = $datalib['majorversion'];
+            }
+            if (empty($minorversion)) {
+                $minorversion = $datalib['minorversion'];
+            }
+            if (!$emptyexpected && array_key_exists($lang, $translations)) {
+                $expectedresult = $translations[$lang];
+            }
+        }
+
+        // Get Language.
+        $json = $this->editorframework->getLanguage($machinename, $majorversion, $minorversion, $lang);
+
+        if ($emptyexpected) {
+            $this->assertFalse($json);
+        } else {
+            $this->assertEquals($expectedresult, $json);
+        }
+    }
+
+    /**
+     * Data provider for test_get_language().
+     *
+     * @return array
+     */
+    public function get_language_provider(): array {
+        return [
+            'No library' => [
+                [],
+                'en',
+                true,
+                'Library1',
+                1,
+                2,
+            ],
+            'One library created but getting translation from an unexisting one' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                    'translation' => [
+                        'es' => '{"libraryStrings": {"key": "valor"}}',
+                        'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                    ],
+                ],
+                'es',
+                true,
+                'AnotherLibrary',
+            ],
+            'One library without any translation' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                ],
+                'es',
+                true,
+            ],
+            'One library with 2 translations (es and fr) - es' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                    'translation' => [
+                        'es' => '{"libraryStrings": {"key": "valor"}}',
+                        'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                    ],
+                ],
+                'es',
+            ],
+            'One library with 2 translations (es and fr) - fr' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                    'translation' => [
+                        'es' => '{"libraryStrings": {"key": "valor"}}',
+                        'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                    ],
+                ],
+                'fr',
+            ],
+            'One library with 2 translations (es and fr) - unexisting translation (de)' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                    'translation' => [
+                        'es' => '{"libraryStrings": {"key": "valor"}}',
+                        'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                    ],
+                ],
+                'de',
+                true
+            ],
+            'One library with 3 translations (one of them English) - fr' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                    'translation' => [
+                        'en' => '{"libraryStrings": {"key": "value"}}',
+                        'es' => '{"libraryStrings": {"key": "valor"}}',
+                        'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                    ],
+                ],
+                'fr',
+            ],
+            'One library with 3 translations (one of them English) - en' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                    'translation' => [
+                        'en' => '{"libraryStrings": {"key": "value"}}',
+                        'es' => '{"libraryStrings": {"key": "valor"}}',
+                        'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                    ],
+                ],
+                'en',
+            ],
+        ];
+    }
+
+    /**
+     * Test that the method getAvailableLanguages retrieves all the language available of a library.
+     *
+     * @dataProvider  get_available_languages_provider
+     *
+     * @param  array  $datalib        Library data to create
+     * @param  array  $expectedlangs  Available languages expected.
+     * @param  string $machinename    The machine readable name of the library(content type)
+     * @param  int    $majorversion   Major part of version number
+     * @param  int    $minorversion   Minor part of version number
+     */
+    public function test_get_available_languages(array $datalib, ?array $expectedlangs = null, ?string $machinename = '',
+            ?int $majorversion = 1, ?int $minorversion = 0): void {
+        $this->resetAfterTest(true);
+
+        // Fetch generator.
+        $generator = \testing_util::get_data_generator();
+        $h5pgenerator = $generator->get_plugin_generator('core_h5p');
+
+        $h5pfilestorage = new file_storage();
+        $h5ptempath = $h5pfilestorage->getTmpPath();
+
+        $translations = [];
+        if ($datalib) {
+            if (array_key_exists('translation', $datalib)) {
+                $translations = $datalib['translation'];
+            }
+            // Create DB entry for this library.
+            $tmplib = $h5pgenerator->create_library_record($datalib['machinename'], $datalib['title'], $datalib['majorversion'],
+                $datalib['minorversion']);
+            // Create the files for this libray.
+            [$library, $files] = $h5pgenerator->create_library($h5ptempath, $tmplib->id, $datalib['machinename'],
+                $datalib['majorversion'], $datalib['minorversion'], $translations);
+            $h5pfilestorage->saveLibrary($library);
+
+            if (empty($machinename)) {
+                $machinename = $datalib['machinename'];
+            }
+            if (empty($majorversion)) {
+                $majorversion = $datalib['majorversion'];
+            }
+            if (empty($minorversion)) {
+                $minorversion = $datalib['minorversion'];
+            }
+        }
+
+        // Get available languages.
+        $langs = $this->editorframework->getAvailableLanguages($machinename, $majorversion, $minorversion);
+
+        $this->assertCount(count($expectedlangs), $langs);
+        $this->assertEquals(ksort($expectedlangs), ksort($langs));
+    }
+
+    /**
+     * Data provider for test_get_available_languages().
+     *
+     * @return array
+     */
+    public function get_available_languages_provider(): array {
+        return [
+            'No library' => [
+                [],
+                [],
+                'Library1',
+                1,
+                2,
+            ],
+            'One library created but getting available from an unexisting one' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                    'translation' => [
+                        'es' => '{"libraryStrings": {"key": "valor"}}',
+                        'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                    ],
+                ],
+                [],
+                'Library2',
+                1,
+                2,
+            ],
+            'One library without any translation' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                ],
+                ['en'],
+            ],
+            'One library with 2 translations (es and fr)' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                    'translation' => [
+                        'es' => '{"libraryStrings": {"key": "valor"}}',
+                        'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                    ],
+                ],
+                ['en', 'es', 'fr'],
+            ],
+            'One library with 3 translations (one of them English)' => [
+                'Library1 1.2' => [
+                    'machinename' => 'Library1',
+                    'title' => 'Lib1',
+                    'majorversion' => 1,
+                    'minorversion' => 2,
+                    'translation' => [
+                        'en' => '{"libraryStrings": {"key": "value"}}',
+                        'es' => '{"libraryStrings": {"key": "valor"}}',
+                        'fr' => '{"libraryStrings": {"key": "valeur"}}',
+                    ],
+                ],
+                ['en', 'es', 'fr'],
+            ],
+        ];
+    }
+
     /**
      * Test that the method getLibraries get the specified libraries or all the content types (runnable = 1).
      */
index c23baba..9cf4313 100644 (file)
@@ -76,15 +76,19 @@ class core_h5p_generator extends \component_generator_base {
      * @param  string $machinename     Name for this library.
      * @param  int    $majorversion    Major version (any number will do).
      * @param  int    $minorversion    Minor version (any number will do).
+     * @param  array  $langs           Languages to be included into the library.
      * @return array A list of library data and files that the core API will understand.
      */
     public function create_library(string $uploaddirectory, int $libraryid, string $machinename, int $majorversion,
-            int $minorversion): array {
-        /** @var array $files an array used in the cache tests. */
-        $files = ['scripts' => [], 'styles' => []];
+            int $minorversion, ?array $langs = []): array {
+        // Array $files used in the cache tests.
+        $files = ['scripts' => [], 'styles' => [], 'language' => []];
 
         check_dir_exists($uploaddirectory . '/' . 'scripts');
         check_dir_exists($uploaddirectory . '/' . 'styles');
+        if (!empty($langs)) {
+            check_dir_exists($uploaddirectory . '/' . 'language');
+        }
 
         $jsonfile = $uploaddirectory . '/' . 'library.json';
         $jsfile = $uploaddirectory . '/' . 'scripts/testlib.min.js';
@@ -92,6 +96,10 @@ class core_h5p_generator extends \component_generator_base {
         $this->create_file($jsonfile);
         $this->create_file($jsfile);
         $this->create_file($cssfile);
+        foreach ($langs as $lang => $value) {
+            $jsonfile = $uploaddirectory . '/' . 'language/' . $lang . '.json';
+            $this->create_file($jsonfile, $value);
+        }
 
         $lib = [
             'title' => 'Test lib',
@@ -120,6 +128,10 @@ class core_h5p_generator extends \component_generator_base {
         $this->add_libfile_to_array('scripts', $path, $version, $files);
         $path = '/' . 'libraries' . '/' . $libraryid .'/' . $libname . '/' . 'styles' . '/' . 'testlib.min.css';
         $this->add_libfile_to_array('styles', $path, $version, $files);
+        foreach ($langs as $lang => $notused) {
+            $path = '/' . 'libraries' . '/' . $libraryid . '/' . $libname . '/' . 'language' . '/' . $lang . '.json';
+            $this->add_libfile_to_array('language', $path, $version, $files);
+        }
 
         return [$lib, $files];
     }
index 31bcd05..08ccfaa 100644 (file)
@@ -688,6 +688,8 @@ $string['langcache'] = 'Cache language menu';
 $string['langcache_desc'] = 'Cache the language menu. If enabled, the list of available translations is cached. The cache is automatically refreshed when you install or delete a language pack via the in-built language packs management tool. If you install a new language pack manually, you have to use Purge all caches feature to refresh the cached list.';
 $string['langlist'] = 'Languages on language menu';
 $string['langmenu'] = 'Display language menu';
+$string['langmenuinsecurelayout'] = 'Display language menu in secure layout';
+$string['langmenuinsecurelayout_desc'] = 'If enabled, a user will be able to change their language when attempting a quiz or other activity using secure layout.';
 $string['langpackwillbeupdated'] = 'NOTE: Moodle will try to download updates for your language packs during the upgrade.';
 $string['langstringcache'] = 'Cache all language strings';
 $string['languagesettings'] = 'Language settings';
@@ -734,6 +736,8 @@ $string['lockrequestcategory'] = 'Prevent category selection';
 $string['log'] = 'Logs';
 $string['logguests'] = 'Log guest access';
 $string['logguests_help'] = 'This setting enables logging of actions by guest account and not logged in users. High profile sites may want to disable this logging for performance reasons. It is recommended to keep this setting enabled on production sites.';
+$string['logininfoinsecurelayout'] = 'Display logged-in user in secure layout';
+$string['logininfoinsecurelayout_desc'] = 'If enabled, the logged-in user\'s full name will be displayed in the navigation bar when attempting a quiz or other activity using secure layout.';
 $string['loginpageautofocus'] = 'Autofocus login page form';
 $string['loginpageautofocus_help'] = 'Enabling this option improves usability of the login page, but automatically focusing fields may be considered an accessibility issue.';
 $string['loglifetime'] = 'Keep logs for';
index eb3dc7b..cb59517 100644 (file)
@@ -57,6 +57,7 @@ $string['cachedef_externalbadges'] = 'External badges for particular user';
 $string['cachedef_fontawesomeiconmapping'] = 'Mapping of icons for font awesome';
 $string['cachedef_suspended_userids'] = 'List of suspended users per course';
 $string['cachedef_groupdata'] = 'Course group information';
+$string['cachedef_h5p_content_type_translations'] = 'H5P content-type libraries translations';
 $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
 $string['cachedef_langmenu'] = 'List of available languages';
 $string['cachedef_message_time_last_message_between_users'] = 'Time created for most recent message in a conversation';
diff --git a/lib/classes/qrcode.php b/lib/classes/qrcode.php
new file mode 100644 (file)
index 0000000..2f24a43
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for generating QR codes. Wrapper class that extends TCPDF.
+ *
+ * @package    core
+ * @copyright  2020 Moodle Pty Ltd.
+ * @author     Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tcpdf/tcpdf_barcodes_2d.php');
+
+/**
+ * Class for generating QR codes. Wrapper class that extends TCPDF.
+ *
+ * @copyright  2020 Moodle Pty Ltd.
+ */
+class core_qrcode extends TCPDF2DBarcode {
+
+    /**
+     * Overrided constructor to force QR codes.
+     *
+     * @param string $data the data to generate the code
+     */
+    public function __construct($data) {
+
+        parent::__construct($data, 'QRCODE');
+    }
+}
index fdf4e3e..7bf811b 100644 (file)
@@ -910,4 +910,63 @@ class manager {
 
         return null;
     }
+
+    /**
+     * Find the path of PHP CLI binary.
+     *
+     * @return string|false The PHP CLI executable PATH
+     */
+    protected static function find_php_cli_path() {
+        global $CFG;
+
+        if (!empty($CFG->pathtophp) && is_executable(trim($CFG->pathtophp))) {
+            return $CFG->pathtophp;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns if Moodle have access to PHP CLI binary or not.
+     *
+     * @return bool
+     */
+    public static function is_runnable():bool {
+        return self::find_php_cli_path() !== false;
+    }
+
+    /**
+     * Executes a cron from web invocation using PHP CLI.
+     *
+     * @param \core\task\task_base $task Task that be executed via CLI.
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function run_from_cli(\core\task\task_base $task):bool {
+        global $CFG;
+
+        if (!self::is_runnable()) {
+            $redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
+            throw new \moodle_exception('cannotfindthepathtothecli', 'core_task', $redirecturl->out());
+        } else {
+            // Shell-escaped path to the PHP binary.
+            $phpbinary = escapeshellarg(self::find_php_cli_path());
+
+            // Shell-escaped path CLI script.
+            $pathcomponents = [$CFG->dirroot, $CFG->admin, 'cli', 'scheduled_task.php'];
+            $scriptpath     = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents));
+
+            // Shell-escaped task name.
+            $classname = get_class($task);
+            $taskarg   = escapeshellarg("--execute={$classname}");
+
+            // Build the CLI command.
+            $command = "{$phpbinary} {$scriptpath} {$taskarg}";
+
+            // Execute it.
+            passthru($command);
+        }
+
+        return true;
+    }
 }
index f15fae8..917e5b5 100644 (file)
@@ -365,89 +365,6 @@ function cron_run_inner_adhoc_task(\core\task\adhoc_task $task) {
     get_mailer('close');
 }
 
-/**
- * Runs a single cron task. This function assumes it is displaying output in pseudo-CLI mode.
- *
- * The function will fail if the task is disabled.
- *
- * Warning: Because this function closes the browser session, it may not be safe to continue
- * with other processing (other than displaying the rest of the page) after using this function!
- *
- * @param \core\task\scheduled_task $task Task to run
- * @return bool True if cron run successful
- */
-function cron_run_single_task(\core\task\scheduled_task $task) {
-    global $CFG, $DB, $USER;
-
-    if (CLI_MAINTENANCE) {
-        echo "CLI maintenance mode active, cron execution suspended.\n";
-        return false;
-    }
-
-    if (moodle_needs_upgrading()) {
-        echo "Moodle upgrade pending, cron execution suspended.\n";
-        return false;
-    }
-
-    // Check task and component is not disabled.
-    $taskname = get_class($task);
-    if ($task->get_disabled()) {
-        echo "Task is disabled ($taskname).\n";
-        return false;
-    }
-    $component = $task->get_component();
-    if ($plugininfo = core_plugin_manager::instance()->get_plugin_info($component)) {
-        if ($plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled()) {
-            echo "Component is not enabled ($component).\n";
-            return false;
-        }
-    }
-
-    // Enable debugging features as per config settings.
-    if (!empty($CFG->showcronsql)) {
-        $DB->set_debug(true);
-    }
-    if (!empty($CFG->showcrondebugging)) {
-        set_debugging(DEBUG_DEVELOPER, true);
-    }
-
-    // Increase time and memory limits.
-    core_php_time_limit::raise();
-    raise_memory_limit(MEMORY_EXTRA);
-
-    // Switch to admin account for cron tasks, but close the session so we don't send this stuff
-    // to the browser.
-    session_write_close();
-    $realuser = clone($USER);
-    cron_setup_user(null, null, true);
-
-    // Get lock for cron task.
-    $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
-    if (!$cronlock = $cronlockfactory->get_lock('core_cron', 1)) {
-        echo "Unable to get cron lock.\n";
-        return false;
-    }
-    if (!$lock = $cronlockfactory->get_lock($taskname, 1)) {
-        $cronlock->release();
-        echo "Unable to get task lock for $taskname.\n";
-        return false;
-    }
-    $task->set_lock($lock);
-    if (!$task->is_blocking()) {
-        $cronlock->release();
-    } else {
-        $task->set_cron_lock($cronlock);
-    }
-
-    // Run actual tasks.
-    cron_run_inner_scheduled_task($task);
-
-    // Go back to real user account.
-    cron_setup_user($realuser, null, true);
-
-    return true;
-}
-
 /**
  * Output some standard information during cron runs. Specifically current time
  * and memory usage. This method also does gc_collect_cycles() (before displaying
index fc05d97..a5bd37e 100644 (file)
@@ -446,4 +446,12 @@ $definitions = array(
         'simpledata' => true,
         'staticacceleration' => true,
     ],
+
+    // Language strings for H5P content-type libraries.
+    // Key "{$libraryname}/{$language}"" contains translations for a given library and language.
+    // Key "$libraryname" has a list of all of the available languages for the library.
+    'h5p_content_type_translations' => [
+        'mode' => cache_store::MODE_APPLICATION,
+        'simpledata' => true,
+    ],
 );
index 01b389e..938746e 100644 (file)
@@ -3406,3 +3406,22 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
     core_collator::asort_objects_by_property($return, 'title');
     return $return;
 }
+
+/**
+ * Runs a single cron task. This function assumes it is displaying output in pseudo-CLI mode.
+ *
+ * The function will fail if the task is disabled.
+ *
+ * Warning: Because this function closes the browser session, it may not be safe to continue
+ * with other processing (other than displaying the rest of the page) after using this function!
+ *
+ * @deprecated since Moodle 3.9 MDL-63580. Please use the \core\task\manager::run_from_cli($task).
+ * @todo final deprecation. To be removed in Moodle 4.3 MDL-63594.
+ * @param \core\task\scheduled_task $task Task to run
+ * @return bool True if cron run successful
+ */
+function cron_run_single_task(\core\task\scheduled_task $task) {
+    debugging('cron_run_single_task() is deprecated. Please use \\core\task\manager::run_from_cli() instead.',
+        DEBUG_DEVELOPER);
+    return \core\task\manager::run_from_cli($task);
+}
index 81e2995..2e589f5 100644 (file)
@@ -3462,6 +3462,35 @@ EOD;
         );
     }
 
+    /**
+     * Secure layout login info.
+     *
+     * @return string
+     */
+    public function secure_layout_login_info() {
+        if (get_config('core', 'logininfoinsecurelayout')) {
+            return $this->login_info(false);
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * Returns the language menu in the secure layout.
+     *
+     * No custom menu items are passed though, such that it will render only the language selection.
+     *
+     * @return string
+     */
+    public function secure_layout_language_menu() {
+        if (get_config('core', 'langmenuinsecurelayout')) {
+            $custommenu = new custom_menu('', current_language());
+            return $this->render_custom_menu($custommenu);
+        } else {
+            return '';
+        }
+    }
+
     /**
      * This renders the navbar.
      * Uses bootstrap compatible html.
index 71627e0..1baa41c 100644 (file)
@@ -18,3 +18,21 @@ Feature: Page displaying with secure layout
     When I follow "Fixture link"
     Then I should see "Acceptance test site" in the "nav" "css_element"
     But "Acceptance test site" "link" should not exist
+
+  Scenario: Confirm that the user name is displayed in the navbar without a link
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | logininfoinsecurelayout | 1 |
+    And I am on "Course 1" course homepage
+    When I follow "Fixture link"
+    Then I should see "You are logged in as Admin User" in the "nav" "css_element"
+    But "Logout" "link" should not exist
+
+  Scenario: Confirm that the custom menu items do not appear when language selection is enabled
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | langmenuinsecurelayout | 1 |
+      | custommenuitems | -This is a custom item\|/customurl/ |
+    And I am on "Course 1" course homepage
+    When I follow "Fixture link"
+    Then I should not see "This is a custom item" in the "nav" "css_element"
diff --git a/lib/tests/qrcode_test.php b/lib/tests/qrcode_test.php
new file mode 100644 (file)
index 0000000..95152c4
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Test QR code functionality.
+ *
+ * @package    core
+ * @copyright  Moodle Pty Ltd
+ * @author     <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A set of tests for some of the QR code functionality within Moodle.
+ *
+ * @package    core
+ * @copyright  Moodle Pty Ltd
+ * @author     <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_qrcode_testcase extends basic_testcase {
+
+    /**
+     * Basic test to generate a QR code and check that the library is not broken.
+     */
+    public function test_generate_basic_qr() {
+        // The QR code generator library apply masks by random order, this is why everytime a QR code is generated the resultant
+        // binary file can be different. This is why tests are limited.
+
+        $text = 'abc';
+        $color = 'black';
+        $qrcode = new core_qrcode($text, $color);
+        $svgdata = $qrcode->getBarcodeSVGcode(1, 1);
+
+        // Just check the SVG was generated.
+        $this->assertContains('<desc>' . $text . '</desc>', $svgdata);
+        $this->assertContains('fill="' . $color . '"', $svgdata);
+    }
+}
index d8ab4bc..68379ea 100644 (file)
@@ -2,6 +2,13 @@ This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
 === 3.9 ===
+* Following function has been deprecated, please use \core\task\manager::run_from_cli().
+    - cron_run_single_task()
+* Following class has been deprecated, please use \core\task\manager.
+    - \tool_task\run_from_cli
+* Following CLI scripts has been deprecated:
+  - admin/tool/task/cli/schedule_task.php please use admin/cli/scheduled_task.php
+  - admin/tool/task/cli/adhoc_task.php please use admin/cli/adhoc_task.php
 * Old Safe Exam Browser quiz access rule (quizaccess_safebrowser) replaced by new Safe Exam Browser access rule (quizaccess_seb).
   Experimental setting enablesafebrowserintegration was deleted.
 * New CFPropertyList library has been added to Moodle core in /lib/plist.
index e65a202..240147f 100644 (file)
@@ -390,9 +390,11 @@ YUI.add('moodle-core_filepicker', function(Y) {
             div.appendChild(checkboxLabel);
             div.appendChild(checkbox);
 
-
+            // Define the selector for the click event handler.
+            var clickEventSelector = 'tr';
             // Enable the selectable checkboxes
             if (options.disablecheckboxes != undefined && !options.disablecheckboxes) {
+                clickEventSelector = 'tr td:not(:first-child)';
                 cols.unshift({
                     key: "",
                     label: div.getContent(),
@@ -411,7 +413,7 @@ YUI.add('moodle-core_filepicker', function(Y) {
                     }
                     Y.bind(callback, this)(e, record.getAttrs());
                 }
-            }, 'tr td:not(:first-child)', options.callbackcontext, scope.tableview);
+            }, clickEventSelector, options.callbackcontext, scope.tableview);
 
             if (options.rightclickcallback) {
                 scope.tableview.delegate('contextmenu', function (e, tableview) {
diff --git a/repository/tests/behat/edit_file.feature b/repository/tests/behat/edit_file.feature
new file mode 100644 (file)
index 0000000..d58204d
--- /dev/null
@@ -0,0 +1,55 @@
+@core @core_filepicker @_file_upload
+Feature: Edit file feature
+  In order to edit a file
+  As a user
+  I need to be able to select the file in filemanager and modify the file information
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+
+  @javascript
+  Scenario: Select file from "Files" filemanager using "icons" view and edit the name
+    Given I log in as "admin"
+    And I follow "Manage private files"
+    And I click on "Display folder with file icons" "link" in the ".filemanager" "css_element"
+    And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
+    And I should see "empty.txt" in the ".fp-content .fp-file" "css_element"
+    And I click on "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]/descendant::a[normalize-space(.)='empty.txt']" "xpath_element"
+    And I should see "Edit empty.txt"
+    And I set the following fields to these values:
+      | Name  | empty_edited.txt |
+    When I click on "Update" "button"
+    Then I should see "empty_edited.txt" in the ".fp-content .fp-file" "css_element"
+    And I should not see "empty.txt" in the ".fp-content .fp-file" "css_element"
+
+  @javascript
+  Scenario: Select file from "Files" filemanager using "list" view and edit the name
+    Given I log in as "admin"
+    And I follow "Manage private files"
+    And I click on "Display folder with file details" "link" in the ".filemanager" "css_element"
+    And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
+    And I should see "empty.txt" in the ".fp-content .fp-filename" "css_element"
+    And I click on "//span[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename-icon ')]/descendant::a[normalize-space(.)='empty.txt']" "xpath_element"
+    And I should see "Edit empty.txt"
+    And I set the following fields to these values:
+      | Name  | empty_edited.txt |
+    When I click on "Update" "button"
+    Then I should see "empty_edited.txt" in the ".fp-content .fp-filename" "css_element"
+    And I should not see "empty.txt" in the ".fp-content .fp-filename" "css_element"
+
+  @javascript
+  Scenario: Select file from "Files" filemanager using "tree" view and edit the name
+    Given I log in as "admin"
+    And I follow "Manage private files"
+    And I click on "Display folder as file tree" "link" in the ".filemanager" "css_element"
+    And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
+    And I should see "empty.txt" in the ".fp-content .fp-hascontextmenu .fp-filename" "css_element"
+    And I click on "//span[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename-icon ')]/descendant::a[normalize-space(.)='empty.txt']" "xpath_element"
+    And I should see "Edit empty.txt"
+    And I set the following fields to these values:
+      | Name  | empty_edited.txt |
+    When I click on "Update" "button"
+    Then I should see "empty_edited.txt" in the ".fp-content .fp-hascontextmenu .fp-filename" "css_element"
+    And I should not see "empty.txt" in the ".fp-content .fp-hascontextmenu .fp-filename" "css_element"
diff --git a/repository/tests/behat/select_file.feature b/repository/tests/behat/select_file.feature
new file mode 100644 (file)
index 0000000..e7cd4eb
--- /dev/null
@@ -0,0 +1,73 @@
+@core @core_filepicker @_file_upload
+Feature: Select file feature
+  In order to add a file to a filearea
+  As a user
+  I need to be able to select the file using the file picker
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+
+  @javascript
+  Scenario: Select a file from the "Recent files" repository using "icons" view
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Folder" to section "1"
+    And I set the following fields to these values:
+      | Name        | Test folder             |
+      | Description | Test folder description |
+    And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
+    And I click on "Save and display" "button"
+    And I follow "Dashboard" in the user menu
+    And I follow "Manage private files"
+    And I click on "//label[contains(., 'Files')]/ancestor::div[contains(concat(' ', @class, ' '), ' fitem ')]//*[contains(@title, 'Add...')]" "xpath_element"
+    And I click on "Recent files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "Display folder with file icons" "link" in the ".file-picker" "css_element"
+    And I click on "//a[contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')][normalize-space(.)='empty.txt']" "xpath_element"
+    And I should see "Select empty.txt"
+    When I click on "Select this file" "button"
+    Then I should see "1" elements in "Files" filemanager
+    And I should see "empty.txt" in the ".fp-content .fp-file" "css_element"
+
+  @javascript
+  Scenario: Select a file from the "Recent files" repository using "list" view
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Folder" to section "1"
+    And I set the following fields to these values:
+      | Name        | Test folder             |
+      | Description | Test folder description |
+    And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
+    And I click on "Save and display" "button"
+    And I follow "Dashboard" in the user menu
+    And I follow "Manage private files"
+    And I click on "//label[contains(., 'Files')]/ancestor::div[contains(concat(' ', @class, ' '), ' fitem ')]//*[contains(@title, 'Add...')]" "xpath_element"
+    And I click on "Recent files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "Display folder with file details" "link" in the ".file-picker" "css_element"
+    And I click on "//div[contains(concat(' ', normalize-space(@class), ' '), ' file-picker ')]/descendant::span[normalize-space(.)='empty.txt']/ancestor::a" "xpath_element"
+    And I should see "Select empty.txt"
+    When I click on "Select this file" "button"
+    Then I should see "1" elements in "Files" filemanager
+    And I should see "empty.txt" in the ".fp-content .fp-file" "css_element"
+
+  @javascript
+  Scenario: Select a file from the "Recent files" repository using "tree" view
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Folder" to section "1"
+    And I set the following fields to these values:
+      | Name        | Test folder             |
+      | Description | Test folder description |
+    And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager
+    And I click on "Save and display" "button"
+    And I follow "Dashboard" in the user menu
+    And I follow "Manage private files"
+    And I click on "//label[contains(., 'Files')]/ancestor::div[contains(concat(' ', @class, ' '), ' fitem ')]//*[contains(@title, 'Add...')]" "xpath_element"
+    And I click on "Recent files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "Display folder as file tree" "link" in the ".file-picker" "css_element"
+    And I click on "//div[contains(concat(' ', normalize-space(@class), ' '), ' file-picker ')]/descendant::span[normalize-space(.)='empty.txt']/ancestor::a" "xpath_element"
+    And I should see "Select empty.txt"
+    When I click on "Select this file" "button"
+    Then I should see "1" elements in "Files" filemanager
+    And I should see "empty.txt" in the ".fp-content .fp-file" "css_element"
index 7b0ae6d..d4e9643 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    secure navbar.
+    @template theme_boost/navbar-secure
+
+    This template renders the top navbar.
+
+    Example context (json):
+    {
+        "output": {
+            "should_display_navbar_logo": "true",
+            "get_compact_logo_url": "http://example.com/image.png"
+        },
+        "sitename": "Moodle Site"
+    }
 }}
 <nav class="fixed-top navbar navbar-light bg-white navbar-expand moodle-has-zindex" aria-label="{{#str}}navigation{{/str}}">
 
         {{/ output.should_display_navbar_logo }}
         <span class="site-name d-none d-md-inline">{{{ sitename }}}</span>
 
-        <ul class="nav navbar-nav ml-auto">
-            <li class="nav-item">
-                {{{ output.secure_login_info }}}
-            </li>
+        {{# output.secure_layout_language_menu }}
+        <ul class="navbar-nav d-none d-md-flex">
+            <!-- language_menu -->
+            {{{ . }}}
         </ul>
+        {{/ output.secure_layout_language_menu }}
+        {{# output.secure_layout_login_info }}
+        <div class="ml-auto">
+            {{{ . }}}
+        </div>
+        {{/ output.secure_layout_login_info }}
 </nav>
index 3dc50c4..91b6ad4 100644 (file)
@@ -25,8 +25,7 @@
             "should_display_navbar_logo": "true",
             "get_compact_logo_url": "http://example.com/image.png"
         },
-        "sitename": "Moodle Site",
-        "secure_login_info": "Logged in as test user"
+        "sitename": "Moodle Site"
     }
 }}
 <nav class="fixed-top navbar navbar-bootswatch navbar-expand moodle-has-zindex">
     {{/ output.should_display_navbar_logo }}
     <span class="site-name d-none d-md-inline">{{{ sitename }}}</span>
 
-    <ul class="nav navbar-nav ml-auto">
-        <li class="nav-item">
-            {{{ output.secure_login_info }}}
-        </li>
+    {{# output.secure_layout_language_menu }}
+    <ul class="navbar-nav d-none d-md-flex">
+        <!-- language_menu -->
+        {{{ . }}}
     </ul>
+    {{/ output.secure_layout_language_menu }}
+    {{# output.secure_layout_login_info }}
+    <div class="ml-auto">
+        {{{ . }}}
+    </div>
+    {{/ output.secure_layout_login_info }}
 </nav>
index 3d2651d..8aa4d4d 100644 (file)
@@ -4,6 +4,10 @@ information provided here is intended especially for theme designer.
 === 3.9 ===
 
 * Add class .d-print-block to #page, #page-wrapper and #page content to fix Firefox printing problems
+* A function to core_renderer has been added, secure_layout_login_info. This allows the boost and classic templates to
+  display the users full name in a secure layout.
+* Secure layout in themes boost and classic have been modified to allow language selection as they now call the
+  output.secure_layout_language_menu function.
 
 === 3.8 ===