Merge branch 'MDL-48969_m29v9' of https://github.com/sbourget/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 24 Mar 2015 11:48:44 +0000 (11:48 +0000)
committerDan Poltawski <dan@moodle.com>
Tue, 24 Mar 2015 11:48:44 +0000 (11:48 +0000)
226 files changed:
admin/tool/behat/cli/run.php
admin/user/user_bulk_cohortadd.php
backup/moodle2/restore_final_task.class.php
backup/moodle2/restore_stepslib.php
blocks/moodleblock.class.php
blocks/upgrade.txt
calendar/lib.php
calendar/tests/behat/calendar.feature
completion/classes/external.php [new file with mode: 0644]
completion/tests/externallib_test.php [new file with mode: 0644]
composer.json
config-dist.php
course/modedit.php
course/modlib.php
course/moodleform_mod.php
enrol/cohort/edit.php
enrol/cohort/edit_form.php
enrol/meta/addinstance.php
enrol/meta/addinstance_form.php
enrol/paypal/lib.php
filter/data/filter.php
filter/data/tests/filter_test.php [new file with mode: 0644]
grade/grading/form/guide/lib.php
grade/grading/form/guide/tests/guide_test.php [new file with mode: 0644]
grade/report/user/styles.css
grade/tests/behat/grade_to_pass.feature [new file with mode: 0644]
lang/en/enrol.php
lang/en/grades.php
lang/en/webservice.php
lib/ajax/service.php [new file with mode: 0644]
lib/amd/build/ajax.min.js [new file with mode: 0644]
lib/amd/build/config.min.js [new file with mode: 0644]
lib/amd/build/mustache.min.js [new file with mode: 0644]
lib/amd/build/notification.min.js [new file with mode: 0644]
lib/amd/build/str.min.js [new file with mode: 0644]
lib/amd/build/templates.min.js [new file with mode: 0644]
lib/amd/build/url.min.js [new file with mode: 0644]
lib/amd/build/yui.min.js [new file with mode: 0644]
lib/amd/src/ajax.js [new file with mode: 0644]
lib/amd/src/config.js [new file with mode: 0644]
lib/amd/src/first.js
lib/amd/src/mustache.js [new file with mode: 0644]
lib/amd/src/notification.js [new file with mode: 0644]
lib/amd/src/str.js [new file with mode: 0644]
lib/amd/src/templates.js [new file with mode: 0644]
lib/amd/src/url.js [new file with mode: 0644]
lib/amd/src/yui.js [new file with mode: 0644]
lib/behat/classes/behat_selectors.php
lib/blocklib.php
lib/classes/output/external.php [new file with mode: 0644]
lib/classes/output/mustache_filesystem_loader.php [new file with mode: 0644]
lib/classes/output/mustache_javascript_helper.php [new file with mode: 0644]
lib/classes/output/mustache_pix_helper.php [new file with mode: 0644]
lib/classes/output/mustache_string_helper.php [new file with mode: 0644]
lib/classes/output/mustache_uniqid_helper.php [new file with mode: 0644]
lib/coursecatlib.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/clean.js
lib/editor/atto/yui/src/editor/js/editor.js
lib/external/externallib.php
lib/externallib.php
lib/moodlelib.php
lib/mustache/CONTRIBUTING.md [new file with mode: 0644]
lib/mustache/LICENSE [new file with mode: 0644]
lib/mustache/README.md [new file with mode: 0644]
lib/mustache/composer.json [new file with mode: 0644]
lib/mustache/readme_moodle.txt [new file with mode: 0644]
lib/mustache/src/Mustache/Autoloader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache/AbstractCache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache/FilesystemCache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache/NoopCache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Compiler.php [new file with mode: 0644]
lib/mustache/src/Mustache/Context.php [new file with mode: 0644]
lib/mustache/src/Mustache/Engine.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/InvalidArgumentException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/LogicException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/RuntimeException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/SyntaxException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/UnknownFilterException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/UnknownHelperException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/UnknownTemplateException.php [new file with mode: 0644]
lib/mustache/src/Mustache/HelperCollection.php [new file with mode: 0644]
lib/mustache/src/Mustache/LambdaHelper.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/ArrayLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/CascadingLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/FilesystemLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/InlineLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/MutableLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/StringLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Logger.php [new file with mode: 0644]
lib/mustache/src/Mustache/Logger/AbstractLogger.php [new file with mode: 0644]
lib/mustache/src/Mustache/Logger/StreamLogger.php [new file with mode: 0644]
lib/mustache/src/Mustache/Parser.php [new file with mode: 0644]
lib/mustache/src/Mustache/Template.php [new file with mode: 0644]
lib/mustache/src/Mustache/Tokenizer.php [new file with mode: 0644]
lib/outputcomponents.php
lib/outputfactories.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/setup.php
lib/tablelib.php
lib/templates/pix_icon.mustache [new file with mode: 0644]
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_general.php
lib/tests/regex_test.php [new file with mode: 0644]
lib/tests/tablelib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js
lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js
lib/yui/build/moodle-core-checknet/moodle-core-checknet.js
lib/yui/build/moodle-core-handlebars/moodle-core-handlebars-debug.js
lib/yui/build/moodle-core-handlebars/moodle-core-handlebars-min.js
lib/yui/build/moodle-core-handlebars/moodle-core-handlebars.js
lib/yui/src/checknet/js/checknet.js
lib/yui/src/handlebars/js/handlebars.js
message/lib.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/gradingtable.php
mod/book/lib.php
mod/book/tests/lib_test.php [new file with mode: 0644]
mod/choice/backup/moodle2/backup_choice_stepslib.php
mod/choice/db/install.xml
mod/choice/db/upgrade.php
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/mod_form.php
mod/choice/report.php
mod/choice/tests/behat/include_inactive.feature [new file with mode: 0644]
mod/choice/version.php
mod/choice/view.php
mod/data/classes/external.php [new file with mode: 0644]
mod/data/db/services.php [new file with mode: 0644]
mod/data/tests/externallib_test.php [new file with mode: 0644]
mod/data/version.php
mod/feedback/lib.php
mod/forum/classes/post_form.php
mod/forum/db/access.php
mod/forum/lang/en/forum.php
mod/forum/post.php
mod/forum/tests/behat/post_to_multiple_groups.feature [new file with mode: 0644]
mod/forum/tests/behat/separate_group_discussions.feature
mod/forum/tests/behat/separate_group_single_group_discussions.feature
mod/forum/version.php
mod/glossary/import.php
mod/glossary/lib.php
mod/imscp/lib.php
mod/imscp/tests/lib_test.php [new file with mode: 0644]
mod/imscp/view.php
mod/lesson/backup/moodle2/backup_lesson_stepslib.php
mod/lesson/backup/moodle2/restore_lesson_stepslib.php
mod/lesson/classes/event/lesson_restarted.php [new file with mode: 0644]
mod/lesson/classes/event/lesson_resumed.php [new file with mode: 0644]
mod/lesson/classes/event/page_moved.php [new file with mode: 0644]
mod/lesson/db/install.xml
mod/lesson/db/upgrade.php
mod/lesson/editpage.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/mod_form.php
mod/lesson/pagetypes/shortanswer.php
mod/lesson/renderer.php
mod/lesson/tests/behat/completion_condition_time_spent.feature [new file with mode: 0644]
mod/lesson/tests/events_test.php
mod/lesson/version.php
mod/lesson/view.php
mod/lti/view.php
mod/quiz/attemptlib.php
mod/quiz/backup/moodle2/backup_quiz_stepslib.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/classes/structure.php
mod/quiz/db/install.xml
mod/quiz/db/upgrade.php
mod/quiz/edit_rest.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/repaginate.php
mod/quiz/styles.css
mod/quiz/tests/behat/attempt_require_previous.feature [new file with mode: 0644]
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/editing_require_previous.feature [new file with mode: 0644]
mod/quiz/tests/behat/quiz_reset.feature [new file with mode: 0644]
mod/quiz/tests/structure_test.php
mod/quiz/upgrade.txt
mod/quiz/version.php
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js
mod/quiz/yui/src/toolboxes/js/resource.js
mod/quiz/yui/src/toolboxes/js/section.js
mod/quiz/yui/src/toolboxes/js/toolbox.js
mod/quiz/yui/src/util/js/slot.js
mod/scorm/locallib.php
mod/upgrade.txt
mod/wiki/files.php
mod/wiki/filesedit.php
mod/wiki/pagelib.php
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/mod_form.php
phpunit.xml.dist
question/behaviour/behaviourbase.php
question/behaviour/immediatefeedback/behaviour.php
question/behaviour/interactive/behaviour.php
question/behaviour/upgrade.txt
question/engine/questionattempt.php
question/engine/questionusage.php
question/type/multianswer/renderer.php
repository/filesystem/lib.php
user/externallib.php
user/tests/externallib_test.php
version.php
webservice/renderer.php

index c83259f..92c393c 100644 (file)
@@ -128,7 +128,7 @@ array_walk($unrecognised, function (&$v) {
         $v = escapeshellarg($v);
     }
 });
-$extraopts = implode(' ', $unrecognised);
+$extraopts = $unrecognised;
 
 $tags = '';
 
@@ -139,10 +139,10 @@ if ($options['profile']) {
         exit(1);
     }
     $tags = $CFG->behat_config[$profile]['filters']['tags'];
-    $extraopts .= '--profile=\'' . $profile . "'";
+    $extraopts[] = '--profile=\'' . $profile . "'";
 } else if ($options['tags']) {
     $tags = $options['tags'];
-    $extraopts .= '--tags="' . $tags . '"';
+    $extraopts[] = '--tags="' . $tags . '"';
 }
 
 // Update config file if tags defined.
@@ -166,6 +166,7 @@ if ($tags) {
 }
 
 $cmds = array();
+$extraopts = implode(' ', $extraopts);
 echo "Running " . ($options['torun'] - $options['fromrun'] + 1) . " parallel behat sites:" . PHP_EOL;
 
 for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
index deb6143..958d6c1 100644 (file)
@@ -39,7 +39,8 @@ $users = $SESSION->bulk_users;
 $strnever = get_string('never');
 
 $cohorts = array(''=>get_string('choosedots'));
-$allcohorts = $DB->get_records('cohort');
+$allcohorts = $DB->get_records('cohort', null, 'name');
+
 foreach ($allcohorts as $c) {
     if (!empty($c->component)) {
         // external cohorts can not be modified
index e355871..124208a 100644 (file)
@@ -61,11 +61,8 @@ class restore_final_task extends restore_task {
             $this->add_step(new restore_grade_history_structure_step('grade_history', 'grade_history.xml'));
         }
 
-        // Course completion, executed conditionally if restoring to new course
-        if ($this->get_target() !== backup::TARGET_CURRENT_ADDING &&
-            $this->get_target() !== backup::TARGET_EXISTING_ADDING) {
-            $this->add_step(new restore_course_completion_structure_step('course_completion', 'completion.xml'));
-        }
+        // Course completion.
+        $this->add_step(new restore_course_completion_structure_step('course_completion', 'completion.xml'));
 
         // Conditionally restore course badges.
         if ($this->get_setting_value('badges')) {
index d779cfa..ca66102 100644 (file)
@@ -2453,11 +2453,12 @@ class restore_course_completion_structure_step extends restore_structure_step {
      *   2. The backup includes course completion information
      *   3. All modules are restorable
      *   4. All modules are marked for restore.
+     *   5. No completion criteria already exist for the course.
      *
      * @return bool True is safe to execute, false otherwise
      */
     protected function execute_condition() {
-        global $CFG;
+        global $CFG, $DB;
 
         // First check course completion is enabled on this site
         if (empty($CFG->enablecompletion)) {
@@ -2483,11 +2484,16 @@ class restore_course_completion_structure_step extends restore_structure_step {
             return false;
         }
 
-        // Finally check all modules within the backup are being restored.
+        // Check all modules within the backup are being restored.
         if ($this->task->is_excluding_activities()) {
             return false;
         }
 
+        // Check that no completion criteria is already set for the course.
+        if ($DB->record_exists('course_completion_criteria', array('course' => $this->get_courseid()))) {
+            return false;
+        }
+
         return true;
     }
 
index 333893d..a8024a8 100644 (file)
@@ -343,10 +343,13 @@ class block_base {
      * Default behavior: save all variables as $CFG properties
      * You don't need to override this if you 're satisfied with the above
      *
+     * @deprecated since Moodle 2.9 MDL-49385 - Please use Admin Settings functionality to save block configuration.
+     * @todo MDL-49553 This will be deleted in Moodle 3.1
      * @param array $data
      * @return boolean
      */
     function config_save($data) {
+        debugging('config_save($data) is deprecated, use Admin Settings functionality to save block configuration.', DEBUG_DEVELOPER);
         foreach ($data as $name => $value) {
             set_config($name, $value);
         }
index 288eff3..573aaeb 100644 (file)
@@ -4,6 +4,7 @@ information provided here is intended especially for developers.
 === 2.9 ===
 
 * The obsolete method preferred_width() was removed (it was not doing anything)
+* Deprecated block_base::config_save as is not called anywhere and should not be used.
 
 === 2.8 ===
 
index 572238a..605c328 100644 (file)
@@ -659,8 +659,14 @@ function calendar_add_event_metadata($event) {
         $event->courselink = calendar_get_courselink($event->courseid);
         $event->cssclass = 'calendar_event_course';
     } else if ($event->groupid) {                                    // Group event
-        $event->icon = '<img src="'.$OUTPUT->pix_url('i/groupevent') . '" alt="'.get_string('groupevent', 'calendar').'" class="icon" />';
-        $event->courselink = calendar_get_courselink($event->courseid);
+        if ($group = calendar_get_group_cached($event->groupid)) {
+            $groupname = format_string($group->name, true, context_course::instance($group->courseid));
+        } else {
+            $groupname = '';
+        }
+        $event->icon = html_writer::empty_tag('image', array('src' => $OUTPUT->pix_url('i/groupevent'),
+            'alt' => get_string('groupevent', 'calendar'), 'title' => $groupname, 'class' => 'icon'));
+        $event->courselink = calendar_get_courselink($event->courseid) . ', ' . $groupname;
         $event->cssclass = 'calendar_event_group';
     } else if($event->userid) {                                      // User event
         $event->icon = '<img src="'.$OUTPUT->pix_url('i/userevent') . '" alt="'.get_string('userevent', 'calendar').'" class="icon" />';
@@ -1415,6 +1421,20 @@ function calendar_get_course_cached(&$coursecache, $courseid) {
     return $coursecache[$courseid];
 }
 
+/**
+ * Get group from groupid for calendar display
+ *
+ * @param int $groupid
+ * @return stdClass group object with fields 'id', 'name' and 'courseid'
+ */
+function calendar_get_group_cached($groupid) {
+    static $groupscache = array();
+    if (!isset($groupscache[$groupid])) {
+        $groupscache[$groupid] = groups_get_group($groupid, 'id,name,courseid');
+    }
+    return $groupscache[$groupid];
+}
+
 /**
  * Returns the courses to load events for, the
  *
@@ -2947,7 +2967,7 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
     $name = $event->properties['SUMMARY'][0]->value;
     $name = str_replace('\n', '<br />', $name);
     $name = str_replace('\\', '', $name);
-    $name = preg_replace('/\s+/', ' ', $name);
+    $name = preg_replace('/\s+/u', ' ', $name);
 
     $eventrecord = new stdClass;
     $eventrecord->name = clean_param($name, PARAM_NOTAGS);
@@ -2959,7 +2979,7 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
         $description = clean_param($description, PARAM_NOTAGS);
         $description = str_replace('\n', '<br />', $description);
         $description = str_replace('\\', '', $description);
-        $description = preg_replace('/\s+/', ' ', $description);
+        $description = preg_replace('/\s+/u', ' ', $description);
     }
     $eventrecord->description = $description;
 
index 619590c..de0e752 100644 (file)
@@ -9,12 +9,20 @@ Feature: Perform basic calendar functionality
       | username | firstname | lastname | email |
       | student1 | Student | 1 | student1@asd.com |
       | student2 | Student | 2 | student2@asd.com |
+      | student3 | Student | 3 | student3@asd.com |
     And the following "courses" exist:
       | fullname | shortname | format |
       | Course 1 | C1 | topics |
     And the following "course enrolments" exist:
       | user | course | role |
       | student1 | C1 | student |
+      | student3 | C1 | student |
+    And the following "groups" exist:
+      | name | course | idnumber |
+      | Group 1 | C1 | G1 |
+    And the following "group members" exist:
+      | user | group |
+      | student1 | G1 |
     When I log in as "admin"
     And I follow "Course 1"
     And I turn editing mode on
@@ -50,6 +58,23 @@ Feature: Perform basic calendar functionality
     And I follow "This month"
     And I should not see "Really awesome event!"
 
+  Scenario: Create a group event
+    And I create a calendar event with form data:
+      | Type of event | group |
+      | Group | Group 1 |
+      | Event title | Really awesome event! |
+      | Description | Come join this awesome event |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "This month"
+    And I follow "Really awesome event!"
+    And "Group 1" "text" should exist in the ".eventlist" "css_element"
+    And I log out
+    And I log in as "student3"
+    And I follow "This month"
+    And I should not see "Really awesome event!"
+
   Scenario: Create a user event
     And I create a calendar event with form data:
       | Type of event | user |
diff --git a/completion/classes/external.php b/completion/classes/external.php
new file mode 100644 (file)
index 0000000..4d9c32e
--- /dev/null
@@ -0,0 +1,117 @@
+<?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/>.
+
+/**
+ * Completion external API
+ *
+ * @package    core_completion
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 2.9
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once("$CFG->libdir/externallib.php");
+require_once("$CFG->libdir/completionlib.php");
+
+/**
+ * Completion external functions
+ *
+ * @package    core_completion
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 2.9
+ */
+class core_completion_external extends external_api {
+
+    /**
+     * Describes the parameters for update_activity_completion_status_manually.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 2.9
+     */
+    public static function update_activity_completion_status_manually_parameters() {
+        return new external_function_parameters (
+            array(
+                'cmid' => new external_value(PARAM_INT, 'course module id'),
+                'completed' => new external_value(PARAM_BOOL, 'activity completed or not'),
+            )
+        );
+    }
+
+    /**
+     * Update completion status for the current user in an activity, only for activities with manual tracking.
+     * @param  int $cmid      Course module id
+     * @param  bool $completed Activity completed or not
+     * @return array            Result and possible warnings
+     * @since Moodle 2.9
+     * @throws moodle_exception
+     */
+    public static function update_activity_completion_status_manually($cmid,  $completed) {
+
+        // Validate and normalize parameters.
+        $params = self::validate_parameters(self::update_activity_completion_status_manually_parameters(),
+            array('cmid' => $cmid, 'completed' => $completed));
+        $cmid = $params['cmid'];
+        $completed = $params['completed'];
+
+        $warnings = array();
+
+        $context = context_module::instance($cmid);
+        self::validate_context($context);
+
+        list($course, $cm) = get_course_and_cm_from_cmid($cmid);
+
+        // Set up completion object and check it is enabled.
+        $completion = new completion_info($course);
+        if (!$completion->is_enabled()) {
+            throw new moodle_exception('completionnotenabled', 'completion');
+        }
+
+        // Check completion state is manual.
+        if ($cm->completion != COMPLETION_TRACKING_MANUAL) {
+            throw new moodle_exception('cannotmanualctrack', 'error');
+        }
+
+        $targetstate = ($completed) ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE;
+        $completion->update_state($cm, $targetstate);
+
+        $result = array();
+        $result['status'] = true;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the update_activity_completion_status_manually return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 2.9
+     */
+    public static function update_activity_completion_status_manually_returns() {
+
+        return new external_single_structure(
+            array(
+                'status'    => new external_value(PARAM_BOOL, 'status, true if success'),
+                'warnings'  => new external_warnings(),
+            )
+        );
+    }
+
+}
diff --git a/completion/tests/externallib_test.php b/completion/tests/externallib_test.php
new file mode 100644 (file)
index 0000000..24ee1a7
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+/**
+ * External completion functions unit tests
+ *
+ * @package    core_completion
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 2.9
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+/**
+ * External completion functions unit tests
+ *
+ * @package    core_completion
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 2.9
+ */
+class core_completion_externallib_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test update_activity_completion_status_manually
+     */
+    public function test_update_activity_completion_status_manually() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest(true);
+
+        $CFG->enablecompletion = true;
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
+        $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id),
+                                                             array('completion' => 1));
+        $cm = get_coursemodule_from_id('data', $data->cmid);
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
+
+        $this->setUser($user);
+
+        $result = core_completion_external::update_activity_completion_status_manually($data->cmid, true);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(
+            core_completion_external::update_activity_completion_status_manually_returns(), $result);
+
+        // Check in DB.
+        $this->assertEquals(1, $DB->get_field('course_modules_completion', 'completionstate',
+                            array('coursemoduleid' => $data->cmid)));
+
+        // Check using the API.
+        $completion = new completion_info($course);
+        $completiondata = $completion->get_data($cm);
+        $this->assertEquals(1, $completiondata->completionstate);
+        $this->assertTrue($result['status']);
+
+        $result = core_completion_external::update_activity_completion_status_manually($data->cmid, false);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(
+            core_completion_external::update_activity_completion_status_manually_returns(), $result);
+
+        $this->assertEquals(0, $DB->get_field('course_modules_completion', 'completionstate',
+                            array('coursemoduleid' => $data->cmid)));
+        $completiondata = $completion->get_data($cm);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $this->assertTrue($result['status']);
+    }
+}
index 60864a8..d1c1fff 100644 (file)
@@ -8,6 +8,6 @@
     "require-dev": {
         "phpunit/phpunit": "3.7.*",
         "phpunit/dbUnit": "1.2.*",
-        "moodlehq/behat-extension": "1.29.3"
+        "moodlehq/behat-extension": "1.29.4"
     }
 }
index 716d3c5..9ba0d91 100644 (file)
@@ -346,15 +346,6 @@ $CFG->admin = 'admin';
 //       $CFG->forcefirstname = 'Bruce';
 //       $CFG->forcelastname  = 'Simpson';
 //
-// The following setting will turn SQL Error logging on. This will output an
-// entry in apache error log indicating the position of the error and the statement
-// called. This option will action disregarding error_reporting setting.
-//     $CFG->dblogerror = true;
-//
-// The following setting will log every database query to a table called adodb_logsql.
-// Use this setting on a development server only, the table grows quickly!
-//     $CFG->logsql = true;
-//
 // The following setting will turn on username logging into Apache log. For full details regarding setting
 // up of this function please refer to the install section of the document.
 //     $CFG->apacheloguser = 0; // Turn this feature off. Default value.
@@ -711,7 +702,7 @@ $CFG->admin = 'admin';
 // (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');
+//   $CFG->behat_extraallowedsettings = array('somecoresetting', ...);
 //
 // You should explicitly allow the usage of the deprecated behat steps, otherwise an exception will
 // be thrown when using them. The setting is disabled by default.
index 234e3ad..aaa87e9 100644 (file)
@@ -189,6 +189,10 @@ if (!empty($add)) {
                                              'iteminstance'=>$data->instance, 'courseid'=>$course->id))) {
         // add existing outcomes
         foreach ($items as $item) {
+            if (!empty($item->gradepass)) {
+                $decimalpoints = $item->get_decimals();
+                $data->gradepass = format_float($item->gradepass, $decimalpoints);
+            }
             if (!empty($item->outcomeid)) {
                 $data->{'outcome_'.$item->outcomeid} = 1;
             }
index 9a44714..ed653ce 100644 (file)
@@ -191,8 +191,16 @@ function edit_module_post_actions($moduleinfo, $course) {
     // Sync idnumber with grade_item.
     if ($hasgrades && $grade_item = grade_item::fetch(array('itemtype'=>'mod', 'itemmodule'=>$moduleinfo->modulename,
                  'iteminstance'=>$moduleinfo->instance, 'itemnumber'=>0, 'courseid'=>$course->id))) {
+        $gradeupdate = false;
         if ($grade_item->idnumber != $moduleinfo->cmidnumber) {
             $grade_item->idnumber = $moduleinfo->cmidnumber;
+            $gradeupdate = true;
+        }
+        if (isset($moduleinfo->gradepass) && $grade_item->gradepass != $moduleinfo->gradepass) {
+            $grade_item->gradepass = $moduleinfo->gradepass;
+            $gradeupdate = true;
+        }
+        if ($gradeupdate) {
             $grade_item->update();
         }
     }
index 6a3a845..ef638b3 100644 (file)
@@ -299,6 +299,22 @@ abstract class moodleform_mod extends moodleform {
             $errors['assessed'] = get_string('scaleselectionrequired', 'rating');
         }
 
+        // Grade to pass: ensure that the grade to pass is valid for points and scales.
+        // If we are working with a scale, convert into a positive number for validation.
+
+        if (isset($data['gradepass']) && (isset($data['grade']) || isset($data['scale']))) {
+            $scale = isset($data['grade']) ? $data['grade'] : $data['scale'];
+            if ($scale < 0) {
+                $scalevalues = $DB->get_record('scale', array('id' => -$scale));
+                $grade = count(explode(',', $scalevalues->scale));
+            } else {
+                $grade = $scale;
+            }
+            if ($data['gradepass'] > $grade) {
+                $errors['gradepass'] = get_string('gradepassgreaterthangrade', 'grades', $grade);
+            }
+        }
+
         // Completion: Don't let them choose automatic completion without turning
         // on some conditions. Ignore this check when completion settings are
         // locked, as the options are then disabled.
@@ -624,6 +640,9 @@ abstract class moodleform_mod extends moodleform {
                     $mform->addElement('select', 'advancedgradingmethod_'.$areaname,
                         get_string('gradingmethod', 'core_grading'), $this->current->_advancedgradingdata['methods']);
                     $mform->addHelpButton('advancedgradingmethod_'.$areaname, 'gradingmethod', 'core_grading');
+                    if (!$this->_features->rating) {
+                        $mform->disabledIf('advancedgradingmethod_'.$areaname, 'grade[modgrade_type]', 'eq', 'none');
+                    }
 
                 } else {
                     // the module defines multiple gradable areas, display a selector
@@ -644,6 +663,19 @@ abstract class moodleform_mod extends moodleform {
                         get_string('gradecategoryonmodform', 'grades'),
                         grade_get_categories_menu($COURSE->id, $this->_outcomesused));
                 $mform->addHelpButton('gradecat', 'gradecategoryonmodform', 'grades');
+                if (!$this->_features->rating) {
+                    $mform->disabledIf('gradecat', 'grade[modgrade_type]', 'eq', 'none');
+                }
+            }
+
+            // Grade to pass.
+            $mform->addElement('text', 'gradepass', get_string('gradepass', 'grades'));
+            $mform->addHelpButton('gradepass', 'gradepass', 'grades');
+            $mform->setDefault('gradepass', '');
+            $mform->setType('gradepass', PARAM_FLOAT);
+            $mform->addRule('gradepass', null, 'numeric', null, 'client');
+            if (!$this->_features->rating) {
+                $mform->disabledIf('gradepass', 'grade[modgrade_type]', 'eq', 'none');
             }
         }
     }
index 050bdff..02db3a1 100644 (file)
@@ -29,6 +29,7 @@ require_once("$CFG->dirroot/group/lib.php");
 
 $courseid = required_param('courseid', PARAM_INT);
 $instanceid = optional_param('id', 0, PARAM_INT);
+$message = optional_param('message', null, PARAM_TEXT);
 
 $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
 $context = context_course::instance($course->id, MUST_EXIST);
@@ -91,6 +92,10 @@ if ($mform->is_cancelled()) {
         $DB->update_record('enrol', $instance);
     }  else {
         $enrol->add_instance($course, array('name'=>$data->name, 'status'=>$data->status, 'customint1'=>$data->customint1, 'roleid'=>$data->roleid, 'customint2'=>$data->customint2));
+        if (!empty($data->submitbuttonnext)) {
+            $returnurl = new moodle_url($PAGE->url);
+            $returnurl->param('message', 'added');
+        }
     }
     $trace = new null_progress_trace();
     enrol_cohort_sync($trace, $course->id);
@@ -102,5 +107,8 @@ $PAGE->set_heading($course->fullname);
 $PAGE->set_title(get_string('pluginname', 'enrol_cohort'));
 
 echo $OUTPUT->header();
+if ($message === 'added') {
+    echo $OUTPUT->notification(get_string('instanceadded', 'enrol'), 'notifysuccess');
+}
 $mform->display();
 echo $OUTPUT->footer();
index c7d0e2a..bb5ee8a 100644 (file)
@@ -31,7 +31,7 @@ class enrol_cohort_edit_form extends moodleform {
     function definition() {
         global $CFG, $DB;
 
-        $mform  = $this->_form;
+        $mform = $this->_form;
 
         list($instance, $plugin, $course) = $this->_customdata;
         $coursecontext = context_course::instance($course->id);
@@ -97,12 +97,25 @@ class enrol_cohort_edit_form extends moodleform {
         if ($instance->id) {
             $this->add_action_buttons(true);
         } else {
-            $this->add_action_buttons(true, get_string('addinstance', 'enrol'));
+            $this->add_add_buttons();
         }
 
         $this->set_data($instance);
     }
 
+    /**
+     * Adds buttons on create new method form
+     */
+    protected function add_add_buttons() {
+        $mform = $this->_form;
+        $buttonarray = array();
+        $buttonarray[0] = $mform->createElement('submit', 'submitbutton', get_string('addinstance', 'enrol'));
+        $buttonarray[1] = $mform->createElement('submit', 'submitbuttonnext', get_string('addinstanceanother', 'enrol'));
+        $buttonarray[2] = $mform->createElement('cancel');
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+    }
+
     function validation($data, $files) {
         global $DB;
 
index ac2f2ff..3ded040 100644 (file)
@@ -27,6 +27,7 @@ require_once("$CFG->dirroot/enrol/meta/addinstance_form.php");
 require_once("$CFG->dirroot/enrol/meta/locallib.php");
 
 $id = required_param('id', PARAM_INT); // course id
+$message = optional_param('message', null, PARAM_TEXT);
 
 $course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST);
 $context = context_course::instance($course->id, MUST_EXIST);
@@ -52,7 +53,12 @@ if ($mform->is_cancelled()) {
 } else if ($data = $mform->get_data()) {
     $eid = $enrol->add_instance($course, array('customint1'=>$data->link));
     enrol_meta_sync($course->id);
-    redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id)));
+    if (!empty($data->submitbuttonnext)) {
+        redirect(new moodle_url('/enrol/meta/addinstance.php',
+                array('id' => $course->id, 'message' => 'added')));
+    } else {
+        redirect(new moodle_url('/enrol/instances.php', array('id' => $course->id)));
+    }
 }
 
 $PAGE->set_heading($course->fullname);
@@ -60,6 +66,10 @@ $PAGE->set_title(get_string('pluginname', 'enrol_meta'));
 
 echo $OUTPUT->header();
 
+if ($message === 'added') {
+    echo $OUTPUT->notification(get_string('instanceadded', 'enrol'), 'notifysuccess');
+}
+
 $mform->display();
 
 echo $OUTPUT->footer();
index ecc3f54..fb4b748 100644 (file)
@@ -72,11 +72,24 @@ class enrol_meta_addinstance_form extends moodleform {
         $mform->addElement('hidden', 'id', null);
         $mform->setType('id', PARAM_INT);
 
-        $this->add_action_buttons(true, get_string('addinstance', 'enrol'));
+        $this->add_add_buttons();
 
         $this->set_data(array('id'=>$course->id));
     }
 
+    /**
+     * Adds buttons on create new method form
+     */
+    protected function add_add_buttons() {
+        $mform = $this->_form;
+        $buttonarray = array();
+        $buttonarray[0] = $mform->createElement('submit', 'submitbutton', get_string('addinstance', 'enrol'));
+        $buttonarray[1] = $mform->createElement('submit', 'submitbuttonnext', get_string('addinstanceanother', 'enrol'));
+        $buttonarray[2] = $mform->createElement('cancel');
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+    }
+
     function validation($data, $files) {
         global $DB, $CFG;
 
index 6dfe3cc..845e2e4 100644 (file)
@@ -60,7 +60,21 @@ class enrol_paypal_plugin extends enrol_plugin {
      * @return array of pix_icon
      */
     public function get_info_icons(array $instances) {
-        return array(new pix_icon('icon', get_string('pluginname', 'enrol_paypal'), 'enrol_paypal'));
+        $found = false;
+        foreach ($instances as $instance) {
+            if ($instance->enrolstartdate != 0 && $instance->enrolstartdate > time()) {
+                continue;
+            }
+            if ($instance->enrolenddate != 0 && $instance->enrolenddate < time()) {
+                continue;
+            }
+            $found = true;
+            break;
+        }
+        if ($found) {
+            return array(new pix_icon('icon', get_string('pluginname', 'enrol_paypal'), 'enrol_paypal'));
+        }
+        return array();
     }
 
     public function roles_protected() {
index e703d92..4eabac0 100644 (file)
@@ -32,26 +32,36 @@ defined('MOODLE_INTERNAL') || die();
 class filter_data extends moodle_text_filter {
 
     public function filter($text, array $options = array()) {
-        global $CFG, $DB;
+        global $CFG, $DB, $USER;
 
-        // Trivial-cache - keyed on $cachedcontextid
-        static $cachedcontextid;
-        static $contentlist;
+        // Trivial-cache - keyed on $cachedcourseid + $cacheduserid.
+        static $cachedcourseid = null;
+        static $cacheduserid = null;
+        static $coursecontentlist = array();
+        static $sitecontentlist = array();
 
         static $nothingtodo;
 
         // Try to get current course.
         $coursectx = $this->context->get_course_context(false);
         if (!$coursectx) {
+            // We could be in a course category so no entries for courseid == 0 will be found.
             $courseid = 0;
         } else {
             $courseid = $coursectx->instanceid;
         }
 
-        // Initialise/invalidate our trivial cache if dealing with a different context
-        if (!isset($cachedcontextid) || $cachedcontextid !== $this->context->id) {
-            $cachedcontextid = $this->context->id;
-            $contentlist = array();
+        if ($cacheduserid !== $USER->id) {
+            // Invalidate all caches if the user changed.
+            $coursecontentlist = array();
+            $sitecontentlist = array();
+            $cacheduserid = $USER->id;
+            $cachedcourseid = $courseid;
+            $nothingtodo = false;
+        } else if ($courseid != get_site()->id && $courseid != 0 && $cachedcourseid != $courseid) {
+            // Invalidate course-level caches if the course id changed.
+            $coursecontentlist = array();
+            $cachedcourseid = $courseid;
             $nothingtodo = false;
         }
 
@@ -59,6 +69,13 @@ class filter_data extends moodle_text_filter {
             return $text;
         }
 
+        // If courseid == 0 only site entries will be returned.
+        if ($courseid == get_site()->id || $courseid == 0) {
+            $contentlist = & $sitecontentlist;
+        } else {
+            $contentlist = & $coursecontentlist;
+        }
+
         // Create a list of all the resources to search for. It may be cached already.
         if (empty($contentlist)) {
             $coursestosearch = $courseid ? array($courseid) : array(); // Add courseid if found
diff --git a/filter/data/tests/filter_test.php b/filter/data/tests/filter_test.php
new file mode 100644 (file)
index 0000000..17c279c
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests.
+ *
+ * @package filter_data
+ * @category test
+ * @copyright 2015 David Monllao
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/filter/data/filter.php');
+
+/**
+ * Tests for filter_data.
+ *
+ * @package filter_data
+ * @copyright 2015 David Monllao
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class filter_data_filter_testcase extends advanced_testcase {
+
+    /**
+     * Tests that the filter applies the required changes.
+     *
+     * @return void
+     */
+    public function test_filter() {
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        filter_manager::reset_caches();
+
+        filter_set_global_state('data', TEXTFILTER_ON);
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $coursecontext1 = context_course::instance($course1->id);
+
+        $course2 = $this->getDataGenerator()->create_course();
+        $coursecontext2 = context_course::instance($course2->id);
+
+        $sitecontext = context_course::instance(SITEID);
+
+        $site = get_site();
+        $this->add_simple_database_instance($site, array('SiteEntry'));
+        $this->add_simple_database_instance($course1, array('CourseEntry'));
+
+        $html = '<p>I like CourseEntry and SiteEntry</p>';
+
+        // Testing at course level (both site and course).
+        $filtered = format_text($html, FORMAT_HTML, array('context' => $coursecontext1));
+        $this->assertRegExp('/title=(\'|")CourseEntry(\'|")/', $filtered);
+        $this->assertRegExp('/title=(\'|")SiteEntry(\'|")/', $filtered);
+
+        // Testing at site level (only site).
+        $filtered = format_text($html, FORMAT_HTML, array('context' => $sitecontext));
+        $this->assertNotRegExp('/title=(\'|")CourseEntry(\'|")/', $filtered);
+        $this->assertRegExp('/title=(\'|")SiteEntry(\'|")/', $filtered);
+
+        // Changing to another course to test the caches invalidation (only site).
+        $filtered = format_text($html, FORMAT_HTML, array('context' => $coursecontext2));
+        $this->assertNotRegExp('/title=(\'|")CourseEntry(\'|")/', $filtered);
+        $this->assertRegExp('/title=(\'|")SiteEntry(\'|")/', $filtered);
+    }
+
+    /**
+     * Adds a database instance to the provided course + a text field + adds all attached entries.
+     *
+     * @param stdClass $course
+     * @param array $entries A list of entry names.
+     * @return void
+     */
+    protected function add_simple_database_instance($course, $entries = false) {
+        global $DB;
+
+        $database = $this->getDataGenerator()->create_module('data',
+                array('course' => $course->id));
+
+        // A database field.
+        $field = data_get_field_new('text', $database);
+        $fielddetail = new stdClass();
+        $fielddetail->d = $database->id;
+        $fielddetail->mode = 'add';
+        $fielddetail->type = 'text';
+        $fielddetail->sesskey = sesskey();
+        $fielddetail->name = 'Name';
+        $fielddetail->description = 'Some name';
+        $fielddetail->param1 = '1';
+        $field->define_field($fielddetail);
+        $field->insert_field();
+        $recordid = data_add_record($database);
+
+        // Database entries.
+        foreach ($entries as $entrytext) {
+            $datacontent = array();
+            $datacontent['fieldid'] = $field->field->id;
+            $datacontent['recordid'] = $recordid;
+            $datacontent['content'] = $entrytext;
+            $contentid = $DB->insert_record('data_content', $datacontent);
+        }
+    }
+}
index 3565ef6..4a896ea 100644 (file)
@@ -563,8 +563,8 @@ class gradingform_guide_controller extends gradingform_controller {
             return $this->get_instance($instance);
         }
         if ($itemid && $raterid) {
-            if ($rs = $DB->get_records('grading_instances', array('raterid' => $raterid, 'itemid' => $itemid),
-                'timemodified DESC', '*', 0, 1)) {
+            $params = array('definitionid' => $this->definition->id, 'raterid' => $raterid, 'itemid' => $itemid);
+            if ($rs = $DB->get_records('grading_instances', $params, 'timemodified DESC', '*', 0, 1)) {
                 $record = reset($rs);
                 $currentinstance = $this->get_current_instance($raterid, $itemid);
                 if ($record->status == gradingform_guide_instance::INSTANCE_STATUS_INCOMPLETE &&
diff --git a/grade/grading/form/guide/tests/guide_test.php b/grade/grading/form/guide/tests/guide_test.php
new file mode 100644 (file)
index 0000000..5bf953e
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for Marking Guide grading method.
+ *
+ * @package    gradingform_guide
+ * @category   test
+ * @copyright  2015 Nikita Kalinin <nixorv@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/grade/grading/lib.php');
+require_once($CFG->dirroot . '/grade/grading/form/guide/lib.php');
+
+/**
+ * Test cases for the Marking Guide.
+ *
+ * @package    gradingform_guide
+ * @category   test
+ * @copyright  2015 Nikita Kalinin <nixorv@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradingform_guide_testcase extends advanced_testcase {
+    /**
+     * Unit test to get draft instance and create new instance.
+     */
+    public function test_get_or_create_instance() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Create fake areas.
+        $fakearea = (object)array(
+            'contextid'    => 1,
+            'component'    => 'mod_assign',
+            'areaname'     => 'submissions',
+            'activemethod' => 'guide'
+        );
+        $fakearea1id = $DB->insert_record('grading_areas', $fakearea);
+        $fakearea->contextid = 2;
+        $fakearea2id = $DB->insert_record('grading_areas', $fakearea);
+
+        // Create fake definitions.
+        $fakedefinition = (object)array(
+            'areaid'       => $fakearea1id,
+            'method'       => 'guide',
+            'name'         => 'fakedef',
+            'status'       => gradingform_controller::DEFINITION_STATUS_READY,
+            'timecreated'  => 0,
+            'usercreated'  => 1,
+            'timemodified' => 0,
+            'usermodified' => 1,
+        );
+        $fakedef1id = $DB->insert_record('grading_definitions', $fakedefinition);
+        $fakedefinition->areaid = $fakearea2id;
+        $fakedef2id = $DB->insert_record('grading_definitions', $fakedefinition);
+
+        // Create fake guide instance in first area.
+        $fakeinstance = (object)array(
+            'definitionid'   => $fakedef1id,
+            'raterid'        => 1,
+            'itemid'         => 1,
+            'rawgrade'       => null,
+            'status'         => 0,
+            'feedback'       => null,
+            'feedbackformat' => 0,
+            'timemodified'   => 0
+        );
+        $fakeinstanceid = $DB->insert_record('grading_instances', $fakeinstance);
+
+        $manager1 = get_grading_manager($fakearea1id);
+        $manager2 = get_grading_manager($fakearea2id);
+        $controller1 = $manager1->get_controller('guide');
+        $controller2 = $manager2->get_controller('guide');
+
+        $instance1 = $controller1->get_or_create_instance(0, 1, 1);
+        $instance2 = $controller2->get_or_create_instance(0, 1, 1);
+
+        // Definitions should not be the same.
+        $this->assertEquals(false, $instance1->get_data('definitionid') == $instance2->get_data('definitionid'));
+    }
+}
index 614ccca..a17da1c 100644 (file)
@@ -19,6 +19,9 @@
     min-width: 4.5em;
     vertical-align: top;
 }
+.dir-rtl.path-grade-report-user .user-grade td {
+    direction: ltr;
+}
 .path-grade-report-user .user-grade .b1l {
     padding: 0;
     width:24px;
diff --git a/grade/tests/behat/grade_to_pass.feature b/grade/tests/behat/grade_to_pass.feature
new file mode 100644 (file)
index 0000000..8ac6e30
--- /dev/null
@@ -0,0 +1,251 @@
+@core @core_grades
+Feature: We can set the grade to pass value
+  In order to set the grade to pass value
+  As a teacher
+  I assign a grade to pass to an activity while editing the activity.
+  I need to ensure that the grade to pass is visible in the gradebook.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname | format | numsections |
+      | Course 1 | C1 | weeks | 5 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And the following "scales" exist:
+      | name | scale |
+      | Test Scale 1 | Disappointing, Good, Very good, Excellent |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+
+  @javascript
+  Scenario: Validate that switching the type of grading used correctly disables grade to pass
+    When I turn editing mode on
+    And I add a "Assignment" to section "1"
+    And I expand all fieldsets
+    And I set the field "grade[modgrade_type]" to "Point"
+    Then the "Grade to pass" "field" should be enabled
+    And I set the field "grade[modgrade_type]" to "None"
+    And the "Grade to pass" "field" should be disabled
+    And I press "Save and return to course"
+
+  @javascript
+  Scenario: Create an activity with a Grade to pass value greater than the maximum grade
+    When I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test Assignment 1 |
+      | Description | Submit your online text |
+      | assignsubmission_onlinetext_enabled | 1 |
+      | grade[modgrade_type] | Point |
+      | grade[modgrade_point] | 50 |
+      | Grade to pass | 100 |
+    Then I should see "The grade to pass can not be greater than the maximum possible grade 50"
+    And I press "Cancel"
+
+  @javascript
+  Scenario: Set a valid grade to pass for an assignment activity using points
+    When I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test Assignment 1 |
+      | Description | Submit your online text |
+      | assignsubmission_onlinetext_enabled | 1 |
+      | grade[modgrade_type] | Point |
+      | grade[modgrade_point] | 50 |
+      | Grade to pass | 25 |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  assign Test Assignment 1" "link"
+    Then the field "Grade to pass" matches value "25"
+    And I follow "Course 1"
+    And I follow "Test Assignment 1"
+    And I follow "Edit settings"
+    And I expand all fieldsets
+    And I set the field "Grade to pass" to "30"
+    And I press "Save and return to course"
+    And I follow "Grades"
+    And I click on "Edit  assign Test Assignment 1" "link"
+    And the field "Grade to pass" matches value "30"
+
+  @javascript
+  Scenario: Set a valid grade to pass for an assignment activity using scales
+    When I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test Assignment 1 |
+      | Description | Submit your online text |
+      | grade[modgrade_type] | Scale |
+      | grade[modgrade_scale] | Test Scale 1 |
+      | Grade to pass | 3 |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  assign Test Assignment 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "3"
+    And I set the field "Grade to pass" to "4"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Assignment 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "4"
+
+  @javascript
+  Scenario: Set a invalid grade to pass for an assignment activity using scales
+    When I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test Assignment 1 |
+      | Description | Submit your online text |
+      | grade[modgrade_type] | Scale |
+      | grade[modgrade_scale] | Test Scale 1 |
+      | Grade to pass | 10 |
+    Then I should see "The grade to pass can not be greater than the maximum possible grade 4"
+
+  @javascript
+  Scenario: Set a valid grade to pass for workshop activity
+    When I turn editing mode on
+    And I add a "Workshop" to section "1" and I fill the form with:
+      | Workshop name | Test Workshop 1 |
+      | Description | Test workshop |
+      | grade | 80 |
+      | Submission grade to pass | 40 |
+      | gradinggrade | 20 |
+      | Assessment grade to pass | 10 |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  workshop Test Workshop 1 (submission)" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "40"
+    And I set the field "Grade to pass" to "45"
+    And I press "Save changes"
+    And I click on "Edit  workshop Test Workshop 1 (assessment)" "link"
+    And I follow "Show more..."
+    And the field "Grade to pass" matches value "10"
+    And I set the field "Grade to pass" to "15"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Workshop 1"
+    And I follow "Edit settings"
+    And the field "Submission grade to pass" matches value "45"
+    And the field "Assessment grade to pass" matches value "15"
+
+  @javascript
+  Scenario: Set an invalid grade to pass for workshop activity
+    When I turn editing mode on
+    And I add a "Workshop" to section "1" and I fill the form with:
+      | Workshop name | Test Workshop 1 |
+      | Description | Test workshop |
+      | grade | 80 |
+      | Submission grade to pass | 90 |
+      | gradinggrade | 20 |
+      | Assessment grade to pass | 30 |
+    Then "The grade to pass can not be greater than the maximum possible grade 80" "text" should exist in the "#fitem_id_submissiongradepass .error" "css_element"
+    Then "The grade to pass can not be greater than the maximum possible grade 20" "text" should exist in the "#fitem_id_gradinggradepass .error" "css_element"
+
+  @javascript
+  Scenario: Set a valid grade to pass for quiz activity
+    When I turn editing mode on
+    And I add a "Quiz" to section "1" and I fill the form with:
+      | Name | Test Quiz 1 |
+      | Grade to pass | 9.5 |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  quiz Test Quiz 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "9.5"
+    And I set the field "Grade to pass" to "8"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Quiz 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "8.00"
+
+  @javascript
+  Scenario: Set a valid grade to pass for lesson activity
+    When I turn editing mode on
+    And I add a "Lesson" to section "1" and I fill the form with:
+      | Name          | Test Lesson 1 |
+      | Description   | Test          |
+      | Grade to pass | 90            |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  lesson Test Lesson 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "90"
+    And I set the field "Grade to pass" to "80"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Lesson 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "80"
+
+  @javascript
+  Scenario: Set a valid grade to pass for database activity
+    When I turn editing mode on
+    And I add a "Database" to section "1" and I fill the form with:
+      | Name           | Test Database 1    |
+      | Description    | Test               |
+      | Grade to pass  | 90                 |
+      | Aggregate type | Average of ratings |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  data Test Database 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "90"
+    And I set the field "Grade to pass" to "80"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Database 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "80"
+
+  @javascript
+  Scenario: Set an invalid grade to pass for forum activity
+    When I turn editing mode on
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name     | Test Forum 1    |
+      | Description    | Test               |
+      | Grade to pass  | 90                 |
+      | Aggregate type | Average of ratings |
+      | scale[modgrade_point] | 60 |
+    Then I should see "The grade to pass can not be greater than the maximum possible grade 60"
+
+  @javascript
+  Scenario: Set a valid grade to pass for forum activity
+    When I turn editing mode on
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name     | Test Forum 1    |
+      | Description    | Test               |
+      | Grade to pass  | 90                 |
+      | Aggregate type | Average of ratings |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  forum Test Forum 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "90"
+    And I set the field "Grade to pass" to "80"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Forum 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "80"
+
+  @javascript
+  Scenario: Set a valid grade to pass for glossary activity
+    When I turn editing mode on
+    And I add a "Glossary" to section "1" and I fill the form with:
+      | Name           | Test Glossary 1    |
+      | Description    | Test               |
+      | Grade to pass  | 90                 |
+      | Aggregate type | Average of ratings |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  glossary Test Glossary 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "90"
+    And I set the field "Grade to pass" to "80"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Glossary 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "80"
index 2da9eee..b0498eb 100644 (file)
@@ -25,6 +25,7 @@
 
 $string['actenrolshhdr'] = 'Available course enrolment plugins';
 $string['addinstance'] = 'Add method';
+$string['addinstanceanother'] = 'Add method and create another';
 $string['ajaxoneuserfound'] = '1 user found';
 $string['ajaxxusersfound'] = '{$a} users found';
 $string['ajaxnext25'] = 'Next 25...';
@@ -83,6 +84,7 @@ $string['expirythreshold'] = 'Notification threshold';
 $string['expirythreshold_help'] = 'How long before enrolment expiry should users be notified?';
 $string['finishenrollingusers'] = 'Finish enrolling users';
 $string['foundxcohorts'] = 'Found {$a} cohorts';
+$string['instanceadded'] = 'Method added';
 $string['instanceeditselfwarning'] = 'Warning:';
 $string['instanceeditselfwarningtext'] = 'You are enrolled into this course through this enrolment method, changes may affect your access to this course.';
 $string['invalidenrolinstance'] = 'Invalid enrolment instance';
index e80d92e..a15d522 100644 (file)
@@ -296,6 +296,7 @@ $string['gradeoutcomes'] = 'Outcomes';
 $string['gradeoutcomescourses'] = 'Course outcomes';
 $string['gradepass'] = 'Grade to pass';
 $string['gradepass_help'] = 'This setting determines the minimum grade required to pass. The value is used in activity and course completion, and in the gradebook, where pass grades are highlighted in green and fail grades in red.';
+$string['gradepassgreaterthangrade'] = 'The grade to pass can not be greater than the maximum possible grade {$a}';
 $string['gradepointdefault'] = 'Grade point default';
 $string['gradepointdefault_help'] = 'This setting determines the default value for the grade point value available in an activity.';
 $string['gradepointdefault_validateerror'] = 'This setting must be an integer between 1 and the grade point maximum.';
index b059127..83e1056 100644 (file)
@@ -40,6 +40,7 @@ $string['apiexplorer'] = 'API explorer';
 $string['apiexplorernotavalaible'] = 'API explorer not available yet.';
 $string['arguments'] = 'Arguments';
 $string['authmethod'] = 'Authentication method';
+$string['callablefromajax'] = 'Callable from AJAX';
 $string['cannotcreatetoken'] = 'No permission to create web service token for the service {$a}.';
 $string['cannotgetcoursecontents'] = 'Cannot get course contents';
 $string['configwebserviceplugins'] = 'For security reasons, only protocols that are in use should be enabled.';
diff --git a/lib/ajax/service.php b/lib/ajax/service.php
new file mode 100644 (file)
index 0000000..7a72e88
--- /dev/null
@@ -0,0 +1,89 @@
+<?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 used to call any registered externallib function in Moodle.
+ *
+ * It will process more than one request and return more than one response if required.
+ * It is recommended to add webservice functions and re-use this script instead of
+ * writing any new custom ajax scripts.
+ *
+ * @since Moodle 2.9
+ * @package core
+ * @copyright 2015 Damyon Wiese
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('AJAX_SCRIPT', true);
+
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once($CFG->libdir . '/externallib.php');
+
+require_login(null, true, null, true, true);
+
+$rawjson = file_get_contents('php://input');
+
+$requests = json_decode($rawjson, true);
+if ($requests === null) {
+    $lasterror = json_last_error_msg();
+    throw new coding_exception('Invalid json in request: ' . $lasterror);
+}
+$responses = array();
+
+
+foreach ($requests as $request) {
+    $response = array();
+    $methodname = clean_param($request['methodname'], PARAM_ALPHANUMEXT);
+    $index = clean_param($request['index'], PARAM_INT);
+    $args = $request['args'];
+
+    try {
+        $externalfunctioninfo = external_function_info($methodname);
+
+        if (!$externalfunctioninfo->allowed_from_ajax) {
+            throw new moodle_exception('servicenotavailable', 'webservice');
+        }
+
+        // Validate params, this also sorts the params properly, we need the correct order in the next part.
+        $callable = array($externalfunctioninfo->classname, 'validate_parameters');
+        $params = call_user_func($callable,
+                                 $externalfunctioninfo->parameters_desc,
+                                 $args);
+
+        // Execute - gulp!
+        $callable = array($externalfunctioninfo->classname, $externalfunctioninfo->methodname);
+        $result = call_user_func_array($callable,
+                                       array_values($params));
+
+        $response['error'] = false;
+        $response['data'] = $result;
+        $responses[$index] = $response;
+    } catch (Exception $e) {
+        $jsonexception = get_exception_info($e);
+        unset($jsonexception->a);
+        if (!debugging('', DEBUG_DEVELOPER)) {
+            unset($jsonexception->debuginfo);
+            unset($jsonexception->backtrace);
+        }
+        $response['error'] = true;
+        $response['exception'] = $jsonexception;
+        $responses[$index] = $response;
+        // Do not process the remaining requests.
+        break;
+    }
+}
+
+echo json_encode($responses);
diff --git a/lib/amd/build/ajax.min.js b/lib/amd/build/ajax.min.js
new file mode 100644 (file)
index 0000000..72274b8
Binary files /dev/null and b/lib/amd/build/ajax.min.js differ
diff --git a/lib/amd/build/config.min.js b/lib/amd/build/config.min.js
new file mode 100644 (file)
index 0000000..03f444f
Binary files /dev/null and b/lib/amd/build/config.min.js differ
diff --git a/lib/amd/build/mustache.min.js b/lib/amd/build/mustache.min.js
new file mode 100644 (file)
index 0000000..e321d41
Binary files /dev/null and b/lib/amd/build/mustache.min.js differ
diff --git a/lib/amd/build/notification.min.js b/lib/amd/build/notification.min.js
new file mode 100644 (file)
index 0000000..37952ec
Binary files /dev/null and b/lib/amd/build/notification.min.js differ
diff --git a/lib/amd/build/str.min.js b/lib/amd/build/str.min.js
new file mode 100644 (file)
index 0000000..bfdb826
Binary files /dev/null and b/lib/amd/build/str.min.js differ
diff --git a/lib/amd/build/templates.min.js b/lib/amd/build/templates.min.js
new file mode 100644 (file)
index 0000000..c64573a
Binary files /dev/null and b/lib/amd/build/templates.min.js differ
diff --git a/lib/amd/build/url.min.js b/lib/amd/build/url.min.js
new file mode 100644 (file)
index 0000000..e287766
Binary files /dev/null and b/lib/amd/build/url.min.js differ
diff --git a/lib/amd/build/yui.min.js b/lib/amd/build/yui.min.js
new file mode 100644 (file)
index 0000000..c2bc6fa
Binary files /dev/null and b/lib/amd/build/yui.min.js differ
diff --git a/lib/amd/src/ajax.js b/lib/amd/src/ajax.js
new file mode 100644 (file)
index 0000000..4f2cb77
--- /dev/null
@@ -0,0 +1,161 @@
+// 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/>.
+
+/**
+ * Standard Ajax wrapper for Moodle. It calls the central Ajax script,
+ * which can call any existing webservice using the current session.
+ * In addition, it can batch multiple requests and return multiple responses.
+ *
+ * @module     core/ajax
+ * @class      ajax
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['jquery', 'core/config'], function($, config) {
+
+    /**
+     * Success handler. Called when the ajax call succeeds. Checks each response and
+     * resolves or rejects the deferred from that request.
+     *
+     * @method requestSuccess
+     * @private
+     * @param {Object[]} responses Array of responses containing error, exception and data attributes.
+     */
+    var requestSuccess = function(responses) {
+        // Call each of the success handlers.
+        var requests = this;
+        var exception = null;
+        var i = 0;
+        var request;
+        var response;
+
+        for (i = 0; i < requests.length; i++) {
+            request = requests[i];
+
+            response = responses[i];
+            // We may not have responses for all the requests.
+            if (typeof response !== "undefined") {
+                if (response.error === false) {
+                    // Call the done handler if it was provided.
+                    request.deferred.resolve(response.data);
+                } else {
+                    exception = response.exception;
+                    break;
+                }
+            } else {
+                // This is not an expected case.
+                exception = new Error('missing response');
+                break;
+            }
+        }
+        // Something failed, reject the remaining promises.
+        if (exception !== null) {
+            for (; i < requests.length; i++) {
+                request = requests[i];
+                request.deferred.reject(exception);
+            }
+        }
+    };
+
+    /**
+     * Fail handler. Called when the ajax call fails. Rejects all deferreds.
+     *
+     * @method requestFail
+     * @private
+     * @param {jqXHR} jqXHR The ajax object.
+     * @param {string} textStatus The status string.
+     */
+    var requestFail = function(jqXHR, textStatus) {
+        // Reject all the promises.
+        var requests = this;
+
+        var i = 0;
+        for (i = 0; i < requests.length; i++) {
+            var request = requests[i];
+
+            if (typeof request.fail != "undefined") {
+                request.deferred.reject(textStatus);
+            }
+        }
+    };
+
+    return /** @alias module:core/ajax */ {
+        // Public variables and functions.
+        /**
+         * Make a series of ajax requests and return all the responses.
+         *
+         * @method call
+         * @param {Object[]} Array of requests with each containing methodname and args properties.
+         *                   done and fail callbacks can be set for each element in the array, or the
+         *                   can be attached to the promises returned by this function.
+         * @param {Boolean} async Optional, defaults to true.
+         *                  If false - this function will not return until the promises are resolved.
+         * @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
+         */
+        call: function(requests, async) {
+            var ajaxRequestData = [],
+                i,
+                promises = [];
+
+            if (typeof async === "undefined") {
+                async = true;
+            }
+            for (i = 0; i < requests.length; i++) {
+                var request = requests[i];
+                ajaxRequestData.push({
+                    index: i,
+                    methodname: request.methodname,
+                    args: request.args
+                });
+                request.deferred = $.Deferred();
+                promises.push(request.deferred.promise());
+                // Allow setting done and fail handlers as arguments.
+                // This is just a shortcut for the calling code.
+                if (typeof request.done !== "undefined") {
+                    request.deferred.done(request.done);
+                }
+                if (typeof request.fail !== "undefined") {
+                    request.deferred.fail(request.fail);
+                }
+                request.index = i;
+            }
+
+            ajaxRequestData = JSON.stringify(ajaxRequestData);
+            var settings = {
+                type: 'POST',
+                data: ajaxRequestData,
+                context: requests,
+                dataType: 'json',
+                processData: false,
+                async: async
+            };
+
+            // Jquery deprecated done and fail with async=false so we need to do this 2 ways.
+            if (async) {
+                $.ajax(config.wwwroot + '/lib/ajax/service.php', settings)
+                    .done(requestSuccess)
+                    .fail(requestFail);
+            } else {
+                settings.success = requestSuccess;
+                settings.error = requestFail;
+                $.ajax(config.wwwroot + '/lib/ajax/service.php', settings);
+            }
+
+            return promises;
+        }
+    };
+});
diff --git a/lib/amd/src/config.js b/lib/amd/src/config.js
new file mode 100644 (file)
index 0000000..203326b
--- /dev/null
@@ -0,0 +1,30 @@
+// 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/>.
+
+/**
+ * Expose the M.cfg global variable.
+ *
+ * @module     core/config
+ * @class      config
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(function() {
+
+    // This module exposes only the raw data from M.cfg;
+    return /** @alias module:core/config */ M.cfg;
+});
index 22188f3..704c0c5 100644 (file)
@@ -22,5 +22,6 @@
  * @package    core
  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
  */
 define(function() { });
diff --git a/lib/amd/src/mustache.js b/lib/amd/src/mustache.js
new file mode 100644 (file)
index 0000000..6c96ca4
--- /dev/null
@@ -0,0 +1,608 @@
+// The MIT License
+//
+// Copyright (c) 2009 Chris Wanstrath (Ruby)
+// Copyright (c) 2010-2014 Jan Lehnardt (JavaScript)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+// Description of import into Moodle:
+// Download from https://github.com/janl/mustache.js/releases
+// Copy mustache.js into lib/amd/src/ in Moodle folder.
+// Add the license as a comment to the file and these instructions.
+
+/*!
+ * mustache.js - Logic-less {{mustache}} templates with JavaScript
+ * http://github.com/janl/mustache.js
+ */
+/* jshint ignore:start */
+
+(function (global, factory) {
+  if (typeof exports === "object" && exports) {
+    factory(exports); // CommonJS
+  } else if (typeof define === "function" && define.amd) {
+    define(['exports'], factory); // AMD
+  } else {
+    factory(global.Mustache = {}); // <script>
+  }
+}(this, function (mustache) {
+
+  var Object_toString = Object.prototype.toString;
+  var isArray = Array.isArray || function (object) {
+    return Object_toString.call(object) === '[object Array]';
+  };
+
+  function isFunction(object) {
+    return typeof object === 'function';
+  }
+
+  function escapeRegExp(string) {
+    return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
+  }
+
+  // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
+  // See https://github.com/janl/mustache.js/issues/189
+  var RegExp_test = RegExp.prototype.test;
+  function testRegExp(re, string) {
+    return RegExp_test.call(re, string);
+  }
+
+  var nonSpaceRe = /\S/;
+  function isWhitespace(string) {
+    return !testRegExp(nonSpaceRe, string);
+  }
+
+  var entityMap = {
+    "&": "&amp;",
+    "<": "&lt;",
+    ">": "&gt;",
+    '"': '&quot;',
+    "'": '&#39;',
+    "/": '&#x2F;'
+  };
+
+  function escapeHtml(string) {
+    return String(string).replace(/[&<>"'\/]/g, function (s) {
+      return entityMap[s];
+    });
+  }
+
+  var whiteRe = /\s*/;
+  var spaceRe = /\s+/;
+  var equalsRe = /\s*=/;
+  var curlyRe = /\s*\}/;
+  var tagRe = /#|\^|\/|>|\{|&|=|!/;
+
+  /**
+   * Breaks up the given `template` string into a tree of tokens. If the `tags`
+   * argument is given here it must be an array with two string values: the
+   * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
+   * course, the default is to use mustaches (i.e. mustache.tags).
+   *
+   * A token is an array with at least 4 elements. The first element is the
+   * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
+   * did not contain a symbol (i.e. {{myValue}}) this element is "name". For
+   * all text that appears outside a symbol this element is "text".
+   *
+   * The second element of a token is its "value". For mustache tags this is
+   * whatever else was inside the tag besides the opening symbol. For text tokens
+   * this is the text itself.
+   *
+   * The third and fourth elements of the token are the start and end indices,
+   * respectively, of the token in the original template.
+   *
+   * Tokens that are the root node of a subtree contain two more elements: 1) an
+   * array of tokens in the subtree and 2) the index in the original template at
+   * which the closing tag for that section begins.
+   */
+  function parseTemplate(template, tags) {
+    if (!template)
+      return [];
+
+    var sections = [];     // Stack to hold section tokens
+    var tokens = [];       // Buffer to hold the tokens
+    var spaces = [];       // Indices of whitespace tokens on the current line
+    var hasTag = false;    // Is there a {{tag}} on the current line?
+    var nonSpace = false;  // Is there a non-space char on the current line?
+
+    // Strips all whitespace tokens array for the current line
+    // if there was a {{#tag}} on it and otherwise only space.
+    function stripSpace() {
+      if (hasTag && !nonSpace) {
+        while (spaces.length)
+          delete tokens[spaces.pop()];
+      } else {
+        spaces = [];
+      }
+
+      hasTag = false;
+      nonSpace = false;
+    }
+
+    var openingTagRe, closingTagRe, closingCurlyRe;
+    function compileTags(tags) {
+      if (typeof tags === 'string')
+        tags = tags.split(spaceRe, 2);
+
+      if (!isArray(tags) || tags.length !== 2)
+        throw new Error('Invalid tags: ' + tags);
+
+      openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
+      closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
+      closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
+    }
+
+    compileTags(tags || mustache.tags);
+
+    var scanner = new Scanner(template);
+
+    var start, type, value, chr, token, openSection;
+    while (!scanner.eos()) {
+      start = scanner.pos;
+
+      // Match any text between tags.
+      value = scanner.scanUntil(openingTagRe);
+
+      if (value) {
+        for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
+          chr = value.charAt(i);
+
+          if (isWhitespace(chr)) {
+            spaces.push(tokens.length);
+          } else {
+            nonSpace = true;
+          }
+
+          tokens.push([ 'text', chr, start, start + 1 ]);
+          start += 1;
+
+          // Check for whitespace on the current line.
+          if (chr === '\n')
+            stripSpace();
+        }
+      }
+
+      // Match the opening tag.
+      if (!scanner.scan(openingTagRe))
+        break;
+
+      hasTag = true;
+
+      // Get the tag type.
+      type = scanner.scan(tagRe) || 'name';
+      scanner.scan(whiteRe);
+
+      // Get the tag value.
+      if (type === '=') {
+        value = scanner.scanUntil(equalsRe);
+        scanner.scan(equalsRe);
+        scanner.scanUntil(closingTagRe);
+      } else if (type === '{') {
+        value = scanner.scanUntil(closingCurlyRe);
+        scanner.scan(curlyRe);
+        scanner.scanUntil(closingTagRe);
+        type = '&';
+      } else {
+        value = scanner.scanUntil(closingTagRe);
+      }
+
+      // Match the closing tag.
+      if (!scanner.scan(closingTagRe))
+        throw new Error('Unclosed tag at ' + scanner.pos);
+
+      token = [ type, value, start, scanner.pos ];
+      tokens.push(token);
+
+      if (type === '#' || type === '^') {
+        sections.push(token);
+      } else if (type === '/') {
+        // Check section nesting.
+        openSection = sections.pop();
+
+        if (!openSection)
+          throw new Error('Unopened section "' + value + '" at ' + start);
+
+        if (openSection[1] !== value)
+          throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
+      } else if (type === 'name' || type === '{' || type === '&') {
+        nonSpace = true;
+      } else if (type === '=') {
+        // Set the tags for the next time around.
+        compileTags(value);
+      }
+    }
+
+    // Make sure there are no open sections when we're done.
+    openSection = sections.pop();
+
+    if (openSection)
+      throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
+
+    return nestTokens(squashTokens(tokens));
+  }
+
+  /**
+   * Combines the values of consecutive text tokens in the given `tokens` array
+   * to a single token.
+   */
+  function squashTokens(tokens) {
+    var squashedTokens = [];
+
+    var token, lastToken;
+    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+      token = tokens[i];
+
+      if (token) {
+        if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
+          lastToken[1] += token[1];
+          lastToken[3] = token[3];
+        } else {
+          squashedTokens.push(token);
+          lastToken = token;
+        }
+      }
+    }
+
+    return squashedTokens;
+  }
+
+  /**
+   * Forms the given array of `tokens` into a nested tree structure where
+   * tokens that represent a section have two additional items: 1) an array of
+   * all tokens that appear in that section and 2) the index in the original
+   * template that represents the end of that section.
+   */
+  function nestTokens(tokens) {
+    var nestedTokens = [];
+    var collector = nestedTokens;
+    var sections = [];
+
+    var token, section;
+    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+      token = tokens[i];
+
+      switch (token[0]) {
+      case '#':
+      case '^':
+        collector.push(token);
+        sections.push(token);
+        collector = token[4] = [];
+        break;
+      case '/':
+        section = sections.pop();
+        section[5] = token[2];
+        collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
+        break;
+      default:
+        collector.push(token);
+      }
+    }
+
+    return nestedTokens;
+  }
+
+  /**
+   * A simple string scanner that is used by the template parser to find
+   * tokens in template strings.
+   */
+  function Scanner(string) {
+    this.string = string;
+    this.tail = string;
+    this.pos = 0;
+  }
+
+  /**
+   * Returns `true` if the tail is empty (end of string).
+   */
+  Scanner.prototype.eos = function () {
+    return this.tail === "";
+  };
+
+  /**
+   * Tries to match the given regular expression at the current position.
+   * Returns the matched text if it can match, the empty string otherwise.
+   */
+  Scanner.prototype.scan = function (re) {
+    var match = this.tail.match(re);
+
+    if (!match || match.index !== 0)
+      return '';
+
+    var string = match[0];
+
+    this.tail = this.tail.substring(string.length);
+    this.pos += string.length;
+
+    return string;
+  };
+
+  /**
+   * Skips all text until the given regular expression can be matched. Returns
+   * the skipped string, which is the entire tail if no match can be made.
+   */
+  Scanner.prototype.scanUntil = function (re) {
+    var index = this.tail.search(re), match;
+
+    switch (index) {
+    case -1:
+      match = this.tail;
+      this.tail = "";
+      break;
+    case 0:
+      match = "";
+      break;
+    default:
+      match = this.tail.substring(0, index);
+      this.tail = this.tail.substring(index);
+    }
+
+    this.pos += match.length;
+
+    return match;
+  };
+
+  /**
+   * Represents a rendering context by wrapping a view object and
+   * maintaining a reference to the parent context.
+   */
+  function Context(view, parentContext) {
+    this.view = view == null ? {} : view;
+    this.cache = { '.': this.view };
+    this.parent = parentContext;
+  }
+
+  /**
+   * Creates a new context using the given view with this context
+   * as the parent.
+   */
+  Context.prototype.push = function (view) {
+    return new Context(view, this);
+  };
+
+  /**
+   * Returns the value of the given name in this context, traversing
+   * up the context hierarchy if the value is absent in this context's view.
+   */
+  Context.prototype.lookup = function (name) {
+    var cache = this.cache;
+
+    var value;
+    if (name in cache) {
+      value = cache[name];
+    } else {
+      var context = this, names, index;
+
+      while (context) {
+        if (name.indexOf('.') > 0) {
+          value = context.view;
+          names = name.split('.');
+          index = 0;
+
+          while (value != null && index < names.length)
+            value = value[names[index++]];
+        } else if (typeof context.view == 'object') {
+          value = context.view[name];
+        }
+
+        if (value != null)
+          break;
+
+        context = context.parent;
+      }
+
+      cache[name] = value;
+    }
+
+    if (isFunction(value))
+      value = value.call(this.view);
+
+    return value;
+  };
+
+  /**
+   * A Writer knows how to take a stream of tokens and render them to a
+   * string, given a context. It also maintains a cache of templates to
+   * avoid the need to parse the same template twice.
+   */
+  function Writer() {
+    this.cache = {};
+  }
+
+  /**
+   * Clears all cached templates in this writer.
+   */
+  Writer.prototype.clearCache = function () {
+    this.cache = {};
+  };
+
+  /**
+   * Parses and caches the given `template` and returns the array of tokens
+   * that is generated from the parse.
+   */
+  Writer.prototype.parse = function (template, tags) {
+    var cache = this.cache;
+    var tokens = cache[template];
+
+    if (tokens == null)
+      tokens = cache[template] = parseTemplate(template, tags);
+
+    return tokens;
+  };
+
+  /**
+   * High-level method that is used to render the given `template` with
+   * the given `view`.
+   *
+   * The optional `partials` argument may be an object that contains the
+   * names and templates of partials that are used in the template. It may
+   * also be a function that is used to load partial templates on the fly
+   * that takes a single argument: the name of the partial.
+   */
+  Writer.prototype.render = function (template, view, partials) {
+    var tokens = this.parse(template);
+    var context = (view instanceof Context) ? view : new Context(view);
+    return this.renderTokens(tokens, context, partials, template);
+  };
+
+  /**
+   * Low-level method that renders the given array of `tokens` using
+   * the given `context` and `partials`.
+   *
+   * Note: The `originalTemplate` is only ever used to extract the portion
+   * of the original template that was contained in a higher-order section.
+   * If the template doesn't use higher-order sections, this argument may
+   * be omitted.
+   */
+  Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
+    var buffer = '';
+
+    // This function is used to render an arbitrary template
+    // in the current context by higher-order sections.
+    var self = this;
+    function subRender(template) {
+      return self.render(template, context, partials);
+    }
+
+    var token, value;
+    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+      token = tokens[i];
+
+      switch (token[0]) {
+      case '#':
+        value = context.lookup(token[1]);
+
+        if (!value)
+          continue;
+
+        if (isArray(value)) {
+          for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
+            buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
+          }
+        } else if (typeof value === 'object' || typeof value === 'string') {
+          buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
+        } else if (isFunction(value)) {
+          if (typeof originalTemplate !== 'string')
+            throw new Error('Cannot use higher-order sections without the original template');
+
+          // Extract the portion of the original template that the section contains.
+          value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
+
+          if (value != null)
+            buffer += value;
+        } else {
+          buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+        }
+
+        break;
+      case '^':
+        value = context.lookup(token[1]);
+
+        // Use JavaScript's definition of falsy. Include empty arrays.
+        // See https://github.com/janl/mustache.js/issues/186
+        if (!value || (isArray(value) && value.length === 0))
+          buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+
+        break;
+      case '>':
+        if (!partials)
+          continue;
+
+        value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
+
+        if (value != null)
+          buffer += this.renderTokens(this.parse(value), context, partials, value);
+
+        break;
+      case '&':
+        value = context.lookup(token[1]);
+
+        if (value != null)
+          buffer += value;
+
+        break;
+      case 'name':
+        value = context.lookup(token[1]);
+
+        if (value != null)
+          buffer += mustache.escape(value);
+
+        break;
+      case 'text':
+        buffer += token[1];
+        break;
+      }
+    }
+
+    return buffer;
+  };
+
+  mustache.name = "mustache.js";
+  mustache.version = "1.0.0";
+  mustache.tags = [ "{{", "}}" ];
+
+  // All high-level mustache.* functions use this writer.
+  var defaultWriter = new Writer();
+
+  /**
+   * Clears all cached templates in the default writer.
+   */
+  mustache.clearCache = function () {
+    return defaultWriter.clearCache();
+  };
+
+  /**
+   * Parses and caches the given template in the default writer and returns the
+   * array of tokens it contains. Doing this ahead of time avoids the need to
+   * parse templates on the fly as they are rendered.
+   */
+  mustache.parse = function (template, tags) {
+    return defaultWriter.parse(template, tags);
+  };
+
+  /**
+   * Renders the `template` with the given `view` and `partials` using the
+   * default writer.
+   */
+  mustache.render = function (template, view, partials) {
+    return defaultWriter.render(template, view, partials);
+  };
+
+  // This is here for backwards compatibility with 0.4.x.
+  mustache.to_html = function (template, view, partials, send) {
+    var result = mustache.render(template, view, partials);
+
+    if (isFunction(send)) {
+      send(result);
+    } else {
+      return result;
+    }
+  };
+
+  // Export the escaping function so that the user may override it.
+  // See https://github.com/janl/mustache.js/issues/244
+  mustache.escape = escapeHtml;
+
+  // Export these mainly for testing, but also for advanced usage.
+  mustache.Scanner = Scanner;
+  mustache.Context = Context;
+  mustache.Writer = Writer;
+
+}));
+/* jshint ignore:end */
diff --git a/lib/amd/src/notification.js b/lib/amd/src/notification.js
new file mode 100644 (file)
index 0000000..1880b80
--- /dev/null
@@ -0,0 +1,105 @@
+// 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/>.
+
+/**
+ * Wrapper for the YUI M.core.notification class. Allows us to
+ * use the YUI version in AMD code until it is replaced.
+ *
+ * @module     core/notification
+ * @class      notification
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['core/yui'], function(Y) {
+
+    // Private variables and functions.
+
+    return /** @alias module:core/notification */ {
+        // Public variables and functions.
+        /**
+         * Wrap M.core.alert.
+         *
+         * @method alert
+         * @param {string} title
+         * @param {string} message
+         * @param {string} yesLabel
+         */
+        alert: function(title, message, yesLabel) {
+            // Here we are wrapping YUI. This allows us to start transitioning, but
+            // wait for a good alternative without having inconsistent dialogues.
+            Y.use('moodle-core-notification-alert', function () {
+                var alert = new M.core.alert({
+                    title : title,
+                    message : message,
+                    yesLabel: yesLabel
+                });
+
+                alert.show();
+            });
+        },
+
+        /**
+         * Wrap M.core.confirm.
+         *
+         * @method confirm
+         * @param {string} title
+         * @param {string} question
+         * @param {string} yesLabel
+         * @param {string} noLabel
+         * @param {function} callback
+         */
+        confirm: function(title, question, yesLabel, noLabel, callback) {
+            // Here we are wrapping YUI. This allows us to start transitioning, but
+            // wait for a good alternative without having inconsistent dialogues.
+            Y.use('moodle-core-notification-confirm', function () {
+                var modal = new M.core.confirm({
+                    title : title,
+                    question : question,
+                    yesLabel: yesLabel,
+                    noLabel: noLabel
+                });
+
+                modal.on('complete-yes', function() {
+                    callback();
+                });
+                modal.show();
+            });
+        },
+
+        /**
+         * Wrap M.core.exception.
+         *
+         * @method exception
+         * @param {Error} ex
+         */
+        exception: function(ex) {
+            // Fudge some parameters.
+            if (ex.backtrace) {
+                ex.lineNumber = ex.backtrace[0].line;
+                ex.fileName = ex.backtrace[0].file;
+                ex.fileName = '...' + ex.fileName.substr(ex.fileName.length - 20);
+                ex.stack = ex.debuginfo;
+                ex.name = ex.errorcode;
+            }
+            Y.use('moodle-core-notification-exception', function () {
+                var modal = new M.core.exception(ex);
+
+                modal.show();
+            });
+        }
+    };
+});
diff --git a/lib/amd/src/str.js b/lib/amd/src/str.js
new file mode 100644 (file)
index 0000000..d342de1
--- /dev/null
@@ -0,0 +1,141 @@
+// 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/>.
+
+/**
+ * Fetch and render language strings.
+ * Hooks into the old M.str global - but can also fetch missing strings on the fly.
+ *
+ * @module     core/str
+ * @class      str
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['jquery', 'core/ajax'], function($, ajax) {
+
+
+    return /** @alias module:core/str */ {
+        // Public variables and functions.
+        /**
+         * Return a promise object that will be resolved into a string eventually (maybe immediately).
+         *
+         * @method get_string
+         * @param {string} key The language string key
+         * @param {string} component The language string component
+         * @param {string} param The param for variable expansion in the string.
+         * @param {string} lang The users language - if not passed it is deduced.
+         * @return {Promise}
+         */
+        get_string: function(key, component, param, lang) {
+            var deferred = $.Deferred();
+
+            var request = this.get_strings([{
+                key: key,
+                component: component,
+                param: param,
+                lang: lang
+            }]);
+
+            request.done(function(results) {
+                deferred.resolve(results[0]);
+            }).fail(function(ex) {
+                deferred.reject(ex);
+            });
+
+            return deferred.promise();
+        },
+
+        /**
+         * Make a batch request to load a set of strings
+         *
+         * @method get_strings
+         * @param {Object[]} requests Array of { key: key, component: component, param: param, lang: lang };
+         *                                      See get_string for more info on these args.
+         * @return {Promise}
+         */
+        get_strings: function(requests) {
+
+            var deferred = $.Deferred();
+            var results = [];
+            var i = 0;
+            var missing = false;
+            var request;
+
+            for (i = 0; i < requests.length; i++) {
+                request = requests[i];
+                if (typeof M.str[request.component] === "undefined" ||
+                        typeof M.str[request.component][request.key] === "undefined") {
+                    missing = true;
+                }
+            }
+
+            if (!missing) {
+                // We have all the strings already.
+                for (i = 0; i < requests.length; i++) {
+                    request = requests[i];
+
+                    results[i] = M.util.get_string(request.key, request.component, request.param);
+                }
+                deferred.resolve(results);
+            } else {
+                // Something is missing, we might as well load them all.
+                var ajaxrequests = [];
+
+                for (i = 0; i < requests.length; i++) {
+                    request = requests[i];
+
+                    if (typeof request.lang === "undefined") {
+                        request.lang = $('html').attr('lang');
+                    }
+                    ajaxrequests.push({
+                        methodname: 'core_get_string',
+                        args: {
+                            stringid: request.key,
+                            component: request.component,
+                            lang: request.lang,
+                            stringparams: []
+                        }
+                    });
+                }
+
+                var deferreds = ajax.call(ajaxrequests);
+                $.when.apply(null, deferreds).done(
+                    function() {
+                        // Turn the list of arguments (unknown length) into a real array.
+                        var i = 0;
+                        for (i = 0; i < arguments.length; i++) {
+                            request = requests[i];
+                            // Cache all the string templates.
+                            if (typeof M.str[request.component] === "undefined") {
+                                M.str[request.component] = [];
+                            }
+                            M.str[request.component][request.key] = arguments[i];
+                            // And set the results.
+                            results[i] = M.util.get_string(request.key, request.component, request.param).trim();
+                        }
+                        deferred.resolve(results);
+                    }
+                ).fail(
+                    function(ex) {
+                        deferred.reject(ex);
+                    }
+                );
+            }
+
+            return deferred.promise();
+        }
+    };
+});
diff --git a/lib/amd/src/templates.js b/lib/amd/src/templates.js
new file mode 100644 (file)
index 0000000..4f581e5
--- /dev/null
@@ -0,0 +1,374 @@
+// 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/>.
+
+/**
+ * Template renderer for Moodle. Load and render Moodle templates with Mustache.
+ *
+ * @module     core/templates
+ * @package    core
+ * @class      templates
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define([ 'core/mustache',
+         'jquery',
+         'core/ajax',
+         'core/str',
+         'core/notification',
+         'core/url',
+         'core/config'
+       ],
+       function(mustache, $, ajax, str, notification, coreurl, config) {
+
+    // Private variables and functions.
+
+    /** @var {string[]} templateCache - Cache of already loaded templates */
+    var templateCache = {};
+
+    /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
+    var requiredStrings = [];
+
+    /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
+    var requiredJS = [];
+
+    /** @var {Number} uniqid Incrementing value that is changed for every call to render */
+    var uniqid = 1;
+
+    /** @var {String} themeName for the current render */
+    var currentThemeName = '';
+
+    /**
+     * Render image icons.
+     *
+     * @method pixHelper
+     * @private
+     * @param {string} sectionText The text to parse arguments from.
+     * @return {string}
+     */
+    var pixHelper = function(sectionText) {
+        var parts = sectionText.split(',');
+        var key = '';
+        var component = '';
+        var text = '';
+        var result;
+
+        if (parts.length > 0) {
+            key = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            component = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            text = parts.join(',').trim();
+        }
+        var url = coreurl.imageUrl(key, component);
+
+        var templatecontext = {
+            attributes: [
+                { name: 'src', value: url},
+                { name: 'alt', value: text},
+                { name: 'class', value: 'smallicon'}
+            ]
+        };
+        // We forced loading of this early, so it will be in the cache.
+        var template = templateCache[currentThemeName + '/core/pix_icon'];
+        result = mustache.render(template, templatecontext, partialHelper);
+        return result.trim();
+    };
+
+    /**
+     * Load a partial from the cache or ajax.
+     *
+     * @method partialHelper
+     * @private
+     * @param {string} name The partial name to load.
+     * @return {string}
+     */
+    var partialHelper = function(name) {
+        var template = '';
+
+        getTemplate(name, false).done(
+            function(source) {
+                template = source;
+            }
+        ).fail(notification.exception);
+
+        return template;
+    };
+
+    /**
+     * Render blocks of javascript and save them in an array.
+     *
+     * @method jsHelper
+     * @private
+     * @param {string} sectionText The text to save as a js block.
+     * @param {function} helper Used to render the block.
+     * @return {string}
+     */
+    var jsHelper = function(sectionText, helper) {
+        requiredJS.push(helper(sectionText, this));
+        return '';
+    };
+
+    /**
+     * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}
+     * into a get_string call.
+     *
+     * @method stringHelper
+     * @private
+     * @param {string} sectionText The text to parse the arguments from.
+     * @param {function} helper Used to render subsections of the text.
+     * @return {string}
+     */
+    var stringHelper = function(sectionText, helper) {
+        var parts = sectionText.split(',');
+        var key = '';
+        var component = '';
+        var param = '';
+        if (parts.length > 0) {
+            key = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            component = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            param = parts.join(',').trim();
+        }
+
+        if (param !== '') {
+            // Allow variable expansion in the param part only.
+            param = helper(param, this);
+        }
+        // Allow json formatted $a arguments.
+        if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
+            param = JSON.parse(param);
+        }
+
+        var index = requiredStrings.length;
+        requiredStrings.push({key: key, component: component, param: param});
+        return '{{_s' + index + '}}';
+    };
+
+    /**
+     * Add some common helper functions to all context objects passed to templates.
+     * These helpers match exactly the helpers available in php.
+     *
+     * @method addHelpers
+     * @private
+     * @param {Object} context Simple types used as the context for the template.
+     * @param {String} themeName We set this multiple times, because there are async calls.
+     */
+    var addHelpers = function(context, themeName) {
+        currentThemeName = themeName;
+        requiredStrings = [];
+        requiredJS = [];
+        context.uniqid = uniqid++;
+        context.str = function() { return stringHelper; };
+        context.pix = function() { return pixHelper; };
+        context.js = function() { return jsHelper; };
+        context.globals = { config : config };
+        context.currentTheme = themeName;
+    };
+
+    /**
+     * Get all the JS blocks from the last rendered template.
+     *
+     * @method getJS
+     * @private
+     * @param {string[]} strings Replacement strings.
+     * @return {string}
+     */
+    var getJS = function(strings) {
+        var js = '';
+        if (requiredJS.length > 0) {
+            js = requiredJS.join(";\n");
+        }
+
+        var i = 0;
+
+        for (i = 0; i < strings.length; i++) {
+            js = js.replace('{{_s' + i + '}}', strings[i]);
+        }
+        // Re-render to get the final strings.
+        return js;
+    };
+
+    /**
+     * Render a template and then call the callback with the result.
+     *
+     * @method doRender
+     * @private
+     * @param {string} templateSource The mustache template to render.
+     * @param {Object} context Simple types used as the context for the template.
+     * @param {String} themeName Name of the current theme.
+     * @return {Promise} object
+     */
+    var doRender = function(templateSource, context, themeName) {
+        var deferred = $.Deferred();
+
+        currentThemeName = themeName;
+
+        // Make sure we fetch this first.
+        var loadPixTemplate = getTemplate('core/pix_icon', true);
+
+        loadPixTemplate.done(
+            function() {
+                addHelpers(context, themeName);
+                var result = '';
+                try {
+                    result = mustache.render(templateSource, context, partialHelper);
+                } catch (ex) {
+                    deferred.reject(ex);
+                }
+
+                if (requiredStrings.length > 0) {
+                    str.get_strings(requiredStrings).done(
+                        function(strings) {
+                            var i;
+
+                            // Why do we not do another call the render here?
+                            //
+                            // Because that would expose DOS holes. E.g.
+                            // I create an assignment called "{{fish" which
+                            // would get inserted in the template in the first pass
+                            // and cause the template to die on the second pass (unbalanced).
+                            for (i = 0; i < strings.length; i++) {
+                                result = result.replace('{{_s' + i + '}}', strings[i]);
+                            }
+                            deferred.resolve(result.trim(), getJS(strings));
+                        }
+                    ).fail(
+                        function(ex) {
+                            deferred.reject(ex);
+                        }
+                    );
+                } else {
+                    deferred.resolve(result.trim(), getJS([]));
+                }
+            }
+        ).fail(
+            function(ex) {
+                deferred.reject(ex);
+            }
+        );
+        return deferred.promise();
+    };
+
+    /**
+     * Load a template from the cache or ajax request.
+     *
+     * @method getTemplate
+     * @private
+     * @param {string} templateName - should consist of the component and the name of the template like this:
+     *                              core/menu (lib/templates/menu.mustache) or
+     *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
+     * @return {Promise} JQuery promise object resolved when the template has been fetched.
+     */
+    var getTemplate = function(templateName, async) {
+        var deferred = $.Deferred();
+        var parts = templateName.split('/');
+        var component = parts.shift();
+        var name = parts.shift();
+
+        var searchKey = currentThemeName + '/' + templateName;
+
+        if (searchKey in templateCache) {
+            deferred.resolve(templateCache[searchKey]);
+        } else {
+            var promises = ajax.call([{
+                methodname: 'core_output_load_template',
+                args:{
+                    component: component,
+                    template: name,
+                    themename: currentThemeName
+                }
+            }], async);
+            promises[0].done(
+                function (templateSource) {
+                    templateCache[searchKey] = templateSource;
+                    deferred.resolve(templateSource);
+                }
+            ).fail(
+                function (ex) {
+                    deferred.reject(ex);
+                }
+            );
+        }
+        return deferred.promise();
+    };
+
+    return /** @alias module:core/templates */ {
+        // Public variables and functions.
+        /**
+         * Load a template and call doRender on it.
+         *
+         * @method render
+         * @private
+         * @param {string} templateName - should consist of the component and the name of the template like this:
+         *                              core/menu (lib/templates/menu.mustache) or
+         *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
+         * @param {Object} context - Could be array, string or simple value for the context of the template.
+         * @param {string} themeName - Name of the current theme.
+         * @return {Promise} JQuery promise object resolved when the template has been rendered.
+         */
+        render: function(templateName, context, themeName) {
+            var deferred = $.Deferred();
+
+            if (typeof (themeName) === "undefined") {
+                // System context by default.
+                themeName = config.theme;
+            }
+
+            currentThemeName = themeName;
+
+            var loadTemplate = getTemplate(templateName, true);
+
+            loadTemplate.done(
+                function(templateSource) {
+                    var renderPromise = doRender(templateSource, context, themeName);
+
+                    renderPromise.done(
+                        function(result, js) {
+                            deferred.resolve(result, js);
+                        }
+                    ).fail(
+                        function(ex) {
+                            deferred.reject(ex);
+                        }
+                    );
+                }
+            ).fail(
+                function(ex) {
+                    deferred.reject(ex);
+                }
+            );
+            return deferred.promise();
+        },
+
+        /**
+         * Execute a block of JS returned from a template.
+         * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
+         *
+         * @method runTemplateJS
+         * @private
+         * @param {string} source - A block of javascript.
+         */
+        runTemplateJS: function(source) {
+            var newscript = $('<script>').attr('type','text/javascript').html(source);
+            $('head').append(newscript);
+        }
+    };
+});
diff --git a/lib/amd/src/url.js b/lib/amd/src/url.js
new file mode 100644 (file)
index 0000000..908c898
--- /dev/null
@@ -0,0 +1,91 @@
+// 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/>.
+
+/**
+ * URL utility functions.
+ *
+ * @module     core/url
+ * @package    core
+ * @class      url
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['core/config'], function(config) {
+
+
+    return /** @alias module:core/url */ {
+        // Public variables and functions.
+        /**
+         * Generate a style tag referencing this sheet and add it to the head of the page.
+         *
+         * @method fileUrl
+         * @param {string} sheet The style sheet name. Must exist in the theme, or one of it's parents.
+         * @return {string}
+         */
+        fileUrl: function(relativeScript, slashArg) {
+
+            var url = config.wwwroot + relativeScript;
+
+            // Force a /
+            if (slashArg.charAt(0) != '/') {
+                slashArg = '/' + slashArg;
+            }
+            if (config.slasharguments) {
+                url += slashArg;
+            } else {
+                url += '?file=' + encodeURIComponent(slashArg);
+            }
+            return url;
+        },
+
+        /**
+         * Take a path relative to the moodle basedir and do some fixing (see class moodle_url in php).
+         *
+         * @method relativeUrl
+         * @param {string} relativePath The path relative to the moodle basedir.
+         * @return {string}
+         */
+        relativeUrl: function(relativePath) {
+
+            if (relativePath.indexOf('http:') === 0 || relativePath.indexOf('https:') === 0 || relativePath.indexOf('://')) {
+                throw new Error('relativeUrl function does not accept absolute urls');
+            }
+
+            // Fix non-relative paths;
+            if (relativePath.charAt(0) != '/') {
+                relativePath = '/' + relativePath;
+            }
+
+            // Fix admin urls.
+            if (config.admin !== 'admin') {
+                relativePath = relativePath.replace(/^\/admin\//, '/' + config.admin + '/');
+            }
+            return config.wwwroot + relativePath;
+        },
+
+        /**
+         * Wrapper for image_url function.
+         *
+         * @method imageUrl
+         * @param {string} imagename The image name (e.g. t/edit).
+         * @param {string} component The component (e.g. mod_feedback).
+         * @return {string}
+         */
+        imageUrl: function(imagename, component) {
+            return M.util.image_url(imagename, component);
+        }
+    };
+});
diff --git a/lib/amd/src/yui.js b/lib/amd/src/yui.js
new file mode 100644 (file)
index 0000000..af58326
--- /dev/null
@@ -0,0 +1,30 @@
+// 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/>.
+
+/**
+ * Expose the global YUI variable. Note: This is only for scripts that are writing AMD
+ * wrappers for YUI functionality. This is not for plugins.
+ *
+ * @module     core/yui
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(function() {
+
+    // This module exposes only the global yui instance.
+    return /** @alias module:core/yui */ Y;
+});
index 5fd807e..add2130 100644 (file)
@@ -93,51 +93,51 @@ class behat_selectors {
      */
     protected static $moodleselectors = array(
         'activity' => <<<XPATH
-//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][normalize-space(.) = %locator% ]
+.//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][normalize-space(.) = %locator% ]
 XPATH
         , 'block' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' block ') and
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' block ') and
     (contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' ')) or
      descendant::h2[normalize-space(.) = %locator%] or
      @aria-label = %locator%)]
 XPATH
         , 'dialogue' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ') and
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ') and
     normalize-space(descendant::div[
         contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue-hd ')
         ]) = %locator%] |
-//div[contains(concat(' ', normalize-space(@class), ' '), ' yui-dialog ') and
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' yui-dialog ') and
     normalize-space(descendant::div[@class='hd']) = %locator%]
 XPATH
         , 'filemanager' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ')]
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ')]
     /descendant::input[@id = //label[contains(normalize-space(string(.)), %locator%)]/@for]
 XPATH
         , 'list_item' => <<<XPATH
-.//li[contains(normalize-space(.), %locator%)]
+.//li[contains(normalize-space(.), %locator%) and not(.//li[contains(normalize-space(.), %locator%)])]
 XPATH
         , 'question' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' que ')]
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' que ')]
     [contains(div[@class='content']/div[@class='formulation'], %locator%)]
 XPATH
         , 'region' => <<<XPATH
-//*[self::div | self::section | self::aside | self::header | self::footer][./@id = %locator%]
+.//*[self::div | self::section | self::aside | self::header | self::footer][./@id = %locator%]
 XPATH
         , 'section' => <<<XPATH
-//li[contains(concat(' ', normalize-space(@class), ' '), ' section ')][./descendant::*[self::h3]
+.//li[contains(concat(' ', normalize-space(@class), ' '), ' section ')][./descendant::*[self::h3]
     [normalize-space(.) = %locator%][contains(concat(' ', normalize-space(@class), ' '), ' sectionname ') or
     contains(concat(' ', normalize-space(@class), ' '), ' section-title ')]] |
-//div[contains(concat(' ', normalize-space(@class), ' '), ' sitetopic ')]
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' sitetopic ')]
     [./descendant::*[self::h2][normalize-space(.) = %locator%] or %locator% = 'frontpage']
 XPATH
         , 'table' => <<<XPATH
 .//table[(./@id = %locator% or contains(.//caption, %locator%) or contains(concat(' ', normalize-space(@class), ' '), %locator% ))]
 XPATH
         , 'table_row' => <<<XPATH
-.//tr[contains(normalize-space(.), %locator%)]
+.//tr[contains(normalize-space(.), %locator%) and not(.//tr[contains(normalize-space(.), %locator%)])]
 XPATH
         , 'text' => <<<XPATH
-//*[contains(., %locator%)][count(./descendant::*[contains(., %locator%)]) = 0]
+.//*[contains(., %locator%) and not(.//*[contains(., %locator%)])]
 XPATH
     );
 
index 5d1fb24..a44c185 100644 (file)
@@ -1599,8 +1599,10 @@ class block_manager {
         if ($bestgap < $newweight) {
             $newweight = floor($newweight);
             for ($weight = $bestgap + 1; $weight <= $newweight; $weight++) {
-                foreach ($usedweights[$weight] as $biid) {
-                    $this->reposition_block($biid, $newregion, $weight - 1);
+                if (array_key_exists($weight, $usedweights)) {
+                    foreach ($usedweights[$weight] as $biid) {
+                        $this->reposition_block($biid, $newregion, $weight - 1);
+                    }
                 }
             }
             $this->reposition_block($block->instance->id, $newregion, $newweight);
diff --git a/lib/classes/output/external.php b/lib/classes/output/external.php
new file mode 100644 (file)
index 0000000..8e53119
--- /dev/null
@@ -0,0 +1,133 @@
+<?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/>.
+
+/**
+ * Mustache helper to load strings from string_manager.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use external_api;
+use external_function_parameters;
+use external_value;
+use core_component;
+use moodle_exception;
+use context_system;
+use theme_config;
+
+/**
+ * This class contains a list of webservice functions related to output.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class external extends external_api {
+    /**
+     * Returns description of load_template() parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function load_template_parameters() {
+        return new external_function_parameters(
+                array('component' => new external_value(PARAM_COMPONENT, 'component containing the template'),
+                      'template' => new external_value(PARAM_ALPHANUMEXT, 'name of the template'),
+                      'themename' => new external_value(PARAM_ALPHANUMEXT, 'The current theme.'),
+                         )
+            );
+    }
+
+    /**
+     * Can this function be called directly from ajax?
+     *
+     * @return boolean
+     * @since Moodle 2.9
+     */
+    public static function load_template_is_allowed_from_ajax() {
+        return true;
+    }
+
+    /**
+     * Return a mustache template, and all the strings it requires.
+     *
+     * @param string $component The component that holds the template.
+     * @param string $templatename The name of the template.
+     * @param string $themename The name of the current theme.
+     * @return string the template
+     */
+    public static function load_template($component, $template, $themename) {
+        global $DB, $CFG, $PAGE;
+
+        $params = self::validate_parameters(self::load_template_parameters(),
+                                            array('component' => $component,
+                                                  'template' => $template,
+                                                  'themename' => $themename));
+
+        $component = $params['component'];
+        $template = $params['template'];
+        $themename = $params['themename'];
+
+        // Check if this is a valid component.
+        $componentdir = core_component::get_component_directory($component);
+        if (empty($componentdir)) {
+            throw new moodle_exception('filenotfound', 'error');
+        }
+        // Places to look.
+        $candidates = array();
+        // Theme dir.
+        $root = $CFG->dirroot;
+
+        $themeconfig = theme_config::load($themename);
+
+        $candidate = "${root}/theme/${themename}/templates/${component}/${template}.mustache";
+        $candidates[] = $candidate;
+        // Theme parents dir.
+        foreach ($themeconfig->parents as $theme) {
+            $candidate = "${root}/theme/${theme}/templates/${component}/${template}.mustache";
+            $candidates[] = $candidate;
+        }
+        // Component dir.
+        $candidate = "${componentdir}/templates/${template}.mustache";
+        $candidates[] = $candidate;
+        $templatestr = false;
+        foreach ($candidates as $candidate) {
+            if (file_exists($candidate)) {
+                $templatestr = file_get_contents($candidate);
+                break;
+            }
+        }
+        if ($templatestr === false) {
+            throw new moodle_exception('filenotfound', 'error');
+        }
+
+        return $templatestr;
+    }
+
+    /**
+     * Returns description of load_template() result value.
+     *
+     * @return external_description
+     */
+    public static function load_template_returns() {
+        return new external_value(PARAM_RAW, 'template');
+    }
+}
+
diff --git a/lib/classes/output/mustache_filesystem_loader.php b/lib/classes/output/mustache_filesystem_loader.php
new file mode 100644 (file)
index 0000000..efaab81
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Perform some custom name mapping for template file names (strip leading component/).
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use coding_exception;
+
+/**
+ * Perform some custom name mapping for template file names (strip leading component/).
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_filesystem_loader extends \Mustache_Loader_FilesystemLoader {
+
+    /**
+     * Helper function for getting a Mustache template file name.
+     * Strips the leading component as we are already limited to the correct directories.
+     *
+     * @param string $name
+     *
+     * @return string Template file name
+     */
+    protected function getFileName($name) {
+        if (strpos($name, '/') === false) {
+            throw new coding_exception('Templates names must be specified as "componentname/templatename" (' . $name . ' requested) ');
+        }
+        list($component, $templatename) = explode('/', $name, 2);
+        return parent::getFileName($templatename);
+    }
+}
diff --git a/lib/classes/output/mustache_javascript_helper.php b/lib/classes/output/mustache_javascript_helper.php
new file mode 100644 (file)
index 0000000..7ba9b2a
--- /dev/null
@@ -0,0 +1,59 @@
+<?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/>.
+
+/**
+ * Mustache helper that will add JS to the end of the page.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+/**
+ * Store a list of JS calls to insert at the end of the page.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_javascript_helper {
+
+    /** @var page_requirements_manager $requires - Page requirements manager for collecting JS calls. */
+    private $requires = null;
+
+    /**
+     * Create new instance of mustache javascript helper.
+     *
+     * @param page_requirements_manager $requires Page requirements manager.
+     */
+    public function __construct($requires) {
+        $this->requires = $requires;
+    }
+
+    /**
+     * Add the block of text to the page requires so it is appended in the footer. The
+     * content of the block can contain further mustache tags which will be resolved.
+     *
+     * @param string $text The script content of the section.
+     * @param \Mustache_LambdaHelper $helper Used to render the content of this block.
+     */
+    public function help($text, \Mustache_LambdaHelper $helper) {
+        $this->requires->js_amd_inline($helper->render($text));
+    }
+}
diff --git a/lib/classes/output/mustache_pix_helper.php b/lib/classes/output/mustache_pix_helper.php
new file mode 100644 (file)
index 0000000..3b0d261
--- /dev/null
@@ -0,0 +1,78 @@
+<?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/>.
+
+/**
+ * Mustache helper render pix icons.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use Mustache_LambdaHelper;
+use renderer_base;
+
+/**
+ * This class will call pix_icon with the section content.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_pix_helper {
+
+    /** @var renderer_base $renderer A reference to the renderer in use */
+    private $renderer;
+
+    /**
+     * Save a reference to the renderer.
+     * @param renderer_base $renderer
+     */
+    public function __construct(renderer_base $renderer) {
+        $this->renderer = $renderer;
+    }
+
+    /**
+     * Read a pix icon name from a template and get it from pix_icon.
+     *
+     * {{#pix}}t/edit,component,Anything else is alt text{{/pix}}
+     *
+     * The args are comma separated and only the first is required.
+     *
+     * @param string $text The text to parse for arguments.
+     * @param Mustache_LambdaHelper $helper Used to render nested mustache variables.
+     * @return string
+     */
+    public function pix($text, Mustache_LambdaHelper $helper) {
+        // Split the text into an array of variables.
+        $key = strtok($text, ",");
+        $key = trim($key);
+        $component = strtok(",");
+        $component = trim($component);
+        if (!$component) {
+            $component = '';
+        }
+        $text = strtok("");
+        // Allow mustache tags in the last argument.
+        $text = $helper->render($text);
+
+        return trim($this->renderer->pix_icon($key, $text, $component));
+    }
+}
+
diff --git a/lib/classes/output/mustache_string_helper.php b/lib/classes/output/mustache_string_helper.php
new file mode 100644 (file)
index 0000000..1b5d52a
--- /dev/null
@@ -0,0 +1,79 @@
+<?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/>.
+
+/**
+ * Mustache helper to load strings from string_manager.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use Mustache_LambdaHelper;
+use stdClass;
+
+/**
+ * This class will load language strings in a template.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_string_helper {
+
+    /**
+     * Read a lang string from a template and get it from get_string.
+     *
+     * Some examples for calling this from a template are:
+     *
+     * {{#str}}activity{{/str}}
+     * {{#str}}actionchoice, core, {{#str}}delete{{/str}}{{/str}} (Nested)
+     * {{#str}}addinganewto, core, {"what":"This", "to":"That"}{{/str}} (Complex $a)
+     *
+     * The args are comma separated and only the first is required.
+     * The last is a $a argument for get string. For complex data here, use JSON.
+     *
+     * @param string $text The text to parse for arguments.
+     * @param Mustache_LambdaHelper $helper Used to render nested mustache variables.
+     * @return string
+     */
+    public function str($text, Mustache_LambdaHelper $helper) {
+        // Split the text into an array of variables.
+        $key = strtok($text, ",");
+        $key = trim($key);
+        $component = strtok(",");
+        $component = trim($component);
+        if (!$component) {
+            $component = '';
+        }
+
+        $a = new stdClass();
+
+        $next = strtok('');
+        $next = trim($next);
+        if ((strpos($next, '{') === 0) && (strpos($next, '{{') !== 0)) {
+            $rawjson = $helper->render($next);
+            $a = json_decode($rawjson);
+        } else {
+            $a = $helper->render($next);
+        }
+        return get_string($key, $component, $a);
+    }
+}
+
diff --git a/lib/classes/output/mustache_uniqid_helper.php b/lib/classes/output/mustache_uniqid_helper.php
new file mode 100644 (file)
index 0000000..0dbb773
--- /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/>.
+
+/**
+ * Mustache helper that will add JS to the end of the page.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+/**
+ * Lazy create a uniqid per instance of the class. The id is only generated
+ * when this class it converted to a string.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_uniqid_helper {
+
+    /** @var string $uniqid The unique id */
+    private $uniqid = null;
+
+    /**
+     * Init the random variable and return it as a string.
+     *
+     * @return string random id.
+     */
+    public function __toString() {
+        if ($this->uniqid === null) {
+            $this->uniqid = \html_writer::random_id(uniqid());
+        }
+        return $this->uniqid;
+    }
+}
index a027c55..d9609b4 100644 (file)
@@ -1562,6 +1562,9 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         require_once($CFG->libdir.'/questionlib.php');
         require_once($CFG->dirroot.'/cohort/lib.php');
 
+        // Make sure we won't timeout when deleting a lot of courses.
+        $settimeout = core_php_time_limit::raise();
+
         $deletedcourses = array();
 
         // Get children. Note, we don't want to use cache here because it would be rebuilt too often.
index a6391b9..a9ea1a8 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20150224" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20150306" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
       <INDEXES>
-        <INDEX NAME="useridfrom" UNIQUE="false" FIELDS="useridfrom"/>
         <INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
         <INDEX NAME="useridfromto" UNIQUE="false" FIELDS="useridfrom, useridto"/>
       </INDEXES>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
       <INDEXES>
-        <INDEX NAME="useridfrom" UNIQUE="false" FIELDS="useridfrom"/>
         <INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
         <INDEX NAME="useridfromto" UNIQUE="false" FIELDS="useridfrom, useridto"/>
       </INDEXES>
index 0631dd8..47eb28c 100644 (file)
@@ -950,6 +950,21 @@ $functions = array(
         'type'        => 'write',
         'capabilities'=> 'moodle/calendar:manageentries', 'moodle/calendar:manageownentries', 'moodle/calendar:managegroupentries'
     ),
+
+    'core_output_load_template' => array(
+        'classname'   => 'core\output\external',
+        'methodname'  => 'load_template',
+        'description' => 'Load a template for a renderable',
+        'type'        => 'read'
+    ),
+
+    // Completion related functions.
+    'core_completion_update_activity_completion_status_manually' => array(
+        'classname'   => 'core_completion_external',
+        'methodname'  => 'update_activity_completion_status_manually',
+        'description' => 'Update completion status for the current user in an activity, only for activities with manual tracking.',
+        'type'        => 'write',
+    ),
 );
 
 $services = array(
@@ -1006,7 +1021,9 @@ $services = array(
             'gradereport_user_get_grades_table',
             'core_group_get_course_user_groups',
             'core_user_remove_user_device',
-            'core_course_get_courses'
+            'core_course_get_courses',
+            'core_completion_update_activity_completion_status_manually',
+            'mod_data_get_databases_by_courses'
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index a0aed78..a198c09 100644 (file)
@@ -4223,5 +4223,29 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015031100.00);
     }
 
+    if ($oldversion < 2015031400.00) {
+
+        // Define index useridfrom (not unique) to be dropped form message.
+        $table = new xmldb_table('message');
+        $index = new xmldb_index('useridfrom', XMLDB_INDEX_NOTUNIQUE, array('useridfrom'));
+
+        // Conditionally launch drop index useridfrom.
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Define index useridfrom (not unique) to be dropped form message_read.
+        $table = new xmldb_table('message_read');
+        $index = new xmldb_index('useridfrom', XMLDB_INDEX_NOTUNIQUE, array('useridfrom'));
+
+        // Conditionally launch drop index useridfrom.
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015031400.00);
+    }
+
     return true;
 }
index 5116da8..7751186 100644 (file)
@@ -4100,3 +4100,29 @@ function enrol_cohort_search_cohorts(course_enrolment_manager $manager, $offset
     }
     return array('more' => !(bool)$limit, 'offset' => $offset, 'cohorts' => $cohorts);
 }
+
+/**
+ * Is $USER one of the supplied users?
+ *
+ * $user2 will be null if viewing a user's recent conversations
+ *
+ * @deprecated since Moodle 2.9 MDL-49371 - please do not use this function any more.
+ * @todo MDL-49290 This will be deleted in Moodle 3.1.
+ * @param stdClass the first user
+ * @param stdClass the second user or null
+ * @return bool True if the current user is one of either $user1 or $user2
+ */
+function message_current_user_is_involved($user1, $user2) {
+    global $USER;
+
+    debugging('message_current_user_is_involved() is deprecated, please do not use this function.', DEBUG_DEVELOPER);
+
+    if (empty($user1->id) || (!empty($user2) && empty($user2->id))) {
+        throw new coding_exception('Invalid user object detected. Missing id.');
+    }
+
+    if ($user1->id != $USER->id && (empty($user2) || $user2->id != $USER->id)) {
+        return false;
+    }
+    return true;
+}
index 52d04bf..4ee2923 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 6a50ded..516fb57 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index f01c730..cefa893 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 89ccac7..2f6e5ca 100644 (file)
@@ -84,52 +84,236 @@ EditorClean.prototype = {
      * @return {String} The cleaned HTML
      */
     _cleanHTML: function(content) {
-        // What are we doing ?
-        // We are cleaning random HTML from all over the shop into a set of useful html suitable for content.
-        // We are allowing styles etc, but not e.g. font tags, class="MsoNormal" etc.
+        // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.
 
         var rules = [
-            // Source: "http://stackoverflow.com/questions/2875027/clean-microsoft-word-pasted-text-using-javascript"
-            // Source: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work"
-
-            // Remove all HTML comments.
-            {regex: /<!--[\s\S]*?-->/gi, replace: ""},
-            // Source: "http://www.1stclassmedia.co.uk/developers/clean-ms-word-formatting.php"
-            // Remove <?xml>, <\?xml>.
-            {regex: /<\\?\?xml[^>]*>/gi, replace: ""},
-            // Remove <o:blah>, <\o:blah>.
-            {regex: /<\/?\w+:[^>]*>/gi, replace: ""}, // e.g. <o:p...
-            // Remove MSO-blah, MSO:blah (e.g. in style attributes)
-            {regex: /\s*MSO[-:][^;"']*;?/gi, replace: ""},
-            // Remove empty spans
-            {regex: /<span[^>]*>(&nbsp;|\s)*<\/span>/gi, replace: ""},
-            // Remove class="Msoblah"
-            {regex: /class="Mso[^"]*"/gi, replace: ""},
+            // Remove any style blocks. Some browsers do not work well with them in a contenteditable.
+            // Plus style blocks are not allowed in body html, except with "scoped", which most browsers don't support as of 2015.
+            // Reference: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work"
+            {regex: /<style[^>]*>[\s\S]*?<\/style>/gi, replace: ""},
+
+            // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.
+            {regex: /<!--(?![\s\S]*?-->)/gi, replace: ""},
 
             // Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
-            // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body.
-            {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|!\[)[^>]*?>/gi, replace: ""},
-
-            // Source: "http://www.tim-jarrett.com/labs_javascript_scrub_word.php"
-            // Replace extended chars with simple text.
-            {regex: new RegExp(String.fromCharCode(8220), 'gi'), replace: '"'},
-            {regex: new RegExp(String.fromCharCode(8216), 'gi'), replace: "'"},
-            {regex: new RegExp(String.fromCharCode(8217), 'gi'), replace: "'"},
-            {regex: new RegExp(String.fromCharCode(8211), 'gi'), replace: '-'},
-            {regex: new RegExp(String.fromCharCode(8212), 'gi'), replace: '--'},
-            {regex: new RegExp(String.fromCharCode(189), 'gi'), replace: '1/2'},
-            {regex: new RegExp(String.fromCharCode(188), 'gi'), replace: '1/4'},
-            {regex: new RegExp(String.fromCharCode(190), 'gi'), replace: '3/4'},
-            {regex: new RegExp(String.fromCharCode(169), 'gi'), replace: '(c)'},
-            {regex: new RegExp(String.fromCharCode(174), 'gi'), replace: '(r)'},
-            {regex: new RegExp(String.fromCharCode(8230), 'gi'), replace: '...'}
+            // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.
+            {regex: /<\/?(?:title|meta|style|st\d|head|font|html|body|link|!\[)[^>]*?>/gi, replace: ""}
         ];
 
+        return this._filterContentWithRules(content, rules);
+    },
+
+    /**
+     * Take the supplied content and run on the supplied regex rules.
+     *
+     * @method _filterContentWithRules
+     * @private
+     * @param {String} content The content to clean
+     * @param {Array} rules An array of structures: [ {regex: /something/, replace: "something"}, {...}, ...]
+     * @return {String} The cleaned content
+     */
+    _filterContentWithRules: function(content, rules) {
         var i = 0;
         for (i = 0; i < rules.length; i++) {
             content = content.replace(rules[i].regex, rules[i].replace);
         }
 
+        return content;
+    },
+
+    /**
+     * Intercept and clean html paste events.
+     *
+     * @method pasteCleanup
+     * @param {Object} sourceEvent The YUI EventFacade  object
+     * @return {Boolean} True if the passed event should continue, false if not.
+     */
+    pasteCleanup: function(sourceEvent) {
+        // We only expect paste events, but we will check anyways.
+        if (sourceEvent.type === 'paste') {
+            // The YUI event wrapper doesn't provide paste event info, so we need the underlying event.
+            var event = sourceEvent._event;
+            // Check if we have a valid clipboardData object in the event.
+            // IE has a clipboard object at window.clipboardData, but as of IE 11, it does not provide HTML content access.
+            if (event && event.clipboardData && event.clipboardData.getData) {
+                // Check if there is HTML type to be pasted, this is all we care about.
+                var types = event.clipboardData.types;
+                var isHTML = false;
+                // Different browsers use different things to hold the types, so test various functions.
+                if (!types) {
+                    isHTML = false;
+                } else if (typeof types.contains === 'function') {
+                    isHTML = types.contains('text/html');
+                } else if (typeof types.indexOf === 'function') {
+                    isHTML = (types.indexOf('text/html') > -1);
+                    if (!isHTML) {
+                        if ((types.indexOf('com.apple.webarchive') > -1) || (types.indexOf('com.apple.iWork.TSPNativeData') > -1)) {
+                            // This is going to be a specialized Apple paste paste. We cannot capture this, so clean everything.
+                            this.fallbackPasteCleanupDelayed();
+                            return true;
+                        }
+                    }
+                } else {
+                    // We don't know how to handle the clipboard info, so wait for the clipboard event to finish then fallback.
+                    this.fallbackPasteCleanupDelayed();
+                    return true;
+                }
+
+                if (isHTML) {
+                    // Get the clipboard content.
+                    var content;
+                    try {
+                        content = event.clipboardData.getData('text/html');
+                    } catch (error) {
+                        // Something went wrong. Fallback.
+                        this.fallbackPasteCleanupDelayed();
+                        return true;
+                    }
+
+                    // Stop the original paste.
+                    sourceEvent.preventDefault();
+
+                    // Scrub the paste content.
+                    content = this._cleanPasteHTML(content);
+
+                    // Save the current selection.
+                    // Using saveSelection as it produces a more consistent experience.
+                    var selection = window.rangy.saveSelection();
+
+                    // Insert the content.
+                    this.insertContentAtFocusPoint(content);
+
+                    // Restore the selection, and collapse to end.
+                    window.rangy.restoreSelection(selection);
+                    window.rangy.getSelection().collapseToEnd();
+
+                    // Update the text area.
+                    this.updateOriginal();
+                    return false;
+                } else {
+                    // This is a non-html paste event, we can just let this continue on and call updateOriginalDelayed.
+                    this.updateOriginalDelayed();
+                    return true;
+                }
+            } else {
+                // If we reached a here, this probably means the browser has limited (or no) clipboard support.
+                // Wait for the clipboard event to finish then fallback.
+                this.fallbackPasteCleanupDelayed();
+                return true;
+            }
+        }
+
+        // We should never get here - we must have received a non-paste event for some reason.
+        // Um, just call updateOriginalDelayed() - it's safe.
+        this.updateOriginalDelayed();
+        return true;
+    },
+
+    /**
+     * Cleanup code after a paste event if we couldn't intercept the paste content.
+     *
+     * @method fallbackPasteCleanup
+     * @chainable
+     */
+    fallbackPasteCleanup: function() {
+        Y.log('Using fallbackPasteCleanup for atto cleanup', 'debug', LOGNAME);
+
+        // Save the current selection (cursor position).
+        var selection = window.rangy.saveSelection();
+
+        // Get, clean, and replace the content in the editable.
+        var content = this.editor.get('innerHTML');
+        this.editor.set('innerHTML', this._cleanPasteHTML(content));
+
+        // Update the textarea.
+        this.updateOriginal();
+
+        // Restore the selection (cursor position).
+        window.rangy.restoreSelection(selection);
+
+        return this;
+    },
+
+    /**
+     * Calls fallbackPasteCleanup on a short timer to allow the paste event handlers to complete.
+     *
+     * @method fallbackPasteCleanupDelayed
+     * @chainable
+     */
+    fallbackPasteCleanupDelayed: function() {
+        Y.soon(Y.bind(this.fallbackPasteCleanup, this));
+
+        return this;
+    },
+
+    /**
+     * Cleanup html that comes from WYSIWYG paste events. These are more likely to contain messy code that we should strip.
+     *
+     * @method _cleanPasteHTML
+     * @private
+     * @param {String} content The html content to clean
+     * @return {String} The cleaned HTML
+     */
+    _cleanPasteHTML: function(content) {
+        // Return an empty string if passed an invalid or empty object.
+        if (!content || content.length === 0) {
+            return "";
+        }
+
+        // Rules that get rid of the real-nasties and don't care about normalize code (correct quotes, white spaces, etc).
+        var rules = [
+            // Stuff that is specifically from MS Word and similar office packages.
+            // Remove if comment blocks.
+            {regex: /<!--\[if[\s\S]*?endif\]-->/gi, replace: ""},
+            // Remove start and end fragment comment blocks.
+            {regex: /<!--(Start|End)Fragment-->/gi, replace: ""},
+            // Remove any xml blocks.
+            {regex: /<xml[^>]*>[\s\S]*?<\/xml>/gi, replace: ""},
+            // Remove any <?xml><\?xml> blocks.
+            {regex: /<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi, replace: ""},
+            // Remove <o:blah>, <\o:blah>.
+            {regex: /<\/?\w+:[^>]*>/gi, replace: ""}
+        ];
+
+        // Apply the first set of harsher rules.
+        content = this._filterContentWithRules(content, rules);
+
+        // Apply the standard rules, which mainly cleans things like headers, links, and style blocks.
+        content = this._cleanHTML(content);
+
+        // Check if the string is empty or only contains whitespace.
+        if (content.length === 0 || !content.match(/\S/)) {
+            return content;
+        }
+
+        // Now we let the browser normalize the code by loading it into the DOM and then get the html back.
+        // This gives us well quoted, well formatted code to continue our work on. Word may provide very poorly formatted code.
+        var holder = document.createElement('div');
+        holder.innerHTML = content;
+        content = holder.innerHTML;
+        // Free up the DOM memory.
+        holder.innerHTML = "";
+
+        // Run some more rules that care about quotes and whitespace.
+        rules = [
+            // Remove MSO-blah, MSO:blah in style attributes. Only removes one or more that appear in succession.
+            {regex: /(<[^>]*?style\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[-:][^>;"]*;?)+/gi, replace: "$1"},
+            // Remove MSO classes in class attributes. Only removes one or more that appear in succession.
+            {regex: /(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[_a-zA-Z0-9\-]*)+/gi, replace: "$1"},
+            // Remove Apple- classes in class attributes. Only removes one or more that appear in succession.
+            {regex: /(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*Apple-[_a-zA-Z0-9\-]*)+/gi, replace: "$1"},
+            // Remove OLE_LINK# anchors that may litter the code.
+            {regex: /<a [^>]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi, replace: ""},
+            // Remove empty spans.
+            {regex: /<span[^>]*>(&nbsp;|\s)*<\/span>/gi, replace: ""}
+        ];
+
+        // Apply the rules.
+        content = this._filterContentWithRules(content, rules);
+
+        // Reapply the standard cleaner to the content.
+        content = this._cleanHTML(content);
+
         return content;
     }
 };
index 93e4ede..8089a11 100644 (file)
@@ -296,7 +296,8 @@ Y.extend(Editor, Y.Base, {
      * @chainable
      */
     setupAutomaticPolling: function() {
-        this._registerEventHandle(this.editor.on(['keyup', 'paste', 'cut'], this.updateOriginal, this));
+        this._registerEventHandle(this.editor.on(['keyup', 'cut'], this.updateOriginal, this));
+        this._registerEventHandle(this.editor.on('paste', this.pasteCleanup, this));
 
         // Call this.updateOriginal after dropped content has been processed.
         this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this));
index a0526c5..1909c2f 100644 (file)
@@ -77,6 +77,16 @@ class core_external extends external_api {
         return $strparams;
     }
 
+    /**
+     * Can this function be called directly from ajax?
+     *
+     * @return boolean
+     * @since Moodle 2.9
+     */
+    public static function get_string_is_allowed_from_ajax() {
+        return true;
+    }
+
     /**
      * Returns description of get_string parameters
      *
@@ -153,6 +163,17 @@ class core_external extends external_api {
         );
     }
 
+    /**
+     * Can this function be called directly from ajax?
+     *
+     * @return boolean
+     * @since Moodle 2.9
+     */
+    public static function get_strings_is_allowed_from_ajax() {
+        return true;
+    }
+
+
     /**
      * Return multiple call to core get_string()
      *
@@ -216,6 +237,16 @@ class core_external extends external_api {
         );
     }
 
+    /**
+     * Can this function be called directly from ajax?
+     *
+     * @return boolean
+     * @since Moodle 2.9
+     */
+    public static function get_component_strings_is_allowed_from_ajax() {
+        return true;
+    }
+
     /**
      * Return all lang strings of a component - call to core get_component_strings().
      *
index a9ed4bf..d666932 100644 (file)
@@ -56,6 +56,7 @@ function external_function_info($function, $strictness=MUST_EXIST) {
         }
     }
 
+    $function->ajax_method = $function->methodname.'_is_allowed_from_ajax';
     $function->parameters_method = $function->methodname.'_parameters';
     $function->returns_method    = $function->methodname.'_returns';
     $function->deprecated_method = $function->methodname.'_is_deprecated';
@@ -75,6 +76,12 @@ function external_function_info($function, $strictness=MUST_EXIST) {
             $function->deprecated = true;
         }
     }
+    $function->allowed_from_ajax = false;
+    if (method_exists($function->classname, $function->ajax_method)) {
+        if (call_user_func(array($function->classname, $function->ajax_method)) === true) {
+            $function->allowed_from_ajax = true;
+        }
+    }
 
     // fetch the parameters description
     $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method));
index 7d8ea2f..7b74a70 100644 (file)
@@ -1102,7 +1102,7 @@ function clean_param($param, $type) {
             // Remove some nasties.
             $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
             // Convert many whitespace chars into one.
-            $param = preg_replace('/\s+/', ' ', $param);
+            $param = preg_replace('/\s+/u', ' ', $param);
             $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
             return $param;
 
@@ -6842,6 +6842,26 @@ function get_string_manager($forcereload=false) {
                 $translist = explode(',', $CFG->langlist);
             }
 
+            if (!empty($CFG->config_php_settings['customstringmanager'])) {
+                $classname = $CFG->config_php_settings['customstringmanager'];
+
+                if (class_exists($classname)) {
+                    $implements = class_implements($classname);
+
+                    if (isset($implements['core_string_manager'])) {
+                        $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist);
+                        return $singleton;
+
+                    } else {
+                        debugging('Unable to instantiate custom string manager: class '.$classname.
+                            ' does not implement the core_string_manager interface.');
+                    }
+
+                } else {
+                    debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
+                }
+            }
+
             $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist);
 
         } else {
diff --git a/lib/mustache/CONTRIBUTING.md b/lib/mustache/CONTRIBUTING.md
new file mode 100644 (file)
index 0000000..c0b323d
--- /dev/null
@@ -0,0 +1,35 @@
+# Contributions welcome!
+
+
+### Here's a quick guide:
+
+ 1. [Fork the repo on GitHub](https://github.com/bobthecow/mustache.php).
+
+ 2. Update submodules: `git submodule update --init`
+
+ 3. Run the test suite. We only take pull requests with passing tests, and it's great to know that you have a clean slate. Make sure you have PHPUnit 3.5+, then run `phpunit` from the project directory.
+
+ 4. Add tests for your change. Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, add a test!
+
+ 5. Make the tests pass.
+
+ 6. Push your fork to GitHub and submit a pull request against the `dev` branch.
+
+
+### You can do some things to increase the chance that your pull request is accepted the first time:
+
+ * Submit one pull request per fix or feature.
+ * To help with that, do all your work in a feature branch (e.g. `feature/my-alsome-feature`).
+ * Follow the conventions you see used in the project.
+ * Use `phpcs --standard=PSR2` to check your changes against the coding standard.
+ * Write tests that fail without your code, and pass with it.
+ * Don't bump version numbers. Those will be updated — per [semver](http://semver.org) — once your change is merged into `master`.
+ * Update any documentation: docblocks, README, examples, etc.
+ * ... Don't update the wiki until your change is merged and released, but make a note in your pull request so we don't forget.
+
+
+### Mustache.php follows the PSR-* coding standards:
+
+ * [PSR-0: Class and file naming conventions](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md)
+ * [PSR-1: Basic coding standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md)
+ * [PSR-2: Coding style guide](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
diff --git a/lib/mustache/LICENSE b/lib/mustache/LICENSE
new file mode 100644 (file)
index 0000000..0bdbc04
--- /dev/null
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2010-2014 Justin Hileman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/mustache/README.md b/lib/mustache/README.md
new file mode 100644 (file)
index 0000000..b33b355
--- /dev/null
@@ -0,0 +1,71 @@
+Mustache.php
+============
+
+A [Mustache](http://mustache.github.com/) implementation in PHP.
+
+[![Package version](http://img.shields.io/packagist/v/mustache/mustache.svg)](https://packagist.org/packages/mustache/mustache)
+[![Build status](http://img.shields.io/travis/bobthecow/mustache.php/dev.svg)](http://travis-ci.org/bobthecow/mustache.php)
+[![Monthly downloads](http://img.shields.io/packagist/dm/mustache/mustache.svg)](https://packagist.org/packages/mustache/mustache)
+
+
+Usage
+-----
+
+A quick example:
+
+```php
+<?php
+$m = new Mustache_Engine;
+echo $m->render('Hello {{planet}}', array('planet' => 'World!')); // "Hello World!"
+```
+
+
+And a more in-depth example -- this is the canonical Mustache template:
+
+```html+jinja
+Hello {{name}}
+You have just won ${{value}}!
+{{#in_ca}}
+Well, ${{taxed_value}}, after taxes.
+{{/in_ca}}
+```
+
+
+Create a view "context" object -- which could also be an associative array, but those don't do functions quite as well:
+
+```php
+<?php
+class Chris {
+    public $name  = "Chris";
+    public $value = 10000;
+
+    public function taxed_value() {
+        return $this->value - ($this->value * 0.4);
+    }
+
+    public $in_ca = true;
+}
+```
+
+
+And render it:
+
+```php
+<?php
+$m = new Mustache_Engine;
+$chris = new Chris;
+echo $m->render($template, $chris);
+```
+
+
+And That's Not All!
+-------------------
+
+Read [the Mustache.php documentation](https://github.com/bobthecow/mustache.php/wiki/Home) for more information.
+
+
+See Also
+--------
+
+ * [Readme for the Ruby Mustache implementation](http://github.com/defunkt/mustache/blob/master/README.md).
+ * [mustache(5)](http://mustache.github.com/mustache.5.html) man page.
diff --git a/lib/mustache/composer.json b/lib/mustache/composer.json
new file mode 100644 (file)
index 0000000..2969d03
--- /dev/null
@@ -0,0 +1,24 @@
+{
+    "name": "mustache/mustache",
+    "description": "A Mustache implementation in PHP.",
+    "keywords": ["templating", "mustache"],
+    "homepage": "https://github.com/bobthecow/mustache.php",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Justin Hileman",
+            "email": "justin@justinhileman.info",
+            "homepage": "http://justinhileman.com"
+        }
+    ],
+    "require": {
+        "php": ">=5.2.4"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "*"
+    },
+    "autoload": {
+        "psr-0": { "Mustache": "src/" }
+    }
+}
diff --git a/lib/mustache/readme_moodle.txt b/lib/mustache/readme_moodle.txt
new file mode 100644 (file)
index 0000000..31ce19d
--- /dev/null
@@ -0,0 +1,11 @@
+Description of Mustache library import into moodle.
+
+Download from https://github.com/bobthecow/mustache.php
+
+Delete folder "test"
+
+Delete phpunit.xml.dist
+
+Delete hidden files ".*"
+
+Delete folder "bin"
diff --git a/lib/mustache/src/Mustache/Autoloader.php b/lib/mustache/src/Mustache/Autoloader.php
new file mode 100644 (file)
index 0000000..8c8e906
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache class autoloader.
+ */
+class Mustache_Autoloader
+{
+    private $baseDir;
+
+    /**
+     * Autoloader constructor.
+     *
+     * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
+     */
+    public function __construct($baseDir = null)
+    {
+        if ($baseDir === null) {
+            $baseDir = dirname(__FILE__).'/..';
+        }
+
+        // realpath doesn't always work, for example, with stream URIs
+        $realDir = realpath($baseDir);
+        if (is_dir($realDir)) {
+            $this->baseDir = $realDir;
+        } else {
+            $this->baseDir = $baseDir;
+        }
+    }
+
+    /**
+     * Register a new instance as an SPL autoloader.
+     *
+     * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
+     *
+     * @return Mustache_Autoloader Registered Autoloader instance
+     */
+    public static function register($baseDir = null)
+    {
+        $loader = new self($baseDir);
+        spl_autoload_register(array($loader, 'autoload'));
+
+        return $loader;
+    }
+
+    /**
+     * Autoload Mustache classes.
+     *
+     * @param string $class
+     */
+    public function autoload($class)
+    {
+        if ($class[0] === '\\') {
+            $class = substr($class, 1);
+        }
+
+        if (strpos($class, 'Mustache') !== 0) {
+            return;
+        }
+
+        $file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class));
+        if (is_file($file)) {
+            require $file;
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Cache.php b/lib/mustache/src/Mustache/Cache.php
new file mode 100644 (file)
index 0000000..c8fc5d5
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache interface.
+ *
+ * Interface for caching and loading Mustache_Template classes
+ * generated by the Mustache_Compiler.
+ */
+interface Mustache_Cache
+{
+    /**
+     * Load a compiled Mustache_Template class from cache.
+     *
+     * @param string $key
+     *
+     * @return boolean indicates successfully class load
+     */
+    public function load($key);
+
+    /**
+     * Cache and load a compiled Mustache_Template class.
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return void
+     */
+    public function cache($key, $value);
+}
diff --git a/lib/mustache/src/Mustache/Cache/AbstractCache.php b/lib/mustache/src/Mustache/Cache/AbstractCache.php
new file mode 100644 (file)
index 0000000..98b6451
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Abstract Mustache Cache class.
+ *
+ * Provides logging support to child implementations.
+ *
+ * @abstract
+ */
+abstract class Mustache_Cache_AbstractCache implements Mustache_Cache
+{
+    private $logger = null;
+
+    /**
+     * Get the current logger instance.
+     *
+     * @return Mustache_Logger|Psr\Log\LoggerInterface
+     */
+    public function getLogger()
+    {
+        return $this->logger;
+    }
+
+    /**
+     * Set a logger instance.
+     *
+     * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+     */
+    public function setLogger($logger = null)
+    {
+        if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
+            throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+        }
+
+        $this->logger = $logger;
+    }
+
+    /**
+     * Add a log record if logging is enabled.
+     *
+     * @param integer $level   The logging level
+     * @param string  $message The log message
+     * @param array   $context The log context
+     */
+    protected function log($level, $message, array $context = array())
+    {
+        if (isset($this->logger)) {
+            $this->logger->log($level, $message, $context);
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Cache/FilesystemCache.php b/lib/mustache/src/Mustache/Cache/FilesystemCache.php
new file mode 100644 (file)
index 0000000..120ad2d
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache filesystem implementation.
+ *
+ * A FilesystemCache instance caches Mustache Template classes from the filesystem by name:
+ *
+ *     $cache = new Mustache_Cache_FilesystemCache(dirname(__FILE__).'/cache');
+ *     $cache->cache($className, $compiledSource);
+ *
+ * The FilesystemCache benefits from any opcode caching that may be setup in your environment. So do that, k?
+ */
+class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache
+{
+    private $baseDir;
+    private $fileMode;
+
+    /**
+     * Filesystem cache constructor.
+     *
+     * @param string $baseDir  Directory for compiled templates.
+     * @param int    $fileMode Override default permissions for cache files. Defaults to using the system-defined umask.
+     */
+    public function __construct($baseDir, $fileMode = null)
+    {
+        $this->baseDir = $baseDir;
+        $this->fileMode = $fileMode;
+    }
+
+    /**
+     * Load the class from cache using `require_once`.
+     *
+     * @param string $key
+     *
+     * @return boolean
+     */
+    public function load($key)
+    {
+        $fileName = $this->getCacheFilename($key);
+        if (!is_file($fileName)) {
+            return false;
+        }
+
+        require_once $fileName;
+
+        return true;
+    }
+
+    /**
+     * Cache and load the compiled class
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return void
+     */
+    public function cache($key, $value)
+    {
+        $fileName = $this->getCacheFilename($key);
+
+        $this->log(
+            Mustache_Logger::DEBUG,
+            'Writing to template cache: "{fileName}"',
+            array('fileName' => $fileName)
+        );
+
+        $this->writeFile($fileName, $value);
+        $this->load($key);
+    }
+
+    /**
+     * Build the cache filename.
+     * Subclasses should override for custom cache directory structures.
+     *
+     * @param string $name
+     *
+     * @return string
+     */
+    protected function getCacheFilename($name)
+    {
+        return sprintf('%s/%s.php', $this->baseDir, $name);
+    }
+
+    /**
+     * Create cache directory
+     *
+     * @throws Mustache_Exception_RuntimeException If unable to create directory
+     *
+     * @param string $fileName
+     *
+     * @return string
+     */
+    private function buildDirectoryForFilename($fileName)
+    {
+        $dirName = dirname($fileName);
+        if (!is_dir($dirName)) {
+            $this->log(
+                Mustache_Logger::INFO,
+                'Creating Mustache template cache directory: "{dirName}"',
+                array('dirName' => $dirName)
+            );
+
+            @mkdir($dirName, 0777, true);
+            if (!is_dir($dirName)) {
+                throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
+            }
+        }
+
+        return $dirName;
+    }
+
+    /**
+     * Write cache file
+     *
+     * @throws Mustache_Exception_RuntimeException If unable to write file
+     *
+     * @param string $fileName
+     * @param string $value
+     *
+     * @return void
+     */
+    private function writeFile($fileName, $value)
+    {
+        $dirName = $this->buildDirectoryForFilename($fileName);
+
+        $this->log(
+            Mustache_Logger::DEBUG,
+            'Caching compiled template to "{fileName}"',
+            array('fileName' => $fileName)
+        );
+
+        $tempFile = tempnam($dirName, basename($fileName));
+        if (false !== @file_put_contents($tempFile, $value)) {
+            if (@rename($tempFile, $fileName)) {
+                $mode = isset($this->fileMode) ? $this->fileMode : (0666 & ~umask());
+                @chmod($fileName, $mode);
+
+                return;
+            }
+
+            $this->log(
+                Mustache_Logger::ERROR,
+                'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"',
+                array('tempName' => $tempFile, 'fileName' => $fileName)
+            );
+        }
+
+        throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
+    }
+}
diff --git a/lib/mustache/src/Mustache/Cache/NoopCache.php b/lib/mustache/src/Mustache/Cache/NoopCache.php
new file mode 100644 (file)
index 0000000..d3a7e1f
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache in-memory implementation.
+ *
+ * The in-memory cache is used for uncached lambda section templates. It's also useful during development, but is not
+ * recommended for production use.
+ */
+class Mustache_Cache_NoopCache extends Mustache_Cache_AbstractCache
+{
+    /**
+     * Loads nothing. Move along.
+     *
+     * @param string $key
+     *
+     * @return boolean
+     */
+    public function load($key)
+    {
+        return false;
+    }
+
+    /**
+     * Loads the compiled Mustache Template class without caching.
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return void
+     */
+    public function cache($key, $value)
+    {
+        $this->log(
+            Mustache_Logger::WARNING,
+            'Template cache disabled, evaluating "{className}" class at runtime',
+            array('className' => $key)
+        );
+        eval('?>' . $value);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Compiler.php b/lib/mustache/src/Mustache/Compiler.php
new file mode 100644 (file)
index 0000000..72d3414
--- /dev/null
@@ -0,0 +1,646 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Compiler class.
+ *
+ * This class is responsible for turning a Mustache token parse tree into normal PHP source code.
+ */
+class Mustache_Compiler
+{
+
+    private $pragmas;
+    private $defaultPragmas = array();
+    private $sections;
+    private $source;
+    private $indentNextLine;
+    private $customEscape;
+    private $entityFlags;
+    private $charset;
+    private $strictCallables;
+
+    /**
+     * Compile a Mustache token parse tree into PHP source code.
+     *
+     * @param string $source          Mustache Template source code
+     * @param string $tree            Parse tree of Mustache tokens
+     * @param string $name            Mustache Template class name
+     * @param bool   $customEscape    (default: false)
+     * @param string $charset         (default: 'UTF-8')
+     * @param bool   $strictCallables (default: false)
+     * @param int    $entityFlags     (default: ENT_COMPAT)
+     *
+     * @return string Generated PHP source code
+     */
+    public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
+    {
+        $this->pragmas         = $this->defaultPragmas;
+        $this->sections        = array();
+        $this->source          = $source;
+        $this->indentNextLine  = true;
+        $this->customEscape    = $customEscape;
+        $this->entityFlags     = $entityFlags;
+        $this->charset         = $charset;
+        $this->strictCallables = $strictCallables;
+
+        return $this->writeCode($tree, $name);
+    }
+
+    /**
+     * Enable pragmas across all templates, regardless of the presence of pragma
+     * tags in the individual templates.
+     *
+     * @internal Users should set global pragmas in Mustache_Engine, not here :)
+     *
+     * @param string[] $pragmas
+     */
+    public function setPragmas(array $pragmas)
+    {
+        $this->pragmas = array();
+        foreach ($pragmas as $pragma) {
+            $this->pragmas[$pragma] = true;
+        }
+        $this->defaultPragmas = $this->pragmas;
+    }
+
+    /**
+     * Helper function for walking the Mustache token parse tree.
+     *
+     * @throws Mustache_Exception_SyntaxException upon encountering unknown token types.
+     *
+     * @param array $tree  Parse tree of Mustache tokens
+     * @param int   $level (default: 0)
+     *
+     * @return string Generated PHP source code
+     */
+    private function walk(array $tree, $level = 0)
+    {
+        $code = '';
+        $level++;
+        foreach ($tree as $node) {
+            switch ($node[Mustache_Tokenizer::TYPE]) {
+                case Mustache_Tokenizer::T_PRAGMA:
+                    $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true;
+                    break;
+
+                case Mustache_Tokenizer::T_SECTION:
+                    $code .= $this->section(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_INVERTED:
+                    $code .= $this->invertedSection(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARTIAL:
+                    $code .= $this->partial(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARENT:
+                    $code .= $this->parent(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+                        $node[Mustache_Tokenizer::NODES],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_BLOCK_ARG:
+                    $code .= $this->blockArg(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_BLOCK_VAR:
+                    $code .= $this->blockVar(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_COMMENT:
+                    break;
+
+                case Mustache_Tokenizer::T_ESCAPED:
+                case Mustache_Tokenizer::T_UNESCAPED:
+                case Mustache_Tokenizer::T_UNESCAPED_2:
+                    $code .= $this->variable(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_ESCAPED,
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_TEXT:
+                    $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
+                    break;
+
+                default:
+                    throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
+            }
+        }
+
+        return $code;
+    }
+
+    const KLASS = '<?php
+
+        class %s extends Mustache_Template
+        {
+            private $lambdaHelper;%s
+
+            public function renderInternal(Mustache_Context $context, $indent = \'\')
+            {
+                $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
+                $buffer = \'\';
+                $newContext = array();
+        %s
+
+                return $buffer;
+            }
+        %s
+        }';
+
+    const KLASS_NO_LAMBDAS = '<?php
+
+        class %s extends Mustache_Template
+        {%s
+            public function renderInternal(Mustache_Context $context, $indent = \'\')
+            {
+                $buffer = \'\';
+                $newContext = array();
+        %s
+
+                return $buffer;
+            }
+        }';
+
+    const STRICT_CALLABLE = 'protected $strictCallables = true;';
+
+    /**
+     * Generate Mustache Template class PHP source.
+     *
+     * @param array  $tree Parse tree of Mustache tokens
+     * @param string $name Mustache Template class name
+     *
+     * @return string Generated PHP source code
+     */
+    private function writeCode($tree, $name)
+    {
+        $code     = $this->walk($tree);
+        $sections = implode("\n", $this->sections);
+        $klass    = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;
+
+        $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
+
+        return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections);
+    }
+
+    const BLOCK_VAR = '
+        $value = $this->resolveValue($context->findInBlock(%s), $context, $indent);
+        if ($value && !is_array($value) && !is_object($value)) {
+            $buffer .= $value;
+        } else {
+            %s
+        }
+    ';
+
+    /**
+     * Generate Mustache Template inheritance block variable PHP source.
+     *
+     * @param array  $nodes Array of child tokens
+     * @param string $id    Section name
+     * @param int    $start Section start offset
+     * @param int    $end   Section end offset
+     * @param string $otag  Current Mustache opening tag
+     * @param string $ctag  Current Mustache closing tag
+     * @param int    $level
+     *
+     * @return string Generated PHP source code
+     */
+    private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $id = var_export($id, true);
+
+        return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $this->walk($nodes, 2));
+    }
+
+    const BLOCK_ARG = '
+        // %s block_arg
+        $value = $this->section%s($context, $indent, true);
+        $newContext[%s] = %s$value;
+    ';
+
+    /**
+     * Generate Mustache Template inheritance block argument PHP source.
+     *
+     * @param array  $nodes Array of child tokens
+     * @param string $id    Section name
+     * @param int    $start Section start offset
+     * @param int    $end   Section end offset
+     * @param string $otag  Current Mustache opening tag
+     * @param string $ctag  Current Mustache closing tag
+     * @param int    $level
+     *
+     * @return string Generated PHP source code
+     */
+    private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $key = $this->section($nodes, $id, array(), $start, $end, $otag, $ctag, $level, true);
+        $id  = var_export($id, true);
+
+        return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key, $id, $this->flushIndent());
+    }
+
+    const SECTION_CALL = '
+        // %s section
+        $value = $context->%s(%s);%s
+        $buffer .= $this->section%s($context, $indent, $value);
+    ';
+
+    const SECTION = '
+        private function section%s(Mustache_Context $context, $indent, $value)
+        {
+            $buffer = \'\';
+            if (%s) {
+                $source = %s;
+                $result = call_user_func($value, $source, $this->lambdaHelper);
+                if (strpos($result, \'{{\') === false) {
+                    $buffer .= $result;
+                } else {
+                    $buffer .= $this->mustache
+                        ->loadLambda((string) $result%s)
+                        ->renderInternal($context);
+                }
+            } elseif (!empty($value)) {
+                $values = $this->isIterable($value) ? $value : array($value);
+                foreach ($values as $value) {
+                    $context->push($value);
+                    %s
+                    $context->pop();
+                }
+            }
+
+            return $buffer;
+        }';
+
+    /**
+     * Generate Mustache Template section PHP source.
+     *
+     * @param array    $nodes   Array of child tokens
+     * @param string   $id      Section name
+     * @param string[] $filters Array of filters
+     * @param int      $start   Section start offset
+     * @param int      $end     Section end offset
+     * @param string   $otag    Current Mustache opening tag
+     * @param string   $ctag    Current Mustache closing tag
+     * @param int      $level
+     * @param bool     $arg     (default: false)
+     *
+     * @return string Generated section PHP source code
+     */
+    private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $level, $arg = false)
+    {
+        $source   = var_export(substr($this->source, $start, $end - $start), true);
+        $callable = $this->getCallable();
+
+        if ($otag !== '{{' || $ctag !== '}}') {
+            $delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
+        } else {
+            $delims = '';
+        }
+
+        $key = ucfirst(md5($delims."\n".$source));
+
+        if (!isset($this->sections[$key])) {
+            $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $delims, $this->walk($nodes, 2));
+        }
+
+        if ($arg === true) {
+            return $key;
+        } else {
+            $method  = $this->getFindMethod($id);
+            $id      = var_export($id, true);
+            $filters = $this->getFilters($filters, $level);
+
+            return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
+        }
+    }
+
+    const INVERTED_SECTION = '
+        // %s inverted section
+        $value = $context->%s(%s);%s
+        if (empty($value)) {
+            %s
+        }';
+
+    /**
+     * Generate Mustache Template inverted section PHP source.
+     *
+     * @param array    $nodes   Array of child tokens
+     * @param string   $id      Section name
+     * @param string[] $filters Array of filters
+     * @param int      $level
+     *
+     * @return string Generated inverted section PHP source code
+     */
+    private function invertedSection($nodes, $id, $filters, $level)
+    {
+        $method  = $this->getFindMethod($id);
+        $id      = var_export($id, true);
+        $filters = $this->getFilters($filters, $level);
+
+        return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level));
+    }
+
+    const PARTIAL_INDENT = ', $indent . %s';
+    const PARTIAL = '
+        if ($partial = $this->mustache->loadPartial(%s)) {
+            $buffer .= $partial->renderInternal($context%s);
+        }
+    ';
+
+    /**
+     * Generate Mustache Template partial call PHP source.
+     *
+     * @param string $id     Partial name
+     * @param string $indent Whitespace indent to apply to partial
+     * @param int    $level
+     *
+     * @return string Generated partial call PHP source code
+     */
+    private function partial($id, $indent, $level)
+    {
+        if ($indent !== '') {
+            $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true));
+        } else {
+            $indentParam = '';
+        }
+
+        return sprintf(
+            $this->prepare(self::PARTIAL, $level),
+            var_export($id, true),
+            $indentParam
+        );
+    }
+
+    const PARENT = '
+        %s
+
+        if ($parent = $this->mustache->LoadPartial(%s)) {
+            $context->pushBlockContext($newContext);
+            $buffer .= $parent->renderInternal($context, $indent);
+            $context->popBlockContext();
+        }
+    ';
+
+    /**
+     * Generate Mustache Template inheritance parent call PHP source.
+     *
+     * @param string $id       Parent tag name
+     * @param string $indent   Whitespace indent to apply to parent
+     * @param array  $children Child nodes
+     * @param int    $level
+     *
+     * @return string Generated PHP source code
+     */
+    private function parent($id, $indent, array $children, $level)
+    {
+        $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs'));
+
+        return sprintf(
+            $this->prepare(self::PARENT, $level),
+            $this->walk($realChildren, $level),
+            var_export($id, true),
+            var_export($indent, true)
+        );
+    }
+
+    /**
+     * Helper method for filtering out non-block-arg tokens.
+     *
+     * @param array $node
+     *
+     * @return boolean True if $node is a block arg token.
+     */
+    private static function onlyBlockArgs(array $node)
+    {
+        return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG;
+    }
+
+    const VARIABLE = '
+        $value = $this->resolveValue($context->%s(%s), $context, $indent);%s
+        $buffer .= %s%s;
+    ';
+
+    /**
+     * Generate Mustache Template variable interpolation PHP source.
+     *
+     * @param string   $id      Variable name
+     * @param string[] $filters Array of filters
+     * @param boolean  $escape  Escape the variable value for output?
+     * @param int      $level
+     *
+     * @return string Generated variable interpolation PHP source
+     */
+    private function variable($id, $filters, $escape, $level)
+    {
+        $method  = $this->getFindMethod($id);
+        $id      = ($method !== 'last') ? var_export($id, true) : '';
+        $filters = $this->getFilters($filters, $level);
+        $value   = $escape ? $this->getEscape() : '$value';
+
+        return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
+    }
+
+    const FILTER = '
+        $filter = $context->%s(%s);
+        if (!(%s)) {
+            throw new Mustache_Exception_UnknownFilterException(%s);
+        }
+        $value = call_user_func($filter, $value);%s
+    ';
+
+    /**
+     * Generate Mustache Template variable filtering PHP source.
+     *
+     * @param string[] $filters Array of filters
+     * @param int      $level
+     *
+     * @return string Generated filter PHP source
+     */
+    private function getFilters(array $filters, $level)
+    {
+        if (empty($filters)) {
+            return '';
+        }
+
+        $name     = array_shift($filters);
+        $method   = $this->getFindMethod($name);
+        $filter   = ($method !== 'last') ? var_export($name, true) : '';
+        $callable = $this->getCallable('$filter');
+        $msg      = var_export($name, true);
+
+        return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level));
+    }
+
+    const LINE = '$buffer .= "\n";';
+    const TEXT = '$buffer .= %s%s;';
+
+    /**
+     * Generate Mustache Template output Buffer call PHP source.
+     *
+     * @param string $text
+     * @param int    $level
+     *
+     * @return string Generated output Buffer call PHP source
+     */
+    private function text($text, $level)
+    {
+        $indentNextLine = (substr($text, -1) === "\n");
+        $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
+        $this->indentNextLine = $indentNextLine;
+
+        return $code;
+    }
+
+    /**
+     * Prepare PHP source code snippet for output.
+     *
+     * @param string  $text
+     * @param int     $bonus          Additional indent level (default: 0)
+     * @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
+     * @param boolean $appendNewline  Append a newline to the snippet? (default: false)
+     *
+     * @return string PHP source code snippet
+     */
+    private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
+    {
+        $text = ($prependNewline ? "\n" : '').trim($text);
+        if ($prependNewline) {
+            $bonus++;
+        }
+        if ($appendNewline) {
+            $text .= "\n";
+        }
+
+        return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
+    }
+
+    const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)';
+    const CUSTOM_ESCAPE  = 'call_user_func($this->mustache->getEscape(), %s)';
+
+    /**
+     * Get the current escaper.
+     *
+     * @param string $value (default: '$value')
+     *
+     * @return string Either a custom callback, or an inline call to `htmlspecialchars`
+     */
+    private function getEscape($value = '$value')
+    {
+        if ($this->customEscape) {
+            return sprintf(self::CUSTOM_ESCAPE, $value);
+        }
+
+        return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true));
+    }
+
+    /**
+     * Select the appropriate Context `find` method for a given $id.
+     *
+     * The return value will be one of `find`, `findDot` or `last`.
+     *
+     * @see Mustache_Context::find
+     * @see Mustache_Context::findDot
+     * @see Mustache_Context::last
+     *
+     * @param string $id Variable name
+     *
+     * @return string `find` method name
+     */
+    private function getFindMethod($id)
+    {
+        if ($id === '.') {
+            return 'last';
+        }
+
+        if (strpos($id, '.') === false) {
+            return 'find';
+        }
+
+        return 'findDot';
+    }
+
+    const IS_CALLABLE        = '!is_string(%s) && is_callable(%s)';
+    const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
+
+    /**
+     * Helper function to compile strict vs lax "is callable" logic.
+     *
+     * @param string $variable (default: '$value')
+     *
+     * @return string "is callable" logic
+     */
+    private function getCallable($variable = '$value')
+    {
+        $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
+
+        return sprintf($tpl, $variable, $variable);
+    }
+
+    const LINE_INDENT = '$indent . ';
+
+    /**
+     * Get the current $indent prefix to write to the buffer.
+     *
+     * @return string "$indent . " or ""
+     */
+    private function flushIndent()
+    {
+        if (!$this->indentNextLine) {
+            return '';
+        }
+
+        $this->indentNextLine = false;
+
+        return self::LINE_INDENT;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Context.php b/lib/mustache/src/Mustache/Context.php
new file mode 100644 (file)
index 0000000..db03acc
--- /dev/null
@@ -0,0 +1,206 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template rendering Context.
+ */
+class Mustache_Context
+{
+    private $stack      = array();
+    private $blockStack = array();
+
+    /**
+     * Mustache rendering Context constructor.
+     *
+     * @param mixed $context Default rendering context (default: null)
+     */
+    public function __construct($context = null)
+    {
+        if ($context !== null) {
+            $this->stack = array($context);
+        }
+    }
+
+    /**
+     * Push a new Context frame onto the stack.
+     *
+     * @param mixed $value Object or array to use for context
+     */
+    public function push($value)
+    {
+        array_push($this->stack, $value);
+    }
+
+    /**
+     * Push a new Context frame onto the block context stack.
+     *
+     * @param mixed $value Object or array to use for block context
+     */
+    public function pushBlockContext($value)
+    {
+        array_push($this->blockStack, $value);
+    }
+
+    /**
+     * Pop the last Context frame from the stack.
+     *
+     * @return mixed Last Context frame (object or array)
+     */
+    public function pop()
+    {
+        return array_pop($this->stack);
+    }
+
+    /**
+     * Pop the last block Context frame from the stack.
+     *
+     * @return mixed Last block Context frame (object or array)
+     */
+    public function popBlockContext()
+    {
+        return array_pop($this->blockStack);
+    }
+
+    /**
+     * Get the last Context frame.
+     *
+     * @return mixed Last Context frame (object or array)
+     */
+    public function last()
+    {
+        return end($this->stack);
+    }
+
+    /**
+     * Find a variable in the Context stack.
+     *
+     * Starting with the last Context frame (the context of the innermost section), and working back to the top-level
+     * rendering context, look for a variable with the given name:
+     *
+     *  * If the Context frame is an associative array which contains the key $id, returns the value of that element.
+     *  * If the Context frame is an object, this will check first for a public method, then a public property named
+     *    $id. Failing both of these, it will try `__isset` and `__get` magic methods.
+     *  * If a value named $id is not found in any Context frame, returns an empty string.
+     *
+     * @param string $id Variable name
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function find($id)
+    {
+        return $this->findVariableInStack($id, $this->stack);
+    }
+
+    /**
+     * Find a 'dot notation' variable in the Context stack.
+     *
+     * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
+     * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
+     * result. For example, given the following context stack:
+     *
+     *     $data = array(
+     *         'name' => 'Fred',
+     *         'child' => array(
+     *             'name' => 'Bob'
+     *         ),
+     *     );
+     *
+     * ... and the Mustache following template:
+     *
+     *     {{ child.name }}
+     *
+     * ... the `name` value is only searched for within the `child` value of the global Context, not within parent
+     * Context frames.
+     *
+     * @param string $id Dotted variable selector
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function findDot($id)
+    {
+        $chunks = explode('.', $id);
+        $first  = array_shift($chunks);
+        $value  = $this->findVariableInStack($first, $this->stack);
+
+        foreach ($chunks as $chunk) {
+            if ($value === '') {
+                return $value;
+            }
+
+            $value = $this->findVariableInStack($chunk, array($value));
+        }
+
+        return $value;
+    }
+
+    /**
+     * Find an argument in the block context stack.
+     *
+     * @param string $id
+     *
+     * @return mixed Variable value, or '' if not found.
+     */
+    public function findInBlock($id)
+    {
+        foreach ($this->blockStack as $context) {
+            if (array_key_exists($id, $context)) {
+                return $context[$id];
+            }
+        }
+
+        return '';
+    }
+
+    /**
+     * Helper function to find a variable in the Context stack.
+     *
+     * @see Mustache_Context::find
+     *
+     * @param string $id    Variable name
+     * @param array  $stack Context stack
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    private function findVariableInStack($id, array $stack)
+    {
+        for ($i = count($stack) - 1; $i >= 0; $i--) {
+            $frame = &$stack[$i];
+
+            switch (gettype($frame)) {
+                case 'object':
+                    if (!($frame instanceof Closure)) {
+                        // Note that is_callable() *will not work here*
+                        // See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods
+                        if (method_exists($frame, $id)) {
+                            return $frame->$id();
+                        }
+
+                        if (isset($frame->$id)) {
+                            return $frame->$id;
+                        }
+
+                        if ($frame instanceof ArrayAccess && isset($frame[$id])) {
+                            return $frame[$id];
+                        }
+                    }
+                    break;
+
+                case 'array':
+                    if (array_key_exists($id, $frame)) {
+                        return $frame[$id];
+                    }
+                    break;
+            }
+        }
+
+        return '';
+    }
+}
diff --git a/lib/mustache/src/Mustache/Engine.php b/lib/mustache/src/Mustache/Engine.php
new file mode 100644 (file)
index 0000000..4cac394
--- /dev/null
@@ -0,0 +1,785 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache implementation in PHP.
+ *
+ * {@link http://defunkt.github.com/mustache}
+ *
+ * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
+ * logic from template files. In fact, it is not even possible to embed logic in the template.
+ *
+ * This is very, very rad.
+ *
+ * @author Justin Hileman {@link http://justinhileman.com}
+ */
+class Mustache_Engine
+{
+    const VERSION        = '2.7.0';
+    const SPEC_VERSION   = '1.1.2';
+
+    const PRAGMA_FILTERS = 'FILTERS';
+    const PRAGMA_BLOCKS  = 'BLOCKS';
+
+    // Known pragmas
+    private static $knownPragmas = array(
+        self::PRAGMA_FILTERS => true,
+        self::PRAGMA_BLOCKS  => true,
+    );
+
+    // Template cache
+    private $templates = array();
+
+    // Environment
+    private $templateClassPrefix = '__Mustache_';
+    private $cache;
+    private $lambdaCache;
+    private $cacheLambdaTemplates = false;
+    private $loader;
+    private $partialsLoader;
+    private $helpers;
+    private $escape;
+    private $entityFlags = ENT_COMPAT;
+    private $charset = 'UTF-8';
+    private $logger;
+    private $strictCallables = false;
+    private $pragmas = array();
+
+    // Services
+    private $tokenizer;
+    private $parser;
+    private $compiler;
+
+    /**
+     * Mustache class constructor.
+     *
+     * Passing an $options array allows overriding certain Mustache options during instantiation:
+     *
+     *     $options = array(
+     *         // The class prefix for compiled templates. Defaults to '__Mustache_'.
+     *         'template_class_prefix' => '__MyTemplates_',
+     *
+     *         // A Mustache cache instance or a cache directory string for compiled templates.
+     *         // Mustache will not cache templates unless this is set.
+     *         'cache' => dirname(__FILE__).'/tmp/cache/mustache',
+     *
+     *         // Override default permissions for cache files. Defaults to using the system-defined umask. It is
+     *         // *strongly* recommended that you configure your umask properly rather than overriding permissions here.
+     *         'cache_file_mode' => 0666,
+     *
+     *         // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda
+     *         // sections are often too dynamic to benefit from caching.
+     *         'cache_lambda_templates' => true,
+     *
+     *         // A Mustache template loader instance. Uses a StringLoader if not specified.
+     *         'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
+     *
+     *         // A Mustache loader instance for partials.
+     *         'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
+     *
+     *         // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
+     *         // efficient or lazy as a Filesystem (or database) loader.
+     *         'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')),
+     *
+     *         // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
+     *         // sections), or any other valid Mustache context value. They will be prepended to the context stack,
+     *         // so they will be available in any template loaded by this Mustache instance.
+     *         'helpers' => array('i18n' => function ($text) {
+     *             // do something translatey here...
+     *         }),
+     *
+     *         // An 'escape' callback, responsible for escaping double-mustache variables.
+     *         'escape' => function ($value) {
+     *             return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
+     *         },
+     *
+     *         // Type argument for `htmlspecialchars`.  Defaults to ENT_COMPAT.  You may prefer ENT_QUOTES.
+     *         'entity_flags' => ENT_QUOTES,
+     *
+     *         // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
+     *         'charset' => 'ISO-8859-1',
+     *
+     *         // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
+     *         // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
+     *         // available as well:
+     *         'logger' => new Mustache_Logger_StreamLogger('php://stderr'),
+     *
+     *         // Only treat Closure instances and invokable classes as callable. If true, values like
+     *         // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally
+     *         // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
+     *         // helps protect against arbitrary code execution when user input is passed directly into the template.
+     *         // This currently defaults to false, but will default to true in v3.0.
+     *         'strict_callables' => true,
+     *
+     *         // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
+     *         // templates.
+     *         'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
+     *     );
+     *
+     * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable.
+     *
+     * @param array $options (default: array())
+     */
+    public function __construct(array $options = array())
+    {
+        if (isset($options['template_class_prefix'])) {
+            $this->templateClassPrefix = $options['template_class_prefix'];
+        }
+
+        if (isset($options['cache'])) {
+            $cache = $options['cache'];
+
+            if (is_string($cache)) {
+                $mode  = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null;
+                $cache = new Mustache_Cache_FilesystemCache($cache, $mode);
+            }
+
+            $this->setCache($cache);
+        }
+
+        if (isset($options['cache_lambda_templates'])) {
+            $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates'];
+        }
+
+        if (isset($options['loader'])) {
+            $this->setLoader($options['loader']);
+        }
+
+        if (isset($options['partials_loader'])) {
+            $this->setPartialsLoader($options['partials_loader']);
+        }
+
+        if (isset($options['partials'])) {
+            $this->setPartials($options['partials']);
+        }
+
+        if (isset($options['helpers'])) {
+            $this->setHelpers($options['helpers']);
+        }
+
+        if (isset($options['escape'])) {
+            if (!is_callable($options['escape'])) {
+                throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable');
+            }
+
+            $this->escape = $options['escape'];
+        }
+
+        if (isset($options['entity_flags'])) {
+          $this->entityFlags = $options['entity_flags'];
+        }
+
+        if (isset($options['charset'])) {
+            $this->charset = $options['charset'];
+        }
+
+        if (isset($options['logger'])) {
+            $this->setLogger($options['logger']);
+        }
+
+        if (isset($options['strict_callables'])) {
+            $this->strictCallables = $options['strict_callables'];
+        }
+
+        if (isset($options['pragmas'])) {
+            foreach ($options['pragmas'] as $pragma) {
+                if (!isset(self::$knownPragmas[$pragma])) {
+                    throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma));
+                }
+                $this->pragmas[$pragma] = true;
+            }
+        }
+    }
+
+    /**
+     * Shortcut 'render' invocation.
+     *
+     * Equivalent to calling `$mustache->loadTemplate($template)->render($context);`
+     *
+     * @see Mustache_Engine::loadTemplate
+     * @see Mustache_Template::render
+     *
+     * @param string $template
+     * @param mixed  $context  (default: array())
+     *
+     * @return string Rendered template
+     */
+    public function render($template, $context = array())
+    {
+        return $this->loadTemplate($template)->render($context);
+    }
+
+    /**
+     * Get the current Mustache escape callback.
+     *
+     * @return callable|null
+     */
+    public function getEscape()
+    {
+        return $this->escape;
+    }
+
+    /**
+     * Get the current Mustache entitity type to escape.
+     *
+     * @return int
+     */
+    public function getEntityFlags()
+    {
+        return $this->entityFlags;
+    }
+
+    /**
+     * Get the current Mustache character set.
+     *
+     * @return string
+     */
+    public function getCharset()
+    {
+        return $this->charset;
+    }
+
+    /**
+     * Get the current globally enabled pragmas.
+     *
+     * @return array
+     */
+    public function getPragmas()
+    {
+        return array_keys($this->pragmas);
+    }
+
+    /**
+     * Set the Mustache template Loader instance.
+     *
+     * @param Mustache_Loader $loader
+     */
+    public function setLoader(Mustache_Loader $loader)
+    {
+        $this->loader = $loader;
+    }
+
+    /**
+     * Get the current Mustache template Loader instance.
+     *
+     * If no Loader instance has been explicitly specified, this method will instantiate and return
+     * a StringLoader instance.
+     *
+     * @return Mustache_Loader
+     */
+    public function getLoader()
+    {
+        if (!isset($this->loader)) {
+            $this->loader = new Mustache_Loader_StringLoader();
+        }
+
+        return $this->loader;
+    }
+
+    /**
+     * Set the Mustache partials Loader instance.
+     *
+     * @param Mustache_Loader $partialsLoader
+     */
+    public function setPartialsLoader(Mustache_Loader $partialsLoader)
+    {
+        $this->partialsLoader = $partialsLoader;
+    }
+
+    /**
+     * Get the current Mustache partials Loader instance.
+     *
+     * If no Loader instance has been explicitly specified, this method will instantiate and return
+     * an ArrayLoader instance.
+     *
+     * @return Mustache_Loader
+     */
+    public function getPartialsLoader()
+    {
+        if (!isset($this->partialsLoader)) {
+            $this->partialsLoader = new Mustache_Loader_ArrayLoader();
+        }
+
+        return $this->partialsLoader;
+    }
+
+    /**
+     * Set partials for the current partials Loader instance.
+     *
+     * @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable
+     *
+     * @param array $partials (default: array())
+     */
+    public function setPartials(array $partials = array())
+    {
+        if (!isset($this->partialsLoader)) {
+            $this->partialsLoader = new Mustache_Loader_ArrayLoader();
+        }
+
+        if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) {
+            throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
+        }
+
+        $this->partialsLoader->setTemplates($partials);
+    }
+
+    /**
+     * Set an array of Mustache helpers.
+     *
+     * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
+     * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
+     * any template loaded by this Mustache instance.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable
+     *
+     * @param array|Traversable $helpers
+     */
+    public function setHelpers($helpers)
+    {
+        if (!is_array($helpers) && !$helpers instanceof Traversable) {
+            throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers');
+        }
+
+        $this->getHelpers()->clear();
+
+        foreach ($helpers as $name => $helper) {
+            $this->addHelper($name, $helper);
+        }
+    }
+
+    /**
+     * Get the current set of Mustache helpers.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @return Mustache_HelperCollection
+     */
+    public function getHelpers()
+    {
+        if (!isset($this->helpers)) {
+            $this->helpers = new Mustache_HelperCollection();
+        }
+
+        return $this->helpers;
+    }
+
+    /**
+     * Add a new Mustache helper.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function addHelper($name, $helper)
+    {
+        $this->getHelpers()->add($name, $helper);
+    }
+
+    /**
+     * Get a Mustache helper by name.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function getHelper($name)
+    {
+        return $this->getHelpers()->get($name);
+    }
+
+    /**
+     * Check whether this Mustache instance has a helper.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     *
+     * @return boolean True if the helper is present
+     */
+    public function hasHelper($name)
+    {
+        return $this->getHelpers()->has($name);
+    }
+
+    /**
+     * Remove a helper by name.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     */
+    public function removeHelper($name)
+    {
+        $this->getHelpers()->remove($name);
+    }
+
+    /**
+     * Set the Mustache Logger instance.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface.
+     *
+     * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+     */
+    public function setLogger($logger = null)
+    {
+        if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
+            throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+        }
+
+        if ($this->getCache()->getLogger() === null) {
+            $this->getCache()->setLogger($logger);
+        }
+
+        $this->logger = $logger;
+    }
+
+    /**
+     * Get the current Mustache Logger instance.
+     *
+     * @return Mustache_Logger|Psr\Log\LoggerInterface
+     */
+    public function getLogger()
+    {
+        return $this->logger;
+    }
+
+    /**
+     * Set the Mustache Tokenizer instance.
+     *
+     * @param Mustache_Tokenizer $tokenizer
+     */
+    public function setTokenizer(Mustache_Tokenizer $tokenizer)
+    {
+        $this->tokenizer = $tokenizer;
+    }
+
+    /**
+     * Get the current Mustache Tokenizer instance.
+     *
+     * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Tokenizer
+     */
+    public function getTokenizer()
+    {
+        if (!isset($this->tokenizer)) {
+            $this->tokenizer = new Mustache_Tokenizer();
+        }
+
+        return $this->tokenizer;
+    }
+
+    /**
+     * Set the Mustache Parser instance.
+     *
+     * @param Mustache_Parser $parser
+     */
+    public function setParser(Mustache_Parser $parser)
+    {
+        $this->parser = $parser;
+    }
+
+    /**
+     * Get the current Mustache Parser instance.
+     *
+     * If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Parser
+     */
+    public function getParser()
+    {
+        if (!isset($this->parser)) {
+            $this->parser = new Mustache_Parser();
+        }
+
+        return $this->parser;
+    }
+
+    /**
+     * Set the Mustache Compiler instance.
+     *
+     * @param Mustache_Compiler $compiler
+     */
+    public function setCompiler(Mustache_Compiler $compiler)
+    {
+        $this->compiler = $compiler;
+    }
+
+    /**
+     * Get the current Mustache Compiler instance.
+     *
+     * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Compiler
+     */
+    public function getCompiler()
+    {
+        if (!isset($this->compiler)) {
+            $this->compiler = new Mustache_Compiler();
+        }
+
+        return $this->compiler;
+    }
+
+    /**
+     * Set the Mustache Cache instance.
+     *
+     * @param Mustache_Cache $cache
+     */
+    public function setCache(Mustache_Cache $cache)
+    {
+        if (isset($this->logger) && $cache->getLogger() === null) {
+            $cache->setLogger($this->getLogger());
+        }
+
+        $this->cache = $cache;
+    }
+
+    /**
+     * Get the current Mustache Cache instance.
+     *
+     * If no Cache instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Cache
+     */
+    public function getCache()
+    {
+        if (!isset($this->cache)) {
+            $this->setCache(new Mustache_Cache_NoopCache());
+        }
+
+        return $this->cache;
+    }
+
+    /**
+     * Get the current Lambda Cache instance.
+     *
+     * If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache.
+     *
+     * @see Mustache_Engine::getCache
+     *
+     * @return Mustache_Cache
+     */
+    protected function getLambdaCache()
+    {
+        if ($this->cacheLambdaTemplates) {
+            return $this->getCache();
+        }
+
+        if (!isset($this->lambdaCache)) {
+            $this->lambdaCache = new Mustache_Cache_NoopCache();
+        }
+
+        return $this->lambdaCache;
+    }
+
+    /**
+     * Helper method to generate a Mustache template class.
+     *
+     * @param string $source
+     *
+     * @return string Mustache Template class name
+     */
+    public function getTemplateClassName($source)
+    {
+        return $this->templateClassPrefix . md5(sprintf(
+            'version:%s,escape:%s,entity_flags:%i,charset:%s,strict_callables:%s,pragmas:%s,source:%s',
+            self::VERSION,
+            isset($this->escape) ? 'custom' : 'default',
+            $this->entityFlags,
+            $this->charset,
+            $this->strictCallables ? 'true' : 'false',
+            implode(' ', $this->getPragmas()),
+            $source
+        ));
+    }
+
+    /**
+     * Load a Mustache Template by name.
+     *
+     * @param string $name
+     *
+     * @return Mustache_Template
+     */
+    public function loadTemplate($name)
+    {
+        return $this->loadSource($this->getLoader()->load($name));
+    }
+
+    /**
+     * Load a Mustache partial Template by name.
+     *
+     * This is a helper method used internally by Template instances for loading partial templates. You can most likely
+     * ignore it completely.
+     *
+     * @param string $name
+     *
+     * @return Mustache_Template
+     */
+    public function loadPartial($name)
+    {
+        try {
+            if (isset($this->partialsLoader)) {
+                $loader = $this->partialsLoader;
+            } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) {
+                $loader = $this->loader;
+            } else {
+                throw new Mustache_Exception_UnknownTemplateException($name);
+            }
+
+            return $this->loadSource($loader->load($name));
+        } catch (Mustache_Exception_UnknownTemplateException $e) {
+            // If the named partial cannot be found, log then return null.
+            $this->log(
+                Mustache_Logger::WARNING,
+                'Partial not found: "{name}"',
+                array('name' => $e->getTemplateName())
+            );
+        }
+    }
+
+    /**
+     * Load a Mustache lambda Template by source.
+     *
+     * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
+     * likely ignore it completely.
+     *
+     * @param string $source
+     * @param string $delims (default: null)
+     *
+     * @return Mustache_Template
+     */
+    public function loadLambda($source, $delims = null)
+    {
+        if ($delims !== null) {
+            $source = $delims . "\n" . $source;
+        }
+
+        return $this->loadSource($source, $this->getLambdaCache());
+    }
+
+    /**
+     * Instantiate and return a Mustache Template instance by source.
+     *
+     * Optionally provide a Mustache_Cache instance. This is used internally by Mustache_Engine::loadLambda to respect
+     * the 'cache_lambda_templates' configuration option.
+     *
+     * @see Mustache_Engine::loadTemplate
+     * @see Mustache_Engine::loadPartial
+     * @see Mustache_Engine::loadLambda
+     *
+     * @param string         $source
+     * @param Mustache_Cache $cache  (default: null)
+     *
+     * @return Mustache_Template
+     */
+    private function loadSource($source, Mustache_Cache $cache = null)
+    {
+        $className = $this->getTemplateClassName($source);
+
+        if (!isset($this->templates[$className])) {
+            if ($cache === null) {
+                $cache = $this->getCache();
+            }
+
+            if (!class_exists($className, false)) {
+                if (!$cache->load($className)) {
+                    $compiled = $this->compile($source);
+                    $cache->cache($className, $compiled);
+                }
+            }
+
+            $this->log(
+                Mustache_Logger::DEBUG,
+                'Instantiating template: "{className}"',
+                array('className' => $className)
+            );
+
+            $this->templates[$className] = new $className($this);
+        }
+
+        return $this->templates[$className];
+    }
+
+    /**
+     * Helper method to tokenize a Mustache template.
+     *
+     * @see Mustache_Tokenizer::scan
+     *
+     * @param string $source
+     *
+     * @return array Tokens
+     */
+    private function tokenize($source)
+    {
+        return $this->getTokenizer()->scan($source);
+    }
+
+    /**
+     * Helper method to parse a Mustache template.
+     *
+     * @see Mustache_Parser::parse
+     *
+     * @param string $source
+     *
+     * @return array Token tree
+     */
+    private function parse($source)
+    {
+        $parser = $this->getParser();
+        $parser->setPragmas($this->getPragmas());
+
+        return $parser->parse($this->tokenize($source));
+    }
+
+    /**
+     * Helper method to compile a Mustache template.
+     *
+     * @see Mustache_Compiler::compile
+     *
+     * @param string $source
+     *
+     * @return string generated Mustache template class code
+     */
+    private function compile($source)
+    {
+        $tree = $this->parse($source);
+        $name = $this->getTemplateClassName($source);
+
+        $this->log(
+            Mustache_Logger::INFO,
+            'Compiling template to "{className}" class',
+            array('className' => $name)
+        );
+
+        $compiler = $this->getCompiler();
+        $compiler->setPragmas($this->getPragmas());
+
+        return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
+    }
+
+    /**
+     * Add a log record if logging is enabled.
+     *
+     * @param integer $level   The logging level
+     * @param string  $message The log message
+     * @param array   $context The log context
+     */
+    private function log($level, $message, array $context = array())
+    {
+        if (isset($this->logger)) {
+            $this->logger->log($level, $message, $context);
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception.php b/lib/mustache/src/Mustache/Exception.php
new file mode 100644 (file)
index 0000000..8a2b01c
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Exception interface.
+ */
+interface Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/InvalidArgumentException.php b/lib/mustache/src/Mustache/Exception/InvalidArgumentException.php
new file mode 100644 (file)
index 0000000..9bd1107
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Invalid argument exception.
+ */
+class Mustache_Exception_InvalidArgumentException extends InvalidArgumentException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/LogicException.php b/lib/mustache/src/Mustache/Exception/LogicException.php
new file mode 100644 (file)
index 0000000..255ce54
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Logic exception.
+ */
+class Mustache_Exception_LogicException extends LogicException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/RuntimeException.php b/lib/mustache/src/Mustache/Exception/RuntimeException.php
new file mode 100644 (file)
index 0000000..a3c48f7
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Runtime exception.
+ */
+class Mustache_Exception_RuntimeException extends RuntimeException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/SyntaxException.php b/lib/mustache/src/Mustache/Exception/SyntaxException.php
new file mode 100644 (file)
index 0000000..7a16f7e
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache syntax exception.
+ */
+class Mustache_Exception_SyntaxException extends LogicException implements Mustache_Exception
+{
+    protected $token;
+
+    /**
+     * @param string $msg
+     * @param array  $token
+     */
+    public function __construct($msg, array $token)
+    {
+        $this->token = $token;
+        parent::__construct($msg);
+    }
+
+    /**
+     * @return array
+     */
+    public function getToken()
+    {
+        return $this->token;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception/UnknownFilterException.php b/lib/mustache/src/Mustache/Exception/UnknownFilterException.php
new file mode 100644 (file)
index 0000000..b9a315a
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown filter exception.
+ */
+class Mustache_Exception_UnknownFilterException extends UnexpectedValueException implements Mustache_Exception
+{
+    protected $filterName;
+
+    /**
+     * @param string $filterName
+     */
+    public function __construct($filterName)
+    {
+        $this->filterName = $filterName;
+        parent::__construct(sprintf('Unknown filter: %s', $filterName));
+    }
+
+    public function getFilterName()
+    {
+        return $this->filterName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception/UnknownHelperException.php b/lib/mustache/src/Mustache/Exception/UnknownHelperException.php
new file mode 100644 (file)
index 0000000..226d774
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown helper exception.
+ */
+class Mustache_Exception_UnknownHelperException extends InvalidArgumentException implements Mustache_Exception
+{
+    protected $helperName;
+
+    /**
+     * @param string $helperName
+     */
+    public function __construct($helperName)
+    {
+        $this->helperName = $helperName;
+        parent::__construct(sprintf('Unknown helper: %s', $helperName));
+    }
+
+    public function getHelperName()
+    {
+        return $this->helperName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception/UnknownTemplateException.php b/lib/mustache/src/Mustache/Exception/UnknownTemplateException.php
new file mode 100644 (file)
index 0000000..5dafe89
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown template exception.
+ */
+class Mustache_Exception_UnknownTemplateException extends InvalidArgumentException implements Mustache_Exception
+{
+    protected $templateName;
+
+    /**
+     * @param string $templateName
+     */
+    public function __construct($templateName)
+    {
+        $this->templateName = $templateName;
+        parent::__construct(sprintf('Unknown template: %s', $templateName));
+    }
+
+    public function getTemplateName()
+    {
+        return $this->templateName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/HelperCollection.php b/lib/mustache/src/Mustache/HelperCollection.php
new file mode 100644 (file)
index 0000000..c7c7950
--- /dev/null
@@ -0,0 +1,172 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A collection of helpers for a Mustache instance.
+ */
+class Mustache_HelperCollection
+{
+    private $helpers = array();
+
+    /**
+     * Helper Collection constructor.
+     *
+     * Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable
+     *
+     * @param array|Traversable $helpers (default: null)
+     */
+    public function __construct($helpers = null)
+    {
+        if ($helpers === null) {
+            return;
+        }
+
+        if (!is_array($helpers) && !$helpers instanceof Traversable) {
+            throw new Mustache_Exception_InvalidArgumentException('HelperCollection constructor expects an array of helpers');
+        }
+
+        foreach ($helpers as $name => $helper) {
+            $this->add($name, $helper);
+        }
+    }
+
+    /**
+     * Magic mutator.
+     *
+     * @see Mustache_HelperCollection::add
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function __set($name, $helper)
+    {
+        $this->add($name, $helper);
+    }
+
+    /**
+     * Add a helper to this collection.
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function add($name, $helper)
+    {
+        $this->helpers[$name] = $helper;
+    }
+
+    /**
+     * Magic accessor.
+     *
+     * @see Mustache_HelperCollection::get
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function __get($name)
+    {
+        return $this->get($name);
+    }
+
+    /**
+     * Get a helper by name.
+     *
+     * @throws Mustache_Exception_UnknownHelperException If helper does not exist.
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function get($name)
+    {
+        if (!$this->has($name)) {
+            throw new Mustache_Exception_UnknownHelperException($name);
+        }
+
+        return $this->helpers[$name];
+    }
+
+    /**
+     * Magic isset().
+     *
+     * @see Mustache_HelperCollection::has
+     *
+     * @param string $name
+     *
+     * @return boolean True if helper is present
+     */
+    public function __isset($name)
+    {
+        return $this->has($name);
+    }
+
+    /**
+     * Check whether a given helper is present in the collection.
+     *
+     * @param string $name
+     *
+     * @return boolean True if helper is present
+     */
+    public function has($name)
+    {
+        return array_key_exists($name, $this->helpers);
+    }
+
+    /**
+     * Magic unset().
+     *
+     * @see Mustache_HelperCollection::remove
+     *
+     * @param string $name
+     */
+    public function __unset($name)
+    {
+        $this->remove($name);
+    }
+
+    /**
+     * Check whether a given helper is present in the collection.
+     *
+     * @throws Mustache_Exception_UnknownHelperException if the requested helper is not present.
+     *
+     * @param string $name
+     */
+    public function remove($name)
+    {
+        if (!$this->has($name)) {
+            throw new Mustache_Exception_UnknownHelperException($name);
+        }
+
+        unset($this->helpers[$name]);
+    }
+
+    /**
+     * Clear the helper collection.
+     *
+     * Removes all helpers from this collection
+     */
+    public function clear()
+    {
+        $this->helpers = array();
+    }
+
+    /**
+     * Check whether the helper collection is empty.
+     *
+     * @return boolean True if the collection is empty
+     */
+    public function isEmpty()
+    {
+        return empty($this->helpers);
+    }
+}
diff --git a/lib/mustache/src/Mustache/LambdaHelper.php b/lib/mustache/src/Mustache/LambdaHelper.php
new file mode 100644 (file)
index 0000000..7cd8092
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Lambda Helper.
+ *
+ * Passed as the second argument to section lambdas (higher order sections),
+ * giving them access to a `render` method for rendering a string with the
+ * current context.
+ */
+class Mustache_LambdaHelper
+{
+    private $mustache;
+    private $context;
+
+    /**
+     * Mustache Lambda Helper constructor.
+     *
+     * @param Mustache_Engine  $mustache Mustache engine instance.
+     * @param Mustache_Context $context  Rendering context.
+     */
+    public function __construct(Mustache_Engine $mustache, Mustache_Context $context)
+    {
+        $this->mustache = $mustache;
+        $this->context  = $context;
+    }
+
+    /**
+     * Render a string as a Mustache template with the current rendering context.
+     *
+     * @param string $string
+     *
+     * @return string Rendered template.
+     */
+    public function render($string)
+    {
+        return $this->mustache
+            ->loadLambda((string) $string)
+            ->renderInternal($this->context);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader.php b/lib/mustache/src/Mustache/Loader.php
new file mode 100644 (file)
index 0000000..e75ee3f
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template Loader interface.
+ */
+interface Mustache_Loader
+{
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name);
+}
diff --git a/lib/mustache/src/Mustache/Loader/ArrayLoader.php b/lib/mustache/src/Mustache/Loader/ArrayLoader.php
new file mode 100644 (file)
index 0000000..e7ece91
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template array Loader implementation.
+ *
+ * An ArrayLoader instance loads Mustache Template source by name from an initial array:
+ *
+ *     $loader = new ArrayLoader(
+ *         'foo' => '{{ bar }}',
+ *         'baz' => 'Hey {{ qux }}!'
+ *     );
+ *
+ *     $tpl = $loader->load('foo'); // '{{ bar }}'
+ *
+ * The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials
+ * is set. It can also be used as a quick-and-dirty Template loader.
+ */
+class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader
+{
+    private $templates;
+
+    /**
+     * ArrayLoader constructor.
+     *
+     * @param array $templates Associative array of Template source (default: array())
+     */
+    public function __construct(array $templates = array())
+    {
+        $this->templates = $templates;
+    }
+
+    /**
+     * Load a Template.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        if (!isset($this->templates[$name])) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Set an associative array of Template sources for this loader.
+     *
+     * @param array $templates
+     */
+    public function setTemplates(array $templates)
+    {
+        $this->templates = $templates;
+    }
+
+    /**
+     * Set a Template source by name.
+     *
+     * @param string $name
+     * @param string $template Mustache Template source
+     */
+    public function setTemplate($name, $template)
+    {
+        $this->templates[$name] = $template;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader/CascadingLoader.php b/lib/mustache/src/Mustache/Loader/CascadingLoader.php
new file mode 100644 (file)
index 0000000..d02a273
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed&n