Merge branch 'MDL-44538-master' of https://github.com/lucaboesch/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 31 Jul 2018 09:18:39 +0000 (11:18 +0200)
committerDavid Monllao <davidm@moodle.com>
Tue, 31 Jul 2018 09:18:39 +0000 (11:18 +0200)
204 files changed:
admin/tool/behat/tests/manager_test.php [deleted file]
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/output/data_requests_table.php
admin/tool/dataprivacy/classes/output/my_data_requests_page.php
admin/tool/dataprivacy/classes/task/process_data_request_task.php
admin/tool/dataprivacy/db/access.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/dataexport.feature [new file with mode: 0644]
admin/tool/dataprivacy/version.php
admin/tool/log/store/legacy/classes/log/store.php
admin/tool/log/upgrade.txt [new file with mode: 0644]
admin/tool/policy/classes/output/page_viewalldoc.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/templates/page_viewalldoc.mustache
admin/tool/policy/tests/behat/consent.feature
auth/shibboleth/auth.php
auth/shibboleth/classes/helper.php [new file with mode: 0644]
auth/shibboleth/index_form.html
auth/shibboleth/lang/en/auth_shibboleth.php
auth/shibboleth/lib.php [new file with mode: 0644]
auth/shibboleth/login.php
auth/shibboleth/logout.php
auth/shibboleth/settings.php
auth/shibboleth/upgrade.txt
auth/tests/behat/displayloginfailures.feature
auth/tests/behat/validateagedigitalconsentmap.feature
backup/moodle2/tests/behat/import_multiple_times.feature
backup/util/dbops/restore_dbops.class.php
blocks/myprofile/lang/en/block_myprofile.php
blocks/myprofile/lang/en/deprecated.txt
blocks/recent_activity/block_recent_activity.php
blocks/recent_activity/classes/task/cleanup.php [new file with mode: 0644]
blocks/recent_activity/db/tasks.php [moved from lib/classes/task/events_cron_task.php with 51% similarity]
blocks/recent_activity/lang/en/block_recent_activity.php
blocks/recent_activity/version.php
blocks/rss_client/block_rss_client.php
blocks/rss_client/classes/task/refreshfeeds.php [new file with mode: 0644]
blocks/rss_client/db/tasks.php [moved from lib/password_compat/lib/password.php with 59% similarity]
blocks/rss_client/lang/en/block_rss_client.php
blocks/rss_client/tests/cron_test.php
blocks/rss_client/version.php
calendar/amd/build/event_form.min.js
calendar/amd/build/repository.min.js
calendar/amd/src/event_form.js
calendar/amd/src/repository.js
calendar/classes/local/event/container.php
calendar/classes/local/event/forms/create.php
calendar/classes/local/event/forms/eventtype.php
calendar/classes/local/event/forms/managesubscriptions.php
calendar/classes/local/event/mappers/create_update_form_mapper.php
calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php
calendar/externallib.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/tests/behat/calendar.feature
calendar/tests/externallib_test.php
calendar/tests/lib_test.php
calendar/tests/raw_event_retrieval_strategy_test.php
calendar/upgrade.txt
comment/lib.php
course/ajax/management.php
course/classes/management_renderer.php
course/externallib.php
course/format/singleactivity/lib.php
course/lib.php
enrol/manual/amd/build/form-potential-user-selector.min.js
enrol/manual/amd/src/form-potential-user-selector.js
enrol/manual/tests/behat/quickenrolment.feature [new file with mode: 0644]
enrol/paypal/ipn.php
enrol/self/lang/en/enrol_self.php
enrol/self/lib.php
enrol/self/settings.php
error/index.php
grade/edit/letter/index.php
group/assign.php
install/lang/hu/install.php
install/lang/ja/install.php
lang/en/admin.php
lang/en/calendar.php
lang/en/competency.php
lang/en/deprecated.txt
lang/en/form.php
lang/en/media.php
lang/en/message.php
lang/en/moodle.php
lang/en/webservice.php
lib/adminlib.php
lib/amd/build/ajax.min.js
lib/amd/build/form-autocomplete.min.js
lib/amd/src/ajax.js
lib/amd/src/form-autocomplete.js
lib/behat/classes/behat_config_manager.php
lib/classes/event/base.php
lib/classes/session/manager.php
lib/coursecatlib.php
lib/db/caches.php
lib/db/services.php
lib/db/tasks.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/editor/atto/tests/behat/disablecontrol.feature [new file with mode: 0644]
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/editor.js
lib/editor/tests/fixtures/disable_control_example.php [new file with mode: 0644]
lib/editor/tests/fixtures/editor_form.php [new file with mode: 0644]
lib/editor/textarea/tests/behat/disablecontrol.feature [new file with mode: 0644]
lib/editor/tinymce/module.js
lib/editor/tinymce/tests/behat/disablecontrol.feature [new file with mode: 0644]
lib/eventslib.php [deleted file]
lib/externallib.php
lib/filelib.php
lib/form/filemanager.php
lib/form/form.js
lib/gradelib.php
lib/grouplib.php
lib/medialib.php [deleted file]
lib/moodlelib.php
lib/outputrenderers.php
lib/phpunit/classes/util.php
lib/questionlib.php
lib/setup.php
lib/tests/behat/behat_deprecated.php
lib/tests/behat/behat_general.php
lib/tests/coursecatlib_test.php
lib/tests/event_test.php
lib/tests/eventslib_test.php [deleted file]
lib/tests/filelib_test.php
lib/tests/gradelib_test.php
lib/tests/grouplib_test.php
lib/tests/medialib_test.php
lib/tests/questionlib_test.php
lib/tests/string_manager_standard_test.php
lib/upgrade.txt
lib/upgradelib.php
media/classes/player.php
media/upgrade.txt
message/lib.php
message/output/popup/amd/build/notification_popover_controller.min.js
message/output/popup/amd/src/notification_popover_controller.js
message/output/popup/mark_notification_read.php
mod/assign/lang/en/assign.php
mod/assign/lang/en/deprecated.txt
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/submission/file/locallib.php
mod/assign/submission/file/tests/locallib_test.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/assign/upgrade.txt
mod/data/lang/en/data.php
mod/data/lang/en/deprecated.txt
mod/feedback/lang/en/deprecated.txt
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/feedback/show_nonrespondents.php
mod/feedback/upgrade.txt
mod/forum/lib.php
mod/forum/post.php
mod/lesson/essay.php
mod/lesson/locallib.php
mod/lesson/renderer.php
mod/lti/lib.php
mod/quiz/db/install.xml
mod/quiz/db/upgrade.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/report/statistics/report.php
mod/scorm/datamodels/scorm_13.js
mod/upgrade.txt
mod/wiki/lib.php
pix/i/empty.png [moved from pix/i/emtpy.png with 100% similarity]
pix/i/empty.svg [moved from pix/i/emtpy.svg with 100% similarity]
question/classes/bank/action_column_base.php
question/classes/bank/view.php
question/format/gift/format.php
question/format/gift/tests/giftformat_test.php
question/templates/tag_condition.mustache
question/tests/bank_view_test.php [new file with mode: 0644]
question/tests/privacy_provider_test.php
search/classes/base.php
search/classes/document.php
theme/boost/classes/output/core_course/management/renderer.php
theme/boost/scss/moodle/modules.scss
theme/boost/style/moodle.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
user/amd/build/name_page_filter.min.js [deleted file]
user/amd/src/name_page_filter.js [deleted file]
user/classes/search/user.php
user/editlib.php
user/index.php
user/lib.php
user/profile/lib.php
user/tests/behat/custom_profile_fields.feature [new file with mode: 0644]
user/tests/behat/filter_participants.feature
user/tests/behat/filter_participants_showall.feature [new file with mode: 0644]
user/tests/behat/view_participants.feature
user/tests/search_test.php
user/upgrade.txt [new file with mode: 0644]
version.php

diff --git a/admin/tool/behat/tests/manager_test.php b/admin/tool/behat/tests/manager_test.php
deleted file mode 100644 (file)
index 7b88c80..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-<?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/>.
-
-/**
- * Unit tests for behat manager.
- *
- * @package   tool_behat
- * @copyright  2012 David Monllaó
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->dirroot . '/' . $CFG->admin .'/tool/behat/locallib.php');
-require_once($CFG->libdir . '/behat/classes/util.php');
-require_once($CFG->libdir . '/behat/classes/behat_config_manager.php');
-
-/**
- * Behat manager tests.
- *
- * @package    tool_behat
- * @copyright  2012 David Monllaó
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class tool_behat_manager_testcase extends advanced_testcase {
-
-    /**
-     * behat_config_manager tests.
-     */
-    public function test_merge_configs() {
-
-        // Simple default config.
-        $array1 = array(
-            'the' => 'same',
-            'simple' => 'value',
-            'array' => array(
-                'one' => 'arrayvalue1',
-                'two' => 'arrayvalue2'
-            )
-        );
-
-        // Simple override.
-        $array2 = array(
-            'simple' => 'OVERRIDDEN1',
-            'array' => array(
-                'one' => 'OVERRIDDEN2'
-            ),
-            'newprofile' => array(
-                'anotherlevel' => array(
-                    'andanotherone' => array(
-                        'list1',
-                        'list2'
-                    )
-                )
-            )
-        );
-
-        $array = testable_behat_config_manager::merge_config($array1, $array2);
-        $this->assertDebuggingCalled("Use of merge_config is deprecated, please see behat_config_util");
-
-        // Overrides are applied.
-        $this->assertEquals('OVERRIDDEN1', $array['simple']);
-        $this->assertEquals('OVERRIDDEN2', $array['array']['one']);
-
-        // Other values are respected.
-        $this->assertNotEmpty($array['array']['two']);
-
-        // Completely new nodes are added.
-        $this->assertNotEmpty($array['newprofile']);
-        $this->assertNotEmpty($array['newprofile']['anotherlevel']['andanotherone']);
-        $this->assertEquals('list1', $array['newprofile']['anotherlevel']['andanotherone'][0]);
-        $this->assertEquals('list2', $array['newprofile']['anotherlevel']['andanotherone'][1]);
-
-        // Complex override changing vectors to scalars and scalars to vectors.
-        $array2 = array(
-            'simple' => array(
-                'simple' => 'should',
-                'be' => 'overridden',
-                'by' => 'this-array'
-            ),
-            'array' => 'one'
-        );
-
-        $array = testable_behat_config_manager::merge_config($array1, $array2);
-        $this->assertDebuggingCalled("Use of merge_config is deprecated, please see behat_config_util");
-
-        // Overrides applied.
-        $this->assertNotEmpty($array['simple']);
-        $this->assertNotEmpty($array['array']);
-        $this->assertTrue(is_array($array['simple']));
-        $this->assertFalse(is_array($array['array']));
-
-        // Other values are maintained.
-        $this->assertEquals('same', $array['the']);
-    }
-
-    /**
-     * behat_config_manager tests.
-     */
-    public function test_config_file_contents() {
-        global $CFG;
-
-        $this->resetAfterTest(true);
-
-        // Skip tests if behat is not installed.
-        $vendorpath = $CFG->dirroot . '/vendor';
-        if (!file_exists($vendorpath . '/autoload.php') || !is_dir($vendorpath . '/behat')) {
-            $this->markTestSkipped('Behat not installed.');
-        }
-
-        // Add some fake test url.
-        $CFG->behat_wwwroot = 'http://example.com/behat';
-
-        // To avoid user value at config.php level.
-        unset($CFG->behat_config);
-
-        // List.
-        $features = array(
-            'feature1',
-            'feature2',
-            'feature3'
-        );
-
-        // Associative array.
-        $stepsdefinitions = array(
-            'micarro' => '/me/lo/robaron',
-            'anoche' => '/cuando/yo/dormia'
-        );
-
-        $contents = testable_behat_config_manager::get_config_file_contents($features, $stepsdefinitions);
-        $this->assertDebuggingCalled("Use of get_config_file_contents is deprecated, please see behat_config_util");
-
-        // YAML decides when is is necessary to wrap strings between single quotes, so not controlled
-        // values like paths should not be asserted including the key name as they would depend on the
-        // directories values.
-        $this->assertContains($CFG->dirroot,
-            $contents['default']['extensions']['Moodle\BehatExtension']['moodledirroot']);
-
-        // Not quoted strings.
-        $this->assertEquals('/me/lo/robaron',
-            $contents['default']['extensions']['Moodle\BehatExtension']['steps_definitions']['micarro']);
-
-        // YAML uses single quotes to wrap URL strings.
-        $this->assertEquals($CFG->behat_wwwroot, $contents['default']['extensions']['Behat\MinkExtension']['base_url']);
-
-        // Lists.
-        $this->assertEquals('feature1', $contents['default']['suites']['default']['paths'][0]);
-        $this->assertEquals('feature3', $contents['default']['suites']['default']['paths'][2]);
-
-        unset($CFG->behat_wwwroot);
-    }
-
-}
-
-/**
- * Allows access to internal methods without exposing them.
- *
- * @package    tool_behat
- * @copyright  2012 David Monllaó
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class testable_behat_config_manager extends behat_config_manager {
-
-    /**
-     * Allow access to protected method
-     * @see parent::merge_config()
-     * @param mixed $config
-     * @param mixed $localconfig
-     * @return mixed
-     */
-    public static function merge_config($config, $localconfig) {
-        return parent::merge_config($config, $localconfig);
-    }
-
-    /**
-     * Allow access to protected method
-     * @see parent::get_config_file_contents()
-     * @param array $features
-     * @param array $stepsdefinitions
-     * @return string
-     */
-    public static function get_config_file_contents($features, $stepsdefinitions) {
-        return parent::get_config_file_contents($features, $stepsdefinitions);
-    }
-}
index 9aaea28..8ec0bb5 100644 (file)
@@ -608,6 +608,54 @@ class api {
         return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
     }
 
+    /**
+     * Checks whether a user can download a data request.
+     *
+     * @param int $userid Target user id (subject of data request)
+     * @param int $requesterid Requester user id (person who requsted it)
+     * @param int|null $downloaderid Person who wants to download user id (default current)
+     * @return bool
+     * @throws coding_exception
+     */
+    public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
+        global $USER;
+
+        if (!$downloaderid) {
+            $downloaderid = $USER->id;
+        }
+
+        $usercontext = \context_user::instance($userid);
+        // If it's your own and you have the right capability, you can download it.
+        if ($userid == $downloaderid && has_capability('tool/dataprivacy:downloadownrequest', $usercontext, $downloaderid)) {
+            return true;
+        }
+        // If you can download anyone's in that context, you can download it.
+        if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
+            return true;
+        }
+        // If you can have the 'child access' ability to request in that context, and you are the one
+        // who requested it, then you can download it.
+        if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Gets an action menu link to download a data request.
+     *
+     * @param \context_user $usercontext User context (of user who the data is for)
+     * @param int $requestid Request id
+     * @return \action_menu_link_secondary Action menu link
+     * @throws coding_exception
+     */
+    public static function get_download_link(\context_user $usercontext, $requestid) {
+        $downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
+                'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
+        $downloadtext = get_string('download', 'tool_dataprivacy');
+        return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
+    }
+
     /**
      * Creates a new data purpose.
      *
index d8b0644..51bb133 100644 (file)
@@ -208,6 +208,14 @@ class data_requests_table extends table_sql {
                 break;
         }
 
+        if ($status == api::DATAREQUEST_STATUS_COMPLETE) {
+            $userid = $data->foruser->id;
+            $usercontext = \context_user::instance($userid, IGNORE_MISSING);
+            if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) {
+                $actions[] = api::get_download_link($usercontext, $requestid);
+            }
+        }
+
         $actionsmenu = new action_menu($actions);
         $actionsmenu->set_menu_trigger(get_string('actions'));
         $actionsmenu->set_owner_selector('request-actions-' . $requestid);
index c5e18a1..d82968c 100644 (file)
@@ -95,7 +95,8 @@ class my_data_requests_page implements renderable, templatable {
             $requestexporter = new data_request_exporter($request, ['context' => $outputcontext]);
             $item = $requestexporter->export($output);
 
-            if ($request->get('userid') != $USER->id) {
+            $self = $request->get('userid') == $USER->id;
+            if (!$self) {
                 // Append user name if it differs from $USER.
                 $a = (object)['typename' => $item->typename, 'user' => $item->foruser->fullname];
                 $item->typename = get_string('requesttypeuser', 'tool_dataprivacy', $a);
@@ -110,6 +111,10 @@ class my_data_requests_page implements renderable, templatable {
                     $cancancel = false;
                     // Show download links only for export-type data requests.
                     $candownload = $type == api::DATAREQUEST_TYPE_EXPORT;
+                    if ($usercontext) {
+                        $candownload = api::can_download_data_request_for_user(
+                                $request->get('userid'), $request->get('requestedby'));
+                    }
                     break;
                 case api::DATAREQUEST_STATUS_CANCELLED:
                 case api::DATAREQUEST_STATUS_REJECTED:
@@ -126,10 +131,7 @@ class my_data_requests_page implements renderable, templatable {
                 $actions[] = new action_menu_link_secondary($cancelurl, null, $canceltext, $canceldata);
             }
             if ($candownload && $usercontext) {
-                $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $requestid, '/',
-                        'export.zip', true);
-                $downloadtext = get_string('download', 'tool_dataprivacy');
-                $actions[] = new action_menu_link_secondary($downloadurl, null, $downloadtext);
+                $actions[] = api::get_download_link($usercontext, $requestid);
             }
             if (!empty($actions)) {
                 $actionsmenu = new action_menu($actions);
index c58f574..6db9252 100644 (file)
@@ -139,8 +139,17 @@ class process_data_request_task extends adhoc_task {
 
         $output = $PAGE->get_renderer('tool_dataprivacy');
         $emailonly = false;
+        $notifyuser = true;
         switch ($request->type) {
             case api::DATAREQUEST_TYPE_EXPORT:
+                // Check if the user is allowed to download their own export. (This is for
+                // institutions which centrally co-ordinate subject access request across many
+                // systems, not just one Moodle instance, so we don't want every instance emailing
+                // the user.)
+                if (!api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->userid)) {
+                    $notifyuser = false;
+                }
+
                 $typetext = get_string('requesttypeexport', 'tool_dataprivacy');
                 // We want to notify the user in Moodle about the processing results.
                 $message->notification = 1;
@@ -179,18 +188,40 @@ class process_data_request_task extends adhoc_task {
         $message->fullmessagehtml = $messagehtml;
 
         // Send message to the user involved.
-        if ($emailonly) {
-            email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
-        } else {
-            message_send($message);
+        if ($notifyuser) {
+            if ($emailonly) {
+                email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
+            } else {
+                message_send($message);
+            }
+            mtrace('Message sent to user: ' . $messagetextdata['username']);
         }
-        mtrace('Message sent to user: ' . $messagetextdata['username']);
 
-        // Send to requester as well if this request was made on behalf of another user who's not a DPO,
-        // and has the capability to make data requests for the user (e.g. Parent).
-        if (!api::is_site_dpo($request->requestedby) && $foruser->id != $request->requestedby) {
+        // Send to requester as well in some circumstances.
+        if ($foruser->id != $request->requestedby) {
+            $sendtorequester = false;
+            switch ($request->type) {
+                case api::DATAREQUEST_TYPE_EXPORT:
+                    // Send to the requester as well if they can download it, unless they are the
+                    // DPO. If we didn't notify the user themselves (because they can't download)
+                    // then send to requester even if it is the DPO, as in that case the requester
+                    // needs to take some action.
+                    if (api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->requestedby)) {
+                        $sendtorequester = !$notifyuser || !api::is_site_dpo($request->requestedby);
+                    }
+                    break;
+                case api::DATAREQUEST_TYPE_DELETE:
+                    // Send to the requester if they are not the DPO and if they are allowed to
+                    // create data requests for the user (e.g. Parent).
+                    $sendtorequester = !api::is_site_dpo($request->requestedby) &&
+                            api::can_create_data_request_for_user($request->userid, $request->requestedby);
+                    break;
+                default:
+                    throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
+            }
+
             // Ensure the requester has the capability to make data requests for this user.
-            if (api::can_create_data_request_for_user($request->userid, $request->requestedby)) {
+            if ($sendtorequester) {
                 $requestedby = core_user::get_user($request->requestedby);
                 $message->userto = $requestedby;
                 $messagetextdata['username'] = fullname($requestedby);
index ecc2ec3..ad31867 100644 (file)
@@ -49,4 +49,22 @@ $capabilities = [
         'contextlevel' => CONTEXT_USER,
         'archetypes' => []
     ],
+
+    // Capability for users to download the results of their own data request.
+    'tool/dataprivacy:downloadownrequest' => [
+        'riskbitmask' => 0,
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_USER,
+        'archetypes' => [
+            'user' => CAP_ALLOW
+        ]
+    ],
+
+    // Capability for administrators to download other people's data requests.
+    'tool/dataprivacy:downloadallrequests' => [
+        'riskbitmask' => RISK_PERSONAL,
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_USER,
+        'archetypes' => []
+    ],
 ];
index b01a0b6..4af32a8 100644 (file)
@@ -66,6 +66,8 @@ $string['datadeletionpagehelp'] = 'Data for which the retention period has expir
 $string['dataprivacy:makedatarequestsforchildren'] = 'Make data requests for minors';
 $string['dataprivacy:managedatarequests'] = 'Manage data requests';
 $string['dataprivacy:managedataregistry'] = 'Manage data registry';
+$string['dataprivacy:downloadownrequest'] = 'Download your own exported data';
+$string['dataprivacy:downloadallrequests'] = 'Download exported data for everyone';
 $string['dataregistry'] = 'Data registry';
 $string['dataregistryinfo'] = 'The data registry enables categories (types of data) and purposes (the reasons for processing data) to be set for all content on the site - from users and courses down to activities and blocks. For each purpose, a retention period may be set. When a retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
 $string['datarequestcreatedforuser'] = 'Data request created for {$a}';
index 3c66485..73ffc14 100644 (file)
@@ -185,26 +185,18 @@ function tool_dataprivacy_output_fragment_contextlevel_form($args) {
  * @return bool Returns false if we don't find a file.
  */
 function tool_dataprivacy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) {
-    global $USER;
-
     if ($context->contextlevel == CONTEXT_USER) {
         // Make sure the user is logged in.
         require_login(null, false);
 
-        // Validate the user downloading this archive.
-        $usercontext = context_user::instance($USER->id);
-        // The user downloading this is not the user the archive has been prepared for. Check if it's the requester (e.g. parent).
-        if ($usercontext->instanceid !== $context->instanceid) {
-            // Get the data request ID. This should be the first element of the $args array.
-            $itemid = $args[0];
-            // Fetch the data request object. An invalid ID will throw an exception.
-            $datarequest = new \tool_dataprivacy\data_request($itemid);
-
-            // Check if the user is the requester and has the capability to make data requests for the target user.
-            $candownloadforuser = has_capability('tool/dataprivacy:makedatarequestsforchildren', $context);
-            if ($USER->id != $datarequest->get('requestedby') || !$candownloadforuser) {
-                return false;
-            }
+        // Get the data request ID. This should be the first element of the $args array.
+        $itemid = $args[0];
+        // Fetch the data request object. An invalid ID will throw an exception.
+        $datarequest = new \tool_dataprivacy\data_request($itemid);
+
+        // Check if user is allowed to download it.
+        if (!\tool_dataprivacy\api::can_download_data_request_for_user($context->instanceid, $datarequest->get('requestedby'))) {
+            return false;
         }
 
         // All good. Serve the exported data.
index f4a7a66..c341a63 100644 (file)
@@ -276,6 +276,57 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->assertFalse(api::can_manage_data_requests($nondpoincapable->id));
     }
 
+    /**
+     * Test for api::can_download_data_request_for_user()
+     */
+    public function test_can_download_data_request_for_user() {
+        $generator = $this->getDataGenerator();
+
+        // Three victims.
+        $victim1 = $generator->create_user();
+        $victim2 = $generator->create_user();
+        $victim3 = $generator->create_user();
+
+        // Assign a user as victim 1's parent.
+        $systemcontext = \context_system::instance();
+        $parentrole = $generator->create_role();
+        assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
+        $parent = $generator->create_user();
+        role_assign($parentrole, $parent->id, \context_user::instance($victim1->id));
+
+        // Assign another user as data access wonder woman.
+        $wonderrole = $generator->create_role();
+        assign_capability('tool/dataprivacy:downloadallrequests', CAP_ALLOW, $wonderrole, $systemcontext);
+        $staff = $generator->create_user();
+        role_assign($wonderrole, $staff->id, $systemcontext);
+
+        // Finally, victim 3 has been naughty; stop them accessing their own data.
+        $naughtyrole = $generator->create_role();
+        assign_capability('tool/dataprivacy:downloadownrequest', CAP_PROHIBIT, $naughtyrole, $systemcontext);
+        role_assign($naughtyrole, $victim3->id, $systemcontext);
+
+        // Victims 1 and 2 can access their own data, regardless of who requested it.
+        $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $victim1->id));
+        $this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $victim2->id));
+
+        // Victim 3 cannot access his own data.
+        $this->assertFalse(api::can_download_data_request_for_user($victim3->id, $victim3->id, $victim3->id));
+
+        // Victims 1 and 2 cannot access another victim's data.
+        $this->assertFalse(api::can_download_data_request_for_user($victim2->id, $victim1->id, $victim1->id));
+        $this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $victim2->id));
+
+        // Staff can access everyone's data.
+        $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $staff->id));
+        $this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $staff->id));
+        $this->assertTrue(api::can_download_data_request_for_user($victim3->id, $staff->id, $staff->id));
+
+        // Parent can access victim 1's data only if they requested it.
+        $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $parent->id, $parent->id));
+        $this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $parent->id));
+        $this->assertFalse(api::can_download_data_request_for_user($victim2->id, $parent->id, $parent->id));
+    }
+
     /**
      * Test for api::create_data_request()
      */
@@ -417,27 +468,20 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
      * @return array
      */
     public function get_data_requests_provider() {
-        $generator = new testing_data_generator();
-        $user1 = $generator->create_user();
-        $user2 = $generator->create_user();
-        $user3 = $generator->create_user();
-        $user4 = $generator->create_user();
-        $user5 = $generator->create_user();
-        $users = [$user1, $user2, $user3, $user4, $user5];
         $completeonly = [api::DATAREQUEST_STATUS_COMPLETE];
         $completeandcancelled = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED];
 
         return [
             // Own data requests.
-            [$users, $user1, false, $completeonly],
+            ['user', false, $completeonly],
             // Non-DPO fetching all requets.
-            [$users, $user2, true, $completeonly],
+            ['user', true, $completeonly],
             // Admin fetching all completed and cancelled requests.
-            [$users, get_admin(), true, $completeandcancelled],
+            ['dpo', true, $completeandcancelled],
             // Admin fetching all completed requests.
-            [$users, get_admin(), true, $completeonly],
+            ['dpo', true, $completeonly],
             // Guest fetching all requests.
-            [$users, guest_user(), true, $completeonly],
+            ['guest', true, $completeonly],
         ];
     }
 
@@ -445,12 +489,31 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
      * Test for api::get_data_requests()
      *
      * @dataProvider get_data_requests_provider
-     * @param stdClass[] $users Array of users to create data requests for.
-     * @param stdClass $loggeduser The user logging in.
+     * @param string $usertype The type of the user logging in.
      * @param boolean $fetchall Whether to fetch all records.
      * @param int[] $statuses Status filters.
      */
-    public function test_get_data_requests($users, $loggeduser, $fetchall, $statuses) {
+    public function test_get_data_requests($usertype, $fetchall, $statuses) {
+        $generator = new testing_data_generator();
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $user3 = $generator->create_user();
+        $user4 = $generator->create_user();
+        $user5 = $generator->create_user();
+        $users = [$user1, $user2, $user3, $user4, $user5];
+
+        switch ($usertype) {
+            case 'user':
+                $loggeduser = $user1;
+                break;
+            case 'dpo':
+                $loggeduser = get_admin();
+                break;
+            case 'guest':
+                $loggeduser = guest_user();
+                break;
+        }
+
         $comment = 'Data %s request comment by user %d';
         $exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT);
         $deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE);
diff --git a/admin/tool/dataprivacy/tests/behat/dataexport.feature b/admin/tool/dataprivacy/tests/behat/dataexport.feature
new file mode 100644 (file)
index 0000000..3ab0467
--- /dev/null
@@ -0,0 +1,107 @@
+@tool @tool_dataprivacy
+Feature: Data export from the privacy API
+  In order to export data for users and meet legal requirements
+  As an admin, user, or parent
+  I need to be able to export data for a user
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname      | lastname |
+      | victim   | Victim User    | 1        |
+      | parent   | Long-suffering | Parent   |
+    And the following "roles" exist:
+      | shortname | name  | archetype |
+      | tired     | Tired |           |
+    And the following "permission overrides" exist:
+      | capability                                   | permission | role  | contextlevel | reference |
+      | tool/dataprivacy:makedatarequestsforchildren | Allow      | tired | System       |           |
+    And the following "role assigns" exist:
+      | user   | role  | contextlevel | reference |
+      | parent | tired | User         | victim    |
+    And the following config values are set as admin:
+      | contactdataprotectionofficer | 1 | tool_dataprivacy |
+
+  @javascript
+  Scenario: As admin, export data for a user and download it
+    Given I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "New request"
+    And I set the field "Requesting for" to "Victim User 1"
+    And I press "Save changes"
+    Then I should see "Victim User 1"
+    And I should see "Pending" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Awaiting approval" in the "Victim User 1" "table_row"
+    And I follow "Actions"
+    And I follow "Approve request"
+    And I press "Approve request"
+    And I should see "Approved" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Complete" in the "Victim User 1" "table_row"
+    And I follow "Actions"
+    And following "Download" should download between "1" and "100000" bytes
+
+  @javascript
+  Scenario: As a student, request data export and then download it when approved
+    Given I log in as "victim"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I follow "New request"
+    And I press "Save changes"
+    Then I should see "Export all of my personal data"
+    And I should see "Pending" in the "Export all of my personal data" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Awaiting approval" in the "Export all of my personal data" "table_row"
+
+    And I log out
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "Actions"
+    And I follow "Approve request"
+    And I press "Approve request"
+
+    And I log out
+    And I log in as "victim"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I should see "Approved" in the "Export all of my personal data" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Complete" in the "Export all of my personal data" "table_row"
+    And I follow "Actions"
+    And following "Download" should download between "1" and "100000" bytes
+
+  @javascript
+  Scenario: As a parent, request data export for my child because I don't trust the little blighter
+    Given I log in as "parent"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I follow "New request"
+    And I set the field "Requesting for" to "Victim User 1"
+    And I press "Save changes"
+    Then I should see "Victim User 1"
+    And I should see "Pending" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Awaiting approval" in the "Victim User 1" "table_row"
+
+    And I log out
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "Actions"
+    And I follow "Approve request"
+    And I press "Approve request"
+
+    And I log out
+    And I log in as "parent"
+    And I follow "Profile" in the user menu
+    And I follow "Data requests"
+    And I should see "Approved" in the "Victim User 1" "table_row"
+    And I run all adhoc tasks
+    And I reload the page
+    And I should see "Complete" in the "Victim User 1" "table_row"
+    And I follow "Actions"
+    And following "Download" should download between "1" and "100000" bytes
index 01b8b2b..f2cf6d1 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018051402;
+$plugin->version   = 2018051403;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
index d40eec0..bcb1d54 100644 (file)
@@ -16,6 +16,8 @@
 
 /**
  * Legacy log reader.
+ * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
+ * @todo  MDL-52805 This is to be removed in Moodle 4.0
  *
  * @package    logstore_legacy
  * @copyright  2013 Petr Skoda {@link http://skodak.org}
@@ -30,6 +32,12 @@ class store implements \tool_log\log\store, \core\log\sql_reader {
     use \tool_log\helper\store,
         \tool_log\helper\reader;
 
+    /**
+     * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
+     * @todo  MDL-52805 This is to be removed in Moodle 4.0
+     *
+     * @param \tool_log\log\manager $manager
+     */
     public function __construct(\tool_log\log\manager $manager) {
         $this->helper_setup($manager);
     }
@@ -83,6 +91,17 @@ class store implements \tool_log\log\store, \core\log\sql_reader {
         return array($selectwhere, $params, $sort);
     }
 
+    /**
+     * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
+     * @todo MDL-52805 This will be removed in Moodle 4.0
+     *
+     * @param  string $selectwhere
+     * @param  array  $params
+     * @param  string $sort
+     * @param  int    $limitfrom
+     * @param  int    $limitnum
+     * @return array
+     */
     public function get_events_select($selectwhere, array $params, $sort, $limitfrom, $limitnum) {
         global $DB;
 
@@ -114,6 +133,8 @@ class store implements \tool_log\log\store, \core\log\sql_reader {
 
     /**
      * Fetch records using given criteria returning a Traversable object.
+     * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
+     * @todo MDL-52805 This will be removed in Moodle 4.0
      *
      * Note that the traversable object contains a moodle_recordset, so
      * remember that is important that you call close() once you finish
@@ -146,6 +167,8 @@ class store implements \tool_log\log\store, \core\log\sql_reader {
 
     /**
      * Returns an event from the log data.
+     * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
+     * @todo MDL-52805 This will be removed in Moodle 4.0
      *
      * @param stdClass $data Log data
      * @return \core\event\base
@@ -154,6 +177,14 @@ class store implements \tool_log\log\store, \core\log\sql_reader {
         return \logstore_legacy\event\legacy_logged::restore_legacy($data);
     }
 
+    /**
+     * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
+     * @todo MDL-52805 This will be removed in Moodle 4.0
+     *
+     * @param  string $selectwhere
+     * @param  array  $params
+     * @return int
+     */
     public function get_events_select_count($selectwhere, array $params) {
         global $DB;
 
@@ -170,6 +201,8 @@ class store implements \tool_log\log\store, \core\log\sql_reader {
 
     /**
      * Are the new events appearing in the reader?
+     * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
+     * @todo MDL-52805 This will be removed in Moodle 4.0
      *
      * @return bool true means new log events are being added, false means no new data will be added
      */
@@ -177,6 +210,10 @@ class store implements \tool_log\log\store, \core\log\sql_reader {
         return (bool)$this->get_config('loglegacy', true);
     }
 
+    /**
+     * @deprecated since Moodle 3.6 MDL-52953 - Please use supported log stores such as "standard" or "external" instead.
+     * @todo MDL-52805 This will be removed in Moodle 4.0
+     */
     public function dispose() {
     }
 
diff --git a/admin/tool/log/upgrade.txt b/admin/tool/log/upgrade.txt
new file mode 100644 (file)
index 0000000..e1b65e3
--- /dev/null
@@ -0,0 +1,8 @@
+This files describes API changes in /admin/tool/log - plugins,
+information provided here is intended especially for developers.
+
+
+=== 3.6 ===
+
+* The legacy log store is in its first stage of deprecation and is due for removal in Moodle 4.0. Please use one of
+  the other log stores such as "standard" and "database".
\ No newline at end of file
index 5ca5d99..66ea3aa 100644 (file)
@@ -102,6 +102,7 @@ class page_viewalldoc implements renderable, templatable {
 
         array_walk($data->policies, function($item, $key) {
             $item->policytypestr = get_string('policydoctype'.$item->type, 'tool_policy');
+            $item->policyaudiencestr = get_string('policydocaudience'.$item->audience, 'tool_policy');
         });
 
         return $data;
index fd29175..3a75285 100644 (file)
@@ -84,6 +84,7 @@ $string['inactivatingconfirm'] = '<p>You are about to inactivate policy <em>\'{$
 $string['inactivatingconfirmyes'] = 'Inactivate';
 $string['invalidversionid'] = 'There is no policy with this identifier!';
 $string['irevokethepolicy'] = 'Withdraw user consent';
+$string['listactivepolicies'] = 'List of active policies';
 $string['minorchange'] = 'Minor change';
 $string['minorchangeinfo'] = 'A minor change does not alter the meaning of the policy. Users are not required to agree to the policy again if the edit is marked as a minor change.';
 $string['managepolicies'] = 'Manage policies';
index 89812ff..2f9df36 100644 (file)
                 "name": "Terms &amp; conditions",
                 "summary": "Policy <u>summary</u>",
                 "content": "Policy <em>content</em>",
-                "policytypestr": "Site policy"
+                "policytypestr": "Site policy",
+                "policyaudiencestr": "All users"
             },
             {
                 "id": "5",
                 "name": "Privacy",
                 "summary": "We keep your information private",
                 "content": "Very private",
-                "policytypestr": "Privacy policy"
+                "policytypestr": "Privacy policy",
+                "policyaudiencestr": "Authenticated users"
             }
         ]
     }
 }}
 
 <a id="top"></a>
-<div id="policies_index" class="m-b-3">
-<ul>
+<div id="policies_index">
+<h1>{{# str }} listactivepolicies, tool_policy {{/ str }}</h1>
+<table class="table">
+    <thead>
+    <tr>
+        <th scope="col">{{# str }}policydocname, tool_policy {{/ str }}</th>
+        <th scope="col">{{# str }}policydoctype, tool_policy {{/ str }}</th>
+        <th scope="col">{{# str }}policydocaudience, tool_policy {{/ str }}</th>
+    </tr>
+    </thead>
+    <tbody>
     {{#policies }}
-        <li><a href="#policy-{{id}}">{{{name}}} ({{{policytypestr}}})</a></li>
+        <tr>
+            <td><a href="#policy-{{id}}">{{{name}}}</a></td>
+            <td>{{{ policytypestr }}}</td>
+            <td>{{{ policyaudiencestr }}}</td>
+        </tr>
     {{/policies }}
-</ul>
+    </tbody>
+</table>
 </div>
 
 {{^policies }}
         <hr>
     <div class="policy_version m-b-3">
         <div class="clearfix m-t-2">
-            <h1><a id="policy-{{id}}">{{{name}}}</a></h1>
+            <h2><a id="policy-{{id}}">{{{name}}}</a></h2>
         </div>
         <div class="policy_document_summary clearfix m-b-1">
-            <h2>{{# str }} policydocsummary, tool_policy {{/ str }}</h2>
+            <h3>{{# str }} policydocsummary, tool_policy {{/ str }}</h3>
             {{{summary}}}
         </div>
         <div class="policy_document_content m-t-2">
-            <h2>{{# str }} policydoccontent, tool_policy {{/ str }}</h2>
+            <h3>{{# str }} policydoccontent, tool_policy {{/ str }}</h3>
             {{{content}}}
         </div>
         <div class="pull-right">
index 6c504eb..2b9b026 100644 (file)
@@ -364,7 +364,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I log out
     # Create new policy document.
     And I log in as "admin"
-    And I navigate to "Manage policies" node in "Site administration > Privacy and policies"
+    And I navigate to "Manage policies" node in "Site administration > Users > Privacy and policies"
     And I should see "Policies and agreements"
     And I should see "New policy"
     And I follow "New policy"
@@ -419,7 +419,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     And I log out
     # Create new version of the policy document.
     And I log in as "admin"
-    And I navigate to "Manage policies" node in "Site administration > Privacy and policies"
+    And I navigate to "Manage policies" node in "Site administration > Users > Privacy and policies"
     When I follow "Actions"
     Then I should see "View"
     And I should see "Edit"
@@ -460,6 +460,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
       | This privacy policy | 1    |          | full text3 | short text3 | active   | loggedin |
       | This guests policy  | 0    |          | full text4 | short text4 | active   | guest    |
     And I am on site homepage
+    And I change window size to "large"
     And I follow "Log in"
     When I press "Log in as a guest"
     Then I should see "If you continue browsing this website, you agree to our policies"
index fe6244c..39ca032 100644 (file)
@@ -277,6 +277,32 @@ class auth_plugin_shibboleth extends auth_plugin_base {
             return;
         }
     }
+
+    /**
+     * Return a list of identity providers to display on the login page.
+     *
+     * @param string $wantsurl The requested URL.
+     * @return array List of arrays with keys url, iconurl and name.
+     */
+    public function loginpage_idp_list($wantsurl) {
+        $config = get_config('auth_shibboleth');
+        $result = [];
+
+        // Before displaying the button check that Shibboleth is set-up correctly.
+        if (empty($config->user_attribute)) {
+            return $result;
+        }
+
+        $url = new moodle_url('/auth/shibboleth/index.php');
+        $iconurl = moodle_url::make_pluginfile_url(context_system::instance()->id,
+                                                   'auth_shibboleth',
+                                                   'logo',
+                                                   null,
+                                                   '/',
+                                                   $config->auth_logo);
+        $result[] = ['url' => $url, 'iconurl' => $iconurl, 'name' => $config->login_name];
+        return $result;
+    }
 }
 
 
diff --git a/auth/shibboleth/classes/helper.php b/auth/shibboleth/classes/helper.php
new file mode 100644 (file)
index 0000000..c1d5705
--- /dev/null
@@ -0,0 +1,123 @@
+<?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/>.
+
+/**
+ * Contains a helper class for the Shibboleth authentication plugin.
+ *
+ * @package    auth_shibboleth
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace auth_shibboleth;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The helper class for the Shibboleth authentication plugin.
+ *
+ * @package    auth_shibboleth
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+
+    /**
+     * Delete session of user using file sessions.
+     *
+     * @param string $spsessionid SP-provided Shibboleth Session ID
+     * @return \SoapFault or void if everything was fine
+     */
+    public static function logout_file_session($spsessionid) {
+        global $CFG;
+
+        if (!empty($CFG->session_file_save_path)) {
+            $dir = $CFG->session_file_save_path;
+        } else {
+            $dir = $CFG->dataroot . '/sessions';
+        }
+
+        if (is_dir($dir)) {
+            if ($dh = opendir($dir)) {
+                // Read all session files.
+                while (($file = readdir($dh)) !== false) {
+                    // Check if it is a file.
+                    if (is_file($dir.'/'.$file)) {
+                        // Read session file data.
+                        $data = file($dir.'/'.$file);
+                        if (isset($data[0])) {
+                            $usersession = self::unserializesession($data[0]);
+                            // Check if we have found session that shall be deleted.
+                            if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) {
+                                // If there is a match, delete file.
+                                if ($usersession['SESSION']->shibboleth_session_id == $spsessionid) {
+                                    // Delete session file.
+                                    if (!unlink($dir.'/'.$file)) {
+                                        return new SoapFault('LogoutError', 'Could not delete Moodle session file.');
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                closedir($dh);
+            }
+        }
+    }
+
+    /**
+     * Delete session of user using DB sessions.
+     *
+     * @param string $spsessionid SP-provided Shibboleth Session ID
+     */
+    public static function logout_db_session($spsessionid) {
+        global $CFG, $DB;
+
+        $sessions = $DB->get_records_sql(
+            'SELECT userid, sessdata FROM {sessions} WHERE timemodified > ?',
+            array(time() - $CFG->sessiontimeout)
+        );
+
+        foreach ($sessions as $session) {
+            // Get user session from DB.
+            if (session_decode(base64_decode($session->sessdata))) {
+                if (isset($_SESSION['SESSION']) && isset($_SESSION['SESSION']->shibboleth_session_id)) {
+                    // If there is a match, kill the session.
+                    if ($_SESSION['SESSION']->shibboleth_session_id == trim($spsessionid)) {
+                        // Delete this user's sessions.
+                        \core\session\manager::kill_user_sessions($session->userid);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Unserialize a session string.
+     *
+     * @param string $serializedstring
+     * @return array
+     */
+    private static function unserializesession($serializedstring) {
+        $variables = array();
+        $a = preg_split("/(\w+)\|/", $serializedstring, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+        $counta = count($a);
+        for ($i = 0; $i < $counta; $i = $i + 2) {
+            $variables[$a[$i]] = unserialize($a[$i + 1]);
+        }
+        return $variables;
+    }
+}
index 4d7e7df..144c1fd 100644 (file)
@@ -71,8 +71,8 @@ if ($show_instructions) {
 <?php     if (is_enabled_auth('none')) { // instructions override the rest for security reasons
               print_string("loginstepsnone");
           } else if ($CFG->registerauth == 'email') {
-              if (!empty($CFG->auth_instructions)) {
-                  echo format_text($CFG->auth_instructions);
+              if (!empty($config->auth_instructions)) {
+                  echo format_text($config->auth_instructions);
               } else {
                   print_string("loginsteps", "", "signup.php");
               } ?>
@@ -82,14 +82,14 @@ if ($show_instructions) {
                    </form>
                  </div>
 <?php     } else if (!empty($CFG->registerauth)) {
-              echo format_text($CFG->auth_instructions); ?>
+              echo format_text($config->auth_instructions); ?>
               <div class="signupform">
                 <form action="../../login/signup.php" method="get" id="signup">
                 <div><input type="submit" value="<?php print_string("startsignup") ?>" /></div>
                 </form>
               </div>
 <?php     } else {
-              echo format_text($CFG->auth_instructions);
+              echo format_text($config->auth_instructions);
           } ?>
       </div>
     </div>
index 659e4d1..8ef9ec1 100644 (file)
@@ -25,6 +25,8 @@
 
 $string['auth_shib_auth_method'] = 'Authentication method name';
 $string['auth_shib_auth_method_description'] = 'Provide a name for the Shibboleth authentication method that is familiar to your users. This could be the name of your Shibboleth federation, e.g. <tt>SWITCHaai Login</tt> or <tt>InCommon Login</tt> or similar.';
+$string['auth_shib_auth_logo'] = 'Authentication method logo';
+$string['auth_shib_auth_logo_description'] = 'Provide a logo for the Shibboleth authentication method that is familiar to your users. This could be the logo of your Shibboleth federation, e.g. <tt>SWITCHaai Login</tt> or <tt>InCommon Login</tt> or similar.';
 $string['auth_shib_contact_administrator'] = 'In case you are not associated with the given organizations and you need access to a course on this server, please contact the <a href="mailto:{$a}">Moodle Administrator</a>.';
 $string['auth_shibbolethdescription'] = 'Using this method users are created and authenticated using <a href="http://shibboleth.internet2.edu/">Shibboleth</a>.<br />Be sure to read the <a href="../auth/shibboleth/README.txt">README</a> for Shibboleth on how to set up your Moodle with Shibboleth';
 $string['auth_shibboleth_errormsg'] = 'Please select the organization you are member of!';
diff --git a/auth/shibboleth/lib.php b/auth/shibboleth/lib.php
new file mode 100644 (file)
index 0000000..f8ac757
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the hooks for the Shibboleth authentication module.
+ *
+ * @package auth_shibboleth
+ * @copyright 2018 Fabrice Ménard
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Serves the logo file settings.
+ *
+ * @param stdClass $course course object
+ * @param stdClass $cm course module object
+ * @param stdClass $context context object
+ * @param string $filearea file area
+ * @param array $args extra arguments
+ * @param bool $forcedownload whether or not force download
+ * @param array $options additional options affecting the file serving
+ * @return bool false if file not found, does not return if found - justsend the file
+ */
+function auth_shibboleth_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
+    if ($context->contextlevel != CONTEXT_SYSTEM) {
+        return false;
+    }
+
+    if ($filearea !== 'logo' ) {
+        return false;
+    }
+
+    $itemid = 0;
+
+    $filename = array_pop($args);
+    if (!$args) {
+        $filepath = '/';
+    } else {
+        $filepath = '/'.implode('/', $args).'/';
+    }
+
+    $fs = get_file_storage();
+    $file = $fs->get_file($context->id, 'auth_shibboleth', $filearea, $itemid, $filepath, $filename);
+    if (!$file) {
+        return false;
+    }
+
+    send_stored_file($file, null, 0, $forcedownload, $options);
+}
index 721af80..2ed931a 100644 (file)
 
     $loginurl = (!empty($CFG->alternateloginurl)) ? $CFG->alternateloginurl : '';
 
-
-    if (!empty($CFG->registerauth) or is_enabled_auth('none') or !empty($CFG->auth_instructions)) {
+    $config = get_config('auth_shibboleth');
+    if (!empty($CFG->registerauth) or is_enabled_auth('none') or !empty($config->auth_instructions)) {
         $show_instructions = true;
     } else {
         $show_instructions = false;
     }
 
-    // Set SAML domain cookie
-    $config = get_config('auth_shibboleth');
-
-
     $IdPs = get_idp_list($config->organization_selection);
     if (isset($_POST['idp']) && isset($IdPs[$_POST['idp']])){
         $selectedIdP = $_POST['idp'];
index 83f9234..f514d4e 100644 (file)
@@ -127,75 +127,14 @@ WSDL;
  * @return SoapFault or void if everything was fine
  */
 function LogoutNotification($spsessionid) {
-
-    global $CFG, $SESSION, $DB;
-
-    // Delete session of user using $spsessionid.
-    if(empty($CFG->dbsessions)) {
-
-        // File session
-        $dir = $CFG->dataroot .'/sessions';
-        if (is_dir($dir)) {
-            if ($dh = opendir($dir)) {
-                // Read all session files
-                while (($file = readdir($dh)) !== false) {
-                    // Check if it is a file
-                    if (is_file($dir.'/'.$file)){
-                        $session_key = preg_replace('/sess_/', '', $file);
-
-                        // Read session file data
-                        $data = file($dir.'/'.$file);
-                        if (isset($data[0])){
-                            $usersession = unserializesession($data[0]);
-
-                            // Check if we have found session that shall be deleted
-                            if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) {
-
-                                // If there is a match, delete file
-                                if ($usersession['SESSION']->shibboleth_session_id == $spsessionid) {
-                                    // Delete session file
-                                    if (!unlink($dir.'/'.$file)){
-                                        return new SoapFault('LogoutError', 'Could not delete Moodle session file.');
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-                closedir($dh);
-            }
-        }
-    } else {
-        // DB Sessions.
-        $sessions = $DB->get_records_sql(
-            'SELECT userid, sessdata FROM {sessions} WHERE timemodified > ?',
-            array(time() - $CFG->sessiontimeout)
-        );
-        foreach ($sessions as $session) {
-            // Get user session from DB.
-            if (session_decode(base64_decode($session->sessdata))) {
-                if (isset($_SESSION['SESSION']) && isset($_SESSION['SESSION']->shibboleth_session_id)) {
-                    // If there is a match, kill the session.
-                    if ($_SESSION['SESSION']->shibboleth_session_id == trim($spsessionid)) {
-                        // Delete this user's sessions.
-                        \core\session\manager::kill_user_sessions($session->userid);
-                    }
-                }
-            }
-        }
+    $sessionclass = \core\session\manager::get_handler_class();
+    switch ($sessionclass) {
+        case '\core\session\file':
+            return \auth_shibboleth\helper::logout_file_session($spsessionid);
+        case '\core\session\database':
+            return \auth_shibboleth\helper::logout_db_session($spsessionid);
+        default:
+            throw new moodle_exception("Shibboleth logout not implemented for '$sessionclass'");
     }
     // If no SoapFault was thrown, the function will return OK as the SP assumes.
 }
-
-/*****************************************************************************/
-
-// Same function as in adodb, but cannot be used for file session for some reason...
-function unserializesession($serialized_string) {
-    $variables = array();
-    $a = preg_split("/(\w+)\|/", $serialized_string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
-    $counta = count($a);
-    for ($i = 0; $i < $counta; $i = $i+2) {
-            $variables[$a[$i]] = unserialize($a[$i+1]);
-    }
-    return $variables;
-}
index c901378..e4b4c3a 100644 (file)
@@ -63,6 +63,11 @@ if ($ADMIN->fulltree) {
             get_string('auth_shib_auth_method', 'auth_shibboleth'),
             get_string('auth_shib_auth_method_description', 'auth_shibboleth'), 'Shibboleth Login', PARAM_RAW_TRIMMED));
 
+    // Authentication method logo.
+    $settings->add(new admin_setting_configstoredfile('auth_shibboleth/auth_logo',
+                get_string('auth_shib_auth_logo', 'auth_shibboleth'),
+                get_string('auth_shib_auth_logo_description', 'auth_shibboleth'), 'logo', 0, ['accepted_types' => ['image']]));
+
     // Login directions.
     $settings->add(new admin_setting_configtextarea('auth_shibboleth/auth_instructions',
             get_string('auth_shib_instructions_key', 'auth_shibboleth'),
index dbd7b4b..9083025 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /auth/shibboleth/*,
 information provided here is intended especially for developers.
 
+=== 3.5.2 ===
+
+* Moved the public function unserializesession in auth/shibboleth/logout.php to auth/shibboleth/classes/helper.php and
+  made it private. This function should not have been used outside of this file.
+
 === 3.3 ===
 
 * The config.html file was migrated to use the admin settings API.
index 50b9ec6..4b6e2a9 100644 (file)
@@ -25,11 +25,11 @@ Feature: Test the 'showlogfailures' feature works.
     And I set the field "Password" to "teacher1"
     And I press "Log in"
     # Confirm the notices are displayed.
-    Then I should see "1 failed logins since your last login" in the "nav.navbar" "css_element"
+    Then I should see "1 failed logins since your last login" in the ".navbar" "css_element"
     And I should see "1 failed logins since your last login" in the "page-footer" "region"
     # Confirm the notices disappear when navigating to another page.
     And I am on homepage
-    And I should not see "1 failed logins since your last login" in the "nav.navbar" "css_element"
+    And I should not see "1 failed logins since your last login" in the ".navbar" "css_element"
     And I should not see "1 failed logins since your last login" in the "page-footer" "region"
 
   # Given the user has at least one failed login attempt, when they login, then they should see both header and footer notices.
@@ -46,10 +46,10 @@ Feature: Test the 'showlogfailures' feature works.
     And I set the field "Password" to "admin"
     And I press "Log in"
     # Confirm the notices are displayed.
-    Then I should see "1 failed logins since your last login" in the "nav.navbar" "css_element"
+    Then I should see "1 failed logins since your last login" in the ".navbar" "css_element"
     And I should see "1 failed logins since your last login (Logs)" in the "page-footer" "region"
     # Confirm that the link works and that the notices disappear when navigating to another page.
     And I click on "Logs" "link" in the "page-footer" "region"
     And I should see "User login failed" in the "table.reportlog" "css_element"
-    And I should not see "1 failed logins since your last login" in the "nav.navbar" "css_element"
+    And I should not see "1 failed logins since your last login" in the ".navbar" "css_element"
     And I should not see "1 failed logins since your last login (Logs)" in the "page-footer" "region"
index 9fe1753..7381991 100644 (file)
@@ -6,7 +6,7 @@ Feature: Test validation of 'Age of digital consent' setting.
 
   Background:
     Given I log in as "admin"
-    And I navigate to "Privacy settings" node in "Site administration > Privacy and policies"
+    And I navigate to "Privacy settings" node in "Site administration > Users > Privacy and policies"
 
   Scenario: Admin provides valid value for 'Age of digital consent'.
     Given I set the field "s__agedigitalconsentmap" to multiline:
index ccf6dd1..98d495f 100644 (file)
@@ -27,10 +27,10 @@ Feature: Import course's content's twice
     And I log in as "teacher1"
 
   Scenario: Import course's contents to another course
-    Given I am on "Course 2" course homepage with editing mode on
+    Given I am on "Course 2" course homepage
     And I should not see "Online users"
     And I should not see "Test quiz"
-    And  I import "Course 1" course into "Course 2" course using this options:
+    And I import "Course 1" course into "Course 2" course using this options:
     And I am on "Course 2" course homepage
     And I should see "Online users"
     And I should see "Test quiz"
index ec45776..7ef9117 100644 (file)
@@ -604,7 +604,8 @@ abstract class restore_dbops {
         //             6b) User cannot, check if we are in some contextlevel with fallback
         //                 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
         //                 7b) No fallback, error. End qcat loop
-        //         5b) Match, mark q to be mapped
+        //         5b) Random question, must always create new.
+        //         5c) Match, mark q to be mapped
         // 8) Check if backup is from Moodle >= 3.5 and error if more than one top-level category in the context.
 
         // Get all the contexts (question banks) in restore for the given contextlevel
@@ -708,7 +709,11 @@ abstract class restore_dbops {
                                 break 2; // out from qcat loop (both 7a and 7b), we have decided about ALL categories in context (bank)
                             }
 
-                        // 5b) Match, mark q to be mapped
+                        // 5b) Random questions must always be newly created.
+                        } else if ($question->qtype == 'random') {
+                            // Nothing to mark, newitemid means create
+
+                        // 5c) Match, mark q to be mapped.
                         } else {
                             self::set_backup_ids_record($restoreid, 'question', $question->id, $matchqid);
                         }
@@ -718,7 +723,7 @@ abstract class restore_dbops {
 
             // 8) Check if backup is made on Moodle >= 3.5 and there are more than one top-level category in the context.
             if ($after35 && $topcats > 1) {
-                $errors[] = get_string('restoremultipletopcats', 'questions', $contextid);
+                $errors[] = get_string('restoremultipletopcats', 'question', $contextid);
             }
 
         }
index b7de027..5d886c2 100644 (file)
@@ -46,6 +46,3 @@ $string['myprofile:myaddinstance'] = 'Add a new logged in user block to Dashboar
 $string['myprofile_settings'] = 'Visible user information';
 $string['pluginname'] = 'Logged in user';
 $string['privacy:metadata'] = 'The Logged in user block only shows information about the logged in user and does not store data itself.';
-
-// Deprecated since Moodle 3.2.
-$string['display_un'] = 'Display name';
index 6a46c36..0424b73 100644 (file)
@@ -228,16 +228,6 @@ class block_recent_activity extends block_base {
         return array('all' => true, 'my' => false, 'tag' => false);
     }
 
-    /**
-     * Remove old entries from table block_recent_activity
-     */
-    public function cron() {
-        global $DB;
-        // Those entries will never be displayed as RECENT anyway.
-        $DB->delete_records_select('block_recent_activity', 'timecreated < ?',
-                array(time() - COURSE_MAX_RECENT_PERIOD));
-    }
-
     /**
      * Migrates entries from table {log} into {block_recent_activity}
      *
diff --git a/blocks/recent_activity/classes/task/cleanup.php b/blocks/recent_activity/classes/task/cleanup.php
new file mode 100644 (file)
index 0000000..c3f34c7
--- /dev/null
@@ -0,0 +1,58 @@
+<?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 for updating RSS feeds for rss client block
+ *
+ * @package   block_recent_activity
+ * @author    Farhan Karmali <farhan6318@gmail.com>
+ * @copyright Farhan Karmali 2018
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_recent_activity\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Task for updating RSS feeds for rss client block
+ *
+ * @package   block_recent_activity
+ * @author    Farhan Karmali <farhan6318@gmail.com>
+ * @copyright Farhan Karmali 2018
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cleanup extends \core\task\scheduled_task {
+
+    /**
+     * Name for this task.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('cleanuptask', 'block_recent_activity');
+    }
+
+    /**
+     * Remove old entries from table block_recent_activity
+     */
+    public function execute() {
+        global $DB;
+        // Those entries will never be displayed as RECENT anyway.
+        $DB->delete_records_select('block_recent_activity', 'timecreated < ?',
+            array(time() - COURSE_MAX_RECENT_PERIOD));
+    }
+}
similarity index 51%
rename from lib/classes/task/events_cron_task.php
rename to blocks/recent_activity/db/tasks.php
index 07f8bae..0da9677 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * A scheduled task.
- *
- * @package    core
- * @copyright  2013 onwards Martin Dougiamas  http://dougiamas.com
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * Task definition for block_recent_activity.
+ * @author    Farhan Karmali <farhan6318@gmail.com>
+ * @copyright Farhan Karmali 2018
+ * @package   block_recent_activity
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-namespace core\task;
 
-/**
- * Simple task to run the events cron.
- */
-class events_cron_task extends scheduled_task {
-
-    /**
-     * Get a descriptive name for this task (shown to admins).
-     *
-     * @return string
-     */
-    public function get_name() {
-        return get_string('taskeventscron', 'admin');
-    }
+defined('MOODLE_INTERNAL') || die();
 
-    /**
-     * Do the job.
-     * Throw exceptions on errors (the job will be retried).
-     */
-    public function execute() {
-        events_cron();
-    }
+$tasks = array(
+    array(
+        'classname' => '\block_recent_activity\task\cleanup',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => 'R',
+        'day' => '*',
+        'month' => '*',
+        'dayofweek' => '*',
+        'disabled' => 0
+    )
+);
 
-}
index defbc88..c77dca2 100644 (file)
@@ -23,6 +23,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['cleanuptask'] = 'Cleanup task for recent activity block';
 $string['pluginname'] = 'Recent activity';
 $string['privacy:metadata'] = 'The recent activity block contains a cache of data stored elsewhere in Moodle.';
 $string['privacy:metadata:block_recent_activity'] = 'Temporary log of recent teacher activity. Removed after two days';
index 1943157..48711ce 100644 (file)
@@ -24,7 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018051400;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2018052900;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2018050800;        // Requires this Moodle version
 $plugin->component = 'block_recent_activity'; // Full name of the plugin (used for diagnostics)
-$plugin->cron      = 24*3600;           // Cron interval 1 day.
\ No newline at end of file
index bc6d7c5..2a2fd78 100644 (file)
         }
     }
 
-    /**
-     * cron - goes through all the feeds. If the feed has a skipuntil value
-     * that is less than the current time cron will attempt to retrieve it
-     * with the cache duration set to 0 in order to force the retrieval of
-     * the item and refresh the cache.
-     *
-     * If a feed fails then the skipuntil time of that feed is set to be
-     * later than the next expected cron time. The amount of time will
-     * increase each time the fetch fails until the maximum is reached.
-     *
-     * If a feed that has been failing is successfully retrieved it will
-     * go back to being handled as though it had never failed.
-     *
-     * CRON should therefor process requests for permanently broken RSS
-     * feeds infrequently, and temporarily unavailable feeds will be tried
-     * less often until they become available again.
-     *
-     * @return boolean Always returns true
-     */
-    function cron() {
-        global $CFG, $DB;
-        require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
-
-        // Get the legacy cron time, strangely the cron property of block_base
-        // does not seem to get set. This means we must retrive it here.
-        $this->cron = $DB->get_field('block', 'cron', array('name' => 'rss_client'));
-
-        // We are going to measure execution times
-        $starttime =  microtime();
-        $starttimesec = time();
-
-        // Fetch all site feeds.
-        $rs = $DB->get_recordset('block_rss_client');
-        $counter = 0;
-        mtrace('');
-        foreach ($rs as $rec) {
-            mtrace('    ' . $rec->url . ' ', '');
-
-            // Skip feed if it failed recently.
-            if ($starttimesec < $rec->skipuntil) {
-                mtrace('skipping until ' . userdate($rec->skipuntil));
-                continue;
-            }
-
-            // Fetch the rss feed, using standard simplepie caching
-            // so feeds will be renewed only if cache has expired
-            core_php_time_limit::raise(60);
-
-            $feed =  new moodle_simplepie();
-            // set timeout for longer than normal to be agressive at
-            // fetching feeds if possible..
-            $feed->set_timeout(40);
-            $feed->set_cache_duration(0);
-            $feed->set_feed_url($rec->url);
-            $feed->init();
-
-            if ($feed->error()) {
-                // Skip this feed (for an ever-increasing time if it keeps failing).
-                $rec->skiptime = $this->calculate_skiptime($rec->skiptime);
-                $rec->skipuntil = time() + $rec->skiptime;
-                $DB->update_record('block_rss_client', $rec);
-                mtrace("Error: could not load/find the RSS feed - skipping for {$rec->skiptime} seconds.");
-            } else {
-                mtrace ('ok');
-                // It worked this time, so reset the skiptime.
-                if ($rec->skiptime > 0) {
-                    $rec->skiptime = 0;
-                    $rec->skipuntil = 0;
-                    $DB->update_record('block_rss_client', $rec);
-                }
-                // Only increase the counter when a feed is sucesfully refreshed.
-                $counter ++;
-            }
-        }
-        $rs->close();
-
-        // Show times
-        mtrace($counter . ' feeds refreshed (took ' . microtime_diff($starttime, microtime()) . ' seconds)');
-
-        return true;
-    }
-
     /**
      * Calculates a new skip time for a record based on the current skip time.
      *
      * @param int $currentskip The curreent skip time of a record.
      * @return int A new skip time that should be set.
      */
-    protected function calculate_skiptime($currentskip) {
+    public function calculate_skiptime($currentskip) {
         // The default time to skiptime.
         $newskiptime = $this->cron * 1.1;
         if ($currentskip > 0) {
diff --git a/blocks/rss_client/classes/task/refreshfeeds.php b/blocks/rss_client/classes/task/refreshfeeds.php
new file mode 100644 (file)
index 0000000..3e34e1b
--- /dev/null
@@ -0,0 +1,124 @@
+<?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 for updating RSS feeds for rss client block
+ *
+ * @package   block_rss_client
+ * @author    Farhan Karmali <farhan6318@gmail.com>
+ * @copyright Farhan Karmali 2018
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_rss_client\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Task for updating RSS feeds for rss client block
+ *
+ * @package   block_rss_client
+ * @author    Farhan Karmali <farhan6318@gmail.com>
+ * @copyright Farhan Karmali 2018
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class refreshfeeds extends \core\task\scheduled_task {
+
+    /**
+     * Name for this task.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('refreshfeedstask', 'block_rss_client');
+    }
+
+    /**
+     * This task goes through all the feeds. If the feed has a skipuntil value
+     * that is less than the current time cron will attempt to retrieve it
+     * with the cache duration set to 0 in order to force the retrieval of
+     * the item and refresh the cache.
+     *
+     * If a feed fails then the skipuntil time of that feed is set to be
+     * later than the next expected task time. The amount of time will
+     * increase each time the fetch fails until the maximum is reached.
+     *
+     * If a feed that has been failing is successfully retrieved it will
+     * go back to being handled as though it had never failed.
+     *
+     * Task should therefore process requests for permanently broken RSS
+     * feeds infrequently, and temporarily unavailable feeds will be tried
+     * less often until they become available again.
+     */
+    public function execute() {
+        global $CFG, $DB;
+        require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
+
+        // We are going to measure execution times.
+        $starttime = microtime();
+        $starttimesec = time();
+
+        // Fetch all site feeds.
+        $rs = $DB->get_recordset('block_rss_client');
+        $counter = 0;
+        mtrace('');
+        foreach ($rs as $rec) {
+            mtrace('    ' . $rec->url . ' ', '');
+
+            // Skip feed if it failed recently.
+            if ($starttimesec < $rec->skipuntil) {
+                mtrace('skipping until ' . userdate($rec->skipuntil));
+                continue;
+            }
+
+            // Fetch the rss feed, using standard simplepie caching
+            // so feeds will be renewed only if cache has expired.
+            \core_php_time_limit::raise(60);
+
+            $feed = new \moodle_simplepie();
+            // Set timeout for longer than normal to be agressive at
+            // fetching feeds if possible..
+            $feed->set_timeout(40);
+            $feed->set_cache_duration(0);
+            $feed->set_feed_url($rec->url);
+            $feed->init();
+
+            if ($feed->error()) {
+                // Skip this feed (for an ever-increasing time if it keeps failing).
+                $block = new \block_rss_client();
+                $rec->skiptime = $block->calculate_skiptime($rec->skiptime);
+                $rec->skipuntil = time() + $rec->skiptime;
+                $DB->update_record('block_rss_client', $rec);
+                mtrace("Error: could not load/find the RSS feed - skipping for {$rec->skiptime} seconds.");
+            } else {
+                mtrace ('ok');
+                // It worked this time, so reset the skiptime.
+                if ($rec->skiptime > 0) {
+                    $rec->skiptime = 0;
+                    $rec->skipuntil = 0;
+                    $DB->update_record('block_rss_client', $rec);
+                }
+                // Only increase the counter when a feed is sucesfully refreshed.
+                $counter ++;
+            }
+        }
+        $rs->close();
+
+        // Show times.
+        mtrace($counter . ' feeds refreshed (took ' . microtime_diff($starttime, microtime()) . ' seconds)');
+
+    }
+}
similarity index 59%
rename from lib/password_compat/lib/password.php
rename to blocks/rss_client/db/tasks.php
index a49a2e1..254e8ae 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Deprecation notice for password_compat.
- *
- * @package    core
- * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * Task definition for block_rss_client.
+ * @author    Farhan Karmali <farhan6318@gmail.com>
+ * @copyright Farhan Karmali 2018
+ * @package   block_rss_client
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
-debugging('password_compat is now standard in all versions of PHP that Moodle supports. '
-    . 'You no longer need to include the lib/password_compat/lib/password.php',
-    DEBUG_DEVELOPER);
+$tasks = array(
+    array(
+        'classname' => '\block_rss_client\task\refreshfeeds',
+        'blocking' => 0,
+        'minute' => '*/5',
+        'hour' => '*',
+        'day' => '*',
+        'month' => '*',
+        'dayofweek' => '*',
+        'disabled' => 0
+    )
+);
+
index 3045074..19a9ca5 100644 (file)
@@ -72,6 +72,7 @@ $string['privacy:metadata:block_rss_client:title'] = 'The title of the RSS feed.
 $string['privacy:metadata:block_rss_client:url'] = 'The URL of the RSS feed.';
 $string['privacy:metadata:block_rss_client:userid'] = 'The ID of the user that added the RSS feed.';
 $string['remotenewsfeed'] = 'Remote news feed';
+$string['refreshfeedstask'] = 'Refresh RSS feeds task';
 $string['rss_client:addinstance'] = 'Add a new remote RSS feeds block';
 $string['rss_client:createprivatefeeds'] = 'Create private RSS feeds';
 $string['rss_client:createsharedfeeds'] = 'Create shared RSS feeds';
index 7f99275..bae4cde 100644 (file)
@@ -55,12 +55,12 @@ class block_rss_client_cron_testcase extends advanced_testcase {
         );
         $DB->insert_record('block_rss_client', $record);
 
-        $block = new block_rss_client();
+        $task = new \block_rss_client\task\refreshfeeds();
         ob_start();
 
         // Silence SimplePie php notices.
         $errorlevel = error_reporting($CFG->debug & ~E_USER_NOTICE);
-        $block->cron();
+        $task->execute();
         error_reporting($errorlevel);
 
         $cronoutput = ob_get_clean();
@@ -69,7 +69,7 @@ class block_rss_client_cron_testcase extends advanced_testcase {
     }
 
     /**
-     * Test that when a feed has an error the skip time is increaed correctly.
+     * Test that when a feed has an error the skip time is increased correctly.
      */
     public function test_error() {
         global $DB, $CFG;
@@ -114,20 +114,20 @@ class block_rss_client_cron_testcase extends advanced_testcase {
         );
         $record3->id = $DB->insert_record('block_rss_client', $record3);
 
-        // Run the cron.
-        $block = new block_rss_client();
+        // Run the scheduled task.
+        $task = new \block_rss_client\task\refreshfeeds();
         ob_start();
 
         // Silence SimplePie php notices.
         $errorlevel = error_reporting($CFG->debug & ~E_USER_NOTICE);
-        $block->cron();
+        $task->execute();
         error_reporting($errorlevel);
 
         $cronoutput = ob_get_clean();
         $skiptime1 = $record->skiptime * 2;
         $message1 = 'http://example.com/rss Error: could not load/find the RSS feed - skipping for ' . $skiptime1 . ' seconds.';
         $this->assertContains($message1, $cronoutput);
-        $skiptime2 = 330; // Assumes that the cron time in the version file is 300.
+        $skiptime2 = 0;
         $message2 = 'http://example.com/rss2 Error: could not load/find the RSS feed - skipping for ' . $skiptime2 . ' seconds.';
         $this->assertContains($message2, $cronoutput);
         $skiptime3 = block_rss_client::CLIENT_MAX_SKIPTIME;
index c0e9e59..6f9eac3 100644 (file)
@@ -24,7 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018051400;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2018052900;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2018050800;        // Requires this Moodle version
 $plugin->component = 'block_rss_client'; // Full name of the plugin (used for diagnostics)
-$plugin->cron      = 300;               // Set min time between cron executions to 300 secs (5 mins)
index 0abfe87..0d3bc5d 100644 (file)
Binary files a/calendar/amd/build/event_form.min.js and b/calendar/amd/build/event_form.min.js differ
index 6b1db42..894dcf9 100644 (file)
Binary files a/calendar/amd/build/repository.min.js and b/calendar/amd/build/repository.min.js differ
index 2b6707d..b64639a 100644 (file)
  * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery'], function($) {
+define(['jquery', 'core_calendar/repository'], function($, CalendarRepository) {
 
     var SELECTORS = {
         EVENT_GROUP_COURSE_ID: '[name="groupcourseid"]',
         EVENT_GROUP_ID: '[name="groupid"]',
-        SELECT_OPTION: 'option',
-    };
-
-    /**
-     * Parse the group id select element in the event form and pull out
-     * the course id from the value to allow us to toggle other select
-     * elements based on the course id for the group a user selects.
-     *
-     * This is a little hacky but I couldn't find a better way to pass
-     * the course id for each group id with the limitations of mforms.
-     *
-     * The group id options are rendered with a value like:
-     * "<courseid>-<groupid>"
-     * E.g.
-     * For a group with id 10 in a course with id 3 the value of the
-     * option will be 3-10.
-     *
-     * @method parseGroupSelect
-     * @param {object} formElement The root form element
-     */
-    var parseGroupSelect = function(formElement) {
-        formElement.find(SELECTORS.EVENT_GROUP_ID)
-            .find(SELECTORS.SELECT_OPTION)
-            .each(function(index, element) {
-                element = $(element);
-                var value = element.attr('value');
-                var splits = value.split('-');
-                var courseId = splits[0];
-
-                element.attr('data-course-id', courseId);
-            });
+        SELECT_OPTION: 'option'
     };
 
     /**
@@ -69,39 +39,29 @@ define(['jquery'], function($) {
      */
     var addCourseGroupSelectListeners = function(formElement) {
         var courseGroupSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID);
-        var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID);
-        var groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION);
-        var filterGroupSelectOptions = function() {
-            var selectedCourseId = courseGroupSelect.val();
-            var selectedIndex = null;
-            var hasGroups = false;
-            groupSelectOptions.each(function(index, element) {
-                element = $(element);
 
-                if (element.attr('data-course-id') == selectedCourseId) {
-                    element.removeClass('hidden');
-                    element.prop('disabled', false);
-                    hasGroups = true;
-                    if (selectedIndex === null || element.attr('selected')) {
-                        selectedIndex = index;
-                    }
-                } else {
-                    element.addClass('hidden');
-                    element.prop('disabled', true);
-                }
-            });
+        var loadGroupSelectOptions = function(groups) {
+            var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID),
+                groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION),
+                courseGroups = $(groups);
 
-            if (hasGroups) {
-                groupSelect.prop('disabled', false);
-            } else {
-                groupSelect.prop('disabled', true);
-            }
-
-            groupSelect.prop('selectedIndex', selectedIndex);
+            // Let's clear all options first.
+            groupSelectOptions.remove();
+            groupSelect.prop("disabled", false);
+            courseGroups.each(function(id, group) {
+                $(groupSelect).append($("<option></option>").attr("value", group.id).text(group.name));
+            });
         };
 
-        courseGroupSelect.on('change', filterGroupSelectOptions);
-        filterGroupSelectOptions();
+        // If the user choose a course in the selector do a WS request to get groups.
+        courseGroupSelect.on('change', function() {
+            var courseId = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID).val();
+            CalendarRepository.getCourseGroupsData(courseId)
+                .then(function(groups) {
+                    return loadGroupSelectOptions(groups);
+                })
+                .catch(Notification.exception);
+        });
     };
 
     /**
@@ -112,8 +72,6 @@ define(['jquery'], function($) {
      */
     var init = function(formId) {
         var formElement = $('#' + formId);
-
-        parseGroupSelect(formElement);
         addCourseGroupSelectListeners(formElement);
     };
 
index 049f819..d8166fa 100644 (file)
@@ -181,6 +181,23 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
         return Ajax.call([request])[0];
     };
 
+    /**
+     * Get the groups by course id.
+     *
+     * @param {Number} courseid The course id to fetch the groups from.
+     * @return {promise} Resolved with the course groups.
+     */
+    var getCourseGroupsData = function(courseid) {
+        var request = {
+            methodname: 'core_group_get_course_groups',
+            args: {
+                courseid: courseid
+            }
+        };
+
+        return Ajax.call([request])[0];
+    };
+
     return {
         getEventById: getEventById,
         deleteEvent: deleteEvent,
@@ -188,6 +205,7 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
         submitCreateUpdateForm: submitCreateUpdateForm,
         getCalendarMonthData: getCalendarMonthData,
         getCalendarDayData: getCalendarDayData,
-        getCalendarUpcomingData: getCalendarUpcomingData
+        getCalendarUpcomingData: getCalendarUpcomingData,
+        getCourseGroupsData: getCourseGroupsData
     };
 });
index 597eb70..0afc594 100644 (file)
@@ -81,6 +81,11 @@ class container {
      */
     protected static $modulecache = array();
 
+    /**
+     * @var int The requesting user. All capability checks are done against this user.
+     */
+    protected static $requestinguserid;
+
     /**
      * Initialises the dependency graph if it hasn't yet been.
      */
@@ -117,11 +122,13 @@ class container {
                 [self::class, 'apply_component_provide_event_action'],
                 [self::class, 'apply_component_is_event_visible'],
                 function ($dbrow) {
+                    $requestinguserid = self::get_requesting_user();
+
                     if (!empty($dbrow->categoryid)) {
                         // This is a category event. Check that the category is visible to this user.
-                        $category = \coursecat::get($dbrow->categoryid, IGNORE_MISSING, true);
+                        $category = \coursecat::get($dbrow->categoryid, IGNORE_MISSING, true, $requestinguserid);
 
-                        if (empty($category) || !$category->is_uservisible()) {
+                        if (empty($category) || !$category->is_uservisible($requestinguserid)) {
                             return true;
                         }
                     }
@@ -131,7 +138,7 @@ class container {
                         return false;
                     }
 
-                    $instances = get_fast_modinfo($dbrow->courseid)->instances;
+                    $instances = get_fast_modinfo($dbrow->courseid, $requestinguserid)->instances;
 
                     // If modinfo doesn't know about the module, we should ignore it.
                     if (!isset($instances[$dbrow->modulename]) || !isset($instances[$dbrow->modulename][$dbrow->instance])) {
@@ -156,11 +163,13 @@ class container {
                     }
 
                     $coursecontext = \context_course::instance($dbrow->courseid);
-                    if (!$cm->get_course()->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+                    if (!$cm->get_course()->visible &&
+                            !has_capability('moodle/course:viewhiddencourses', $coursecontext, $requestinguserid)) {
                         return true;
                     }
 
-                    if (!has_capability('moodle/course:view', $coursecontext) && !is_enrolled($coursecontext)) {
+                    if (!has_capability('moodle/course:view', $coursecontext, $requestinguserid) &&
+                            !is_enrolled($coursecontext, $requestinguserid)) {
                         return true;
                     }
 
@@ -191,6 +200,7 @@ class container {
      * Reset all static caches, called between tests.
      */
     public static function reset_caches() {
+        self::$requestinguserid = null;
         self::$eventfactory = null;
         self::$eventmapper = null;
         self::$eventvault = null;
@@ -230,6 +240,31 @@ class container {
         return self::$eventvault;
     }
 
+    /**
+     * Sets the requesting user so that all capability checks are done against this user.
+     * Setting the requesting user (hence calling this function) is optional and if you do not so,
+     * $USER will be used as the requesting user. However, if you wish to set the requesting user yourself,
+     * you should call this function before any other function of the container class is called.
+     *
+     * @param int $userid The user id.
+     * @throws \coding_exception
+     */
+    public static function set_requesting_user($userid) {
+        self::$requestinguserid = $userid;
+    }
+
+    /**
+     * Returns the requesting user id.
+     * It usually is the current user unless it has been set explicitly using set_requesting_user.
+     *
+     * @return int
+     */
+    public static function get_requesting_user() {
+        global $USER;
+
+        return empty(self::$requestinguserid) ? $USER->id : self::$requestinguserid;
+    }
+
     /**
      * Calls callback 'core_calendar_provide_event_action' from the component responsible for the event
      *
@@ -245,14 +280,23 @@ class container {
         $mapper = self::$eventmapper;
         $action = null;
         if ($event->get_course_module()) {
+            $requestinguserid = self::get_requesting_user();
+            $legacyevent = $mapper->from_event_to_legacy_event($event);
+            // We know for a fact that the the requesting user might be different from the logged in user,
+            // but the event mapper is not aware of that.
+            if (empty($event->user) && !empty($legacyevent->userid)) {
+                $legacyevent->userid = $requestinguserid;
+            }
+
             // TODO MDL-58866 Only activity modules currently support this callback.
             // Any other event will not be displayed on the dashboard.
             $action = component_callback(
                 'mod_' . $event->get_course_module()->get('modname'),
                 'core_calendar_provide_event_action',
                 [
-                    $mapper->from_event_to_legacy_event($event),
-                    self::$actionfactory
+                    $legacyevent,
+                    self::$actionfactory,
+                    $requestinguserid
                 ]
             );
         }
@@ -279,12 +323,21 @@ class container {
         $mapper = self::$eventmapper;
         $eventvisible = null;
         if ($event->get_course_module()) {
+            $requestinguserid = self::get_requesting_user();
+            $legacyevent = $mapper->from_event_to_legacy_event($event);
+            // We know for a fact that the the requesting user might be different from the logged in user,
+            // but the event mapper is not aware of that.
+            if (empty($event->user) && !empty($legacyevent->userid)) {
+                $legacyevent->userid = $requestinguserid;
+            }
+
             // TODO MDL-58866 Only activity modules currently support this callback.
             $eventvisible = component_callback(
                 'mod_' . $event->get_course_module()->get('modname'),
                 'core_calendar_is_event_visible',
                 [
-                    $mapper->from_event_to_legacy_event($event)
+                    $legacyevent,
+                    $requestinguserid
                 ]
             );
         }
index 924a6fa..ff9407b 100644 (file)
@@ -66,9 +66,11 @@ class create extends \moodleform {
         $mform = $this->_form;
         $starttime = isset($this->_customdata['starttime']) ? $this->_customdata['starttime'] : 0;
         $editoroptions = !(empty($this->_customdata['editoroptions'])) ? $this->_customdata['editoroptions'] : null;
-        $eventtypes = calendar_get_all_allowed_types();
+        $courseid = !(empty($this->_customdata['courseid'])) ? $this->_customdata['courseid'] : null;
 
-        if (empty($eventtypes)) {
+        $eventtypes = calendar_get_allowed_event_types($courseid);
+
+        if (in_array(true, $eventtypes, true) === false) {
             print_error('nopermissiontoupdatecalendar');
         }
 
@@ -120,18 +122,20 @@ class create extends \moodleform {
      * @return array
      */
     public function validation($data, $files) {
-        global $DB, $CFG;
+        global $DB;
 
         $errors = parent::validation($data, $files);
-        $eventtypes = calendar_get_all_allowed_types();
         $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
         $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid';
-        if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
+        $courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null;
+        $eventtypes = calendar_get_allowed_event_types($courseid);
+
+        if (empty($eventtype) || !isset($eventtypes[$eventtype]) || $eventtypes[$eventtype] == false) {
             $errors['eventtype'] = get_string('invalideventtype', 'calendar');
         }
 
-        if (isset($data[$coursekey]) && $data[$coursekey] > 0) {
-            if ($course = $DB->get_record('course', ['id' => $data[$coursekey]])) {
+        if ($courseid && $courseid > 0) {
+            if ($course = $DB->get_record('course', ['id' => $courseid])) {
                 if ($data['timestart'] < $course->startdate) {
                     $errors['timestart'] = get_string('errorbeforecoursestart', 'calendar');
                 }
@@ -140,11 +144,15 @@ class create extends \moodleform {
             }
         }
 
-        if ($eventtype == 'course' && empty($data['courseid'])) {
+        if ($eventtype == 'course' && empty($courseid)) {
             $errors['courseid'] = get_string('selectacourse');
         }
 
-        if ($eventtype == 'group' && empty($data['groupcourseid'])) {
+        if ($eventtype == 'group' && (!empty($courseid) && empty($data['groupid']))) {
+            $errors['groupcourseid'] = get_string('nogroups', 'core_group');
+        }
+
+        if ($eventtype == 'group' && empty($courseid)) {
             $errors['groupcourseid'] = get_string('selectacourse');
         }
 
index 59a9cd7..f848687 100644 (file)
@@ -53,28 +53,29 @@ trait eventtype {
      * @param array $eventtypes The available event types for the user
      */
     protected function add_event_type_elements($mform, $eventtypes) {
+        global $CFG, $DB;
         $options = [];
 
-        if (isset($eventtypes['user'])) {
+        if (!empty($eventtypes['user'])) {
             $options['user'] = get_string('user');
         }
-        if (isset($eventtypes['group'])) {
+        if (!empty($eventtypes['group'])) {
             $options['group'] = get_string('group');
         }
-        if (isset($eventtypes['course'])) {
+        if (!empty($eventtypes['course'])) {
             $options['course'] = get_string('course');
         }
-        if (isset($eventtypes['category'])) {
+        if (!empty($eventtypes['category'])) {
             $options['category'] = get_string('category');
         }
-        if (isset($eventtypes['site'])) {
+        if (!empty($eventtypes['site'])) {
             $options['site'] = get_string('site');
         }
 
         // If we only have one event type and it's 'user' event then don't bother
         // rendering the select boxes because there is no choice for the user to
         // make.
-        if (count(array_keys($eventtypes)) == 1 && isset($eventtypes['user'])) {
+        if (!empty($eventtypes['user']) && count($options) == 1) {
             $mform->addElement('hidden', 'eventtype');
             $mform->setType('eventtype', PARAM_TEXT);
             $mform->setDefault('eventtype', 'user');
@@ -87,9 +88,9 @@ trait eventtype {
             $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
         }
 
-        if (isset($eventtypes['category'])) {
+        if (!empty($eventtypes['category'])) {
             $categoryoptions = [];
-            foreach ($eventtypes['category'] as $id => $category) {
+            foreach (\coursecat::make_categories_list('moodle/category:manage') as $id => $category) {
                 $categoryoptions[$id] = $category;
             }
 
@@ -97,33 +98,26 @@ trait eventtype {
             $mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
         }
 
-        if (isset($eventtypes['course'])) {
-            $limit = !has_capability('moodle/calendar:manageentries', \context_system::instance());
-            $mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => $limit]);
+        $showall = $CFG->calendar_adminseesall && !has_capability('moodle/calendar:manageentries', \context_system::instance());
+        if (!empty($eventtypes['course'])) {
+            $mform->addElement('course', 'courseid', get_string('course'), ['limittoenrolled' => !$showall]);
             $mform->hideIf('courseid', 'eventtype', 'noteq', 'course');
         }
 
-        if (isset($eventtypes['group'])) {
-            $options = ['limittoenrolled' => true];
-            // Exclude courses without group.
-            if (isset($eventtypes['course']) && isset($eventtypes['groupcourses'])) {
-                $options['exclude'] = array_diff(array_keys($eventtypes['course']),
-                    array_keys($eventtypes['groupcourses']));
-            }
-
-            $mform->addElement('course', 'groupcourseid', get_string('course'), $options);
+        if (!empty($eventtypes['group'])) {
+            $groups = !(empty($this->_customdata['groups'])) ? $this->_customdata['groups'] : null;
+            // Get the list of courses without groups to filter on the course selector.
+            $sql = "SELECT c.id
+                      FROM {course} c
+                     WHERE c.id NOT IN (
+                            SELECT DISTINCT courseid FROM {groups}
+                           )";
+            $coursesnogroup = $DB->get_records_sql($sql);
+            $mform->addElement('course', 'groupcourseid', get_string('course'),  ['limittoenrolled' => !$showall,
+                    'exclude' => array_keys($coursesnogroup)]);
             $mform->hideIf('groupcourseid', 'eventtype', 'noteq', 'group');
 
-            $groupoptions = [];
-            foreach ($eventtypes['group'] as $group) {
-                // We are formatting it this way in order to provide the javascript both
-                // the course and group ids so that it can enhance the form for the user.
-                $index = "{$group->courseid}-{$group->id}";
-                $groupoptions[$index] = format_string($group->name, true,
-                    ['context' => \context_course::instance($group->courseid)]);
-            }
-
-            $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
+            $mform->addElement('select', 'groupid', get_string('group'), $groups);
             $mform->hideIf('groupid', 'eventtype', 'noteq', 'group');
             // We handle the group select hide/show actions on the event_form module.
         }
index 0e5a0b6..102761f 100644 (file)
@@ -41,8 +41,8 @@ class managesubscriptions extends \moodleform {
      */
     public function definition() {
         $mform = $this->_form;
-        $eventtypes = calendar_get_all_allowed_types();
-        if (empty($eventtypes)) {
+        $eventtypes = calendar_get_allowed_event_types();
+        if (in_array(true, $eventtypes, true) === false) {
             print_error('nopermissiontoupdatecalendar');
         }
 
@@ -100,9 +100,10 @@ class managesubscriptions extends \moodleform {
 
         $errors = parent::validation($data, $files);
 
-        $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
-        $eventtypes = calendar_get_all_allowed_types();
         $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
+        $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid';
+        $courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null;
+        $eventtypes = calendar_get_allowed_event_types($courseid);
 
         if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
             $errors['eventtype'] = get_string('invalideventtype', 'calendar');
index 91c1435..032d839 100644 (file)
@@ -58,7 +58,7 @@ class create_update_form_mapper implements create_update_form_mapper_interface {
 
         if ($legacyevent->eventtype == 'group') {
             // Set up the correct value for the to display on the form.
-            $data->groupid = "{$legacyevent->courseid}-{$legacyevent->groupid}";
+            $data->groupid = $legacyevent->groupid;
             $data->groupcourseid = $legacyevent->courseid;
         }
         if ($legacyevent->eventtype == 'course') {
@@ -93,12 +93,8 @@ class create_update_form_mapper implements create_update_form_mapper_interface {
                 $properties->courseid = $data->groupcourseid;
                 unset($properties->groupcourseid);
             }
-
-            // Pull the group id back out of the value. The form saves the value
-            // as "<courseid>-<groupid>" to allow the javascript to work correctly.
             if (isset($data->groupid)) {
-                list($courseid, $groupid) = explode('-', $data->groupid);
-                $properties->groupid = $groupid;
+                $properties->groupid = $data->groupid;
             }
         } else {
             // Default course id if none is set.
index d4775ee..37d1a80 100644 (file)
@@ -96,11 +96,24 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
             return array();
         }
 
+        if (is_numeric($users)) {
+            $users = array($users);
+        }
+        if (is_numeric($groups)) {
+            $groups = array($groups);
+        }
+        if (is_numeric($courses)) {
+            $courses = array($courses);
+        }
+        if (is_numeric($categories)) {
+            $categories = array($categories);
+        }
+
         // Array of filter conditions. To be concatenated by the OR operator.
         $filters = [];
 
         // User filter.
-        if ((is_array($users) && !empty($users)) or is_numeric($users)) {
+        if (is_array($users) && !empty($users)) {
             // Events from a number of users.
             list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED);
             $filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)";
@@ -112,7 +125,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
         // Boolean false (no users at all): We don't need to do anything.
 
         // Group filter.
-        if ((is_array($groups) && !empty($groups)) or is_numeric($groups)) {
+        if (is_array($groups) && !empty($groups)) {
             // Events from a number of groups.
             list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED);
             $filters[] = "e.groupid $insqlgroups";
@@ -124,7 +137,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
         // Boolean false (no groups at all): We don't need to do anything.
 
         // Course filter.
-        if ((is_array($courses) && !empty($courses)) or is_numeric($courses)) {
+        if (is_array($courses) && !empty($courses)) {
             list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED);
             $filters[] = "(e.groupid = 0 AND e.courseid $insqlcourses)";
             $params = array_merge($params, $inparamscourses);
@@ -134,7 +147,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
         }
 
         // Category filter.
-        if ((is_array($categories) && !empty($categories)) or is_numeric($categories)) {
+        if (is_array($categories) && !empty($categories)) {
             list($insqlcategories, $inparamscategories) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED);
             $filters[] = "(e.groupid = 0 AND e.courseid = 0 AND e.categoryid $insqlcategories)";
             $params = array_merge($params, $inparamscategories);
@@ -168,54 +181,81 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
         // Build SQL subquery and conditions for filtered events based on priorities.
         $subquerywhere = '';
         $subqueryconditions = [];
-
-        // Get the user's courses. Otherwise, get the default courses being shown by the calendar.
-        $usercourses = calendar_get_default_courses();
-
-        // Set calendar filters.
-        list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true);
         $subqueryparams = [];
-
-        // Flag to indicate whether the query needs to exclude group overrides.
-        $viewgroupsonly = false;
-
-        if ($user) {
-            // Set filter condition for the user's events.
-            $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)";
-            $subqueryparams['user'] = $user;
-
-            foreach ($usercourses as $courseid) {
-                if (has_capability('moodle/site:accessallgroups', \context_course::instance($courseid))) {
-                    $usergroupmembership = groups_get_all_groups($courseid, $user, 0, 'g.id');
-                    if (count($usergroupmembership) == 0) {
-                        $viewgroupsonly = true;
-                        break;
+        $allusercourses = [];
+
+        if (is_array($users) && !empty($users)) {
+            $userrecords = $DB->get_records_sql("SELECT * FROM {user} WHERE id $insqlusers", $inparamsusers);
+            foreach ($userrecords as $userrecord) {
+                // Get the user's courses. Otherwise, get the default courses being shown by the calendar.
+                $usercourses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce',
+                        false, $userrecord->id);
+
+                // Set calendar filters.
+                list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true, $userrecord);
+
+                $allusercourses = array_merge($allusercourses, $usercourses);
+
+                // Flag to indicate whether the query needs to exclude group overrides.
+                $viewgroupsonly = false;
+
+                if ($user) {
+                    // Set filter condition for the user's events.
+                    // Even though $user is a single scalar, we still use get_in_or_equal() because we are inside a loop.
+                    list($inusers, $inuserparams) = $DB->get_in_or_equal($user, SQL_PARAMS_NAMED);
+                    $subqueryconditions[] = "(ev.userid $inusers AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)";
+                    $subqueryparams = array_merge($subqueryparams, $inuserparams);
+
+                    foreach ($usercourses as $courseid) {
+                        if (has_capability('moodle/site:accessallgroups', \context_course::instance($courseid), $userrecord)) {
+                            $usergroupmembership = groups_get_all_groups($courseid, $user, 0, 'g.id');
+                            if (count($usergroupmembership) == 0) {
+                                $viewgroupsonly = true;
+                                break;
+                            }
+                        }
                     }
                 }
+
+                // Set filter condition for the user's group events.
+                if ($usergroups === true || $viewgroupsonly) {
+                    // Fetch group events, but not group overrides.
+                    $subqueryconditions[] = "(ev.groupid != 0 AND ev.eventtype = 'group')";
+                } else if (!empty($usergroups)) {
+                    // Fetch group events and group overrides.
+                    list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED);
+                    $subqueryconditions[] = "(ev.groupid $inusergroups)";
+                    $subqueryparams = array_merge($subqueryparams, $inusergroupparams);
+                }
+            }
+        } else if ($users === true) {
+            // Events from ALL users.
+            $subqueryconditions[] = "(ev.userid != 0 AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)";
+
+            if (is_array($groups)) {
+                // Events from a number of groups.
+                list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED);
+                $subqueryconditions[] = "ev.groupid $insqlgroups";
+                $subqueryparams = array_merge($subqueryparams, $inparamsgroups);
+            } else if ($groups === true) {
+                // Events from ALL groups.
+                $subqueryconditions[] = "ev.groupid != 0";
             }
-        }
 
-        // Set filter condition for the user's group events.
-        if ($usergroups === true || $viewgroupsonly) {
-            // Fetch group events, but not group overrides.
-            $subqueryconditions[] = "(ev.groupid != 0 AND ev.eventtype = 'group')";
-        } else if (!empty($usergroups)) {
-            // Fetch group events and group overrides.
-            list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED);
-            $subqueryconditions[] = "(ev.groupid $inusergroups)";
-            $subqueryparams = array_merge($subqueryparams, $inusergroupparams);
+            if ($courses === true) {
+                // ALL course events. It's not needed to worry about users' access as $users = true.
+                $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid != 0 AND ev.categoryid = 0)";
+            }
         }
 
         // Get courses to be used for the subquery.
         $subquerycourses = [];
         if (is_array($courses)) {
             $subquerycourses = $courses;
-        } else if (is_numeric($courses)) {
-            $subquerycourses[] = $courses;
         }
         // Merge with user courses, if necessary.
-        if (!empty($usercourses)) {
-            $subquerycourses = array_merge($subquerycourses, $usercourses);
+        if (!empty($allusercourses)) {
+            $subquerycourses = array_merge($subquerycourses, $allusercourses);
             // Make sure we remove duplicate values.
             $subquerycourses = array_unique($subquerycourses);
         }
index 10fa760..80e60cd 100644 (file)
@@ -865,15 +865,31 @@ class core_calendar_external extends external_api {
         self::validate_context($context);
         parse_str($params['formdata'], $data);
 
+        $eventtype = isset($data['eventtype']) ? $data['eventtype'] : null;
+        $coursekey = ($eventtype == 'group') ? 'groupcourseid' : 'courseid';
+        $courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null;
+        $editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context);
+        $formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid];
+        if ($courseid) {
+            require_once($CFG->libdir . '/grouplib.php');
+            $groupcoursedata = groups_get_course_data($courseid);
+            if (!empty($groupcoursedata->groups)) {
+                $formoptions['groups'] = [];
+                foreach ($groupcoursedata->groups as $groupid => $groupdata) {
+                    $formoptions['groups'][$groupid] = $groupdata->name;
+                }
+            }
+        }
+
         if (!empty($data['id'])) {
             $eventid = clean_param($data['id'], PARAM_INT);
             $legacyevent = calendar_event::load($eventid);
             $legacyevent->count_repeats();
-            $formoptions = ['event' => $legacyevent];
+            $formoptions['event'] = $legacyevent;
             $mform = new update_event_form(null, $formoptions, 'post', '', null, true, $data);
         } else {
             $legacyevent = null;
-            $mform = new create_event_form(null, null, 'post', '', null, true, $data);
+            $mform = new create_event_form(null, $formoptions, 'post', '', null, true, $data);
         }
 
         if ($validateddata = $mform->get_data()) {
index 60d3d0f..c3c7aac 100644 (file)
@@ -1070,7 +1070,7 @@ class calendar_information {
             $category = (\coursecat::get($course->category, MUST_EXIST, true))->get_db_record();
         } else if (!empty($categoryid)) {
             $course = get_site();
-            $courses = calendar_get_default_courses();
+            $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce');
 
             // Filter available courses to those within this category or it's children.
             $ids = [$categoryid];
@@ -1084,7 +1084,7 @@ class calendar_information {
             $calendar->context = context_coursecat::instance($categoryid);
         } else {
             $course = get_site();
-            $courses = calendar_get_default_courses();
+            $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce');
             $category = null;
 
             $calendar->context = context_system::instance();
@@ -2039,34 +2039,29 @@ function calendar_events_by_day($events, $month, $year, &$eventsbyday, &$duratio
  *
  * @param array $courseeventsfrom An array of courses to load calendar events for
  * @param bool $ignorefilters specify the use of filters, false is set as default
+ * @param stdClass $user The user object. This defaults to the global $USER object.
  * @return array An array of courses, groups, and user to load calendar events for based upon filters
  */
-function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) {
-    global $USER, $CFG;
+function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false, stdClass $user = null) {
+    global $CFG, $USER;
 
-    // For backwards compatability we have to check whether the courses array contains
-    // just id's in which case we need to load course objects.
-    $coursestoload = array();
-    foreach ($courseeventsfrom as $id => $something) {
-        if (!is_object($something)) {
-            $coursestoload[] = $id;
-            unset($courseeventsfrom[$id]);
-        }
+    if (is_null($user)) {
+        $user = $USER;
     }
 
     $courses = array();
-    $user = false;
+    $userid = false;
     $group = false;
 
     // Get the capabilities that allow seeing group events from all groups.
     $allgroupscaps = array('moodle/site:accessallgroups', 'moodle/calendar:manageentries');
 
-    $isloggedin = isloggedin();
+    $isvaliduser = !empty($user->id);
 
-    if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_COURSE)) {
+    if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_COURSE, $user)) {
         $courses = array_keys($courseeventsfrom);
     }
-    if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_GLOBAL)) {
+    if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_GLOBAL, $user)) {
         $courses[] = SITEID;
     }
     $courses = array_unique($courses);
@@ -2080,11 +2075,11 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) {
         $courses[] = SITEID;
     }
 
-    if ($ignorefilters || ($isloggedin && calendar_show_event_type(CALENDAR_EVENT_USER))) {
-        $user = $USER->id;
+    if ($ignorefilters || ($isvaliduser && calendar_show_event_type(CALENDAR_EVENT_USER, $user))) {
+        $userid = $user->id;
     }
 
-    if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP) || $ignorefilters)) {
+    if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP, $user) || $ignorefilters)) {
 
         if (count($courseeventsfrom) == 1) {
             $course = reset($courseeventsfrom);
@@ -2096,16 +2091,16 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) {
         if ($group === false) {
             if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, \context_system::instance())) {
                 $group = true;
-            } else if ($isloggedin) {
+            } else if ($isvaliduser) {
                 $groupids = array();
                 foreach ($courseeventsfrom as $courseid => $course) {
                     // If the user is an editing teacher in there.
-                    if (!empty($USER->groupmember[$course->id])) {
+                    if (!empty($user->groupmember[$course->id])) {
                         // We've already cached the users groups for this course so we can just use that.
-                        $groupids = array_merge($groupids, $USER->groupmember[$course->id]);
+                        $groupids = array_merge($groupids, $user->groupmember[$course->id]);
                     } else if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
                         // If this course has groups, show events from all of those related to the current user.
-                        $coursegroups = groups_get_user_groups($course->id, $USER->id);
+                        $coursegroups = groups_get_user_groups($course->id, $user->id);
                         $groupids = array_merge($groupids, $coursegroups['0']);
                     }
                 }
@@ -2119,7 +2114,7 @@ function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) {
         $courses = false;
     }
 
-    return array($courses, $group, $user);
+    return array($courses, $group, $userid);
 }
 
 /**
@@ -2317,20 +2312,25 @@ function calendar_delete_event_allowed($event) {
  *
  * @param int $courseid (optional) If passed, an additional course can be returned for admins (the current course).
  * @param string $fields Comma separated list of course fields to return.
- * @param bool $canmanage If true, this will return the list of courses the current user can create events in, rather
+ * @param bool $canmanage If true, this will return the list of courses the user can create events in, rather
  *                        than the list of courses they see events from (an admin can always add events in a course
  *                        calendar, even if they are not enrolled in the course).
+ * @param int $userid (optional) The user which this function returns the default courses for.
+ *                        By default the current user.
  * @return array $courses Array of courses to display
  */
-function calendar_get_default_courses($courseid = null, $fields = '*', $canmanage=false) {
-    global $CFG, $DB;
+function calendar_get_default_courses($courseid = null, $fields = '*', $canmanage = false, int $userid = null) {
+    global $CFG, $USER;
 
-    if (!isloggedin()) {
-        return array();
+    if (!$userid) {
+        if (!isloggedin()) {
+            return array();
+        }
+        $userid = $USER->id;
     }
 
-    if (has_capability('moodle/calendar:manageentries', context_system::instance()) &&
-            (!empty($CFG->calendar_adminseesall) || $canmanage)) {
+    if ((!empty($CFG->calendar_adminseesall) || $canmanage) &&
+            has_capability('moodle/calendar:manageentries', context_system::instance(), $userid)) {
 
         // Add a c. prefix to every field as expected by get_courses function.
         $fieldlist = explode(',', $fields);
@@ -2340,11 +2340,11 @@ function calendar_get_default_courses($courseid = null, $fields = '*', $canmanag
         }, $fieldlist);
         $courses = get_courses('all', 'c.shortname', implode(',', $prefixedfields));
     } else {
-        $courses = enrol_get_my_courses($fields);
+        $courses = enrol_get_users_courses($userid, true, $fields);
     }
 
     if ($courseid && $courseid != SITEID) {
-        if (empty($courses[$courseid]) && has_capability('moodle/calendar:manageentries', context_system::instance())) {
+        if (empty($courses[$courseid]) && has_capability('moodle/calendar:manageentries', context_system::instance(), $userid)) {
             // Allow a site admin to see calendars from courses he is not enrolled in.
             // This will come from $COURSE.
             $courses[$courseid] = get_course($courseid);
@@ -2570,76 +2570,6 @@ function calendar_get_allowed_types(&$allowed, $course = null, $groups = null, $
     }
 }
 
-/**
- * Get all of the allowed types for all of the courses and groups
- * the logged in user belongs to.
- *
- * The returned array will optionally have 5 keys:
- *      'user' : true if the logged in user can create user events
- *      'site' : true if the logged in user can create site events
- *      'category' : array of course categories that the user can create events for
- *      'course' : array of courses that the user can create events for
- *      'group': array of groups that the user can create events for
- *      'groupcourses' : array of courses that the groups belong to (can
- *                       be different from the list in 'course'.
- *
- * @return array The array of allowed types.
- */
-function calendar_get_all_allowed_types() {
-    global $CFG, $USER, $DB;
-
-    require_once($CFG->libdir . '/enrollib.php');
-
-    $types = [];
-
-    $allowed = new stdClass();
-
-    calendar_get_allowed_types($allowed);
-
-    if ($allowed->user) {
-        $types['user'] = true;
-    }
-
-    if ($allowed->site) {
-        $types['site'] = true;
-    }
-
-    if (coursecat::has_manage_capability_on_any()) {
-        $types['category'] = coursecat::make_categories_list('moodle/category:manage');
-    }
-
-    // This function warms the context cache for the course so the calls
-    // to load the course context in calendar_get_allowed_types don't result
-    // in additional DB queries.
-    $courses = calendar_get_default_courses(null, 'id, groupmode, groupmodeforce', true);
-
-    // We want to pre-fetch all of the groups for each course in a single
-    // query to avoid calendar_get_allowed_types from hitting the DB for
-    // each separate course.
-    $groups = groups_get_all_groups_for_courses($courses);
-
-    foreach ($courses as $course) {
-        $coursegroups = isset($groups[$course->id]) ? $groups[$course->id] : null;
-        calendar_get_allowed_types($allowed, $course, $coursegroups);
-
-        if (!empty($allowed->courses)) {
-            $types['course'][$course->id] = $course;
-        }
-
-        if (!empty($allowed->groups)) {
-            $types['groupcourses'][$course->id] = $course;
-
-            if (!isset($types['group'])) {
-                $types['group'] = array_values($allowed->groups);
-            } else {
-                $types['group'] = array_merge($types['group'], array_values($allowed->groups));
-            }
-        }
-    }
-
-    return $types;
-}
-
 /**
  * See if user can add calendar entries at all used to print the "New Event" button.
  *
@@ -3328,6 +3258,11 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses,
         return $param;
     }, [$users, $groups, $courses, $categories]);
 
+    // If a single user is provided, we can use that for capability checks.
+    // Otherwise current logged in user is used - See MDL-58768.
+    if (is_array($userparam) && count($userparam) == 1) {
+        \core_calendar\local\event\container::set_requesting_user($userparam[0]);
+    }
     $mapper = \core_calendar\local\event\container::get_event_mapper();
     $events = \core_calendar\local\api::get_events(
         $tstart,
@@ -3508,18 +3443,18 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
  */
 function calendar_output_fragment_event_form($args) {
     global $CFG, $OUTPUT, $USER;
-
+    require_once($CFG->libdir . '/grouplib.php');
     $html = '';
     $data = [];
     $eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
     $starttime = isset($args['starttime']) ? clean_param($args['starttime'], PARAM_INT) : null;
-    $courseid = isset($args['courseid']) ? clean_param($args['courseid'], PARAM_INT) : null;
+    $courseid = (isset($args['courseid']) && $args['courseid'] != SITEID) ? clean_param($args['courseid'], PARAM_INT) : null;
     $categoryid = isset($args['categoryid']) ? clean_param($args['categoryid'], PARAM_INT) : null;
     $event = null;
     $hasformdata = isset($args['formdata']) && !empty($args['formdata']);
     $context = \context_user::instance($USER->id);
     $editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context);
-    $formoptions = ['editoroptions' => $editoroptions];
+    $formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid];
     $draftitemid = 0;
 
     if ($hasformdata) {
@@ -3534,6 +3469,13 @@ function calendar_output_fragment_event_form($args) {
     }
 
     if (is_null($eventid)) {
+        if (!empty($courseid)) {
+            $groupcoursedata = groups_get_course_data($courseid);
+            $formoptions['groups'] = [];
+            foreach ($groupcoursedata->groups as $groupid => $groupdata) {
+                $formoptions['groups'][$groupid] = $groupdata->name;
+            }
+        }
         $mform = new \core_calendar\local\event\forms\create(
             null,
             $formoptions,
@@ -3545,16 +3487,19 @@ function calendar_output_fragment_event_form($args) {
         );
 
         // Let's check first which event types user can add.
-        calendar_get_allowed_types($allowed, $courseid);
+        $eventtypes = calendar_get_allowed_event_types($courseid);
 
         // If the user is on course context and is allowed to add course events set the event type default to course.
-        if ($courseid != SITEID && !empty($allowed->courses)) {
+        if ($courseid != SITEID && !empty($eventtypes['course'])) {
             $data['eventtype'] = 'course';
             $data['courseid'] = $courseid;
             $data['groupcourseid'] = $courseid;
-        } else if (!empty($categoryid) && !empty($allowed->category)) {
+        } else if (!empty($categoryid) && !empty($eventtypes['category'])) {
             $data['eventtype'] = 'category';
             $data['categoryid'] = $categoryid;
+        } else if (!empty($groupcoursedata) && !empty($eventtypes['group'])) {
+            $data['groupcourseid'] = $courseid;
+            $data['groups'] = $groupcoursedata->groups;
         }
         $mform->set_data($data);
     } else {
@@ -3564,6 +3509,15 @@ function calendar_output_fragment_event_form($args) {
         $data = array_merge((array) $eventdata, $data);
         $event->count_repeats();
         $formoptions['event'] = $event;
+
+        if (!empty($event->courseid)) {
+            $groupcoursedata = groups_get_course_data($event->courseid);
+            $formoptions['groups'] = [];
+            foreach ($groupcoursedata->groups as $groupid => $groupdata) {
+                $formoptions['groups'][$groupid] = $groupdata->name;
+            }
+        }
+
         $data['description']['text'] = file_prepare_draft_area(
             $draftitemid,
             $event->context->id,
@@ -3692,3 +3646,175 @@ function calendar_is_valid_eventtype($type) {
     ];
     return in_array($type, $validtypes);
 }
+
+/**
+ * Get event types the user can create event based on categories, courses and groups
+ * the logged in user belongs to.
+ *
+ * @param int|null $courseid The course id.
+ * @return array The array of allowed types.
+ */
+function calendar_get_allowed_event_types(int $courseid = null) {
+    global $DB, $CFG, $USER;
+
+    $types = [
+        'user' => false,
+        'site' => false,
+        'course' => false,
+        'group' => false,
+        'category' => false
+    ];
+
+    if (!empty($courseid) && $courseid != SITEID) {
+        $context = \context_course::instance($courseid);
+        $groups = groups_get_all_groups($courseid);
+
+        $types['user'] = has_capability('moodle/calendar:manageownentries', $context);
+
+        if (has_capability('moodle/calendar:manageentries', $context) || !empty($CFG->calendar_adminseesall)) {
+            $types['course'] = true;
+
+            $types['group'] = (!empty($groups) && has_capability('moodle/site:accessallgroups', $context))
+                || array_filter($groups, function($group) use ($USER) {
+                    return groups_is_member($group->id);
+                });
+        } else if (has_capability('moodle/calendar:managegroupentries', $context)) {
+            $types['group'] = (!empty($groups) && has_capability('moodle/site:accessallgroups', $context))
+                || array_filter($groups, function($group) use ($USER) {
+                    return groups_is_member($group->id);
+                });
+        }
+    }
+
+    if (has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID))) {
+        $types['site'] = true;
+    }
+
+    if (has_capability('moodle/calendar:manageownentries', \context_system::instance())) {
+        $types['user'] = true;
+    }
+    if (coursecat::has_manage_capability_on_any()) {
+        $types['category'] = true;
+    }
+
+    // We still don't know if the user can create group and course events, so iterate over the courses to find out
+    // if the user has capabilities in one of the courses.
+    if ($types['course'] == false || $types['group'] == false) {
+        if ($CFG->calendar_adminseesall && has_capability('moodle/calendar:manageentries', context_system::instance())) {
+            $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
+                      FROM {course} c
+                      JOIN {context} ctx ON ctx.contextlevel = ? AND ctx.instanceid = c.id
+                     WHERE c.id IN (
+                            SELECT DISTINCT courseid FROM {groups}
+                        )";
+            $courseswithgroups = $DB->get_recordset_sql($sql, [CONTEXT_COURSE]);
+            foreach ($courseswithgroups as $course) {
+                context_helper::preload_from_record($course);
+                $context = context_course::instance($course->id);
+
+                if (has_capability('moodle/calendar:manageentries', $context)) {
+                    if (has_any_capability(['moodle/site:accessallgroups', 'moodle/calendar:managegroupentries'], $context)) {
+                        // The user can manage group entries or access any group.
+                        $types['group'] = true;
+                        $types['course'] = true;
+                        break;
+                    }
+                }
+            }
+            $courseswithgroups->close();
+
+            if (false === $types['course']) {
+                // Course is still not confirmed. There may have been no courses with a group in them.
+                $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
+                $sql = "SELECT
+                            c.id, c.visible, {$ctxfields}
+                        FROM {course}
+                        JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
+                $params = [
+                    'contextlevel' => CONTEXT_COURSE,
+                ];
+                $courses = $DB->get_recordset_sql($sql, $params);
+                foreach ($courses as $course) {
+                    context_helper::preload_from_record($course);
+                    $context = context_course::instance($course->id);
+                    if (has_capability('moodle/calendar:manageentries', $context)) {
+                        $types['course'] = true;
+                        break;
+                    }
+                }
+                $courses->close();
+            }
+
+        } else {
+            $courses = calendar_get_default_courses(null, 'id');
+            if (empty($courses)) {
+                return $types;
+            }
+
+            $courseids = array_map(function($c) {
+                return $c->id;
+            }, $courses);
+
+            // Check whether the user has access to create events within courses which have groups.
+            list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+            $sql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
+                      FROM {course} c
+                      JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id
+                     WHERE c.id $insql
+                       AND c.id IN (SELECT DISTINCT courseid FROM {groups})";
+            $params['contextlevel'] = CONTEXT_COURSE;
+            $courseswithgroups = $DB->get_recordset_sql($sql, $params);
+            foreach ($courseswithgroups as $coursewithgroup) {
+                context_helper::preload_from_record($coursewithgroup);
+                $context = context_course::instance($coursewithgroup->id);
+
+                if (has_capability('moodle/calendar:manageentries', $context)) {
+                    // The user has access to manage calendar entries for the whole course.
+                    // This includes groups if they have the accessallgroups capability.
+                    $types['course'] = true;
+                    if (has_capability('moodle/site:accessallgroups', $context)) {
+                        // The user also has access to all groups so they can add calendar entries to any group.
+                        // The manageentries capability overrides the managegroupentries capability.
+                        $types['group'] = true;
+                        break;
+                    }
+
+                    if (empty($types['group']) && has_capability('moodle/calendar:managegroupentries', $context)) {
+                        // The user has the managegroupentries capability.
+                        // If they have access to _any_ group, then they can create calendar entries within that group.
+                        $types['group'] = !empty(groups_get_all_groups($coursewithgroup->id, $USER->id));
+                    }
+                }
+
+                // Okay, course and group event types are allowed, no need to keep the loop iteration.
+                if ($types['course'] == true && $types['group'] == true) {
+                    break;
+                }
+            }
+            $courseswithgroups->close();
+
+            if (false === $types['course']) {
+                list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+                $contextsql = "SELECT c.id, " . context_helper::get_preload_record_columns_sql('ctx') . "
+                                FROM {course} c
+                                JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id
+                                WHERE c.id $insql";
+                $params['contextlevel'] = CONTEXT_COURSE;
+                $contextrecords = $DB->get_recordset_sql($contextsql, $params);
+                foreach ($contextrecords as $course) {
+                    context_helper::preload_from_record($course);
+                    $coursecontext = context_course::instance($course->id);
+                    if (has_capability('moodle/calendar:manageentries', $coursecontext)
+                            && ($courseid == $course->id || empty($courseid))) {
+                        $types['course'] = true;
+                        break;
+                    }
+                }
+                $contextrecords->close();
+            }
+
+        }
+    }
+
+    return $types;
+}
index 75ddc09..87289a6 100644 (file)
@@ -61,7 +61,7 @@ if (!calendar_user_can_add_event($course)) {
     print_error('errorcannotimport', 'calendar');
 }
 
-$form = new \core_calendar\local\event\forms\managesubscriptions();
+$form = new \core_calendar\local\event\forms\managesubscriptions(null, ['courseid' => $course->id]);
 $form->set_data(array(
     'course' => $course->id
 ));
@@ -105,26 +105,26 @@ if (!empty($formdata)) {
     }
 }
 
-$types = calendar_get_all_allowed_types();
+$types = calendar_get_allowed_event_types($courseid);
 
 $searches = [];
 $params = [];
 
 $usedefaultfilters = true;
-if (!empty($courseid) && $courseid == SITEID && isset($types['site'])) {
+if (!empty($courseid) && $courseid == SITEID && !empty($types['site'])) {
     $searches[] = "(eventtype = 'site')";
     $searches[] = "(eventtype = 'user' AND userid = :userid)";
     $params['userid'] = $USER->id;
     $usedefaultfilters = false;
 }
 
-if (!empty($courseid) && isset($types['course']) && array_key_exists($courseid, $types['course'])) {
+if (!empty($courseid) && !empty($types['course'])) {
     $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid = :courseid)";
     $params += ['courseid' => $courseid];
     $usedefaultfilters = false;
 }
 
-if (!empty($categoryid) && isset($types['category']) && array_key_exists($categoryid, $types['category'])) {
+if (!empty($categoryid) && !empty($types['category'])) {
     $searches[] = "(eventtype = 'category' AND categoryid = :categoryid)";
     $params += ['categoryid' => $categoryid];
     $usedefaultfilters = false;
@@ -134,19 +134,27 @@ if ($usedefaultfilters) {
     $searches[] = "(eventtype = 'user' AND userid = :userid)";
     $params['userid'] = $USER->id;
 
-    if (isset($types['site'])) {
+    if (!empty($types['site'])) {
         $searches[] = "(eventtype = 'site' AND courseid  = :siteid)";
         $params += ['siteid' => SITEID];
     }
 
-    if (isset($types['course'])) {
-        list($courseinsql, $courseparams) = $DB->get_in_or_equal(array_keys($types['course']), SQL_PARAMS_NAMED, 'course');
-        $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid {$courseinsql})";
-        $params += $courseparams;
+    if (!empty($types['course'])) {
+        $courses = calendar_get_default_courses(null, 'id', true);
+        if (!empty($courses)) {
+            $courseids = array_map(function ($c) {
+                return $c->id;
+            }, $courses);
+
+            list($courseinsql, $courseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, 'course');
+            $searches[] = "((eventtype = 'course' OR eventtype = 'group') AND courseid {$courseinsql})";
+            $params += $courseparams;
+        }
     }
 
-    if (isset($types['category'])) {
-        list($categoryinsql, $categoryparams) = $DB->get_in_or_equal(array_keys($types['category']), SQL_PARAMS_NAMED, 'category');
+    if (!empty($types['category'])) {
+        list($categoryinsql, $categoryparams) = $DB->get_in_or_equal(
+                array_keys(\coursecat::make_categories_list('moodle/category:manage')), SQL_PARAMS_NAMED, 'category');
         $searches[] = "(eventtype = 'category' AND categoryid {$categoryinsql})";
         $params += $categoryparams;
     }
index 5603daa..5483edb 100644 (file)
@@ -170,8 +170,8 @@ Feature: Perform basic calendar functionality
     And I click on "New event" "button"
     When I click on "Save" "button"
     Then I should see "Required"
-    And I am on site homepage
-    And I follow "Calendar"
+    And I am on homepage
+    And I follow "This month"
     And I click on "New event" "button"
     And I set the field "Type of event" to "Course"
     When I click on "Save" "button"
index 28b42d3..ed623d0 100644 (file)
@@ -1989,12 +1989,12 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $this->resetAfterTest(true);
         $this->setUser($user);
 
-        $this->expectException('moodle_exception');
-
-        external_api::clean_returnvalue(
+        $result = external_api::clean_returnvalue(
             core_calendar_external::submit_create_update_form_returns(),
             core_calendar_external::submit_create_update_form($querystring)
         );
+
+        $this->assertTrue($result['validationerror']);
     }
 
     /**
@@ -2027,7 +2027,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'minute' => 0,
             ],
             'eventtype' => 'group',
-            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupid' => $group->id,
             'groupcourseid' => $course->id,
             'description' => [
                 'text' => '',
@@ -2100,7 +2100,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'minute' => 0,
             ],
             'eventtype' => 'group',
-            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupid' => $group->id,
             'groupcourseid' => $course->id,
             'description' => [
                 'text' => '',
@@ -2174,7 +2174,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'minute' => 0,
             ],
             'eventtype' => 'group',
-            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupid' => $group->id,
             'groupcourseid' => $course->id,
             'description' => [
                 'text' => '',
@@ -2248,7 +2248,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
                 'minute' => 0,
             ],
             'eventtype' => 'group',
-            'groupid' => "{$course->id}-{$group->id}", // The form format.
+            'groupid' => $group->id,
             'groupcourseid' => $course->id,
             'description' => [
                 'text' => '',
index bf8b6ca..35809ec 100644 (file)
@@ -418,65 +418,112 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->assertCount(3, $events);
     }
 
-    public function test_calendar_get_all_allowed_types_no_types() {
+    public function test_calendar_get_default_courses() {
+        global $USER, $CFG;
+
+        $this->resetAfterTest(true);
+
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
-        $systemcontext = context_system::instance();
-        $sitecontext = context_course::instance(SITEID);
-        $roleid = $generator->create_role();
+        $course1 = $generator->create_course();
+        $course2 = $generator->create_course();
+        $course3 = $generator->create_course();
+        $context = context_course::instance($course1->id);
 
-        $generator->role_assign($roleid, $user->id, $systemcontext->id);
-        $generator->role_assign($roleid, $user->id, $sitecontext->id);
-        $this->setUser($user);
+        $this->setAdminUser();
+        $admin = clone $USER;
 
-        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $sitecontext, true);
-        assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $systemcontext, true);
+        $teacher = $generator->create_user();
+        $generator->enrol_user($teacher->id, $course1->id, 'teacher');
+        $generator->enrol_user($admin->id, $course1->id, 'teacher');
 
-        $types = calendar_get_all_allowed_types();
-        $this->assertEmpty($types);
-    }
+        $CFG->calendar_adminseesall = false;
 
-    public function test_calendar_get_all_allowed_types_user() {
-        $generator = $this->getDataGenerator();
-        $user = $generator->create_user();
-        $context = context_system::instance();
-        $roleid = $generator->create_role();
+        $courses = calendar_get_default_courses();
+        // Only enrolled in one course.
+        $this->assertCount(1, $courses);
+        $courses = calendar_get_default_courses($course2->id);
+        // Enrolled course + current course.
+        $this->assertCount(2, $courses);
+        $CFG->calendar_adminseesall = true;
+        $courses = calendar_get_default_courses();
+        // All courses + SITE.
+        $this->assertCount(4, $courses);
+        $courses = calendar_get_default_courses($course2->id);
+        // All courses + SITE.
+        $this->assertCount(4, $courses);
 
-        $generator->role_assign($roleid, $user->id, $context->id);
-        $this->setUser($user);
+        $this->setUser($teacher);
+
+        $CFG->calendar_adminseesall = false;
+
+        $courses = calendar_get_default_courses();
+        // Only enrolled in one course.
+        $this->assertCount(1, $courses);
+        $courses = calendar_get_default_courses($course2->id);
+        // Enrolled course only (ignore current).
+        $this->assertCount(1, $courses);
+        // This setting should not affect teachers.
+        $CFG->calendar_adminseesall = true;
+        $courses = calendar_get_default_courses();
+        // Only enrolled in one course.
+        $this->assertCount(1, $courses);
+        $courses = calendar_get_default_courses($course2->id);
+        // Enrolled course only (ignore current).
+        $this->assertCount(1, $courses);
 
-        assign_capability('moodle/calendar:manageownentries', CAP_ALLOW, $roleid, $context, true);
+        // Now, log out and test again.
+        $this->setUser();
 
-        $types = calendar_get_all_allowed_types();
-        $this->assertTrue($types['user']);
+        $CFG->calendar_adminseesall = false;
 
-        assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $context, true);
+        $courses = calendar_get_default_courses(null, '*', false, $teacher->id);
+        // Only enrolled in one course.
+        $this->assertCount(1, $courses);
+        $courses = calendar_get_default_courses($course2->id, '*', false, $teacher->id);
+        // Enrolled course only (ignore current).
+        $this->assertCount(1, $courses);
+        // This setting should not affect teachers.
+        $CFG->calendar_adminseesall = true;
+        $courses = calendar_get_default_courses(null, '*', false, $teacher->id);
+        // Only enrolled in one course.
+        $this->assertCount(1, $courses);
+        $courses = calendar_get_default_courses($course2->id, '*', false, $teacher->id);
+        // Enrolled course only (ignore current).
+        $this->assertCount(1, $courses);
 
-        $types = calendar_get_all_allowed_types();
-        $this->assertArrayNotHasKey('user', $types);
     }
 
-    public function test_calendar_get_all_allowed_types_site() {
+    /**
+     * Confirm that the skip events flag causes the calendar_get_view function
+     * to avoid querying for the calendar events.
+     */
+    public function test_calendar_get_view_skip_events() {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
-        $context = context_course::instance(SITEID);
-        $roleid = $generator->create_role();
+        $skipnavigation = true;
+        $skipevents = true;
+        $event = create_event([
+            'eventtype' => 'user',
+            'userid' => $user->id
+        ]);
 
-        $generator->role_assign($roleid, $user->id, $context->id);
         $this->setUser($user);
+        $calendar = \calendar_information::create(time() - 10, SITEID, null);
 
-        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
-
-        $types = calendar_get_all_allowed_types();
-        $this->assertTrue($types['site']);
+        list($data, $template) = calendar_get_view($calendar, 'day', $skipnavigation, $skipevents);
+        $this->assertEmpty($data->events);
 
-        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+        $skipevents = false;
+        list($data, $template) = calendar_get_view($calendar, 'day', $skipnavigation, $skipevents);
 
-        $types = calendar_get_all_allowed_types();
-        $this->assertArrayNotHasKey('site', $types);
+        $this->assertEquals($event->id, $data->events[0]->id);
     }
 
-    public function test_calendar_get_all_allowed_types_course() {
+    public function test_calendar_get_allowed_event_types_course() {
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
         $course1 = $generator->create_course(); // Has capability.
@@ -504,35 +551,18 @@ class core_calendar_lib_testcase extends advanced_testcase {
 
         // The user only has the correct capability in course 1 so that is the only
         // one that should be in the results.
-        $types = calendar_get_all_allowed_types();
-        $typecourses = $types['course'];
-        $this->assertCount(1, $typecourses);
-        $this->assertEquals($course1->id, $typecourses[$course1->id]->id);
+        $types = calendar_get_allowed_event_types($course1->id);
+        $this->assertTrue($types['course']);
 
-        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context2, true);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context1, true);
 
         // The user only now has the correct capability in both course 1 and 2 so we
         // expect both to be in the results.
-        $types = calendar_get_all_allowed_types();
-        $typecourses = $types['course'];
-        // Sort the results by id ascending to ensure the test is consistent
-        // and repeatable.
-        usort($typecourses, function($a, $b) {
-            $aid = $a->id;
-            $bid = $b->id;
-
-            if ($aid == $bid) {
-                return 0;
-            }
-            return ($aid < $bid) ? -1 : 1;
-        });
-
-        $this->assertCount(2, $typecourses);
-        $this->assertEquals($course1->id, $typecourses[0]->id);
-        $this->assertEquals($course2->id, $typecourses[1]->id);
+        $types = calendar_get_allowed_event_types($course3->id);
+        $this->assertFalse($types['course']);
     }
 
-    public function test_calendar_get_all_allowed_types_group_no_groups() {
+    public function test_calendar_get_allowed_event_types_group_no_acces_to_diff_groups() {
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
         $course = $generator->create_course();
@@ -545,224 +575,201 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $this->setUser($user);
 
         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
 
-        // The user has the correct capability in the course but there are
-        // no groups so we shouldn't see a group type.
-        $types = calendar_get_all_allowed_types();
-        $typecourses = $types['course'];
-        $this->assertCount(1, $typecourses);
-        $this->assertEquals($course->id, $typecourses[$course->id]->id);
-        $this->assertArrayNotHasKey('group', $types);
-        $this->assertArrayNotHasKey('groupcourses', $types);
+        // The user has the correct capability in the course but they aren't a member
+        // of any of the groups and don't have the accessallgroups capability.
+        $types = calendar_get_allowed_event_types($course->id);
+        $this->assertTrue($types['course']);
+        $this->assertFalse($types['group']);
     }
 
-    public function test_calendar_get_all_allowed_types_group_no_acces_to_diff_groups() {
+    public function test_calendar_get_allowed_event_types_group_no_groups() {
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
         $course = $generator->create_course();
         $context = context_course::instance($course->id);
-        $group1 = $generator->create_group(array('courseid' => $course->id));
-        $group2 = $generator->create_group(array('courseid' => $course->id));
         $roleid = $generator->create_role();
-
         $generator->enrol_user($user->id, $course->id, 'student');
         $generator->role_assign($roleid, $user->id, $context->id);
-
         $this->setUser($user);
-
         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
-        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
-
-        // The user has the correct capability in the course but they aren't a member
-        // of any of the groups and don't have the accessallgroups capability.
-        $types = calendar_get_all_allowed_types();
-        $typecourses = $types['course'];
-        $this->assertCount(1, $typecourses);
-        $this->assertEquals($course->id, $typecourses[$course->id]->id);
-        $this->assertArrayNotHasKey('group', $types);
-        $this->assertArrayNotHasKey('groupcourses', $types);
+        // The user has the correct capability in the course but there are
+        // no groups so we shouldn't see a group type.
+        $types = calendar_get_allowed_event_types($course->id);
+        $this->assertTrue($types['course']);
     }
 
-    public function test_calendar_get_all_allowed_types_group_access_all_groups() {
+    public function test_calendar_get_allowed_event_types_group_access_all_groups() {
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
         $course1 = $generator->create_course();
         $course2 = $generator->create_course();
+        $generator->create_group(array('courseid' => $course1->id));
+        $generator->create_group(array('courseid' => $course2->id));
         $context1 = context_course::instance($course1->id);
         $context2 = context_course::instance($course2->id);
-        $group1 = $generator->create_group(array('courseid' => $course1->id));
-        $group2 = $generator->create_group(array('courseid' => $course1->id));
         $roleid = $generator->create_role();
-
         $generator->enrol_user($user->id, $course1->id, 'student');
         $generator->enrol_user($user->id, $course2->id, 'student');
         $generator->role_assign($roleid, $user->id, $context1->id);
         $generator->role_assign($roleid, $user->id, $context2->id);
-
         $this->setUser($user);
-
         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context1, true);
         assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context2, true);
         assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context1, true);
         assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context2, true);
-
         // The user has the correct capability in the course and has
         // the accessallgroups capability.
-        $types = calendar_get_all_allowed_types();
-        $typecourses = $types['course'];
-        $typegroups = $types['group'];
-        $typegroupcourses = $types['groupcourses'];
-        $idascfunc = function($a, $b) {
-            $aid = $a->id;
-            $bid = $b->id;
-
-            if ($aid == $bid) {
-                return 0;
-            }
-            return ($aid < $bid) ? -1 : 1;
-        };
-        // Sort the results by id ascending to ensure the test is consistent
-        // and repeatable.
-        usort($typecourses, $idascfunc);
-        usort($typegroups, $idascfunc);
-
-        $this->assertCount(2, $typecourses);
-        $this->assertEquals($course1->id, $typecourses[0]->id);
-        $this->assertEquals($course2->id, $typecourses[1]->id);
-        $this->assertCount(1, $typegroupcourses);
-        $this->assertEquals($course1->id, $typegroupcourses[$course1->id]->id);
-        $this->assertCount(2, $typegroups);
-        $this->assertEquals($group1->id, $typegroups[0]->id);
-        $this->assertEquals($group2->id, $typegroups[1]->id);
+        $types = calendar_get_allowed_event_types($course1->id);
+        $this->assertTrue($types['group']);
     }
-
-    public function test_calendar_get_all_allowed_types_group_no_access_all_groups() {
+    public function test_calendar_get_allowed_event_types_group_no_access_all_groups() {
         $generator = $this->getDataGenerator();
         $user = $generator->create_user();
         $course = $generator->create_course();
         $context = context_course::instance($course->id);
         $group1 = $generator->create_group(array('courseid' => $course->id));
         $group2 = $generator->create_group(array('courseid' => $course->id));
-        $group3 = $generator->create_group(array('courseid' => $course->id));
         $roleid = $generator->create_role();
-
         $generator->enrol_user($user->id, $course->id, 'student');
         $generator->role_assign($roleid, $user->id, $context->id);
         $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $user->id));
         $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user->id));
-
         $this->setUser($user);
-
-        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
         assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
-
         // The user has the correct capability in the course but can't access
         // groups that they are not a member of.
-        $types = calendar_get_all_allowed_types();
-        $typegroups = $types['group'];
-        $typegroupcourses = $types['groupcourses'];
-        $idascfunc = function($a, $b) {
-            $aid = $a->id;
-            $bid = $b->id;
-
-            if ($aid == $bid) {
-                return 0;
-            }
-            return ($aid < $bid) ? -1 : 1;
-        };
-        // Sort the results by id ascending to ensure the test is consistent
-        // and repeatable.
-        usort($typegroups, $idascfunc);
-
-        $this->assertCount(1, $typegroupcourses);
-        $this->assertEquals($course->id, $typegroupcourses[$course->id]->id);
-        $this->assertCount(2, $typegroups);
-        $this->assertEquals($group1->id, $typegroups[0]->id);
-        $this->assertEquals($group2->id, $typegroups[1]->id);
+        $types = calendar_get_allowed_event_types($course->id);
+        $this->assertFalse($types['group']);
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context, true);
+        $types = calendar_get_allowed_event_types($course->id);
+        $this->assertTrue($types['group']);
     }
 
-    public function test_calendar_get_default_courses() {
-        global $USER, $CFG;
+    /**
+     * This is a setup helper function that create some users, courses, groups and group memberships.
+     * This is useful to prepare the environment for testing the calendar_set_filters function.
+     *
+     * @return array An array of ($users, $courses, $coursegroups)
+     */
+    protected function setup_test_calendar_set_filters() {
+        $generator = $this->getDataGenerator();
 
-        $this->resetAfterTest(true);
+        // Create some users.
+        $users = [];
+        $users[] = $generator->create_user();
+        $users[] = $generator->create_user();
+        $users[] = $generator->create_user();
+
+        // Create some courses.
+        $courses = [];
+        $courses[] = $generator->create_course();
+        $courses[] = $generator->create_course();
+        $courses[] = $generator->create_course();
+        $courses[] = $generator->create_course();
+
+        // Create some groups.
+        $coursegroups = [];
+        $coursegroups[$courses[0]->id] = [];
+        $coursegroups[$courses[0]->id][] = $generator->create_group(['courseid' => $courses[0]->id]);
+        $coursegroups[$courses[0]->id][] = $generator->create_group(['courseid' => $courses[0]->id]);
+        $coursegroups[$courses[2]->id] = [];
+        $coursegroups[$courses[2]->id][] = $generator->create_group(['courseid' => $courses[2]->id]);
+        $coursegroups[$courses[2]->id][] = $generator->create_group(['courseid' => $courses[2]->id]);
+        $coursegroups[$courses[3]->id] = [];
+        $coursegroups[$courses[3]->id][] = $generator->create_group(['courseid' => $courses[3]->id]);
+        $coursegroups[$courses[3]->id][] = $generator->create_group(['courseid' => $courses[3]->id]);
+
+        // Create some enrolments and group memberships.
+        $generator->enrol_user($users[0]->id, $courses[0]->id, 'student');
+        $generator->create_group_member(['groupid' => $coursegroups[$courses[0]->id][0]->id, 'userid' => $users[0]->id]);
+        $generator->enrol_user($users[1]->id, $courses[0]->id, 'student');
+        $generator->create_group_member(['groupid' => $coursegroups[$courses[0]->id][1]->id, 'userid' => $users[1]->id]);
+        $generator->enrol_user($users[0]->id, $courses[1]->id, 'student');
+        $generator->enrol_user($users[0]->id, $courses[2]->id, 'student');
+
+        return array($users, $courses, $coursegroups);
+    }
 
-        $generator = $this->getDataGenerator();
-        $user = $generator->create_user();
-        $course1 = $generator->create_course();
-        $course2 = $generator->create_course();
-        $course3 = $generator->create_course();
-        $context = context_course::instance($course1->id);
+    /**
+     * This function tests calendar_set_filters for the case when user is not logged in.
+     */
+    public function test_calendar_set_filters_not_logged_in() {
+        $this->resetAfterTest();
 
-        $this->setAdminUser();
-        $admin = clone $USER;
+        list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters();
 
-        $teacher = $generator->create_user();
-        $generator->enrol_user($teacher->id, $course1->id, 'teacher');
-        $generator->enrol_user($admin->id, $course1->id, 'teacher');
+        $defaultcourses = calendar_get_default_courses(null, '*', false, $users[0]->id);
+        list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses);
 
-        $CFG->calendar_adminseesall = false;
+        $this->assertEquals(
+                [$courses[0]->id, $courses[1]->id, $courses[2]->id, SITEID],
+                array_values($courseids),
+                '', 0.0, 10, true);
+        $this->assertFalse($groupids);
+        $this->assertFalse($userid);
+    }
 
-        $courses = calendar_get_default_courses();
-        // Only enrolled in one course.
-        $this->assertCount(1, $courses);
-        $courses = calendar_get_default_courses($course2->id);
-        // Enrolled course + current course.
-        $this->assertCount(2, $courses);
-        $CFG->calendar_adminseesall = true;
-        $courses = calendar_get_default_courses();
-        // All courses + SITE.
-        $this->assertCount(4, $courses);
-        $courses = calendar_get_default_courses($course2->id);
-        // All courses + SITE.
-        $this->assertCount(4, $courses);
+    /**
+     * This function tests calendar_set_filters for the case when no one is logged in, but a user id is provided.
+     */
+    public function test_calendar_set_filters_not_logged_in_with_user() {
+        $this->resetAfterTest();
 
-        $this->setUser($teacher);
+        list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters();
 
-        $CFG->calendar_adminseesall = false;
+        $defaultcourses = calendar_get_default_courses(null, '*', false, $users[1]->id);
+        list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false, $users[1]);
 
-        $courses = calendar_get_default_courses();
-        // Only enrolled in one course.
-        $this->assertCount(1, $courses);
-        $courses = calendar_get_default_courses($course2->id);
-        // Enrolled course only (ignore current).
-        $this->assertCount(1, $courses);
-        // This setting should not affect teachers.
-        $CFG->calendar_adminseesall = true;
-        $courses = calendar_get_default_courses();
-        // Only enrolled in one course.
-        $this->assertCount(1, $courses);
-        $courses = calendar_get_default_courses($course2->id);
-        // Enrolled course only (ignore current).
-        $this->assertCount(1, $courses);
+        $this->assertEquals(array($courses[0]->id, SITEID), array_values($courseids));
+        $this->assertEquals(array($coursegroups[$courses[0]->id][1]->id), $groupids);
+        $this->assertEquals($users[1]->id, $userid);
+
+        $defaultcourses = calendar_get_default_courses(null, '*', false, $users[0]->id);
+        list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false, $users[0]);
+
+        $this->assertEquals(
+                [$courses[0]->id, $courses[1]->id, $courses[2]->id, SITEID],
+                array_values($courseids),
+                '', 0.0, 10, true);
+        $this->assertEquals(array($coursegroups[$courses[0]->id][0]->id), $groupids);
+        $this->assertEquals($users[0]->id, $userid);
 
     }
 
     /**
-     * Confirm that the skip events flag causes the calendar_get_view function
-     * to avoid querying for the calendar events.
+     * This function tests calendar_set_filters for the case when user is logged in, but no user id is provided.
      */
-    public function test_calendar_get_view_skip_events() {
-        $this->resetAfterTest(true);
-        $this->setAdminUser();
+    public function test_calendar_set_filters_logged_in_no_user() {
+        $this->resetAfterTest();
 
-        $generator = $this->getDataGenerator();
-        $user = $generator->create_user();
-        $skipnavigation = true;
-        $skipevents = true;
-        $event = create_event([
-            'eventtype' => 'user',
-            'userid' => $user->id
-        ]);
+        list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters();
 
-        $this->setUser($user);
-        $calendar = \calendar_information::create(time() - 10, SITEID, null);
+        $this->setUser($users[0]);
+        $defaultcourses = calendar_get_default_courses(null, '*', false, $users[0]->id);
+        list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false);
+        $this->assertEquals([$courses[0]->id, $courses[1]->id, $courses[2]->id, SITEID], array_values($courseids), '', 0.0, 10,
+                true);
+        $this->assertEquals(array($coursegroups[$courses[0]->id][0]->id), $groupids);
+        $this->assertEquals($users[0]->id, $userid);
+    }
 
-        list($data, $template) = calendar_get_view($calendar, 'day', $skipnavigation, $skipevents);
-        $this->assertEmpty($data->events);
+    /**
+     * This function tests calendar_set_filters for the case when a user is logged in, but another user id is provided.
+     */
+    public function test_calendar_set_filters_logged_in_another_user() {
+        $this->resetAfterTest();
 
-        $skipevents = false;
-        list($data, $template) = calendar_get_view($calendar, 'day', $skipnavigation, $skipevents);
+        list($users, $courses, $coursegroups) = $this->setup_test_calendar_set_filters();
 
-        $this->assertEquals($event->id, $data->events[0]->id);
+        $this->setUser($users[0]);
+        $defaultcourses = calendar_get_default_courses(null, '*', false, $users[1]->id);
+        list($courseids, $groupids, $userid) = calendar_set_filters($defaultcourses, false, $users[1]);
+
+        $this->assertEquals(array($courses[0]->id, SITEID), array_values($courseids));
+        $this->assertEquals(array($coursegroups[$courses[0]->id][1]->id), $groupids);
+        $this->assertEquals($users[1]->id, $userid);
     }
 }
index 1a927c2..82cd7a8 100644 (file)
@@ -90,15 +90,29 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
         $this->assertCount(2, $events);
 
         // Disable the lesson module.
-        $modulerecord = $DB->get_record('modules', ['name' => 'lesson']);
-        $modulerecord->visible = 0;
-        $DB->update_record('modules', $modulerecord);
+        $DB->set_field('modules', 'visible', 0, ['name' => 'lesson']);
 
         // Check that we only return the assign event.
         $events = $retrievalstrategy->get_raw_events(null, [0], null);
         $this->assertCount(1, $events);
         $event = reset($events);
         $this->assertEquals('assign', $event->modulename);
+
+        // Now, log out and repeat the above test in the reverse order.
+        $this->setUser();
+
+        // Check that we only return the assign event (given that the lesson module is still disabled).
+        $events = $retrievalstrategy->get_raw_events([$student->id], [0], null);
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertEquals('assign', $event->modulename);
+
+        // Enable the lesson module.
+        $DB->set_field('modules', 'visible', 1, ['name' => 'lesson']);
+
+        // Get all events.
+        $events = $retrievalstrategy->get_raw_events(null, [0], null);
+        $this->assertCount(2, $events);
     }
 
     /**
@@ -209,37 +223,37 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
             calendar_event::create($event, false);
         }
 
-        $timestart = $now - 100;
-        $timeend = $now + (3 * 86400);
         $groups = [$group1->id, $group2->id];
 
-        // Get user override events.
-        $this->setUser($useroverridestudent);
-        $events = $retrievalstrategy->get_raw_events([$useroverridestudent->id], $groups, [$course->id]);
-        $this->assertCount(1, $events);
-        $event = reset($events);
-        $this->assertEquals('Assignment 1 due date - User override', $event->name);
-
-        // Get events for user that does not belong to any group and has no user override events.
-        $this->setUser($nogroupstudent);
-        $events = $retrievalstrategy->get_raw_events([$nogroupstudent->id], $groups, [$course->id]);
-        $this->assertCount(1, $events);
-        $event = reset($events);
-        $this->assertEquals('Assignment 1 due date', $event->name);
-
-        // Get events for user that belongs to groups A and B and has no user override events.
-        $this->setUser($group12student);
-        $events = $retrievalstrategy->get_raw_events([$group12student->id], $groups, [$course->id]);
-        $this->assertCount(1, $events);
-        $event = reset($events);
-        $this->assertEquals('Assignment 1 due date - Group A override', $event->name);
-
-        // Get events for user that belongs to group A and has no user override events.
-        $this->setUser($group1student);
-        $events = $retrievalstrategy->get_raw_events([$group1student->id], $groups, [$course->id]);
-        $this->assertCount(1, $events);
-        $event = reset($events);
-        $this->assertEquals('Assignment 1 due date - Group A override', $event->name);
+        // Do the following tests multiple times when logged in with different users. Also run the whole set when logged out.
+        // In any cases, the tests should not depend on the logged-in user.
+        foreach ([$useroverridestudent, $nogroupstudent, $group12student, $group1student, null] as $login) {
+            $this->setUser($login);
+
+            // Get user override events.
+            $events = $retrievalstrategy->get_raw_events([$useroverridestudent->id], $groups, [$course->id]);
+            $this->assertCount(1, $events);
+            $event = reset($events);
+            $this->assertEquals('Assignment 1 due date - User override', $event->name);
+
+            // Get events for user that does not belong to any group and has no user override events.
+            $events = $retrievalstrategy->get_raw_events([$nogroupstudent->id], $groups, [$course->id]);
+            $this->assertCount(1, $events);
+            $event = reset($events);
+            $this->assertEquals('Assignment 1 due date', $event->name);
+
+            // Get events for user that belongs to groups A and B and has no user override events.
+            $events = $retrievalstrategy->get_raw_events([$group12student->id], $groups, [$course->id]);
+            $this->assertCount(1, $events);
+            $event = reset($events);
+            $this->assertEquals('Assignment 1 due date - Group A override', $event->name);
+
+            // Get events for user that belongs to group A and has no user override events.
+            $events = $retrievalstrategy->get_raw_events([$group1student->id], $groups, [$course->id]);
+            $this->assertCount(1, $events);
+            $event = reset($events);
+            $this->assertEquals('Assignment 1 due date - Group A override', $event->name);
+        }
 
         // Add repeating events.
         $repeatingevents = [
@@ -290,8 +304,6 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
      * Test retrieval strategy with category specifications.
      */
     public function test_get_raw_events_category() {
-        global $DB;
-
         $this->resetAfterTest();
         $retrievalstrategy = new raw_event_retrieval_strategy();
         $generator = $this->getDataGenerator();
@@ -351,4 +363,88 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
         $events = $retrievalstrategy->get_raw_events(null, null, null, [$category1->id, $category2->id]);
         $this->assertCount(2, $events);
     }
+
+    public function test_get_raw_events_for_multiple_users() {
+        $this->resetAfterTest();
+
+        $generator = $this->getDataGenerator();
+
+        // Create users.
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $user3 = $generator->create_user();
+
+        // Create user events.
+        $events = [
+            [
+                'name' => 'User1 Event',
+                'eventtype' => 'user',
+                'userid' => $user1->id,
+                'timestart' => time(),
+            ], [
+                'name' => 'User2 Event',
+                'eventtype' => 'user',
+                'userid' => $user2->id,
+                'timestart' => time(),
+            ], [
+                'name' => 'User3 Event',
+                'eventtype' => 'user',
+                'userid' => $user3->id,
+                'timestart' => time(),
+            ]
+        ];
+        foreach ($events as $event) {
+            calendar_event::create($event, false);
+        }
+
+        $retrievalstrategy = new raw_event_retrieval_strategy();
+
+        // Get all events.
+        $events = $retrievalstrategy->get_raw_events([$user1->id, $user2->id]);
+        $this->assertCount(2, $events);
+        $this->assertEquals(
+                ['User1 Event', 'User2 Event'],
+                array_column($events, 'name'),
+                '', 0.0, 10, true);
+    }
+
+    public function test_get_raw_events_for_groups_with_no_members() {
+        $this->resetAfterTest();
+
+        $generator = $this->getDataGenerator();
+
+        $course = $generator->create_course();
+
+        // Create groups.
+        $group1 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 1']);
+        $group2 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 2']);
+
+        // Create group events.
+        $events = [
+            [
+                'name' => 'Group 1 Event',
+                'eventtype' => 'group',
+                'groupid' => $group1->id,
+                'timestart' => time(),
+            ], [
+                'name' => 'Group 2 Event',
+                'eventtype' => 'group',
+                'groupid' => $group2->id,
+                'timestart' => time(),
+            ]
+        ];
+        foreach ($events as $event) {
+            calendar_event::create($event, false);
+        }
+
+        $retrievalstrategy = new raw_event_retrieval_strategy;
+
+        // Get group eventsl.
+        $events = $retrievalstrategy->get_raw_events(null, [$group1->id, $group2->id]);
+        $this->assertCount(2, $events);
+        $this->assertEquals(
+                ['Group 1 Event', 'Group 2 Event'],
+                array_column($events, 'name'),
+                '', 0.0, 10, true);
+    }
 }
index 359ee3e..6d3489e 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in /calendar/* ,
 information provided here is intended especially for developers.
 
+=== 3.6 ===
+* calendar_get_default_courses() function now has optional $userid parameter.
+* calendar_set_filters() function now has optional $user parameter.
+* The core_calendar\local\event\container class now provides two new helper methods for getting and setting the requesting user:
+  set_requesting_user() and get_requesting_user().
+
 === 3.5 ===
 * core_calendar_external::get_calendar_events now returns the categoryid for category events.
 
index 9034c84..07f7a5b 100644 (file)
@@ -493,7 +493,8 @@ class comment {
                 $textareaattrs = array(
                     'name' => 'content',
                     'rows' => 2,
-                    'id' => 'dlg-content-'.$this->cid
+                    'id' => 'dlg-content-'.$this->cid,
+                    'aria-label' => get_string('addcomment')
                 );
                 if (!$this->fullwidth) {
                     $textareaattrs['cols'] = '20';
index b3b8a0d..d6f79c3 100644 (file)
@@ -156,4 +156,4 @@ echo $OUTPUT->footer();
 // Thats all folks.
 // Don't ever even consider putting anything after this. It just wouldn't make sense.
 // But you already knew that, you smart developer you.
-exit;
\ No newline at end of file
+exit;
index eca4de1..e2a4192 100644 (file)
@@ -247,8 +247,7 @@ class core_course_management_renderer extends plugin_renderer_base {
                 'i/empty',
                 '',
                 'moodle',
-                array('class' => 'tree-icon', 'title' => get_string('showcategory', 'moodle', $text))
-            );
+                array('class' => 'tree-icon'));
             $icon = html_writer::span($icon, 'float-left');
         }
         $actions = \core_course\management\helper::get_category_listitem_actions($category);
@@ -288,7 +287,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         $html .= html_writer::end_div();
         if ($isexpanded) {
             $html .= html_writer::start_tag('ul',
-                array('class' => 'ml-1', 'role' => 'group', 'id' => 'subcategoryof'.$category->id));
+                array('class' => 'ml', 'role' => 'group', 'id' => 'subcategoryof'.$category->id));
             $catatlevel = \core_course\management\helper::get_expanded_categories($category->path);
             $catatlevel[] = array_shift($selectedcategories);
             $catatlevel = array_unique($catatlevel);
index 07f2d9f..455a528 100644 (file)
@@ -941,7 +941,11 @@ class core_course_external extends external_api {
 
                 // Make sure maxbytes are less then CFG->maxbytes.
                 if (array_key_exists('maxbytes', $course)) {
-                    $course['maxbytes'] = get_max_upload_file_size($CFG->maxbytes, $course['maxbytes']);
+                    // We allow updates back to 0 max bytes, a special value denoting the course uses the site limit.
+                    // Otherwise, either use the size specified, or cap at the max size for the course.
+                    if ($course['maxbytes'] != 0) {
+                        $course['maxbytes'] = get_max_upload_file_size($CFG->maxbytes, $course['maxbytes']);
+                    }
                 }
 
                 if (!empty($course['courseformatoptions'])) {
index e910366..50fa91a 100644 (file)
@@ -342,8 +342,7 @@ class format_singleactivity extends format_base {
 
     /**
      * Checks if the activity type has multiple items in the activity chooser.
-     * This may happen as a result of defining callback modulename_get_shortcuts()
-     * or [deprecated] modulename_get_types() - TODO MDL-53697 remove this line.
+     * This may happen as a result of defining callback modulename_get_shortcuts().
      *
      * @return bool|null (null if the check is not possible)
      */
index 2062044..69cbc89 100644 (file)
@@ -669,9 +669,6 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
         }
         $defaultmodule->archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
 
-        // Legacy support for callback get_types() - do not use any more, use get_shortcuts() instead!
-        $typescallbackexists = component_callback_exists($modname, 'get_types');
-
         // Each module can implement callback modulename_get_shortcuts() in its lib.php and return the list
         // of elements to be added to activity chooser.
         $items = component_callback($modname, 'get_shortcuts', array($defaultmodule), null);
@@ -699,58 +696,14 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
                 $modlist[$course->id][$modname][$item->name] = $item;
             }
             $return += $modlist[$course->id][$modname];
-            if ($typescallbackexists) {
-                debugging('Both callbacks get_shortcuts() and get_types() are found in module ' . $modname .
-                    '. Callback get_types() will be completely ignored', DEBUG_DEVELOPER);
-            }
             // If get_shortcuts() callback is defined, the default module action is not added.
             // It is a responsibility of the callback to add it to the return value unless it is not needed.
             continue;
         }
 
-        if ($typescallbackexists) {
-            debugging('Callback get_types() is found in module ' . $modname . ', this functionality is deprecated, ' .
-                'please use callback get_shortcuts() instead', DEBUG_DEVELOPER);
-        }
-        $types = component_callback($modname, 'get_types', array(), MOD_SUBTYPE_NO_CHILDREN);
-        if ($types !== MOD_SUBTYPE_NO_CHILDREN) {
-            // Legacy support for deprecated callback get_types(). To be removed in Moodle 3.5. TODO MDL-53697.
-            if (is_array($types) && count($types) > 0) {
-                $grouptitle = $modnamestr;
-                $icon = $OUTPUT->pix_icon('icon', '', $modname, array('class' => 'icon'));
-                foreach($types as $type) {
-                    if ($type->typestr === '--') {
-                        continue;
-                    }
-                    if (strpos($type->typestr, '--') === 0) {
-                        $grouptitle = str_replace('--', '', $type->typestr);
-                        continue;
-                    }
-                    // Set the Sub Type metadata.
-                    $subtype = new stdClass();
-                    $subtype->title = get_string('activitytypetitle', '',
-                        (object)['activity' => $grouptitle, 'type' => $type->typestr]);
-                    $subtype->type = str_replace('&amp;', '&', $type->type);
-                    $typename = preg_replace('/.*type=/', '', $subtype->type);
-                    $subtype->archetype = $type->modclass;
-
-                    if (!empty($type->help)) {
-                        $subtype->help = $type->help;
-                    } else if (get_string_manager()->string_exists('help' . $subtype->name, $modname)) {
-                        $subtype->help = get_string('help' . $subtype->name, $modname);
-                    }
-                    $subtype->link = new moodle_url($urlbase, array('add' => $modname, 'type' => $typename));
-                    $subtype->name = $modname . ':' . $subtype->link;
-                    $subtype->icon = $icon;
-                    $modlist[$course->id][$modname][$subtype->name] = $subtype;
-                }
-                $return += $modlist[$course->id][$modname];
-            }
-        } else {
-            // Neither get_shortcuts() nor get_types() callbacks found, use the default item for the activity chooser.
-            $modlist[$course->id][$modname][$modname] = $defaultmodule;
-            $return[$modname] = $defaultmodule;
-        }
+        // The callback get_shortcuts() was not found, use the default item for the activity chooser.
+        $modlist[$course->id][$modname][$modname] = $defaultmodule;
+        $return[$modname] = $defaultmodule;
     }
 
     core_collator::asort_objects_by_property($return, 'title');
index d697a8b..2ced5d6 100644 (file)
Binary files a/enrol/manual/amd/build/form-potential-user-selector.min.js and b/enrol/manual/amd/build/form-potential-user-selector.min.js differ
index 49bdcbf..066f59d 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
+define(['jquery', 'core/ajax', 'core/templates', 'core/str'], function($, Ajax, Templates, Str) {
+
+    /** @var {Number} Maximum number of users to show. */
+    var MAXUSERS = 100;
 
     return /** @alias module:enrol_manual/form-potential-user-selector */ {
 
         processResults: function(selector, results) {
             var users = [];
-            $.each(results, function(index, user) {
-                users.push({
-                    value: user.id,
-                    label: user._label
+            if ($.isArray(results)) {
+                $.each(results, function(index, user) {
+                    users.push({
+                        value: user.id,
+                        label: user._label
+                    });
                 });
-            });
-            return users;
+                return users;
+
+            } else {
+                return results;
+            }
         },
 
         transport: function(selector, query, success, failure) {
@@ -57,7 +65,7 @@ define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
                     search: query,
                     searchanywhere: true,
                     page: 0,
-                    perpage: 30
+                    perpage: MAXUSERS + 1
                 }
             }]);
 
@@ -65,30 +73,38 @@ define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
                 var promises = [],
                     i = 0;
 
-                // Render the label.
-                $.each(results, function(index, user) {
-                    var ctx = user,
-                        identity = [];
-                    $.each(['idnumber', 'email', 'phone1', 'phone2', 'department', 'institution'], function(i, k) {
-                        if (typeof user[k] !== 'undefined' && user[k] !== '') {
-                            ctx.hasidentity = true;
-                            identity.push(user[k]);
-                        }
+                if (results.length <= MAXUSERS) {
+                    // Render the label.
+                    $.each(results, function(index, user) {
+                        var ctx = user,
+                            identity = [];
+                        $.each(['idnumber', 'email', 'phone1', 'phone2', 'department', 'institution'], function(i, k) {
+                            if (typeof user[k] !== 'undefined' && user[k] !== '') {
+                                ctx.hasidentity = true;
+                                identity.push(user[k]);
+                            }
+                        });
+                        ctx.identity = identity.join(', ');
+                        promises.push(Templates.render('enrol_manual/form-user-selector-suggestion', ctx));
                     });
-                    ctx.identity = identity.join(', ');
-                    promises.push(Templates.render('enrol_manual/form-user-selector-suggestion', ctx));
-                });
 
-                // Apply the label to the results.
-                return $.when.apply($.when, promises).then(function() {
-                    var args = arguments;
-                    $.each(results, function(index, user) {
-                        user._label = args[i];
-                        i++;
+                    // Apply the label to the results.
+                    return $.when.apply($.when, promises).then(function() {
+                        var args = arguments;
+                        $.each(results, function(index, user) {
+                            user._label = args[i];
+                            i++;
+                        });
+                        success(results);
+                        return;
                     });
-                    success(results);
-                    return;
-                });
+
+                } else {
+                    return Str.get_string('toomanyuserstoshow', 'core', '>' + MAXUSERS).then(function(toomanyuserstoshow) {
+                        success(toomanyuserstoshow);
+                        return;
+                    });
+                }
 
             }).fail(failure);
         }
diff --git a/enrol/manual/tests/behat/quickenrolment.feature b/enrol/manual/tests/behat/quickenrolment.feature
new file mode 100644 (file)
index 0000000..c5e3549
--- /dev/null
@@ -0,0 +1,155 @@
+@enrol @enrol_manual
+Feature: Teacher can search and enrol users one by one into the course
+  In order to quickly enrol particular students into my course
+  As a teacher
+  I can search for the students and enrol them into the course
+
+  Background:
+    Given the following "users" exist:
+      | username    | firstname | lastname | email                   |
+      | teacher001  | Teacher   | 001      | teacher001@example.com  |
+      | student001  | Student   | 001      | student001@example.com  |
+      | student002  | Student   | 002      | student002@example.com  |
+      | student003  | Student   | 003      | student003@example.com  |
+      | student004  | Student   | 004      | student004@example.com  |
+      | student005  | Student   | 005      | student005@example.com  |
+      | student006  | Student   | 006      | student006@example.com  |
+      | student007  | Student   | 007      | student007@example.com  |
+      | student008  | Student   | 008      | student008@example.com  |
+      | student009  | Student   | 009      | student009@example.com  |
+      | student010  | Student   | 010      | student010@example.com  |
+      | student011  | Student   | 011      | student011@example.com  |
+      | student012  | Student   | 012      | student012@example.com  |
+      | student013  | Student   | 013      | student013@example.com  |
+      | student014  | Student   | 014      | student014@example.com  |
+      | student015  | Student   | 015      | student015@example.com  |
+      | student016  | Student   | 016      | student016@example.com  |
+      | student017  | Student   | 017      | student017@example.com  |
+      | student018  | Student   | 018      | student018@example.com  |
+      | student019  | Student   | 019      | student019@example.com  |
+      | student020  | Student   | 020      | student020@example.com  |
+      | student021  | Student   | 021      | student021@example.com  |
+      | student022  | Student   | 022      | student022@example.com  |
+      | student023  | Student   | 023      | student023@example.com  |
+      | student024  | Student   | 024      | student024@example.com  |
+      | student025  | Student   | 025      | student025@example.com  |
+      | student026  | Student   | 026      | student026@example.com  |
+      | student027  | Student   | 027      | student027@example.com  |
+      | student028  | Student   | 028      | student028@example.com  |
+      | student029  | Student   | 029      | student029@example.com  |
+      | student030  | Student   | 030      | student030@example.com  |
+      | student031  | Student   | 031      | student031@example.com  |
+      | student032  | Student   | 032      | student032@example.com  |
+      | student033  | Student   | 033      | student033@example.com  |
+      | student034  | Student   | 034      | student034@example.com  |
+      | student035  | Student   | 035      | student035@example.com  |
+      | student036  | Student   | 036      | student036@example.com  |
+      | student037  | Student   | 037      | student037@example.com  |
+      | student038  | Student   | 038      | student038@example.com  |
+      | student039  | Student   | 039      | student039@example.com  |
+      | student040  | Student   | 040      | student040@example.com  |
+      | student041  | Student   | 041      | student041@example.com  |
+      | student042  | Student   | 042      | student042@example.com  |
+      | student043  | Student   | 043      | student043@example.com  |
+      | student044  | Student   | 044      | student044@example.com  |
+      | student045  | Student   | 045      | student045@example.com  |
+      | student046  | Student   | 046      | student046@example.com  |
+      | student047  | Student   | 047      | student047@example.com  |
+      | student048  | Student   | 048      | student048@example.com  |
+      | student049  | Student   | 049      | student049@example.com  |
+      | student050  | Student   | 050      | student050@example.com  |
+      | student051  | Student   | 051      | student051@example.com  |
+      | student052  | Student   | 052      | student052@example.com  |
+      | student053  | Student   | 053      | student053@example.com  |
+      | student054  | Student   | 054      | student054@example.com  |
+      | student055  | Student   | 055      | student055@example.com  |
+      | student056  | Student   | 056      | student056@example.com  |
+      | student057  | Student   | 057      | student057@example.com  |
+      | student058  | Student   | 058      | student058@example.com  |
+      | student059  | Student   | 059      | student059@example.com  |
+      | student060  | Student   | 060      | student060@example.com  |
+      | student061  | Student   | 061      | student061@example.com  |
+      | student062  | Student   | 062      | student062@example.com  |
+      | student063  | Student   | 063      | student063@example.com  |
+      | student064  | Student   | 064      | student064@example.com  |
+      | student065  | Student   | 065      | student065@example.com  |
+      | student066  | Student   | 066      | student066@example.com  |
+      | student067  | Student   | 067      | student067@example.com  |
+      | student068  | Student   | 068      | student068@example.com  |
+      | student069  | Student   | 069      | student069@example.com  |
+      | student070  | Student   | 070      | student070@example.com  |
+      | student071  | Student   | 071      | student071@example.com  |
+      | student072  | Student   | 072      | student072@example.com  |
+      | student073  | Student   | 073      | student073@example.com  |
+      | student074  | Student   | 074      | student074@example.com  |
+      | student075  | Student   | 075      | student075@example.com  |
+      | student076  | Student   | 076      | student076@example.com  |
+      | student077  | Student   | 077      | student077@example.com  |
+      | student078  | Student   | 078      | student078@example.com  |
+      | student079  | Student   | 079      | student079@example.com  |
+      | student080  | Student   | 080      | student080@example.com  |
+      | student081  | Student   | 081      | student081@example.com  |
+      | student082  | Student   | 082      | student082@example.com  |
+      | student083  | Student   | 083      | student083@example.com  |
+      | student084  | Student   | 084      | student084@example.com  |
+      | student085  | Student   | 085      | student085@example.com  |
+      | student086  | Student   | 086      | student086@example.com  |
+      | student087  | Student   | 087      | student087@example.com  |
+      | student088  | Student   | 088      | student088@example.com  |
+      | student089  | Student   | 089      | student089@example.com  |
+      | student090  | Student   | 090      | student090@example.com  |
+      | student091  | Student   | 091      | student091@example.com  |
+      | student092  | Student   | 092      | student092@example.com  |
+      | student093  | Student   | 093      | student093@example.com  |
+      | student094  | Student   | 094      | student094@example.com  |
+      | student095  | Student   | 095      | student095@example.com  |
+      | student096  | Student   | 096      | student096@example.com  |
+      | student097  | Student   | 097      | student097@example.com  |
+      | student098  | Student   | 098      | student098@example.com  |
+      | student099  | Student   | 099      | student099@example.com  |
+    And the following "courses" exist:
+      | fullname    | shortname |
+      | Course 001  | C001      |
+    And the following "course enrolments" exist:
+      | user        | course    | role            |
+      | teacher001  | C001      | editingteacher  |
+    And I log in as "teacher001"
+    And I am on "Course 001" course homepage
+
+  @javascript
+  Scenario: Teacher can search and enrol one particular student
+    Given I navigate to course participants
+    And I press "Enrol users"
+    When I set the field "Select users" to "student001"
+    And I should see "Student 001"
+    And I click on "Enrol users" "button" in the "Enrol users" "dialogue"
+    Then I should see "Active" in the "Student 001" "table_row"
+
+  @javascript
+  Scenario: Searching for a non-existing user
+    Given I navigate to course participants
+    And I press "Enrol users"
+    And I set the field "Select users" to "qwertyuiop"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    Then I should see "No suggestions"
+
+  @javascript
+  Scenario: If there are less than 100 matching users, all are displayed for selection
+    Given I navigate to course participants
+    And I press "Enrol users"
+    When I set the field "Select users" to "example.com"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    And I click on "Student 099" item in the autocomplete list
+    Then I should see "Student 099"
+
+  @javascript
+  Scenario: If there are more than 100 matching users, inform there are too many.
+    Given the following "users" exist:
+      | username    | firstname | lastname | email                   |
+      | student100  | Student   | 100      | student100@example.com  |
+      | student101  | Student   | 101      | student101@example.com  |
+    And I navigate to course participants
+    And I press "Enrol users"
+    When I set the field "Select users" to "example.com"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    Then I should see "Too many users (>100) to show"
index a686f13..388b9b7 100644 (file)
@@ -35,7 +35,6 @@ define('NO_DEBUG_DISPLAY', true);
 // @codingStandardsIgnoreLine This script does not require login.
 require("../../config.php");
 require_once("lib.php");
-require_once($CFG->libdir.'/eventslib.php');
 require_once($CFG->libdir.'/enrollib.php');
 require_once($CFG->libdir . '/filelib.php');
 
index a52720f..509f04f 100644 (file)
@@ -65,6 +65,8 @@ $string['expirymessageenrolledbody'] = 'Dear {$a->user},
 This is a notification that your enrolment in the course \'{$a->course}\' is due to expire on {$a->timeend}.
 
 If you need help, please contact {$a->enroller}.';
+$string['expirynotifyall'] = 'Teacher and enrolled user';
+$string['expirynotifyenroller'] = 'Teacher only';
 $string['groupkey'] = 'Use group enrolment keys';
 $string['groupkey_desc'] = 'Use group enrolment keys by default.';
 $string['groupkey_help'] = 'In addition to restricting access to the course to only those who know the key, use of group enrolment keys means users are automatically added to groups when they enrol in the course.
index 87e9c14..bac3887 100644 (file)
@@ -657,8 +657,8 @@ class enrol_self_plugin extends enrol_plugin {
      */
     protected function get_expirynotify_options() {
         $options = array(0 => get_string('no'),
-                         1 => get_string('expirynotifyenroller', 'core_enrol'),
-                         2 => get_string('expirynotifyall', 'core_enrol'));
+                         1 => get_string('expirynotifyenroller', 'enrol_self'),
+                         2 => get_string('expirynotifyall', 'enrol_self'));
         return $options;
     }
 
index 25dadf0..6375ea2 100644 (file)
@@ -85,7 +85,9 @@ if ($ADMIN->fulltree) {
     $settings->add(new admin_setting_configduration('enrol_self/enrolperiod',
         get_string('enrolperiod', 'enrol_self'), get_string('enrolperiod_desc', 'enrol_self'), 0));
 
-    $options = array(0 => get_string('no'), 1 => get_string('expirynotifyenroller', 'core_enrol'), 2 => get_string('expirynotifyall', 'core_enrol'));
+    $options = array(0 => get_string('no'),
+                     1 => get_string('expirynotifyenroller', 'enrol_self'),
+                     2 => get_string('expirynotifyall', 'enrol_self'));
     $settings->add(new admin_setting_configselect('enrol_self/expirynotify',
         get_string('expirynotify', 'core_enrol'), get_string('expirynotify_help', 'core_enrol'), 0, $options));
 
index 2e3280d..dbb69cd 100644 (file)
@@ -1,7 +1,6 @@
 <?php
 
     require('../config.php');
-    require_once($CFG->libdir.'/eventslib.php');
 
     // Form submitted, do not check referer (original page unknown).
     if ($form = data_submitted()) {
index 393afe4..dedd4bf 100644 (file)
@@ -141,7 +141,7 @@ if (!$edit) {
         if (!$admin and empty($data->override)) {
             $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
             foreach ($records as $record) {
-                $DB->delete_record('grade_letters', array('id' => $record->id));
+                $DB->delete_records('grade_letters', array('id' => $record->id));
                 // Trigger the letter grade deleted event.
                 $event = \core\event\grade_letter_deleted::create(array(
                     'objectid' => $record->id,
index eca8ae4..806646f 100644 (file)
@@ -59,6 +59,8 @@ if ($frm = data_submitted() and confirm_sesskey()) {
         // Invalidate the course groups cache seeing as we've changed it.
         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
 
+        // Invalidate the user_group_groupings cache, too.
+        cache_helper::purge_by_definition('core', 'user_group_groupings');
     } else if (isset($frm->remove) and !empty($frm->removeselect)) {
         foreach ($frm->removeselect as $groupid) {
             // Ask this method not to purge the cache, we'll do it ourselves afterwards.
@@ -66,6 +68,9 @@ if ($frm = data_submitted() and confirm_sesskey()) {
         }
         // Invalidate the course groups cache seeing as we've changed it.
         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+
+        // Invalidate the user_group_groupings cache, too.
+        cache_helper::purge_by_definition('core', 'user_group_groupings');
     }
 }
 
index b966608..964592c 100644 (file)
@@ -30,7 +30,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['admindirname'] = 'Rendszergazdakönyvtár';
+$string['admindirname'] = 'Rendszergazda-könyvtár';
 $string['availablelangs'] = 'Elérhető nyelvek listája';
 $string['chooselanguagehead'] = 'Nyelv kiválasztása';
 $string['chooselanguagesub'] = 'Válasszon nyelvet a telepítéshez! Ez lesz a portál alapbeállítás szerinti nyelve, de később módosíthatja.';
index 7cd886a..578ef82 100644 (file)
@@ -46,7 +46,7 @@ $string['dbprefix'] = 'テーブル接頭辞';
 $string['dirroot'] = 'Moodleディレクトリ';
 $string['environmenthead'] = 'あなたの環境を確認しています ...';
 $string['environmentsub2'] = 'それぞれのMoodleリリースにはPHPバージョンの最小必要条件および多くの必須PHP拡張モジュールがあります。完全な環境チェックはインストールおよびアップグレードごとに実行されます。新しいPHPバージョンのインストールまたはPHP拡張モジュールの有効化に関して分からない場合、あなたのサーバ管理者にご連絡ください。';
-$string['errorsinenvironment'] = 'ç\92°å¢\83ã\83\81ã\82§ã\83\83ã\82¯ã\81\8c失敗しました!';
+$string['errorsinenvironment'] = 'ç\92°å¢\83ã\83\81ã\82§ã\83\83ã\82¯ã\81«失敗しました!';
 $string['installation'] = 'インストレーション';
 $string['langdownloaderror'] = '残念ですが、言語「 {$a} 」をダウンロードできませんでした。インストール処理は英語で継続されます。';
 $string['memorylimithelp'] = '<p>現在、サーバのPHPメモリ制限が {$a} に設定されています。</p>
index cf656d3..6c38f65 100644 (file)
@@ -1301,8 +1301,7 @@ $string['cachesession'] = 'Session cache';
 $string['cachesessionhelp'] = 'User specific cache that expires when the user\'s session ends. Designed to alleviate session bloat/strain.';
 $string['cacheapplication'] = 'Application cache';
 $string['cacheapplicationhelp'] = 'Cached items are shared among all users and expire by a determined time to live (ttl).';
-// Deprecated since Moodle 3.2.
-$string['mobile'] = 'Mobile';
+
 // Deprecated since Moodle 3.3.
 $string['loginpasswordautocomplete'] = 'Prevent password autocompletion on login form';
 $string['loginpasswordautocomplete_help'] = 'If enabled, users are not allowed to save their account password in their browser.';
index d93fb9c..6003668 100644 (file)
@@ -266,9 +266,6 @@ $string['when'] = 'When';
 $string['yesterday'] = 'Yesterday';
 $string['youcandeleteallrepeats'] = 'This event is part of a repeating event series. You can delete this event only, or all {$a} events in the series at once.';
 
-// Deprecated since Moodle 3.2.
-$string['for'] = 'for';
-
 // Deprecated since Moodle 3.4.
 $string['quickdownloadcalendar'] = 'Quick download / subscribe to calendar';
 $string['ical'] = 'iCal';
index 606cac2..ca114fc 100644 (file)
@@ -200,6 +200,3 @@ $string['usercompetencystatus_idle'] = 'Idle';
 $string['usercompetencystatus_inreview'] = 'In review';
 $string['usercompetencystatus_waitingforreview'] = 'Waiting for review';
 $string['userplans'] = 'Learning plans';
-
-// Deprecated since Moodle 3.2.
-$string['invalidpersistent'] = 'Invalid persistent';
index db1cf82..d3b31d1 100644 (file)
@@ -5,34 +5,6 @@ myfilesmanage,core
 mypreferences,core_grades
 myprofile,core
 viewallmyentries,core_blog
-modchooserenable,core
-modchooserdisable,core
-invalidpersistent,core_competency
-revealpassword,core_form
-mediasettings,core_media
-legacyheading,core_media
-legacyheading_desc,core_media
-mobile,core_admin
-for,core_calendar
-context,core_message
-discussion,core_message
-emptysearchstring,core_message
-formorethan,core_message
-keywords,core_message
-messagehistory,core_message
-newsearch,core_message
-nosearchresults,core_message
-onlymycourses,core_message
-pagerefreshes,core_message
-page-message-x,core_message
-recent,core_message
-savemysettings,core_message
-search,core_message
-settingssaved,core_message
-strftimedaydatetime,core_message
-timenosee,core_message
-timesent,core_message
-userssearchresults,core_message
 loginpasswordautocomplete,core_admin
 loginpasswordautocomplete_help,core_admin
 deletecomment,core
index 3f18137..6344ad5 100644 (file)
@@ -85,6 +85,3 @@ $string['timeunit'] = 'Time unit';
 $string['timing'] = 'Timing';
 $string['unmaskpassword'] = 'Unmask';
 $string['year'] = 'Year';
-
-// Deprecated since 3.2.
-$string['revealpassword'] = 'Reveal';
index 03fe641..76372d5 100644 (file)
@@ -36,8 +36,3 @@ Where two players support the same format, enabling both increases compatibility
 $string['privacy:metadata'] = 'Media embedding does not store any personal data.';
 $string['supports'] = 'Supports';
 $string['videoextensions'] = 'Video: {$a}';
-
-// Deprecated since Moodle 3.2.
-$string['mediasettings'] = 'Media embedding';
-$string['legacyheading'] = 'Legacy media players';
-$string['legacyheading_desc'] = 'These players are not frequently used on the Web and require browser plugins that are less widely installed.';
index 93ec3a2..917d352 100644 (file)
@@ -76,7 +76,6 @@ $string['messagingdisabled'] = 'Messaging is disabled on this site, emails will
 $string['newonlymsg'] = 'Show only new';
 $string['newmessage'] = 'New message';
 $string['newmessagesearch'] = 'Select or search for a contact to send a new message.';
-$string['newsearch'] = 'New search';
 $string['noframesjs'] = 'Use more accessible interface';
 $string['nocontacts'] = 'No contacts';
 $string['nomessages'] = 'No messages';
@@ -180,24 +179,3 @@ $string['viewnotificationresource'] = 'Go to: {$a}';
 $string['viewunreadmessageswith'] = 'View unread messages with {$a}';
 $string['writeamessage'] = 'Write a message...';
 $string['you'] = 'You:';
-
-// Deprecated since Moodle 3.2.
-$string['context'] = 'context';
-$string['discussion'] = 'Discussion';
-$string['emptysearchstring'] = 'You must search for something';
-$string['formorethan'] = 'For more than';
-$string['keywords'] = 'Keywords';
-$string['messagehistory'] = 'Message history';
-$string['newsearch'] = 'New search';
-$string['nosearchresults'] = 'There were no results from your search';
-$string['onlymycourses'] = 'Only in my courses';
-$string['pagerefreshes'] = 'This page refreshes automatically every {$a} seconds';
-$string['page-message-x'] = 'Any message pages';
-$string['recent'] = 'Recent';
-$string['savemysettings'] = 'Save my settings';
-$string['search'] = 'Search';
-$string['settingssaved'] = 'Your settings have been saved';
-$string['strftimedaydatetime'] = '%A, %d %B %Y, %I:%M %p';
-$string['timenosee'] = 'Minutes since I was last seen online';
-$string['timesent'] = 'Time sent';
-$string['userssearchresults'] = 'Users found: {$a}';
index e3521f0..84b7020 100644 (file)
@@ -2205,10 +2205,6 @@ $string['yourwordforx'] = 'Your word for \'{$a}\'';
 $string['zippingbackup'] = 'Zipping backup';
 $string['deprecatedeventname'] = '{$a} (no longer in use)';
 
-// Deprecated since Moodle 3.2.
-$string['modchooserenable'] = 'Activity chooser on';
-$string['modchooserdisable'] = 'Activity chooser off';
-
 // Deprecated since Moodle 3.3.
 $string['deletecomment'] = 'Delete this comment';
 $string['sectionusedefaultname'] = 'Use default section name';
index b6cf1de..1aac870 100644 (file)
@@ -186,6 +186,7 @@ $string['service'] = 'Service';
 $string['servicehelpexplanation'] = 'A service is a set of functions. A service can be accessed by all users or just specified users.';
 $string['servicename'] = 'Service name';
 $string['servicenotavailable'] = 'Web service is not available (it doesn\'t exist or might be disabled)';
+$string['servicerequireslogin'] = 'Web service is not available (the session has been logged out or has expired)';
 $string['servicesbuiltin'] = 'Built-in services';
 $string['servicescustom'] = 'Custom services';
 $string['serviceusers'] = 'Authorised users';
index c3e39cd..0c7b156 100644 (file)
@@ -234,9 +234,6 @@ function uninstall_plugin($type, $name) {
     // delete the capabilities that were defined by this module
     capabilities_cleanup($component);
 
-    // remove event handlers and dequeue pending events
-    events_uninstall($component);
-
     // Delete all remaining files in the filepool owned by the component.
     $fs = get_file_storage();
     $fs->delete_component_files($component);
index 8247f32..a585b99 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index 954829b..c59dc5d 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 481247c..088deca 100644 (file)
@@ -25,7 +25,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since      2.9
  */
-define(['jquery', 'core/config', 'core/log'], function($, config, Log) {
+define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Log, URL) {
 
     // Keeps track of when the user leaves the page so we know not to show an error.
     var unloading = false;
@@ -79,9 +79,14 @@ define(['jquery', 'core/config', 'core/log'], function($, config, Log) {
         }
         // Something failed, reject the remaining promises.
         if (exception !== null) {
-            for (; i < requests.length; i++) {
-                request = requests[i];
-                request.deferred.reject(exception);
+            // If the user isn't doing anything too important, redirect to the login page.
+            if (exception.errorcode === "servicerequireslogin") {
+                window.location = URL.relativeUrl("/login/index.php");
+            } else {
+                for (; i < requests.length; i++) {
+                    request = requests[i];
+                    request.deferred.reject(exception);
+                }
             }
         }
     };
index ea2fab3..afe6e3f 100644 (file)
@@ -355,7 +355,10 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             });
             // If we found any matches, show the list.
             inputElement.attr('aria-expanded', true);
-            if (matchingElements) {
+            if (originalSelect.attr('data-notice')) {
+                // Display a notice rather than actual suggestions.
+                suggestionsElement.html(originalSelect.attr('data-notice'));
+            } else if (matchingElements) {
                 // We only activate the first item in the list if tags is false,
                 // because otherwise "Enter" would select the first item, instead of
                 // creating a new tag.
@@ -512,15 +515,21 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 var option = $('<option>');
                 originalSelect.append(option);
             }
-            // And add all the new ones returned from ajax.
-            $.each(processedResults, function(resultIndex, result) {
-                if (existingValues.indexOf(String(result.value)) === -1) {
-                    var option = $('<option>');
-                    option.append(result.label);
-                    option.attr('value', result.value);
-                    originalSelect.append(option);
-                }
-            });
+            if ($.isArray(processedResults)) {
+                // Add all the new ones returned from ajax.
+                $.each(processedResults, function(resultIndex, result) {
+                    if (existingValues.indexOf(String(result.value)) === -1) {
+                        var option = $('<option>');
+                        option.append(result.label);
+                        option.attr('value', result.value);
+                        originalSelect.append(option);
+                    }
+                });
+                originalSelect.attr('data-notice', '');
+            } else {
+                // The AJAX handler returned a string instead of the array.
+                originalSelect.attr('data-notice', processedResults);
+            }
             // Update the list of suggestions now from the new values in the select list.
             updateSuggestions(options, state, '', originalSelect);
             M.util.js_complete(pendingKey);
index 7285e39..75c727f 100644 (file)
@@ -132,37 +132,19 @@ class behat_config_manager {
     }
 
     /**
-     * Search feature files for set of tags.
-     *
-     * @param array $features set of feature files.
-     * @param string $tags list of tags (currently support && only.)
-     * @return array filtered list of feature files with tags.
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    public static function get_features_with_tags($features, $tags) {
-
-        debugging('Use of get_features_with_tags is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->filtered_features_with_tags($features, $tags);
+    public static function get_features_with_tags() {
+        throw new coding_exception('get_features_with_tags() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Gets the list of Moodle steps definitions
-     *
-     * Class name as a key and the filepath as value
-     *
-     * Externalized from update_config_file() to use
-     * it from the steps definitions web interface
-     *
-     * @return array
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
     public static function get_components_steps_definitions() {
-
-        debugging('Use of get_components_steps_definitions is deprecated, please see behat_config_util::get_components_contexts',
-            DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->get_components_contexts();
+        throw new coding_exception('get_components_steps_definitions() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
@@ -321,160 +303,59 @@ class behat_config_manager {
     }
 
     /**
-     * Behat config file specifing the main context class,
-     * the required Behat extensions and Moodle test wwwroot.
-     *
-     * @param array $features The system feature files
-     * @param array $stepsdefinitions The system steps definitions
-     * @return string
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected static function get_config_file_contents($features, $stepsdefinitions) {
-
-        debugging('Use of get_config_file_contents is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->get_config_file_contents($features, $stepsdefinitions);
+    protected static function get_config_file_contents() {
+        throw new coding_exception('get_config_file_contents() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Parse $CFG->behat_config and return the array with required config structure for behat.yml
-     *
-     * @param string $profile profile name
-     * @param array $values values for profile
-     * @return array
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected static function merge_behat_config($profile, $values) {
-
-        debugging('Use of merge_behat_config is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        self::get_behat_config_util()->get_behat_config_for_profile($profile, $values);
+    protected static function merge_behat_config() {
+        throw new coding_exception('merge_behat_config() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Parse $CFG->behat_profile and return the array with required config structure for behat.yml.
-     *
-     * $CFG->behat_profiles = array(
-     *     'profile' = array(
-     *         'browser' => 'firefox',
-     *         'tags' => '@javascript',
-     *         'wd_host' => 'http://127.0.0.1:4444/wd/hub',
-     *         'capabilities' => array(
-     *             'platform' => 'Linux',
-     *             'version' => 44
-     *         )
-     *     )
-     * );
-     *
-     * @param string $profile profile name
-     * @param array $values values for profile.
-     * @return array
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected static function get_behat_profile($profile, $values) {
-        // Values should be an array.
-        if (!is_array($values)) {
-            return array();
-        }
-
-        // Check suite values.
-        $behatprofilesuites = array();
-        // Fill tags information.
-        if (isset($values['tags'])) {
-            $behatprofilesuites = array(
-                'suites' => array(
-                    'default' => array(
-                        'filters' => array(
-                            'tags' => $values['tags'],
-                        )
-                    )
-                )
-            );
-        }
-
-        // Selenium2 config values.
-        $behatprofileextension = array();
-        $seleniumconfig = array();
-        if (isset($values['browser'])) {
-            $seleniumconfig['browser'] = $values['browser'];
-        }
-        if (isset($values['wd_host'])) {
-            $seleniumconfig['wd_host'] = $values['wd_host'];
-        }
-        if (isset($values['capabilities'])) {
-            $seleniumconfig['capabilities'] = $values['capabilities'];
-        }
-        if (!empty($seleniumconfig)) {
-            $behatprofileextension = array(
-                'extensions' => array(
-                    'Behat\MinkExtension' => array(
-                        'selenium2' => $seleniumconfig,
-                    )
-                )
-            );
-        }
-
-        return array($profile => array_merge($behatprofilesuites, $behatprofileextension));
+    protected static function get_behat_profile() {
+        throw new coding_exception('get_behat_profile() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Attempt to split feature list into fairish buckets using timing information, if available.
-     * Simply add each one to lightest buckets until all files allocated.
-     * PGA = Profile Guided Allocation. I made it up just now.
-     * CAUTION: workers must agree on allocation, do not be random anywhere!
-     *
-     * @param array $features Behat feature files array
-     * @param int $nbuckets Number of buckets to divide into
-     * @param int $instance Index number of this instance
-     * @return array Feature files array, sorted into allocations
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected static function profile_guided_allocate($features, $nbuckets, $instance) {
-
-        debugging('Use of profile_guided_allocate is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->profile_guided_allocate($features, $nbuckets, $instance);
+    protected static function profile_guided_allocate() {
+        throw new coding_exception('profile_guided_allocate() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Overrides default config with local config values
-     *
-     * array_merge does not merge completely the array's values
-     *
-     * @param mixed $config The node of the default config
-     * @param mixed $localconfig The node of the local config
-     * @return mixed The merge result
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected static function merge_config($config, $localconfig) {
-
-        debugging('Use of merge_config is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->merge_config($config, $localconfig);
+    protected static function merge_config() {
+        throw new coding_exception('merge_config() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * Cleans the path returned by get_components_with_tests() to standarize it
-     *
-     * @see tests_finder::get_all_directories_with_tests() it returns the path including /tests/
-     * @param string $path
-     * @return string The string without the last /tests part
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
-    protected final static function clean_path($path) {
-
-        debugging('Use of clean_path is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->clean_path($path);
+    protected final static function clean_path() {
+        throw new coding_exception('clean_path() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
     /**
-     * The relative path where components stores their behat tests
-     *
-     * @return string
-     * @deprecated since 3.2 MDL-55072 - please use behat_config_util.php
-     * @todo MDL-55365 This will be deleted in Moodle 3.6.
+     * @deprecated since 3.2 - please use behat_config_util.php
      */
     protected final static function get_behat_tests_path() {
-        debugging('Use of get_behat_tests_path is deprecated, please see behat_config_util', DEBUG_DEVELOPER);
-        return self::get_behat_config_util()->get_behat_tests_path();
+        throw new coding_exception('get_behat_tests_path() can not be used anymore. ' .
+            'Please use behat_config_util instead.');
     }
 
 }
index 27f74c0..0d7831f 100644 (file)
@@ -834,10 +834,6 @@ abstract class base implements \IteratorAggregate {
         \core\event\manager::dispatch($this);
 
         $this->dispatched = true;
-
-        if ($legacyeventname = static::get_legacy_eventname()) {
-            events_trigger_legacy($legacyeventname, $this->get_legacy_eventdata());
-        }
     }
 
     /**
index 2960b77..f617cc2 100644 (file)
@@ -123,28 +123,34 @@ class manager {
     }
 
     /**
-     * Create handler instance.
+     * Get fully qualified name of session handler class.
+     *
+     * @return string The name of the handler class
      */
-    protected static function load_handler() {
+    public static function get_handler_class() {
         global $CFG, $DB;
 
-        if (self::$handler) {
-            return;
-        }
-
-        // Find out which handler to use.
         if (PHPUNIT_TEST) {
-            $class = '\core\session\file';
-
+            return '\core\session\file';
         } else if (!empty($CFG->session_handler_class)) {
-            $class = $CFG->session_handler_class;
-
+            return $CFG->session_handler_class;
         } else if (!empty($CFG->dbsessions) and $DB->session_lock_supported()) {
-            $class = '\core\session\database';
+            return '\core\session\database';
+        }
 
-        } else {
-            $class = '\core\session\file';
+        return '\core\session\file';
+    }
+
+    /**
+     * Create handler instance.
+     */
+    protected static function load_handler() {
+        if (self::$handler) {
+            return;
         }
+
+        // Find out which handler to use.
+        $class = self::get_handler_class();
         self::$handler = new $class();
     }
 
diff --git