Compare commits

..

43 Commits

Author SHA1 Message Date
Huong Nguyen 239be346a1 Moodle release 4.1.21 2025-10-04 09:02:50 +07:00
AMOS bot e23a5c6b85 Automatically generated installer lang files 2025-10-04 00:07:41 +00:00
Shamim Rezaie fd6667f694 weekly release 4.1.20+ 2025-10-03 20:02:43 +10:00
Mihail Geshoski 6d990ff157 MDL-75087 mod_assign: Fix failing behat tests in classic 2025-10-03 14:23:15 +07:00
Mihail Geshoski 5ad39879ea MDL-75087 mod_assign: Fix failing unit test 2025-10-02 18:10:13 +07:00
Rajneel Totaram 0e8027d3dd MDL-75087 mod_assign: Respect assignment timer
This patch has better handling of timed assignments.
Users cannot directly access the submission page without
confirming to begin a timed assignment.
2025-10-02 10:48:05 +08:00
Mihail Geshoski 13ab8ce3f6 Merge branch 'MDL-86637-401' of https://github.com/cameron1729/moodle into MOODLE_401_STABLE 2025-10-02 01:31:42 +08:00
yusufwib01 321f20bde4 MDL-86353 mod_assign: Sync exception handling in PdfType and PdfReader 2025-09-30 09:24:29 +07:00
Paul Holden ccb94debcf MDL-85421 cohort: limit requests for all cohorts to system context. 2025-09-29 13:34:09 +07:00
David Woloszyn 596f290ca7 MDL-86327 auth_webservice: Stricter check on login 2025-09-29 08:24:56 +07:00
yusufwib01 3cf753aea9 MDL-86261 calendar: Improve event form to show accessible groups 2025-09-26 12:00:20 +00:00
cescobedo 2689a8fdd3 weekly release 4.1.20+ 2025-09-26 13:44:36 +02:00
AMOS bot 0ef7b2853d Automatically generated installer lang files 2025-09-25 00:07:39 +00:00
Sara Arjona 3389ddbe77 weekly release 4.1.20+ 2025-09-23 17:21:20 +02:00
Huong Nguyen 9c0d3f8037 Merge branch 'MDL-84870-401' of https://github.com/andimendunia/moodle into MOODLE_401_STABLE 2025-09-22 08:29:01 +07:00
Huong Nguyen 7b5bdf9b6e weekly release 4.1.20+ 2025-09-18 08:34:34 +07:00
Huong Nguyen 78468dffaa Merge branch 'install_401_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_401_STABLE 2025-09-18 08:34:32 +07:00
Huong Nguyen d314c4719d MDL-86495 core: Improve PHPUnit for IPLookup 2025-09-16 08:52:18 +07:00
Andi Permana 4b30d08025 MDL-84870 core: Update security.txt expiry 2025-09-15 13:04:41 +07:00
Cameron Ball bc29ef5d9e MDL-86637 core: Use ReflectionProperty to access static navigation data
In PHP 7.4.0, ReflectionClass and static properties don't play nicely
together. Using ReflectionProperty side steps the issue.
2025-09-15 13:01:56 +08:00
Huong Nguyen 3030cde4ba MDL-86495 core: Better errors handling for IP Lookup
- Since `Geoplugin` is not free anymore, a new admin setting called `GeoPlugin API key` has been added for user to input
  their API key if they still want to use `Geoplugin` service.
- The `iplookup_find_location()` method has been improved to handle error responses more effectively.
2025-09-09 08:47:14 +07:00
AMOS bot bfef5e4581 Automatically generated installer lang files 2025-09-06 00:07:42 +00:00
AMOS bot 17c4412033 Automatically generated installer lang files 2025-09-03 00:07:39 +00:00
AMOS bot 5b9e5416d5 Automatically generated installer lang files 2025-09-01 00:07:35 +00:00
cescobedo d3777d8a06 weekly release 4.1.20+ 2025-08-29 15:01:25 +02:00
Huong Nguyen 3613faf8d6 Merge branch 'MDL-86452-401' of https://github.com/andrewnicols/moodle into MOODLE_401_STABLE 2025-08-29 08:46:43 +07:00
Andrew Nicols 9b060f43ce MDL-86452 core: Reset navigation data between tests 2025-08-28 13:13:05 +08:00
cescobedo d63b359944 weekly release 4.1.20+ 2025-08-26 15:54:21 +02:00
cescobedo 144c57c4ff Merge branch 'install_401_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_401_STABLE 2025-08-26 15:54:19 +02:00
Mihail Geshoski 9956df769b Merge branch 'MDL-85677-401' of https://github.com/lameze/moodle into MOODLE_401_STABLE 2025-08-26 11:55:55 +08:00
Huong Nguyen b2fab52908 Merge branch 'MDL-86315-401' of https://github.com/davewoloszyn/moodle into MOODLE_401_STABLE 2025-08-26 09:23:42 +07:00
Simey Lameze 787199fa99 MDL-85677 behat: test to verify SEB download link visibility 2025-08-26 09:19:15 +08:00
Mihail Geshoski 0afa5bfdb5 Merge branch 'MDL-85676-401' of https://github.com/lameze/moodle into MOODLE_401_STABLE 2025-08-26 00:42:17 +08:00
Simey Lameze 9247cf1bd6 MDL-85676 behat: validate SEB attempt message 2025-08-25 14:08:20 +08:00
AMOS bot 42eeec6261 Automatically generated installer lang files 2025-08-24 00:09:44 +00:00
Mihail Geshoski 8b234f6690 weekly release 4.1.20+ 2025-08-22 20:40:46 +08:00
Mihail Geshoski f4795ade4a Merge branch 'install_401_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_401_STABLE 2025-08-22 20:40:42 +08:00
Huong Nguyen fc1a79d882 Merge branch 'MDL-86231-401' of https://github.com/andrewnicols/moodle into MOODLE_401_STABLE 2025-08-22 08:47:08 +07:00
AMOS bot 6b715e7e7f Automatically generated installer lang files 2025-08-16 00:07:39 +00:00
David Woloszyn bd96199040 MDL-86315 registration: Unset emails in hub when dependant value not set 2025-08-15 13:01:17 +10:00
AMOS bot 127b856a73 Automatically generated installer lang files 2025-08-14 00:07:47 +00:00
AMOS bot 28f675da81 Automatically generated installer lang files 2025-08-13 00:07:48 +00:00
Andrew Nicols f2552bb9c4 MDL-86231 behat: Support array callable syntax in execute method 2025-08-08 11:42:26 +08:00
34 changed files with 494 additions and 70 deletions
@@ -0,0 +1,24 @@
issueNumber: MDL-86231
notes:
core:
- message: >
The Behat `::execute()` method now accepts an array-style callable in
addition to the string `classname::method` format.
The following formats are now accepted:
```php
// String format:
$this->execute('behat_general::i_click_on', [...]);
// Array format:
$this->execute([behat_general::class],' i_click_on'], [...]);
```
type: improved
+8
View File
@@ -51,6 +51,14 @@ if ($hassiteconfig) {
$temp->add(new admin_setting_countrycodes('allcountrycodes', new lang_string('allcountrycodes', 'core_admin'),
new lang_string('configallcountrycodes', 'core_admin')));
$temp->add(new admin_setting_configtext(
'geopluginapikey',
new lang_string('geopluginapikey', 'core_admin'),
new lang_string('geopluginapikey_desc', 'core_admin'),
'',
PARAM_TEXT,
));
}
$ADMIN->add('location', $temp);
+9 -1
View File
@@ -73,7 +73,15 @@ class auth_plugin_webservice extends auth_plugin_base {
function user_login_webservice($username, $password) {
global $CFG, $DB;
// special web service login
if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
$user = $DB->get_record('user', [
'username' => $username,
'auth' => 'webservice',
'deleted' => '0',
'suspended' => '0',
'mnethostid' => $CFG->mnet_localhost_id,
]);
if ($user) {
return validate_internal_user_password($user, $password);
}
return false;
+3 -3
View File
@@ -911,10 +911,10 @@ class core_calendar_external extends external_api {
$formoptions['eventtypes'] = $allowedeeventtypes;
if ($courseid) {
require_once($CFG->libdir . '/grouplib.php');
$groupcoursedata = groups_get_course_data($courseid);
if (!empty($groupcoursedata->groups)) {
$groupcoursedata = groups_get_all_groups($courseid);
if (!empty($groupcoursedata)) {
$formoptions['groups'] = [];
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
foreach ($groupcoursedata as $groupid => $groupdata) {
$formoptions['groups'][$groupid] = $groupdata->name;
}
}
+4 -4
View File
@@ -3608,9 +3608,9 @@ function calendar_output_fragment_event_form($args) {
if (is_null($eventid)) {
if (!empty($courseid)) {
$groupcoursedata = groups_get_course_data($courseid);
$groupcoursedata = groups_get_all_groups($courseid);
$formoptions['groups'] = [];
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
foreach ($groupcoursedata as $groupid => $groupdata) {
$formoptions['groups'][$groupid] = $groupdata->name;
}
}
@@ -3652,9 +3652,9 @@ function calendar_output_fragment_event_form($args) {
$formoptions['event'] = $event;
if (!empty($event->courseid)) {
$groupcoursedata = groups_get_course_data($event->courseid);
$groupcoursedata = groups_get_all_groups($event->courseid);
$formoptions['groups'] = [];
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
foreach ($groupcoursedata as $groupid => $groupdata) {
$formoptions['groups'][$groupid] = $groupdata->name;
}
}
+6
View File
@@ -380,6 +380,12 @@ class core_cohort_external extends external_api {
$results = array_merge($results, cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query));
}
} else if ($includes == 'all') {
$contextsystem = context_system::instance();
if (!$context instanceof context_system &&
!has_any_capability(['moodle/cohort:view', 'moodle/cohort:manage'], $contextsystem)) {
throw new required_capability_exception($contextsystem, 'moodle/cohort:view', 'nopermissions', '');
}
$results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
$results = $results['cohorts'];
} else {
+16 -6
View File
@@ -555,6 +555,7 @@ final class externallib_test extends externallib_advanced_testcase {
role_assign($userrole, $catuser->id, $catcontext->id);
// Enrol user in the course.
$this->getDataGenerator()->enrol_user($creator->id, $course->id);
$this->getDataGenerator()->enrol_user($courseuser->id, $course->id, 'courserole');
$syscontext = array('contextid' => \context_system::instance()->id);
@@ -612,18 +613,27 @@ final class externallib_test extends externallib_advanced_testcase {
$this->assertEquals(3, count($result['cohorts']));
// A user in the course context with the system cohort:view capability. Check that all the system cohorts are returned.
$this->setUser($courseuser);
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
$this->assertEquals(1, count($result['cohorts']));
$this->assertEquals('Cohortsearch 1', $result['cohorts'][$cohort1->id]->name);
$this->assertEquals(3, count($result['cohorts']));
// A user in the course context without the ability to view system cohorts.
$this->setUser($courseuser);
try {
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
$this->fail('Exception expected');
} catch (\Throwable $e) {
$this->assertInstanceOf(\required_capability_exception::class, $e);
$this->assertStringContainsString('(View site-wide cohorts)', $e->getMessage());
}
// Detect invalid parameter $includes.
$this->setUser($creator);
try {
$result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'invalid');
$this->fail('Invalid parameter includes');
} catch (\coding_exception $e) {
// All good.
$this->fail('Exception expected');
} catch (\Throwable $e) {
$this->assertInstanceOf(\coding_exception::class, $e);
$this->assertStringContainsString('Invalid parameter value for \'includes\'', $e->getMessage());
}
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
/**
* Automatically generated strings for Moodle installer
*
* Do not edit this file manually! It contains just a subset of strings
* needed during the very first steps of installation. This file was
* generated automatically by export-installer.php (which is part of AMOS
* {@link https://moodledev.io/general/projects/api/amos}) using the
* list of strings defined in install/stringnames.txt file.
*
* @package installer
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['cliincorrectvalueerror'] = 'Mearachd, luach ceàrr "{$a->value}" airson "{$a->option}"';
$string['cliincorrectvalueretry'] = 'Luach ceàrr, feuch a-rithist';
$string['cliunknowoption'] = 'Roghainnean nach deach an aithneachadh:
{$a}
Cleachd— an roghainn airson cuideachadh';
$string['environmentrequireversion'] = 'tha tionndadh {$a->needed} deatamach agus tha thusa a ruith {$a->current}';
+1
View File
@@ -32,3 +32,4 @@ defined('MOODLE_INTERNAL') || die();
$string['language'] = 'Cànan';
$string['moodlelogo'] = 'Logo Mhoodle';
$string['previous'] = 'Roimhe';
$string['reload'] = 'Ath-luchdaich';
+1 -1
View File
@@ -45,7 +45,7 @@ $string['dmlexceptiononinstall'] = '<p>データベースエラーが発生し
$string['downloadedfilecheckfailed'] = 'ダウンロードファイルのチェックに失敗しました。';
$string['invalidmd5'] = 'チェック変数が正しくありません - 再度お試しください。';
$string['missingrequiredfield'] = 'いくつかの必須入力フィールドが入力されていません。';
$string['remotedownloaderror'] = '<p>あなたのサーバへのコンポーネントのダウンロードに失敗しました。プロキシ設定を確認してください。PHP cURL拡張モジュールの使用を強くお勧めします。</p>
$string['remotedownloaderror'] = '<p>あなたのサーバへのコンポーネントのダウンロードに失敗しました。プロキシ設定を確認ください。PHP cURL拡張モジュールの使用を強くお勧めします。</p>
<p><a href="{$a->url}">{$a->url}</a>ファイルを手動でダウンロードした後、あなたのサーバの「{$a->dest}」にコピーおよび展開してください。</p>';
$string['wrongdestpath'] = '宛先パスが正しくありません。';
$string['wrongsourcebase'] = 'ソースURLベースが正しくありません。';
+4 -1
View File
@@ -32,10 +32,13 @@ defined('MOODLE_INTERNAL') || die();
$string['clianswerno'] = 'n';
$string['cliansweryes'] = 'y';
$string['cliincorrectvalueerror'] = 'Xato, “{$a->option}” uchun notogri “{$a->value}” qiymati';
$string['cliincorrectvalueretry'] = 'Noto\'g\'ri qiymat, iltimos qayta urinib ko\'ring';
$string['cliincorrectvalueretry'] = 'Notogri qiymat, iltimos qayta urinib koring';
$string['clitypevalue'] = 'qiymatni kiritish';
$string['clitypevaluedefault'] = 'qiymatni kiriting, ({$a}) odatiy qiymatni ishlatish uchun Enter tumgasini bosing';
$string['cliunknowoption'] = 'Noma\'lum parametrlar:
{$a}
Iltimos, -- help parametridan foydalaning';
$string['cliyesnoprompt'] = 'y tugmasini bosing (ha degani) yoki n tugmasini bosing (yoq degani)';
$string['environmentrequireinstall'] = 'ornatilishi va yoqilishi shart.';
$string['environmentrequireversion'] = '{$a->needed} versiyasi talab etiladi, sizda esa hozirda {$a->current} ishlatilmoqda';
$string['upgradekeyset'] = 'Yangilash kaliti (ornatmaslik uchun bosh qoldiring)';
+39
View File
@@ -0,0 +1,39 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
/**
* Automatically generated strings for Moodle installer
*
* Do not edit this file manually! It contains just a subset of strings
* needed during the very first steps of installation. This file was
* generated automatically by export-installer.php (which is part of AMOS
* {@link https://moodledev.io/general/projects/api/amos}) using the
* list of strings defined in install/stringnames.txt file.
*
* @package installer
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['cannotcreatedboninstall'] = '<p>Ma\'lumotlar bazasini yaratib bo\'lmadi.</p>
<p>Ko\'rsatilgan ma\'lumotlar bazasi mavjud emas va berilgan foydalanuvchida uni yaratish huquqi yo\'q.</p>
<p>Sayt administratori ma\'lumotlar bazasi konfiguratsiyasini tekshirishi kerak.</p>';
$string['cannotcreatelangdir'] = 'Til katalogini yaratib bo\'lmadi';
$string['cannotcreatetempdir'] = 'Vaqtinchalik jildni yaratib bolmadi';
$string['cannotdownloadcomponents'] = 'Komponentlarni yuklab olib bo\'lmadi';
$string['cannotdownloadzipfile'] = 'ZIP faylini yuklab olib bo\'lmadi';
$string['cannotfindcomponent'] = 'Komponentni topib bo\'lmadi';
+4 -4
View File
@@ -29,9 +29,9 @@
defined('MOODLE_INTERNAL') || die();
$string['admindirname'] = 'Administrator katalogi nomi';
$string['availablelangs'] = 'Mavjud(foydalanish mumkin bo\'lgan)til paketlari';
$string['dataroot'] = 'Ma\'lumotlar katalogi';
$string['admindirname'] = 'Admin katalogi';
$string['availablelangs'] = 'Mavjud til paketlari';
$string['dataroot'] = 'Malumotlar katalogi';
$string['dbprefix'] = 'Jadvallar prefiksi';
$string['dirroot'] = 'Moodle kataloglari';
$string['dirroot'] = 'Moodle katalogi';
$string['installation'] = 'O\'rnatish';
+10 -5
View File
@@ -64,7 +64,7 @@ function iplookup_find_location($ip) {
return $info;
} else {
} else if (!empty($CFG->geopluginapikey)) {
require_once($CFG->libdir.'/filelib.php');
if (strpos($ip, ':') !== false) {
@@ -73,11 +73,13 @@ function iplookup_find_location($ip) {
return $info;
}
$ipdata = download_file_content('http://www.geoplugin.net/json.gp?ip='.$ip);
if ($ipdata) {
$ipdata = preg_replace('/^geoPlugin\((.*)\)\s*$/s', '$1', $ipdata);
$ipdata = json_decode($ipdata, true);
$requesturl = new moodle_url('https://api.geoplugin.com', ['ip' => $ip, 'auth' => $CFG->geopluginapikey]);
$response = download_file_content($requesturl->out(false), null, null, true);
if ($response->response_code != 200) {
$info['error'] = get_string('cannotgeoplugin', 'error');
return $info;
}
$ipdata = json_decode($response->results, true);
if (!is_array($ipdata)) {
$info['error'] = get_string('cannotgeoplugin', 'error');
return $info;
@@ -103,4 +105,7 @@ function iplookup_find_location($ip) {
return $info;
}
$info['error'] = get_string('iplookupfailed', 'error', $ip);
return $info;
}
+5 -1
View File
@@ -50,8 +50,12 @@ final class geoip_test extends \advanced_testcase {
* @param string $ip The IP to test
*/
public function test_ip($ip): void {
global $CFG;
if (!defined('TEST_GEOIP_APIKEY') || empty(TEST_GEOIP_APIKEY)) {
$this->markTestSkipped('External geo tests are disabled.');
}
$this->resetAfterTest();
$CFG->geopluginapikey = TEST_GEOIP_APIKEY;
$this->setup_geoip2file();
// Note: The results we get from the iplookup tests are beyond our control.
+9 -1
View File
@@ -35,12 +35,20 @@ final class geoplugin_test extends \advanced_testcase {
}
/**
* In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php
* In order to execute this test:
* - PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php
* - GeoPlugin API key should be defined in config.php as TEST_GEOIP_APIKEY
*/
public function setUp(): void {
global $CFG;
if (!PHPUNIT_LONGTEST) {
$this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
}
if (!defined('TEST_GEOIP_APIKEY') || empty(TEST_GEOIP_APIKEY)) {
$this->markTestSkipped('External geo tests are disabled.');
}
$CFG->geopluginapikey = TEST_GEOIP_APIKEY;
}
/**
+2
View File
@@ -667,6 +667,8 @@ $string['fullnamedisplayprivate'] = 'Full name format - private';
$string['gdrequired'] = 'The GD extension is now required by Moodle for image conversion.';
$string['generalsettings'] = 'General settings';
$string['geoipfile'] = 'GeoLite2 City MaxMind DB';
$string['geopluginapikey'] = 'GeoPlugin API key';
$string['geopluginapikey_desc'] = 'The API key used to access the GeoPlugin service. Get your own key at <a href="https://www.geoplugin.com/" target="_blank">GeoPlugin page</a>.';
$string['getremoteaddrconf'] = 'Logged IP address source';
$string['globalsearch'] = 'Global search';
$string['globalsearchmanage'] = 'Manage global search';
+25 -7
View File
@@ -1053,19 +1053,37 @@ EOF;
/**
* Helper function to execute api in a given context.
*
* @param string $contextapi context in which api is defined.
* @param array $params list of params to pass.
* Note: The contextapi does not support a callback.
*
* @param string|array $contextapi context in which api is defined.
* @param array|mixed $params list of params to pass or a single parameter
* @throws Exception
* @throws DriverException
*/
protected function execute($contextapi, $params = array()) {
protected function execute(
$contextapi,
$params = []
): void {
if (!is_array($params)) {
$params = array($params);
$params = [$params];
}
if (is_string($contextapi)) {
$contextapi = explode('::', $contextapi);
}
if (count($contextapi) !== 2) {
throw new DriverException('Invalid contextapi format, expected "context::api" or ["context", "api"]');
}
// Get required context and execute the api.
$contextapi = explode("::", $contextapi);
$context = behat_context_helper::get($contextapi[0]);
call_user_func_array(array($context, $contextapi[1]), $params);
[$classname, $method] = $contextapi;
if (!is_string($classname) || !is_string($method)) {
throw new DriverException('Invalid contextapi format, expected "context::api" or ["context", "api"]');
}
$context = behat_context_helper::get($classname);
call_user_func_array([$context, $method], $params);
// NOTE: Wait for pending js and look for exception are not optional, as this might lead to unexpected results.
// Don't make them optional for performance reasons.
+2 -2
View File
@@ -293,11 +293,11 @@ class site_registration_form extends \moodleform {
if ($data = parent::get_data()) {
// Never return '*newemail' checkboxes, always return 'emailalertemail' and 'commnewsemail' even if not applicable.
if (empty($data->emailalert) || empty($data->emailalertnewemail)) {
$data->emailalertemail = null;
$data->emailalertemail = '';
}
unset($data->emailalertnewemail);
if (empty($data->commnews) || empty($data->commnewsnewemail)) {
$data->commnewsemail = null;
$data->commnewsemail = '';
}
unset($data->commnewsnewemail);
// Always return 'contactable'.
+14
View File
@@ -998,7 +998,21 @@ class navigation_node implements renderable {
);
}
}
}
/**
* Reset all static data.
*
* @throws coding_exception if called outside of a unit test
*/
public static function reset_all_data(): void {
if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) {
throw new coding_exception('Resetting all data is not allowed outside of PHPUnit tests.');
}
self::$fullmeurl = null;
self::$autofindactive = true;
self::$loadadmintree = false;
}
}
+1
View File
@@ -199,6 +199,7 @@ class phpunit_util extends testing_util {
// reinitialise following globals
$OUTPUT = new bootstrap_renderer();
$PAGE = new moodle_page();
\navigation_node::reset_all_data();
$FULLME = null;
$ME = null;
$SCRIPT = null;
+23
View File
@@ -145,6 +145,29 @@ STRING;
self::assertTag(['id' => 'testid'], "<div><div>");
}
/**
* Test that the navigation node URL is overridden correctly.
*/
public function test_set_navigation_url(): void {
\navigation_node::override_active_url(new \moodle_url('/foo/bar/baz'));
$property = new \ReflectionProperty(\navigation_node::class, 'fullmeurl');
$property->setAccessible(true);
$this->assertNotNull($property->getValue());
}
/**
* Test that the after-test teardown correctly resets the navigation node URL.
*
* @depends test_set_navigation_url
*/
public function test_navigation_url_reset(): void {
$property = new \ReflectionProperty(\navigation_node::class, 'fullmeurl');
$property->setAccessible(true);
$this->assertNull($property->getValue());
}
// Uncomment following tests to see logging of unexpected changes in global state and database.
/*
public function test_db_modification() {
+12 -12
View File
@@ -78,7 +78,7 @@ class behat_general extends behat_base {
* @Given /^I am on homepage$/
*/
public function i_am_on_homepage() {
$this->execute('behat_general::i_visit', ['/']);
$this->execute([self::class, 'i_visit'], ['/']);
}
/**
@@ -87,7 +87,7 @@ class behat_general extends behat_base {
* @Given /^I am on site homepage$/
*/
public function i_am_on_site_homepage() {
$this->execute('behat_general::i_visit', ['/?redirect=0']);
$this->execute([self::class, 'i_visit'], ['/?redirect=0']);
}
/**
@@ -96,7 +96,7 @@ class behat_general extends behat_base {
* @Given /^I am on course index$/
*/
public function i_am_on_course_index() {
$this->execute('behat_general::i_visit', ['/course/index.php']);
$this->execute([self::class, 'i_visit'], ['/course/index.php']);
}
/**
@@ -442,7 +442,7 @@ class behat_general extends behat_base {
*/
public function i_click_on_confirming_the_dialogue($element, $selectortype) {
$this->i_click_on($element, $selectortype);
$this->execute('behat_general::accept_currently_displayed_alert_dialog', []);
$this->execute([self::class, 'accept_currently_displayed_alert_dialog'], []);
$this->wait_until_the_page_is_ready();
}
@@ -456,7 +456,7 @@ class behat_general extends behat_base {
*/
public function i_click_on_dismissing_the_dialogue($element, $selectortype) {
$this->i_click_on($element, $selectortype);
$this->execute('behat_general::dismiss_currently_displayed_alert_dialog', []);
$this->execute([self::class, 'dismiss_currently_displayed_alert_dialog'], []);
$this->wait_until_the_page_is_ready();
}
@@ -1145,7 +1145,7 @@ EOF;
* @Given /^I trigger cron$/
*/
public function i_trigger_cron() {
$this->execute('behat_general::i_visit', ['/admin/cron.php']);
$this->execute([self::class, 'i_visit'], ['/admin/cron.php']);
}
/**
@@ -2138,8 +2138,8 @@ EOF;
}
// Gets the node based on the requested selector type and locator.
$node = $this->get_selected_node($selectortype, $element);
$this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
$this->execute('behat_general::i_press_named_key', ['', 'tab']);
$this->execute([self::class, 'i_click_on'], [$node, 'NodeElement']);
$this->execute([self::class, 'i_press_named_key'], ['', 'tab']);
}
/**
@@ -2252,9 +2252,9 @@ EOF;
*/
public function i_manually_press_tab($shift = '') {
if (empty($shift)) {
$this->execute('behat_general::i_press_named_key', ['', 'tab']);
$this->execute([self::class, 'i_press_named_key'], ['', 'tab']);
} else {
$this->execute('behat_general::i_press_named_key', ['shift', 'tab']);
$this->execute([self::class, 'i_press_named_key'], ['shift', 'tab']);
}
}
@@ -2317,7 +2317,7 @@ EOF;
* @throws DriverException
*/
public function i_manually_press_enter() {
$this->execute('behat_general::i_press_named_key', ['', 'enter']);
$this->execute([self::class, 'i_press_named_key'], ['', 'enter']);
}
/**
@@ -2352,7 +2352,7 @@ EOF;
*/
public function i_click_on_the_dynamic_tab(string $tabname): void {
$xpath = "//*[@id='dynamictabs-tabs'][descendant::a[contains(text(), '" . $this->escape($tabname) . "')]]";
$this->execute('behat_general::i_click_on_in_the',
$this->execute([self::class, 'i_click_on_in_the'],
[$tabname, 'link', $xpath, 'xpath_element']);
}
+3
View File
@@ -48,6 +48,9 @@ final class navigationlib_test extends \advanced_testcase {
protected function setup_node() {
global $PAGE, $SITE;
// Perform a reset between tests to reset the PAGE.
$this->resetAfterTest();
$PAGE->set_url('/');
$PAGE->set_course($SITE);
+4
View File
@@ -1,6 +1,10 @@
This files describes API changes in core libraries and APIs,
information provided here is intended especially for developers.
=== 4.1.21 ===
* Moodle's Behat `::execute()` method now supports the use of array callable syntax for calling other steps.
=== 4.1.20 ===
* Add a new method has_valid_group in \core\report_helper that will return true or false depending if the user has a valid group. This is mainly
@@ -89,6 +89,16 @@ class user_submission_actionmenu implements templatable, renderable {
}
}
/**
* Has the submission started.
*
* @return bool The status of the submission; true if started, otherwise false.
*/
protected function is_submission_started(): bool {
return (is_object($this->teamsubmission) && isset($this->teamsubmission->timestarted))
|| (is_object($this->submission) && isset($this->submission->timestarted));
}
/**
* Export the submission buttons for the page.
*
@@ -125,19 +135,20 @@ class user_submission_actionmenu implements templatable, renderable {
$data['edit']['help'] = $newattempthelp->export_for_template($output);
}
if ($status === ASSIGN_SUBMISSION_STATUS_NEW) {
if ($this->timelimit && empty($this->submission->timestarted)) {
$timelimitenabled = get_config('assign', 'enabletimelimit');
if ($timelimitenabled && $this->timelimit && !$this->is_submission_started()) {
$confirmation = new \confirm_action(
get_string('confirmstart', 'assign', format_time($this->timelimit)),
null,
get_string('beginassignment', 'assign')
);
$urlparams = array('id' => $this->cmid, 'action' => 'editsubmission');
// The 'begin' flag indicates that the user is starting a timed assignment.
$urlparams = ['id' => $this->cmid, 'action' => 'editsubmission', 'begin' => 1];
$beginbutton = new \action_link(
new moodle_url('/mod/assign/view.php', $urlparams),
get_string('beginassignment', 'assign'),
$confirmation,
['class' => 'btn btn-primary']
get_string('beginassignment', 'assign'),
$confirmation,
['class' => 'btn btn-primary']
);
$data['edit']['button'] = $beginbutton->export_for_template($output);
$data['edit']['begin'] = true;
@@ -27,22 +27,33 @@ class PdfType
* @param PdfType $value
* @param PdfParser $parser
* @param bool $stopAtIndirectObject
* @param array $ensuredObjectsList A list of all ensured indirect objects to prevent recursion
* @return PdfType
* @throws CrossReferenceException
* @throws PdfParserException
*/
public static function resolve(PdfType $value, PdfParser $parser, $stopAtIndirectObject = false)
{
public static function resolve(
PdfType $value,
PdfParser $parser,
$stopAtIndirectObject = false,
array &$ensuredObjectsList = []
) {
if ($value instanceof PdfIndirectObjectReference) {
$value = $parser->getIndirectObject($value->value);
}
if ($value instanceof PdfIndirectObject) {
if ($stopAtIndirectObject === true) {
return $value;
}
return self::resolve($value->value, $parser, $stopAtIndirectObject);
}
if ($value instanceof PdfIndirectObjectReference) {
return self::resolve($parser->getIndirectObject($value->value), $parser, $stopAtIndirectObject);
if (\in_array($value->objectNumber, $ensuredObjectsList, true)) {
throw new PdfParserException(
\sprintf('Indirect reference recursion detected (%s).', $value->objectNumber)
);
}
$ensuredObjectsList[] = $value->objectNumber;
return self::resolve($value->value, $parser, $stopAtIndirectObject, $ensuredObjectsList);
}
return $value;
@@ -139,12 +139,19 @@ class PdfReader
$page = $this->pages[$pageNumber - 1];
if ($page instanceof PdfIndirectObjectReference) {
$readPages = function ($kids) use (&$readPages) {
$alreadyReadKids = [];
$readPages = function ($kids) use (&$readPages, &$alreadyReadKids) {
$kids = PdfArray::ensure($kids);
/** @noinspection LoopWhichDoesNotLoopInspection */
foreach ($kids->value as $reference) {
$reference = PdfIndirectObjectReference::ensure($reference);
if (\in_array($reference->value, $alreadyReadKids, true)) {
throw new PdfReaderException('Recursive pages dictionary detected.');
}
$alreadyReadKids[] = $reference->value;
$object = $this->parser->getIndirectObject($reference->value);
$type = PdfDictionary::get($object->value, 'Type');
@@ -168,6 +175,7 @@ class PdfReader
if ($type->value === 'Pages') {
$kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser);
try {
$alreadyReadKids[] = $page->objectNumber;
$page = $this->pages[$pageNumber - 1] = $readPages($kids);
} catch (PdfReaderException $e) {
if ($e->getCode() !== PdfReaderException::KIDS_EMPTY) {
@@ -202,7 +210,8 @@ class PdfReader
return;
}
$readPages = function ($kids, $count) use (&$readPages, $readAll) {
$alreadyReadKids = [];
$readPages = function ($kids, $count) use (&$readPages, &$alreadyReadKids, $readAll) {
$kids = PdfArray::ensure($kids);
$isLeaf = ($count->value === \count($kids->value));
@@ -214,6 +223,11 @@ class PdfReader
continue;
}
if (\in_array($reference->value, $alreadyReadKids, true)) {
throw new PdfReaderException('Recursive pages dictionary detected.');
}
$alreadyReadKids[] = $reference->value;
$object = $this->parser->getIndirectObject($reference->value);
$type = PdfDictionary::get($object->value, 'Type');
+31 -4
View File
@@ -3145,7 +3145,9 @@ class assign {
if ($create) {
$action = optional_param('action', '', PARAM_TEXT);
if ($action == 'editsubmission') {
if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
$starttimer = optional_param('begin', 0, PARAM_INT);
// Only start the timer if the user has clicked the 'Begin assignment' button.
if (empty($submission->timestarted) && $this->get_instance()->timelimit && $starttimer) {
$submission->timestarted = time();
$DB->update_record('assign_submission', $submission);
}
@@ -3788,7 +3790,9 @@ class assign {
if ($create) {
$action = optional_param('action', '', PARAM_TEXT);
if ($action == 'editsubmission') {
if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
$starttimer = optional_param('begin', 0, PARAM_INT);
// Only start the timer if the user has clicked the 'Begin assignment' button.
if (empty($submission->timestarted) && $this->get_instance()->timelimit && $starttimer) {
$submission->timestarted = time();
$DB->update_record('assign_submission', $submission);
}
@@ -4796,7 +4800,7 @@ class assign {
* @return string The page output.
*/
protected function view_edit_submission_page($mform, $notices) {
global $CFG, $USER, $DB, $PAGE;
global $CFG, $USER, $DB, $PAGE, $OUTPUT;
$o = '';
require_once($CFG->dirroot . '/mod/assign/submission_form.php');
@@ -4881,7 +4885,30 @@ class assign {
$o .= $this->get_renderer()->notification($notice);
}
$o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
if (
$submission->status == ASSIGN_SUBMISSION_STATUS_NEW && $this->get_instance()->timelimit &&
empty($submission->timestarted)
) {
// Timed assignment should always get a confirmation that the user wants to start it.
$confirmation = new \confirm_action(
get_string('confirmstart', 'assign', format_time($this->get_instance()->timelimit)),
null,
get_string('beginassignment', 'assign')
);
// The 'begin' flag indicates that the user is starting a timed assignment.
$urlparams = ['id' => $this->get_course_module()->id, 'action' => 'editsubmission', 'begin' => 1];
$beginbutton = new \action_link(
new moodle_url('/mod/assign/view.php', $urlparams),
get_string('beginassignment', 'assign'),
$confirmation,
['class' => 'btn btn-primary']
);
$o .= $OUTPUT->render($beginbutton);
} else {
$o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
}
$o .= $this->view_footer();
\mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
@@ -0,0 +1,84 @@
@mod @mod_assign
Feature: In a timed assignment, students should confirm before starting the timer
In order to submit a timed assignment
As a student
I need to confirm to begin the assignment before the timer starts
Background:
Given the following config values are set as admin:
| config | value | plugin |
| enabletimelimit | 1 | assign |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
And the following "activities" exist:
| activity | course | name | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes | teamsubmission | duedate | timelimit |
| assign | C1 | Timed assignment 1 | 1 | 1 | 2097152 | 0 | ##+1 hours 30 minutes## | 60 |
| assign | C1 | Group assignment 2 | 1 | 1 | 2097152 | 1 | ##+1 hours 30 minutes## | 60 |
Scenario: Access a timed assignment from the course page
Given I am on the "Timed assignment 1" Activity page logged in as student1
And "Begin assignment" "link" should exist
When I reload the page
Then "Begin assignment" "link" should exist
And "#mod_assign_timelimit_block" "css_element" should not exist
@javascript
Scenario: Access a timed assignment from the Dashboard
Given I am logged in as student1
When I click on "Timed assignment 1" "link" in the "Calendar" "block"
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
Then "Begin assignment" "link" should exist
And I reload the page
And "Begin assignment" "link" should exist
And "#mod_assign_timelimit_block" "css_element" should not exist
# Repeat the steps to confirm timer doesn't start automatically.
And I follow "Dashboard"
And I click on "Timed assignment 1" "link" in the "Calendar" "block"
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
And "Begin assignment" "link" should exist
And "#mod_assign_timelimit_block" "css_element" should not exist
# Now start the timer.
And I click on "Begin assignment" "link"
And I click on "Begin assignment" "button" in the ".confirmation-buttons" "css_element"
And "#mod_assign_timelimit_block" "css_element" should exist
@javascript
Scenario: Access a timed group assignment from the Dashboard
Given the following "groups" exist:
| name | course | idnumber |
| Group 1 | C1 | CG1 |
And the following "group members" exist:
| user | group |
| student1 | CG1 |
| student2 | CG1 |
And I am logged in as student1
When I click on "Group assignment 2" "link" in the "Calendar" "block"
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
Then "Begin assignment" "link" should exist
And I reload the page
And "Begin assignment" "link" should exist
And "#mod_assign_timelimit_block" "css_element" should not exist
# Repeat the steps to confirm timer doesn't start automatically.
And I follow "Dashboard"
And I click on "Group assignment 2" "link" in the "Calendar" "block"
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
And "Begin assignment" "link" should exist
And "#mod_assign_timelimit_block" "css_element" should not exist
# Now start the timer.
And I click on "Begin assignment" "link"
And I click on "Begin assignment" "button" in the ".confirmation-buttons" "css_element"
And "#mod_assign_timelimit_block" "css_element" should exist
And I log out
# Now check the submission has started for the other group member too.
And I am on the "Group assignment 2" "assign activity" page logged in as student2
And "Begin assignment" "link" should not exist
And "Add submission" "button" should exist
+2
View File
@@ -2430,6 +2430,8 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
// Set optional param to indicate start time required.
$_GET['action'] = 'editsubmission';
// Set optional param to indicate that assignment submission timer should start.
$_GET['begin'] = 1;
$cm = get_coursemodule_from_instance('assign', $instance->id);
$context = \context_module::instance($cm->id);
(new \assign($context, $cm, $cm->course))->get_user_submission(0, true);
@@ -0,0 +1,57 @@
@mod_quiz @quizaccess @quizaccess_seb
Feature: SEB settings in quiz access rule
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
Scenario Outline: Require the use of Safe Exam Browser set to Yes Configure manually shows message to students
Given the following "activities" exist:
| activity | course | name | seb_requiresafeexambrowser | idnumber |
| quiz | C1 | Quiz 1 | <sebsetting> | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Activity module | quiz1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Reading | Can you read this? |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Reading | 1 |
When I am on the "Quiz 1" "quiz activity" page logged in as student1
Then I <messagevisibility> see "This quiz has been configured so that students may only attempt it using the Safe Exam Browser."
And "Attempt quiz" "button" <attemptbuttonvisibility> exist
Examples:
| sebsetting | messagevisibility | attemptbuttonvisibility |
| 0 | should not | should |
| 1 | should | should not |
Scenario Outline: Show Safe Exam Browser download button setting controls visibility of download link
Given the following "activities" exist:
| activity | course | name | idnumber | seb_requiresafeexambrowser | seb_showsebdownloadlink |
| quiz | C1 | Quiz 1 | quiz1 | 1 | <seb_showsebdownloadlink> |
And the following "question categories" exist:
| contextlevel | reference | name |
| Activity module | quiz1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Reading | Can you read this? |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Reading | 1 |
When I am on the "Quiz 1" "quiz activity" page logged in as student1
Then "Download Safe Exam Browser" "button" <downloadseblinkvisibility> exist
And I should see "This quiz has been configured so that students may only attempt it using the Safe Exam Browser."
Examples:
| seb_showsebdownloadlink | downloadseblinkvisibility |
| 0 | should not |
| 1 | should |
+1 -1
View File
@@ -15,4 +15,4 @@ Policy: https://moodledev.io/general/development/process/security
# Expiry date of this document
# This information is considered current and up to date until 3 weeks after the next major Moodle LMS release
Expires: 2025-10-27T01:00:00.000Z
Expires: 2026-05-11T01:00:00.000Z
+2 -2
View File
@@ -29,9 +29,9 @@
defined('MOODLE_INTERNAL') || die();
$version = 2022112820.00; // 20221128 = branching date YYYYMMDD - do not modify!
$version = 2022112821.00; // 20221128 = branching date YYYYMMDD - do not modify!
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.1.20 (Build: 20250811)'; // Human-friendly version name
$release = '4.1.21 (Build: 20251006)'; // Human-friendly version name
$branch = '401'; // This version's branch.
$maturity = MATURITY_STABLE; // This version's maturity level.