Merge branch 'MDL-40626-master' of git://github.com/danpoltawski/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 22 Jul 2013 15:40:03 +0000 (17:40 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 22 Jul 2013 15:40:03 +0000 (17:40 +0200)
75 files changed:
admin/cli/install.php
admin/tool/behat/cli/util.php
badges/backpack.js
badges/badge.php
badges/mybadges.php
badges/renderer.php
blocks/course_overview/renderer.php
blocks/recent_activity/block_recent_activity.php
config-dist.php
course/format/singleactivity/format.php [new file with mode: 0644]
course/format/singleactivity/lang/en/format_singleactivity.php [new file with mode: 0644]
course/format/singleactivity/lib.php [new file with mode: 0644]
course/format/singleactivity/renderer.php [new file with mode: 0644]
course/format/singleactivity/settings.php [new file with mode: 0644]
course/format/singleactivity/settingslib.php [new file with mode: 0644]
course/format/singleactivity/styles.css [new file with mode: 0644]
course/format/singleactivity/version.php [new file with mode: 0644]
enrol/category/classes/observer.php [new file with mode: 0644]
enrol/category/db/events.php
enrol/category/locallib.php
enrol/category/tests/plugin_test.php [moved from enrol/category/tests/sync_test.php with 98% similarity]
grade/import/lib.php
grade/lib.php
grade/report/grader/lib.php
grade/report/lib.php
grade/report/user/lib.php
group/lib.php
install.php
lang/en/badges.php
lang/en/cache.php
lib/accesslib.php
lib/badgeslib.php
lib/behat/lib.php
lib/classes/component.php
lib/classes/event/base.php [new file with mode: 0644]
lib/classes/event/manager.php [new file with mode: 0644]
lib/classes/event/role_assigned.php [new file with mode: 0644]
lib/classes/event/role_unassigned.php [new file with mode: 0644]
lib/datalib.php
lib/db/caches.php
lib/db/events.php
lib/deprecatedlib.php
lib/dml/moodle_database.php
lib/editor/tinymce/plugins/spellchecker/config.php
lib/editor/tinymce/plugins/spellchecker/lib.php
lib/editor/tinymce/plugins/spellchecker/settings.php
lib/javascript.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputlib.php
lib/outputrequirementslib.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/event_sink.php [new file with mode: 0644]
lib/phpunit/classes/util.php
lib/phpunit/lib.php
lib/pluginlib.php
lib/setup.php
lib/setuplib.php
lib/testing/classes/util.php
lib/tests/accesslib_test.php
lib/tests/event_test.php [new file with mode: 0644]
lib/tests/eventslib_test.php
lib/tests/fixtures/event_fixtures.php [new file with mode: 0644]
lib/tests/fixtures/events.php
lib/tests/setuplib_test.php
lib/upgrade.txt
lib/upgradelib.php
report/participation/index.php
theme/font.php [new file with mode: 0644]
theme/image.php
theme/index.php
theme/javascript.php
theme/styles.php
user/index.php
user/selector/lib.php

index c2d08d1..2ab997e 100644 (file)
@@ -394,8 +394,9 @@ if ($interactive) {
         cli_error(get_string('pathserrcreatedataroot', 'install', $a));
     }
 }
-$CFG->tempdir  = $CFG->dataroot.'/temp';
-$CFG->cachedir = $CFG->dataroot.'/cache';
+$CFG->tempdir       = $CFG->dataroot.'/temp';
+$CFG->cachedir      = $CFG->dataroot.'/cache';
+$CFG->localcachedir = $CFG->dataroot.'/localcache';
 
 // download required lang packs
 if ($CFG->lang !== 'en') {
index cef4ea7..ab41e3d 100644 (file)
@@ -143,6 +143,9 @@ foreach ($vars as $var) {
     $CFG->{$var} = $CFG->{'behat_' . $var};
 }
 
+// Clean $CFG extra values before performing any action.
+behat_clean_init_config();
+
 $CFG->noemailever = true;
 $CFG->passwordsaltmain = 'moodle';
 
@@ -151,6 +154,7 @@ $CFG->jsrev = 1;
 
 // Unset cache and temp directories to reset them again with the new $CFG->dataroot.
 unset($CFG->cachedir);
+unset($CFG->localcachedir);
 unset($CFG->tempdir);
 
 // Continues setup.
index 68e736b..98678e8 100644 (file)
@@ -2,7 +2,20 @@
  * Push badges to backpack.
  */
 function addtobackpack(event, args) {
-    OpenBadges.issue([args.assertion], function(errors, successes) { });
+    var badgetable = Y.one('#issued-badge-table');
+    var errordiv = Y.one('#addtobackpack-error');
+    var errortext = M.util.get_string('error:backpackproblem', 'badges');
+    var errorhtml = '<div id="addtobackpack-error" class="box boxaligncenter notifyproblem">' + errortext + '</div>';
+
+    if (typeof OpenBadges !== 'undefined') {
+        OpenBadges.issue([args.assertion], function(errors, successes) { });
+    } else {
+        // Add error div if it doesn't exist yet.
+        if (!errordiv) {
+            var badgerror = Y.Node.create(errorhtml);
+            badgetable.insert(badgerror, 'before');
+        }
+    }
 }
 
 /**
index cd90239..339b147 100644 (file)
@@ -56,10 +56,8 @@ if (isloggedin()) {
     navigation_node::override_active_url($url);
 }
 
-// TODO: Better way of pushing badges to Mozilla backpack?
-if (!empty($CFG->badges_allowexternalbackpack)) {
-    $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true);
-}
+// Include JS files for backpack support.
+badges_setup_backpack_js();
 
 echo $OUTPUT->header();
 
index 46f4c1d..d944e73 100644 (file)
@@ -90,11 +90,8 @@ $PAGE->set_title($title);
 $PAGE->set_heading($title);
 $PAGE->set_pagelayout('mydashboard');
 
-// TODO: Better way of pushing badges to Mozilla backpack?
-if (!empty($CFG->badges_allowexternalbackpack)) {
-    $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true);
-    $PAGE->requires->js('/badges/backpack.js', true);
-}
+// Include JS files for backpack support.
+badges_setup_backpack_js();
 
 $output = $PAGE->get_renderer('core', 'badges');
 $badges = badges_get_user_badges($USER->id);
index e6e9d2d..f6abcb6 100644 (file)
@@ -282,6 +282,7 @@ class core_badges_renderer extends plugin_renderer_base {
         $today = strtotime($today_date);
 
         $table = new html_table();
+        $table->id = 'issued-badge-table';
 
         $imagetable = new html_table();
         $imagetable->attributes = array('class' => 'clearfix badgeissuedimage');
@@ -294,11 +295,13 @@ class core_badges_renderer extends plugin_renderer_base {
             $expiration = isset($issued['expires']) ? strtotime($issued['expires']) : $today + 1;
             if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $today) && badges_user_has_backpack($USER->id)) {
                 $assertion = new moodle_url('/badges/assertion.php', array('b' => $ibadge->hash));
+                $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false)));
                 $attributes = array(
-                        'type' => 'button',
-                        'value' => get_string('addtobackpack', 'badges'),
-                        'onclick' => 'OpenBadges.issue(["' . $assertion->out(false) . '"], function(errors, successes) { })');
+                        'type'  => 'button',
+                        'id'    => 'addbutton',
+                        'value' => get_string('addtobackpack', 'badges'));
                 $tobackpack = html_writer::tag('input', '', $attributes);
+                $this->output->add_action_handler($action, 'addbutton');
                 $imagetable->data[] = array($tobackpack);
             }
         }
@@ -459,7 +462,7 @@ class core_badges_renderer extends plugin_renderer_base {
                     get_string('downloadall'), 'POST', array('class' => 'activatebadge'));
 
         // Local badges.
-        $localhtml = html_writer::start_tag('fieldset', array('class' => 'generalbox'));
+        $localhtml = html_writer::start_tag('fieldset', array('id' => 'issued-badge-table', 'class' => 'generalbox'));
         $localhtml .= html_writer::tag('legend',
                     $this->output->heading_with_help(get_string('localbadges', 'badges', $SITE->fullname), 'localbadgesh', 'badges'));
         if ($badges->badges) {
index bb29569..6f33899 100644 (file)
@@ -103,10 +103,10 @@ class block_course_overview_renderer extends plugin_renderer_base {
 
             // No need to pass title through s() here as it will be done automatically by html_writer.
             $attributes = array('title' => $course->fullname);
-            if (empty($course->visible)) {
-                $attributes['class'] = 'dimmed';
-            }
             if ($course->id > 0) {
+                if (empty($course->visible)) {
+                    $attributes['class'] = 'dimmed';
+                }
                 $courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
                 $coursefullname = format_string($course->fullname, true, $course->id);
                 $link = html_writer::link($courseurl, $coursefullname, $attributes);
index b62eefb..c58c359 100644 (file)
@@ -100,13 +100,17 @@ class block_recent_activity extends block_base {
     }
 
     /**
-     * Returns all recent enrollments
+     * Returns all recent enrolments.
+     *
+     * This function previously used get_recent_enrolments located in lib/deprecatedlib.php which would
+     * return an empty array which was identified in MDL-36993. The use of this function outside the
+     * deprecated lib was removed in MDL-40649.
      *
      * @todo MDL-36993 this function always return empty array
      * @return array array of entries from {user} table
      */
     protected function get_recent_enrolments() {
-        return get_recent_enrolments($this->page->course->id, $this->get_timestart());
+        return array();
     }
 
     /**
index e22d246..6598ce3 100644 (file)
@@ -213,6 +213,7 @@ $CFG->admin = 'admin';
 //     $CFG->xsendfilealiases = array(
 //         '/dataroot/' => $CFG->dataroot,
 //         '/cachedir/' => '/var/www/moodle/cache',    // for custom $CFG->cachedir locations
+//         '/localcachedir/' => '/var/local/cache',    // for custom $CFG->localcachedir locations
 //         '/tempdir/'  => '/var/www/moodle/temp',     // for custom $CFG->tempdir locations
 //         '/filedir'   => '/var/www/moodle/filedir',  // for custom $CFG->filedir locations
 //     );
@@ -353,10 +354,12 @@ $CFG->admin = 'admin';
 //
 // It is possible to specify different cache and temp directories, use local fast filesystem
 // for normal web servers. Server clusters MUST use shared filesystem for cachedir!
+// Localcachedir is intended for server clusters, it does not have to be shared by cluster nodes.
 // The directories must not be accessible via web.
 //
-//     $CFG->tempdir = '/var/www/moodle/temp';
-//     $CFG->cachedir = '/var/www/moodle/cache';
+//     $CFG->tempdir = '/var/www/moodle/temp';        // Files used during one HTTP request only.
+//     $CFG->cachedir = '/var/www/moodle/cache';      // Directory MUST BE SHARED by all cluster nodes, locking required.
+//     $CFG->localcachedir = '/var/local/cache';      // Intended for local node caching.
 //
 // Some filesystems such as NFS may not support file locking operations.
 // Locking resolves race conditions and is strongly recommended for production servers.
@@ -631,6 +634,11 @@ $CFG->admin = 'admin';
 // Example:
 //   $CFG->behat_restart_browser_after = 7200;     // Restarts the browser session after 2 hours
 //
+// All this page's extra Moodle settings are compared against a white list of allowed settings
+// (the basic and behat_* ones) to avoid problems with production environments. This setting can be
+// used to expand the default white list with an array of extra settings.
+// Example:
+//   $CFG->behat_extraallowedsettings = array('logsql', 'dblogerror');
 
 //=========================================================================
 // ALL DONE!  To continue installation, visit your main page with a browser
diff --git a/course/format/singleactivity/format.php b/course/format/singleactivity/format.php
new file mode 100644 (file)
index 0000000..5996ef4
--- /dev/null
@@ -0,0 +1,31 @@
+<?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/>.
+
+/**
+ * format.php - course format featuring single activity included from view.php
+ *
+ * if we are not redirected before this point this means we want to
+ * either manage orphaned activities - i.e. display section 1,
+ * or the activity is not setup, does not have url or is not accessible at the
+ * moment
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$courserenderer = $PAGE->get_renderer('format_singleactivity');
+echo $courserenderer->display($course, $section != 0);
diff --git a/course/format/singleactivity/lang/en/format_singleactivity.php b/course/format/singleactivity/lang/en/format_singleactivity.php
new file mode 100644 (file)
index 0000000..9b2887b
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Strings for component 'format_singleactivity'
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['activitytype'] = 'Type of activity';
+$string['activitytype_help'] = 'Choose the type of activity or resource to use in this course';
+$string['defactivitytype'] = 'Default type of activity';
+$string['defactivitytypedesc'] = 'Specify the activity type that will be selected by default when creating a new course';
+$string['erroractivitytype'] = 'Type of activity is not set up in Course settings';
+$string['orphaned'] = 'Orphaned';
+$string['orphanedwarning'] = 'These activities are unreachable by users!';
+$string['pluginname'] = 'Single activity format';
+$string['sectionname'] = '';
+$string['warningchangeformat'] = 'When changing the existing course format to "Single activity" make sure that you removed all extra activities from the course including the "News forum". Note that sections structure may be modified.';
diff --git a/course/format/singleactivity/lib.php b/course/format/singleactivity/lib.php
new file mode 100644 (file)
index 0000000..02795d1
--- /dev/null
@@ -0,0 +1,443 @@
+<?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 main class for the course format singleactivity
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot. '/course/format/lib.php');
+
+/**
+ * Main class for the singleactivity course format
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity extends format_base {
+    /** @var cm_info the current activity. Use get_activity() to retrieve it. */
+    private $activity = false;
+
+    /**
+     * The URL to use for the specified course
+     *
+     * @param int|stdClass $section Section object from database or just field course_sections.section
+     *     if null the course view page is returned
+     * @param array $options options for view URL. At the moment core uses:
+     *     'navigation' (bool) if true and section has no separate page, the function returns null
+     *     'sr' (int) used by multipage formats to specify to which section to return
+     * @return null|moodle_url
+     */
+    public function get_view_url($section, $options = array()) {
+        $sectionnum = $section;
+        if (is_object($sectionnum)) {
+            $sectionnum = $section->section;
+        }
+        if ($sectionnum == 1) {
+            return new moodle_url('/course/view.php', array('id' => $this->courseid, 'section' => 1));
+        }
+        if (!empty($options['navigation']) && $section !== null) {
+            return null;
+        }
+        return new moodle_url('/course/view.php', array('id' => $this->courseid));
+    }
+
+    /**
+     * Loads all of the course sections into the navigation
+     *
+     * @param global_navigation $navigation
+     * @param navigation_node $node The course node within the navigation
+     */
+    public function extend_course_navigation($navigation, navigation_node $node) {
+        // Display orphaned activities for the users who can see them.
+        $context = context_course::instance($this->courseid);
+        if (has_all_capabilities(array('moodle/course:viewhiddensections',
+                'moodle/course:viewhiddenactivities'), $context)) {
+            $modinfo = get_fast_modinfo($this->courseid);
+            if (!empty($modinfo->sections[1])) {
+                $section1 = $modinfo->get_section_info(1);
+                // Show orphaned activities.
+                $orphanednode = $node->add(get_string('orphaned', 'format_singleactivity'),
+                        $this->get_view_url(1), navigation_node::TYPE_SECTION, null, $section1->id);
+                $orphanednode->nodetype = navigation_node::NODETYPE_BRANCH;
+                $orphanednode->add_class('orphaned');
+                foreach ($modinfo->sections[1] as $cmid) {
+                    $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]);
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds a course module to the navigation node
+     *
+     * This is basically copied from function global_navigation::load_section_activities()
+     * because it is not accessible from outside.
+     *
+     * @param navigation_node $node
+     * @param cm_info $cm
+     * @return null|navigation_node
+     */
+    protected function navigation_add_activity(navigation_node $node, $cm) {
+        if (!$cm->uservisible) {
+            return null;
+        }
+        $action = $cm->get_url();
+        if (!$action) {
+            // Do not add to navigation activity without url (i.e. labels).
+            return null;
+        }
+        $activityname = format_string($cm->name, true, array('context' => context_module::instance($cm->id)));
+        if ($cm->icon) {
+            $icon = new pix_icon($cm->icon, $cm->modfullname, $cm->iconcomponent);
+        } else {
+            $icon = new pix_icon('icon', $cm->modfullname, $cm->modname);
+        }
+        $activitynode = $node->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon);
+        if (global_navigation::module_extends_navigation($cm->modname)) {
+            $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
+        } else {
+            $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
+        }
+        return $activitynode;
+    }
+
+    /**
+     * Returns the list of blocks to be automatically added for the newly created course
+     *
+     * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
+     *     each of values is an array of block names (for left and right side columns)
+     */
+    public function get_default_blocks() {
+        // No blocks for this format because course view page is not displayed anyway.
+        return array(
+            BLOCK_POS_LEFT => array(),
+            BLOCK_POS_RIGHT => array()
+        );
+    }
+
+    /**
+     * Definitions of the additional options that this course format uses for course
+     *
+     * Singleactivity course format uses one option 'activitytype'
+     *
+     * @param bool $foreditform
+     * @return array of options
+     */
+    public function course_format_options($foreditform = false) {
+        static $courseformatoptions = false;
+        if ($courseformatoptions === false) {
+            $config = get_config('format_singleactivity');
+            $courseformatoptions = array(
+                'activitytype' => array(
+                    'default' => $config->activitytype,
+                    'type' => PARAM_TEXT,
+                ),
+            );
+        }
+        if ($foreditform && !isset($courseformatoptions['activitytype']['label'])) {
+            $availabletypes = get_module_types_names();
+            $courseformatoptionsedit = array(
+                'activitytype' => array(
+                    'label' => new lang_string('activitytype', 'format_singleactivity'),
+                    'help' => 'activitytype',
+                    'help_component' => 'format_singleactivity',
+                    'element_type' => 'select',
+                    'element_attributes' => array($availabletypes),
+                ),
+            );
+            $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
+        }
+        return $courseformatoptions;
+    }
+
+    /**
+     * Adds format options elements to the course/section edit form
+     *
+     * This function is called from {@link course_edit_form::definition_after_data()}
+     *
+     * Format singleactivity adds a warning when format of the course is about to be changed.
+     *
+     * @param MoodleQuickForm $mform form the elements are added to
+     * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
+     * @return array array of references to the added form elements
+     */
+    public function create_edit_form_elements(&$mform, $forsection = false) {
+        global $PAGE;
+        $elements = parent::create_edit_form_elements($mform, $forsection);
+        if (!$forsection && ($course = $PAGE->course) && !empty($course->format) &&
+                $course->format !== 'site' && $course->format !== 'singleactivity') {
+            // This is the existing course in other format, display a warning.
+            $element = $mform->addElement('static', '', '',
+                    html_writer::tag('span', get_string('warningchangeformat', 'format_singleactivity'),
+                            array('class' => 'error')));
+            array_unshift($elements, $element);
+        }
+        return $elements;
+    }
+
+    /**
+     * Make sure that current active activity is in section 0
+     *
+     * All other activities are moved to section 1 that will be displayed as 'Orphaned'.
+     * It may be needed after the course format was changed or activitytype in
+     * course settings has been changed.
+     *
+     * @return null|cm_info current activity
+     */
+    public function reorder_activities() {
+        course_create_sections_if_missing($this->courseid, array(0, 1));
+        foreach ($this->get_sections() as $sectionnum => $section) {
+            if (($sectionnum && $section->visible) ||
+                    (!$sectionnum && !$section->visible)) {
+                // Make sure that 0 section is visible and all others are hidden.
+                set_section_visible($this->courseid, $sectionnum, $sectionnum == 0);
+            }
+        }
+        $modinfo = get_fast_modinfo($this->courseid);
+
+        // Find the current activity (first activity with the specified type in all course activities).
+        $activitytype = $this->get_activitytype();
+        $activity = null;
+        if (!empty($activitytype)) {
+            foreach ($modinfo->sections as $sectionnum => $cmlist) {
+                foreach ($cmlist as $cmid) {
+                    if ($modinfo->cms[$cmid]->modname === $activitytype) {
+                        $activity = $modinfo->cms[$cmid];
+                        break 2;
+                    }
+                }
+            }
+        }
+
+        // Make sure the current activity is in the 0-section.
+        if ($activity && $activity->sectionnum != 0) {
+            moveto_module($activity, $modinfo->get_section_info(0));
+            // Cache was reset so get modinfo again.
+            $modinfo = get_fast_modinfo($this->courseid);
+        }
+
+        // Move all other activities into section 1 (the order must be kept).
+        $hasvisibleactivities = false;
+        $firstorphanedcm = null;
+        foreach ($modinfo->sections as $sectionnum => $cmlist) {
+            if ($sectionnum && !empty($cmlist) && $firstorphanedcm === null) {
+                $firstorphanedcm = reset($cmlist);
+            }
+            foreach ($cmlist as $cmid) {
+                if ($sectionnum > 1) {
+                    moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1));
+                } else if (!$hasvisibleactivities && $sectionnum == 1 && $modinfo->get_cm($cmid)->visible) {
+                    $hasvisibleactivities = true;
+                }
+            }
+        }
+        if (!empty($modinfo->sections[0])) {
+            foreach ($modinfo->sections[0] as $cmid) {
+                if (!$activity || $cmid != $activity->id) {
+                    moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1), $firstorphanedcm);
+                }
+            }
+        }
+        if ($hasvisibleactivities) {
+            set_section_visible($this->courseid, 1, false);
+        }
+        return $activity;
+    }
+
+    /**
+     * Returns the name of activity type used for this course
+     *
+     * @return string|null
+     */
+    protected function get_activitytype() {
+        $options = $this->get_format_options();
+        $availabletypes = get_module_types_names();
+        if (!empty($options['activitytype']) &&
+                array_key_exists($options['activitytype'], $availabletypes)) {
+            return $options['activitytype'];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the current activity if exists
+     *
+     * @return null|cm_info
+     */
+    protected function get_activity() {
+        if ($this->activity === false) {
+            $this->activity = $this->reorder_activities();
+        }
+        return $this->activity;
+    }
+
+    /**
+     * Checks if the current user can add the activity of the specified type to this course.
+     *
+     * @return bool
+     */
+    protected function can_add_activity() {
+        global $CFG;
+        if (!($modname = $this->get_activitytype())) {
+            return false;
+        }
+        if (!has_capability('moodle/course:manageactivities', context_course::instance($this->courseid))) {
+            return false;
+        }
+        if (!course_allowed_module($this->get_course(), $modname)) {
+            return false;
+        }
+        $libfile = "$CFG->dirroot/mod/$modname/lib.php";
+        if (!file_exists($libfile)) {
+            return null;
+        }
+        return true;
+    }
+
+    /**
+     * Checks if the activity type requires subtypes.
+     *
+     * @return bool|null (null if the check is not possible)
+     */
+    public function activity_has_subtypes() {
+        global $CFG;
+        if (!($modname = $this->get_activitytype())) {
+            return null;
+        }
+        $libfile = "$CFG->dirroot/mod/$modname/lib.php";
+        if (!file_exists($libfile)) {
+            return null;
+        }
+        include_once($libfile);
+        return function_exists($modname. '_get_types');
+    }
+
+    /**
+     * Allows course format to execute code on moodle_page::set_course()
+     *
+     * This function is executed before the output starts.
+     *
+     * If everything is configured correctly, user is redirected from the
+     * default course view page to the activity view page.
+     *
+     * "Section 1" is the administrative page to manage orphaned activities
+     *
+     * If user is on course view page and there is no module added to the course
+     * and the user has 'moodle/course:manageactivities' capability, redirect to create module
+     * form.
+     *
+     * @param moodle_page $page instance of page calling set_course
+     */
+    public function page_set_course(moodle_page $page) {
+        global $PAGE;
+        $page->add_body_class('format-'. $this->get_format());
+        if ($PAGE == $page && $page->has_set_url() &&
+                $page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
+            $edit = optional_param('edit', -1, PARAM_BOOL);
+            if (($edit == 0 || $edit == 1) && confirm_sesskey()) {
+                // This is a request to turn editing mode on or off, do not redirect here, /course/view.php will do redirection.
+                return;
+            }
+            $cm = $this->get_activity();
+            $cursection = optional_param('section', null, PARAM_INT);
+            if (!empty($cursection) && has_capability('moodle/course:viewhiddensections',
+                    context_course::instance($this->courseid))) {
+                // Display orphaned activities (course view page, section 1).
+                return;
+            }
+            if (!$this->get_activitytype()) {
+                if (has_capability('moodle/course:update', context_course::instance($this->courseid))) {
+                    // Teacher is redirected to edit course page.
+                    $url = new moodle_url('/course/edit.php', array('id' => $this->courseid));
+                    redirect($url, get_string('erroractivitytype', 'format_singleactivity'));
+                } else {
+                    // Student sees an empty course page.
+                    return;
+                }
+            }
+            if ($cm === null) {
+                if ($this->can_add_activity()) {
+                    // This is a user who has capability to create an activity.
+                    if ($this->activity_has_subtypes()) {
+                        // Activity that requires subtype can not be added automatically.
+                        if (optional_param('addactivity', 0, PARAM_INT)) {
+                            return;
+                        } else {
+                            $url = new moodle_url('/course/view.php', array('id' => $this->courseid, 'addactivity' => 1));
+                            redirect($url);
+                        }
+                    }
+                    // Redirect to the add activity form.
+                    $url = new moodle_url('/course/mod.php', array('id' => $this->courseid,
+                        'section' => 0, 'sesskey' => sesskey(), 'add' => $this->get_activitytype()));
+                    redirect($url);
+                } else {
+                    // Student views an empty course page.
+                    return;
+                }
+            } else if (!$cm->uservisible || !$cm->get_url()) {
+                // Activity is set but not visible to current user or does not have url.
+                // Display course page (either empty or with availability restriction info).
+                return;
+            } else {
+                // Everything is set up and accessible, redirect to the activity page!
+                redirect($cm->get_url());
+            }
+        }
+    }
+
+    /**
+     * Allows course format to execute code on moodle_page::set_cm()
+     *
+     * If we are inside the main module for this course, remove extra node level
+     * from navigation: substitute course node with activity node, move all children
+     *
+     * @param moodle_page $page instance of page calling set_cm
+     */
+    public function page_set_cm(moodle_page $page) {
+        global $PAGE;
+        parent::page_set_cm($page);
+        if ($PAGE == $page && ($cm = $this->get_activity()) &&
+                $cm->uservisible &&
+                ($cm->id === $page->cm->id) &&
+                ($activitynode = $page->navigation->find($cm->id, navigation_node::TYPE_ACTIVITY)) &&
+                ($node = $page->navigation->find($page->course->id, navigation_node::TYPE_COURSE))) {
+            // Substitute course node with activity node, move all children.
+            $node->action = $activitynode->action;
+            $node->type = $activitynode->type;
+            $node->id = $activitynode->id;
+            $node->key = $activitynode->key;
+            $node->isactive = $node->isactive || $activitynode->isactive;
+            $node->icon = null;
+            if ($activitynode->children->count()) {
+                foreach ($activitynode->children as &$child) {
+                    $child->remove();
+                    $node->add_node($child);
+                }
+            } else {
+                $node->search_for_active_node();
+            }
+            $activitynode->remove();
+        }
+    }
+}
diff --git a/course/format/singleactivity/renderer.php b/course/format/singleactivity/renderer.php
new file mode 100644 (file)
index 0000000..e9961a0
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderer for outputting the singleactivity course format.
+ *
+ * @package    format_singleactivity
+ * @copyright  2013 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Basic renderer for singleactivity format.
+ *
+ * @package    format_singleactivity
+ * @copyright  2013 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity_renderer extends plugin_renderer_base {
+
+    /**
+     * Displays the activities list in cases when course view page is not
+     * redirected to the activity page.
+     *
+     * @param stdClass $course record from table course
+     * @param bool $orphaned if false displays the main activity (if present)
+     *     if true displays all other activities
+     */
+    public function display($course, $orphaned) {
+        $courserenderer = $this->page->get_renderer('core', 'course');
+        $output = '';
+        $modinfo = get_fast_modinfo($course);
+        if ($orphaned) {
+            if (!empty($modinfo->sections[1])) {
+                $output .= $this->output->heading(get_string('orphaned', 'format_singleactivity'), 3, 'sectionname');
+                $output .= $this->output->box(get_string('orphanedwarning', 'format_singleactivity'));
+                $output .= $courserenderer->course_section_cm_list($course, 1, 1);
+            }
+        } else {
+            $output .= $courserenderer->course_section_cm_list($course, 0, 0);
+            if (empty($modinfo->sections[0]) && course_get_format($course)->activity_has_subtypes()) {
+                // Course format was unable to automatically redirect to add module page.
+                $output .= $courserenderer->course_section_add_cm_control($course, 0, 0);
+            }
+        }
+        return $output;
+    }
+}
diff --git a/course/format/singleactivity/settings.php b/course/format/singleactivity/settings.php
new file mode 100644 (file)
index 0000000..8573600
--- /dev/null
@@ -0,0 +1,33 @@
+<?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/>.
+
+/**
+ * Settings for format_singleactivity
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+require_once($CFG->dirroot. '/course/format/singleactivity/settingslib.php');
+
+if ($ADMIN->fulltree) {
+    $settings->add(new format_singleactivity_admin_setting_activitytype('format_singleactivity/activitytype',
+            new lang_string('defactivitytype', 'format_singleactivity'),
+            new lang_string('defactivitytypedesc', 'format_singleactivity'),
+            'forum', null));
+}
diff --git a/course/format/singleactivity/settingslib.php b/course/format/singleactivity/settingslib.php
new file mode 100644 (file)
index 0000000..75494fc
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * Settings class for format_singleactivity
+ *
+ * @package    format_singleactivity
+ * @copyright  2013 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Admin settings class for the format singleactivity activitytype choice
+ *
+ * @package    format_singleactivity
+ * @copyright  2013 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class format_singleactivity_admin_setting_activitytype extends admin_setting_configselect {
+    /**
+     * This function may be used in ancestors for lazy loading of choices
+     *
+     * Override this method if loading of choices is expensive, such
+     * as when it requires multiple db requests.
+     *
+     * @return bool true if loaded, false if error
+     */
+    public function load_choices() {
+        global $CFG;
+        require_once($CFG->dirroot. '/course/lib.php');
+        if (is_array($this->choices)) {
+            return true;
+        }
+        $this->choices = get_module_types_names();
+        return true;
+    }
+}
diff --git a/course/format/singleactivity/styles.css b/course/format/singleactivity/styles.css
new file mode 100644 (file)
index 0000000..65f7875
--- /dev/null
@@ -0,0 +1,10 @@
+/* Hide confusing form elements "Display description on course page" and
+"Save and return to course" from module edit form because they
+are not applicable in single activity course format */
+body.format-singleactivity.path-mod.pagelayout-admin form.mform #fitem_id_showdescription,
+body.format-singleactivity.path-mod.pagelayout-admin form.mform .fitem_actionbuttons#fgroup_id_buttonar #id_submitbutton {display:none;}
+
+/* In mod_quiz hide "Back to course" button */
+body.format-singleactivity.path-mod-quiz .quizattempt .continuebutton {display:none;}
+
+body.format-singleactivity .tree_item.orphaned a {color:red;}
diff --git a/course/format/singleactivity/version.php b/course/format/singleactivity/version.php
new file mode 100644 (file)
index 0000000..dea4103
--- /dev/null
@@ -0,0 +1,29 @@
+<?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/>.
+
+/**
+ * Version details
+ *
+ * @package    format_singleactivity
+ * @copyright  2012 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2013071200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2013070800;        // Requires this Moodle version (2.6)
+$plugin->component = 'format_singleactivity';    // Full name of the plugin (used for diagnostics).
diff --git a/enrol/category/classes/observer.php b/enrol/category/classes/observer.php
new file mode 100644 (file)
index 0000000..2b8a8d7
--- /dev/null
@@ -0,0 +1,153 @@
+<?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/>.
+
+/**
+ * Local stuff for category enrolment plugin.
+ *
+ * @package    enrol_category
+ * @copyright  2010 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Event handler for category enrolment plugin.
+ *
+ * We try to keep everything in sync via listening to events,
+ * it may fail sometimes, so we always do a full sync in cron too.
+ */
+class enrol_category_observer {
+    /**
+     * Triggered when user is assigned a new role.
+     *
+     * @param \core\event\role_assigned $event
+     */
+    public static function role_assigned(\core\event\role_assigned $event) {
+        global $DB;
+
+        if (!enrol_is_enabled('category')) {
+            return;
+        }
+
+        $ra = new stdClass();
+        $ra->roleid = $event->objectid;
+        $ra->userid = $event->relateduserid;
+        $ra->contextid = $event->contextid;
+
+        //only category level roles are interesting
+        $parentcontext = context::instance_by_id($ra->contextid);
+        if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
+            return;
+        }
+
+        // Make sure the role is to be actually synchronised,
+        // please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync).
+        $syscontext = context_system::instance();
+        if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
+            return;
+        }
+
+        // Add necessary enrol instances.
+        $plugin = enrol_get_plugin('category');
+        $sql = "SELECT c.*
+                  FROM {course} c
+                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
+             LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
+                 WHERE e.id IS NULL";
+        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%');
+        $rs = $DB->get_recordset_sql($sql, $params);
+        foreach ($rs as $course) {
+            $plugin->add_instance($course);
+        }
+        $rs->close();
+
+        // Now look for missing enrolments.
+        $sql = "SELECT e.*
+                  FROM {course} c
+                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
+                  JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
+             LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
+                 WHERE ue.id IS NULL";
+        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
+        $rs = $DB->get_recordset_sql($sql, $params);
+        foreach ($rs as $instance) {
+            $plugin->enrol_user($instance, $ra->userid, null, time());
+        }
+        $rs->close();
+    }
+
+    /**
+     * Triggered when user role is unassigned.
+     *
+     * @param \core\event\role_unassigned $event
+     */
+    public static function role_unassigned(\core\event\role_unassigned $event) {
+        global $DB;
+
+        if (!enrol_is_enabled('category')) {
+            return;
+        }
+
+        $ra = new stdClass();
+        $ra->userid = $event->relateduserid;
+        $ra->contextid = $event->contextid;
+
+        // only category level roles are interesting
+        $parentcontext = context::instance_by_id($ra->contextid);
+        if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
+            return;
+        }
+
+        // Now this is going to be a bit slow, take all enrolments in child courses and verify each separately.
+        $syscontext = context_system::instance();
+        if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
+            return;
+        }
+
+        $plugin = enrol_get_plugin('category');
+
+        $sql = "SELECT e.*
+                  FROM {course} c
+                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
+                  JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
+                  JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)";
+        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
+        $rs = $DB->get_recordset_sql($sql, $params);
+
+        list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
+        $params['userid'] = $ra->userid;
+
+        foreach ($rs as $instance) {
+            $coursecontext = context_course::instance($instance->courseid);
+            $contextids = $coursecontext->get_parent_context_ids();
+            array_pop($contextids); // Remove system context, we are interested in categories only.
+
+            list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
+            $params = array_merge($params, $contextparams);
+
+            $sql = "SELECT ra.id
+                      FROM {role_assignments} ra
+                     WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
+            if (!$DB->record_exists_sql($sql, $params)) {
+                // User does not have any interesting role in any parent context, let's unenrol.
+                $plugin->unenrol_user($instance, $ra->userid);
+            }
+        }
+        $rs->close();
+    }
+}
index 3a67866..1fea12c 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-/* List of handlers */
-$handlers = array (
-    'role_assigned' => array (
-        'handlerfile'      => '/enrol/category/locallib.php',
-        'handlerfunction'  => array('enrol_category_handler', 'role_assigned'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+$observers = array (
+
+    array (
+        'eventname' => '\core\event\role_assigned',
+        'callback'  => 'enrol_category_observer::role_assigned',
     ),
 
-    'role_unassigned' => array (
-        'handlerfile'      => '/enrol/category/locallib.php',
-        'handlerfunction'  => array('enrol_category_handler', 'role_unassigned'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+    array (
+        'eventname' => '\core\event\role_unassigned',
+        'callback'  => 'enrol_category_observer::role_unassigned',
     ),
 
-);
\ No newline at end of file
+);
index 4240276..404ff8f 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-
-/**
- * Event handler for category enrolment plugin.
- *
- * We try to keep everything in sync via listening to events,
- * it may fail sometimes, so we always do a full sync in cron too.
- */
-class enrol_category_handler {
-    /**
-     * Triggered when user is assigned a new role.
-     * @static
-     * @param stdClass $ra
-     * @return bool
-     */
-    public static function role_assigned($ra) {
-        global $DB;
-
-        if (!enrol_is_enabled('category')) {
-            return true;
-        }
-
-        //only category level roles are interesting
-        $parentcontext = context::instance_by_id($ra->contextid);
-        if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
-            return true;
-        }
-
-        // Make sure the role is to be actually synchronised,
-        // please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync).
-        $syscontext = context_system::instance();
-        if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
-            return true;
-        }
-
-        // Add necessary enrol instances.
-        $plugin = enrol_get_plugin('category');
-        $sql = "SELECT c.*
-                  FROM {course} c
-                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
-             LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
-                 WHERE e.id IS NULL";
-        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%');
-        $rs = $DB->get_recordset_sql($sql, $params);
-        foreach ($rs as $course) {
-            $plugin->add_instance($course);
-        }
-        $rs->close();
-
-        // Now look for missing enrolments.
-        $sql = "SELECT e.*
-                  FROM {course} c
-                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
-                  JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
-             LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
-                 WHERE ue.id IS NULL";
-        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
-        $rs = $DB->get_recordset_sql($sql, $params);
-        foreach ($rs as $instance) {
-            $plugin->enrol_user($instance, $ra->userid, null, $ra->timemodified);
-        }
-        $rs->close();
-
-        return true;
-    }
-
-    /**
-     * Triggered when user role is unassigned.
-     * @static
-     * @param stdClass $ra
-     * @return bool
-     */
-    public static function role_unassigned($ra) {
-        global $DB;
-
-        if (!enrol_is_enabled('category')) {
-            return true;
-        }
-
-        // only category level roles are interesting
-        $parentcontext = context::instance_by_id($ra->contextid);
-        if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
-            return true;
-        }
-
-        // Now this is going to be a bit slow, take all enrolments in child courses and verify each separately.
-        $syscontext = context_system::instance();
-        if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
-            return true;
-        }
-
-        $plugin = enrol_get_plugin('category');
-
-        $sql = "SELECT e.*
-                  FROM {course} c
-                  JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
-                  JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')
-                  JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)";
-        $params = array('courselevel'=>CONTEXT_COURSE, 'match'=>$parentcontext->path.'/%', 'userid'=>$ra->userid);
-        $rs = $DB->get_recordset_sql($sql, $params);
-
-        list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
-        $params['userid'] = $ra->userid;
-
-        foreach ($rs as $instance) {
-            $coursecontext = context_course::instance($instance->courseid);
-            $contextids = $coursecontext->get_parent_context_ids();
-            array_pop($contextids); // Remove system context, we are interested in categories only.
-
-            list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
-            $params = array_merge($params, $contextparams);
-
-            $sql = "SELECT ra.id
-                      FROM {role_assignments} ra
-                     WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
-            if (!$DB->record_exists_sql($sql, $params)) {
-                // User does not have any interesting role in any parent context, let's unenrol.
-                $plugin->unenrol_user($instance, $ra->userid);
-            }
-        }
-        $rs->close();
-
-        return true;
-    }
-}
-
 /**
  * Sync all category enrolments in one course
  * @param stdClass $course
similarity index 98%
rename from enrol/category/tests/sync_test.php
rename to enrol/category/tests/plugin_test.php
index ca8d4cd..3d9ab46 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-global $CFG;
-require_once($CFG->dirroot.'/enrol/category/locallib.php');
-
-class enrol_category_testcase extends advanced_testcase {
+class enrol_category_plugin_testcase extends advanced_testcase {
 
     protected function enable_plugin() {
         $enabled = enrol_get_plugins(true);
@@ -107,7 +104,8 @@ class enrol_category_testcase extends advanced_testcase {
     }
 
     public function test_handler_sync() {
-        global $DB;
+        global $DB, $CFG;
+        require_once($CFG->dirroot.'/enrol/category/locallib.php');
 
         $this->resetAfterTest();
 
@@ -179,7 +177,8 @@ class enrol_category_testcase extends advanced_testcase {
     }
 
     public function test_sync_course() {
-        global $DB;
+        global $DB, $CFG;
+        require_once($CFG->dirroot.'/enrol/category/locallib.php');
 
         $this->resetAfterTest();
 
@@ -272,7 +271,8 @@ class enrol_category_testcase extends advanced_testcase {
     }
 
     public function test_sync_full() {
-        global $DB;
+        global $DB, $CFG;
+        require_once($CFG->dirroot.'/enrol/category/locallib.php');
 
         $this->resetAfterTest();
 
index aae171e..5a61de4 100644 (file)
@@ -161,12 +161,16 @@ function grade_import_commit($courseid, $importcode, $importfeedback=true, $verb
  */
 function get_unenrolled_users_in_import($importcode, $courseid) {
     global $CFG, $DB;
-    $relatedctxcondition = get_related_contexts_string(context_course::instance($courseid));
 
-    //users with a gradeable role
+    $coursecontext = context_course::instance($courseid);
+
+    // We want to query both the current context and parent contexts.
+    list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+    // Users with a gradeable role.
     list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
 
-    //enrolled users
+    // Enrolled users.
     $context = context_course::instance($courseid);
     list($enrolledsql, $enrolledparams) = get_enrolled_sql($context);
     list($sort, $sortparams) = users_order_by_sql('u');
@@ -183,11 +187,11 @@ function get_unenrolled_users_in_import($importcode, $courseid) {
               LEFT JOIN ($enrolledsql) je
                         ON je.id = u.id
               LEFT JOIN {role_assignments} ra
-                        ON (giv.userid = ra.userid AND ra.roleid $gradebookrolessql AND ra.contextid $relatedctxcondition)
+                        ON (giv.userid = ra.userid AND ra.roleid $gradebookrolessql AND ra.contextid $relatedctxsql)
              WHERE giv.importcode = :importcode
                    AND (ra.id IS NULL OR je.id IS NULL)
           ORDER BY gradeidnumber, $sort";
-    $params = array_merge($gradebookrolesparams, $enrolledparams, $sortparams);
+    $params = array_merge($gradebookrolesparams, $enrolledparams, $sortparams, $relatedctxparams);
     $params['importcode'] = $importcode;
 
     return $DB->get_records_sql($sql, $params);
index c08ff66..3603ac4 100644 (file)
@@ -138,18 +138,17 @@ class graded_users_iterator {
         export_verify_grades($this->course->id);
         $course_item = grade_item::fetch_course_item($this->course->id);
         if ($course_item->needsupdate) {
-            // can not calculate all final grades - sorry
+            // Can not calculate all final grades - sorry.
             return false;
         }
 
         $coursecontext = context_course::instance($this->course->id);
-        $relatedcontexts = get_related_contexts_string($coursecontext);
 
-        list($gradebookroles_sql, $params) =
-            $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
+        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+        list($gradebookroles_sql, $params) = $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
         list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, '', 0, $this->onlyactive);
 
-        $params = array_merge($params, $enrolledparams);
+        $params = array_merge($params, $enrolledparams, $relatedctxparams);
 
         if ($this->groupid) {
             $groupsql = "INNER JOIN {groups_members} gm ON gm.userid = u.id";
@@ -162,7 +161,7 @@ class graded_users_iterator {
         }
 
         if (empty($this->sortfield1)) {
-            // we must do some sorting even if not specified
+            // We must do some sorting even if not specified.
             $ofields = ", u.id AS usrt";
             $order   = "usrt ASC";
 
@@ -174,8 +173,8 @@ class graded_users_iterator {
                 $order   .= ", usrt2 $this->sortorder2";
             }
             if ($this->sortfield1 != 'id' and $this->sortfield2 != 'id') {
-                // user order MUST be the same in both queries,
-                // must include the only unique user->id if not already present
+                // User order MUST be the same in both queries,
+                // must include the only unique user->id if not already present.
                 $ofields .= ", u.id AS usrt";
                 $order   .= ", usrt ASC";
             }
@@ -199,7 +198,6 @@ class graded_users_iterator {
             }
         }
 
-        // $params contents: gradebookroles and groupid (for $groupwheresql)
         $users_sql = "SELECT $userfields $ofields
                         FROM {user} u
                         JOIN ($enrolledsql) je ON je.id = u.id
@@ -208,7 +206,7 @@ class graded_users_iterator {
                                   SELECT DISTINCT ra.userid
                                     FROM {role_assignments} ra
                                    WHERE ra.roleid $gradebookroles_sql
-                                     AND ra.contextid $relatedcontexts
+                                     AND ra.contextid $relatedctxsql
                              ) rainner ON rainner.userid = u.id
                          WHERE u.deleted = 0
                              $groupwheresql
@@ -226,7 +224,6 @@ class graded_users_iterator {
             $itemids = array_keys($this->grade_items);
             list($itemidsql, $grades_params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED, 'items');
             $params = array_merge($params, $grades_params);
-            // $params contents: gradebookroles, enrolledparams, groupid (for $groupwheresql) and itemids
 
             $grades_sql = "SELECT g.* $ofields
                              FROM {grade_grades} g
@@ -237,7 +234,7 @@ class graded_users_iterator {
                                       SELECT DISTINCT ra.userid
                                         FROM {role_assignments} ra
                                        WHERE ra.roleid $gradebookroles_sql
-                                         AND ra.contextid $relatedcontexts
+                                         AND ra.contextid $relatedctxsql
                                   ) rainner ON rainner.userid = u.id
                               WHERE u.deleted = 0
                               AND g.itemid $itemidsql
@@ -418,7 +415,7 @@ function print_graded_users_selector($course, $actionpage, $userid=0, $groupid=0
 }
 
 function grade_get_graded_users_select($report, $course, $userid, $groupid, $includeall) {
-    global $USER;
+    global $USER, $CFG;
 
     if (is_null($userid)) {
         $userid = $USER->id;
index 9ee5b17..1c6fd33 100644 (file)
@@ -398,24 +398,25 @@ class grade_report_grader extends grade_report {
             return;
         }
 
-        //limit to users with a gradeable role
+        // Limit to users with a gradeable role.
         list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 
-        //limit to users with an active enrollment
+        // Limit to users with an active enrollment.
         list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
 
-        //fields we need from the user table
+        // Fields we need from the user table.
         $userfields = user_picture::fields('u', get_extra_user_fields($this->context));
 
-        $sortjoin = $sort = $params = null;
+        // We want to query both the current context and parent contexts.
+        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
 
-        //if the user has clicked one of the sort asc/desc arrows
+        // If the user has clicked one of the sort asc/desc arrows.
         if (is_numeric($this->sortitemid)) {
-            $params = array_merge(array('gitemid'=>$this->sortitemid), $gradebookrolesparams, $this->groupwheresql_params, $enrolledparams);
+            $params = array_merge(array('gitemid' => $this->sortitemid), $gradebookrolesparams, $this->groupwheresql_params, $enrolledparams,
+                $relatedctxparams);
 
             $sortjoin = "LEFT JOIN {grade_grades} g ON g.userid = u.id AND g.itemid = $this->sortitemid";
             $sort = "g.finalgrade $this->sortorder";
-
         } else {
             $sortjoin = '';
             switch($this->sortitemid) {
@@ -434,7 +435,7 @@ class grade_report_grader extends grade_report {
                     break;
             }
 
-            $params = array_merge($gradebookrolesparams, $this->groupwheresql_params, $enrolledparams);
+            $params = array_merge($gradebookrolesparams, $this->groupwheresql_params, $enrolledparams, $relatedctxparams);
         }
 
         $sql = "SELECT $userfields
@@ -446,12 +447,11 @@ class grade_report_grader extends grade_report {
                            SELECT DISTINCT ra.userid
                              FROM {role_assignments} ra
                             WHERE ra.roleid IN ($this->gradebookroles)
-                              AND ra.contextid " . get_related_contexts_string($this->context) . "
+                              AND ra.contextid $relatedctxsql
                        ) rainner ON rainner.userid = u.id
                    AND u.deleted = 0
                    $this->groupwheresql
               ORDER BY $sort";
-
         $studentsperpage = $this->get_students_per_page();
         $this->users = $DB->get_records_sql($sql, $params, $studentsperpage * $this->page, $studentsperpage);
 
@@ -1277,56 +1277,46 @@ class grade_report_grader extends grade_report {
      * @return array Array of rows for the right part of the report
      */
     public function get_right_avg_row($rows=array(), $grouponly=false) {
-        global $CFG, $USER, $DB, $OUTPUT;
+        global $USER, $DB, $OUTPUT;
 
         if (!$this->canviewhidden) {
-            // totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
-            // better not show them at all if user can not see all hidden grades
+            // Totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
+            // better not show them at all if user can not see all hidden grades.
             return $rows;
         }
 
-        $showaverages = $this->get_pref('showaverages');
-        $showaveragesgroup = $this->currentgroup && $showaverages;
-
         $averagesdisplaytype   = $this->get_pref('averagesdisplaytype');
         $averagesdecimalpoints = $this->get_pref('averagesdecimalpoints');
         $meanselection         = $this->get_pref('meanselection');
         $shownumberofgrades    = $this->get_pref('shownumberofgrades');
 
-        $avghtml = '';
-        $avgcssclass = 'avg';
-
         if ($grouponly) {
-            $straverage = get_string('groupavg', 'grades');
             $showaverages = $this->currentgroup && $this->get_pref('showaverages');
             $groupsql = $this->groupsql;
             $groupwheresql = $this->groupwheresql;
             $groupwheresqlparams = $this->groupwheresql_params;
-            $avgcssclass = 'groupavg';
         } else {
-            $straverage = get_string('overallaverage', 'grades');
             $showaverages = $this->get_pref('showaverages');
             $groupsql = "";
             $groupwheresql = "";
             $groupwheresqlparams = array();
         }
 
-        if ($shownumberofgrades) {
-            $straverage .= ' (' . get_string('submissions', 'grades') . ') ';
-        }
+        if ($showaverages) {
+            $totalcount = $this->get_numusers($grouponly);
 
-        $totalcount = $this->get_numusers($grouponly);
+            // Limit to users with a gradeable role.
+            list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 
-        //limit to users with a gradeable role
-        list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
+            // Limit to users with an active enrollment.
+            list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
 
-        //limit to users with an active enrollment
-        list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
+            // We want to query both the current context and parent contexts.
+            list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
 
-        if ($showaverages) {
-            $params = array_merge(array('courseid'=>$this->courseid), $gradebookrolesparams, $enrolledparams, $groupwheresqlparams);
+            $params = array_merge(array('courseid' => $this->courseid), $gradebookrolesparams, $enrolledparams, $groupwheresqlparams, $relatedctxparams);
 
-            // find sums of all grade items in course
+            // Find sums of all grade items in course.
             $sql = "SELECT g.itemid, SUM(g.finalgrade) AS sum
                       FROM {grade_items} gi
                       JOIN {grade_grades} g ON g.itemid = gi.id
@@ -1336,7 +1326,7 @@ class grade_report_grader extends grade_report {
                                SELECT DISTINCT ra.userid
                                  FROM {role_assignments} ra
                                 WHERE ra.roleid $gradebookrolessql
-                                  AND ra.contextid " . get_related_contexts_string($this->context) . "
+                                  AND ra.contextid $relatedctxsql
                            ) rainner ON rainner.userid = u.id
                       $groupsql
                      WHERE gi.courseid = :courseid
@@ -1365,7 +1355,7 @@ class grade_report_grader extends grade_report {
                       $groupsql
                      WHERE gi.courseid = :courseid
                            AND ra.roleid $gradebookrolessql
-                           AND ra.contextid ".get_related_contexts_string($this->context)."
+                           AND ra.contextid $relatedctxsql
                            AND u.deleted = 0
                            AND g.id IS NULL
                            $groupwheresql
@@ -1403,8 +1393,6 @@ class grade_report_grader extends grade_report {
                     $meancount = $totalcount;
                 }
 
-                $decimalpoints = $item->get_decimals();
-
                 // Determine which display type to use for this average
                 if ($USER->gradeediting[$this->courseid]) {
                     $displaytype = GRADE_DISPLAY_TYPE_REAL;
index 38459d8..512a8c2 100644 (file)
@@ -271,18 +271,21 @@ abstract class grade_report {
      * @return int Count of users
      */
     public function get_numusers($groups=true) {
-        global $CFG, $DB;
+        global $DB;
 
         $groupsql      = "";
         $groupwheresql = "";
 
-        //limit to users with a gradeable role
+        // Limit to users with a gradeable role.
         list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 
-        //limit to users with an active enrollment
+        // Limit to users with an active enrollment.
         list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
 
-        $params = array_merge($gradebookrolesparams, $enrolledparams);
+        // We want to query both the current context and parent contexts.
+        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+        $params = array_merge($gradebookrolesparams, $enrolledparams, $relatedctxparams);
 
         if ($groups) {
             $groupsql      = $this->groupsql;
@@ -300,7 +303,7 @@ abstract class grade_report {
                       WHERE ra.roleid $gradebookrolessql
                             AND u.deleted = 0
                             $groupwheresql
-                            AND ra.contextid ".get_related_contexts_string($this->context);
+                            AND ra.contextid $relatedctxsql";
         return $DB->count_records_sql($countsql, $params);
     }
 
index d9232ed..c5920ff 100644 (file)
@@ -611,42 +611,34 @@ class grade_report_user extends grade_report {
 
     /**
      * Builds the grade item averages.
-     *
      */
     function calculate_averages() {
         global $USER, $DB;
 
         if ($this->showaverage) {
-            // this settings are actually grader report settings (not user report)
+            // This settings are actually grader report settings (not user report)
             // however we're using them as having two separate but identical settings the
-            // user would have to keep in sync would be annoying
+            // user would have to keep in sync would be annoying.
             $averagesdisplaytype   = $this->get_pref('averagesdisplaytype');
             $averagesdecimalpoints = $this->get_pref('averagesdecimalpoints');
             $meanselection         = $this->get_pref('meanselection');
             $shownumberofgrades    = $this->get_pref('shownumberofgrades');
 
             $avghtml = '';
-            $avgcssclass = 'avg';
-
-            $straverage = get_string('overallaverage', 'grades');
-
             $groupsql = $this->groupsql;
             $groupwheresql = $this->groupwheresql;
-            //$groupwheresqlparams = ;
-
-            if ($shownumberofgrades) {
-                $straverage .= ' (' . get_string('submissions', 'grades') . ') ';
-            }
-
             $totalcount = $this->get_numusers(false);
 
-            //limit to users with a gradeable role ie students
+            // We want to query both the current context and parent contexts.
+            list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+            // Limit to users with a gradeable role ie students.
             list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 
-            //limit to users with an active enrolment
+            // Limit to users with an active enrolment.
             list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
 
-            $params = array_merge($this->groupwheresql_params, $gradebookrolesparams, $enrolledparams);
+            $params = array_merge($this->groupwheresql_params, $gradebookrolesparams, $enrolledparams, $relatedctxparams);
             $params['courseid'] = $this->courseid;
 
             // find sums of all grade items in course
@@ -659,7 +651,7 @@ class grade_report_user extends grade_report {
                                    SELECT DISTINCT ra.userid
                                      FROM {role_assignments} ra
                                     WHERE ra.roleid $gradebookrolessql
-                                      AND ra.contextid " . get_related_contexts_string($this->context) . "
+                                      AND ra.contextid $relatedctxsql
                            ) rainner ON rainner.userid = u.id
                       $groupsql
                      WHERE gi.courseid = :courseid
@@ -690,7 +682,7 @@ class grade_report_user extends grade_report {
                                SELECT DISTINCT ra.userid
                                  FROM {role_assignments} ra
                                 WHERE ra.roleid $gradebookrolessql
-                                  AND ra.contextid " . get_related_contexts_string($this->context) . "
+                                  AND ra.contextid $relatedctxsql
                            ) rainner ON rainner.userid = u.id
                       LEFT JOIN {grade_grades} gg
                              ON (gg.itemid = gi.id AND gg.userid = u.id AND gg.finalgrade IS NOT NULL AND gg.hidden = 0)
@@ -731,8 +723,6 @@ class grade_report_user extends grade_report {
                     $mean_count = $totalcount;
                 }
 
-                $decimalpoints = $item->get_decimals();
-
                 // Determine which display type to use for this average
                 if (!empty($USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
                     $displaytype = GRADE_DISPLAY_TYPE_REAL;
@@ -747,7 +737,6 @@ class grade_report_user extends grade_report {
                 // Override grade_item setting if a display preference (not inherit) was set for the averages
                 if ($averagesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
                     $decimalpoints = $item->get_decimals();
-
                 } else {
                     $decimalpoints = $averagesdecimalpoints;
                 }
index 86d23f7..f3f2f7e 100644 (file)
@@ -684,16 +684,16 @@ function groups_get_potential_members($courseid, $roleid = null, $cohortid = nul
 
     $context = context_course::instance($courseid);
 
-    // we are looking for all users with this role assigned in this context or higher
-    $listofcontexts = get_related_contexts_string($context);
-
     list($esql, $params) = get_enrolled_sql($context);
 
     if ($roleid) {
-        $params['roleid'] = $roleid;
+        // We are looking for all users with this role assigned in this context or higher.
+        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+        $params = array_merge($params, $relatedctxparams, array('roleid' => $roleid));
         $where = "WHERE u.id IN (SELECT userid
                                    FROM {role_assignments}
-                                  WHERE roleid = :roleid AND contextid $listofcontexts)";
+                                  WHERE roleid = :roleid AND contextid $relatedctxsql)";
     } else {
         $where = "";
     }
@@ -812,12 +812,15 @@ function groups_unassign_grouping($groupingid, $groupid, $invalidatecache = true
  */
 function groups_get_members_by_role($groupid, $courseid, $fields='u.*',
         $sort=null, $extrawheretest='', $whereorsortparams=array()) {
-    global $CFG, $DB;
+    global $DB;
 
     // Retrieve information about all users and their roles on the course or
     // parent ('related') contexts
     $context = context_course::instance($courseid);
 
+    // We are looking for all users with this role assigned in this context or higher.
+    list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
     if ($extrawheretest) {
         $extrawheretest = ' AND ' . $extrawheretest;
     }
@@ -830,12 +833,12 @@ function groups_get_members_by_role($groupid, $courseid, $fields='u.*',
     $sql = "SELECT r.id AS roleid, u.id AS userid, $fields
               FROM {groups_members} gm
               JOIN {user} u ON u.id = gm.userid
-         LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid ".get_related_contexts_string($context).")
+         LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid $relatedctxsql)
          LEFT JOIN {role} r ON r.id = ra.roleid
              WHERE gm.groupid=:mgroupid
                    ".$extrawheretest."
           ORDER BY r.sortorder, $sort";
-    $whereorsortparams['mgroupid'] = $groupid;
+    $whereorsortparams = array_merge($whereorsortparams, $relatedctxparams, array('mgroupid' => $groupid));
     $rs = $DB->get_recordset_sql($sql, $whereorsortparams);
 
     return groups_calculate_role_people($rs, $context);
index f591b00..412952b 100644 (file)
@@ -170,6 +170,7 @@ $CFG->httpswwwroot         = $CFG->wwwroot;
 $CFG->dataroot             = $config->dataroot;
 $CFG->tempdir              = $CFG->dataroot.'/temp';
 $CFG->cachedir             = $CFG->dataroot.'/cache';
+$CFG->localcachedir        = $CFG->dataroot.'/localcache';
 $CFG->admin                = $config->admin;
 $CFG->docroot              = 'http://docs.moodle.org';
 $CFG->langotherroot        = $CFG->dataroot.'/lang';
index 437c5dd..9e5b147 100644 (file)
@@ -198,6 +198,7 @@ $string['donotaward'] = 'Currently, this badge is not active, so it cannot be aw
 $string['editsettings'] = 'Edit settings';
 $string['enablebadges'] = 'Enable badges';
 $string['error:backpacknotavailable'] = 'Your site is not accessible from the Internet, so any badges issued from this site cannot be verified by external backpack services.';
+$string['error:backpackproblem'] = 'There was a problem connecting to your backpack service provider. Please try again later.';
 $string['error:cannotact'] = 'Cannot activate the badge. ';
 $string['error:cannotawardbadge'] = 'Cannot award badge to a user.';
 $string['error:clone'] = 'Cannot clone the badge.';
index 72e913d..be7bb40 100644 (file)
@@ -47,6 +47,7 @@ $string['cachedef_eventinvalidation'] = 'Event invalidation';
 $string['cachedef_groupdata'] = 'Course group information';
 $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
 $string['cachedef_locking'] = 'Locking';
+$string['cachedef_observers'] = 'Event observers';
 $string['cachedef_plugininfo_base'] = 'Plugin info - base';
 $string['cachedef_plugininfo_block'] = 'Plugin info - blocks';
 $string['cachedef_plugininfo_filter'] = 'Plugin info - filters';
index ed50168..1b4f87e 100644 (file)
@@ -1685,7 +1685,11 @@ function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0,
         reload_all_capabilities();
     }
 
-    events_trigger('role_assigned', $ra);
+    $event = \core\event\role_assigned::create(
+        array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
+            'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
+    $event->add_record_snapshot('role_assignments', $ra);
+    $event->trigger();
 
     return $ra->id;
 }
@@ -1769,8 +1773,12 @@ function role_unassign_all(array $params, $subcontexts = false, $includemanual =
             if (!empty($USER->id) && $USER->id == $ra->userid) {
                 reload_all_capabilities();
             }
+            $event = \core\event\role_unassigned::create(
+                array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
+                    'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
+            $event->add_record_snapshot('role_assignments', $ra);
+            $event->trigger();
         }
-        events_trigger('role_unassigned', $ra);
     }
     unset($ras);
 
@@ -1796,7 +1804,11 @@ function role_unassign_all(array $params, $subcontexts = false, $includemanual =
                     if (!empty($USER->id) && $USER->id == $ra->userid) {
                         reload_all_capabilities();
                     }
-                    events_trigger('role_unassigned', $ra);
+                    $event = \core\event\role_unassigned::create(
+                        array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
+                            'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
+                    $event->add_record_snapshot('role_assignments', $ra);
+                    $event->trigger();
                 }
             }
         }
@@ -6901,7 +6913,7 @@ class context_module extends context {
      * Is this context part of any course? If yes return course context.
      *
      * @param bool $strict true means throw exception if not found, false means return false if not found
-     * @return course_context context of the enclosing course, null if not found or exception
+     * @return context_course context of the enclosing course, null if not found or exception
      */
     public function get_course_context($strict = true) {
         return $this->get_parent_context();
@@ -7264,24 +7276,6 @@ function get_sorted_contexts($select, $params = array()) {
             ", $params);
 }
 
-/**
- * Gets a string for sql calls, searching for stuff in this context or above
- *
- * NOTE: use $DB->get_in_or_equal($context->get_parent_context_ids()...
- *
- * @deprecated since 2.2, $context->use get_parent_context_ids() instead
- * @param context $context
- * @return string
- */
-function get_related_contexts_string(context $context) {
-
-    if ($parents = $context->get_parent_context_ids()) {
-        return (' IN ('.$context->id.','.implode(',', $parents).')');
-    } else {
-        return (' ='.$context->id);
-    }
-}
-
 /**
  * Given context and array of users, returns array of users whose enrolment status is suspended,
  * or enrolment has expired or has not started. Also removes those users from the given array
index 8d879f7..1cff7fd 100644 (file)
@@ -1345,3 +1345,19 @@ function badges_handle_course_deletion($courseid) {
         $DB->update_record('badge', $toupdate);
     }
 }
+
+/**
+ * Loads JS files required for backpack support.
+ *
+ * @uses   $CFG, $PAGE
+ * @return void
+ */
+function badges_setup_backpack_js() {
+    global $CFG, $PAGE;
+    if (!empty($CFG->badges_allowexternalbackpack)) {
+        $PAGE->requires->string_for_js('error:backpackproblem', 'badges');
+        $protocol = (strpos($CFG->wwwroot, 'https://') === 0) ? 'https://' : 'http://';
+        $PAGE->requires->js(new moodle_url($protocol . 'backpack.openbadges.org/issuer.js'), true);
+        $PAGE->requires->js('/badges/backpack.js', true);
+    }
+}
index e35b0e6..559821e 100644 (file)
@@ -141,3 +141,35 @@ function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) {
     // Also use the internal error handler so we keep the usual behaviour.
     return false;
 }
+
+/**
+ * Restrict the config.php settings allowed.
+ *
+ * When running the behat features the config.php
+ * settings should not affect the results.
+ *
+ * @return void
+ */
+function behat_clean_init_config() {
+    global $CFG;
+
+    $allowed = array_flip(array(
+        'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
+        'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 'dboptions',
+        'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword', 'proxybypass',
+        'theme'
+    ));
+
+    // Add extra allowed settings.
+    if (!empty($CFG->behat_extraallowedsettings)) {
+        $allowed = array_merge($allowed, array_flip($CFG->behat_extraallowedsettings));
+    }
+
+    // Also allowing behat_ prefixed attributes.
+    foreach ($CFG as $key => $value) {
+        if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) {
+            unset($CFG->{$key});
+        }
+    }
+
+}
index 195ec41..09a9205 100644 (file)
@@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die();
  */
 class core_component {
     /** @var array list of ignored directories - watch out for auth/db exception */
-    protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true);
+    protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true, 'fonts'=>true);
     /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
     protected static $supportsubplugins = array('mod', 'editor', 'local');
 
diff --git a/lib/classes/event/base.php b/lib/classes/event/base.php
new file mode 100644 (file)
index 0000000..fd031d4
--- /dev/null
@@ -0,0 +1,617 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core\event;
+
+/**
+ * Base event class.
+ *
+ * @package    core
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * All other event classes must extend this class.
+ *
+ * @package    core
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ *
+ * @property-read string $eventname Name of the event (=== class name with leading \)
+ * @property-read string $component Full frankenstyle component name
+ * @property-read string $action what happened
+ * @property-read string $target what/who was target of the action
+ * @property-read string $objecttable name of database table where is object record stored
+ * @property-read int $objectid optional id of the object
+ * @property-read string $crud letter indicating event type
+ * @property-read int $level log level (number between 1 and 100)
+ * @property-read int $contextid
+ * @property-read int $contextlevel
+ * @property-read int $contextinstanceid
+ * @property-read int $userid who did this?
+ * @property-read int $courseid
+ * @property-read int $relateduserid
+ * @property-read mixed $other array or scalar, can not contain objects
+ * @property-read int $timecreated
+ */
+abstract class base implements \IteratorAggregate {
+    /** @var array event data */
+    protected $data;
+
+    /** @var array the format is standardised by logging API */
+    protected $logextra;
+
+    /** @var \context of this event */
+    protected $context;
+
+    /**
+     * @var bool indicates if event was already triggered,
+     *           this prevents second attempt to trigger event.
+     */
+    private $triggered;
+
+    /**
+     * @var bool indicates if event was already dispatched,
+     *           this prevents direct calling of manager::dispatch($event).
+     */
+    private $dispatched;
+
+    /**
+     * @var bool indicates if event was restored from storage,
+     *           this prevents triggering of restored events.
+     */
+    private $restored;
+
+    /** @var array list of event properties */
+    private static $fields = array(
+        'eventname', 'component', 'action', 'target', 'objecttable', 'objectid', 'crud', 'level', 'contextid',
+        'contextlevel', 'contextinstanceid', 'userid', 'courseid', 'relateduserid', 'other',
+        'timecreated');
+
+    /** @var array simple record cache */
+    private $recordsnapshots = array();
+
+    /**
+     * Private constructor, use create() or restore() methods instead.
+     */
+    private final function __construct() {
+        $this->data = array_fill_keys(self::$fields, null);
+    }
+
+    /**
+     * Create new event.
+     *
+     * The optional data keys as:
+     * 1/ objectid - the id of the object specified in class name
+     * 2/ context - the context of this event
+     * 3/ other - the other data describing the event, can not contain objects
+     * 4/ relateduserid - the id of user which is somehow related to this event
+     *
+     * @param array $data
+     * @return \core\event\base returns instance of new event
+     *
+     * @throws \coding_exception
+     */
+    public static final function create(array $data = null) {
+        global $PAGE, $USER;
+
+        $data = (array)$data;
+
+        /** @var \core\event\base $event */
+        $event = new static();
+        $event->triggered = false;
+        $event->restored = false;
+        $event->dispatched = false;
+
+        // Set static event data specific for child class.
+        $event->init();
+
+        // Set automatic data.
+        $event->data['timecreated'] = time();
+
+        $classname = get_class($event);
+        $parts = explode('\\', $classname);
+        if (count($parts) !== 3 or $parts[1] !== 'event') {
+            throw new \coding_exception("Invalid event class name '$classname', it must be defined in component\\event\\ namespace");
+        }
+        $event->data['eventname'] = '\\'.$classname;
+        $event->data['component'] = $parts[0];
+
+        $pos = strrpos($parts[2], '_');
+        if ($pos === false) {
+            throw new \coding_exception("Invalid event class name '$classname', there must be at least one underscore separating object and action words");
+        }
+        $event->data['target'] = substr($parts[2], 0, $pos);
+        $event->data['action'] = substr($parts[2], $pos+1);
+
+        // Set optional data or use defaults.
+        $event->data['objectid'] = isset($data['objectid']) ? $data['objectid'] : null;
+        $event->data['courseid'] = isset($data['courseid']) ? $data['courseid'] : null;
+        $event->data['userid'] = isset($data['userid']) ? $data['userid'] : $USER->id;
+        $event->data['other'] = isset($data['other']) ? $data['other'] : null;
+        $event->data['relateduserid'] = isset($data['relateduserid']) ? $data['relateduserid'] : null;
+
+        if (isset($event->context)) {
+            if (isset($data['context'])) {
+                debugging('Context was already set in init() method, ignoring context parameter', DEBUG_DEVELOPER);
+            }
+
+        } else if (!empty($data['context'])) {
+            $event->context = $data['context'];
+
+        } else if (!empty($data['contextid'])) {
+            $event->context = \context::instance_by_id($data['contextid'], MUST_EXIST);
+
+        } else {
+            throw new \coding_exception('context (or contextid) is a required event property, system context may be hardcoded in init() method.');
+        }
+
+        $event->data['contextid'] = $event->context->id;
+        $event->data['contextlevel'] = $event->context->contextlevel;
+        $event->data['contextinstanceid'] = $event->context->instanceid;
+
+        if (!isset($event->data['courseid'])) {
+            if ($coursecontext = $event->context->get_course_context(false)) {
+                $event->data['courseid'] = $coursecontext->id;
+            } else {
+                $event->data['courseid'] = 0;
+            }
+        }
+
+        if (!array_key_exists('relateduserid', $data) and $event->context->contextlevel == CONTEXT_USER) {
+            $event->data['relateduserid'] = $event->context->instanceid;
+        }
+
+        // Warn developers if they do something wrong.
+        if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
+            static $automatickeys = array('eventname', 'component', 'action', 'target', 'contextlevel', 'contextinstanceid', 'timecreated');
+            static $initkeys = array('crud', 'level', 'objecttable');
+
+            foreach ($data as $key => $ignored) {
+                if ($key === 'context') {
+                    continue;
+
+                } else if (in_array($key, $automatickeys)) {
+                    debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, it is set automatically");
+
+                } else if (in_array($key, $initkeys)) {
+                    debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, you need to set it in init() method");
+
+                } else if (!in_array($key, self::$fields)) {
+                    debugging("Data key '$key' does not exist in \\core\\event\\base");
+                }
+            }
+        }
+
+        // Let developers validate their custom data (such as $this->data['other'], contextlevel, etc.).
+        $event->validate_data();
+
+        return $event;
+    }
+
+    /**
+     * Override in subclass.
+     *
+     * Set all required data properties:
+     *  1/ crud - letter [crud]     TODO: MDL-37658
+     *  2/ level - number 1...100   TODO: MDL-37658
+     *  3/ objecttable - name of database table if objectid specified
+     *
+     * Optionally it can set:
+     * a/ fixed system context
+     *
+     * @return void
+     */
+    protected abstract function init();
+
+    /**
+     * Let developers validate their custom data (such as $this->data['other'], contextlevel, etc.).
+     *
+     * Throw \coding_exception or debugging() notice in case of any problems.
+     */
+    protected function validate_data() {
+        // Override if you want to validate event properties when
+        // creating new events.
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * Override in subclass, we can not make it static and abstract at the same time.
+     *
+     * TODO: MDL-37658
+     *
+     * @return string|\lang_string
+     */
+    public static function get_name() {
+        // Override in subclass with real lang string.
+        $parts = explode('\\', __CLASS__);
+        if (count($parts) !== 3) {
+            return 'unknown event';
+        }
+        return $parts[0].': '.str_replace('_', ' ', $parts[2]);
+    }
+
+    /**
+     * Returns localised description of what happened.
+     *
+     * TODO: MDL-37658
+     *
+     * @return string|\lang_string
+     */
+    public function get_description() {
+        return null;
+    }
+
+    /**
+     * Define whether a user can view the event or not.
+     *
+     * @param int|\stdClass $user_or_id ID of the user.
+     * @return bool True if the user can view the event, false otherwise.
+     */
+    public function can_view($user_or_id = null) {
+        return is_siteadmin($user_or_id);
+    }
+
+    /**
+     * Restore event from existing historic data.
+     *
+     * @param array $data
+     * @param array $logextra the format is standardised by logging API
+     * @return bool|\core\event\base
+     */
+    public static final function restore(array $data, array $logextra) {
+        $classname = $data['eventname'];
+        $component = $data['component'];
+        $action = $data['action'];
+        $target = $data['target'];
+
+        // Security: make 100% sure this really is an event class.
+        if ($classname !== "\\{$component}\\event\\{$target}_{$action}") {
+            return false;
+        }
+
+        if (!class_exists($classname)) {
+            return false;
+        }
+        $event = new $classname();
+        if (!($event instanceof \core\event\base)) {
+            return false;
+        }
+
+        $event->restored = true;
+        $event->triggered = true;
+        $event->dispatched = true;
+        $event->logextra = $logextra;
+
+        foreach (self::$fields as $key) {
+            if (!array_key_exists($key, $data)) {
+                debugging("Event restore data must contain key $key");
+                $data[$key] = null;
+            }
+        }
+        if (count($data) != count(self::$fields)) {
+            foreach ($data as $key => $value) {
+                if (!in_array($key, self::$fields)) {
+                    debugging("Event restore data cannot contain key $key");
+                    unset($data[$key]);
+                }
+            }
+        }
+        $event->data = $data;
+
+        return $event;
+    }
+
+    /**
+     * Returns event context.
+     * @return \context
+     */
+    public function get_context() {
+        if (isset($this->context)) {
+            return $this->context;
+        }
+        $this->context = \context::instance_by_id($this->data['contextid'], false);
+        return $this->context;
+    }
+
+    /**
+     * Returns relevant URL, override in subclasses.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return null;
+    }
+
+    /**
+     * Return standardised event data as array.
+     *
+     * @return array
+     */
+    public function get_data() {
+        return $this->data;
+    }
+
+    /**
+     * Return auxiliary data that was stored in logs.
+     *
+     * TODO: MDL-37658
+     *
+     * @return array the format is standardised by logging API
+     */
+    public function get_logextra() {
+        return $this->logextra;
+    }
+
+    /**
+     * Does this event replace legacy event?
+     *
+     * Note: do not use directly!
+     *
+     * @return null|string legacy event name
+     */
+    protected function get_legacy_eventname() {
+        return null;
+    }
+
+    /**
+     * Legacy event data if get_legacy_eventname() is not empty.
+     *
+     * Note: do not use directly!
+     *
+     * @return mixed
+     */
+    protected function get_legacy_eventdata() {
+        return null;
+    }
+
+    /**
+     * Doest this event replace add_to_log() statement?
+     *
+     * Note: do not use directly!
+     *
+     * @return null|array of parameters to be passed to legacy add_to_log() function.
+     */
+    protected function get_legacy_logdata() {
+        return null;
+    }
+
+    /**
+     * Validate all properties right before triggering the event.
+     *
+     * This throws coding exceptions for fatal problems and debugging for minor problems.
+     *
+     * @throws \coding_exception
+     */
+    protected final function validate_before_trigger() {
+        global $DB;
+
+        if (empty($this->data['crud'])) {
+            throw new \coding_exception('crud must be specified in init() method of each method');
+        }
+        if (empty($this->data['level'])) {
+            throw new \coding_exception('level must be specified in init() method of each method');
+        }
+        if (!empty($this->data['objectid']) and empty($this->data['objecttable'])) {
+            throw new \coding_exception('objecttable must be specified in init() method if objectid present');
+        }
+
+        if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
+            // Ideally these should be coding exceptions, but we need to skip these for performance reasons
+            // on production servers.
+
+            if (!in_array($this->data['crud'], array('c', 'r', 'u', 'd'), true)) {
+                debugging("Invalid event crud value specified.");
+            }
+            if (!is_number($this->data['level'])) {
+                debugging('Event property level must be a number');
+            }
+            if (self::$fields !== array_keys($this->data)) {
+                debugging('Number of event data fields must not be changed in event classes');
+            }
+            $encoded = json_encode($this->data['other']);
+            if ($encoded === false or $this->data['other'] !== json_decode($encoded, true)) {
+                debugging('other event data must be compatible with json encoding');
+            }
+            if ($this->data['userid'] and !is_number($this->data['userid'])) {
+                debugging('Event property userid must be a number');
+            }
+            if ($this->data['courseid'] and !is_number($this->data['courseid'])) {
+                debugging('Event property courseid must be a number');
+            }
+            if ($this->data['objectid'] and !is_number($this->data['objectid'])) {
+                debugging('Event property objectid must be a number');
+            }
+            if ($this->data['relateduserid'] and !is_number($this->data['relateduserid'])) {
+                debugging('Event property relateduserid must be a number');
+            }
+            if ($this->data['objecttable']) {
+                if (!$DB->get_manager()->table_exists($this->data['objecttable'])) {
+                    debugging('Unknown table specified in objecttable field');
+                }
+            }
+        }
+    }
+
+    /**
+     * Trigger event.
+     */
+    public final function trigger() {
+        global $CFG;
+
+        if ($this->restored) {
+            throw new \coding_exception('Can not trigger restored event');
+        }
+        if ($this->triggered or $this->dispatched) {
+            throw new \coding_exception('Can not trigger event twice');
+        }
+
+        $this->validate_before_trigger();
+
+        $this->triggered = true;
+
+        if (isset($CFG->loglifetime) and $CFG->loglifetime != -1) {
+            if ($data = $this->get_legacy_logdata()) {
+                call_user_func_array('add_to_log', $data);
+            }
+        }
+
+        if (PHPUNIT_TEST and \phpunit_util::is_redirecting_events()) {
+            $this->dispatched = true;
+            \phpunit_util::event_triggered($this);
+            return;
+        }
+
+        \core\event\manager::dispatch($this);
+
+        $this->dispatched = true;
+
+        if ($legacyeventname = $this->get_legacy_eventname()) {
+            events_trigger($legacyeventname, $this->get_legacy_eventdata());
+        }
+    }
+
+    /**
+     * Was this event already triggered?
+     *
+     * @return bool
+     */
+    public final function is_triggered() {
+        return $this->triggered;
+    }
+
+    /**
+     * Used from event manager to prevent direct access.
+     *
+     * @return bool
+     */
+    public final function is_dispatched() {
+        return $this->dispatched;
+    }
+
+    /**
+     * Was this event restored?
+     *
+     * @return bool
+     */
+    public final function is_restored() {
+        return $this->restored;
+    }
+
+    /**
+     * Add cached data that will be most probably used in event observers.
+     *
+     * This is used to improve performance, but it is required for data
+     * that was just deleted.
+     *
+     * @param string $tablename
+     * @param \stdClass $record
+     *
+     * @throws \coding_exception if used after ::trigger()
+     */
+    public final function add_record_snapshot($tablename, $record) {
+        global $DB;
+
+        if ($this->triggered) {
+            throw new \coding_exception('It is not possible to add snapshots after triggering of events');
+        }
+
+        // NOTE: this might use some kind of MUC cache,
+        //       hopefully we will not run out of memory here...
+        if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
+            if (!$DB->get_manager()->table_exists($tablename)) {
+                debugging("Invalid table name '$tablename' specified, database table does not exist.");
+            }
+        }
+        $this->recordsnapshots[$tablename][$record->id] = $record;
+    }
+
+    /**
+     * Returns cached record or fetches data from database if not cached.
+     *
+     * @param string $tablename
+     * @param int $id
+     * @return \stdClass
+     *
+     * @throws \coding_exception if used after ::restore()
+     */
+    public final function get_record_snapshot($tablename, $id) {
+        global $DB;
+
+        if ($this->restored) {
+            throw new \coding_exception('It is not possible to get snapshots from restored events');
+        }
+
+        if (isset($this->recordsnapshots[$tablename][$id])) {
+            return $this->recordsnapshots[$tablename][$id];
+        }
+
+        $record = $DB->get_record($tablename, array('id'=>$id));
+        $this->recordsnapshots[$tablename][$id] = $record;
+
+        return $record;
+    }
+
+    /**
+     * Magic getter for read only access.
+     *
+     * @param string $name
+     * @return mixed
+     */
+    public function __get($name) {
+        if (array_key_exists($name, $this->data)) {
+            return $this->data[$name];
+        }
+
+        debugging("Accessing non-existent event property '$name'");
+    }
+
+    /**
+     * Magic setter.
+     *
+     * Note: we must not allow modification of data from outside,
+     *       after trigger() the data MUST NOT CHANGE!!!
+     *
+     * @param string $name
+     * @param mixed $value
+     *
+     * @throws \coding_exception
+     */
+    public function __set($name, $value) {
+        throw new \coding_exception('Event properties must not be modified.');
+    }
+
+    /**
+     * Is data property set?
+     *
+     * @param string $name
+     * @return bool
+     */
+    public function __isset($name) {
+        return isset($this->data[$name]);
+    }
+
+    /**
+     * Create an iterator because magic vars can't be seen by 'foreach'.
+     *
+     * @return \ArrayIterator
+     */
+    public function getIterator() {
+        return new \ArrayIterator($this->data);
+    }
+}
diff --git a/lib/classes/event/manager.php b/lib/classes/event/manager.php
new file mode 100644 (file)
index 0000000..d384029
--- /dev/null
@@ -0,0 +1,349 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core\event;
+
+/**
+ * New event manager class.
+ *
+ * @package    core
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Class used for event dispatching.
+ *
+ * Note: Do NOT use directly in your code, it is intended to be used from
+ *       base event class only.
+ */
+class manager {
+    /** @var array buffer of event for dispatching */
+    protected static $buffer = array();
+
+    /** @var array buffer for events that were not sent to external observers when DB transaction in progress */
+    protected static $extbuffer = array();
+
+    /** @var bool evert dispatching already in progress - prevents nesting */
+    protected static $dispatching = false;
+
+    /** @var array cache of all observers */
+    protected static $allobservers = null;
+
+    /** @var bool should we reload observers after the test? */
+    protected static $reloadaftertest = false;
+
+    /**
+     * Trigger new event.
+     *
+     * @internal to be used only from \core\event\base::trigger() method.
+     * @param \core\event\base $event
+     *
+     * @throws \coding_Exception if used directly.
+     */
+    public static function dispatch(\core\event\base $event) {
+        if (during_initial_install()) {
+            return;
+        }
+        if (!$event->is_triggered() or $event->is_dispatched()) {
+            throw new \coding_exception('Illegal event dispatching attempted.');
+        }
+
+        self::$buffer[] = $event;
+
+        if (self::$dispatching) {
+            return;
+        }
+
+        self::$dispatching = true;
+        self::process_buffers();
+        self::$dispatching = false;
+    }
+
+    /**
+     * Notification from DML layer.
+     * @internal to be used from DML layer only.
+     */
+    public static function database_transaction_commited() {
+        if (self::$dispatching or empty(self::$extbuffer)) {
+            return;
+        }
+
+        self::$dispatching = true;
+        self::process_buffers();
+        self::$dispatching = false;
+    }
+
+    /**
+     * Notification from DML layer.
+     * @internal to be used from DML layer only.
+     */
+    public static function database_transaction_rolledback() {
+        self::$extbuffer = array();
+    }
+
+    protected static function process_buffers() {
+        global $DB, $CFG;
+
+        while (self::$buffer or self::$extbuffer) {
+
+            $fromextbuffer = false;
+            $addedtoextbuffer = false;
+
+            if (self::$extbuffer and !$DB->is_transaction_started()) {
+                $fromextbuffer = true;
+                $event = reset(self::$extbuffer);
+                unset(self::$extbuffer[key(self::$extbuffer)]);
+
+            } else if (self::$buffer) {
+                $event = reset(self::$buffer);
+                unset(self::$buffer[key(self::$buffer)]);
+
+            } else {
+                return;
+            }
+
+            $observers = self::get_event_observers('\\'.get_class($event));
+
+            foreach ($observers as $observer) {
+                if ($observer->internal) {
+                    if ($fromextbuffer) {
+                        // Do not send buffered external events to internal handlers,
+                        // they processed them already.
+                        continue;
+                    }
+                } else {
+                    if ($DB->is_transaction_started()) {
+                        if ($fromextbuffer) {
+                            // Weird!
+                            continue;
+                        }
+                        // Do not notify external observers while in DB transaction.
+                        if (!$addedtoextbuffer) {
+                            self::$extbuffer[] = $event;
+                            $addedtoextbuffer = true;
+                        }
+                        continue;
+                    }
+                }
+
+                if (isset($observer->includefile) and file_exists($observer->includefile)) {
+                    include_once($observer->includefile);
+                }
+                if (is_callable($observer->callable)) {
+                    try {
+                        call_user_func($observer->callable, $event);
+                    } catch (\Exception $e) {
+                        // Observers are notified before installation and upgrade, this may throw errors.
+                        if (empty($CFG->upgraderunning)) {
+                            // Ignore errors during upgrade, otherwise warn developers.
+                            debugging("Exception encountered in event observer '$observer->callable': ".$e->getMessage(), DEBUG_DEVELOPER, $e->getTrace());
+                        }
+                    }
+                } else {
+                    debugging("Can not execute event observer '$observer->callable'");
+                }
+            }
+
+            // TODO: Invent some infinite loop protection in case events cross-trigger one another.
+        }
+    }
+
+    /**
+     * Returns list of event observers.
+     * @param string $classname
+     * @return array
+     */
+    protected static function get_event_observers($classname) {
+        self::init_all_observers();
+
+        if (isset(self::$allobservers[$classname])) {
+            return self::$allobservers[$classname];
+        }
+
+        if (isset(self::$allobservers['*'])) {
+            return self::$allobservers['*'];
+        }
+
+        return array();
+    }
+
+    /**
+     * Initialise the list of observers.
+     */
+    protected static function init_all_observers() {
+        global $CFG;
+
+        if (is_array(self::$allobservers)) {
+            return;
+        }
+
+        if (!PHPUNIT_TEST and !during_initial_install()) {
+            $cache = \cache::make('core', 'observers');
+            $cached = $cache->get('all');
+            $dirroot = $cache->get('dirroot');
+            if ($dirroot === $CFG->dirroot and is_array($cached)) {
+                self::$allobservers = $cached;
+                return;
+            }
+        }
+
+        self::$allobservers = array();
+
+        $plugintypes = \core_component::get_plugin_types();
+        $systemdone = false;
+        foreach ($plugintypes as $plugintype => $ignored) {
+            $plugins = \core_component::get_plugin_list($plugintype);
+            if (!$systemdone) {
+                $plugins[] = "$CFG->dirroot/lib";
+                $systemdone = true;
+            }
+
+            foreach ($plugins as $fulldir) {
+                if (!file_exists("$fulldir/db/events.php")) {
+                    continue;
+                }
+                $observers = null;
+                include("$fulldir/db/events.php");
+                if (!is_array($observers)) {
+                    continue;
+                }
+                self::add_observers($observers, "$fulldir/db/events.php");
+            }
+        }
+
+        self::order_all_observers();
+
+        if (!PHPUNIT_TEST and !during_initial_install()) {
+            $cache->set('all', self::$allobservers);
+            $cache->set('dirroot', $CFG->dirroot);
+        }
+    }
+
+    /**
+     * Add observers.
+     * @param array $observers
+     * @param string $file
+     */
+    protected static function add_observers(array $observers, $file) {
+        global $CFG;
+
+        foreach ($observers as $observer) {
+            if (empty($observer['eventname']) or !is_string($observer['eventname'])) {
+                debugging("Invalid 'eventname' detected in $file observer definition", DEBUG_DEVELOPER);
+                continue;
+            }
+            if ($observer['eventname'] !== '*' and strpos($observer['eventname'], '\\') !== 0) {
+                $observer['eventname'] = '\\'.$observer['eventname'];
+            }
+            if (empty($observer['callback'])) {
+                debugging("Invalid 'callback' detected in $file observer definition", DEBUG_DEVELOPER);
+                continue;
+            }
+            $o = new \stdClass();
+            $o->callable = $observer['callback'];
+            if (!isset($observer['priority'])) {
+                $o->priority = 0;
+            } else {
+                $o->priority = (int)$observer['priority'];
+            }
+            if (!isset($observer['internal'])) {
+                $o->internal = true;
+            } else {
+                $o->internal = (bool)$observer['internal'];
+            }
+            if (empty($observer['includefile'])) {
+                $o->includefile = null;
+            } else {
+                if ($CFG->admin !== 'admin' and strpos($observer['includefile'], '/admin/') === 0) {
+                    $observer['includefile'] = preg_replace('|^/admin/|', '/'.$CFG->admin.'/', $observer['includefile']);
+                }
+                if (!file_exists($observer['includefile'])) {
+                    debugging("Invalid 'includefile' detected in $file observer definition", DEBUG_DEVELOPER);
+                    continue;
+                }
+                $o->includefile = $observer['includefile'];
+            }
+            self::$allobservers[$observer['eventname']][] = $o;
+        }
+    }
+
+    /**
+     * Reorder observers to allow quick lookup of observer for each event.
+     */
+    protected static function order_all_observers() {
+        $catchall = array();
+        if (isset(self::$allobservers['*'])) {
+            $catchall = self::$allobservers['*'];
+            unset(self::$allobservers['*']); // Move it to the end.
+            \core_collator::asort_objects_by_property($catchall, 'priority', \core_collator::SORT_NUMERIC);
+            $catchall = array_reverse($catchall);
+            self::$allobservers['*'] = $catchall;
+        }
+        foreach (self::$allobservers as $classname => $observers) {
+            if ($classname === '*') {
+                continue;
+            }
+            if ($catchall) {
+                $observers = array_merge($observers, $catchall);
+            }
+
+            \core_collator::asort_objects_by_property($observers, 'priority', \core_collator::SORT_NUMERIC);
+            self::$allobservers[$classname] = array_reverse($observers);
+        }
+    }
+
+    /**
+     * Replace all standard observers.
+     * @param array $observers
+     * @return array
+     *
+     * @throws \coding_Exception if used outside of unit tests.
+     */
+    public static function phpunit_replace_observers(array $observers) {
+        if (!PHPUNIT_TEST) {
+            throw new \coding_exception('Cannot override event observers outside of phpunit tests!');
+        }
+
+        self::phpunit_reset();
+        self::$allobservers = array();
+        self::$reloadaftertest = true;
+
+        self::add_observers($observers, 'phpunit');
+        self::order_all_observers();
+
+        return self::$allobservers;
+    }
+
+    /**
+     * Reset everything if necessary.
+     * @private
+     *
+     * @throws \coding_Exception if used outside of unit tests.
+     */
+    public static function phpunit_reset() {
+        if (!PHPUNIT_TEST) {
+            throw new \coding_exception('Cannot reset event manager outside of phpunit tests!');
+        }
+        self::$buffer = array();
+        self::$extbuffer = array();
+        self::$dispatching = false;
+        if (!self::$reloadaftertest) {
+            self::$allobservers = null;
+        }
+        self::$reloadaftertest = false;
+    }
+}
diff --git a/lib/classes/event/role_assigned.php b/lib/classes/event/role_assigned.php
new file mode 100644 (file)
index 0000000..79c7daa
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core\event;
+
+/**
+ * Role assigned event.
+ *
+ * @package    core
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+class role_assigned extends base {
+    protected function init() {
+        $this->data['objecttable'] = 'role';
+        $this->data['crud'] = 'c';
+        // TODO: MDL-37658 set level
+        $this->data['level'] = 50;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string|\lang_string
+     */
+    public static function get_name() {
+        //TODO: MDL-37658 localise
+        return 'Role assigned';
+    }
+
+    /**
+     * Returns localised description of what happened.
+     *
+     * @return string|\lang_string
+     */
+    public function get_description() {
+        //TODO: MDL-37658 localise
+        return 'Role '.$this->objectid.' was assigned to user '.$this->relateduserid.' in context '.$this->contextid;
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new moodle_url('/admin/roles/assign.php', array('contextid'=>$this->contextid, 'roleid'=>$this->objectid));
+    }
+
+    /**
+     * Does this event replace legacy event?
+     *
+     * @return null|string legacy event name
+     */
+    protected function get_legacy_eventname() {
+        return 'role_assigned';
+    }
+
+    /**
+     * Legacy event data if get_legacy_eventname() is not empty.
+     *
+     * @return mixed
+     */
+    protected function get_legacy_eventdata() {
+        return $this->get_record_snapshot('role_assignments', $this->data['other']['id']);
+    }
+}
diff --git a/lib/classes/event/role_unassigned.php b/lib/classes/event/role_unassigned.php
new file mode 100644 (file)
index 0000000..75501e8
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core\event;
+
+/**
+ * Role unassigned event.
+ *
+ * @package    core
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+class role_unassigned extends base {
+    protected function init() {
+        $this->data['objecttable'] = 'role';
+        $this->data['crud'] = 'd';
+        // TODO: MDL-37658 set level
+        $this->data['level'] = 50;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string|\lang_string
+     */
+    public static function get_name() {
+        //TODO: MDL-37658 localise
+        return 'Role unassigned';
+    }
+
+    /**
+     * Returns localised description of what happened.
+     *
+     * @return string|\lang_string
+     */
+    public function get_description() {
+        //TODO: MDL-37658 localise
+        return 'Role '.$this->objectid.'was unassigned from user '.$this->relateduserid.' in context '.$this->contextid;
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new moodle_url('/admin/roles/assign.php', array('contextid'=>$this->contextid, 'roleid'=>$this->objectid));
+    }
+
+    /**
+     * Does this event replace legacy event?
+     *
+     * @return null|string legacy event name
+     */
+    protected function get_legacy_eventname() {
+        return 'role_unassigned';
+    }
+
+    /**
+     * Legacy event data if get_legacy_eventname() is not empty.
+     *
+     * @return mixed
+     */
+    protected function get_legacy_eventdata() {
+        return $this->get_record_snapshot('role_assignments', $this->data['other']['id']);
+    }
+}
index e7683ea..fada229 100644 (file)
@@ -177,14 +177,17 @@ function search_users($courseid, $groupid, $searchtext, $sort='', array $excepti
 
         } else {
             $context = context_course::instance($courseid);
-            $contextlists = get_related_contexts_string($context);
+
+            // We want to query both the current context and parent contexts.
+            list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
 
             $sql = "SELECT u.id, u.firstname, u.lastname, u.email
                       FROM {user} u
                       JOIN {role_assignments} ra ON ra.userid = u.id
-                     WHERE $select AND ra.contextid $contextlists
+                     WHERE $select AND ra.contextid $relatedctxsql
                            $except
                     $order";
+            $params = array_merge($params, $relatedctxparams);
             return $DB->get_records_sql($sql, $params);
         }
     }
index 980837e..ffb2877 100644 (file)
@@ -121,6 +121,15 @@ $definitions = array(
         'mode' => cache_store::MODE_APPLICATION,
     ),
 
+    // Cache for the list of event observers.
+    'observers' => array(
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'persistent' => true,
+        'persistentmaxsize' => 2,
+    ),
+
     // Cache used by the {@link plugininfo_base} class.
     'plugininfo_base' => array(
         'mode' => cache_store::MODE_APPLICATION,
index f6ebc9e..1e354fc 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-/* List of handlers */
+/* List of legacy event handlers */
 
 $handlers = array(
 
-/*
- * portfolio queued event - for non interactive file transfers
- * NOTE: this is a HACK, please do not add any more things like this here
- *       (it is just abusing cron to do very time consuming things which is wrong any way)
- *
- * TODO: this has to be moved into separate queueing framework....
- */
-    'portfolio_send' => array (
-        'handlerfile'      => '/lib/portfolio.php',
-        'handlerfunction'  => 'portfolio_handle_event',    // argument to call_user_func(), could be an array
-        'schedule'         => 'cron',
-        'internal'         => 0,
-    ),
     'course_completed' => array (
         'handlerfile'      => '/lib/badgeslib.php',
         'handlerfunction'  => 'badges_award_handle_course_criteria_review',
@@ -69,6 +56,20 @@ $handlers = array(
         'internal'         => 1,
     ),
 
+/*
+ * portfolio queued event - for non interactive file transfers
+ * NOTE: this is a HACK, please do not add any more things like this here
+ *       (it is just abusing cron to do very time consuming things which is wrong any way)
+ *
+ * TODO: this has to be moved into separate queueing framework....
+ */
+    'portfolio_send' => array (
+        'handlerfile'      => '/lib/portfolio.php',
+        'handlerfunction'  => 'portfolio_handle_event',    // argument to call_user_func(), could be an array
+        'schedule'         => 'cron',
+        'internal'         => 0,
+    ),
+
 /* no more here please, core should not consume any events!!!!!!! */
 );
 
index ca55789..ff33108 100644 (file)
@@ -455,9 +455,6 @@ function is_course_participant($userid, $courseid) {
  *
  * used to print recent activity
  *
- * @todo MDL-36993 this function is still used in block_recent_activity, deprecate properly
- * @global object
- * @uses CONTEXT_COURSE
  * @param int $courseid The course in question.
  * @param int $timestart The date to check forward of
  * @return object|false  {@link $USER} records or false if error.
@@ -465,8 +462,9 @@ function is_course_participant($userid, $courseid) {
 function get_recent_enrolments($courseid, $timestart) {
     global $DB;
 
-    $context = context_course::instance($courseid);
+    debugging('get_recent_enrolments() is deprecated as it returned inaccurate results.', DEBUG_DEVELOPER);
 
+    $context = context_course::instance($courseid);
     $sql = "SELECT u.id, u.firstname, u.lastname, MAX(l.time)
               FROM {user} u, {role_assignments} ra, {log} l
              WHERE l.time > ?
@@ -4337,3 +4335,20 @@ function context_instance_preload_sql($joinon, $contextlevel, $tablealias) {
     $join = "LEFT JOIN {context} $tablealias ON ($tablealias.instanceid = $joinon AND $tablealias.contextlevel = $contextlevel)";
     return array($select, $join);
 }
+
+/**
+ * Gets a string for sql calls, searching for stuff in this context or above.
+ *
+ * @deprecated since 2.2
+ * @see context::get_parent_context_ids()
+ * @param context $context
+ * @return string
+ */
+function get_related_contexts_string(context $context) {
+    debugging('get_related_contexts_string() is deprecated, please use $context->get_parent_context_ids(true) instead.', DEBUG_DEVELOPER);
+    if ($parents = $context->get_parent_context_ids()) {
+        return (' IN ('.$context->id.','.implode(',', $parents).')');
+    } else {
+        return (' ='.$context->id);
+    }
+}
index 2a6676c..5835c88 100644 (file)
@@ -2210,6 +2210,10 @@ abstract class moodle_database {
             $this->commit_transaction();
         }
         array_pop($this->transactions);
+
+        if (empty($this->transactions)) {
+            \core\event\manager::database_transaction_commited();
+        }
     }
 
     /**
@@ -2255,6 +2259,7 @@ abstract class moodle_database {
         if (empty($this->transactions)) {
             // finally top most level rolled back
             $this->force_rollback = false;
+            \core\event\manager::database_transaction_rolledback();
         }
         throw $e;
     }
index 4f1ac23..98adf18 100644 (file)
@@ -28,25 +28,18 @@ require('../../../../../config.php');
 
 // General settings
 $engine = get_config('tinymce_spellchecker', 'spellengine');
-if (!$engine) {
-    $engine = 'GoogleSpell';
+if (!$engine or $engine === 'GoogleSpell') {
+    $engine = 'PSpell';
 }
 $config['general.engine'] = $engine;
 
-// GoogleSpell settings
-$config['GoogleSpell.proxyhost'] = isset($CFG->proxyhost) ? $CFG->proxyhost : '';
-$config['GoogleSpell.proxyport'] = isset($CFG->proxyport) ? $CFG->proxyport : '';
-$config['GoogleSpell.proxytype'] = isset($CFG->proxytype) ? $CFG->proxytype : 'HTML';
-$config['GoogleSpell.proxyuser'] = isset($CFG->proxyuser) ? $CFG->proxyuser : '';
-$config['GoogleSpell.proxypassword'] = isset($CFG->proxypassword) ? $CFG->proxypassword : '';
-
-if ($config['general.engine'] === 'PSpell' || $config['general.engine'] === 'PSpellShell') {
+if ($config['general.engine'] === 'PSpell') {
     // PSpell settings
     $config['PSpell.mode'] = PSPELL_FAST;
     $config['PSpell.spelling'] = "";
     $config['PSpell.jargon'] = "";
     $config['PSpell.encoding'] = "";
-
+} else if ($config['general.engine'] === 'PSpellShell') {
     // PSpellShell settings
     $config['PSpellShell.mode'] = PSPELL_FAST;
     $config['PSpellShell.aspell'] = $CFG->aspellpath;
index 3f85665..8c14e08 100644 (file)
@@ -36,6 +36,12 @@ class tinymce_spellchecker extends editor_tinymce_plugin {
             return;
         }
 
+        // Check some speller is configured.
+        $engine = $this->get_config('spellengine', '');
+        if (!$engine or $engine === 'GoogleSpell') {
+            return;
+        }
+
         // Check at least one language is supported.
         $spelllanguagelist = $this->get_config('spelllanguagelist', '');
         if ($spelllanguagelist !== '') {
index c4081e3..2be8fde 100644 (file)
@@ -26,11 +26,11 @@ defined('MOODLE_INTERNAL') || die;
 
 if ($ADMIN->fulltree) {
     $options = array(
+        ''=>get_string('none'),
         'PSpell'=>'PSpell',
-        'GoogleSpell'=>'Google Spell',
         'PSpellShell'=>'PSpellShell');
     $settings->add(new admin_setting_configselect('tinymce_spellchecker/spellengine',
-        get_string('spellengine', 'admin'), '', 'GoogleSpell', $options));
+        get_string('spellengine', 'admin'), '', '', $options));
     $settings->add(new admin_setting_configtextarea('tinymce_spellchecker/spelllanguagelist',
         get_string('spelllanguagelist', 'admin'), '',
         '+English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr,German=de,Italian=it,Polish=pl,' .
index 7a97dfc..998b896 100644 (file)
@@ -79,7 +79,7 @@ $etag = sha1($rev.implode(',', $jsfiles));
 
 // Use the caching only for meaningful revision numbers which prevents future cache poisoning.
 if ($rev > 0 and $rev < (time() + 60*60)) {
-    $candidate = $CFG->cachedir.'/js/'.$etag;
+    $candidate = $CFG->localcachedir.'/js/'.$etag;
 
     if (file_exists($candidate)) {
         if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
index f79a2db..997048e 100644 (file)
@@ -1560,6 +1560,12 @@ function purge_all_caches() {
     // hack: this script may get called after the purifier was initialised,
     // but we do not want to verify repeatedly this exists in each call
     make_cache_directory('htmlpurifier');
+
+    // This is the only place where we purge local caches, we are only adding files there.
+    // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
+    remove_dir($CFG->localcachedir, true);
+    set_config('localcachedirpurged', time());
+    make_localcache_directory('', true);
 }
 
 /**
index 55c5962..42d309d 100644 (file)
@@ -1266,7 +1266,8 @@ class global_navigation extends navigation_node {
         // Remove any empty root nodes
         foreach ($this->rootnodes as $node) {
             // Dont remove the home node
-            if ($node->key !== 'home' && !$node->has_children()) {
+            /** @var navigation_node $node */
+            if ($node->key !== 'home' && !$node->has_children() && !$node->isactive) {
                 $node->remove();
             }
         }
index 8a0b046..08da9d8 100644 (file)
@@ -44,7 +44,6 @@ require_once($CFG->libdir.'/outputrequirementslib.php');
  */
 function theme_reset_all_caches() {
     global $CFG, $PAGE;
-    require_once("$CFG->libdir/filelib.php");
 
     $next = time();
     if (isset($CFG->themerev) and $next <= $CFG->themerev and $CFG->themerev - $next < 60*60) {
@@ -55,7 +54,6 @@ function theme_reset_all_caches() {
     }
 
     set_config('themerev', $next); // time is unique even when you reset/switch database
-    fulldelete("$CFG->cachedir/theme");
 
     if ($PAGE) {
         $PAGE->reload_theme();
@@ -1016,7 +1014,7 @@ class theme_config {
      */
     public function post_process($css) {
         // now resolve all image locations
-        if (preg_match_all('/\[\[pix:([a-z_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
+        if (preg_match_all('/\[\[pix:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
             $replaced = array();
             foreach ($matches as $match) {
                 if (isset($replaced[$match[0]])) {
@@ -1032,6 +1030,23 @@ class theme_config {
             }
         }
 
+        // Now resolve all font locations.
+        if (preg_match_all('/\[\[font:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
+            $replaced = array();
+            foreach ($matches as $match) {
+                if (isset($replaced[$match[0]])) {
+                    continue;
+                }
+                $replaced[$match[0]] = true;
+                $fontname = $match[2];
+                $component = rtrim($match[1], '|');
+                $fonturl = $this->font_url($fontname, $component)->out(false);
+                // We do not need full url because the font.php is always in the same dir.
+                $fonturl = preg_replace('|^http.?://[^/]+|', '', $fonturl);
+                $css = str_replace($match[0], $fonturl, $css);
+            }
+        }
+
         // now resolve all theme settings or do any other postprocessing
         $csspostprocess = $this->csspostprocess;
         if (function_exists($csspostprocess)) {
@@ -1088,6 +1103,42 @@ class theme_config {
         return $url;
     }
 
+    /**
+     * Return the URL for a font
+     *
+     * @param string $font the name of the font (including extension).
+     * @param string $component specification of one plugin like in get_string()
+     * @return moodle_url
+     */
+    public function font_url($font, $component) {
+        global $CFG;
+
+        $params = array('theme'=>$this->name);
+
+        if (empty($component) or $component === 'moodle' or $component === 'core') {
+            $params['component'] = 'core';
+        } else {
+            $params['component'] = $component;
+        }
+
+        $rev = theme_get_revision();
+        if ($rev != -1) {
+            $params['rev'] = $rev;
+        }
+
+        $params['font'] = $font;
+
+        $url = new moodle_url("$CFG->httpswwwroot/theme/font.php");
+        if (!empty($CFG->slasharguments) and $rev > 0) {
+            $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['font'];
+            $url->set_slashargument($path, 'noparam', true);
+        } else {
+            $url->params($params);
+        }
+
+        return $url;
+    }
+
     /**
      * Returns URL to the stored file via pluginfile.php.
      *
@@ -1236,6 +1287,69 @@ class theme_config {
         }
     }
 
+    /**
+     * Resolves the real font location.
+     *
+     * @param string $font name of font file
+     * @param string $component
+     * @return string full file path
+     */
+    public function resolve_font_location($font, $component) {
+        global $CFG;
+
+        if ($component === 'moodle' or $component === 'core' or empty($component)) {
+            if (file_exists("$this->dir/fonts_core/$font")) {
+                return "$this->dir/fonts_core/$font";
+            }
+            foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
+                if (file_exists("$parent_config->dir/fonts_core/$font")) {
+                    return "$parent_config->dir/fonts_core/$font";
+                }
+            }
+            if (file_exists("$CFG->dataroot/fonts/$font")) {
+                return "$CFG->dataroot/fonts/$font";
+            }
+            if (file_exists("$CFG->dirroot/lib/fonts/$font")) {
+                return "$CFG->dirroot/lib/fonts/$font";
+            }
+            return null;
+
+        } else if ($component === 'theme') { // Exception.
+            if (file_exists("$this->dir/fonts/$font")) {
+                return "$this->dir/fonts/$font";
+            }
+            foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
+                if (file_exists("$parent_config->dir/fonts/$font")) {
+                    return "$parent_config->dir/fonts/$font";
+                }
+            }
+            return null;
+
+        } else {
+            if (strpos($component, '_') === false) {
+                $component = 'mod_'.$component;
+            }
+            list($type, $plugin) = explode('_', $component, 2);
+
+            if (file_exists("$this->dir/fonts_plugins/$type/$plugin/$font")) {
+                return "$this->dir/fonts_plugins/$type/$plugin/$font";
+            }
+            foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
+                if (file_exists("$parent_config->dir/fonts_plugins/$type/$plugin/$font")) {
+                    return "$parent_config->dir/fonts_plugins/$type/$plugin/$font";
+                }
+            }
+            if (file_exists("$CFG->dataroot/fonts_plugins/$type/$plugin/$font")) {
+                return "$CFG->dataroot/fonts_plugins/$type/$plugin/$font";
+            }
+            $dir = core_component::get_plugin_directory($type, $plugin);
+            if (file_exists("$dir/fonts/$font")) {
+                return "$dir/fonts/$font";
+            }
+            return null;
+        }
+    }
+
     /**
      * Return true if we should look for SVG images as well.
      *
index bce03a5..49551c0 100644 (file)
@@ -1744,7 +1744,6 @@ class YUI_config {
  */
 function js_reset_all_caches() {
     global $CFG;
-    require_once("$CFG->libdir/filelib.php");
 
     $next = time();
     if (isset($CFG->jsrev) and $next <= $CFG->jsrev and $CFG->jsrev - $next < 60*60) {
@@ -1755,5 +1754,4 @@ function js_reset_all_caches() {
     }
 
     set_config('jsrev', $next);
-    fulldelete("$CFG->cachedir/js");
 }
index c04d339..418a9a9 100644 (file)
@@ -319,6 +319,19 @@ abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
         return phpunit_util::start_message_redirection();
     }
 
+    /**
+     * Starts event redirection.
+     *
+     * You can verify if events were triggered or not by inspecting the events
+     * array in the returned event sink instance. The redirection
+     * can be stopped by calling $sink->close();
+     *
+     * @return phpunit_event_sink
+     */
+    public function redirectEvents() {
+        return phpunit_util::start_event_redirection();
+    }
+
     /**
      * Cleanup after all tests are executed.
      *
diff --git a/lib/phpunit/classes/event_sink.php b/lib/phpunit/classes/event_sink.php
new file mode 100644 (file)
index 0000000..df5c98c
--- /dev/null
@@ -0,0 +1,87 @@
+<?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/>.
+
+/**
+ * Event sink.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * Event redirection sink.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class phpunit_event_sink {
+    /** @var \core\event\base[] array of events */
+    protected $events = array();
+
+    /**
+     * Stop event redirection.
+     *
+     * Use if you do not want event redirected any more.
+     */
+    public function close() {
+        phpunit_util::stop_event_redirection();
+    }
+
+    /**
+     * To be called from phpunit_util only!
+     *
+     * @private
+     * @param \core\event\base $event record from event_read table
+     */
+    public function add_event(\core\event\base $event) {
+        /* Number events from 0. */
+        $this->events[] = $event;
+    }
+
+    /**
+     * Returns all redirected events.
+     *
+     * The instances are records form the event_read table.
+     * The array indexes are numbered from 0 and the order is matching
+     * the creation of events.
+     *
+     * @return \core\event\base[]
+     */
+    public function get_events() {
+        return $this->events;
+    }
+
+    /**
+     * Return number of events redirected to this sink.
+     *
+     * @return int
+     */
+    public function count() {
+        return count($this->events);
+    }
+
+    /**
+     * Removes all previously stored events.
+     */
+    public function clear() {
+        $this->events = array();
+    }
+}
index b049409..09a4f82 100644 (file)
@@ -43,6 +43,9 @@ class phpunit_util extends testing_util {
     /** @var phpunit_message_sink alternative target for moodle messaging */
     protected static $messagesink = null;
 
+    /** @var phpunit_message_sink alternative target for moodle messaging */
+    protected static $eventsink = null;
+
     /**
      * @var array Files to skip when resetting dataroot folder
      */
@@ -95,6 +98,9 @@ class phpunit_util extends testing_util {
         // Stop any message redirection.
         phpunit_util::stop_message_redirection();
 
+        // Stop any message redirection.
+        phpunit_util::stop_event_redirection();
+
         // Release memory and indirectly call destroy() methods to release resource handles, etc.
         gc_collect_cycles();
 
@@ -182,6 +188,7 @@ class phpunit_util extends testing_util {
         session_set_user($user);
 
         // reset all static caches
+        \core\event\manager::phpunit_reset();
         accesslib_clear_all_caches(true);
         get_string_manager()->reset_caches(true);
         reset_text_filters_cache(true);
@@ -660,4 +667,57 @@ class phpunit_util extends testing_util {
             self::$messagesink->add_message($message);
         }
     }
+
+    /**
+     * Start event redirection.
+     *
+     * @private
+     * Note: Do not call directly from tests,
+     *       use $sink = $this->redirectEvents() instead.
+     *
+     * @return phpunit_event_sink
+     */
+    public static function start_event_redirection() {
+        if (self::$eventsink) {
+            self::stop_event_redirection();
+        }
+        self::$eventsink = new phpunit_event_sink();
+        return self::$eventsink;
+    }
+
+    /**
+     * End event redirection.
+     *
+     * @private
+     * Note: Do not call directly from tests,
+     *       use $sink->close() instead.
+     */
+    public static function stop_event_redirection() {
+        self::$eventsink = null;
+    }
+
+    /**
+     * Are events redirected to some sink?
+     *
+     * Note: to be called from \core\event\base only!
+     *
+     * @private
+     * @return bool
+     */
+    public static function is_redirecting_events() {
+        return !empty(self::$eventsink);
+    }
+
+    /**
+     * To be called from \core\event\base only!
+     *
+     * @private
+     * @param \core\event\base $event record from event_read table
+     * @return bool true means send event, false means event "sent" to sink.
+     */
+    public static function event_triggered(\core\event\base $event) {
+        if (self::$eventsink) {
+            self::$eventsink->add_event($event);
+        }
+    }
 }
index 2ceaf3e..5dfdeab 100644 (file)
@@ -29,6 +29,7 @@ require_once('PHPUnit/Autoload.php');
 require_once('PHPUnit/Extensions/Database/Autoload.php');
 
 require_once(__DIR__.'/classes/util.php');
+require_once(__DIR__.'/classes/event_sink.php');
 require_once(__DIR__.'/classes/message_sink.php');
 require_once(__DIR__.'/classes/basic_testcase.php');
 require_once(__DIR__.'/classes/database_driver_testcase.php');
index 992ca01..70c977e 100644 (file)
@@ -728,7 +728,7 @@ class plugin_manager {
             ),
 
             'format' => array(
-                'scorm', 'social', 'topics', 'weeks'
+                'scorm', 'singleactivity', 'social', 'topics', 'weeks'
             ),
 
             'gradeexport' => array(
index d0c2ab4..2781b14 100644 (file)
@@ -37,7 +37,8 @@
  *  - $CFG->dirroot  - Path to moodle's library folder on server's filesystem.
  *  - $CFG->libdir   - Path to moodle's library folder on server's filesystem.
  *  - $CFG->tempdir  - Path to moodle's temp file directory on server's filesystem.
- *  - $CFG->cachedir - Path to moodle's cache directory on server's filesystem.
+ *  - $CFG->cachedir - Path to moodle's cache directory on server's filesystem (shared by cluster nodes).
+ *  - $CFG->localcachedir - Path to moodle's local cache directory (not shared by cluster nodes).
  *
  * @global object $CFG
  * @name $CFG
@@ -131,6 +132,10 @@ if (!defined('BEHAT_SITE_RUNNING') && !empty($CFG->behat_dataroot) &&
         // actions like reset the site or use data generators.
         define('BEHAT_SITE_RUNNING', true);
 
+        // Clean extra config.php settings.
+        require_once(__DIR__ . '/../lib/behat/lib.php');
+        behat_clean_init_config();
+
         $CFG->wwwroot = $CFG->behat_wwwroot;
         $CFG->passwordsaltmain = 'moodle';
         $CFG->prefix = $CFG->behat_prefix;
@@ -156,6 +161,11 @@ if (!isset($CFG->cachedir)) {
     $CFG->cachedir = "$CFG->dataroot/cache";
 }
 
+// Allow overriding of localcachedir.
+if (!isset($CFG->localcachedir)) {
+    $CFG->localcachedir = "$CFG->dataroot/localcache";
+}
+
 // The current directory in PHP version 4.3.0 and above isn't necessarily the
 // directory of the script when run from the command line. The require_once()
 // would fail, so we'll have to chdir()
index 4c9ba97..81ebd50 100644 (file)
@@ -529,7 +529,7 @@ function get_exception_info($ex) {
     // Remove some absolute paths from message and debugging info.
     $searches = array();
     $replaces = array();
-    $cfgnames = array('tempdir', 'cachedir', 'themedir',
+    $cfgnames = array('tempdir', 'cachedir', 'localcachedir', 'themedir',
         'langmenucachefile', 'langcacheroot', 'dataroot', 'dirroot');
     foreach ($cfgnames as $cfgname) {
         if (property_exists($CFG, $cfgname)) {
@@ -1311,7 +1311,10 @@ function make_upload_directory($directory, $exceptiononerror = true) {
         debugging('Use make_temp_directory() for creation of temporary directory and $CFG->tempdir to get the location.');
 
     } else if (strpos($directory, 'cache/') === 0 or $directory === 'cache') {
-        debugging('Use make_cache_directory() for creation of chache directory and $CFG->cachedir to get the location.');
+        debugging('Use make_cache_directory() for creation of cache directory and $CFG->cachedir to get the location.');
+
+    } else if (strpos($directory, 'localcache/') === 0 or $directory === 'localcache') {
+        debugging('Use make_localcache_directory() for creation of local cache directory and $CFG->localcachedir to get the location.');
     }
 
     protect_directory($CFG->dataroot);
@@ -1340,6 +1343,8 @@ function make_temp_directory($directory, $exceptiononerror = true) {
 /**
  * Create a directory under cachedir and make sure it is writable.
  *
+ * Note: this cache directory is shared by all cluster nodes.
+ *
  * @param string $directory  the full path of the directory to be created under $CFG->cachedir
  * @param bool $exceptiononerror throw exception if error encountered
  * @return string|false Returns full path to directory if successful, false if not; may throw exception
@@ -1355,6 +1360,58 @@ function make_cache_directory($directory, $exceptiononerror = true) {
     return make_writable_directory("$CFG->cachedir/$directory", $exceptiononerror);
 }
 
+/**
+ * Create a directory under localcachedir and make sure it is writable.
+ * The files in this directory MUST NOT change, use revisions or content hashes to
+ * work around this limitation - this means you can only add new files here.
+ *
+ * The content of this directory gets purged automatically on all cluster nodes
+ * after calling purge_all_caches() before new data is written to this directory.
+ *
+ * Note: this local cache directory does not need to be shared by cluster nodes.
+ *
+ * @param string $directory the relative path of the directory to be created under $CFG->localcachedir
+ * @param bool $exceptiononerror throw exception if error encountered
+ * @return string|false Returns full path to directory if successful, false if not; may throw exception
+ */
+function make_localcache_directory($directory, $exceptiononerror = true) {
+    global $CFG;
+
+    make_writable_directory($CFG->localcachedir, $exceptiononerror);
+
+    if ($CFG->localcachedir !== "$CFG->dataroot/localcache") {
+        protect_directory($CFG->localcachedir);
+    } else {
+        protect_directory($CFG->dataroot);
+    }
+
+    if (!isset($CFG->localcachedirpurged)) {
+        $CFG->localcachedirpurged = 0;
+    }
+    $timestampfile = "$CFG->localcachedir/.lastpurged";
+
+    if (!file_exists($timestampfile)) {
+        touch($timestampfile);
+        @chmod($timestampfile, $CFG->filepermissions);
+
+    } else if (filemtime($timestampfile) <  $CFG->localcachedirpurged) {
+        // This means our local cached dir was not purged yet.
+        remove_dir($CFG->localcachedir, true);
+        if ($CFG->localcachedir !== "$CFG->dataroot/localcache") {
+            protect_directory($CFG->localcachedir);
+        }
+        touch($timestampfile);
+        @chmod($timestampfile, $CFG->filepermissions);
+        clearstatcache();
+    }
+
+    if ($directory === '') {
+        return $CFG->localcachedir;
+    }
+
+    return make_writable_directory("$CFG->localcachedir/$directory", $exceptiononerror);
+}
+
 /**
  * Checks if current user is a web crawler.
  *
index 71ebacc..4dfa12c 100644 (file)
@@ -599,6 +599,7 @@ abstract class testing_util {
         make_temp_directory('');
         make_cache_directory('');
         make_cache_directory('htmlpurifier');
+        make_localcache_directory('');
         // Reset the cache API so that it recreates it's required directories as well.
         cache_factory::reset();
         // Purge all data from the caches. This is required for consistency.
index 863cfc3..034b90b 100644 (file)
@@ -491,6 +491,26 @@ class accesslib_testcase extends advanced_testcase {
         $this->assertSame('1', $ras->itemid);
         $this->assertEquals($USER->id, $ras->modifierid);
         $this->assertEquals(666, $ras->timemodified);
+
+        // Test event triggered.
+
+        $user2 = $this->getDataGenerator()->create_user();
+        $sink = $this->redirectEvents();
+        $raid = role_assign($role->id, $user2->id, $context->id);
+        $events = $sink->get_events();
+        $sink->close();
+        $this->assertCount(1, $events);
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\role_assigned', $event);
+        $this->assertEquals('role', $event->target);
+        $this->assertEquals('role', $event->objecttable);
+        $this->assertEquals($role->id, $event->objectid);
+        $this->assertEquals($context->id, $event->contextid);
+        $this->assertEquals($user2->id, $event->relateduserid);
+        $this->assertCount(3, $event->other);
+        $this->assertEquals($raid, $event->other['id']);
+        $this->assertSame('', $event->other['component']);
+        $this->assertEquals(0, $event->other['itemid']);
     }
 
     /**
@@ -506,7 +526,7 @@ class accesslib_testcase extends advanced_testcase {
         $course = $this->getDataGenerator()->create_course();
         $role = $DB->get_record('role', array('shortname'=>'student'));
 
-        $context = context_system::instance();
+        $context = context_course::instance($course->id);
         role_assign($role->id, $user->id, $context->id);
         $this->assertTrue($DB->record_exists('role_assignments', array('userid'=>$user->id, 'roleid'=>$role->id, 'contextid'=>$context->id)));
         role_unassign($role->id, $user->id, $context->id);
@@ -516,6 +536,25 @@ class accesslib_testcase extends advanced_testcase {
         $this->assertTrue($DB->record_exists('role_assignments', array('userid'=>$user->id, 'roleid'=>$role->id, 'contextid'=>$context->id)));
         role_unassign($role->id, $user->id, $context->id, 'enrol_self', 1);
         $this->assertFalse($DB->record_exists('role_assignments', array('userid'=>$user->id, 'roleid'=>$role->id, 'contextid'=>$context->id)));
+
+        // Test event triggered.
+
+        role_assign($role->id, $user->id, $context->id);
+        $sink = $this->redirectEvents();
+        role_unassign($role->id, $user->id, $context->id);
+        $events = $sink->get_events();
+        $sink->close();
+        $this->assertCount(1, $events);
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\role_unassigned', $event);
+        $this->assertEquals('role', $event->target);
+        $this->assertEquals('role', $event->objecttable);
+        $this->assertEquals($role->id, $event->objectid);
+        $this->assertEquals($context->id, $event->contextid);
+        $this->assertEquals($user->id, $event->relateduserid);
+        $this->assertCount(3, $event->other);
+        $this->assertSame('', $event->other['component']);
+        $this->assertEquals(0, $event->other['itemid']);
     }
 
     /**
@@ -530,6 +569,7 @@ class accesslib_testcase extends advanced_testcase {
         $user = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course();
         $role = $DB->get_record('role', array('shortname'=>'student'));
+        $role2 = $DB->get_record('role', array('shortname'=>'teacher'));
         $syscontext = context_system::instance();
         $coursecontext = context_course::instance($course->id);
         $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
@@ -559,6 +599,18 @@ class accesslib_testcase extends advanced_testcase {
         $this->assertEquals(4, $DB->count_records('role_assignments', array('userid'=>$user->id)));
         role_unassign_all(array('userid'=>$user->id, 'contextid'=>$coursecontext->id, 'component'=>'enrol_self'), true, true);
         $this->assertEquals(1, $DB->count_records('role_assignments', array('userid'=>$user->id)));
+
+        // Test events triggered.
+
+        role_assign($role2->id, $user->id, $coursecontext->id);
+        role_assign($role2->id, $user->id, $modcontext->id);
+        $sink = $this->redirectEvents();
+        role_unassign_all(array('userid'=>$user->id, 'roleid'=>$role2->id));
+        $events = $sink->get_events();
+        $sink->close();
+        $this->assertCount(2, $events);
+        $this->assertInstanceOf('\core\event\role_unassigned', $events[0]);
+        $this->assertInstanceOf('\core\event\role_unassigned', $events[1]);
     }
 
     /**
diff --git a/lib/tests/event_test.php b/lib/tests/event_test.php
new file mode 100644 (file)
index 0000000..052ed5e
--- /dev/null
@@ -0,0 +1,689 @@
+<?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/>.
+
+/**
+ * Tests for event manager, base event and observers.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__.'/fixtures/event_fixtures.php');
+
+class core_event_testcase extends advanced_testcase {
+
+    protected function setUp() {
+        global $CFG;
+        // No need to always modify log table here.
+        $CFG->loglifetime = '-1';
+    }
+
+    protected function tearDown() {
+        global $CFG;
+        $CFG->loglifetime = '0';
+    }
+
+    public function test_event_properties() {
+        global $USER;
+
+        $system = \context_system::instance();
+        $event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>$system, 'objectid'=>5, 'other'=>array('sample'=>null, 'xx'=>10)));
+
+        $this->assertSame('\core_tests\event\unittest_executed', $event->eventname);
+        $this->assertSame('core_tests', $event->component);
+        $this->assertSame('executed', $event->action);
+        $this->assertSame('unittest', $event->target);
+        $this->assertSame(5, $event->objectid);
+        $this->assertSame('u', $event->crud);
+        $this->assertSame(10, $event->level);
+
+        $this->assertSame($system, $event->get_context());
+        $this->assertSame($system->id, $event->contextid);
+        $this->assertSame($system->contextlevel, $event->contextlevel);
+        $this->assertSame($system->instanceid, $event->contextinstanceid);
+
+        $this->assertSame($USER->id, $event->userid);
+        $this->assertSame(1, $event->courseid);
+
+        $this->assertNull($event->relateduserid);
+        $this->assertFalse(isset($event->relateduserid));
+
+        $this->assertSame(array('sample'=>null, 'xx'=>10), $event->other);
+        $this->assertTrue(isset($event->other['xx']));
+        $this->assertFalse(isset($event->other['sample']));
+
+        $this->assertLessThanOrEqual(time(), $event->timecreated);
+
+        try {
+            $event->courseid = 2;
+            $this->fail('Exception expected on event modification');
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        try {
+            $event->xxxx = 1;
+            $this->fail('Exception expected on event modification');
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        $event2 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'contextid'=>$system->id, 'objectid'=>5, 'other'=>array('sample'=>null, 'xx'=>10)));
+        $this->assertSame($event->get_context(), $event2->get_context());
+    }
+
+    public function test_observers_parsing() {
+
+        $observers = array(
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                'callback'    => '\core_tests\event\unittest_observer::observe_one',
+                'includefile' => 'lib/tests/fixtures/event_fixtures.php',
+            ),
+            array(
+                'eventname'   => '*',
+                'callback'    => array('\core_tests\event\unittest_observer', 'observe_all'),
+                'includefile' => null,
+                'internal'    => 1,
+                'priority'    => 9999,
+            ),
+            array(
+                'eventname'   => '\core\event\unknown_executed',
+                'callback'    => '\core_tests\event\unittest_observer::broken_observer',
+                'priority'    => 100,
+            ),
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                'callback'    => '\core_tests\event\unittest_observer::external_observer',
+                'priority'    => 200,
+                'internal'    => 0,
+            ),
+        );
+
+        $result = \core\event\manager::phpunit_replace_observers($observers);
+
+        $this->assertCount(3, $result);
+        end($result);
+        $this->assertSame('*', key($result));
+
+        $expected = array();
+        $observer = new stdClass();
+        $observer->callable = array('\core_tests\event\unittest_observer', 'observe_all');
+        $observer->priority = 9999;
+        $observer->internal = true;
+        $observer->includefile = null;
+        $expected[0] = $observer;
+        $observer = new stdClass();
+        $observer->callable = '\core_tests\event\unittest_observer::external_observer';
+        $observer->priority = 200;
+        $observer->internal = false;
+        $observer->includefile = null;
+        $expected[1] = $observer;
+        $observer = new stdClass();
+        $observer->callable = '\core_tests\event\unittest_observer::observe_one';
+        $observer->priority = 0;
+        $observer->internal = true;
+        $observer->includefile = 'lib/tests/fixtures/event_fixtures.php';
+        $expected[2] = $observer;
+
+        $this->assertEquals($expected, $result['\core_tests\event\unittest_executed']);
+
+        $expected = array();
+        $observer = new stdClass();
+        $observer->callable = array('\core_tests\event\unittest_observer', 'observe_all');
+        $observer->priority = 9999;
+        $observer->internal = true;
+        $observer->includefile = null;
+        $expected[0] = $observer;
+        $observer = new stdClass();
+        $observer->callable = '\core_tests\event\unittest_observer::broken_observer';
+        $observer->priority = 100;
+        $observer->internal = true;
+        $observer->includefile = null;
+        $expected[1] = $observer;
+
+        $this->assertEquals($expected, $result['\core\event\unknown_executed']);
+
+        $expected = array();
+        $observer = new stdClass();
+        $observer->callable = array('\core_tests\event\unittest_observer', 'observe_all');
+        $observer->priority = 9999;
+        $observer->internal = true;
+        $observer->includefile = null;
+        $expected[0] = $observer;
+
+        $this->assertEquals($expected, $result['*']);
+
+
+        // Now test broken stuff...
+
+        $observers = array(
+            array(
+                'eventname'   => 'core_tests\event\unittest_executed', // Fix leading backslash.
+                'callback'    => '\core_tests\event\unittest_observer::observe_one',
+                'includefile' => 'lib/tests/fixtures/event_fixtures.php',
+                'internal'    => 1, // Cast to bool.
+            ),
+        );
+        $result = \core\event\manager::phpunit_replace_observers($observers);
+        $this->assertCount(1, $result);
+        $expected = array();
+        $observer = new stdClass();
+        $observer->callable = '\core_tests\event\unittest_observer::observe_one';
+        $observer->priority = 0;
+        $observer->internal = true;
+        $observer->includefile = 'lib/tests/fixtures/event_fixtures.php';
+        $expected[0] = $observer;
+        $this->assertEquals($expected, $result['\core_tests\event\unittest_executed']);
+
+        $observers = array(
+            array(
+                // Missing eventclass.
+                'callback'    => '\core_tests\event\unittest_observer::observe_one',
+                'includefile' => 'lib/tests/fixtures/event_fixtures.php',
+            ),
+        );
+        $result = \core\event\manager::phpunit_replace_observers($observers);
+        $this->assertCount(0, $result);
+        $this->assertDebuggingCalled();
+
+        $observers = array(
+            array(
+                'eventname'   => '', // Empty eventclass.
+                'callback'    => '\core_tests\event\unittest_observer::observe_one',
+                'includefile' => 'lib/tests/fixtures/event_fixtures.php',
+            ),
+        );
+        $result = \core\event\manager::phpunit_replace_observers($observers);
+        $this->assertCount(0, $result);
+        $this->assertDebuggingCalled();
+
+        $observers = array(
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                // Missing callable.
+                'includefile' => 'lib/tests/fixtures/event_fixtures.php',
+            ),
+        );
+        $result = \core\event\manager::phpunit_replace_observers($observers);
+        $this->assertCount(0, $result);
+        $this->assertDebuggingCalled();
+
+        $observers = array(
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                'callback'    => '', // empty callable
+                'includefile' => 'lib/tests/fixtures/event_fixtures.php',
+            ),
+        );
+        $result = \core\event\manager::phpunit_replace_observers($observers);
+        $this->assertCount(0, $result);
+        $this->assertDebuggingCalled();
+
+        $observers = array(
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                'callback'    => '\core_tests\event\unittest_observer::observe_one',
+                'includefile' => 'lib/tests/fixtures/event_fixtures.php_xxx', // Missing file.
+            ),
+        );
+        $result = \core\event\manager::phpunit_replace_observers($observers);
+        $this->assertCount(0, $result);
+        $this->assertDebuggingCalled();
+    }
+
+    public function test_normal_dispatching() {
+        $observers = array(
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                'callback'    => '\core_tests\event\unittest_observer::observe_one',
+            ),
+            array(
+                'eventname'   => '*',
+                'callback'    => '\core_tests\event\unittest_observer::observe_all',
+                'includefile' => null,
+                'internal'    => 1,
+                'priority'    => 9999,
+            ),
+        );
+
+        \core\event\manager::phpunit_replace_observers($observers);
+        \core_tests\event\unittest_observer::reset();
+
+        $event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
+        $event1->nest = 1;
+        $this->assertFalse($event1->is_triggered());
+        $this->assertFalse($event1->is_dispatched());
+        $this->assertFalse($event1->is_restored());
+        $event1->trigger();
+        $this->assertTrue($event1->is_triggered());
+        $this->assertTrue($event1->is_dispatched());
+        $this->assertFalse($event1->is_restored());
+
+        $event1 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
+        $event1->trigger();
+
+        $this->assertSame(
+            array('observe_all-nesting-1', 'observe_one-1', 'observe_all-3', 'observe_one-3', 'observe_all-2', 'observe_one-2'),
+            \core_tests\event\unittest_observer::$info);
+    }
+
+    public function test_event_sink() {
+        $sink = $this->redirectEvents();
+        $event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
+        $event1->trigger();
+        $this->assertSame(1, $sink->count());
+        $retult = $sink->get_events();
+        $this->assertSame($event1, $retult[0]);
+
+        $event2 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
+        $event2->trigger();
+        $this->assertSame(2, $sink->count());
+        $retult = $sink->get_events();
+        $this->assertSame($event1, $retult[0]);
+        $this->assertSame($event2, $retult[1]);
+
+        $sink->clear();
+        $this->assertSame(0, $sink->count());
+        $this->assertSame(array(), $sink->get_events());
+
+        $event3 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>3, 'xx'=>10)));
+        $event3->trigger();
+        $this->assertSame(1, $sink->count());
+        $retult = $sink->get_events();
+        $this->assertSame($event3, $retult[0]);
+
+        $sink->close();
+        $event4 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>4, 'xx'=>10)));
+        $event4->trigger();
+        $this->assertSame(1, $sink->count());
+        $retult = $sink->get_events();
+        $this->assertSame($event3, $retult[0]);
+    }
+
+    public function test_ignore_exceptions() {
+        $observers = array(
+
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                'callback'    => '\core_tests\event\unittest_observer::observe_one',
+            ),
+
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                'callback'    => '\core_tests\event\unittest_observer::broken_observer',
+                'priority'    => 100,
+            ),
+        );
+
+        \core\event\manager::phpunit_replace_observers($observers);
+        \core_tests\event\unittest_observer::reset();
+
+        $event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
+        $event1->trigger();
+        $this->assertDebuggingCalled();
+
+        $event1 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
+        $event1->trigger();
+        $this->assertDebuggingCalled();
+
+        $this->assertSame(
+            array('broken_observer-1', 'observe_one-1', 'broken_observer-2', 'observe_one-2'),
+            \core_tests\event\unittest_observer::$info);
+    }
+
+    public function test_external_buffer() {
+        global $DB;
+
+        $this->preventResetByRollback();
+
+        $observers = array(
+
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                'callback'    => '\core_tests\event\unittest_observer::observe_one',
+            ),
+
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                'callback'    => '\core_tests\event\unittest_observer::external_observer',
+                'priority'    => 200,
+                'internal'    => 0,
+            ),
+        );
+
+        \core\event\manager::phpunit_replace_observers($observers);
+        \core_tests\event\unittest_observer::reset();
+
+        $event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
+        $event1->trigger();
+        $event2 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
+        $event2->trigger();
+
+        $this->assertSame(
+            array('external_observer-1', 'observe_one-1', 'external_observer-2', 'observe_one-2'),
+            \core_tests\event\unittest_observer::$info);
+
+        \core\event\manager::phpunit_replace_observers($observers);
+        \core_tests\event\unittest_observer::reset();
+
+        $this->assertSame(array(), \core_tests\event\unittest_observer::$info);
+
+        $trans = $DB->start_delegated_transaction();
+
+        $event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
+        $event1->trigger();
+        $event2 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
+        $event2->trigger();
+
+        $this->assertSame(
+            array('observe_one-1', 'observe_one-2'),
+            \core_tests\event\unittest_observer::$info);
+
+        $trans->allow_commit();
+
+        $this->assertSame(
+            array('observe_one-1', 'observe_one-2', 'external_observer-1', 'external_observer-2'),
+            \core_tests\event\unittest_observer::$info);
+
+        \core\event\manager::phpunit_replace_observers($observers);
+        \core_tests\event\unittest_observer::reset();
+
+        $event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
+        $event1->trigger();
+        $trans = $DB->start_delegated_transaction();
+        $event2 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>2, 'xx'=>10)));
+        $event2->trigger();
+        try {
+            $trans->rollback(new \moodle_exception('xxx'));
+            $this->fail('Expecting exception');
+        } catch (\moodle_exception $e) {
+        }
+
+        $this->assertSame(
+            array('external_observer-1', 'observe_one-1', 'observe_one-2'),
+            \core_tests\event\unittest_observer::$info);
+    }
+
+    public function test_legacy() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $observers = array(
+            array(
+                'eventname'   => '\core_tests\event\unittest_executed',
+                'callback'    => '\core_tests\event\unittest_observer::observe_one',
+            ),
+            array(
+                'eventname'   => '*',
+                'callback'    => '\core_tests\event\unittest_observer::observe_all',
+                'includefile' => null,
+                'internal'    => 1,
+                'priority'    => 9999,
+            ),
+        );
+
+        $DB->delete_records('log', array());
+        events_update_definition('unittest');
+        $DB->delete_records_select('events_handlers', "component <> 'unittest'");
+        events_get_handlers('reset');
+        $this->assertEquals(3, $DB->count_records('events_handlers'));
+        set_config('loglifetime', 60*60*24*5);
+
+        \core\event\manager::phpunit_replace_observers($observers);
+        \core_tests\event\unittest_observer::reset();
+
+        $event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>5, 'xx'=>10)));
+        $event1->trigger();
+
+        $event2 = \core_tests\event\unittest_executed::create(array('courseid'=>2, 'context'=>\context_system::instance(), 'other'=>array('sample'=>6, 'xx'=>11)));
+        $event2->nest = true;
+        $event2->trigger();
+
+
+        $this->assertSame(
+            array('observe_all-1', 'observe_one-1', 'legacy_handler-1', 'observe_all-nesting-2', 'legacy_handler-3', 'observe_one-2', 'observe_all-3', 'observe_one-3', 'legacy_handler-2'),
+            \core_tests\event\unittest_observer::$info);
+
+        $this->assertSame($event1, \core_tests\event\unittest_observer::$event[0]);
+        $this->assertSame($event1, \core_tests\event\unittest_observer::$event[1]);
+        $this->assertSame(array(1, 5), \core_tests\event\unittest_observer::$event[2]);
+
+
+        $logs = $DB->get_records('log', array(), 'id ASC');
+        $this->assertCount(3, $logs);
+
+        $log = array_shift($logs);
+        $this->assertEquals(1, $log->course);
+        $this->assertSame('core_unittest', $log->module);
+        $this->assertSame('view', $log->action);
+
+        $log = array_shift($logs);
+        $this->assertEquals(2, $log->course);
+        $this->assertSame('core_unittest', $log->module);
+        $this->assertSame('view', $log->action);
+
+        $log = array_shift($logs);
+        $this->assertEquals(3, $log->course);
+        $this->assertSame('core_unittest', $log->module);
+        $this->assertSame('view', $log->action);
+    }
+
+    public function test_restore_event() {
+        $event1 = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
+        $data1 = $event1->get_data();
+
+        $event2 = \core\event\base::restore($data1, array('origin'=>'clid'));
+        $data2 = $event2->get_data();
+
+        $this->assertTrue($event2->is_triggered());
+        $this->assertTrue($event2->is_restored());
+        $this->assertEquals($data1, $data2);
+        $this->assertInstanceOf('core_tests\event\unittest_executed', $event2);
+
+        $this->assertEquals($event1->get_context(), $event2->get_context());
+
+        // Now test problematic data.
+        $data3 = $data1;
+        $data3['eventname'] = '\\a\\b\\c';
+        $event3 = \core\event\base::restore($data3, array());
+        $this->assertFalse($event3, 'Class name must match');
+
+        $data4 = $data1;
+        unset($data4['userid']);
+        $event4 = \core\event\base::restore($data4, array());
+        $this->assertInstanceOf('core_tests\event\unittest_executed', $event4);
+        $this->assertDebuggingCalled();
+
+        $data5 = $data1;
+        $data5['xx'] = 'xx';
+        $event5 = \core\event\base::restore($data5, array());
+        $this->assertInstanceOf('core_tests\event\unittest_executed', $event5);
+        $this->assertDebuggingCalled();
+
+    }
+
+    public function test_trigger_problems() {
+        $event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>5, 'xx'=>10)));
+        $event->trigger();
+        try {
+            $event->trigger();
+            $this->fail('Exception expected on double trigger');
+        } catch (Exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        $data = $event->get_data();
+        $restored = \core_tests\event\unittest_executed::restore($data, array());
+        $this->assertTrue($restored->is_triggered());
+        $this->assertTrue($restored->is_restored());
+
+        try {
+            $restored->trigger();
+            $this->fail('Exception expected on triggering of restored event');
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        $event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>5, 'xx'=>10)));
+        try {
+            \core\event\manager::dispatch($event);
+            $this->fail('Exception expected on manual event dispatching');
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+    }
+
+    public function test_bad_events() {
+        try {
+            $event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'other'=>array('sample'=>5, 'xx'=>10)));
+            $this->fail('Exception expected when context and contextid missing');
+        } catch (Exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        $event = \core_tests\event\bad_event1::create(array('context'=>\context_system::instance()));
+        try {
+            $event->trigger();
+            $this->fail('Exception expected when $data not valid');
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('\coding_exception', $e);
+        }
+
+        $event = \core_tests\event\bad_event2::create(array('context'=>\context_system::instance()));
+        try {
+            $event->trigger();
+            $this->fail('Exception expected when $data not valid');
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('\coding_exception', $e);
+        }
+
+        $event = \core_tests\event\bad_event3::create(array('context'=>\context_system::instance()));
+        @$event->trigger();
+        $this->assertDebuggingCalled();
+
+        $event = \core_tests\event\bad_event4::create(array('context'=>\context_system::instance()));
+        @$event->trigger();
+        $this->assertDebuggingCalled();
+
+        $event = \core_tests\event\bad_event5::create(array('context'=>\context_system::instance()));
+        @$event->trigger();
+        $this->assertDebuggingCalled();
+
+        $event = \core_tests\event\bad_event6::create(array('context'=>\context_system::instance()));
+        $event->trigger();
+        $this->assertDebuggingCalled();
+
+        $event = \core_tests\event\bad_event7::create(array('objectid'=>1, 'context'=>\context_system::instance()));
+        try {
+            $event->trigger();
+            $this->fail('Exception expected when $data contains objectid by objecttable not specified');
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('\coding_exception', $e);
+        }
+    }
+
+    public function test_problematic_events() {
+        global $CFG;
+        $event1 = \core_tests\event\problematic_event1::create(array('context'=>\context_system::instance()));
+        $this->assertDebuggingNotCalled();
+        $this->assertNull($event1->xxx);
+        $this->assertDebuggingCalled();
+
+        $event2 = \core_tests\event\problematic_event1::create(array('xxx'=>0, 'context'=>\context_system::instance()));
+        $this->assertDebuggingCalled();
+
+        $CFG->debug = 0;
+        $event3 = \core_tests\event\problematic_event1::create(array('xxx'=>0, 'context'=>\context_system::instance()));
+        $this->assertDebuggingNotCalled();
+        $CFG->debug = E_ALL | E_STRICT;
+
+        $event4 = \core_tests\event\problematic_event1::create(array('context'=>\context_system::instance(), 'other'=>array('a'=>1)));
+        $event4->trigger();
+        $this->assertDebuggingNotCalled();
+
+        $event5 = \core_tests\event\problematic_event1::create(array('context'=>\context_system::instance(), 'other'=>(object)array('a'=>1)));
+        $this->assertDebuggingNotCalled();
+        $event5->trigger();
+        $this->assertDebuggingCalled();
+
+        $url = new moodle_url('/admin/');
+        $event6 = \core_tests\event\problematic_event1::create(array('context'=>\context_system::instance(), 'other'=>array('a'=>$url)));
+        $this->assertDebuggingNotCalled();
+        $event6->trigger();
+        $this->assertDebuggingCalled();
+
+        $event = \core_tests\event\problematic_event2::create(array());
+        $this->assertDebuggingNotCalled();
+        $event = \core_tests\event\problematic_event2::create(array('context'=>\context_system::instance()));
+        $this->assertDebuggingCalled();
+
+        $event = \core_tests\event\problematic_event3::create(array('other'=>1));
+        $this->assertDebuggingNotCalled();
+        $event = \core_tests\event\problematic_event3::create(array());
+        $this->assertDebuggingCalled();
+    }
+
+    public function test_record_snapshots() {
+        global $DB;
+
+        $event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
+        $course1 = $DB->get_record('course', array('id'=>1));
+        $this->assertNotEmpty($course1);
+
+        $event->add_record_snapshot('course', $course1);
+
+        $result = $event->get_record_snapshot('course', 1, $course1);
+        $this->assertSame($course1, $result);
+
+        $user = $event->get_record_snapshot('user', 1);
+        $this->assertEquals(1, $user->id);
+        $this->assertSame('guest', $user->username);
+
+        $event->add_record_snapshot('course', $course1);
+        $event->trigger();
+        try {
+            $event->add_record_snapshot('course', $course1);
+            $this->fail('Updating of snapshots after trigger is not ok');;
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('\coding_exception', $e);
+        }
+
+        $event2 = \core_tests\event\unittest_executed::restore($event->get_data(), array());
+        try {
+            $event2->get_record_snapshot('course', 1, $course1);
+            $this->fail('Reading of snapshots from restored events is not ok');;
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('\coding_exception', $e);
+        }
+    }
+
+    public function test_iteration() {
+        $event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10)));
+
+        $data = array();
+        foreach ($event as $k => $v) {
+            $data[$k] = $v;
+        }
+
+        $this->assertSame($event->get_data(), $data);
+    }
+}
index 82e61e5..e19209b 100644 (file)
@@ -27,7 +27,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 
-class eventslib_testcase extends advanced_testcase {
+class core_eventslib_testcase extends advanced_testcase {
 
     /**
      * Create temporary entries in the database for these tests.
diff --git a/lib/tests/fixtures/event_fixtures.php b/lib/tests/fixtures/event_fixtures.php
new file mode 100644 (file)
index 0000000..c8a287f
--- /dev/null
@@ -0,0 +1,186 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_tests\event;
+
+/**
+ * Fixtures for new event testing.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+class unittest_executed extends \core\event\base {
+    public $nest = false;
+
+    public static function get_name() {
+        return 'xxx';
+    }
+
+    public function get_description() {
+        return 'yyy';
+    }
+
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['level'] = 10;
+    }
+
+    public function get_url() {
+        return new moodle_url('/somepath/somefile.php', array('id'=>$this->data['other']['sample']));
+    }
+
+    protected function get_legacy_eventname() {
+        return 'test_legacy';
+    }
+
+    protected function get_legacy_eventdata() {
+        return array($this->data['courseid'], $this->data['other']['sample']);
+    }
+
+    protected function get_legacy_logdata() {
+        return array($this->data['courseid'], 'core_unittest', 'view', 'unittest.php?id='.$this->data['other']['sample']);
+    }
+}
+
+
+class unittest_observer {
+    public static $info = array();
+    public static $event = array();
+
+    public static function reset() {
+        self::$info = array();
+        self::$event = array();
+    }
+
+    public static function observe_one(unittest_executed $event) {
+        self::$info[] = 'observe_one-'.$event->courseid;
+        self::$event[] = $event;
+    }
+
+    public static function external_observer(\core\event\base $event) {
+        self::$info[] = 'external_observer-'.$event->courseid;
+        self::$event[] = $event;
+    }
+
+    public static function broken_observer(\core\event\base $event) {
+        self::$info[] = 'broken_observer-'.$event->courseid;
+        self::$event[] = $event;
+        throw new \Exception('someerror');
+    }
+
+    public static function observe_all(unittest_executed $event) {
+        self::$event[] = $event;
+        if ($event->nest) {
+            self::$info[] = 'observe_all-nesting-'.$event->courseid;
+            unittest_executed::create(array('courseid'=>3, 'context'=>\context_system::instance(), 'other'=>array('sample'=>666, 'xx'=>666)))->trigger();
+        } else {
+            self::$info[] = 'observe_all-'.$event->courseid;
+        }
+    }
+
+    public static function legacy_handler($data) {
+        self::$info[] = 'legacy_handler-'.$data[0];
+        self::$event[] = $data;
+    }
+}
+
+class bad_event1 extends \core\event\base {
+    protected function init() {
+        //$this->data['crud'] = 'u';
+        $this->data['level'] = 10;
+    }
+}
+
+class bad_event2 extends \core\event\base {
+    protected function init() {
+        $this->data['crud'] = 'u';
+        //$this->data['level'] = 10;
+    }
+}
+
+class bad_event3 extends \core\event\base {
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['level'] = 10;
+        unset($this->data['courseid']);
+    }
+}
+
+class bad_event4 extends \core\event\base {
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['level'] = 10;
+        $this->data['xxx'] = 1;
+    }
+}
+
+class bad_event5 extends \core\event\base {
+    protected function init() {
+        $this->data['crud'] = 'x';
+        $this->data['level'] = 10;
+    }
+}
+
+class bad_event6 extends \core\event\base {
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['level'] = 10;
+        $this->data['objecttable'] = 'xxx_xxx_xx';
+    }
+}
+
+class bad_event7 extends \core\event\base {
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['level'] = 10;
+        $this->data['objecttable'] = null;
+    }
+}
+
+class problematic_event1 extends \core\event\base {
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['level'] = 10;
+    }
+}
+
+class problematic_event2 extends \core\event\base {
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['level'] = 10;
+        $this->context = \context_system::instance();
+    }
+}
+
+class problematic_event3 extends \core\event\base {
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['level'] = 10;
+        $this->context = \context_system::instance();
+    }
+
+    protected function validate_data() {
+        if (empty($this->data['other'])) {
+            debugging('other is missing');
+        }
+    }
+}
index 5d2e657..8121e4c 100644 (file)
@@ -37,6 +37,14 @@ $handlers = array (
         'handlerfunction'  => array('eventslib_sample_handler_class', 'static_method'),
         'schedule'         => 'cron',
         'internal'         => 1,
-    )
+    ),
+
+    'test_legacy' => array (
+        'handlerfile'      => '/lib/tests/event_test.php',
+        'handlerfunction'  => '\core_tests\event\unittest_observer::legacy_handler',
+        'schedule'         => 'instant',
+        'internal'         => 1,
+    ),
+
 );
 
index 63431ee..12dcb0e 100644 (file)
@@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright 2012 The Open University
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_setuplib_testcase extends basic_testcase {
+class core_setuplib_testcase extends advanced_testcase {
 
     /**
      * Test get_docs_url_standard in the normal case when we should link to Moodle docs.
@@ -126,7 +126,7 @@ class core_setuplib_testcase extends basic_testcase {
         global $CFG;
 
         // This doesn't test them all possible ones, but these are set for unit tests.
-        $cfgnames = array('dataroot', 'dirroot', 'tempdir', 'cachedir');
+        $cfgnames = array('dataroot', 'dirroot', 'tempdir', 'cachedir', 'localcachedir');
 
         $fixture  = '';
         $expected = '';
@@ -142,4 +142,80 @@ class core_setuplib_testcase extends basic_testcase {
         $this->assertContains($expected, $exceptioninfo->message, 'Exception message does not contain system paths');
         $this->assertContains($expected, $exceptioninfo->debuginfo, 'Exception debug info does not contain system paths');
     }
+
+    public function test_localcachedir() {
+        global $CFG;
+
+        $this->resetAfterTest(true);
+
+        // Test default location - can not be modified in phpunit tests because we override everything in config.php.
+        $this->assertSame("$CFG->dataroot/localcache", $CFG->localcachedir);
+
+        $now = time();
+        $timestampfile = "$CFG->localcachedir/.lastpurged";
+
+        $dir = make_localcache_directory('', false);
+        $this->assertSame($CFG->localcachedir, $dir);
+        $this->assertFileNotExists("$CFG->localcachedir/.htaccess");
+        $this->assertFileExists($timestampfile);
+        $this->assertGreaterThanOrEqual($now, filemtime($timestampfile));
+        $this->assertLessThanOrEqual(time(), filemtime($timestampfile));
+
+        $dir = make_localcache_directory('test/test', false);
+        $this->assertSame("$CFG->localcachedir/test/test", $dir);
+
+        // Test custom location.
+        $CFG->localcachedir = "$CFG->dataroot/testlocalcache";
+        $now = time();
+        $timestampfile = "$CFG->localcachedir/.lastpurged";
+        $this->assertFileNotExists($timestampfile);
+
+        $dir = make_localcache_directory('', false);
+        $this->assertSame($CFG->localcachedir, $dir);
+        $this->assertFileExists("$CFG->localcachedir/.htaccess");
+        $this->assertFileExists($timestampfile);
+        $this->assertGreaterThanOrEqual($now, filemtime($timestampfile));
+        $this->assertLessThanOrEqual(time(), filemtime($timestampfile));
+
+        $dir = make_localcache_directory('test', false);
+        $this->assertSame("$CFG->localcachedir/test", $dir);
+
+        $prevtime = filemtime($timestampfile);
+        $dir = make_localcache_directory('pokus', false);
+        $this->assertSame("$CFG->localcachedir/pokus", $dir);
+        $this->assertSame($prevtime, filemtime($timestampfile));
+
+
+        // Test purging.
+        $testfile = "$CFG->localcachedir/test/test.txt";
+        $this->assertTrue(touch($testfile));
+
+        $now = time();
+        set_config('localcachedirpurged', $now - 2);
+        purge_all_caches();
+        $this->assertFileNotExists($testfile);
+        $this->assertFileNotExists(dirname($testfile));
+        $this->assertFileExists($timestampfile);
+        $this->assertGreaterThanOrEqual($now, filemtime($timestampfile));
+        $this->assertLessThanOrEqual(time(), filemtime($timestampfile));
+        $this->assertGreaterThanOrEqual($now, $CFG->localcachedirpurged);
+        $this->assertLessThanOrEqual(time(), $CFG->localcachedirpurged);
+
+        // Simulates purge_all_caches() on another server node.
+        make_localcache_directory('test', false);
+        $this->assertTrue(touch($testfile));
+        set_config('localcachedirpurged', $now - 1);
+        $this->assertTrue(touch($timestampfile, $now - 2));
+        clearstatcache();
+        $this->assertSame($now - 2, filemtime($timestampfile));
+
+        $now = time();
+        $dir = make_localcache_directory('', false);
+        $this->assertSame("$CFG->localcachedir", $dir);
+        $this->assertFileNotExists($testfile);
+        $this->assertFileNotExists(dirname($testfile));
+        $this->assertFileExists($timestampfile);
+        $this->assertGreaterThanOrEqual($now, filemtime($timestampfile));
+        $this->assertLessThanOrEqual(time(), filemtime($timestampfile));
+    }
 }
index 4c865c0..d0a4192 100644 (file)
@@ -10,6 +10,7 @@ information provided here is intended especially for developers.
   core_component::get_plugin_list() does not accept empty parameter any more.
 * Use core_text::* instead of textlib:: and also core_collator::* instead of collatorlib::*.
 * Use new function moodleform::mock_submit() to simulate form submission in unit tests (backported).
+* New $CFG->localcachedir setting useful for cluster nodes. Admins have to update X-Sendfile aliases if used.
 
 DEPRECATIONS:
 Various previously deprecated functions have now been altered to throw DEBUG_DEVELOPER debugging notices
@@ -41,6 +42,8 @@ Accesslib:
     * get_role_context_caps()       ->  (no replacement)
     * load_temp_role()              ->  (no replacement)
     * remove_temp_roles()           ->  (no replacement)
+    * get_related_contexts_string() ->  $context->get_parent_context_ids(true)
+    * get_recent_enrolments()       ->  (no replacement)
 
 Enrollment:
     * get_course_participants()     -> get_enrolled_users()
index 32e4637..1f552d5 100644 (file)
@@ -1467,12 +1467,17 @@ function install_core($version, $verbose) {
     global $CFG, $DB;
 
     // We can not call purge_all_caches() yet, make sure the temp and cache dirs exist and are empty.
-    make_cache_directory('', true);
     remove_dir($CFG->cachedir.'', true);
-    make_temp_directory('', true);
+    make_cache_directory('', true);
+
+    remove_dir($CFG->localcachedir.'', true);
+    make_localcache_directory('', true);
+
     remove_dir($CFG->tempdir.'', true);
-    make_writable_directory($CFG->dataroot.'/muc', true);
+    make_temp_directory('', true);
+
     remove_dir($CFG->dataroot.'/muc', true);
+    make_writable_directory($CFG->dataroot.'/muc', true);
 
     try {
         set_time_limit(600);
index 5e36846..7b0efd3 100644 (file)
@@ -214,14 +214,16 @@ if (!empty($instanceid) && !empty($roleid)) {
     list($actionsql, $params) = $DB->get_in_or_equal($actions, SQL_PARAMS_NAMED, 'action');
     $actionsql = "action $actionsql";
 
-    $relatedcontexts = get_related_contexts_string($context);
+    // We want to query both the current context and parent contexts.
+    list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
 
     $sql = "SELECT ra.userid, u.firstname, u.lastname, u.idnumber, l.actioncount AS count
-            FROM (SELECT * FROM {role_assignments} WHERE contextid $relatedcontexts AND roleid = :roleid ) ra
+            FROM (SELECT * FROM {role_assignments} WHERE contextid $relatedctxsql AND roleid = :roleid ) ra
             JOIN {user} u ON u.id = ra.userid
             LEFT JOIN (
                 SELECT userid, COUNT(action) AS actioncount FROM {log} WHERE cmid = :instanceid AND time > :timefrom AND $actionsql GROUP BY userid
             ) l ON (l.userid = ra.userid)";
+    $params = array_merge($params, $relatedctxparams);
     $params['roleid'] = $roleid;
     $params['instanceid'] = $instanceid;
     $params['timefrom'] = $timefrom;
@@ -239,7 +241,7 @@ if (!empty($instanceid) && !empty($roleid)) {
     $countsql = "SELECT COUNT(DISTINCT(ra.userid))
                    FROM {role_assignments} ra
                    JOIN {user} u ON u.id = ra.userid
-                  WHERE ra.contextid $relatedcontexts AND ra.roleid = :roleid";
+                  WHERE ra.contextid $relatedctxsql AND ra.roleid = :roleid";
 
     $totalcount = $DB->count_records_sql($countsql, $params);
 
diff --git a/theme/font.php b/theme/font.php
new file mode 100644 (file)
index 0000000..f093421
--- /dev/null
@@ -0,0 +1,235 @@
+<?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 is responsible for serving the fonts used in CSS.
+ *
+ * Note: it is recommended to use only WOFF (Web Open Font Format) fonts.
+ *
+ * @package   core
+ * @copyright 2013 Petr Skoda (skodak)  {@link http://skodak.org}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// Disable moodle specific debug messages and any errors in output,
+// comment out when debugging or better look into error log!
+define('NO_DEBUG_DISPLAY', true);
+
+define('ABORT_AFTER_CONFIG', true);
+require('../config.php');
+
+if ($slashargument = min_get_slash_argument()) {
+    $slashargument = ltrim($slashargument, '/');
+    if (substr_count($slashargument, '/') < 3) {
+        font_not_found();
+    }
+    list($themename, $component, $rev, $font) = explode('/', $slashargument, 4);
+    $themename = min_clean_param($themename, 'SAFEDIR');
+    $component = min_clean_param($component, 'SAFEDIR');
+    $rev       = min_clean_param($rev, 'INT');
+    $font      = min_clean_param($font, 'RAW');
+
+} else {
+    $themename = min_optional_param('theme', 'standard', 'SAFEDIR');
+    $component = min_optional_param('component', 'core', 'SAFEDIR');
+    $rev       = min_optional_param('rev', -1, 'INT');
+    $font      = min_optional_param('font', '', 'RAW');
+}
+
+if (!$font) {
+    font_not_found();
+}
+
+if (empty($component) or $component === 'moodle' or $component === 'core') {
+    $component = 'core';
+}
+
+if (preg_match('/^[a-z0-9_-]+\.woff$/i', $font, $matches)) {
+    // This is the real standard!
+    $font = $matches[0];
+    $mimetype = 'application/font-woff';
+
+} else if (preg_match('^[a-z0-9_-]+/\.ttf$/i', $font, $matches)) {
+    $font = $matches[0];
+    $mimetype = 'application/x-font-ttf';
+
+} else if (preg_match('^[a-z0-9_-]+/\.otf$/i', $font, $matches)) {
+    $font = $matches[0];
+    $mimetype = 'application/x-font-opentype';
+
+} else if (preg_match('^[a-z0-9_-]+/\.oet$/i', $font, $matches)) {
+    // IE8 must die!!!
+    $font = $matches[0];
+    $mimetype = 'application/vnd.ms-fontobject';
+
+} else {
+    font_not_found();
+}
+
+if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
+    // Normal theme exists.
+} else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) {
+    // Theme exists in alternative location.
+} else {
+    font_not_found();
+}
+
+$candidatelocation = "$CFG->localcachedir/theme/$rev/$themename/fonts/$component";
+$etag = sha1("$rev/$themename/$component/$font");
+
+if ($rev > 0) {
+    if (file_exists("$candidatelocation/$font.error")) {
+        font_not_found();
+    }
+
+    if (file_exists("$candidatelocation/$font")) {
+        if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+            // We do not actually need to verify the etag value because our files
+            // never change in cache because we increment the rev parameter.
+            $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often.
+            header('HTTP/1.1 304 Not Modified');
+            header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
+            header('Cache-Control: public, max-age='.$lifetime);
+            header('Content-Type: '.$mimetype);
+            header('Etag: "'.$etag.'"');
+            die;
+        }
+        send_cached_font("$candidatelocation/$font", $etag, $font, $mimetype);
+    }
+}
+
+// Ok, now we need to start normal moodle script, we need to load all libs and $DB.
+define('ABORT_AFTER_CONFIG_CANCEL', true);
+
+define('NO_MOODLE_COOKIES', true); // Session not used here.
+define('NO_UPGRADE_CHECK', true);  // Ignore upgrade check.
+
+require("$CFG->dirroot/lib/setup.php");
+
+$theme = theme_config::load($themename);
+$themerev = theme_get_revision();
+
+$fontfile = $theme->resolve_font_location($font, $component);
+
+if ($themerev <= 0 or $rev != $themerev) {
+    // Do not send caching headers if they do not request current revision,
+    // we do not want to pollute browser caches with outdated fonts.
+    if (empty($fontfile) or !is_readable($fontfile)) {
+        font_not_found();
+    }
+    send_uncached_font($fontfile, $font, $mimetype);
+}
+
+make_localcache_directory('theme', false);
+
+if (empty($fontfile) or !is_readable($fontfile)) {
+    if (!file_exists($candidatelocation)) {
+        @mkdir($candidatelocation, $CFG->directorypermissions, true);
+    }
+    // Make note we can not find this file.
+    $cachefont = "$candidatelocation/$font.error";
+    $fp = fopen($cachefont, 'w');
+    fclose($fp);
+    font_not_found();
+}
+
+$cachefont = cache_font($font, $fontfile, $candidatelocation);
+if (connection_aborted()) {
+    die;
+}
+// Make sure nothing failed.
+clearstatcache();
+if (file_exists($cachefont)) {
+    send_cached_font($cachefont, $etag, $font, $mimetype);
+}
+
+send_uncached_font($fontfile, $font, $mimetype);
+
+
+
+// Utility functions.
+
+function send_cached_font($fontpath, $etag, $font, $mimetype) {
+    global $CFG;
+    require("$CFG->dirroot/lib/xsendfilelib.php");
+
+    $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often.
+
+    header('Etag: "'.$etag.'"');
+    header('Content-Disposition: inline; filename="'.$font.'"');
+    header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($fontpath)) .' GMT');
+    header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
+    header('Pragma: ');
+    header('Cache-Control: public, max-age='.$lifetime);
+    header('Accept-Ranges: none');
+    header('Content-Type: '.$mimetype);
+    header('Content-Length: '.filesize($fontpath));
+
+    if (xsendfile($fontpath)) {
+        die;
+    }
+
+    // No need to gzip already compressed fonts.
+
+    readfile($fontpath);
+    die;
+}
+
+function send_uncached_font($fontpath, $font, $mimetype) {
+    header('Content-Disposition: inline; filename="'.$font.'"');
+    header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
+    header('Expires: '. gmdate('D, d M Y H:i:s', time() + 15) .' GMT');
+    header('Pragma: ');
+    header('Accept-Ranges: none');
+    header('Content-Type: '.$mimetype);
+    header('Content-Length: '.filesize($fontpath));
+
+    readfile($fontpath);
+    die;
+}
+
+function font_not_found() {
+    header('HTTP/1.0 404 not found');
+    die('font was not found, sorry.');
+}
+
+/**
+ * Caches a given font file.
+ *
+ * @param string $font The name of the font that was requested.
+ * @param string $fontfile The location of the font file we want to cache.
+ * @param string $candidatelocation The location to cache it in.
+ * @return string The path to the cached font.
+ */
+function cache_font($font, $fontfile, $candidatelocation) {
+    global $CFG;
+    $cachefont = "$candidatelocation/$font";
+
+    clearstatcache();
+    if (!file_exists($candidatelocation)) {
+        @mkdir($candidatelocation, $CFG->directorypermissions, true);
+    }
+
+    // Prevent serving of incomplete file from concurrent request,
+    // the rename() should be more atomic than copy().
+    ignore_user_abort(true);
+    if (@copy($fontfile, $cachefont.'.tmp')) {
+        rename($cachefont.'.tmp', $cachefont);
+        @chmod($cachefont, $CFG->filepermissions);
+        @unlink($cachefont.'.tmp'); // Just in case anything fails.
+    }
+    return $cachefont;
+}
index 9e4ba1b..e3839b2 100644 (file)
@@ -74,7 +74,7 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
     image_not_found();
 }
 
-$candidatelocation = "$CFG->cachedir/theme/$rev/$themename/pix/$component";
+$candidatelocation = "$CFG->localcachedir/theme/$rev/$themename/pix/$component";
 $etag = sha1("$rev/$themename/$component/$image");
 
 if ($rev > 0) {
@@ -142,7 +142,7 @@ if ($themerev <= 0 or $rev != $themerev) {
     send_uncached_image($imagefile);
 }
 
-make_cache_directory('theme', false);
+make_localcache_directory('theme', false);
 
 // We're not using SVG and there is no cached version of this file (in any format).
 // As we're going to be caching a format other than svg, and because svg use is conditional we need to ensure that at the same
index 66a400c..c51665f 100644 (file)
@@ -97,11 +97,11 @@ if (!empty($CFG->enabledevicedetection) && empty($device)) {
     $table->head = array(get_string('devicetype', 'admin'), get_string('currenttheme', 'admin'), get_string('info'));
 
     $devices = get_device_type_list();
-    foreach ($devices as $device) {
+    foreach ($devices as $thedevice) {
 
         $headingthemename = ''; // To output the picked theme name when needed
-        $themename = get_selected_theme_for_device_type($device);
-        if (!$themename && $device == 'default') {
+        $themename = get_selected_theme_for_device_type($thedevice);
+        if (!$themename && $thedevice == 'default') {
             $themename = theme_config::DEFAULT_THEME;
         }
 
@@ -112,7 +112,7 @@ if (!empty($CFG->enabledevicedetection) && empty($device)) {
             $themename = clean_param($themename, PARAM_THEME);
             if (empty($themename)) {
                 // Likely the theme has been deleted
-                unset_config(get_device_cfg_var_name($device));
+                unset_config(get_device_cfg_var_name($thedevice));
             } else {
                 $strthemename = get_string('pluginname', 'theme_'.$themename);
                 // link to the screenshot, now mandatory - the image path is hardcoded because we need image from other themes, not the current one
@@ -123,19 +123,19 @@ if (!empty($CFG->enabledevicedetection) && empty($device)) {
                 $headingthemename = $OUTPUT->heading($strthemename, 3);
             }
             // If not default device then show option to unset theme.
-            if ($device != 'default') {
+            if ($thedevice != 'default') {
                 $unsetthemestr = get_string('unsettheme', 'admin');
-                $unsetthemeurl = new moodle_url('/theme/index.php', array('device' => $device, 'sesskey' => sesskey(), 'unsettheme' => true));
+                $unsetthemeurl = new moodle_url('/theme/index.php', array('device' => $thedevice, 'sesskey' => sesskey(), 'unsettheme' => true));
                 $unsetthemebutton = new single_button($unsetthemeurl, $unsetthemestr, 'get');
                 $unsetthemebutton = $OUTPUT->render($unsetthemebutton);
             }
         }
 
-        $deviceurl = new moodle_url('/theme/index.php', array('device' => $device, 'sesskey' => sesskey()));
+        $deviceurl = new moodle_url('/theme/index.php', array('device' => $thedevice, 'sesskey' => sesskey()));
         $select = new single_button($deviceurl, $strthemeselect, 'get');
 
         $table->data[] = array(
-            $OUTPUT->heading(ucfirst($device), 3),
+            $OUTPUT->heading(ucfirst($thedevice), 3),
             $screenshotcell,
             $headingthemename . $OUTPUT->render($select) . $unsetthemebutton
         );
@@ -217,7 +217,11 @@ if (!empty($CFG->enabledevicedetection) && empty($device)) {
 echo $OUTPUT->header('themeselector');
 echo $OUTPUT->heading($heading);
 
-echo $OUTPUT->single_button(new moodle_url('index.php', array('sesskey' => sesskey(), 'reset' => 1, 'device' => $device)), get_string('themeresetcaches', 'admin'));
+$params = array('sesskey' => sesskey(), 'reset' => 1);
+if (!empty($device)) {
+    $params['device'] = $device;
+}
+echo $OUTPUT->single_button(new moodle_url('index.php', $params), get_string('themeresetcaches', 'admin'));
 
 echo html_writer::table($table);
 
index a514237..8c34573 100644 (file)
@@ -63,7 +63,7 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
     die('Theme was not found, sorry.');
 }
 
-$candidate = "$CFG->cachedir/theme/$rev/$themename/javascript_$type.js";
+$candidate = "$CFG->localcachedir/theme/$rev/$themename/javascript_$type.js";
 $etag = sha1("$rev/$themename/$type");
 
 if ($rev > 0 and file_exists($candidate)) {
@@ -93,7 +93,7 @@ if ($themerev <= 0 or $rev != $themerev) {
     js_send_uncached($theme->javascript_content($type));
 }
 
-make_cache_directory('theme', false);
+make_localcache_directory('theme', false);
 
 js_write_cache_file_content($candidate, js_minify($theme->javascript_files($type)));
 // Verify nothing failed in cache file creation.
index f0129c3..0279b51 100644 (file)
@@ -87,7 +87,7 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
     die('Theme was not found, sorry.');
 }
 
-$candidatedir = "$CFG->cachedir/theme/$rev/$themename/css";
+$candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
 $etag = "$rev/$themename/$type";
 $candidatename = $type;
 if (!$usesvg) {
@@ -148,7 +148,7 @@ if ($themerev <= 0 or $themerev != $rev) {
     $etag = sha1($etag);
 }
 
-make_cache_directory('theme', false);
+make_localcache_directory('theme', false);
 
 if ($type === 'editor') {
     $cssfiles = $theme->editor_css_files();
index c5200a7..8130981 100644 (file)
                 ));
     $table->setup();
 
-    // we are looking for all users with this role assigned in this context or higher
-    $contextlist = get_related_contexts_string($context);
-
     list($esql, $params) = get_enrolled_sql($context, NULL, $currentgroup, true);
     $joins = array("FROM {user} u");
     $wheres = array();
 
     // limit list to users with some role only
     if ($roleid) {
-        $wheres[] = "u.id IN (SELECT userid FROM {role_assignments} WHERE roleid = :roleid AND contextid $contextlist)";
-        $params['roleid'] = $roleid;
+        // We want to query both the current context and parent contexts.
+        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
+        $wheres[] = "u.id IN (SELECT userid FROM {role_assignments} WHERE roleid = :roleid AND contextid $relatedctxsql)";
+        $params = array_merge($params, array('roleid' => $roleid), $relatedctxparams);
     }
 
     $from = implode("\n", $joins);
index f64e687..565c5fc 100644 (file)
@@ -781,6 +781,9 @@ class group_non_members_selector extends groups_user_selector_base {
             $roleparams = array();
         }
 
+        // We want to query both the current context and parent contexts.
+        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
+
         // Get the search condition.
         list($searchcondition, $searchparams) = $this->search_sql($search, 'u');
 
@@ -794,7 +797,7 @@ class group_non_members_selector extends groups_user_selector_base {
                             WHERE igm.userid = u.id AND ig.courseid = :courseid) AS numgroups";
         $sql = "   FROM {user} u
                    JOIN ($enrolsql) e ON e.id = u.id
-              LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid " . get_related_contexts_string($context) . " AND ra.roleid $roleids)
+              LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid $relatedctxsql AND ra.roleid $roleids)
               LEFT JOIN {role} r ON r.id = ra.roleid
               LEFT JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)
                   WHERE u.deleted = 0
@@ -804,7 +807,7 @@ class group_non_members_selector extends groups_user_selector_base {
         list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext);
         $orderby = ' ORDER BY ' . $sort;
 
-        $params = array_merge($searchparams, $roleparams, $enrolparams);
+        $params = array_merge($searchparams, $roleparams, $enrolparams, $relatedctxparams);
         $params['courseid'] = $this->courseid;
         $params['groupid']  = $this->groupid;