Merge branch 'wip-MDL-37983-master' of git://github.com/abgreeve/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 4 Mar 2013 15:42:25 +0000 (16:42 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 4 Mar 2013 15:42:25 +0000 (16:42 +0100)
280 files changed:
.jshintrc
README.txt
admin/tool/behat/cli/util.php
admin/tool/behat/tests/behat/nasty_strings.feature [new file with mode: 0644]
backup/import.php
blocks/community/renderer.php
blocks/html/backup/moodle1/lib.php
blocks/navigation/version.php
blocks/navigation/yui/navigation/navigation.js
blocks/recent_activity/block_recent_activity.php
blocks/rss_client/backup/moodle1/lib.php
blocks/settings/lang/en/block_settings.php
blog/edit_form.php
cache/classes/config.php
cache/classes/definition.php
cache/classes/factory.php
cache/classes/helper.php
cache/classes/loaders.php
cache/locallib.php
cache/stores/memcache/lib.php
cache/stores/memcached/lib.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
calendar/managesubscriptions.php
comment/comment_ajax.php
comment/lib.php
config-dist.php
course/dndupload.js
course/editcategory.php
course/externallib.php
course/lib.php
course/manage.php
course/tests/behat/behat_course.php
course/tests/courselib_test.php
course/tests/externallib_test.php
course/yui/modchooser/modchooser.js
course/yui/toolboxes/toolboxes.js
enrol/cohort/lib.php
enrol/flatfile/adminlib.php [new file with mode: 0644]
enrol/flatfile/db/install.php [new file with mode: 0644]
enrol/flatfile/settings.php
enrol/imsenterprise/lib.php
enrol/imsenterprise/locallib.php
enrol/locallib.php
enrol/manual/ajax.php
enrol/manual/lib.php
enrol/manual/manage.php
enrol/manual/yui/quickenrolment/quickenrolment.js
enrol/paypal/lib.php
enrol/self/lib.php
filter/activitynames/filter.php
grade/edit/outcome/import.php
grade/report/grader/module.js
group/externallib.php
group/groupings.php
group/tests/externallib_test.php
install/lang/he/moodle.php
lang/en/admin.php
lang/en/block.php
lang/en/countries.php
lang/en/grading.php
lang/en/plugin.php
lang/en/rating.php
lang/en/repository.php
lib/behat/behat_base.php
lib/behat/form_field/behat_form_editor.php
lib/blocklib.php
lib/conditionlib.php
lib/csslib.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/dmllib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/filestorage/stored_file.php
lib/filestorage/tests/file_storage_test.php
lib/filterlib.php
lib/form/dndupload.js
lib/form/filemanager.js
lib/form/yui/shortforms/shortforms.js
lib/form/yui/showadvanced/showadvanced.js
lib/google/Google_Client.php [new file with mode: 0644]
lib/google/LICENSE [new file with mode: 0644]
lib/google/NOTICE [new file with mode: 0644]
lib/google/README [new file with mode: 0644]
lib/google/auth/Google_AssertionCredentials.php [new file with mode: 0644]
lib/google/auth/Google_Auth.php [new file with mode: 0644]
lib/google/auth/Google_AuthNone.php [new file with mode: 0644]
lib/google/auth/Google_LoginTicket.php [new file with mode: 0644]
lib/google/auth/Google_OAuth2.php [new file with mode: 0644]
lib/google/auth/Google_P12Signer.php [new file with mode: 0644]
lib/google/auth/Google_PemVerifier.php [new file with mode: 0644]
lib/google/auth/Google_Signer.php [new file with mode: 0644]
lib/google/auth/Google_Verifier.php [new file with mode: 0644]
lib/google/cache/Google_ApcCache.php [new file with mode: 0644]
lib/google/cache/Google_Cache.php [new file with mode: 0644]
lib/google/cache/Google_FileCache.php [new file with mode: 0644]
lib/google/cache/Google_MemcacheCache.php [new file with mode: 0644]
lib/google/config.php [new file with mode: 0644]
lib/google/contrib/Google_AdexchangebuyerService.php [new file with mode: 0644]
lib/google/contrib/Google_AdsenseService.php [new file with mode: 0644]
lib/google/contrib/Google_AdsensehostService.php [new file with mode: 0644]
lib/google/contrib/Google_AnalyticsService.php [new file with mode: 0644]
lib/google/contrib/Google_BigqueryService.php [new file with mode: 0644]
lib/google/contrib/Google_BloggerService.php [new file with mode: 0644]
lib/google/contrib/Google_BooksService.php [new file with mode: 0644]
lib/google/contrib/Google_CalendarService.php [new file with mode: 0644]
lib/google/contrib/Google_ComputeService.php [new file with mode: 0644]
lib/google/contrib/Google_CustomsearchService.php [new file with mode: 0644]
lib/google/contrib/Google_DriveService.php [new file with mode: 0644]
lib/google/contrib/Google_FreebaseService.php [new file with mode: 0644]
lib/google/contrib/Google_FusiontablesService.php [new file with mode: 0644]
lib/google/contrib/Google_GanService.php [new file with mode: 0644]
lib/google/contrib/Google_LatitudeService.php [new file with mode: 0644]
lib/google/contrib/Google_LicensingService.php [new file with mode: 0644]
lib/google/contrib/Google_ModeratorService.php [new file with mode: 0644]
lib/google/contrib/Google_Oauth2Service.php [new file with mode: 0644]
lib/google/contrib/Google_OrkutService.php [new file with mode: 0644]
lib/google/contrib/Google_PagespeedonlineService.php [new file with mode: 0644]
lib/google/contrib/Google_PlusMomentsService.php [new file with mode: 0644]
lib/google/contrib/Google_PlusService.php [new file with mode: 0644]
lib/google/contrib/Google_PredictionService.php [new file with mode: 0644]
lib/google/contrib/Google_ShoppingService.php [new file with mode: 0644]
lib/google/contrib/Google_SiteVerificationService.php [new file with mode: 0644]
lib/google/contrib/Google_StorageService.php [new file with mode: 0644]
lib/google/contrib/Google_TaskqueueService.php [new file with mode: 0644]
lib/google/contrib/Google_TasksService.php [new file with mode: 0644]
lib/google/contrib/Google_TranslateService.php [new file with mode: 0644]
lib/google/contrib/Google_UrlshortenerService.php [new file with mode: 0644]
lib/google/contrib/Google_WebfontsService.php [new file with mode: 0644]
lib/google/contrib/Google_YoutubeService.php [new file with mode: 0644]
lib/google/curlio.php [new file with mode: 0644]
lib/google/external/URITemplateParser.php [new file with mode: 0644]
lib/google/io/Google_CacheParser.php [new file with mode: 0644]
lib/google/io/Google_CurlIO.php [new file with mode: 0644]
lib/google/io/Google_HttpRequest.php [new file with mode: 0644]
lib/google/io/Google_IO.php [new file with mode: 0644]
lib/google/io/Google_REST.php [new file with mode: 0644]
lib/google/io/cacerts.pem [new file with mode: 0644]
lib/google/local_config.php [new file with mode: 0644]
lib/google/readme_moodle.txt [new file with mode: 0644]
lib/google/service/Google_BatchRequest.php [new file with mode: 0644]
lib/google/service/Google_MediaFileUpload.php [new file with mode: 0644]
lib/google/service/Google_Model.php [new file with mode: 0644]
lib/google/service/Google_Service.php [new file with mode: 0644]
lib/google/service/Google_ServiceResource.php [new file with mode: 0644]
lib/google/service/Google_Utils.php [new file with mode: 0644]
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputlib.php
lib/outputrequirementslib.php
lib/pear/Crypt/CHAP.php
lib/phpunit/classes/util.php
lib/pluginlib.php
lib/setup.php
lib/setuplib.php
lib/simplepie/moodle_simplepie.php
lib/testing/classes/nasty_strings.php [new file with mode: 0644]
lib/testing/generator/data_generator.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/behat_navigation.php
lib/tests/behat/behat_transformations.php [new file with mode: 0644]
lib/tests/conditionlib_test.php
lib/tests/filelib_test.php
lib/tests/setuplib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
lib/xhprof/readme_moodle.txt
lib/xhprof/xhprof_html/callgraph.php
lib/xhprof/xhprof_html/css/xhprof.css
lib/xhprof/xhprof_html/index.php
lib/xhprof/xhprof_html/jquery/indicator.gif
lib/xhprof/xhprof_html/typeahead.php
lib/xhprof/xhprof_lib/utils/callgraph_utils.php
lib/xhprof/xhprof_lib/utils/xhprof_runs.php
mod/assign/externallib.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/renderable.php
mod/assign/renderer.php
mod/assign/styles.css
mod/assign/submission/comments/locallib.php
mod/assign/submission/file/locallib.php
mod/assign/tests/base_test.php [new file with mode: 0644]
mod/assign/tests/generator/lib.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/assign/tests/upgradelib_test.php
mod/assign/view.php
mod/assignment/type/upload/assignment.class.php
mod/data/field/latlong/field.class.php
mod/data/import.php
mod/data/renderer.php
mod/folder/backup/moodle2/backup_folder_stepslib.php
mod/folder/db/install.xml
mod/folder/db/upgrade.php
mod/folder/edit.php
mod/folder/lang/en/folder.php
mod/folder/lib.php
mod/folder/mod_form.php
mod/folder/module.js
mod/folder/renderer.php
mod/folder/version.php
mod/folder/view.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/search.php
mod/forum/subscribe.php
mod/label/db/upgrade.php
mod/label/lib.php
mod/label/version.php
mod/lesson/report.php
mod/lti/locallib.php
mod/quiz/accessrule/openclosedate/rule.php
mod/quiz/accessrule/openclosedate/tests/rule_test.php
mod/quiz/lib.php
mod/quiz/mod_form.php
mod/quiz/styles.css
mod/quiz/version.php
mod/scorm/lang/en/scorm.php
mod/scorm/mod_form.php
mod/scorm/report/basic/report.php
mod/scorm/report/graphs/graph.php
mod/scorm/report/interactions/report.php
mod/workshop/form/assessment_form.php
notes/delete.php
notes/edit.php
notes/externallib.php
notes/lib.php
notes/tests/externallib_test.php
question/format/blackboard/tests/fixtures/sample_blackboard.dat
question/format/blackboard_six/formatbase.php
question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat
question/format/xml/format.php
question/format/xml/tests/fixtures/truefalse.xml
report/courseoverview/index.php
report/courseoverview/settings.php
report/log/locallib.php
repository/coursefiles/lib.php
repository/equella/lib.php
repository/filepicker.js
repository/filepicker.php
repository/filesystem/lib.php
repository/flickr_public/lib.php
repository/googledocs/lang/en/repository_googledocs.php
repository/googledocs/lib.php
repository/googledocs/version.php
repository/lib.php
repository/local/lib.php
repository/merlot/lib.php
repository/recent/lib.php
repository/s3/lib.php
repository/tests/repository_test.php
repository/upgrade.txt
repository/upload/lib.php
repository/url/lib.php
repository/user/lib.php
repository/webdav/lib.php
repository/wikimedia/lib.php
repository/youtube/lib.php
tag/coursetagslib.php
theme/base/style/admin.css
theme/base/style/core.css
theme/base/version.php
theme/formal_white/lib.php
theme/formal_white/style/formal_white.css
user/addnote.php
user/externallib.php
user/groupaddnote.php
user/tests/externallib_test.php
user/view.php
version.php

index 9b833d9..06de646 100644 (file)
--- a/.jshintrc
+++ b/.jshintrc
@@ -1,6 +1,5 @@
 {
     "browser":      true,
-    "node":         true,
     "yui":          true,
     "bitwise":      true,
     "curly":        true,
index 245848b..b1017df 100644 (file)
@@ -8,7 +8,7 @@ a few minutes:
 1) Move the Moodle files into your web directory.
 
 2) Create a single database for Moodle to store all
-   it's tables in (or choose an existing database).
+   its tables in (or choose an existing database).
 
 3) Visit your Moodle site with a browser, you should
    be taken to the install.php script, which will lead
index cfd3581..8cba367 100644 (file)
@@ -84,6 +84,7 @@ error_reporting(E_ALL | E_STRICT);
 ini_set('display_errors', '1');
 ini_set('log_errors', '1');
 
+// Getting $CFG data.
 require_once(__DIR__ . '/../../../../config.php');
 
 // CFG->behat_prefix must be set and with value different than CFG->prefix and phpunit_prefix.
@@ -141,6 +142,10 @@ foreach ($vars as $var) {
 $CFG->noemailever = true;
 $CFG->passwordsaltmain = 'moodle';
 
+// Unset cache and temp directories to reset them again with the new $CFG->dataroot.
+unset($CFG->cachedir);
+unset($CFG->tempdir);
+
 // Continues setup.
 define('ABORT_AFTER_CONFIG_CANCEL', true);
 require("$CFG->dirroot/lib/setup.php");
diff --git a/admin/tool/behat/tests/behat/nasty_strings.feature b/admin/tool/behat/tests/behat/nasty_strings.feature
new file mode 100644 (file)
index 0000000..9135798
--- /dev/null
@@ -0,0 +1,60 @@
+@tool_behat
+Feature: Transform steps arguments
+  In order to write tests with complex nasty arguments
+  As a tests writer
+  I need to apply some transformations to the steps arguments
+
+  Background:
+    Given I am on homepage
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And I log in as "admin"
+    And I follow "Admin User"
+    And I follow "Edit profile"
+
+  Scenario: Use nasty strings on steps arguments
+    When I fill in "Surname" with "$NASTYSTRING1"
+    And I fill in "Description" with "$NASTYSTRING2"
+    And I fill in "City/town" with "$NASTYSTRING3"
+    And I press "Update profile"
+    And I follow "Edit profile"
+    Then I should not see "NASTYSTRING"
+    And the "Surname" field should match "$NASTYSTRING1" value
+    And the "City/town" field should match "$NASTYSTRING3" value
+
+  Scenario: Use nasty strings on table nodes
+    When I fill the moodle form with:
+      | Surname | $NASTYSTRING1 |
+      | Description | $NASTYSTRING2 |
+      | City/town | $NASTYSTRING3 |
+    And I press "Update profile"
+    And I follow "Edit profile"
+    Then I should not see "NASTYSTRING"
+    And the "Surname" field should match "$NASTYSTRING1" value
+    And the "City/town" field should match "$NASTYSTRING3" value
+
+  Scenario: Use double quotes
+    When I fill the moodle form with:
+      | First name | va"lue1 |
+      | Description | va\"lue2 |
+    And I fill in "City/town" with "va\"lue3"
+    And I press "Update profile"
+    And I follow "Edit profile"
+    Then I should not see "NASTYSTRING"
+    And the "First name" field should match "va\"lue1" value
+    And the "Description" field should match "va\"lue2" value
+    And the "City/town" field should match "va\"lue3" value
+
+  @javascript
+  Scenario: Nasty strings with other contents
+    When I fill in "First name" with "My Firstname $NASTYSTRING1"
+    And I fill the moodle form with:
+      | Surname | My Surname $NASTYSTRING2 |
+    And I press "Update profile"
+    And I follow "Edit profile"
+    Then I should not see "NASTYSTRING"
+    And I should see "My Firstname"
+    And I should see "My Surname"
+    And the "First name" field should match "My Firstname $NASTYSTRING1" value
+    And the "Surname" field should match "My Surname $NASTYSTRING2" value
index 4de77b2..6dd5038 100644 (file)
@@ -11,6 +11,8 @@ require_once($CFG->dirroot . '/backup/util/ui/import_extensions.php');
 $courseid = required_param('id', PARAM_INT);
 // The id of the course we are importing FROM (will only be set if past first stage
 $importcourseid = optional_param('importid', false, PARAM_INT);
+// We just want to check if a search has been run. True if anything is there.
+$searchcourses = optional_param('searchcourses', false, PARAM_BOOL);
 // The target method for the restore (adding or deleting)
 $restoretarget = optional_param('target', backup::TARGET_CURRENT_ADDING, PARAM_INT);
 
@@ -36,7 +38,7 @@ $PAGE->set_pagelayout('incourse');
 $renderer = $PAGE->get_renderer('core','backup');
 
 // Check if we already have a import course id
-if ($importcourseid === false) {
+if ($importcourseid === false || $searchcourses) {
     // Obviously not... show the selector so one can be chosen
     $url = new moodle_url('/backup/import.php', array('id'=>$courseid));
     $search = new import_course_search(array('url'=>$url));
@@ -160,4 +162,4 @@ echo $renderer->progress_bar($backup->get_progress_bar());
 echo $backup->display($renderer);
 $backup->destroy();
 unset($backup);
-echo $OUTPUT->footer();
\ No newline at end of file
+echo $OUTPUT->footer();
index 4fb6c50..dca9839 100644 (file)
@@ -272,7 +272,7 @@ class block_community_renderer extends plugin_renderer_base {
                         'downloadcourseid' => $course->id, 'huburl' => $huburl,
                         'coursefullname' => $course->fullname, 'backupsize' => $course->backupsize);
                     $downloadurl = new moodle_url("/blocks/community/communitycourse.php", $params);
-                    $downloadbuttonhtml = html_writer::tag('a', get_string('download', 'block_community'),
+                    $downloadbuttonhtml = html_writer::tag('a', get_string('install', 'block_community'),
                                     array('href' => $downloadurl, 'class' => 'centeredbutton, hubcoursedownload'));
                 }
 
index d4a491f..6373a01 100644 (file)
@@ -1,46 +1,46 @@
-<?php\r
-\r
-/**\r
- * Provides support for the conversion of moodle1 backup to the moodle2 format\r
- *\r
- * @package    block_html\r
- * @copyright  2012 Paul Nicholls\r
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r
- */\r
-\r
-defined('MOODLE_INTERNAL') || die();\r
-\r
-/**\r
- * Block conversion handler for html\r
- */\r
-class moodle1_block_html_handler extends moodle1_block_handler {\r
-    private $fileman = null;\r
-    protected function convert_configdata(array $olddata) {\r
-        $instanceid = $olddata['id'];\r
-        $contextid  = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']);\r
-        $configdata = unserialize(base64_decode($olddata['configdata']));\r
-\r
-        // get a fresh new file manager for this instance\r
-        $this->fileman = $this->converter->get_file_manager($contextid, 'block_html');\r
-\r
-        // convert course files embedded in the block content\r
-        $this->fileman->filearea = 'content';\r
-        $this->fileman->itemid   = 0;\r
-        $configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman);\r
-        $configdata->format = FORMAT_HTML;\r
-\r
-        return base64_encode(serialize($configdata));\r
-    }\r
-\r
-    protected function write_inforef_xml($newdata, $data) {\r
-        $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml");\r
-        $this->xmlwriter->begin_tag('inforef');\r
-        $this->xmlwriter->begin_tag('fileref');\r
-        foreach ($this->fileman->get_fileids() as $fileid) {\r
-            $this->write_xml('file', array('id' => $fileid));\r
-        }\r
-        $this->xmlwriter->end_tag('fileref');\r
-        $this->xmlwriter->end_tag('inforef');\r
-        $this->close_xml_writer();\r
-    }\r
-}
\ No newline at end of file
+<?php
+
+/**
+ * Provides support for the conversion of moodle1 backup to the moodle2 format
+ *
+ * @package    block_html
+ * @copyright  2012 Paul Nicholls
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Block conversion handler for html
+ */
+class moodle1_block_html_handler extends moodle1_block_handler {
+    private $fileman = null;
+    protected function convert_configdata(array $olddata) {
+        $instanceid = $olddata['id'];
+        $contextid  = $this->converter->get_contextid(CONTEXT_BLOCK, $olddata['id']);
+        $configdata = unserialize(base64_decode($olddata['configdata']));
+
+        // get a fresh new file manager for this instance
+        $this->fileman = $this->converter->get_file_manager($contextid, 'block_html');
+
+        // convert course files embedded in the block content
+        $this->fileman->filearea = 'content';
+        $this->fileman->itemid   = 0;
+        $configdata->text = moodle1_converter::migrate_referenced_files($configdata->text, $this->fileman);
+        $configdata->format = FORMAT_HTML;
+
+        return base64_encode(serialize($configdata));
+    }
+
+    protected function write_inforef_xml($newdata, $data) {
+        $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml");
+        $this->xmlwriter->begin_tag('inforef');
+        $this->xmlwriter->begin_tag('fileref');
+        foreach ($this->fileman->get_fileids() as $fileid) {
+            $this->write_xml('file', array('id' => $fileid));
+        }
+        $this->xmlwriter->end_tag('fileref');
+        $this->xmlwriter->end_tag('inforef');
+        $this->close_xml_writer();
+    }
+}
index 70d6054..7fdea6b 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012112900;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2013020800;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2012112900;        // Requires this Moodle version
 $plugin->component = 'block_navigation'; // Full name of the plugin (used for diagnostics)
index 62f417e..b513e56 100644 (file)
@@ -86,6 +86,8 @@ var NODETYPE = {
     SYSTEM : 1,
     /** @type int Course category = 10 */
     CATEGORY : 10,
+    /** @type int MYCATEGORY = 11 */
+    MYCATEGORY : 11,
     /** @type int Course = 20 */
     COURSE : 20,
     /** @type int Course section = 30 */
@@ -118,6 +120,10 @@ TREE.prototype = {
      * The tree's ID, normally its block instance id.
      */
     id : null,
+    /**
+     * An array of initialised branches.
+     */
+    branches : [],
     /**
      * Initialise the tree object when its first created.
      */
@@ -132,9 +138,8 @@ TREE.prototype = {
         }
 
         // Delegate event to toggle expansion
-        var self = this;
-        Y.delegate('click', function(e){self.toggleExpansion(e);}, node.one('.block_tree'), '.tree_item.branch');
-        Y.delegate('actionkey', function(e){self.toggleExpansion(e);}, node.one('.block_tree'), '.tree_item.branch');
+        Y.delegate('click', this.toggleExpansion, node.one('.block_tree'), '.tree_item.branch', this);
+        Y.delegate('actionkey', this.toggleExpansion, node.one('.block_tree'), '.tree_item.branch', this);
 
         // Gather the expandable branches ready for initialisation.
         var expansions = [];
@@ -145,7 +150,7 @@ TREE.prototype = {
         }
         // Establish each expandable branch as a tree branch.
         for (var i in expansions) {
-            new BRANCH({
+            var branch = new BRANCH({
                 tree:this,
                 branchobj:expansions[i],
                 overrides : {
@@ -155,6 +160,12 @@ TREE.prototype = {
                 }
             }).wire();
             M.block_navigation.expandablebranchcount++;
+            this.branches[branch.get('id')] = branch;
+        }
+        if (M.block_navigation.expandablebranchcount > 0) {
+            // Delegate some events to handle AJAX loading.
+            Y.delegate('click', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this);
+            Y.delegate('actionkey', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this);
         }
 
         // Call the generic blocks init method to add all the generic stuff
@@ -162,6 +173,14 @@ TREE.prototype = {
             this.initialise_block(Y, node);
         }
     },
+    /**
+     * Fire actions for a branch when an event occurs.
+     */
+    fire_branch_action : function(event) {
+        var id = event.currentTarget.getAttribute('id');
+        var branch = this.branches[id];
+        branch.ajaxLoad(event);
+    },
     /**
      * This is a callback function responsible for expanding and collapsing the
      * branches of the tree. It is delegated to rather than multiple event handles.
@@ -269,11 +288,6 @@ BRANCH.prototype = {
      * The node for this branch (p)
      */
     node : null,
-    /**
-     * A reference to the ajax load event handlers when created.
-     */
-    event_ajaxload : null,
-    event_ajaxload_actionkey : null,
     /**
      * Initialises the branch when it is first created.
      */
@@ -376,8 +390,8 @@ BRANCH.prototype = {
             return false;
         }
         if (this.get('expandable')) {
-            this.event_ajaxload = this.node.on('ajaxload|click', this.ajaxLoad, this);
-            this.event_ajaxload_actionkey = this.node.on('actionkey', this.ajaxLoad, this);
+            this.node.setAttribute('data-expandable', '1');
+            this.node.setAttribute('data-loaded', '0');
         }
         return this;
     },
@@ -405,15 +419,21 @@ BRANCH.prototype = {
             e.stopPropagation();
         }
         if (e.type = 'actionkey' && e.action == 'enter' && e.target.test('A')) {
-            this.event_ajaxload_actionkey.detach();
-            this.event_ajaxload.detach();
-            return true; // no ajaxLoad for enter
+            // No ajaxLoad for enter.
+            this.node.setAttribute('data-expandable', '0');
+            this.node.setAttribute('data-loaded', '1');
+            return true;
         }
 
         if (this.node.hasClass('loadingbranch')) {
+            // Already loading. Just skip.
             return true;
         }
 
+        if (this.node.getAttribute('data-loaded') === '1') {
+            // We've already loaded this stuff.
+            return true;
+        }
         this.node.addClass('loadingbranch');
 
         var params = {
@@ -440,8 +460,7 @@ BRANCH.prototype = {
      */
     ajaxProcessResponse : function(tid, outcome) {
         this.node.removeClass('loadingbranch');
-        this.event_ajaxload.detach();
-        this.event_ajaxload_actionkey.detach();
+        this.node.setAttribute('data-loaded', '1');
         try {
             var object = Y.JSON.parse(outcome.responseText);
             if (object.children && object.children.length > 0) {
@@ -454,10 +473,10 @@ BRANCH.prototype = {
                         this.addChild(object.children[i]);
                     }
                 }
-                if ((this.get('type') == NODETYPE.CATEGORY || this.get('type') == NODETYPE.ROOTNODE) && coursecount >= M.block_navigation.courselimit) {
+                if ((this.get('type') == NODETYPE.CATEGORY || this.get('type') == NODETYPE.ROOTNODE || this.get('type') == NODETYPE.MYCATEGORY)
+                    && coursecount >= M.block_navigation.courselimit) {
                     this.addViewAllCoursesChild(this);
                 }
-                this.get('tree').toggleExpansion({target:this.node});
                 return true;
             }
         } catch (ex) {
@@ -475,6 +494,7 @@ BRANCH.prototype = {
         // Make the new branch into an object
         var branch = new BRANCH({tree:this.get('tree'), branchobj:branchobj});
         if (branch.draw(this.getChildrenUL())) {
+            this.get('tree').branches[branch.get('id')] = branch;
             branch.wire();
             var count = 0, i, children = branch.get('children');
             for (i in children) {
@@ -486,7 +506,8 @@ BRANCH.prototype = {
                     branch.addChild(children[i]);
                 }
             }
-            if (branch.get('type') == NODETYPE.CATEGORY && count >= M.block_navigation.courselimit) {
+            if ((branch.get('type') == NODETYPE.CATEGORY || branch.get('type') == NODETYPE.MYCATEGORY)
+                && count >= M.block_navigation.courselimit) {
                 this.addViewAllCoursesChild(branch);
             }
         }
index 846ab98..b62eefb 100644 (file)
@@ -113,7 +113,8 @@ class block_recent_activity extends block_base {
      * Returns list of recent changes in course structure
      *
      * It includes adding, editing or deleting of the resources or activities
-     * Excludes changes on labels, and also if activity was both added and deleted
+     * Excludes changes on modules without a view link (i.e. labels), and also
+     * if activity was both added and deleted
      *
      * @return array array of changes. Each element is an array containing attributes:
      *    'action' - one of: 'add mod', 'update mod', 'delete mod'
@@ -135,13 +136,6 @@ class block_recent_activity extends block_base {
             foreach ($logs as $key => $log) {
                 $info = explode(' ', $log->info);
 
-                // note: in most cases I replaced hardcoding of label with use of
-                // $cm->has_view() but it was not possible to do this here because
-                // we don't necessarily have the $cm for it
-                if ($info[0] == 'label') {     // Labels are ignored in recent activity
-                    continue;
-                }
-
                 if (count($info) != 2) {
                     debugging("Incorrect log entry info: id = ".$log->id, DEBUG_DEVELOPER);
                     continue;
@@ -151,6 +145,11 @@ class block_recent_activity extends block_base {
                 $instanceid = $info[1];
 
                 if ($log->action == 'delete mod') {
+                    if (plugin_supports('mod', $modname, FEATURE_NO_VIEW_LINK, false)) {
+                        // we should better call cm_info::has_view() because it can be
+                        // dynamic. But there is no instance of cm_info now
+                        continue;
+                    }
                     // unfortunately we do not know if the mod was visible
                     if (!array_key_exists($log->info, $newgones)) {
                         $changelist[$log->info] = array('action' => $log->action,
@@ -168,7 +167,7 @@ class block_recent_activity extends block_base {
                         continue;
                     }
                     $cm = $modinfo->instances[$modname][$instanceid];
-                    if ($cm->uservisible && empty($changelist[$log->info])) {
+                    if ($cm->has_view() && $cm->uservisible && empty($changelist[$log->info])) {
                         $changelist[$log->info] = array('action' => $log->action, 'module' => $cm);
                     }
                 }
index 95da634..d698365 100644 (file)
@@ -1,34 +1,34 @@
-<?php\r
-\r
-/**\r
- * Provides support for the conversion of moodle1 backup to the moodle2 format\r
- *\r
- * @package    block_rss_client\r
- * @copyright  2012 Paul Nicholls\r
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r
- */\r
-\r
-defined('MOODLE_INTERNAL') || die();\r
-\r
-/**\r
- * Block conversion handler for rss_client\r
- */\r
-class moodle1_block_rss_client_handler extends moodle1_block_handler {\r
-    public function process_block(array $data) {\r
-        parent::process_block($data);\r
-        $instanceid = $data['id'];\r
-        $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);\r
-\r
-        // Moodle 1.9 backups do not include sufficient data to restore feeds, so we need an empty shell rss_client.xml\r
-        // for the restore process to find\r
-        $this->open_xml_writer("course/blocks/{$data['name']}_{$instanceid}/rss_client.xml");\r
-        $this->xmlwriter->begin_tag('block', array('id' => $instanceid, 'contextid' => $contextid, 'blockname' => 'rss_client'));\r
-        $this->xmlwriter->begin_tag('rss_client', array('id' => $instanceid));\r
-        $this->xmlwriter->full_tag('feeds', '');\r
-        $this->xmlwriter->end_tag('rss_client');\r
-        $this->xmlwriter->end_tag('block');\r
-        $this->close_xml_writer();\r
-\r
-        return $data;\r
-    }\r
-}
\ No newline at end of file
+<?php
+
+/**
+ * Provides support for the conversion of moodle1 backup to the moodle2 format
+ *
+ * @package    block_rss_client
+ * @copyright  2012 Paul Nicholls
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Block conversion handler for rss_client
+ */
+class moodle1_block_rss_client_handler extends moodle1_block_handler {
+    public function process_block(array $data) {
+        parent::process_block($data);
+        $instanceid = $data['id'];
+        $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);
+
+        // Moodle 1.9 backups do not include sufficient data to restore feeds, so we need an empty shell rss_client.xml
+        // for the restore process to find
+        $this->open_xml_writer("course/blocks/{$data['name']}_{$instanceid}/rss_client.xml");
+        $this->xmlwriter->begin_tag('block', array('id' => $instanceid, 'contextid' => $contextid, 'blockname' => 'rss_client'));
+        $this->xmlwriter->begin_tag('rss_client', array('id' => $instanceid));
+        $this->xmlwriter->full_tag('feeds', '');
+        $this->xmlwriter->end_tag('rss_client');
+        $this->xmlwriter->end_tag('block');
+        $this->close_xml_writer();
+
+        return $data;
+    }
+}
index 3563843..2ac37aa 100644 (file)
@@ -25,6 +25,6 @@
  */
 
 $string['enabledock'] = 'Allow the user to dock this block';
-$string['pluginname'] = 'Settings';
-$string['settings:addinstance'] = 'Add a new settings block';
-$string['settings:myaddinstance'] = 'Add a new settings block to the My Moodle page';
+$string['pluginname'] = 'Administration';
+$string['settings:addinstance'] = 'Add a new administration block';
+$string['settings:myaddinstance'] = 'Add a new administration block to the My Moodle page';
index 919fed1..42ab6e3 100644 (file)
@@ -87,8 +87,8 @@ class blog_edit_form extends moodleform {
                 }
 
                 if (has_capability('moodle/blog:associatecourse', $context)) {
-                    $mform->addElement('header', 'assochdr', get_string('associations', 'blog'));\r
-                    $mform->addElement('advcheckbox', 'courseassoc', get_string('associatewithcourse', 'blog', $a), null, null, array(0, $contextid));\r
+                    $mform->addElement('header', 'assochdr', get_string('associations', 'blog'));
+                    $mform->addElement('advcheckbox', 'courseassoc', get_string('associatewithcourse', 'blog', $a), null, null, array(0, $contextid));
                     $mform->setDefault('courseassoc', $contextid);
                 }
 
index fee8aea..232aa46 100644 (file)
@@ -71,6 +71,12 @@ class cache_config {
      */
     protected $configlocks = array();
 
+    /**
+     * The site identifier used when the cache config was last saved.
+     * @var string
+     */
+    protected $siteidentifier = null;
+
     /**
      * Please use cache_config::instance to get an instance of the cache config that is ready to be used.
      */
@@ -139,6 +145,12 @@ class cache_config {
         $this->configdefinitionmappings = array();
         $this->configlockmappings = array();
 
+        $siteidentifier = 'unknown';
+        if (array_key_exists('siteidentifier', $configuration)) {
+            $siteidentifier = $configuration['siteidentifier'];
+        }
+        $this->siteidentifier = $siteidentifier;
+
         // Filter the lock instances.
         $defaultlock = null;
         foreach ($configuration['locks'] as $conf) {
@@ -271,6 +283,14 @@ class cache_config {
         return true;
     }
 
+    /**
+     * Returns the site identifier used by the cache API.
+     * @return string
+     */
+    public function get_site_identifier() {
+        return $this->siteidentifier;
+    }
+
     /**
      * Includes the configuration file and makes sure it contains the expected bits.
      *
index e278bbb..8c5a16a 100644 (file)
@@ -723,7 +723,8 @@ class cache_definition {
      */
     public function generate_single_key_prefix() {
         if ($this->keyprefixsingle === null) {
-            $this->keyprefixsingle = $this->mode.'/'.$this->mode;
+            $this->keyprefixsingle = $this->mode.'/'.$this->component.'/'.$this->area;
+            $this->keyprefixsingle .= '/'.$this->get_cache_identifier();
             $identifiers = $this->get_identifiers();
             if ($identifiers) {
                 foreach ($identifiers as $key => $value) {
@@ -746,6 +747,7 @@ class cache_definition {
                 'mode' => $this->mode,
                 'component' => $this->component,
                 'area' => $this->area,
+                'siteidentifier' => $this->get_cache_identifier()
             );
             if (!empty($this->identifiers)) {
                 $identifiers = array();
@@ -785,4 +787,13 @@ class cache_definition {
     public function get_invalidation_events() {
         return $this->invalidationevents;
     }
+
+    /**
+     * Returns a cache identification string.
+     *
+     * @return string A string to be used as part of keys.
+     */
+    protected function get_cache_identifier() {
+        return cache_helper::get_site_identifier();
+    }
 }
\ No newline at end of file
index f3d520c..43f075b 100644 (file)
@@ -75,6 +75,12 @@ class cache_factory {
      */
     protected $cachesfromparams = array();
 
+    /**
+     * An array of stores organised by definitions.
+     * @var array
+     */
+    protected $definitionstores = array();
+
     /**
      * An array of instantiated stores.
      * @var array
@@ -203,6 +209,7 @@ class cache_factory {
         }
         // Get the class. Note this is a late static binding so we need to use get_called_class.
         $definition = cache_definition::load_adhoc($mode, $component, $area, $options);
+        $config = $this->create_config_instance();
         $definition->set_identifiers($identifiers);
         $cache = $this->create_cache($definition, $identifiers);
         if ($definition->should_be_persistent()) {
@@ -271,9 +278,27 @@ class cache_factory {
         // order to address the issues.
         $store = $this->stores[$name]->create_clone($details);
         $store->initialise($definition);
+        $definitionid = $definition->get_id();
+        if (!isset($this->definitionstores[$definitionid])) {
+            $this->definitionstores[$definitionid] = array();
+        }
+        $this->definitionstores[$definitionid][] = $store;
         return $store;
     }
 
+    /**
+     * Returns an array of cache stores that have been initialised for use in definitions.
+     * @param cache_definition $definition
+     * @return array
+     */
+    public function get_store_instances_in_use(cache_definition $definition) {
+        $id = $definition->get_id();
+        if (!isset($this->definitionstores[$id])) {
+            return array();
+        }
+        return $this->definitionstores[$id];
+    }
+
     /**
      * Creates a cache config instance with the ability to write if required.
      *
index 4439a56..20e2f6d 100644 (file)
@@ -54,6 +54,13 @@ class cache_helper {
      */
     protected static $instance;
 
+    /**
+     * The site identifier used by the cache.
+     * Set the first time get_site_identifier is called.
+     * @var string
+     */
+    protected static $siteidentifier = null;
+
     /**
      * Returns true if the cache API can be initialised before Moodle has finished initialising itself.
      *
@@ -305,15 +312,18 @@ class cache_helper {
         foreach ($instance->get_definitions() as $name => $definitionarr) {
             $definition = cache_definition::load($name, $definitionarr);
             if ($definition->invalidates_on_event($event)) {
-                // Create the cache.
-                $cache = $factory->create_cache($definition);
-                // Initialise, in case of a store.
-                if ($cache instanceof cache_store) {
-                    $cache->initialise($definition);
+                // Check if this definition would result in a persistent loader being in use.
+                if ($definition->should_be_persistent()) {
+                    // There may be a persistent cache loader. Lets purge that first so that any persistent data is removed.
+                    $cache = $factory->create_cache_from_definition($definition->get_component(), $definition->get_area());
+                    $cache->purge();
+                }
+                // Get all of the store instances that are in use for this store.
+                $stores = $factory->get_store_instances_in_use($definition);
+                foreach ($stores as $store) {
+                    // Purge each store individually.
+                    $store->purge();
                 }
-                // Purge the cache.
-                $cache->purge();
-
                 // We need to flag the event in the "Event invalidation" cache if it hasn't already happened.
                 if ($invalidationeventset === false) {
                     // Get the event invalidation cache.
@@ -489,11 +499,52 @@ class cache_helper {
      */
     public static function update_definitions($coreonly = false) {
         global $CFG;
-        // Include locallib
+        // Include locallib.
         require_once($CFG->dirroot.'/cache/locallib.php');
         // First update definitions
         cache_config_writer::update_definitions($coreonly);
         // Second reset anything we have already initialised to ensure we're all up to date.
         cache_factory::reset();
     }
-}
\ No newline at end of file
+
+    /**
+     * Update the site identifier stored by the cache API.
+     *
+     * @param string $siteidentifier
+     */
+    public static function update_site_identifier($siteidentifier) {
+        global $CFG;
+        // Include locallib.
+        require_once($CFG->dirroot.'/cache/locallib.php');
+        $factory = cache_factory::instance();
+        $factory->updating_started();
+        $config = $factory->create_config_instance(true);
+        $config->update_site_identifier($siteidentifier);
+        $factory->updating_finished();
+        cache_factory::reset();
+    }
+
+    /**
+     * Returns the site identifier.
+     *
+     * @return string
+     */
+    public static function get_site_identifier() {
+        if (is_null(self::$siteidentifier)) {
+            $factory = cache_factory::instance();
+            $config = $factory->create_config_instance();
+            self::$siteidentifier = $config->get_site_identifier();
+        }
+        return self::$siteidentifier;
+    }
+
+    /**
+     * Returns the site version.
+     *
+     * @return string
+     */
+    public static function get_site_version() {
+        global $CFG;
+        return (string)$CFG->version;
+    }
+}
index 4835360..6402e2c 100644 (file)
@@ -1094,10 +1094,11 @@ class cache_application extends cache implements cache_loader_with_locking {
             $cache = cache::make('core', 'eventinvalidation');
             $events = $cache->get_many($definition->get_invalidation_events());
             $todelete = array();
+            $purgeall = false;
             // Iterate the returned data for the events.
             foreach ($events as $event => $keys) {
                 if ($keys === false) {
-                    // There are no keys.
+                    // No data to be invalidated yet.
                     continue;
                 }
                 // Look at each key and check the timestamp.
@@ -1105,11 +1106,18 @@ class cache_application extends cache implements cache_loader_with_locking {
                     // If the timestamp of the event is more than or equal to the last invalidation (happened between the last
                     // invalidation and now)then we need to invaliate the key.
                     if ($timestamp >= $lastinvalidation) {
-                        $todelete[] = $key;
+                        if ($key === 'purged') {
+                            $purgeall = true;
+                            break;
+                        } else {
+                            $todelete[] = $key;
+                        }
                     }
                 }
             }
-            if (!empty($todelete)) {
+            if ($purgeall) {
+                $this->purge();
+            } else if (!empty($todelete)) {
                 $todelete = array_unique($todelete);
                 $this->delete_many($todelete);
             }
@@ -1435,6 +1443,7 @@ class cache_session extends cache {
             $cache = cache::make('core', 'eventinvalidation');
             $events = $cache->get_many($definition->get_invalidation_events());
             $todelete = array();
+            $purgeall = false;
             // Iterate the returned data for the events.
             foreach ($events as $event => $keys) {
                 if ($keys === false) {
@@ -1446,11 +1455,18 @@ class cache_session extends cache {
                     // If the timestamp of the event is more than or equal to the last invalidation (happened between the last
                     // invalidation and now)then we need to invaliate the key.
                     if ($timestamp >= $lastinvalidation) {
-                        $todelete[] = $key;
+                        if ($key === 'purged') {
+                            $purgeall = true;
+                            break;
+                        } else {
+                            $todelete[] = $key;
+                        }
                     }
                 }
             }
-            if (!empty($todelete)) {
+            if ($purgeall) {
+                $this->purge();
+            } else if (!empty($todelete)) {
                 $todelete = array_unique($todelete);
                 $this->delete_many($todelete);
             }
index ce926a5..4d6d745 100644 (file)
@@ -119,6 +119,7 @@ class cache_config_writer extends cache_config {
      */
     protected function generate_configuration_array() {
         $configuration = array();
+        $configuration['siteidentifier'] = $this->siteidentifier;
         $configuration['stores'] = $this->configstores;
         $configuration['modemappings'] = $this->configmodemappings;
         $configuration['definitions'] = $this->configdefinitions;
@@ -524,6 +525,15 @@ class cache_config_writer extends cache_config {
         $this->config_save();
     }
 
+    /**
+     * Update the site identifier stored by the cache API.
+     *
+     * @param string $siteidentifier
+     */
+    public function update_site_identifier($siteidentifier) {
+        $this->siteidentifier = md5((string)$siteidentifier);
+        $this->config_save();
+    }
 }
 
 /**
@@ -1002,4 +1012,4 @@ abstract class cache_administration_helper extends cache_helper {
         }
         return $locks;
     }
-}
\ No newline at end of file
+}
index 193f5c7..906b355 100644 (file)
@@ -116,8 +116,8 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
         foreach ($this->servers as $server) {
             $this->connection->addServer($server[0], $server[1], true, $server[2]);
             // Test the connection to this server.
-            $this->isready = @$this->connection->set("ping", 'ping', MEMCACHE_COMPRESSED, 1);
         }
+        $this->isready = @$this->connection->set($this->parse_key('ping'), 'ping', MEMCACHE_COMPRESSED, 1);
     }
 
     /**
@@ -191,6 +191,20 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
         return self::MODE_APPLICATION + self::MODE_SESSION;
     }
 
+    /**
+     * Parses the given key to make it work for this memcache backend.
+     *
+     * @param string $key The raw key.
+     * @return string The resulting key.
+     */
+    protected function parse_key($key) {
+        if (strlen($key) > 245) {
+            $key = '_sha1_'.sha1($key);
+        }
+        $key = 'mdl_'.$key;
+        return $key;
+    }
+
     /**
      * Retrieves an item from the cache store given its key.
      *
@@ -198,7 +212,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return mixed The data that was associated with the key, or false if the key did not exist.
      */
     public function get($key) {
-        return $this->connection->get($key);
+        return $this->connection->get($this->parse_key($key));
     }
 
     /**
@@ -211,16 +225,23 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      *      be set to false.
      */
     public function get_many($keys) {
-        $result = $this->connection->get($keys);
+        $mkeys = array();
+        foreach ($keys as $key) {
+            $mkeys[$key] = $this->parse_key($key);
+        }
+        $result = $this->connection->get($mkeys);
         if (!is_array($result)) {
             $result = array();
         }
-        foreach ($keys as $key) {
-            if (!array_key_exists($key, $result)) {
-                $result[$key] = false;
+        $return = array();
+        foreach ($mkeys as $key => $mkey) {
+            if (!array_key_exists($mkey, $result)) {
+                $return[$key] = false;
+            } else {
+                $return[$key] = $result[$mkey];
             }
         }
-        return $result;
+        return $return;
     }
 
     /**
@@ -231,7 +252,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return bool True if the operation was a success false otherwise.
      */
     public function set($key, $data) {
-        return $this->connection->set($key, $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl());
+        return $this->connection->set($this->parse_key($key), $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl());
     }
 
     /**
@@ -245,7 +266,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
     public function set_many(array $keyvaluearray) {
         $count = 0;
         foreach ($keyvaluearray as $pair) {
-            if ($this->connection->set($pair['key'], $pair['value'], MEMCACHE_COMPRESSED, $this->definition->get_ttl())) {
+            if ($this->connection->set($this->parse_key($pair['key']), $pair['value'], MEMCACHE_COMPRESSED, $this->definition->get_ttl())) {
                 $count++;
             }
         }
@@ -259,7 +280,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
      * @return bool Returns true if the operation was a success, false otherwise.
      */
     public function delete($key) {
-        return $this->connection->delete($key);
+        return $this->connection->delete($this->parse_key($key));
     }
 
     /**
index 77d2275..98d3955 100644 (file)
@@ -140,8 +140,8 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
                 $this->connection->setOption($key, $value);
             }
             $this->connection->addServers($this->servers);
-            $this->isready = @$this->connection->set("ping", 'ping', 1);
         }
+        $this->isready = @$this->connection->set("ping", 'ping', 1);
     }
 
     /**
index 98d9c5b..edef90d 100644 (file)
@@ -422,7 +422,7 @@ class cache_phpunit_tests extends advanced_testcase {
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
             'area' => 'ttltest',
-            'ttl' => -10
+            'ttl' => -86400 // Set to a day in the past to be extra sure.
         ));
         $cache = cache::make('phpunit', 'ttltest');
         $this->assertInstanceOf('cache_application', $cache);
@@ -553,6 +553,8 @@ class cache_phpunit_tests extends advanced_testcase {
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
             'area' => 'eventinvalidationtest',
+            'simplekeys' => true,
+            'simpledata' => true,
             'invalidationevents' => array(
                 'crazyevent'
             )
@@ -567,13 +569,16 @@ class cache_phpunit_tests extends advanced_testcase {
 
         // OK data added, data invalidated, and invalidation time has been set.
         // Now we need to manually add back the data and adjust the invalidation time.
-        $timefile = $CFG->dataroot.'/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/a65/a65b1dc524cf6e03c1795197c84d5231eb229b86.cache';
+        $hash = md5(cache_store::MODE_APPLICATION.'/phpunit/eventinvalidationtest/'.$CFG->wwwroot.'phpunit');
+        $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las/lastinvalidation-$hash.cache";
+        // Make sure the file is correct.
+        $this->assertTrue(file_exists($timefile));
         $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate.
         make_writable_directory(dirname($timefile));
         file_put_contents($timefile, $timecont);
         $this->assertTrue(file_exists($timefile));
 
-        $datafile = $CFG->dataroot.'/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/626/626e9c7a45febd98f064c2b383de8d9d4ebbde7b.cache';
+        $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes/testkey1-$hash.cache";
         $datacont = serialize("test data 1");
         make_writable_directory(dirname($datafile));
         file_put_contents($datafile, $datacont);
@@ -586,6 +591,8 @@ class cache_phpunit_tests extends advanced_testcase {
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
             'area' => 'eventinvalidationtest',
+            'simplekeys' => true,
+            'simpledata' => true,
         ));
         $cache = cache::make('phpunit', 'eventinvalidationtest');
         $this->assertEquals('test data 1', $cache->get('testkey1'));
@@ -597,6 +604,8 @@ class cache_phpunit_tests extends advanced_testcase {
             'mode' => cache_store::MODE_APPLICATION,
             'component' => 'phpunit',
             'area' => 'eventinvalidationtest',
+            'simplekeys' => true,
+            'simpledata' => true,
             'invalidationevents' => array(
                 'crazyevent'
             )
@@ -618,6 +627,15 @@ class cache_phpunit_tests extends advanced_testcase {
                 'crazyevent'
             )
         ));
+        $instance->phpunit_add_definition('phpunit/eventpurgetestpersistent', array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'eventpurgetestpersistent',
+            'persistent' => true,
+            'invalidationevents' => array(
+                'crazyevent'
+            )
+        ));
         $cache = cache::make('phpunit', 'eventpurgetest');
 
         $this->assertTrue($cache->set('testkey1', 'test data 1'));
@@ -631,6 +649,71 @@ class cache_phpunit_tests extends advanced_testcase {
         // Check things have been removed.
         $this->assertFalse($cache->get('testkey1'));
         $this->assertFalse($cache->get('testkey2'));
+
+        // Now test the persistent cache.
+        $cache = cache::make('phpunit', 'eventpurgetestpersistent');
+        $this->assertTrue($cache->set('testkey1', 'test data 1'));
+        $this->assertEquals('test data 1', $cache->get('testkey1'));
+        $this->assertTrue($cache->set('testkey2', 'test data 2'));
+        $this->assertEquals('test data 2', $cache->get('testkey2'));
+
+        // Purge the event.
+        cache_helper::purge_by_event('crazyevent');
+
+        // Check things have been removed.
+        $this->assertFalse($cache->get('testkey1'));
+        $this->assertFalse($cache->get('testkey2'));
+    }
+
+    /**
+     * Tests session cache event purge
+     */
+    public function test_session_event_purge() {
+        $instance = cache_config_phpunittest::instance();
+        $instance->phpunit_add_definition('phpunit/eventpurgetest', array(
+            'mode' => cache_store::MODE_SESSION,
+            'component' => 'phpunit',
+            'area' => 'eventpurgetest',
+            'invalidationevents' => array(
+                'crazyevent'
+            )
+        ));
+        $instance->phpunit_add_definition('phpunit/eventpurgetestpersistent', array(
+            'mode' => cache_store::MODE_SESSION,
+            'component' => 'phpunit',
+            'area' => 'eventpurgetestpersistent',
+            'persistent' => true,
+            'invalidationevents' => array(
+                'crazyevent'
+            )
+        ));
+        $cache = cache::make('phpunit', 'eventpurgetest');
+
+        $this->assertTrue($cache->set('testkey1', 'test data 1'));
+        $this->assertEquals('test data 1', $cache->get('testkey1'));
+        $this->assertTrue($cache->set('testkey2', 'test data 2'));
+        $this->assertEquals('test data 2', $cache->get('testkey2'));
+
+        // Purge the event.
+        cache_helper::purge_by_event('crazyevent');
+
+        // Check things have been removed.
+        $this->assertFalse($cache->get('testkey1'));
+        $this->assertFalse($cache->get('testkey2'));
+
+        // Now test the persistent cache.
+        $cache = cache::make('phpunit', 'eventpurgetestpersistent');
+        $this->assertTrue($cache->set('testkey1', 'test data 1'));
+        $this->assertEquals('test data 1', $cache->get('testkey1'));
+        $this->assertTrue($cache->set('testkey2', 'test data 2'));
+        $this->assertEquals('test data 2', $cache->get('testkey2'));
+
+        // Purge the event.
+        cache_helper::purge_by_event('crazyevent');
+
+        // Check things have been removed.
+        $this->assertFalse($cache->get('testkey1'));
+        $this->assertFalse($cache->get('testkey2'));
     }
 
     /**
@@ -766,4 +849,4 @@ class cache_phpunit_tests extends advanced_testcase {
         $this->assertTrue($cache->set('test', 'test'));
         $this->assertEquals('test', $cache->get('test'));
     }
-}
\ No newline at end of file
+}
index d5e5297..42ce92b 100644 (file)
@@ -46,7 +46,7 @@ class cache_config_phpunittest extends cache_config_writer {
                 case cache_store::MODE_APPLICATION:
                     $properties['overrideclass'] = 'cache_phpunit_application';
                     break;
-                case cache_store::MDOE_SESSION:
+                case cache_store::MODE_SESSION:
                     $properties['overrideclass'] = 'cache_phpunit_session';
                     break;
                 case cache_store::MODE_REQUEST:
@@ -102,6 +102,16 @@ class cache_config_phpunittest extends cache_config_writer {
             'sort' => (int)$sort
         );
     }
+
+    /**
+     * Overrides the default site identifier used by the Cache API so that we can be sure of what it is.
+     *
+     * @return string
+     */
+    public function get_site_identifier() {
+        global $CFG;
+        return $CFG->wwwroot.'phpunit';
+    }
 }
 
 /**
index 07265bc..21782aa 100644 (file)
@@ -115,6 +115,14 @@ $PAGE->set_button(calendar_preferences_button($course));
 $renderer = $PAGE->get_renderer('core_calendar');
 
 echo $OUTPUT->header();
+
+// Filter subscriptions which user can't edit.
+foreach($subscriptions as $subscription) {
+    if (!calendar_can_edit_subscription($subscription)) {
+        unset($subscriptions[$subscription->id]);
+    }
+}
+
 // Display a table of subscriptions.
 echo $renderer->subscription_details($courseid, $subscriptions, $importresults);
 // Display the add subscription form.
index 91b99ca..c1fe37c 100644 (file)
@@ -22,6 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 define('AJAX_SCRIPT', true);
+define('NO_DEBUG_DISPLAY', true);
 
 require_once('../config.php');
 require_once($CFG->dirroot . '/comment/lib.php');
@@ -35,6 +36,10 @@ if (empty($CFG->usecomments)) {
 
 list($context, $course, $cm) = get_context_info_array($contextid);
 
+if ( $contextid == SYSCONTEXTID ) {
+    $course = $SITE;
+}
+
 $PAGE->set_url('/comment/comment_ajax.php');
 
 // Allow anonymous user to view comments providing forcelogin now enabled
index c146119..9ca0a41 100644 (file)
@@ -526,10 +526,10 @@ class comment {
             $c->content     = $u->ccontent;
             $c->format      = $u->cformat;
             $c->timecreated = $u->ctimecreated;
+            $c->strftimeformat = get_string('strftimerecent', 'langconfig');
             $url = new moodle_url('/user/view.php', array('id'=>$u->id, 'course'=>$this->courseid));
             $c->profileurl = $url->out(false);
             $c->fullname = fullname($u);
-            $c->time = userdate($c->timecreated, get_string('strftimerecent', 'langconfig'));
             $c->content = format_text($c->content, $c->format, $formatoptions);
             $c->avatar = $OUTPUT->user_picture($u, array('size'=>18));
 
@@ -628,12 +628,27 @@ class comment {
         $cmt_id = $DB->insert_record('comments', $newcmt);
         if (!empty($cmt_id)) {
             $newcmt->id = $cmt_id;
-            $newcmt->time = userdate($now, get_string('strftimerecent', 'langconfig'));
+            $newcmt->strftimeformat = get_string('strftimerecent', 'langconfig');
             $newcmt->fullname = fullname($USER);
             $url = new moodle_url('/user/view.php', array('id' => $USER->id, 'course' => $this->courseid));
             $newcmt->profileurl = $url->out();
             $newcmt->content = format_text($newcmt->content, $format, array('overflowdiv'=>true));
             $newcmt->avatar = $OUTPUT->user_picture($USER, array('size'=>16));
+
+            $commentlist = array($newcmt);
+
+            if (!empty($this->plugintype)) {
+                // Call the display callback to allow the plugin to format the newly added comment.
+                $commentlist = plugin_callback($this->plugintype,
+                                               $this->pluginname,
+                                               'comment',
+                                               'display',
+                                               array($commentlist, $this->comment_param),
+                                               $commentlist);
+                $newcmt = $commentlist[0];
+            }
+            $newcmt->time = userdate($newcmt->timecreated, $newcmt->strftimeformat);
+
             return $newcmt;
         } else {
             throw new comment_exception('dbupdatefailed');
@@ -793,7 +808,7 @@ class comment {
         $replacements[] = $cmt->avatar;
         $replacements[] = html_writer::link($cmt->profileurl, $cmt->fullname);
         $replacements[] = $cmt->content;
-        $replacements[] = userdate($cmt->timecreated, get_string('strftimerecent', 'langconfig'));
+        $replacements[] = userdate($cmt->timecreated, $cmt->strftimeformat);
 
         // use html template to format a single comment.
         return str_replace($patterns, $replacements, $this->template);
index 0cdcc5f..40f8577 100644 (file)
@@ -353,7 +353,8 @@ $CFG->admin = 'admin';
 //
 //     $CFG->themedir = '/location/of/extra/themes';
 //
-// It is possible to specify different cache and temp directories, use local fast filesystem.
+// It is possible to specify different cache and temp directories, use local fast filesystem
+// for normal web servers. Server clusters MUST use shared filesystem for cachedir!
 // The directories must not be accessible via web.
 //
 //     $CFG->tempdir = '/var/www/moodle/temp';
index ee8eb70..291583c 100644 (file)
@@ -549,7 +549,7 @@ M.course_dndupload = {
         var extension = '';
         var dotpos = file.name.lastIndexOf('.');
         if (dotpos != -1) {
-            extension = file.name.substr(dotpos+1, file.name.length);
+            extension = file.name.substr(dotpos+1, file.name.length).toLowerCase();
         }
 
         for (var i=0; i<filehandlers.length; i++) {
index b61ecaa..b7184b7 100644 (file)
@@ -131,25 +131,9 @@ if ($mform->is_cancelled()) {
     redirect('manage.php?id='.$newcategory->id);
 }
 
-// Unfortunately the navigation never generates correctly for this page because technically this page doesn't actually
-// exist on the navigation; you get here through the course management page.
-// First up we'll try to make the course management page active seeing as that is where the user thinks they are.
-// The big prolem here is that the course management page is a common page for both editing users and common users and
-// is only added to the admin tree if the user has permission to edit at the system level.
-$node = $PAGE->settingsnav->get('root');
-if ($node) {
-    $node = $node->get('courses');
-    if ($node) {
-        $node = $node->get('coursemgmt');
-    }
-}
-if ($node) {
-    // The course management page exists so make that active.
-    $node->make_active();
-} else {
-    // Failing that we'll override the URL, not as accurate and chances are things
-    // won't be 100% correct all the time but should work most times.
-    // A common reason to arrive here is having the management capability within only a particular category (not at system level).
+// Page "Add new category" (with "Top" as a parent) does not exist in navigation.
+// We pretend we are on course management page.
+if (empty($id) && empty($parent)) {
     navigation_node::override_active_url(new moodle_url('/course/manage.php'));
 }
 
index a7277f4..3063966 100644 (file)
@@ -1678,7 +1678,6 @@ class core_course_external extends external_api {
             $newcategory = new stdClass();
             $newcategory->name = $category['name'];
             $newcategory->parent = $category['parent'];
-            $newcategory->sortorder = 999; // Same as in the course/editcategory.php .
             // Format the description.
             if (!empty($category['description'])) {
                 $newcategory->description = $category['description'];
@@ -1921,6 +1920,70 @@ class core_course_external extends external_api {
         return null;
     }
 
+    /**
+     * Describes the parameters for delete_modules.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 2.5
+     */
+    public static function delete_modules_parameters() {
+        return new external_function_parameters (
+            array(
+                'cmids' => new external_multiple_structure(new external_value(PARAM_INT, 'course module ID',
+                        VALUE_REQUIRED, '', NULL_NOT_ALLOWED), 'Array of course module IDs'),
+            )
+        );
+    }
+
+    /**
+     * Deletes a list of provided module instances.
+     *
+     * @param array $cmids the course module ids
+     * @since Moodle 2.5
+     */
+    public static function delete_modules($cmids) {
+        global $CFG, $DB;
+
+        // Require course file containing the course delete module function.
+        require_once($CFG->dirroot . "/course/lib.php");
+
+        // Clean the parameters.
+        $params = self::validate_parameters(self::delete_modules_parameters(), array('cmids' => $cmids));
+
+        // Keep track of the course ids we have performed a capability check on to avoid repeating.
+        $arrcourseschecked = array();
+
+        foreach ($params['cmids'] as $cmid) {
+            // Get the course module.
+            $cm = $DB->get_record('course_modules', array('id' => $cmid), '*', MUST_EXIST);
+
+            // Check if we have not yet confirmed they have permission in this course.
+            if (!in_array($cm->course, $arrcourseschecked)) {
+                // Ensure the current user has required permission in this course.
+                $context = context_course::instance($cm->course);
+                self::validate_context($context);
+                // Add to the array.
+                $arrcourseschecked[] = $cm->course;
+            }
+
+            // Ensure they can delete this module.
+            $modcontext = context_module::instance($cm->id);
+            require_capability('moodle/course:manageactivities', $modcontext);
+
+            // Delete the module.
+            course_delete_module($cm->id);
+        }
+    }
+
+    /**
+     * Describes the delete_modules return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 2.5
+     */
+    public static function delete_modules_returns() {
+        return null;
+    }
 }
 
 /**
index 5b901b2..ba5d976 100644 (file)
@@ -1920,14 +1920,14 @@ function print_course_search($value="", $return=false, $format="plain") {
         $output  = '<form id="'.$id.'" action="'.$CFG->wwwroot.'/course/search.php" method="get">';
         $output .= '<fieldset class="coursesearchbox invisiblefieldset">';
         $output .= '<label for="shortsearchbox">'.$strsearchcourses.': </label>';
-        $output .= '<input type="text" id="shortsearchbox" size="12" name="search" alt="'.s($strsearchcourses).'" value="'.s($value).'" />';
+        $output .= '<input type="text" id="shortsearchbox" size="12" name="search" value="'.s($value).'" />';
         $output .= '<input type="submit" value="'.get_string('go').'" />';
         $output .= '</fieldset></form>';
     } else if ($format == 'navbar') {
         $output  = '<form id="coursesearchnavbar" action="'.$CFG->wwwroot.'/course/search.php" method="get">';
         $output .= '<fieldset class="coursesearchbox invisiblefieldset">';
         $output .= '<label for="navsearchbox">'.$strsearchcourses.': </label>';
-        $output .= '<input type="text" id="navsearchbox" size="20" name="search" alt="'.s($strsearchcourses).'" value="'.s($value).'" />';
+        $output .= '<input type="text" id="navsearchbox" size="20" name="search" value="'.s($value).'" />';
         $output .= '<input type="submit" value="'.get_string('go').'" />';
         $output .= '</fieldset></form>';
     }
@@ -2531,7 +2531,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
     $actions = array();
 
     // AJAX edit title
-    if ($mod->modname !== 'label' && $hasmanageactivities &&
+    if ($mod->has_view() && $hasmanageactivities &&
                 (($mod->course == $COURSE->id && course_ajax_enabled($COURSE)) ||
                  ($mod->course == SITEID && course_ajax_enabled($SITE)))) {
         // we will not display link if we are on some other-course page (where we should not see this module anyway)
@@ -3684,16 +3684,18 @@ class course_request {
  * @param stdClass $currentcontext Current context of block
  */
 function course_page_type_list($pagetype, $parentcontext, $currentcontext) {
-    // if above course context ,display all course fomats
-    list($currentcontext, $course, $cm) = get_context_info_array($currentcontext->id);
-    if ($course->id == SITEID) {
-        return array('*'=>get_string('page-x', 'pagetype'));
-    } else {
-        return array('*'=>get_string('page-x', 'pagetype'),
-            'course-*'=>get_string('page-course-x', 'pagetype'),
-            'course-view-*'=>get_string('page-course-view-x', 'pagetype')
-        );
+    // $currentcontext could be null, get_context_info_array() will throw an error if this is the case.
+    if (isset($currentcontext)) {
+        // if above course context ,display all course fomats
+        list($currentcontext, $course, $cm) = get_context_info_array($currentcontext->id);
+        if ($course->id == SITEID) {
+            return array('*'=>get_string('page-x', 'pagetype'));
+        }
     }
+    return array('*'=>get_string('page-x', 'pagetype'),
+        'course-*'=>get_string('page-course-x', 'pagetype'),
+        'course-view-*'=>get_string('page-course-view-x', 'pagetype')
+    );
 }
 
 /**
index 26827a7..177e922 100644 (file)
@@ -339,8 +339,8 @@ if (!isset($category)) {
     $table->head = array(
         get_string('categories'),
         get_string('courses'),
-        get_string('movecategoryto'),
         get_string('edit'),
+        get_string('movecategoryto'),
     );
     $table->colclasses = array(
         'leftalign name',
index e20cdc5..0f1e0ca 100644 (file)
@@ -83,9 +83,6 @@ class behat_course extends behat_base {
      */
     public function i_add_to_section($activity, $section) {
 
-        $activity = $this->fixStepArgument($activity);
-        $section = $this->fixStepArgument($section);
-
         // Clicks add activity or resource section link.
         $sectionxpath = "//*[@id='section-" . $section . "']/*/*/*/div[@class='section-modchooser']/span/a";
         $sectionnode = $this->find('xpath', $sectionxpath);
index a59351b..0e6275e 100644 (file)
@@ -297,22 +297,71 @@ class courselib_testcase extends advanced_testcase {
     }
 
     public function test_move_module_in_course() {
+        global $DB;
+
         $this->resetAfterTest(true);
         // Setup fixture
-        $course = $this->getDataGenerator()->create_course(array('numsections'=>5));
+        $course = $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections' => true));
         $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
 
         $cms = get_fast_modinfo($course)->get_cms();
         $cm = reset($cms);
 
-        course_create_sections_if_missing($course, 3);
-        $section3 = get_fast_modinfo($course)->get_section_info(3);
+        $newsection = get_fast_modinfo($course)->get_section_info(3);
+        $oldsectionid = $cm->section;
+
+        // Perform the move
+        moveto_module($cm, $newsection);
 
-        moveto_module($cm, $section3);
+        // reset of get_fast_modinfo is usually called the code calling moveto_module so call it here
+        get_fast_modinfo(0, 0, true);
+        $cms = get_fast_modinfo($course)->get_cms();
+        $cm = reset($cms);
 
+        // Check that the cached modinfo contains the correct section info
         $modinfo = get_fast_modinfo($course);
         $this->assertTrue(empty($modinfo->sections[0]));
         $this->assertFalse(empty($modinfo->sections[3]));
+
+        // Check that the old section's sequence no longer contains this ID
+        $oldsection = $DB->get_record('course_sections', array('id' => $oldsectionid));
+        $oldsequences = explode(',', $newsection->sequence);
+        $this->assertFalse(in_array($cm->id, $oldsequences));
+
+        // Check that the new section's sequence now contains this ID
+        $newsection = $DB->get_record('course_sections', array('id' => $newsection->id));
+        $newsequences = explode(',', $newsection->sequence);
+        $this->assertTrue(in_array($cm->id, $newsequences));
+
+        // Check that the section number has been changed in the cm
+        $this->assertEquals($newsection->id, $cm->section);
+
+
+        // Perform a second move as some issues were only seen on the second move
+        $newsection = get_fast_modinfo($course)->get_section_info(2);
+        $oldsectionid = $cm->section;
+        $result = moveto_module($cm, $newsection);
+        $this->assertTrue($result);
+
+        // reset of get_fast_modinfo is usually called the code calling moveto_module so call it here
+        get_fast_modinfo(0, 0, true);
+        $cms = get_fast_modinfo($course)->get_cms();
+        $cm = reset($cms);
+
+        // Check that the cached modinfo contains the correct section info
+        $modinfo = get_fast_modinfo($course);
+        $this->assertTrue(empty($modinfo->sections[0]));
+        $this->assertFalse(empty($modinfo->sections[2]));
+
+        // Check that the old section's sequence no longer contains this ID
+        $oldsection = $DB->get_record('course_sections', array('id' => $oldsectionid));
+        $oldsequences = explode(',', $newsection->sequence);
+        $this->assertFalse(in_array($cm->id, $oldsequences));
+
+        // Check that the new section's sequence now contains this ID
+        $newsection = $DB->get_record('course_sections', array('id' => $newsection->id));
+        $newsequences = explode(',', $newsection->sequence);
+        $this->assertTrue(in_array($cm->id, $newsequences));
     }
 
     public function test_module_visibility() {
@@ -435,4 +484,55 @@ class courselib_testcase extends advanced_testcase {
         }
     }
 
+    public function test_course_page_type_list() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Create a category.
+        $category = new stdClass();
+        $category->name = 'Test Category';
+
+        $testcategory = $this->getDataGenerator()->create_category($category);
+
+        // Create a course.
+        $course = new stdClass();
+        $course->fullname = 'Apu loves Unit Təsts';
+        $course->shortname = 'Spread the lŭve';
+        $course->idnumber = '123';
+        $course->summary = 'Awesome!';
+        $course->summaryformat = FORMAT_PLAIN;
+        $course->format = 'topics';
+        $course->newsitems = 0;
+        $course->numsections = 5;
+        $course->category = $testcategory->id;
+
+        $testcourse = $this->getDataGenerator()->create_course($course);
+
+        // Create contexts.
+        $coursecontext = context_course::instance($testcourse->id);
+        $parentcontext = $coursecontext->get_parent_context(); // Not actually used.
+        $pagetype = 'page-course-x'; // Not used either.
+        $pagetypelist = course_page_type_list($pagetype, $parentcontext, $coursecontext);
+
+        // Page type lists for normal courses.
+        $testpagetypelist1 = array();
+        $testpagetypelist1['*'] = 'Any page';
+        $testpagetypelist1['course-*'] = 'Any course page';
+        $testpagetypelist1['course-view-*'] = 'Any type of course main page';
+
+        $this->assertEquals($testpagetypelist1, $pagetypelist);
+
+        // Get the context for the front page course.
+        $sitecoursecontext = context_course::instance(SITEID);
+        $pagetypelist = course_page_type_list($pagetype, $parentcontext, $sitecoursecontext);
+
+        // Page type list for the front page course.
+        $testpagetypelist2 = array('*' => 'Any page');
+        $this->assertEquals($testpagetypelist2, $pagetypelist);
+
+        // Make sure that providing no current context to the function doesn't result in an error.
+        // Calls made from generate_page_type_patterns() may provide null values.
+        $pagetypelist = course_page_type_list($pagetype, null, null);
+        $this->assertEquals($pagetypelist, $testpagetypelist1);
+    }
 }
index 1489295..66b1fc7 100644 (file)
@@ -111,8 +111,12 @@ class core_course_external_testcase extends externallib_advanced_testcase {
         $category2 = $DB->get_record('course_categories', array('id' => $category2->id));
         $category3 = $DB->get_record('course_categories', array('id' => $category3->id));
 
-        $this->assertGreaterThanOrEqual($category1->sortorder, $category3->sortorder);
-        $this->assertGreaterThanOrEqual($category2->sortorder, $category3->sortorder);
+        // sortorder sequence (and sortorder) must be:
+        // category 1
+        //   category 3
+        // category 2
+        $this->assertGreaterThan($category1->sortorder, $category3->sortorder);
+        $this->assertGreaterThan($category3->sortorder, $category2->sortorder);
 
         // Call without required capability
         $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
@@ -843,4 +847,104 @@ class core_course_external_testcase extends externallib_advanced_testcase {
         $updatedcoursewarnings = core_course_external::update_courses($courses);
         $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
     }
+
+    /**
+     * Test delete course_module.
+     */
+    public function test_delete_modules() {
+        global $DB;
+
+        // Ensure we reset the data after this test.
+        $this->resetAfterTest(true);
+
+        // Create a user.
+        $user = self::getDataGenerator()->create_user();
+
+        // Set the tests to run as the user.
+        self::setUser($user);
+
+        // Create a course to add the modules.
+        $course = self::getDataGenerator()->create_course();
+
+        // Create two test modules.
+        $record = new stdClass();
+        $record->course = $course->id;
+        $module1 = self::getDataGenerator()->create_module('forum', $record);
+        $module2 = self::getDataGenerator()->create_module('assignment', $record);
+
+        // Check the forum was correctly created.
+        $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id)));
+
+        // Check the assignment was correctly created.
+        $this->assertEquals(1, $DB->count_records('assignment', array('id' => $module2->id)));
+
+        // Check data exists in the course modules table.
+        $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
+                array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
+
+        // Enrol the user in the course.
+        $enrol = enrol_get_plugin('manual');
+        $enrolinstances = enrol_get_instances($course->id, true);
+        foreach ($enrolinstances as $courseenrolinstance) {
+            if ($courseenrolinstance->enrol == "manual") {
+                $instance = $courseenrolinstance;
+                break;
+            }
+        }
+        $enrol->enrol_user($instance, $user->id);
+
+        // Assign capabilities to delete module 1.
+        $modcontext = context_module::instance($module1->cmid);
+        $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id);
+
+        // Assign capabilities to delete module 2.
+        $modcontext = context_module::instance($module2->cmid);
+        $newrole = create_role('Role 2', 'role2', 'Role 2 description');
+        $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole);
+
+        // Deleting these module instances.
+        core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
+
+        // Check the forum was deleted.
+        $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id)));
+
+        // Check the assignment was deleted.
+        $this->assertEquals(0, $DB->count_records('assignment', array('id' => $module2->id)));
+
+        // Check we retrieve no data in the course modules table.
+        $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
+                array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
+
+        // Call with non-existent course module id and ensure exception thrown.
+        try {
+            core_course_external::delete_modules(array('1337'));
+            $this->fail('Exception expected due to missing course module.');
+        } catch (dml_missing_record_exception $e) {
+            $this->assertEquals('invalidrecord', $e->errorcode);
+        }
+
+        // Create two modules.
+        $module1 = self::getDataGenerator()->create_module('forum', $record);
+        $module2 = self::getDataGenerator()->create_module('assignment', $record);
+
+        // Since these modules were recreated the user will not have capabilities
+        // to delete them, ensure exception is thrown if they try.
+        try {
+            core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
+            $this->fail('Exception expected due to missing capability.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('nopermissions', $e->errorcode);
+        }
+
+        // Unenrol user from the course.
+        $enrol->unenrol_user($instance, $user->id);
+
+        // Try and delete modules from the course the user was unenrolled in, make sure exception thrown.
+        try {
+            core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
+            $this->fail('Exception expected due to being unenrolled from the course.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('requireloginerror', $e->errorcode);
+        }
+    }
 }
index 377e5ae..ce39f93 100644 (file)
@@ -161,6 +161,6 @@ YUI.add('moodle-course-modchooser', function(Y) {
     }
 },
 '@VERSION@', {
-    requires:['base', 'overlay', 'moodle-core-chooserdialogue', 'transition', 'moodle-course-coursebase']
+    requires:['base', 'overlay', 'moodle-core-chooserdialogue', 'moodle-course-coursebase']
 }
 );
index 4e6ad15..8560eef 100644 (file)
@@ -15,7 +15,6 @@ YUI.add('moodle-course-toolboxes', function(Y) {
         GROUPSNONE : 'a.editing_groupsnone',
         GROUPSSEPARATE : 'a.editing_groupsseparate',
         GROUPSVISIBLE : 'a.editing_groupsvisible',
-        HASLABEL : 'label',
         HIDE : 'a.editing_hide',
         HIGHLIGHT : 'a.editing_highlight',
         INSTANCENAME : 'span.instancename',
@@ -62,7 +61,7 @@ YUI.add('moodle-course-toolboxes', function(Y) {
 
             var dimarea;
             var toggle_class;
-            if (this.is_label(element)) {
+            if (this.get_instance_name(element) == null) {
                 toggle_class = CSS.DIMMEDTEXT;
                 dimarea = element.all(CSS.MODINDENTDIV + ' > div').item(1);
             } else {
@@ -172,8 +171,19 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             Y.io(uri, config);
             return responsetext;
         },
-        is_label : function(target) {
-            return target.hasClass(CSS.HASLABEL);
+        /**
+         * Return the name of the activity instance
+         *
+         * If activity has no name (for example label) null is returned
+         *
+         * @param element The <li> element to determine a name for
+         * @return string|null Instance name
+         */
+        get_instance_name : function(target) {
+            if (target.one(CSS.INSTANCENAME)) {
+                return target.one(CSS.INSTANCENAME).get('firstChild').get('data');
+            }
+            return null;
         },
         /**
          * Return the module ID for the specified element
@@ -330,19 +340,16 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             // Get the element we're working on
             var element   = e.target.ancestor(CSS.ACTIVITYLI);
 
+            // Create confirm string (different if element has or does not have name)
             var confirmstring = '';
-            if (this.is_label(element)) {
-                // Labels are slightly different to other activities
-                var plugindata = {
-                    type : M.util.get_string('pluginname', 'label')
-                }
-                confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata)
-            } else {
-                var plugindata = {
-                    type : M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1]),
-                    name : element.one(CSS.INSTANCENAME).get('firstChild').get('data')
-                }
+            var plugindata = {
+                type : M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1])
+            }
+            if (this.get_instance_name(element) != null) {
+                plugindata.name = this.get_instance_name(element)
                 confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata);
+            } else {
+                confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata)
             }
 
             // Confirm element removal
index a2370d4..6741732 100644 (file)
@@ -118,7 +118,8 @@ class enrol_cohort_plugin extends enrol_plugin {
 
         if (has_capability('enrol/cohort:config', $context)) {
             $editlink = new moodle_url("/enrol/cohort/edit.php", array('courseid'=>$instance->courseid, 'id'=>$instance->id));
-            $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('i/edit', get_string('edit'), 'core', array('class'=>'icon')));
+            $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
+                    array('class' => 'smallicon')));
         }
 
         return $icons;
diff --git a/enrol/flatfile/adminlib.php b/enrol/flatfile/adminlib.php
new file mode 100644 (file)
index 0000000..971a847
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * Special flatfile settings.
+ *
+ * @package    enrol_flatfile
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/adminlib.php");
+
+
+/**
+ * Setting class that stores only non-empty values.
+ */
+class enrol_flatfile_role_setting extends admin_setting_configtext {
+
+    public function __construct($role) {
+        parent::__construct('enrol_flatfile/map_'.$role->id, $role->localname, '', $role->shortname);
+    }
+
+    public function config_read($name) {
+        $value = parent::config_read($name);
+        if (is_null($value)) {
+            // In other settings NULL means we have to ask user for new value,
+            // here we just ignore missing role mappings.
+            $value = '';
+        }
+        return $value;
+    }
+
+    public function config_write($name, $value) {
+        if ($value === '') {
+            // We do not want empty values in config table,
+            // delete it instead.
+            $value = null;
+        }
+        return parent::config_write($name, $value);
+    }
+}
diff --git a/enrol/flatfile/db/install.php b/enrol/flatfile/db/install.php
new file mode 100644 (file)
index 0000000..93c2cda
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Flatfile enrolment plugin installation.
+ *
+ * @package    enrol_flatfile
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+function xmldb_enrol_flatfile_install() {
+    global $CFG, $DB;
+
+    // Flatfile role mappings are empty by default now.
+    $roles = get_all_roles();
+    foreach ($roles as $role) {
+        set_config('map_'.$role->id, $role->shortname, 'enrol_flatfile');
+    }
+}
index d8bba47..379568e 100644 (file)
@@ -25,6 +25,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once(__DIR__.'/adminlib.php');
+
 if ($ADMIN->fulltree) {
 
     //--- general settings -----------------------------------------------------------------------------------
@@ -61,8 +63,8 @@ if ($ADMIN->fulltree) {
 
         $roles = role_fix_names(get_all_roles());
 
-        foreach ($roles as $id => $role) {
-            $settings->add(new admin_setting_configtext('enrol_flatfile/map_'.$id, $role->localname, '', $role->shortname));
+        foreach ($roles as $role) {
+            $settings->add(new enrol_flatfile_role_setting($role));
         }
         unset($roles);
     }
index 401d11a..8198f4b 100644 (file)
@@ -386,13 +386,6 @@ function process_group_tag($tagcontents) {
                             $this->log_line('No ' . $imsname . ' description tag found for ' . $coursecode . ' coursecode, using ' . $coursecode . ' instead');
                             $course->{$courseattr} = $coursecode;
                         }
-
-                        if ($courseattr == 'summary') {
-                            $format = FORMAT_HTML;
-                        } else {
-                            $format = FORMAT_PLAIN;
-                        }
-                        $course->{$courseattr} = format_text($course->$courseattr, $format);
                     }
 
                     $course->idnumber = $coursecode;
index 13c6114..921bb30 100644 (file)
@@ -114,9 +114,9 @@ class imsenterprise_courses {
      * @return array Array of assignable values
      */
     function get_imsnames($courseattr) {
-\r
-        $values = $this->imsnames;\r
-        if ($courseattr == 'summary') {\r
+
+        $values = $this->imsnames;
+        if ($courseattr == 'summary') {
             $values = array_merge(array('ignore' => get_string('emptyattribute', 'enrol_imsenterprise')), $values);
         }
         return $values;
index 5e947b6..3f6a506 100644 (file)
@@ -318,11 +318,12 @@ class course_enrolment_manager {
      * @param array $params query parameters.
      * @param int $page which page number of the results to show.
      * @param int $perpage number of users per page.
+     * @param int $addedenrollment number of users added to enrollment.
      * @return array with two elememts:
      *      int total number of users matching the search.
      *      array of user objects returned by the query.
      */
-    protected function execute_search_queries($search, $fields, $countfields, $sql, array $params, $page, $perpage) {
+    protected function execute_search_queries($search, $fields, $countfields, $sql, array $params, $page, $perpage, $addedenrollment=0) {
         global $DB, $CFG;
 
         list($sort, $sortparams) = users_order_by_sql('u', $search, $this->get_context());
@@ -330,7 +331,7 @@ class course_enrolment_manager {
 
         $totalusers = $DB->count_records_sql($countfields . $sql, $params);
         $availableusers = $DB->get_records_sql($fields . $sql . $order,
-                array_merge($params, $sortparams), $page*$perpage, $perpage);
+                array_merge($params, $sortparams), ($page*$perpage) - $addedenrollment, $perpage);
 
         return array('totalusers' => $totalusers, 'users' => $availableusers);
     }
@@ -344,9 +345,10 @@ class course_enrolment_manager {
      * @param bool $searchanywhere
      * @param int $page Defaults to 0
      * @param int $perpage Defaults to 25
+     * @param int $addedenrollment Defaults to 0
      * @return array Array(totalusers => int, users => array)
      */
-    public function get_potential_users($enrolid, $search='', $searchanywhere=false, $page=0, $perpage=25) {
+    public function get_potential_users($enrolid, $search='', $searchanywhere=false, $page=0, $perpage=25, $addedenrollment=0) {
         global $DB;
 
         list($ufields, $params, $wherecondition) = $this->get_basic_search_conditions($search, $searchanywhere);
@@ -359,7 +361,7 @@ class course_enrolment_manager {
                       AND ue.id IS NULL";
         $params['enrolid'] = $enrolid;
 
-        return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage);
+        return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage, $addedenrollment);
     }
 
     /**
index 093ee05..ea4acd8 100644 (file)
@@ -67,7 +67,9 @@ switch ($action) {
         $enrolid = required_param('enrolid', PARAM_INT);
         $search = optional_param('search', '', PARAM_RAW);
         $page = optional_param('page', 0, PARAM_INT);
-        $outcome->response = $manager->get_potential_users($enrolid, $search, $searchanywhere, $page);
+        $addedenrollment = optional_param('enrolcount', 0, PARAM_INT);
+        $perpage = optional_param('perpage', 25, PARAM_INT);  //  This value is hard-coded to 25 in quickenrolment.js
+        $outcome->response = $manager->get_potential_users($enrolid, $search, $searchanywhere, $page, $perpage, $addedenrollment);
         $extrafields = get_extra_user_fields($context);
         foreach ($outcome->response['users'] as &$user) {
             $user->picture = $OUTPUT->user_picture($user);
index 7ffba73..ab6f719 100644 (file)
@@ -68,7 +68,9 @@ class enrol_manual_plugin extends enrol_plugin {
 
         $context = context_course::instance($instance->courseid, MUST_EXIST);
 
-        if (!has_capability('enrol/manual:manage', $context) or !has_capability('enrol/manual:enrol', $context) or !has_capability('enrol/manual:unenrol', $context)) {
+        if (!has_capability('enrol/manual:enrol', $context)) {
+            // Note: manage capability not used here because it is used for editing
+            // of existing enrolments which is not possible here.
             return NULL;
         }
 
@@ -111,13 +113,14 @@ class enrol_manual_plugin extends enrol_plugin {
 
         $icons = array();
 
-        if (has_capability('enrol/manual:manage', $context)) {
+        if (has_capability('enrol/manual:enrol', $context) or has_capability('enrol/manual:unenrol', $context)) {
             $managelink = new moodle_url("/enrol/manual/manage.php", array('enrolid'=>$instance->id));
             $icons[] = $OUTPUT->action_icon($managelink, new pix_icon('t/enrolusers', get_string('enrolusers', 'enrol_manual'), 'core', array('class'=>'iconsmall')));
         }
         if (has_capability('enrol/manual:config', $context)) {
             $editlink = new moodle_url("/enrol/manual/edit.php", array('courseid'=>$instance->courseid));
-            $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('i/edit', get_string('edit'), 'core', array('class'=>'icon')));
+            $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
+                    array('class' => 'iconsmall')));
         }
 
         return $icons;
@@ -435,10 +438,14 @@ class enrol_manual_plugin extends enrol_plugin {
     public function get_bulk_operations(course_enrolment_manager $manager) {
         global $CFG;
         require_once($CFG->dirroot.'/enrol/manual/locallib.php');
-        $bulkoperations = array(
-            'editselectedusers' => new enrol_manual_editselectedusers_operation($manager, $this),
-            'deleteselectedusers' => new enrol_manual_deleteselectedusers_operation($manager, $this)
-        );
+        $context = $manager->get_context();
+        $bulkoperations = array();
+        if (has_capability("enrol/manual:manage", $context)) {
+            $bulkoperations['editselectedusers'] = new enrol_manual_editselectedusers_operation($manager, $this);
+        }
+        if (has_capability("enrol/manual:unenrol", $context)) {
+            $bulkoperations['deleteselectedusers'] = new enrol_manual_deleteselectedusers_operation($manager, $this);
+        }
         return $bulkoperations;
     }
 
index f7eab04..7d91e4b 100644 (file)
@@ -35,9 +35,17 @@ $course = $DB->get_record('course', array('id'=>$instance->courseid), '*', MUST_
 $context = context_course::instance($course->id, MUST_EXIST);
 
 require_login($course);
-require_capability('enrol/manual:enrol', $context);
-require_capability('enrol/manual:manage', $context);
-require_capability('enrol/manual:unenrol', $context);
+$canenrol = has_capability('enrol/manual:enrol', $context);
+$canunenrol = has_capability('enrol/manual:unenrol', $context);
+
+// Note: manage capability not used here because it is used for editing
+// of existing enrolments which is not possible here.
+
+if (!$canenrol and !$canunenrol) {
+    // No need to invent new error strings here...
+    require_capability('enrol/manual:enrol', $context);
+    require_capability('enrol/manual:unenrol', $context);
+}
 
 if ($roleid < 0) {
     $roleid = $instance->roleid;
@@ -95,7 +103,7 @@ if ($course->startdate > 0) {
 $basemenu[3] = get_string('today') . ' (' . userdate($today, $timeformat) . ')' ;
 
 // Process add and removes.
-if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
+if ($canenrol && optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
     $userstoassign = $potentialuserselector->get_selected_users();
     if (!empty($userstoassign)) {
         foreach($userstoassign as $adduser) {
@@ -126,7 +134,7 @@ if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
 }
 
 // Process incoming role unassignments.
-if (optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) {
+if ($canunenrol && optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) {
     $userstounassign = $currentuserselector->get_selected_users();
     if (!empty($userstounassign)) {
         foreach($userstounassign as $removeuser) {
@@ -145,6 +153,9 @@ if (optional_param('remove', false, PARAM_BOOL) && confirm_sesskey()) {
 echo $OUTPUT->header();
 echo $OUTPUT->heading($instancename);
 
+$addenabled = $canenrol ? '' : 'disabled="disabled"';
+$removeenabled = $canunenrol ? '' : 'disabled="disabled"';
+
 ?>
 <form id="assignform" method="post" action="<?php echo $PAGE->url ?>"><div>
   <input type="hidden" name="sesskey" value="<?php echo sesskey() ?>" />
@@ -157,7 +168,7 @@ echo $OUTPUT->heading($instancename);
       </td>
       <td id="buttonscell">
           <div id="addcontrols">
-              <input name="add" id="add" type="submit" value="<?php echo $OUTPUT->larrow().'&nbsp;'.get_string('add'); ?>" title="<?php print_string('add'); ?>" /><br />
+              <input name="add" <?php echo $addenabled; ?> id="add" type="submit" value="<?php echo $OUTPUT->larrow().'&nbsp;'.get_string('add'); ?>" title="<?php print_string('add'); ?>" /><br />
 
               <div class="enroloptions">
 
@@ -174,7 +185,7 @@ echo $OUTPUT->heading($instancename);
           </div>
 
           <div id="removecontrols">
-              <input name="remove" id="remove" type="submit" value="<?php echo get_string('remove').'&nbsp;'.$OUTPUT->rarrow(); ?>" title="<?php print_string('remove'); ?>" />
+              <input name="remove" id="remove" <?php echo $removeenabled; ?> type="submit" value="<?php echo get_string('remove').'&nbsp;'.$OUTPUT->rarrow(); ?>" title="<?php print_string('remove'); ?>" />
           </div>
       </td>
       <td id="potentialcell">
index aad6e62..8362185 100644 (file)
@@ -23,7 +23,9 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
         DEFAULTDURATION : 'defaultDuration',
         ASSIGNABLEROLES : 'assignableRoles',
         DISABLEGRADEHISTORY : 'disableGradeHistory',
-        RECOVERGRADESDEFAULT : 'recoverGradesDefault'
+        RECOVERGRADESDEFAULT : 'recoverGradesDefault',
+        ENROLCOUNT : 'enrolCount',
+        PERPAGE : 'perPage'
     };
     /** CSS classes for nodes in structure **/
     var CSS = {
@@ -309,6 +311,9 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             params['action'] = 'searchusers';
             params['search'] = this.get(UEP.SEARCH).get('value');
             params['page'] = this.get(UEP.PAGE);
+            params['enrolcount'] = this.get(UEP.ENROLCOUNT);
+            params['perpage'] = this.get(UEP.PERPAGE);
+
             if (this.get(UEP.MULTIPLE)) {
                 alert('oh no there are multiple');
             } else {
@@ -376,7 +381,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 var content = create('<div class="'+CSS.SEARCHRESULTS+'"></div>')
                     .append(create('<div class="'+CSS.TOTALUSERS+'">'+usersstr+'</div>'))
                     .append(users);
-                if (result.response.totalusers > (this.get(UEP.PAGE)+1)*25) {
+                if (result.response.totalusers > (this.get(UEP.PAGE)+1)*this.get(UEP.PERPAGE)) {
                     var fetchmore = create('<div class="'+CSS.MORERESULTS+'"><a href="#">'+M.str.enrol.ajaxnext25+'</a></div>');
                     fetchmore.on('click', this.search, this, true);
                     content.append(fetchmore)
@@ -384,7 +389,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 this.setContent(content);
                 Y.delegate("click", this.enrolUser, users, '.'+CSS.USER+' .'+CSS.ENROL, this, args);
             } else {
-                if (result.response.totalusers <= (this.get(UEP.PAGE)+1)*25) {
+                if (result.response.totalusers <= (this.get(UEP.PAGE)+1)*this.get(UEP.PERPAGE)) {
                     this.get(UEP.BASE).one('.'+CSS.MORERESULTS).remove();
                 }
             }
@@ -420,6 +425,8 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                                 args.userNode.addClass(CSS.ENROLLED);
                                 args.userNode.one('.'+CSS.ENROL).remove();
                                 this.set(UEP.REQUIREREFRESH, true);
+                                var countenrol = this.get(UEP.ENROLCOUNT)+1;
+                                this.set(UEP.ENROLCOUNT, countenrol);
                             }
                         } catch (e) {
                             new M.core.exception(e);
@@ -532,6 +539,14 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             },
             recoverGradesDefault : {
                 value : ''
+            },
+            enrolCount : {
+                value : 0,
+                validator : Y.Lang.isNumber
+            },
+            perPage : {
+                value: 25,
+                Validator: Y.Lang.isNumber
             }
         }
     });
index ca3cd74..b79dbe1 100644 (file)
@@ -118,7 +118,8 @@ class enrol_paypal_plugin extends enrol_plugin {
 
         if (has_capability('enrol/paypal:config', $context)) {
             $editlink = new moodle_url("/enrol/paypal/edit.php", array('courseid'=>$instance->courseid, 'id'=>$instance->id));
-            $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('i/edit', get_string('edit'), 'core', array('class'=>'icon')));
+            $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
+                    array('class' => 'smallicon')));
         }
 
         return $icons;
index b624d92..31679c7 100644 (file)
@@ -160,7 +160,8 @@ class enrol_self_plugin extends enrol_plugin {
 
         if (has_capability('enrol/self:config', $context)) {
             $editlink = new moodle_url("/enrol/self/edit.php", array('courseid'=>$instance->courseid, 'id'=>$instance->id));
-            $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('i/edit', get_string('edit'), 'core', array('class'=>'icon')));
+            $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
+                array('class' => 'smallicon')));
         }
 
         return $icons;
index bef80bc..e739e4a 100644 (file)
@@ -36,8 +36,6 @@ class filter_activitynames extends moodle_text_filter {
     static $cachedcourseid;
 
     function filter($text, array $options = array()) {
-        global $CFG, $COURSE, $DB;
-
         if (!$courseid = get_courseid_from_context($this->context)) {
             return $text;
         }
@@ -53,38 +51,28 @@ class filter_activitynames extends moodle_text_filter {
         if (is_null(self::$activitylist)) {
             self::$activitylist = array();
 
-            if ($COURSE->id == $courseid) {
-                $course = $COURSE;
-            } else {
-                $course = $DB->get_record("course", array("id"=>$courseid));
-            }
-
-            if (!isset($course->modinfo)) {
-                return $text;
-            }
-
-        /// Casting $course->modinfo to string prevents one notice when the field is null
-            $modinfo = unserialize((string)$course->modinfo);
-
-            if (!empty($modinfo)) {
-
+            $modinfo = get_fast_modinfo($courseid);
+            if (!empty($modinfo->cms)) {
                 self::$activitylist = array();      /// We will store all the activities here
 
                 //Sort modinfo by name length
-                usort($modinfo, 'filter_activitynames_comparemodulenamesbylength');
+                $sortedactivities = fullclone($modinfo->cms);
+                usort($sortedactivities, 'filter_activitynames_comparemodulenamesbylength');
 
-                foreach ($modinfo as $activity) {
+                foreach ($sortedactivities as $cm) {
                     //Exclude labels, hidden activities and activities for group members only
-                    if ($activity->mod != "label" and $activity->visible and empty($activity->groupmembersonly)) {
-                        $title = s(trim(strip_tags($activity->name)));
-                        $currentname = trim($activity->name);
+                    if ($cm->visible and empty($cm->groupmembersonly) and $cm->has_view()) {
+                        $title = s(trim(strip_tags($cm->name)));
+                        $currentname = trim($cm->name);
                         $entitisedname  = s($currentname);
                         /// Avoid empty or unlinkable activity names
                         if (!empty($title)) {
-                            $href_tag_begin = "<a class=\"autolink\" title=\"$title\" href=\"$CFG->wwwroot/mod/$activity->mod/view.php?id=$activity->cm\">";
-                            self::$activitylist[] = new filterobject($currentname, $href_tag_begin, '</a>', false, true);
+                            $href_tag_begin = html_writer::start_tag('a',
+                                    array('class' => 'autolink', 'title' => $title,
+                                        'href' => $cm->get_url()));
+                            self::$activitylist[$cm->id] = new filterobject($currentname, $href_tag_begin, '</a>', false, true);
                             if ($currentname != $entitisedname) { /// If name has some entity (&amp; &quot; &lt; &gt;) add that filter too. MDL-17545
-                                self::$activitylist[] = new filterobject($entitisedname, $href_tag_begin, '</a>', false, true);
+                                self::$activitylist[$cm->id.'-e'] = new filterobject($entitisedname, $href_tag_begin, '</a>', false, true);
                             }
                         }
                     }
@@ -92,8 +80,19 @@ class filter_activitynames extends moodle_text_filter {
             }
         }
 
+        $filterslist = array();
         if (self::$activitylist) {
-            return $text = filter_phrases ($text, self::$activitylist);
+            $cmid = $this->context->instanceid;
+            if ($this->context->contextlevel == CONTEXT_MODULE && isset(self::$activitylist[$cmid])) {
+                // remove filterobjects for the current module
+                $filterslist = array_diff_key(self::$activitylist, array($cmid => 1, $cmid.'-e' => 1));
+            } else {
+                $filterslist = self::$activitylist;
+            }
+        }
+
+        if ($filterslist) {
+            return $text = filter_phrases($text, $filterslist);
         } else {
             return $text;
         }
index c10a858..6e21e46 100644 (file)
@@ -30,7 +30,7 @@ require_once('import_outcomes_form.php');
 
 $courseid = optional_param('courseid', 0, PARAM_INT);
 $action   = optional_param('action', '', PARAM_ALPHA);
-$scope    = optional_param('scope', 'global', PARAM_ALPHA);
+$scope    = optional_param('scope', 'custom', PARAM_ALPHA);
 
 $PAGE->set_url('/grade/edit/outcome/import.php', array('courseid' => $courseid));
 
index 0ee92ba..8d5f090 100644 (file)
@@ -323,9 +323,9 @@ M.gradereport_grader.classes.ajax = function(report, cfg) {
                 this.existingfields[userid][itemid] = new M.gradereport_grader.classes.existingfield(this, userid, itemid);
             }
         }
-        // Hide the Update button
+        // Disable the Update button as we're saving using ajax.
         submitbutton = this.report.Y.one('#gradersubmit');
-        submitbutton.setStyle('visibility', 'hidden');
+        submitbutton.set('disabled', true);
     }
 };
 /**
@@ -741,9 +741,11 @@ M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) {
     this.editfeedback = ajax.showquickfeedback;
     this.grade = this.report.Y.one('#grade_'+userid+'_'+itemid);
 
-    for(var i = 0; i < this.report.grades.length; i++) {
-        if (this.report.grades[i]['user']==this.userid && this.report.grades[i]['item']==this.itemid) {
-            this.oldgrade = this.report.grades[i]['grade'];
+    if (this.report.grades) {
+        for (var i = 0; i < this.report.grades.length; i++) {
+            if (this.report.grades[i]['user']==this.userid && this.report.grades[i]['item']==this.itemid) {
+                this.oldgrade = this.report.grades[i]['grade'];
+            }
         }
     }
 
index 5963c6e..48d656f 100644 (file)
@@ -760,6 +760,7 @@ class core_group_external extends external_api {
             array(
                 'groupingids' => new external_multiple_structure(new external_value(PARAM_INT, 'grouping ID')
                         , 'List of grouping id. A grouping id is an integer.'),
+                'returngroups' => new external_value(PARAM_BOOL, 'return associated groups', VALUE_DEFAULT, 0)
             )
         );
     }
@@ -768,15 +769,18 @@ class core_group_external extends external_api {
      * Get groupings definition specified by ids
      *
      * @param array $groupingids arrays of grouping ids
+     * @param boolean $returngroups return the associated groups if true. The default is false.
      * @return array of grouping objects (id, courseid, name)
      * @since Moodle 2.3
      */
-    public static function get_groupings($groupingids) {
-        global $CFG;
+    public static function get_groupings($groupingids, $returngroups = false) {
+        global $CFG, $DB;
         require_once("$CFG->dirroot/group/lib.php");
         require_once("$CFG->libdir/filelib.php");
 
-        $params = self::validate_parameters(self::get_groupings_parameters(), array('groupingids'=>$groupingids));
+        $params = self::validate_parameters(self::get_groupings_parameters(),
+                                            array('groupingids' => $groupingids,
+                                                  'returngroups' => $returngroups));
 
         $groupings = array();
         foreach ($params['groupingids'] as $groupingid) {
@@ -799,7 +803,30 @@ class core_group_external extends external_api {
                 external_format_text($grouping->description, $grouping->descriptionformat,
                         $context->id, 'grouping', 'description', $grouping->id);
 
-            $groupings[] = (array)$grouping;
+            $groupingarray = (array)$grouping;
+
+            if ($params['returngroups']) {
+                $grouprecords = $DB->get_records_sql("SELECT * FROM {groups} g INNER JOIN {groupings_groups} gg ".
+                                               "ON g.id = gg.groupid WHERE gg.groupingid = ? ".
+                                               "ORDER BY groupid", array($groupingid));
+                if ($grouprecords) {
+                    $groups = array();
+                    foreach ($grouprecords as $grouprecord) {
+                        list($grouprecord->description, $grouprecord->descriptionformat) =
+                        external_format_text($grouprecord->description, $grouprecord->descriptionformat,
+                        $context->id, 'group', 'description', $grouprecord->groupid);
+                        $groups[] = array('id' => $grouprecord->groupid,
+                                          'name' => $grouprecord->name,
+                                          'description' => $grouprecord->description,
+                                          'descriptionformat' => $grouprecord->descriptionformat,
+                                          'enrolmentkey' => $grouprecord->enrolmentkey,
+                                          'courseid' => $grouprecord->courseid
+                                          );
+                    }
+                    $groupingarray['groups'] = $groups;
+                }
+            }
+            $groupings[] = $groupingarray;
         }
 
         return $groupings;
@@ -819,7 +846,19 @@ class core_group_external extends external_api {
                     'courseid' => new external_value(PARAM_INT, 'id of course'),
                     'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
                     'description' => new external_value(PARAM_RAW, 'grouping description text'),
-                    'descriptionformat' => new external_format_value('description')
+                    'descriptionformat' => new external_format_value('description'),
+                    'groups' => new external_multiple_structure(
+                        new external_single_structure(
+                            array(
+                                'id' => new external_value(PARAM_INT, 'group record id'),
+                                'courseid' => new external_value(PARAM_INT, 'id of course'),
+                                'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
+                                'description' => new external_value(PARAM_RAW, 'group description text'),
+                                'descriptionformat' => new external_format_value('description'),
+                                'enrolmentkey' => new external_value(PARAM_RAW, 'group enrol secret phrase')
+                            )
+                        ),
+                    'optional groups', VALUE_OPTIONAL)
                 )
             )
         );
index 04800d2..ec8b094 100644 (file)
@@ -83,17 +83,20 @@ if ($groupings = $DB->get_records('groupings', array('courseid'=>$course->id), '
         }
         $line[2] = $DB->count_records('course_modules', array('course'=>$course->id, 'groupingid'=>$grouping->id));
 
-        $buttons  = "<a title=\"$stredit\" href=\"grouping.php?id=$grouping->id\"><img".
-                    " src=\"" . $OUTPUT->pix_url('t/edit') . "\" class=\"iconsmall\" alt=\"$stredit\" /></a> ";
+        $url = new moodle_url('/group/grouping.php', array('id' => $grouping->id));
+        $buttons  = html_writer::link($url, $OUTPUT->pix_icon('t/edit', $stredit, 'core',
+                array('class' => 'iconsmall')), array('title' => $stredit));
         if (empty($grouping->idnumber) || $canchangeidnumber) {
-            // It's only possible to delete groups without an idnumber unless the user has the changeidnumber capability
-            $buttons .= "<a title=\"$strdelete\" href=\"grouping.php?id=$grouping->id&amp;delete=1\"><img".
-                        " src=\"" . $OUTPUT->pix_url('t/delete') . "\" class=\"iconsmall\" alt=\"$strdelete\" /></a> ";
+            // It's only possible to delete groups without an idnumber unless the user has the changeidnumber capability.
+            $url = new moodle_url('/group/grouping.php', array('id' => $grouping->id, 'delete' => 1));
+            $buttons .= html_writer::link($url, $OUTPUT->pix_icon('t/delete', $strdelete, 'core',
+                    array('class' => 'iconsmall')), array('title' => $strdelete));
         } else {
             $buttons .= $OUTPUT->spacer();
         }
-        $buttons .= "<a title=\"$strmanagegrping\" href=\"assign.php?id=$grouping->id\"><img".
-                    " src=\"" . $OUTPUT->pix_url('i/group') . "\" class=\"icon\" alt=\"$strmanagegrping\" /></a> ";
+        $url = new moodle_url('/group/assign.php', array('id' => $grouping->id));
+        $buttons .= html_writer::link($url, $OUTPUT->pix_icon('t/groups', $strmanagegrping, 'core',
+                array('class' => 'iconsmall')), array('title' => $strmanagegrping));
 
         $line[3] = $buttons;
         $data[] = $line;
index 996d3c6..8048d18 100644 (file)
@@ -30,6 +30,7 @@ global $CFG;
 
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 require_once($CFG->dirroot . '/group/externallib.php');
+require_once($CFG->dirroot . '/group/lib.php');
 
 class core_group_external_testcase extends externallib_advanced_testcase {
 
@@ -207,4 +208,81 @@ class core_group_external_testcase extends externallib_advanced_testcase {
         $this->setExpectedException('required_capability_exception');
         $froups = core_group_external::delete_groups(array($group3->id));
     }
+
+    /**
+     * Test get_groupings
+     */
+    public function test_get_groupings() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course = self::getDataGenerator()->create_course();
+
+        $groupingdata = array();
+        $groupingdata['courseid'] = $course->id;
+        $groupingdata['name'] = 'Grouping Test';
+        $groupingdata['description'] = 'Grouping Test description';
+        $groupingdata['descriptionformat'] = FORMAT_MOODLE;
+
+        $grouping = self::getDataGenerator()->create_grouping($groupingdata);
+
+        // Set the required capabilities by the external function.
+        $context = context_course::instance($course->id);
+        $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id);
+        $this->assignUserCapability('moodle/course:view', $context->id, $roleid);
+
+        // Call the external function without specifying the optional parameter.
+        $groupings = core_group_external::get_groupings(array($grouping->id));
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $groupings = external_api::clean_returnvalue(core_group_external::get_groupings_returns(), $groupings);
+
+        $this->assertEquals(1, count($groupings));
+
+        $group1data = array();
+        $group1data['courseid'] = $course->id;
+        $group1data['name'] = 'Group Test 1';
+        $group1data['description'] = 'Group Test 1 description';
+        $group1data['descriptionformat'] = FORMAT_MOODLE;
+        $group2data = array();
+        $group2data['courseid'] = $course->id;
+        $group2data['name'] = 'Group Test 2';
+        $group2data['description'] = 'Group Test 2 description';
+        $group2data['descriptionformat'] = FORMAT_MOODLE;
+
+        $group1 = self::getDataGenerator()->create_group($group1data);
+        $group2 = self::getDataGenerator()->create_group($group2data);
+
+        groups_assign_grouping($grouping->id, $group1->id);
+        groups_assign_grouping($grouping->id, $group2->id);
+
+        // Call the external function specifying that groups are returned.
+        $groupings = core_group_external::get_groupings(array($grouping->id), true);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $groupings = external_api::clean_returnvalue(core_group_external::get_groupings_returns(), $groupings);
+        $this->assertEquals(1, count($groupings));
+        $this->assertEquals(2, count($groupings[0]['groups']));
+        foreach ($groupings[0]['groups'] as $group) {
+            $dbgroup = $DB->get_record('groups', array('id' => $group['id']), '*', MUST_EXIST);
+            $dbgroupinggroups = $DB->get_record('groupings_groups',
+                                                array('groupingid' => $groupings[0]['id'],
+                                                      'groupid' => $group['id']),
+                                                '*', MUST_EXIST);
+            switch ($dbgroup->name) {
+                case $group1->name:
+                    $groupdescription = $group1->description;
+                    $groupcourseid = $group1->courseid;
+                    break;
+                case $group2->name:
+                    $groupdescription = $group2->description;
+                    $groupcourseid = $group2->courseid;
+                    break;
+                default:
+                    throw new moodle_exception('unknowgroupname');
+                    break;
+            }
+            $this->assertEquals($dbgroup->description, $groupdescription);
+            $this->assertEquals($dbgroup->courseid, $groupcourseid);
+        }
+    }
 }
index cf90fc3..63d637a 100644 (file)
@@ -30,7 +30,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['language'] = '×\97×\91×\99×\9cת ×©×¤×\94';
+$string['language'] = 'שפת ×\9e×\9eשק';
 $string['next'] = 'הלאה';
 $string['previous'] = 'קודם';
 $string['reload'] = 'טען מחדש';
index 4996af6..53f68e2 100644 (file)
@@ -1035,7 +1035,7 @@ $string['updateavailable_version'] = 'Version {$a}';
 $string['updateavailableinstall'] = 'Install this update';
 $string['updateavailablenot'] = 'Your Moodle code is up-to-date!';
 $string['updatenotifications'] = 'Update notifications';
-$string['updatenotificationfooter'] = 'Your Moodle site {$a->siteurl} is configured to automatically check for available updates. You are receiving this message as the administrator of the site. You can disable automatic checks for available updates in the Site administration section of the Settings block. You can customize the delivery of this message via your personal Messaging setting in the My profile settings section.';
+$string['updatenotificationfooter'] = 'Your Moodle site {$a->siteurl} is configured to automatically check for available updates. You are receiving this message as the administrator of the site. You can disable automatic checks for available updates in the Site administration section of the Administration block. You can customize the delivery of this message via your personal Messaging setting in the My profile settings section.';
 $string['updatenotificationsubject'] = 'Moodle updates are available ({$a->siteurl})';
 $string['updateautocheck'] = 'Automatically check for available updates';
 $string['updateautocheck_desc'] = 'If enabled, your site will automatically check for available updates for both Moodle code and all additional plugins. If there is a new update available, a notification will be sent to site admins.';
index 8e7d0c0..eefb901 100644 (file)
@@ -42,6 +42,7 @@ $string['defaultweight_help'] = 'The default weight allows you to choose roughly
 $string['deletecheck'] = 'Delete {$a} block?';
 $string['deleteblock'] = 'Delete {$a} block';
 $string['deleteblockcheck'] = 'Are you sure that you want to delete this block titled {$a}?';
+$string['deleteblockwarning'] = '<p>You are about to delete a block that appears elsewhere.</p><p>Original block location: {$a->location}<br />Display on page types: {$a->pagetype}</p><p>Are you sure you want to continue?</p>';
 $string['hideblock'] = 'Hide {$a} block';
 $string['hidedockpanel'] = 'Hide the dock panel';
 $string['hidepanel'] = 'Hide panel';
index b4ef21a..190e57d 100644 (file)
@@ -30,7 +30,6 @@ $string['AG'] = 'Antigua And Barbuda';
 $string['AI'] = 'Anguilla';
 $string['AL'] = 'Albania';
 $string['AM'] = 'Armenia';
-$string['AN'] = 'Netherlands Antilles';
 $string['AO'] = 'Angola';
 $string['AQ'] = 'Antarctica';
 $string['AR'] = 'Argentina';
@@ -52,7 +51,8 @@ $string['BJ'] = 'Benin';
 $string['BL'] = 'Saint Barthélemy';
 $string['BM'] = 'Bermuda';
 $string['BN'] = 'Brunei Darussalam';
-$string['BO'] = 'Bolivia';
+$string['BO'] = 'Bolivia, Plurinational State Of';
+$string['BQ'] = 'Bonaire, Sint Eustatius And Saba';
 $string['BR'] = 'Brazil';
 $string['BS'] = 'Bahamas';
 $string['BT'] = 'Bhutan';
@@ -65,6 +65,7 @@ $string['CC'] = 'Cocos (Keeling) Islands';
 $string['CD'] = 'Congo, The Democratic Republic Of The';
 $string['CF'] = 'Central African Republic';
 $string['CG'] = 'Congo';
+$string['CH'] = 'Switzerland';
 $string['CI'] = 'Côte D\'Ivoire';
 $string['CK'] = 'Cook Islands';
 $string['CL'] = 'Chile';
@@ -74,6 +75,7 @@ $string['CO'] = 'Colombia';
 $string['CR'] = 'Costa Rica';
 $string['CU'] = 'Cuba';
 $string['CV'] = 'Cape Verde';
+$string['CW'] = 'Curaçao';
 $string['CX'] = 'Christmas Island';
 $string['CY'] = 'Cyprus';
 $string['CZ'] = 'Czech Republic';
@@ -121,7 +123,6 @@ $string['HN'] = 'Honduras';
 $string['HR'] = 'Croatia';
 $string['HT'] = 'Haiti';
 $string['HU'] = 'Hungary';
-$string['CH'] = 'Switzerland';
 $string['ID'] = 'Indonesia';
 $string['IE'] = 'Ireland';
 $string['IL'] = 'Israel';
@@ -157,12 +158,12 @@ $string['LS'] = 'Lesotho';
 $string['LT'] = 'Lithuania';
 $string['LU'] = 'Luxembourg';
 $string['LV'] = 'Latvia';
-$string['LY'] = 'Libyan Arab Jamahiriya';
+$string['LY'] = 'Libya';
 $string['MA'] = 'Morocco';
 $string['MC'] = 'Monaco';
 $string['MD'] = 'Moldova, Republic Of';
 $string['ME'] = 'Montenegro';
-$string['MF'] = 'Saint Martin';
+$string['MF'] = 'Saint Martin (French Part)';
 $string['MG'] = 'Madagascar';
 $string['MH'] = 'Marshall Islands';
 $string['MK'] = 'Macedonia, The Former Yugoslav Republic Of';
@@ -204,7 +205,7 @@ $string['PL'] = 'Poland';
 $string['PM'] = 'Saint Pierre And Miquelon';
 $string['PN'] = 'Pitcairn';
 $string['PR'] = 'Puerto Rico';
-$string['PS'] = 'Palestinian Territory, Occupied';
+$string['PS'] = 'Palestine, State Of';
 $string['PT'] = 'Portugal';
 $string['PW'] = 'Palau';
 $string['PY'] = 'Paraguay';
@@ -220,7 +221,7 @@ $string['SC'] = 'Seychelles';
 $string['SD'] = 'Sudan';
 $string['SE'] = 'Sweden';
 $string['SG'] = 'Singapore';
-$string['SH'] = 'Saint Helena';
+$string['SH'] = 'Saint Helena, Ascension And Tristan Da Cunha';
 $string['SI'] = 'Slovenia';
 $string['SJ'] = 'Svalbard And Jan Mayen';
 $string['SK'] = 'Slovakia';
@@ -229,8 +230,10 @@ $string['SM'] = 'San Marino';
 $string['SN'] = 'Senegal';
 $string['SO'] = 'Somalia';
 $string['SR'] = 'Suriname';
+$string['SS'] = 'South Sudan';
 $string['ST'] = 'Sao Tome And Principe';
 $string['SV'] = 'El Salvador';
+$string['SX'] = 'Sint Maarten (Dutch Part)';
 $string['SY'] = 'Syrian Arab Republic';
 $string['SZ'] = 'Swaziland';
 $string['TC'] = 'Turks And Caicos Islands';
@@ -257,7 +260,7 @@ $string['UY'] = 'Uruguay';
 $string['UZ'] = 'Uzbekistan';
 $string['VA'] = 'Holy See (Vatican City State)';
 $string['VC'] = 'Saint Vincent And The Grenadines';
-$string['VE'] = 'Venezuela';
+$string['VE'] = 'Venezuela, Bolivarian Republic Of';
 $string['VG'] = 'Virgin Islands, British';
 $string['VI'] = 'Virgin Islands, U.S.';
 $string['VN'] = 'Viet Nam';
index fed4bbc..3be622d 100644 (file)
@@ -31,7 +31,7 @@ $string['activemethodinfonone'] = 'There is no advanced grading method selected
 $string['changeactivemethod'] = 'Change active grading method to';
 $string['clicktoclose'] = 'click to close';
 $string['exc_gradingformelement'] = 'Unable to instantiate grading form element';
-$string['formnotavailable'] = 'Advanced grading method was selected to use but the grading form is not available yet. You may need to define it first via a link in the Settings block.';
+$string['formnotavailable'] = 'Advanced grading method was selected to use but the grading form is not available yet. You may need to define it first via a link in the Administration block.';
 $string['gradingformunavailable'] = 'Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status.';
 $string['gradingmanagement'] = 'Advanced grading';
 $string['gradingmanagementtitle'] = 'Advanced grading: {$a->component} ({$a->area})';
index 4d4dd8c..ae31942 100644 (file)
@@ -29,6 +29,7 @@ $string['actions'] = 'Actions';
 $string['availability'] = 'Availability';
 $string['checkforupdates'] = 'Check for available updates';
 $string['checkforupdateslast'] = 'Last check done on {$a}';
+$string['detectedmisplacedplugin'] = 'Plugin "{$a->component}" is installed in incorrect location "{$a->current}", expected location is "{$a->expected}"';
 $string['displayname'] = 'Plugin name';
 $string['err_response_curl'] = 'Unable to fetch available updates data - unexpected cURL error.';
 $string['err_response_format_version'] = 'Unexpected version of the response format. Please try to re-check for available updates.';
index 3ab441e..79cc8d2 100644 (file)
@@ -54,4 +54,4 @@ $string['ratinginvalid'] = 'Rating is invalid';
 $string['ratingtime'] = 'Restrict ratings to items with dates in this range:';
 $string['ratings'] = 'Ratings';
 $string['rolewarning'] = 'Roles with permission to rate';
-$string['rolewarning_help'] = 'To submit ratings users require the moodle/rating:rate capability and any module specific capabilities. Users assigned the following roles should be able to rate items. The list of roles may be amended via the permissions link in the settings block.';
+$string['rolewarning_help'] = 'To submit ratings users require the moodle/rating:rate capability and any module specific capabilities. Users assigned the following roles should be able to rate items. The list of roles may be amended via the permissions link in the administration block.';
index 25713be..5ee707f 100644 (file)
@@ -158,7 +158,7 @@ $string['nofilesattached'] = 'No files attached';
 $string['nofilesavailable'] = 'No files available';
 $string['nomorefiles'] = 'No more attachments allowed';
 $string['nopathselected'] = 'No destination path select yet (double click tree node to select)';
-$string['nopermissiontoaccess'] = 'No permission to access this repository';
+$string['nopermissiontoaccess'] = 'No permission to access this repository.';
 $string['noresult'] = 'No search result';
 $string['norepositoriesavailable'] = 'Sorry, none of your current repositories can return files in the required format.';
 $string['norepositoriesexternalavailable'] = 'Sorry, none of your current repositories can return external files.';
index 41505e0..d703a77 100644 (file)
@@ -51,21 +51,6 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      */
     const TIMEOUT = 6;
 
-    /**
-     * Returns fixed step argument (with \\" replaced back to ").
-     *
-     * \\ is the chars combination to add when you
-     * want to escape the " character that is used as var
-     * delimiter.
-     *
-     * @see Behat\MinkExtension\Context\MinkContext
-     * @param string $argument
-     * @return string
-     */
-    protected function fixStepArgument($argument) {
-        return str_replace('\\"', '"', $argument);
-    }
-
     /**
      * Locates url, based on provided path.
      * Override to provide custom routing mechanism.
index c2bf778..04e0b3c 100644 (file)
@@ -49,10 +49,21 @@ class behat_form_editor extends behat_form_field {
      */
     public function set_value($value) {
 
-        // Set the value to the iframe and save it to the textarea.
-        $editorid = $this->field->getAttribute('id');
-        $this->session->executeScript('tinyMCE.get("'.$editorid.'").setContent("' . $value . '");');
-        $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+        // If tinyMCE var exists means that we are using that editor.
+        if ($this->is_editor_available()) {
+
+            // Set the value to the iframe and save it to the textarea.
+            $editorid = $this->field->getAttribute('id');
+
+            $this->session->executeScript('
+                tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
+                tinyMCE.get("'.$editorid.'").save();
+            ');
+
+        } else {
+            // Set the value to a textarea otherwise.
+            parent::set_value($value);
+        }
     }
 
     /**
@@ -62,12 +73,36 @@ class behat_form_editor extends behat_form_field {
      */
     public function get_value() {
 
-        // Save the current iframe value in case default value has been edited.
-        $editorid = $this->field->getAttribute('id');
-        $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+        // If tinyMCE var exists means that we are using that editor.
+        if ($this->is_editor_available()) {
+
+            // Save the current iframe value in case default value has been edited.
+            $editorid = $this->field->getAttribute('id');
+            $this->session->executeScript('tinyMCE.get("'.$editorid.'").save();');
+        }
 
         return $this->field->getValue();
     }
 
+    /**
+     * Returns if the HTML editor is available.
+     *
+     * The editor availability depends on the driver running the tests; Goutte
+     * can not execute Javascript, also some Moodle settings disables the HTML
+     * editor.
+     *
+     * @return bool
+     */
+    protected function is_editor_available() {
+
+        // Non-JS drivers throws exceptions when running JS.
+        try {
+            $available = $this->session->evaluateScript('return (typeof tinyMCE != "undefined")');
+        } catch (Exception $e) {
+            $available = false;
+        }
+
+        return $available;
+    }
 }
 
index 5066ae5..f374414 100644 (file)
@@ -1176,6 +1176,27 @@ class block_manager {
             $strdeletecheck = get_string('deletecheck', 'block', $blocktitle);
             $message = get_string('deleteblockcheck', 'block', $blocktitle);
 
+            // If the block is being shown in sub contexts display a warning.
+            if ($block->instance->showinsubcontexts == 1) {
+                $parentcontext = context::instance_by_id($block->instance->parentcontextid);
+                $systemcontext = context_system::instance();
+                $messagestring = new stdClass();
+                $messagestring->location = $parentcontext->get_context_name();
+
+                // Checking for blocks that may have visibility on the front page and pages added on that.
+                if ($parentcontext->id != $systemcontext->id && is_inside_frontpage($parentcontext)) {
+                    $messagestring->pagetype = get_string('showonfrontpageandsubs', 'block');
+                } else {
+                    $pagetypes = generate_page_type_patterns($this->page->pagetype, $parentcontext);
+                    $messagestring->pagetype = $block->instance->pagetypepattern;
+                    if (isset($pagetypes[$block->instance->pagetypepattern])) {
+                        $messagestring->pagetype = $pagetypes[$block->instance->pagetypepattern];
+                    }
+                }
+
+                $message = get_string('deleteblockwarning', 'block', $messagestring);
+            }
+
             $PAGE->navbar->add($strdeletecheck);
             $PAGE->set_title($blocktitle . ': ' . $strdeletecheck);
             $PAGE->set_heading($site->fullname);
index 8633ecb..affc574 100644 (file)
@@ -613,7 +613,6 @@ abstract class condition_info_base {
             'email' => get_user_field_name('email'),
             'city' => get_user_field_name('city'),
             'country' => get_user_field_name('country'),
-            'interests' => get_user_field_name('interests'),
             'url' => get_user_field_name('url'),
             'icq' => get_user_field_name('icq'),
             'skype' => get_user_field_name('skype'),
@@ -1297,7 +1296,7 @@ abstract class condition_info_base {
      * @param int $fieldid the user profile field id
      * @return string the user value, or false if user does not have a user field value yet
      */
-    private function get_cached_user_profile_field($userid, $fieldid) {
+    protected function get_cached_user_profile_field($userid, $fieldid) {
         global $USER, $DB, $CFG;
 
         if ($userid === 0) {
index b246000..0d4a626 100644 (file)
@@ -1747,7 +1747,13 @@ class css_rule {
         $css = $this->out();
         $errors = array();
         foreach ($this->styles as $style) {
-            if ($style->has_error()) {
+            if (is_array($style)) {
+                foreach ($style as $s) {
+                    if ($style instanceof css_style && $style->has_error()) {
+                        $errors[] = "  * ".$style->get_last_error();
+                    }
+                }
+            } else if ($style instanceof css_style && $style->has_error()) {
                 $errors[] = "  * ".$style->get_last_error();
             }
         }
index 1602999..2ba5735 100644 (file)
       </KEYS>
       <INDEXES>
         <INDEX NAME="action" UNIQUE="false" FIELDS="action" COMMENT="insert/update/delete"/>
+        <INDEX NAME="timemodified" UNIQUE="false" FIELDS="timemodified"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="grade_import_newitem" COMMENT="temporary table for storing new grade_item names from grade import">
index a2a8d93..725b52a 100644 (file)
@@ -273,6 +273,15 @@ $functions = array(
         'capabilities'=> 'moodle/user:create',
     ),
 
+    'core_user_get_users' => array(
+        'classname'   => 'core_user_external',
+        'methodname'  => 'get_users',
+        'classpath'   => 'user/externallib.php',
+        'description' => 'search for users matching the parameters',
+        'type'        => 'read',
+        'capabilities'=> 'moodle/user:viewdetails, moodle/user:viewhiddendetails, moodle/course:useremail, moodle/user:update',
+    ),
+
     'moodle_user_get_users_by_id' => array(
         'classname'   => 'core_user_external',
         'methodname'  => 'get_users_by_id',
@@ -505,6 +514,15 @@ $functions = array(
         'capabilities'=> 'moodle/course:delete',
     ),
 
+    'core_course_delete_modules' => array(
+        'classname' => 'core_course_external',
+        'methodname' => 'delete_modules',
+        'classpath' => 'course/externallib.php',
+        'description' => 'Deletes all specified module instances',
+        'type' => 'write',
+        'capabilities' => 'moodle/course:manageactivities'
+    ),
+
     'core_course_duplicate_course' => array(
         'classname'   => 'core_course_external',
         'methodname'  => 'duplicate_course',
@@ -664,6 +682,33 @@ $functions = array(
         'capabilities'=> 'moodle/notes:manage',
     ),
 
+    'core_notes_delete_notes' => array(
+        'classname'   => 'core_notes_external',
+        'methodname'  => 'delete_notes',
+        'classpath'   => 'notes/externallib.php',
+        'description' => 'Delete notes',
+        'type'        => 'write',
+        'capabilities'=> 'moodle/notes:manage',
+    ),
+
+    'core_notes_get_notes' => array(
+        'classname'   => 'core_notes_external',
+        'methodname'  => 'get_notes',
+        'classpath'   => 'notes/externallib.php',
+        'description' => 'Get notes',
+        'type'        => 'read',
+        'capabilities'=> 'moodle/notes:view',
+    ),
+
+    'core_notes_update_notes' => array(
+        'classname'   => 'core_notes_external',
+        'methodname'  => 'update_notes',
+        'classpath'   => 'notes/externallib.php',
+        'description' => 'Update notes',
+        'type'        => 'write',
+        'capabilities'=> 'moodle/notes:manage',
+    ),
+
     // === webservice related functions ===
 
     'moodle_webservice_get_siteinfo' => array(
index b563121..4478ecc 100644 (file)
@@ -1577,5 +1577,114 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2013021100.01);
     }
 
+    if ($oldversion < 2013021800.00) {
+        // Add the site identifier to the cache config's file.
+        $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
+        cache_helper::update_site_identifier($siteidentifier);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013021800.00);
+    }
+
+    if ($oldversion < 2013021801.00) {
+        // Fixing possible wrong MIME types for SMART Notebook files.
+        $extensions = array('%.gallery', '%.galleryitem', '%.gallerycollection', '%.nbk', '%.notebook', '%.xbk');
+        $select = $DB->sql_like('filename', '?', false);
+        foreach ($extensions as $extension) {
+            $DB->set_field_select(
+                'files',
+                'mimetype',
+                'application/x-smarttech-notebook',
+                $select,
+                array($extension)
+            );
+        }
+        upgrade_main_savepoint(true, 2013021801.00);
+    }
+
+    if ($oldversion < 2013021801.01) {
+        // Retrieve the list of course_sections as a recordset to save memory
+        $coursesections = $DB->get_recordset('course_sections', null, 'course, id', 'id, course, sequence');
+        foreach ($coursesections as $coursesection) {
+            // Retrieve all of the actual modules in this course and section combination to reduce DB calls
+            $actualsectionmodules = $DB->get_records('course_modules',
+                    array('course' => $coursesection->course, 'section' => $coursesection->id), '', 'id, section');
+
+            // Break out the current sequence so that we can compare it
+            $currentsequence = explode(',', $coursesection->sequence);
+            $newsequence = array();
+
+            // Check each of the modules in the current sequence
+            foreach ($currentsequence as $module) {
+                if (isset($actualsectionmodules[$module])) {
+                    $newsequence[] = $module;
+                    // We unset the actualsectionmodules so that we don't get duplicates and that we can add orphaned
+                    // modules later
+                    unset($actualsectionmodules[$module]);
+                }
+            }
+
+            // Append any modules which have somehow been orphaned
+            foreach ($actualsectionmodules as $module) {
+                $newsequence[] = $module->id;
+            }
+
+            // Piece it all back together
+            $sequence = implode(',', $newsequence);
+
+            // Only update if there have been changes
+            if ($sequence !== $coursesection->sequence) {
+                $coursesection->sequence = $sequence;
+                $DB->update_record('course_sections', $coursesection);
+
+                // And clear the sectioncache and modinfo cache - they'll be regenerated on next use
+                $course = new stdClass();
+                $course->id = $coursesection->course;
+                $course->sectioncache = null;
+                $course->modinfo = null;
+                $DB->update_record('course', $course);
+            }
+        }
+        $coursesections->close();
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013021801.01);
+    }
+
+    if ($oldversion < 2013021902.00) {
+        // ISO country change: Netherlands Antilles is split into BQ, CW & SX
+        // http://www.iso.org/iso/iso_3166-1_newsletter_vi-8_split_of_the_dutch_antilles_final-en.pdf
+        $sql = "UPDATE {user} SET country = '' WHERE country = ?";
+        $DB->execute($sql, array('AN'));
+
+        upgrade_main_savepoint(true, 2013021902.00);
+    }
+
+    if ($oldversion < 2013022600.00) {
+        // Delete entries regarding invalid 'interests' option which breaks course.
+        $DB->delete_records('course_sections_avail_fields', array('userfield' => 'interests'));
+        $DB->delete_records('course_modules_avail_fields', array('userfield' => 'interests'));
+        // Clear course cache (will be rebuilt on first visit) in case of changes to these.
+        rebuild_course_cache(0, true);
+
+        upgrade_main_savepoint(true, 2013022600.00);
+    }
+
+    // Add index to field "timemodified" for grade_grades_history table.
+    if ($oldversion < 2013030400.00) {
+        $table = new xmldb_table('grade_grades_history');
+        $field = new xmldb_field('timemodified');
+
+        if ($dbman->field_exists($table, $field)) {
+            $index = new xmldb_index('timemodified', XMLDB_INDEX_NOTUNIQUE, array('timemodified'));
+            if (!$dbman->index_exists($table, $index)) {
+                $dbman->add_index($table, $index);
+            }
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013030400.00);
+    }
+
     return true;
 }
index 005f30d..a2229b9 100644 (file)
  * used to interact with the DB. All the dunctions in this library must be
  * generic and work against the major number of RDBMS possible. This is the
  * list of currently supported and tested DBs: mysql, postresql, mssql, oracle
-
+ *
  * This library is automatically included by Moodle core so you never need to
  * include it yourself.
-
+ *
  * For more info about the functions available in this library, please visit:
  *     http://docs.moodle.org/en/DML_functions
  * (feel free to modify, improve and document such page, thanks!)
index d5b84aa..fc69751 100644 (file)
@@ -1469,6 +1469,10 @@ function &get_mimetypes_array() {
         'fdf'  => array ('type'=>'application/pdf', 'icon'=>'pdf'),
         'flv'  => array ('type'=>'video/x-flv', 'icon'=>'flash', 'groups'=>array('video','web_video'), 'string'=>'video'),
         'f4v'  => array ('type'=>'video/mp4', 'icon'=>'flash', 'groups'=>array('video','web_video'), 'string'=>'video'),
+
+        'gallery'           => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+        'galleryitem'       => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+        'gallerycollection' => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
         'gif'  => array ('type'=>'image/gif', 'icon'=>'gif', 'groups'=>array('image', 'web_image'), 'string'=>'image'),
         'gtar' => array ('type'=>'application/x-gtar', 'icon'=>'archive', 'groups'=>array('archive'), 'string'=>'archive'),
         'tgz'  => array ('type'=>'application/g-zip', 'icon'=>'archive', 'groups'=>array('archive'), 'string'=>'archive'),
@@ -1512,6 +1516,9 @@ function &get_mimetypes_array() {
         'mpe'  => array ('type'=>'video/mpeg', 'icon'=>'mpeg', 'groups'=>array('video','web_video'), 'string'=>'video'),
         'mpg'  => array ('type'=>'video/mpeg', 'icon'=>'mpeg', 'groups'=>array('video','web_video'), 'string'=>'video'),
 
+        'nbk'       => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+        'notebook'  => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
+
         'odt'  => array ('type'=>'application/vnd.oasis.opendocument.text', 'icon'=>'writer', 'groups'=>array('document')),
         'ott'  => array ('type'=>'application/vnd.oasis.opendocument.text-template', 'icon'=>'writer', 'groups'=>array('document')),
         'oth'  => array ('type'=>'application/vnd.oasis.opendocument.text-web', 'icon'=>'oth', 'groups'=>array('document')),
@@ -1591,6 +1598,8 @@ function &get_mimetypes_array() {
         'webm'  => array ('type'=>'video/webm', 'icon'=>'video', 'groups'=>array('video'), 'string'=>'video'),
         'wmv'  => array ('type'=>'video/x-ms-wmv', 'icon'=>'wmv', 'groups'=>array('video'), 'string'=>'video'),
         'asf'  => array ('type'=>'video/x-ms-asf', 'icon'=>'wmv', 'groups'=>array('video'), 'string'=>'video'),
+
+        'xbk'  => array ('type'=>'application/x-smarttech-notebook', 'icon'=>'archive'),
         'xdp'  => array ('type'=>'application/pdf', 'icon'=>'pdf'),
         'xfd'  => array ('type'=>'application/pdf', 'icon'=>'pdf'),
         'xfdf' => array ('type'=>'application/pdf', 'icon'=>'pdf'),
@@ -1605,6 +1614,7 @@ function &get_mimetypes_array() {
 
         'xml'  => array ('type'=>'application/xml', 'icon'=>'markup'),
         'xsl'  => array ('type'=>'text/xml', 'icon'=>'markup'),
+
         'zip'  => array ('type'=>'application/zip', 'icon'=>'archive', 'groups'=>array('archive'), 'string'=>'archive')
     );
     return $mimearray;
@@ -2935,14 +2945,22 @@ class curl {
     }
 
     /**
-     * Set curl options
+     * Set curl options.
      *
-     * @param array $options If array is null, this function will
-     * reset the options to default value.
+     * Do not use the curl constants to define the options, pass a string
+     * corresponding to that constant. Ie. to set CURLOPT_MAXREDIRS, pass
+     * array('CURLOPT_MAXREDIRS' => 10) or array('maxredirs' => 10) to this method.
+     *
+     * @param array $options If array is null, this function will reset the options to default value.
+     * @return void
+     * @throws coding_exception If an option uses constant value instead of option name.
      */
     public function setopt($options = array()) {
         if (is_array($options)) {
-            foreach($options as $name => $val){
+            foreach ($options as $name => $val){
+                if (!is_string($name)) {
+                    throw new coding_exception('Curl options should be defined using strings, not constant values.');
+                }
                 if (stripos($name, 'CURLOPT_') === false) {
                     $name = strtoupper('CURLOPT_'.$name);
                 }
@@ -3060,6 +3078,12 @@ class curl {
         }
         curl_setopt($curl, CURLOPT_HTTPHEADER, $this->header);
 
+        // Bypass proxy (for this request only) if required.
+        if (!empty($this->options['CURLOPT_URL']) &&
+                is_proxybypass($this->options['CURLOPT_URL'])) {
+            unset($this->options['CURLOPT_PROXY']);
+        }
+
         if ($this->debug){
             echo '<h1>Options</h1>';
             var_dump($this->options);
@@ -3067,11 +3091,9 @@ class curl {
             var_dump($this->header);
         }
 
-        // set options
+        // Set options.
         foreach($this->options as $name => $val) {
-            if (is_string($name)) {
-                $name = constant(strtoupper($name));
-            }
+            $name = constant(strtoupper($name));
             curl_setopt($curl, $name, $val);
         }
         return $curl;
@@ -3420,6 +3442,45 @@ class curl {
     public function get_errno() {
         return $this->errno;
     }
+
+    /**
+     * When using a proxy, an additional HTTP response code may appear at
+     * the start of the header. For example, when using https over a proxy
+     * there may be 'HTTP/1.0 200 Connection Established'. Other codes are
+     * also possible and some may come with their own headers.
+     *
+     * If using the return value containing all headers, this function can be
+     * called to remove unwanted doubles.
+     *
+     * Note that it is not possible to distinguish this situation from valid
+     * data unless you know the actual response part (below the headers)
+     * will not be included in this string, or else will not 'look like' HTTP
+     * headers. As a result it is not safe to call this function for general
+     * data.
+     *
+     * @param string $input Input HTTP response
+     * @return string HTTP response with additional headers stripped if any
+     */
+    public static function strip_double_headers($input) {
+        // I have tried to make this regular expression as specific as possible
+        // to avoid any case where it does weird stuff if you happen to put
+        // HTTP/1.1 200 at the start of any line in your RSS file. This should
+        // also make it faster because it can abandon regex processing as soon
+        // as it hits something that doesn't look like an http header. The
+        // header definition is taken from RFC 822, except I didn't support
+        // folding which is never used in practice.
+        $crlf = "\r\n";
+        return preg_replace(
+                // HTTP version and status code (ignore value of code).
+                '~^HTTP/1\..*' . $crlf .
+                // Header name: character between 33 and 126 decimal, except colon.
+                // Colon. Header value: any character except \r and \n. CRLF.
+                '(?:[\x21-\x39\x3b-\x7e]+:[^' . $crlf . ']+' . $crlf . ')*' .
+                // Headers are terminated by another CRLF (blank line).
+                $crlf .
+                // Second HTTP status code, this time must be 200.
+                '(HTTP/1.[01] 200 )~', '$1', $input);
+    }
 }
 
 /**
index 9c44430..8a40431 100644 (file)
@@ -180,6 +180,89 @@ class file_storage {
         return $preview;
     }
 
+    /**
+     * Return an available file name.
+     *
+     * This will return the next available file name in the area, adding/incrementing a suffix
+     * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
+     *
+     * If the file name passed is available without modification, it is returned as is.
+     *
+     * @param int $contextid context ID.
+     * @param string $component component.
+     * @param string $filearea file area.
+     * @param int $itemid area item ID.
+     * @param string $filepath the file path.
+     * @param string $filename the file name.
+     * @return string available file name.
+     * @throws coding_exception if the file name is invalid.
+     * @since 2.5
+     */
+    public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
+        global $DB;
+
+        // Do not accept '.' or an empty file name (zero is acceptable).
+        if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
+            throw new coding_exception('Invalid file name passed', $filename);
+        }
+
+        // The file does not exist, we return the same file name.
+        if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
+            return $filename;
+        }
+
+        // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
+        $pathinfo = pathinfo($filename);
+        $basename = $pathinfo['filename'];
+        $matches = array();
+        if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
+            $basename = $matches[1];
+        }
+
+        $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
+        if (isset($pathinfo['extension'])) {
+            $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
+        }
+
+        $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
+        $filenamelen = $DB->sql_length('f.filename');
+        $sql = "SELECT filename
+                FROM {files} f
+                WHERE
+                    f.contextid = :contextid AND
+                    f.component = :component AND
+                    f.filearea = :filearea AND
+                    f.itemid = :itemid AND
+                    f.filepath = :filepath AND
+                    $filenamelikesql
+                ORDER BY
+                    $filenamelen DESC,
+                    f.filename DESC";
+        $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
+                'filepath' => $filepath, 'filenamelike' => $filenamelike);
+        $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
+
+        // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
+        // would both be returned, but only the one only containing digits should be used.
+        $number = 1;
+        foreach ($results as $result) {
+            $resultbasename = pathinfo($result, PATHINFO_FILENAME);
+            $matches = array();
+            if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
+                $number = $matches[2] + 1;
+                break;
+            }
+        }
+
+        // Constructing the new filename.
+        $newfilename = $basename . ' (' . $number . ')';
+        if (isset($pathinfo['extension'])) {
+            $newfilename .= '.' . $pathinfo['extension'];
+        }
+
+        return $newfilename;
+    }
+
     /**
      * Generates a preview image for the stored file
      *
@@ -1635,6 +1718,11 @@ class file_storage {
     public function deleted_file_cleanup($contenthash) {
         global $DB;
 
+        if ($contenthash === sha1('')) {
+            // No need to delete empty content file with sha1('') content hash.
+            return;
+        }
+
         //Note: this section is critical - in theory file could be reused at the same
         //      time, if this happens we can still recover the file from trash
         if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
@@ -1852,8 +1940,8 @@ class file_storage {
         $referencehash = sha1($reference);
 
         $sql = "SELECT repositoryid, id FROM {files_reference}
-                 WHERE referencehash = ? and reference = ?";
-        $rs = $DB->get_recordset_sql($sql, array($referencehash, $reference));
+                 WHERE referencehash = ?";
+        $rs = $DB->get_recordset_sql($sql, array($referencehash));
 
         $now = time();
         foreach ($rs as $record) {
index fee7bfa..84d9510 100644 (file)
@@ -275,26 +275,32 @@ class stored_file {
     public function delete() {
         global $DB;
 
-        $transaction = $DB->start_delegated_transaction();
+        if ($this->is_directory()) {
+            // Directories can not be referenced, just delete the record.
+            $DB->delete_records('files', array('id'=>$this->file_record->id));
 
-        // If there are other files referring to this file, convert them to copies.
-        if ($files = $this->fs->get_references_by_storedfile($this)) {
-            foreach ($files as $file) {
-                $this->fs->import_external_file($file);
+        } else {
+            $transaction = $DB->start_delegated_transaction();
+
+            // If there are other files referring to this file, convert them to copies.
+            if ($files = $this->fs->get_references_by_storedfile($this)) {
+                foreach ($files as $file) {
+                    $this->fs->import_external_file($file);
+                }
             }
-        }
 
-        // If this file is a reference (alias) to another file, unlink it first.
-        if ($this->is_external_file()) {
-            $this->delete_reference();
-        }
+            // If this file is a reference (alias) to another file, unlink it first.
+            if ($this->is_external_file()) {
+                $this->delete_reference();
+            }
 
-        // Now delete the file record.
-        $DB->delete_records('files', array('id'=>$this->file_record->id));
+            // Now delete the file record.
+            $DB->delete_records('files', array('id'=>$this->file_record->id));
 
-        $transaction->allow_commit();
+            $transaction->allow_commit();
+        }
 
-        // moves pool file to trash if content not needed any more
+        // Move pool file to trash if content not needed any more.
         $this->fs->deleted_file_cleanup($this->file_record->contenthash);
         return true; // BC only
     }
index 049acfe..c634bfb 100644 (file)
@@ -1352,4 +1352,67 @@ class filestoragelib_testcase extends advanced_testcase {
             $aliasrecord->filearea, $aliasrecord->itemid, '/B/', 'symlink.txt');
         $this->assertTrue($symlink2->is_external_file());
     }
+
+    public function test_get_unused_filename() {
+        global $USER;
+        $this->resetAfterTest(true);
+
+        $fs = get_file_storage();
+        $this->setAdminUser();
+        $contextid = context_user::instance($USER->id)->id;
+        $component = 'user';
+        $filearea = 'private';
+        $itemid = 0;
+        $filepath = '/';
+
+        // Create some private files.
+        $file = new stdClass;
+        $file->contextid = $contextid;
+        $file->component = 'user';
+        $file->filearea  = 'private';
+        $file->itemid    = 0;
+        $file->filepath  = '/';
+        $file->source    = 'test';
+        $filenames = array('foo.txt', 'foo (1).txt', 'foo (20).txt', 'foo (999)', 'bar.jpg', 'What (a cool file).jpg',
+                'Hurray! (1).php', 'Hurray! (2).php', 'Hurray! (9a).php', 'Hurray! (abc).php');
+        foreach ($filenames as $key => $filename) {
+            $file->filename = $filename;
+            $userfile = $fs->create_file_from_string($file, "file $key $filename content");
+            $this->assertInstanceOf('stored_file', $userfile);
+        }
+
+        // Asserting new generated names.
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'unused.txt');
+        $this->assertEquals('unused.txt', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo.txt');
+        $this->assertEquals('foo (21).txt', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo (1).txt');
+        $this->assertEquals('foo (21).txt', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo (2).txt');
+        $this->assertEquals('foo (2).txt', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo (20).txt');
+        $this->assertEquals('foo (21).txt', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo');
+        $this->assertEquals('foo', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo (123)');
+        $this->assertEquals('foo (123)', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'foo (999)');
+        $this->assertEquals('foo (1000)', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'bar.png');
+        $this->assertEquals('bar.png', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'bar (12).png');
+        $this->assertEquals('bar (12).png', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'bar.jpg');
+        $this->assertEquals('bar (1).jpg', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'bar (1).jpg');
+        $this->assertEquals('bar (1).jpg', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'What (a cool file).jpg');
+        $this->assertEquals('What (a cool file) (1).jpg', $newfilename);
+        $newfilename = $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, 'Hurray! (1).php');
+        $this->assertEquals('Hurray! (3).php', $newfilename);
+
+        $this->setExpectedException('coding_exception');
+        $fs->get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, '');
+    }
+
 }
index b8760c0..a01de83 100644 (file)
@@ -90,6 +90,25 @@ class filter_manager {
         return self::$singletoninstance;
     }
 
+    /**
+     * Resets the caches, usually to be called between unit tests
+     */
+    public static function reset_caches() {
+        if (self::$singletoninstance) {
+            self::$singletoninstance->unload_all_filters();
+        }
+        self::$singletoninstance = null;
+    }
+
+    /**
+     * Unloads all filters and other cached information
+     */
+    protected function unload_all_filters() {
+        $this->textfilters = array();
+        $this->stringfilters = array();
+        $this->stringfilternames = array();
+    }
+
     /**
      * Load all the filters required by this context.
      *
@@ -286,6 +305,16 @@ class performance_measuring_filter_manager extends filter_manager {
     protected $textsfiltered = 0;
     protected $stringsfiltered = 0;
 
+    /**
+     * Unloads all filters and other cached information
+     */
+    protected function unload_all_filters() {
+        parent::unload_all_filters();
+        $this->filterscreated = 0;
+        $this->textsfiltered = 0;
+        $this->stringsfiltered = 0;
+    }
+
     /**
      * @param string $filtername
      * @param object $context
index 084f922..e6a6ad3 100644 (file)
@@ -796,12 +796,12 @@ M.form_dndupload.init = function(Y, options) {
                 extension = filename.substr(dotpos, filename.length);
             }
 
-            // Look to see if the name already has _NN at the end of it.
+            // Look to see if the name already has (NN) at the end of it.
             var number = 0;
-            var hasnumber = basename.match(/^(.*)_(\d+)$/);
-            if (hasnumber != null) {
+            var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
+            if (hasnumber !== null) {
                 // Note the current number & remove it from the basename.
-                number = parseInt(hasnumber[2]);
+                number = parseInt(hasnumber[2], 10);
                 basename = hasnumber[1];
             }
 
@@ -809,7 +809,7 @@ M.form_dndupload.init = function(Y, options) {
             var newname;
             do {
                 number++;
-                newname = basename + '_' + number + extension;
+                newname = basename + ' (' + number + ')' + extension;
             } while (this.has_name_clash(newname));
 
             return newname;
index 19080d9..917ad8c 100644 (file)
@@ -254,7 +254,7 @@ M.form_filemanager.init = function(Y, options) {
 
             this.msg_dlg.set('headerContent', header);
             this.msg_dlg_node.removeClass('fp-msg-info').removeClass('fp-msg-error').addClass('fp-msg-'+type)
-            this.msg_dlg_node.one('.fp-msg-text').setContent(msg);
+            this.msg_dlg_node.one('.fp-msg-text').setContent(Y.Escape.html(msg));
             this.msg_dlg.show();
         },
         is_disabled: function() {
@@ -325,7 +325,7 @@ M.form_filemanager.init = function(Y, options) {
                     }
                     this.mkdir_dialog.show();
                     Y.one('#fm-newname-'+scope.client_id).focus();
-                    Y.all('#fm-curpath-'+scope.client_id).setContent(this.currentpath)
+                    Y.all('#fm-curpath-'+scope.client_id).setContent(Y.Escape.html(this.currentpath))
                 }, this);
             } else {
                 this.filemanager.addClass('fm-nomkdir');
@@ -412,7 +412,7 @@ M.form_filemanager.init = function(Y, options) {
                     } else {
                         el.addClass('odd');
                     }
-                    el.one('.fp-path-folder-name').setContent(p[i].name).
+                    el.one('.fp-path-folder-name').setContent(Y.Escape.html(p[i].name)).
                         on('click', function(e, path) {
                             e.preventDefault();
                             if (!this.is_disabled()) {
@@ -602,7 +602,7 @@ M.form_filemanager.init = function(Y, options) {
             for (var i in licenses) {
                 var option = Y.Node.create('<option/>').
                     set('value', licenses[i].shortname).
-                    setContent(licenses[i].fullname);
+                    setContent(Y.Escape.html(licenses[i].fullname));
                 node.appendChild(option)
             }
         },
@@ -621,7 +621,7 @@ M.form_filemanager.init = function(Y, options) {
             node.setContent('');
             for (var i in list) {
                 node.appendChild(Y.Node.create('<option/>').
-                    set('value', list[i]).setContent(list[i]))
+                    set('value', list[i]).setContent(Y.Escape.html(list[i])));
             }
         },
         update_file: function(confirmed) {
@@ -923,7 +923,7 @@ M.form_filemanager.init = function(Y, options) {
                 if (selectnode.one('.fp-'+attrs[i])) {
                     var value = (node[attrs[i]+'_f']) ? node[attrs[i]+'_f'] : (node[attrs[i]] ? node[attrs[i]] : '');
                     selectnode.one('.fp-'+attrs[i]).addClassIf('fp-unknown', ''+value == '')
-                        .one('.fp-value').setContent(value);
+                        .one('.fp-value').setContent(Y.Escape.html(value));
                 }
             }
             // display thumbnail
@@ -948,7 +948,7 @@ M.form_filemanager.init = function(Y, options) {
                             selectnode.one('.fp-original').removeClass('fp-loading');
                             if (obj.original) {
                                 node.original = obj.original;
-                                selectnode.one('.fp-original .fp-value').setContent(node.original);
+                                selectnode.one('.fp-original .fp-value').setContent(Y.Escape.html(node.original));
                             } else {
                                 selectnode.one('.fp-original .fp-value').setContent(M.str.repository.unknownsource);
                             }
@@ -976,7 +976,7 @@ M.form_filemanager.init = function(Y, options) {
                                 for (var i in obj.references) {
                                     node.reflist += '<li>'+obj.references[i]+'</li>';
                                 }
-                                selectnode.one('.fp-reflist .fp-value').setContent(node.reflist);
+                                selectnode.one('.fp-reflist .fp-value').setContent(Y.Escape.html(node.reflist));
                             } else {
                                 selectnode.one('.fp-reflist .fp-value').setContent('');
                             }
index c874791..a14746d 100644 (file)
@@ -84,7 +84,12 @@ YUI.add('moodle-form-shortforms', function(Y) {
             fieldset.toggleClass(CSS.COLLAPSED);
             // Get corresponding hidden variable
             // - and invert it.
-            var statuselement = new Y.one('input[name=mform_isexpanded_'+fieldset.get('id')+']');
+            var statuselement = Y.one('input[name=mform_isexpanded_'+fieldset.get('id')+']');
+            if (!statuselement) {
+                Y.log("M.form.shortforms::switch_state was called on an fieldset without a status field: '" +
+                    fieldset.get('id') + "'", 'debug');
+                return;
+            }
             statuselement.set('value', Math.abs(Number(statuselement.get('value'))-1));
         }
     });
index 562e20e..f43e48f 100644 (file)
@@ -67,7 +67,12 @@ YUI.add('moodle-form-showadvanced', function(Y) {
             Y.one('#'+this.get('formid')).delegate('click', this.switch_state, SELECTORS.FIELDSETCONTAINSADVANCED+' .'+CSS.MORELESSTOGGLER);
         },
         process_fieldset : function(fieldset) {
-            var statuselement = new Y.one('input[name=mform_showmore_'+fieldset.get('id')+']');
+            var statuselement = Y.one('input[name=mform_showmore_'+fieldset.get('id')+']');
+            if (!statuselement) {
+                Y.log("M.form.showadvanced::process_fieldset was called on an fieldset without a status field: '" +
+                    fieldset.get('id') + "'", 'debug');
+                return;
+            }
             var morelesslink = Y.Node.create('<a href="#"></a>');
             morelesslink.addClass(CSS.MORELESSTOGGLER);
             if (statuselement.get('value') === '0') {
diff --git a/lib/google/Google_Client.php b/lib/google/Google_Client.php
new file mode 100644 (file)
index 0000000..e6026a1
--- /dev/null
@@ -0,0 +1,453 @@
+<?php
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Check for the required json and curl extensions, the Google APIs PHP Client
+// won't function without them.
+if (! function_exists('curl_init')) {
+  throw new Exception('Google PHP API Client requires the CURL PHP extension');
+}
+
+if (! function_exists('json_decode')) {
+  throw new Exception('Google PHP API Client requires the JSON PHP extension');
+}
+
+if (! function_exists('http_build_query')) {
+  throw new Exception('Google PHP API Client requires http_build_query()');
+}
+
+if (! ini_get('date.timezone') && function_exists('date_default_timezone_set')) {
+  date_default_timezone_set('UTC');
+}
+
+// hack around with the include paths a bit so the library 'just works'
+set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path());
+
+require_once "config.php";
+// If a local configuration file is found, merge it's values with the default configuration
+if (file_exists(dirname(__FILE__)  . '/local_config.php')) {
+  $defaultConfig = $apiConfig;
+  require_once (dirname(__FILE__)  . '/local_config.php');
+  $apiConfig = array_merge($defaultConfig, $apiConfig);
+}
+
+// Include the top level classes, they each include their own dependencies
+require_once 'service/Google_Model.php';
+require_once 'service/Google_Service.php';
+require_once 'service/Google_ServiceResource.php';
+require_once 'auth/Google_AssertionCredentials.php';
+require_once 'auth/Google_Signer.php';
+require_once 'auth/Google_P12Signer.php';
+require_once 'service/Google_BatchRequest.php';
+require_once 'external/URITemplateParser.php';
+require_once 'auth/Google_Auth.php';
+require_once 'cache/Google_Cache.php';
+require_once 'io/Google_IO.php';
+require_once('service/Google_MediaFileUpload.php');
+
+/**
+ * The Google API Client
+ * http://code.google.com/p/google-api-php-client/
+ *
+ * @author Chris Chabot <chabotc@google.com>
+ * @author Chirag Shah <chirags@google.com>
+ */
+class Google_Client {
+  /**
+   * @static
+   * @var Google_Auth $auth
+   */
+  static $auth;
+
+  /**
+   * @static
+   * @var Google_IO $io
+   */
+  static $io;
+
+  /**
+   * @static
+   * @var Google_Cache $cache
+   */
+  static $cache;
+
+  /**
+   * @static
+   * @var boolean $useBatch
+   */
+  static $useBatch = false;
+
+  /** @var array $scopes */
+  protected $scopes = array();
+
+  /** @var bool $useObjects */
+  protected $useObjects = false;
+
+  // definitions of services that are discovered.
+  protected $services = array();
+
+  // Used to track authenticated state, can't discover services after doing authenticate()
+  private $authenticated = false;
+
+  public function __construct($config = array()) {
+    global $apiConfig;
+    $apiConfig = array_merge($apiConfig, $config);
+    self::$cache = new $apiConfig['cacheClass']();
+    self::$auth = new $apiConfig['authClass']();
+    self::$io = new $apiConfig['ioClass']();
+  }
+
+  /**
+   * Add a service
+   */
+  public function addService($service, $version = false) {
+    global $apiConfig;
+    if ($this->authenticated) {
+      throw new Google_Exception('Cant add services after having authenticated');
+    }
+    $this->services[$service] = array();
+    if (isset($apiConfig['services'][$service])) {
+      // Merge the service descriptor with the default values
+      $this->services[$service] = array_merge($this->services[$service], $apiConfig['services'][$service]);
+    }
+  }
+
+  public function authenticate($code = null) {
+    $service = $this->prepareService();
+    $this->authenticated = true;
+    return self::$auth->authenticate($service, $code);
+  }
+
+  /**
+   * @return array
+   * @visible For Testing
+   */
+  public function prepareService() {
+    $service = array();
+    $scopes = array();
+    if ($this->scopes) {
+      $scopes = $this->scopes;
+    } else {
+      foreach ($this->services as $key => $val) {
+        if (isset($val['scope'])) {
+          if (is_array($val['scope'])) {
+            $scopes = array_merge($val['scope'], $scopes);
+          } else {
+            $scopes[] = $val['scope'];
+          }
+        } else {
+          $scopes[] = 'https://www.googleapis.com/auth/' . $key;
+        }
+        unset($val['discoveryURI']);
+        unset($val['scope']);
+        $service = array_merge($service, $val);
+      }
+    }
+    $service['scope'] = implode(' ', $scopes);
+    return $service;
+  }
+
+  /**
+   * Set the OAuth 2.0 access token using the string that resulted from calling authenticate()
+   * or Google_Client#getAccessToken().
+   * @param string $accessToken JSON encoded string containing in the following format:
+   * {"access_token":"TOKEN", "refresh_token":"TOKEN", "token_type":"Bearer",
+   *  "expires_in":3600, "id_token":"TOKEN", "created":1320790426}
+   */
+  public function setAccessToken($accessToken) {
+    if ($accessToken == null || 'null' == $accessToken) {
+      $accessToken = null;
+    }
+    self::$auth->setAccessToken($accessToken);
+  }
+
+  /**
+   * Set the type of Auth class the client should use.
+   * @param string $authClassName
+   */
+  public function setAuthClass($authClassName) {
+    self::$auth = new $authClassName();
+  }
+
+  /**
+   * Construct the OAuth 2.0 authorization request URI.
+   * @return string
+   */
+  public function createAuthUrl() {
+    $service = $this->prepareService();
+    return self::$auth->createAuthUrl($service['scope']);
+  }
+
+  /**
+   * Get the OAuth 2.0 access token.
+   * @return string $accessToken JSON encoded string in the following format:
+   * {"access_token":"TOKEN", "refresh_token":"TOKEN", "token_type":"Bearer",
+   *  "expires_in":3600,"id_token":"TOKEN", "created":1320790426}
+   */
+  public function getAccessToken() {
+    $token = self::$auth->getAccessToken();
+    return (null == $token || 'null' == $token) ? null : $token;
+  }
+
+  /**
+   * Returns if the access_token is expired.
+   * @return bool Returns True if the access_token is expired.
+   */
+  public function isAccessTokenExpired() {
+    return self::$auth->isAccessTokenExpired();
+  }
+
+  /**
+   * Set the developer key to use, these are obtained through the API Console.
+   * @see http://code.google.com/apis/console-help/#generatingdevkeys
+   * @param string $developerKey
+   */
+  public function setDeveloperKey($developerKey) {
+    self::$auth->setDeveloperKey($developerKey);
+  }
+
+  /**
+   * Set OAuth 2.0 "state" parameter to achieve per-request customization.
+   * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.1.2.2
+   * @param string $state
+   */
+  public function setState($state) {
+    self::$auth->setState($state);
+  }
+
+  /**
+   * @param string $accessType Possible values for access_type include:
+   *  {@code "offline"} to request offline access from the user. (This is the default value)
+   *  {@code "online"} to request online access from the user.
+   */
+  public function setAccessType($accessType) {
+    self::$auth->setAccessType($accessType);
+  }
+
+  /**
+   * @param string $approvalPrompt Possible values for approval_prompt include:
+   *  {@code "force"} to force the approval UI to appear. (This is the default value)
+   *  {@code "auto"} to request auto-approval when possible.
+   */
+  public function setApprovalPrompt($approvalPrompt) {
+    self::$auth->setApprovalPrompt($approvalPrompt);
+  }
+
+  /**
+   * Set the application name, this is included in the User-Agent HTTP header.
+   * @param string $applicationName
+   */
+  public function setApplicationName($applicationName) {
+    global $apiConfig;
+    $apiConfig['application_name'] = $applicationName;
+  }
+
+  /**
+   * Set the OAuth 2.0 Client ID.
+   * @param string $clientId
+   */
+  public function setClientId($clientId) {
+    global $apiConfig;
+    $apiConfig['oauth2_client_id'] = $clientId;
+    self::$auth->clientId = $clientId;
+  }
+
+  /**
+   * Get the OAuth 2.0 Client ID.
+   */
+  public function getClientId() {
+    return self::$auth->clientId;
+  }
+  
+  /**
+   * Set the OAuth 2.0 Client Secret.
+   * @param string $clientSecret
+   */
+  public function setClientSecret($clientSecret) {
+    global $apiConfig;
+    $apiConfig['oauth2_client_secret'] = $clientSecret;
+    self::$auth->clientSecret = $clientSecret;
+  }
+
+  /**
+   * Get the OAuth 2.0 Client Secret.
+   */
+  public function getClientSecret() {
+    return self::$auth->clientSecret;
+  }
+
+  /**
+   * Set the OAuth 2.0 Redirect URI.
+   * @param string $redirectUri
+   */
+  public function setRedirectUri($redirectUri) {
+    global $apiConfig;
+    $apiConfig['oauth2_redirect_uri'] = $redirectUri;
+    self::$auth->redirectUri = $redirectUri;
+  }
+
+  /**
+   * Get the OAuth 2.0 Redirect URI.
+   */
+  public function getRedirectUri() {
+    return self::$auth->redirectUri;
+  }
+
+  /**
+   * Fetches a fresh OAuth 2.0 access token with the given refresh token.
+   * @param string $refreshToken
+   * @return void
+   */
+  public function refreshToken($refreshToken) {
+    self::$auth->refreshToken($refreshToken);
+  }
+
+  /**
+   * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
+   * token, if a token isn't provided.
+   * @throws Google_AuthException
+   * @param string|null $token The token (access token or a refresh token) that should be revoked.
+   * @return boolean Returns True if the revocation was successful, otherwise False.
+   */
+  public function revokeToken($token = null) {
+    self::$auth->revokeToken($token);
+  }
+
+  /**
+   * Verify an id_token. This method will verify the current id_token, if one
+   * isn't provided.
+   * @throws Google_AuthException
+   * @param string|null $token The token (id_token) that should be verified.
+   * @return Google_LoginTicket Returns an apiLoginTicket if the verification was
+   * successful.
+   */
+  public function verifyIdToken($token = null) {
+    return self::$auth->verifyIdToken($token);
+  }
+
+  /**
+   * @param Google_AssertionCredentials $creds
+   * @return void
+   */
+  public function setAssertionCredentials(Google_AssertionCredentials $creds) {
+    self::$auth->setAssertionCredentials($creds);
+  }
+
+  /**
+   * This function allows you to overrule the automatically generated scopes,
+   * so that you can ask for more or less permission in the auth flow
+   * Set this before you call authenticate() though!
+   * @param array $scopes, ie: array('https://www.googleapis.com/auth/plus.me', 'https://www.googleapis.com/auth/moderator')
+   */
+  public function setScopes($scopes) {
+    $this->scopes = is_string($scopes) ? explode(" ", $scopes) : $scopes;
+  }
+
+  /**
+   * Declare if objects should be returned by the api service classes.
+   *
+   * @param boolean $useObjects True if objects should be returned by the service classes.
+   * False if associative arrays should be returned (default behavior).
+   * @experimental
+   */
+  public function setUseObjects($useObjects) {
+    global $apiConfig;
+    $apiConfig['use_objects'] = $useObjects;
+  }
+
+  /**
+   * Declare if objects should be returned by the api service classes.
+   *
+   * @param boolean $useBatch True if the experimental batch support should
+   * be enabled. Defaults to False.
+   * @experimental
+   */
+  public function setUseBatch($useBatch) {
+    self::$useBatch = $useBatch;
+  }
+
+  /**
+   * @static
+   * @return Google_Auth the implementation of apiAuth.
+   */
+  public static function getAuth() {
+    return Google_Client::$auth;
+  }
+
+  /**
+   * @static
+   * @return Google_IO the implementation of apiIo.
+   */
+  public static function getIo() {
+    return Google_Client::$io;
+  }
+
+  /**
+   * @return Google_Cache the implementation of apiCache.
+   */
+  public function getCache() {
+    return Google_Client::$cache;
+  }
+}
+
+// Exceptions that the Google PHP API Library can throw
+class Google_Exception extends Exception {}
+class Google_AuthException extends Google_Exception {}
+class Google_CacheException extends Google_Exception {}
+class Google_IOException extends Google_Exception {}
+class Google_ServiceException extends Google_Exception {
+  /**
+   * Optional list of errors returned in a JSON body of an HTTP error response.
+   */
+  protected $errors = array();
+
+  /**
+   * Override default constructor to add ability to set $errors.
+   *
+   * @param string $message
+   * @param int $code
+   * @param Exception|null $previous
+   * @param [{string, string}] errors List of errors returned in an HTTP
+   * response.  Defaults to [].
+   */
+  public function __construct($message, $code = 0, Exception $previous = null,
+                              $errors = array()) {
+    if(version_compare(PHP_VERSION, '5.3.0') >= 0) {
+      parent::__construct($message, $code, $previous);
+    } else {
+      parent::__construct($message, $code);
+    }
+    
+    $this->errors = $errors;
+  }
+
+  /**
+   * An example of the possible errors returned.
+   *
+   * {
+   *   "domain": "global",
+   *   "reason": "authError",
+   *   "message": "Invalid Credentials",
+   *   "locationType": "header",
+   *   "location": "Authorization",
+   * }
+   *
+   * @return [{string, string}] List of errors return in an HTTP response or [].
+   */
+  public function getErrors() {
+    return $this->errors;
+  }
+}
diff --git a/lib/google/LICENSE b/lib/google/LICENSE
new file mode 100644 (file)
index 0000000..a148ba5
--- /dev/null
@@ -0,0 +1,203 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction,
+and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by
+the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all
+other entities that control, are controlled by, or are under common
+control with that entity. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or
+otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity
+exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation
+source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical
+transformation or translation of a Source form, including but
+not limited to compiled object code, generated documentation,
+and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or
+Object form, made available under the License, as indicated by a
+copyright notice that is included in or attached to the work
+(an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object
+form, that is based on (or derived from) the Work and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship. For the purposes
+of this License, Derivative Works shall not include works that remain
+separable from, or merely link (or bind by name) to the interfaces of,
+the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including
+the original version of the Work and any modifications or additions
+to that Work or Derivative Works thereof, that is intentionally
+submitted to Licensor for inclusion in the Work by the copyright owner
+or by an individual or Legal Entity authorized to submit on behalf of
+the copyright owner. For the purposes of this definition, "submitted"
+means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems,
+and issue tracking systems that are managed by, or on behalf of, the
+Licensor for the purpose of discussing and improving the Work, but
+excluding communication that is conspicuously marked or otherwise
+designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity
+on behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the
+Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+(except as stated in this section) patent license to make, have made,
+use, offer to sell, sell, import, and otherwise transfer the Work,
+where such license applies only to those patent claims licensable
+by such Contributor that are necessarily infringed by their
+Contribution(s) alone or by combination of their Contribution(s)
+with the Work to which such Contribution(s) was submitted. If You
+institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work
+or a Contribution incorporated within the Work constitutes direct
+or contributory patent infringement, then any patent licenses
+granted to You under this License for that Work shall terminate
+as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+Work or Derivative Works thereof in any medium, with or without
+modifications, and in Source or Object form, provided that You
+meet the following conditions:
+
+(a) You must give any other recipients of the Work or
+Derivative Works a copy of this License; and
+
+(b) You must cause any modified files to carry prominent notices
+stating that You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works
+that You distribute, all copyright, patent, trademark, and
+attribution notices from the Source form of the Work,
+excluding those notices that do not pertain to any part of
+the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its
+distribution, then any Derivative Works that You distribute must
+include a readable copy of the attribution notices contained
+within such NOTICE file, excluding those notices that do not
+pertain to any part of the Derivative Works, in at least one
+of the following places: within a NOTICE text file distributed
+as part of the Derivative Works; within the Source form or
+documentation, if provided along with the Derivative Works; or,
+within a display generated by the Derivative Works, if and
+wherever such third-party notices normally appear. The contents
+of the NOTICE file are for informational purposes only and
+do not modify the License. You may add Your own attribution
+notices within Derivative Works that You distribute, alongside
+or as an addendum to the NOTICE text from the Work, provided
+that such additional attribution notices cannot be construed
+as modifying the License.
+
+You may add Your own copyright statement to Your modifications and
+may provide additional or different license terms and conditions
+for use, reproduction, or distribution of Your modifications, or
+for any such Derivative Works as a whole, provided Your use,
+reproduction, and distribution of the Work otherwise complies with
+the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+any Contribution intentionally submitted for inclusion in the Work
+by You to the Licensor shall be under the terms and conditions of
+this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify
+the terms of any separate license agreement you may have executed
+with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+names, trademarks, service marks, or product names of the Licensor,
+except as required for reasonable and customary use in describing the
+origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+agreed to in writing, Licensor provides the Work (and each
+Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied, including, without limitation, any warranties or conditions
+of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Work and assume any
+risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+whether in tort (including negligence), contract, or otherwise,
+unless required by applicable law (such as deliberate and grossly
+negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special,
+incidental, or consequential damages of any character arising as a
+result of this License or out of the use or inability to use the
+Work (including but not limited to damages for loss of goodwill,
+work stoppage, computer failure or malfunction, or any and all
+other commercial damages or losses), even if such Contributor
+has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+the Work or Derivative Works thereof, You may choose to offer,
+and charge a fee for, acceptance of support, warranty, indemnity,
+or other liability obligations and/or rights consistent with this
+License. However, in accepting such obligations, You may act only
+on Your own behalf and on Your sole responsibility, not on behalf
+of any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason
+of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following
+boilerplate notice, with the fields enclosed by brackets "[]"
+replaced with your own identifying information. (Don't include
+the brackets!) The text should be enclosed in the appropriate
+comment syntax for the file format. We also recommend that a
+file or class name and description of purpose be included on the
+same "printed page" as the copyright notice for easier
+identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
diff --git a/lib/google/NOTICE b/lib/google/NOTICE
new file mode 100644 (file)
index 0000000..22d7cb5
--- /dev/null
@@ -0,0 +1,4 @@
+This product contains the following libraries:
+
+XRDS-Simple library from http://code.google.com/p/diso/
+Apache License 2.0
diff --git a/lib/google/README b/lib/google/README
new file mode 100644 (file)
index 0000000..42c42c0
--- /dev/null
@@ -0,0 +1,40 @@
+Google APIs Client Library for PHP
+=====================================
+
+== Description
+The Google API Client Library enables you to work with Google APIs such as Google+, Drive, Tasks, or Latitude on your server.
+
+Requirements:
+  PHP 5.2.x or higher [http://www.php.net/]
+  PHP Curl extension [http://www.php.net/manual/en/intro.curl.php]
+  PHP JSON extension [http://php.net/manual/en/book.json.php]
+
+Project page:
+  http://code.google.com/p/google-api-php-client
+
+OAuth 2 instructions:
+  http://code.google.com/p/google-api-php-client/wiki/OAuth2
+
+Report a defect or feature request here:
+  http://code.google.com/p/google-api-php-client/issues/entry
+
+Subscribe to project updates in your feed reader:
+  http://code.google.com/feeds/p/google-api-php-client/updates/basic
+
+Supported sample applications:
+  http://code.google.com/p/google-api-php-client/wiki/Samples
+
+== Basic Example
+  <?php
+  require_once 'path/to/src/Google_Client.php';
+  require_once 'path/to/src/contrib/apiBooksService.php';
+
+  $client = new Google_Client();
+  $service = new Google_BooksService($client);
+
+  $optParams = array('filter' => 'free-ebooks');
+  $results = $service->volumes->listVolumes('Henry David Thoreau', $optParams);
+
+  foreach ($results['items'] as $item) {
+    print($item['volumeInfo']['title'] . '<br>');
+  }
diff --git a/lib/google/auth/Google_AssertionCredentials.php b/lib/google/auth/Google_AssertionCredentials.php
new file mode 100644 (file)
index 0000000..0d7aeb3
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Credentials object used for OAuth 2.0 Signed JWT assertion grants.
+ *
+ * @author Chirag Shah <chirags@google.com>
+ */
+class Google_AssertionCredentials {
+  const MAX_TOKEN_LIFETIME_SECS = 3600;
+
+  public $serviceAccountName;
+  public $scopes;
+  public $privateKey;
+  public $privateKeyPassword;
+  public $assertionType;
+  public $prn;
+
+  /**
+   * @param $serviceAccountName
+   * @param $scopes array List of scopes
+   * @param $privateKey
+   * @param string $privateKeyPassword
+   * @param string $assertionType
+   * @param bool|string $prn The email address of the user for which the
+   *               application is requesting delegated access.
+   */
+  public function __construct(
+      $serviceAccountName,
+      $scopes,
+      $privateKey,
+      $privateKeyPassword = 'notasecret',
+      $assertionType = 'http://oauth.net/grant_type/jwt/1.0/bearer',
+      $prn = false) {
+    $this->serviceAccountName = $serviceAccountName;
+    $this->scopes = is_string($scopes) ? $scopes : implode(' ', $scopes);
+    $this->privateKey = $privateKey;
+    $this->privateKeyPassword = $privateKeyPassword;
+    $this->assertionType = $assertionType;
+    $this->prn = $prn;
+  }
+
+  public function generateAssertion() {
+    $now = time();
+
+    $jwtParams = array(
+          'aud' => Google_OAuth2::OAUTH2_TOKEN_URI,
+          'scope' => $this->scopes,
+          'iat' => $now,
+          'exp' => $now + self::MAX_TOKEN_LIFETIME_SECS,
+          'iss' => $this->serviceAccountName,
+    );
+
+    if ($this->prn !== false) {
+      $jwtParams['prn'] = $this->prn;
+    }
+
+    return $this->makeSignedJwt($jwtParams);
+  }
+
+  /**
+   * Creates a signed JWT.
+   * @param array $payload
+   * @return string The signed JWT.
+   */
+  private function makeSignedJwt($payload) {
+    $header = array('typ' => 'JWT', 'alg' => 'RS256');
+
+    $segments = array(
+      Google_Utils::urlSafeB64Encode(json_encode($header)),
+      Google_Utils::urlSafeB64Encode(json_encode($payload))
+    );
+
+    $signingInput = implode('.', $segments);
+    $signer = new Google_P12Signer($this->privateKey, $this->privateKeyPassword);
+    $signature = $signer->sign($signingInput);
+    $segments[] = Google_Utils::urlSafeB64Encode($signature);
+
+    return implode(".", $segments);
+  }
+}
diff --git a/lib/google/auth/Google_Auth.php b/lib/google/auth/Google_Auth.php
new file mode 100644 (file)
index 0000000..010782d
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+require_once "Google_AuthNone.php";
+require_once "Google_OAuth2.php";
+
+/**
+ * Abstract class for the Authentication in the API client
+ * @author Chris Chabot <chabotc@google.com>
+ *
+ */
+abstract class Google_Auth {
+  abstract public function authenticate($service);
+  abstract public function sign(Google_HttpRequest $request);
+  abstract public function createAuthUrl($scope);
+
+  abstract public function getAccessToken();
+  abstract public function setAccessToken($accessToken);
+  abstract public function setDeveloperKey($developerKey);
+  abstract public function refreshToken($refreshToken);
+  abstract public function revokeToken();
+}
diff --git a/lib/google/auth/Google_AuthNone.php b/lib/google/auth/Google_AuthNone.php
new file mode 100644 (file)
index 0000000..6ca6bc2
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Do-nothing authentication implementation, use this if you want to make un-authenticated calls
+ * @author Chris Chabot <chabotc@google.com>
+ * @author Chirag Shah <chirags@google.com>
+ */
+class Google_AuthNone extends Google_Auth {
+  public $key = null;
+
+  public function __construct() {
+    global $apiConfig;
+    if (!empty($apiConfig['developer_key'])) {
+      $this->setDeveloperKey($apiConfig['developer_key']);
+    }
+  }
+
+  public function setDeveloperKey($key) {$this->key = $key;}
+  public function authenticate($service) {/*noop*/}
+  public function setAccessToken($accessToken) {/* noop*/}
+  public function getAccessToken() {return null;}
+  public function createAuthUrl($scope) {return null;}
+  public function refreshToken($refreshToken) {/* noop*/}
+  public function revokeToken() {/* noop*/}
+
+  public function sign(Google_HttpRequest $request) {
+    if ($this->key) {
+      $request->setUrl($request->getUrl() . ((strpos($request->getUrl(), '?') === false) ? '?' : '&')
+          . 'key='.urlencode($this->key));
+    }
+    return $request;
+  }
+}
diff --git a/lib/google/auth/Google_LoginTicket.php b/lib/google/auth/Google_LoginTicket.php
new file mode 100644 (file)
index 0000000..c0ce614
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Class to hold information about an authenticated login.
+ *
+ * @author Brian Eaton <beaton@google.com>
+ */
+class Google_LoginTicket {
+  const USER_ATTR = "id";
+
+  // Information from id token envelope.
+  private $envelope;
+
+  // Information from id token payload.
+  private $payload;
+
+  /**
+   * Creates a user based on the supplied token.
+   *
+   * @param string $envelope Header from a verified authentication token.
+   * @param string $payload Information from a verified authentication token.
+   */
+  public function __construct($envelope, $payload) {
+    $this->envelope = $envelope;
+    $this->payload = $payload;
+  }
+
+  /**
+   * Returns the numeric identifier for the user.
+   * @throws Google_AuthException
+   * @return
+   */
+  public function getUserId() {
+    if (array_key_exists(self::USER_ATTR, $this->payload)) {
+      return $this->payload[self::USER_ATTR];
+    }
+    throw new Google_AuthException("No user_id in token");
+  }
+
+  /**
+   * Returns attributes from the login ticket.  This can contain
+   * various information about the user session.
+   * @return array
+   */
+  public function getAttributes() {
+    return array("envelope" => $this->envelope, "payload" => $this->payload);
+  }
+}
diff --git a/lib/google/auth/Google_OAuth2.php b/lib/google/auth/Google_OAuth2.php
new file mode 100644 (file)
index 0000000..7394316
--- /dev/null
@@ -0,0 +1,444 @@
+<?php
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+require_once "Google_Verifier.php";
+require_once "Google_LoginTicket.php";
+require_once "service/Google_Utils.php";
+
+/**
+ * Authentication class that deals with the OAuth 2 web-server authentication flow
+ *
+ * @author Chris Chabot <chabotc@google.com>
+ * @author Chirag Shah <chirags@google.com>
+ *
+ */
+class Google_OAuth2 extends Google_Auth {
+  public $clientId;
+  public $clientSecret;
+  public $developerKey;
+  public $token;
+  public $redirectUri;
+  public $state;
+  public $accessType = 'offline';
+  public $approvalPrompt = 'force';
+
+  /** @var Google_AssertionCredentials $assertionCredentials */
+  public $assertionCredentials;
+
+  const OAUTH2_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke';
+  const OAUTH2_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token';
+  const OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth';
+  const OAUTH2_FEDERATED_SIGNON_CERTS_URL = 'https://www.googleapis.com/oauth2/v1/certs';
+  const CLOCK_SKEW_SECS = 300; // five minutes in seconds
+  const AUTH_TOKEN_LIFETIME_SECS = 300; // five minutes in seconds
+  const MAX_TOKEN_LIFETIME_SECS = 86400; // one day in seconds
+
+  /**
+   * Instantiates the class, but does not initiate the login flow, leaving it
+   * to the discretion of the caller (which is done by calling authenticate()).
+   */
+  public function __construct() {
+    global $apiConfig;
+    
+    if (! empty($apiConfig['developer_key'])) {
+      $this->developerKey = $apiConfig['developer_key'];
+    }
+
+    if (! empty($apiConfig['oauth2_client_id'])) {
+      $this->clientId = $apiConfig['oauth2_client_id'];
+    }
+
+    if (! empty($apiConfig['oauth2_client_secret'])) {
+      $this->clientSecret = $apiConfig['oauth2_client_secret'];
+    }
+
+    if (! empty($apiConfig['oauth2_redirect_uri'])) {
+      $this->redirectUri = $apiConfig['oauth2_redirect_uri'];
+    }
+    
+    if (! empty($apiConfig['oauth2_access_type'])) {
+      $this->accessType = $apiConfig['oauth2_access_type'];
+    }
+
+    if (! empty($apiConfig['oauth2_approval_prompt'])) {
+      $this->approvalPrompt = $apiConfig['oauth2_approval_prompt'];
+    }
+  }
+
+  /**
+   * @param $service
+   * @param string|null $code
+   * @throws Google_AuthException
+   * @return string
+   */
+  public function authenticate($service, $code = null) {
+    if (!$code && isset($_GET['code'])) {
+      $code = $_GET['code'];
+    }
+
+    if ($code) {
+      // We got here from the redirect from a successful authorization grant, fetch the access token
+      $request = Google_Client::$io->makeRequest(new Google_HttpRequest(self::OAUTH2_TOKEN_URI, 'POST', array(), array(
+          'code' => $code,
+          'grant_type' => 'authorization_code',
+          'redirect_uri' => $this->redirectUri,
+          'client_id' => $this->clientId,
+          'client_secret' => $this->clientSecret
+      )));
+
+      if ($request->getResponseHttpCode() == 200) {
+        $this->setAccessToken($request->getResponseBody());
+        $this->token['created'] = time();
+        return $this->getAccessToken();
+      } else {
+        $response = $request->getResponseBody();
+        $decodedResponse = json_decode($response, true);
+        if ($decodedResponse != null && $decodedResponse['error']) {
+          $response = $decodedResponse['error'];
+        }
+        throw new Google_AuthException("Error fetching OAuth2 access token, message: '$response'", $request->getResponseHttpCode());
+      }
+    }
+
+    $authUrl = $this->createAuthUrl($service['scope']);
+    header('Location: ' . $authUrl);
+    return true;
+  } 
+
+  /**
+   * Create a URL to obtain user authorization.
+   * The authorization endpoint allows the user to first
+   * authenticate, and then grant/deny the access request.
+   * @param string $scope The scope is expressed as a list of space-delimited strings.
+   * @return string
+   */
+  public function createAuthUrl($scope) {
+    $params = array(
+        'response_type=code',
+        'redirect_uri=' . urlencode($this->redirectUri),
+        'client_id=' . urlencode($this->clientId),
+        'scope=' . urlencode($scope),
+        'access_type=' . urlencode($this->accessType),
+        'approval_prompt=' . urlencode($this->approvalPrompt)
+    );
+
+    if (isset($this->state)) {
+      $params[] = 'state=' . urlencode($this->state);
+    }
+    $params = implode('&', $params);
+    return self::OAUTH2_AUTH_URL . "?$params";
+  }
+
+  /**
+   * @param string $token
+   * @throws Google_AuthException
+   */
+  public function setAccessToken($token) {
+    $token = json_decode($token, true);
+    if ($token == null) {
+      throw new Google_AuthException('Could not json decode the token');
+    }
+    if (! isset($token['access_token'])) {
+      throw new Google_AuthException("Invalid token format");
+    }
+    $this->token = $token;
+  }
+
+  public function getAccessToken() {
+    return json_encode($this->token);
+  }
+
+  public function setDeveloperKey($developerKey) {
+    $this->developerKey = $developerKey;
+  }
+
+  public function setState($state) {
+    $this->state = $state;
+  }
+
+  public function setAccessType($accessType) {
+    $this->accessType = $accessType;
+  }
+
+  public function setApprovalPrompt($approvalPrompt) {
+    $this->approvalPrompt = $approvalPrompt;
+  }
+
+  public function setAssertionCredentials(Google_AssertionCredentials $creds) {
+    $this->assertionCredentials = $creds;
+  }
+
+  /**
+   * Include an accessToken in a given apiHttpRequest.
+   * @param Google_HttpRequest $request
+   * @return Google_HttpRequest
+   * @throws Google_AuthException
+   */
+  public function sign(Google_HttpRequest $request) {
+    // add the developer key to the request before signing it
+    if ($this->developerKey) {
+      $requestUrl = $request->getUrl();
+      $requestUrl .= (strpos($request->getUrl(), '?') === false) ? '?' : '&';
+      $requestUrl .=  'key=' . urlencode($this->developerKey);
+      $request->setUrl($requestUrl);
+    }
+
+    // Cannot sign the request without an OAuth access token.
+    if (null == $this->token && null == $this->assertionCredentials) {
+      return $request;
+    }
+
+    // Check if the token is set to expire in the next 30 seconds
+    // (or has already expired).
+    if ($this->isAccessTokenExpired()) {
+      if ($this->assertionCredentials) {
+        $this->refreshTokenWithAssertion();
+      } else {
+        if (! array_key_exists('refresh_token', $this->token)) {
+            throw new Google_AuthException("The OAuth 2.0 access token has expired, "
+                . "and a refresh token is not available. Refresh tokens are not "
+                . "returned for responses that were auto-approved.");
+        }
+        $this->refreshToken($this->token['refresh_token']);
+      }
+    }
+
+    // Add the OAuth2 header to the request
+    $request->setRequestHeaders(
+        array('Authorization' => 'Bearer ' . $this->token['access_token'])
+    );
+
+    return $request;
+  }
+
+  /**
+   * Fetches a fresh access token with the given refresh token.
+   * @param string $refreshToken
+   * @return void
+   */
+  public function refreshToken($refreshToken) {
+    $this->refreshTokenRequest(array(
+        'client_id' => $this->clientId,
+        'client_secret' => $this->clientSecret,
+        'refresh_token' => $refreshToken,
+        'grant_type' => 'refresh_token'
+    ));
+  }
+
+  /**
+   * Fetches a fresh access token with a given assertion token.
+   * @param Google_AssertionCredentials $assertionCredentials optional.
+   * @return void
+   */
+  public function refreshTokenWithAssertion($assertionCredentials = null) {
+    if (!$assertionCredentials) {
+      $assertionCredentials = $this->assertionCredentials;
+    }
+
+    $this->refreshTokenRequest(array(
+        'grant_type' => 'assertion',
+        'assertion_type' => $assertionCredentials->assertionType,
+        'assertion' => $assertionCredentials->generateAssertion(),
+    ));
+  }
+
+  private function refreshTokenRequest($params) {
+    $http = new Google_HttpRequest(self::OAUTH2_TOKEN_URI, 'POST', array(), $params);
+    $request = Google_Client::$io->makeRequest($http);
+
+    $code = $request->getResponseHttpCode();
+    $body = $request->getResponseBody();
+    if (200 == $code) {
+      $token = json_decode($body, true);
+      if ($token == null) {
+        throw new Google_AuthException("Could not json decode the access token");
+      }
+
+      if (! isset($token['access_token']) || ! isset($token['expires_in'])) {
+        throw new Google_AuthException("Invalid token format");
+      }
+
+      $this->token['access_token'] = $token['access_token'];
+      $this->token['expires_in'] = $token['expires_in'];
+      $this->token['created'] = time();
+    } else {
+      throw new Google_AuthException("Error refreshing the OAuth2 token, message: '$body'", $code);
+    }
+  }
+
+    /**
+     * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
+     * token, if a token isn't provided.
+     * @throws Google_AuthException
+     * @param string|null $token The token (access token or a refresh token) that should be revoked.
+     * @return boolean Returns True if the revocation was successful, otherwise False.
+     */
+  public function revokeToken($token = null) {
+    if (!$token) {
+      $token = $this->token['access_token'];
+    }
+    $request = new Google_HttpRequest(self::OAUTH2_REVOKE_URI, 'POST', array(), "token=$token");
+    $response = Google_Client::$io->makeRequest($request);
+    $code = $response->getResponseHttpCode();
+    if ($code == 200) {
+      $this->token = null;
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Returns if the access_token is expired.
+   * @return bool Returns True if the access_token is expired.
+   */
+  public function isAccessTokenExpired() {
+    if (null == $this->token) {
+      return true;
+    }
+
+    // If the token is set to expire in the next 30 seconds.
+    $expired = ($this->token['created']
+        + ($this->token['expires_in'] - 30)) < time();
+
+    return $expired;
+  }
+
+  // Gets federated sign-on certificates to use for verifying identity tokens.
+  // Returns certs as array structure, where keys are key ids, and values
+  // are PEM encoded certificates.
+  private function getFederatedSignOnCerts() {
+    // This relies on makeRequest caching certificate responses.
+    $request = Google_Client::$io->makeRequest(new Google_HttpRequest(
+        self::OAUTH2_FEDERATED_SIGNON_CERTS_URL));
+    if ($request->getResponseHttpCode() == 200) {
+      $certs = json_decode($request->getResponseBody(), true);
+      if ($certs) {
+        return $certs;
+      }
+    }
+    throw new Google_AuthException(
+        "Failed to retrieve verification certificates: '" .
+            $request->getResponseBody() . "'.",
+        $request->getResponseHttpCode());
+  }
+
+  /**
+   * Verifies an id token and returns the authenticated apiLoginTicket.
+   * Throws an exception if the id token is not valid.
+   * The audience parameter can be used to control which id tokens are
+   * accepted.  By default, the id token must have been issued to this OAuth2 client.
+   *
+   * @param $id_token
+   * @param $audience
+   * @return Google_LoginTicket
+   */
+  public function verifyIdToken($id_token = null, $audience = null) {
+    if (!$id_token) {
+      $id_token = $this->token['id_token'];
+    }
+
+    $certs = $this->getFederatedSignonCerts();
+    if (!$audience) {
+      $audience = $this->clientId;
+    }
+    return $this->verifySignedJwtWithCerts($id_token, $certs, $audience);
+  }
+
+  // Verifies the id token, returns the verified token contents.
+  // Visible for testing.
+  function verifySignedJwtWithCerts($jwt, $certs, $required_audience) {
+    $segments = explode(".", $jwt);
+    if (count($segments) != 3) {
+      throw new Google_AuthException("Wrong number of segments in token: $jwt");
+    }
+    $signed = $segments[0] . "." . $segments[1];
+    $signature = Google_Utils::urlSafeB64Decode($segments[2]);
+
+    // Parse envelope.
+    $envelope = json_decode(Google_Utils::urlSafeB64Decode($segments[0]), true);
+    if (!$envelope) {
+      throw new Google_AuthException("Can't parse token envelope: " . $segments[0]);
+    }
+
+    // Parse token
+    $json_body = Google_Utils::urlSafeB64Decode($segments[1]);
+    $payload = json_decode($json_body, true);
+    if (!$payload) {
+      throw new Google_AuthException("Can't parse token payload: " . $segments[1]);
+    }
+
+    // Check signature
+    $verified = false;
+    foreach ($certs as $keyName => $pem) {
+      $public_key = new Google_PemVerifier($pem);
+      if ($public_key->verify($signed, $signature)) {
+        $verified = true;
+        break;
+      }
+    }
+
+    if (!$verified) {
+      throw new Google_AuthException("Invalid token signature: $jwt");
+    }
+
+    // Check issued-at timestamp
+    $iat = 0;
+    if (array_key_exists("iat", $payload)) {
+      $iat = $payload["iat"];
+    }
+    if (!$iat) {
+      throw new Google_AuthException("No issue time in token: $json_body");
+    }
+    $earliest = $iat - self::CLOCK_SKEW_SECS;
+
+    // Check expiration timestamp
+    $now = time();
+    $exp = 0;
+    if (array_key_exists("exp", $payload)) {
+      $exp = $payload["exp"];
+    }
+    if (!$exp) {
+      throw new Google_AuthException("No expiration time in token: $json_body");
+    }
+    if ($exp >= $now + self::MAX_TOKEN_LIFETIME_SECS) {
+      throw new Google_AuthException(
+          "Expiration time too far in future: $json_body");
+    }
+
+    $latest = $exp + self::CLOCK_SKEW_SECS;
+    if ($now < $earliest) {
+      throw new Google_AuthException(
+          "Token used too early, $now < $earliest: $json_body");
+    }
+    if ($now > $latest) {
+      throw new Google_AuthException(
+          "Token used too late, $now > $latest: $json_body");
+    }
+
+    // TODO(beaton): check issuer field?
+
+    // Check audience
+    $aud = $payload["aud"];
+    if ($aud != $required_audience) {
+      throw new Google_AuthException("Wrong recipient, $aud != $required_audience: $json_body");
+    }
+
+    // All good.
+    return new Google_LoginTicket($envelope, $payload);
+  }
+}
diff --git a/lib/google/auth/Google_P12Signer.php b/lib/google/auth/Google_P12Signer.php
new file mode 100644 (file)
index 0000000..1bed590
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Signs data.
+ *
+ * Only used for testing.
+ *
+ * @author Brian Eaton <beaton@google.com>
+ */
+class Google_P12Signer extends Google_Signer {
+  // OpenSSL private key resource
+  private $privateKey;
+
+  // Creates a new signer from a .p12 file.
+  function __construct($p12, $password) {
+    if (!function_exists('openssl_x509_read')) {
+      throw new Exception(
+          'The Google PHP API library needs the openssl PHP extension');
+    }
+
+    // This throws on error
+    $certs = array();
+    if (!openssl_pkcs12_read($p12, $certs, $password)) {
+      throw new Google_AuthException("Unable to parse the p12 file.  " .
+          "Is this a .p12 file?  Is the password correct?  OpenSSL error: " .
+          openssl_error_string());
+    }
+    // TODO(beaton): is this part of the contract for the openssl_pkcs12_read
+    // method?  What happens if there are multiple private keys?  Do we care?
+    if (!array_key_exists("pkey", $certs) || !$certs["pkey"]) {
+      throw new Google_AuthException("No private key found in p12 file.");
+    }
+    $this->privateKey = openssl_pkey_get_private($certs["pkey"]);
+    if (!$this->privateKey) {
+      throw new Google_AuthException("Unable to load private key in ");
+    }
+  }
+
+  function __destruct() {
+    if ($this->privateKey) {
+      openssl_pkey_free($this->privateKey);
+    }
+  }
+
+  function sign($data) {
+    if(version_compare(PHP_VERSION, '5.3.0') < 0) {
+      throw new Google_AuthException(
+        "PHP 5.3.0 or higher is required to use service accounts.");
+    }
+    if (!openssl_sign($data, $signature, $this->privateKey, "sha256")) {
+      throw new Google_AuthException("Unable to sign data");
+    }
+    return $signature;
+  }
+}
diff --git a/lib/google/auth/Google_PemVerifier.php b/lib/google/auth/Google_PemVerifier.php
new file mode 100644 (file)
index 0000000..6c1c85f
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Verifies signatures using PEM encoded certificates.
+ *
+ * @author Brian Eaton <beaton@google.com>
+ */
+class Google_PemVerifier extends Google_Verifier {
+  private $publicKey;
+
+  /**
+   * Constructs a verifier from the supplied PEM-encoded certificate.
+   *
+   * $pem: a PEM encoded certificate (not a file).
+   * @param $pem
+   * @throws Google_AuthException
+   * @throws Google_Exception
+   */
+  function __construct($pem) {
+    if (!function_exists('openssl_x509_read')) {
+      throw new Google_Exception('Google API PHP client needs the openssl PHP extension');
+    }
+    $this->publicKey = openssl_x509_read($pem);
+    if (!$this->publicKey) {
+      throw new Google_AuthException("Unable to parse PEM: $pem");
+    }
+  }
+
+  function __destruct() {
+    if ($this->publicKey) {
+      openssl_x509_free($this->publicKey);
+    }
+  }
+
+  /**
+   * Verifies the signature on data.
+   *
+   * Returns true if the signature is valid, false otherwise.
+   * @param $data
+   * @param $signature
+   * @throws Google_AuthException
+   * @return bool
+   */
+  function verify($data, $signature) {
+    $status = openssl_verify($data, $signature, $this->publicKey, "sha256");
+    if ($status === -1) {
+      throw new Google_AuthException('Signature verification error: ' . openssl_error_string());
+    }
+    return $status === 1;
+  }
+}
diff --git a/lib/google/auth/Google_Signer.php b/lib/google/auth/Google_Signer.php
new file mode 100644 (file)
index 0000000..7892baa
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+require_once "Google_P12Signer.php";
+
+/**
+ * Signs data.
+ *
+ * @author Brian Eaton <beaton@google.com>
+ */
+abstract class Google_Signer {
+  /**
+   * Signs