Merge branch 'MDL-64990_master' of git://github.com/markn86/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 19 Mar 2019 22:03:56 +0000 (23:03 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 19 Mar 2019 22:03:56 +0000 (23:03 +0100)
244 files changed:
.eslintignore
.stylelintignore
admin/classes/form/testoutgoingmailconf_form.php [new file with mode: 0644]
admin/message.php
admin/roles/classes/check_users_selector.php
admin/settings/messaging.php [new file with mode: 0644]
admin/settings/plugins.php
admin/settings/server.php
admin/settings/subsystems.php
admin/settings/top.php
admin/testoutgoingmailconf.php [new file with mode: 0644]
admin/tests/behat/behat_admin.php
admin/tool/analytics/amd/build/model.min.js
admin/tool/analytics/amd/src/model.js
admin/tool/analytics/classes/output/model_logs.php
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/cli/evaluate_model.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/templates/evaluation_mode_selection.mustache [new file with mode: 0644]
admin/tool/customlang/classes/output/renderer.php [new file with mode: 0644]
admin/tool/customlang/classes/output/translator.php [new file with mode: 0644]
admin/tool/customlang/renderer.php [deleted file]
admin/tool/customlang/styles.css [deleted file]
admin/tool/customlang/templates/translator.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/coverage.php [new file with mode: 0644]
admin/tool/policy/classes/output/page_agreedocs.php
admin/tool/policy/tests/behat/acceptances.feature
analytics/classes/classifier.php
analytics/classes/model.php
analytics/classes/regressor.php
analytics/classes/stats.php [new file with mode: 0644]
analytics/tests/prediction_test.php
analytics/tests/stats_test.php [new file with mode: 0644]
analytics/upgrade.txt
badges/index.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lang/en/deprecated.txt
blocks/myoverview/templates/nav-sort-selector.mustache
blocks/myoverview/templates/progress-bar.mustache
blocks/timeline/templates/nav-day-filter.mustache
blocks/timeline/templates/nav-view-selector.mustache
blocks/timeline/tests/behat/block_timeline_courses.feature
blocks/timeline/tests/behat/block_timeline_dates.feature
blocks/timeline/tests/behat/block_timeline_pagelimit_persistence.feature
cache/stores/mongodb/MongoDB/BulkWriteResult.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/ChangeStream.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Client.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Collection.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Database.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/DeleteResult.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Exception/BadMethodCallException.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Exception/Exception.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Exception/InvalidArgumentException.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Exception/ResumeTokenException.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Exception/RuntimeException.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Exception/UnexpectedValueException.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Exception/UnsupportedException.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/GridFS/Bucket.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/GridFS/CollectionWrapper.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/GridFS/Exception/CorruptFileException.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/GridFS/Exception/FileNotFoundException.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/GridFS/ReadableStream.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/GridFS/StreamWrapper.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/GridFS/WritableStream.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/InsertManyResult.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/InsertOneResult.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/MapReduceResult.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/BSONArray.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/BSONDocument.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/BSONIterator.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/CachingIterator.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/CollectionInfo.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/CollectionInfoCommandIterator.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/CollectionInfoIterator.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/DatabaseInfo.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/DatabaseInfoIterator.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/DatabaseInfoLegacyIterator.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/IndexInfo.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/IndexInfoIterator.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/IndexInfoIteratorIterator.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/IndexInput.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Model/TypeMapArrayIterator.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/Aggregate.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/BulkWrite.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/Count.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/CountDocuments.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/CreateCollection.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/CreateIndexes.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/DatabaseCommand.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/Delete.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/DeleteMany.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/DeleteOne.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/Distinct.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/DropCollection.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/DropDatabase.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/DropIndexes.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/EstimatedDocumentCount.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/Executable.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/Explain.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/Explainable.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/Find.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/FindAndModify.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/FindOne.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/FindOneAndDelete.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/FindOneAndReplace.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/FindOneAndUpdate.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/InsertMany.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/InsertOne.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/ListCollections.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/ListDatabases.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/ListIndexes.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/MapReduce.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/ModifyCollection.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/ReplaceOne.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/Update.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/UpdateMany.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/UpdateOne.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/Operation/Watch.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/UpdateResult.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/functions.php [new file with mode: 0755]
cache/stores/mongodb/MongoDB/readme_moodle.txt [new file with mode: 0644]
cache/stores/mongodb/addinstanceform.php
cache/stores/mongodb/lib.php
cache/stores/mongodb/thirdpartylibs.xml [new file with mode: 0644]
cache/upgrade.txt
completion/classes/progress.php
completion/tests/progress_test.php
config-dist.php
course/tests/behat/behat_course.php
enrol/externallib.php
enrol/tests/externallib_test.php
enrol/upgrade.txt
files/renderer.php
grade/import/csv/classes/load_data.php
grade/import/csv/tests/load_data_test.php
group/lib.php
group/tests/behat/behat_groups.php
install/lang/hi/moodle.php
lang/en/admin.php
lang/en/deprecated.txt
lang/en/grades.php
lang/en/hub.php
lang/en/message.php
lang/en/plugin.php
lib/adminlib.php
lib/amd/build/templates.min.js
lib/amd/src/templates.js
lib/behat/behat_base.php
lib/behat/classes/partial_named_selector.php
lib/behat/form_field/behat_form_filemanager.php
lib/behat/form_field/behat_form_passwordunmask.php
lib/behat/form_field/behat_form_select.php
lib/classes/component.php
lib/classes/hub/registration.php
lib/classes/message/manager.php
lib/classes/output/external.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/output/mustache_template_source_loader.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/mlbackend/php/classes/processor.php
lib/mlbackend/python/classes/processor.php
lib/phpunit/classes/coverage_info.php [new file with mode: 0644]
lib/phpunit/classes/util.php
lib/tests/behat/behat_app.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_permissions.php
lib/tests/coverage.php [new file with mode: 0644]
lib/tests/messagelib_test.php
lib/upgrade.txt
message/amd/build/message_drawer_events.min.js
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation_constants.min.js
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_conversation_renderer.min.js
message/amd/build/message_drawer_view_conversation_state_manager.min.js
message/amd/build/message_drawer_view_overview_section.min.js
message/amd/build/message_repository.min.js
message/amd/src/message_drawer_events.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_conversation_constants.js
message/amd/src/message_drawer_view_conversation_patcher.js
message/amd/src/message_drawer_view_conversation_renderer.js
message/amd/src/message_drawer_view_conversation_state_manager.js
message/amd/src/message_drawer_view_overview_section.js
message/amd/src/message_repository.js
message/classes/api.php
message/classes/privacy/provider.php
message/defaultoutputs.php
message/externallib.php
message/renderer.php
message/templates/message_drawer_conversations_list.mustache
message/templates/message_drawer_view_conversation_header_content_type_private.mustache
message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache
message/templates/message_drawer_view_conversation_header_content_type_public.mustache
message/tests/api_test.php
message/tests/behat/behat_message.php
message/tests/behat/group_conversation.feature [new file with mode: 0644]
message/tests/externallib_test.php
message/tests/privacy_provider_test.php
mod/assign/feedback/editpdf/tests/behat/behat_assignfeedback_editpdf.php
mod/feedback/tests/behat/behat_mod_feedback.php
mod/forum/db/services.php
mod/forum/externallib.php
mod/forum/tests/externallib_test.php
mod/forum/version.php
mod/lti/lib.php
mod/lti/tests/lib_test.php
mod/quiz/lib.php
mod/quiz/tests/lib_test.php
mod/workshop/allocation/manual/tests/behat/behat_workshopallocation_manual.php
mod/workshop/lib.php
mod/workshop/tests/lib_test.php
phpunit.xml.dist
pix/i/muted.png [new file with mode: 0644]
pix/i/muted.svg [new file with mode: 0644]
privacy/tests/coverage.php [new file with mode: 0644]
repository/nextcloud/lang/en/repository_nextcloud.php
repository/tests/behat/behat_filepicker.php
repository/upload/tests/behat/behat_repository_upload.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/filemanager.scss
theme/boost/style/moodle.css
theme/boost/tests/behat/group_conversation.feature [new file with mode: 0644]
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache
theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private_no_controls.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_public.mustache
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_admin.php
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_filepicker.php
theme/bootstrapbase/tests/behat/behat_theme_bootstrapbase_behat_repository_upload.php
user/lib.php
version.php
webservice/externallib.php
webservice/tests/externallib_test.php
webservice/upgrade.txt

index 420eb21..9153e18 100644 (file)
@@ -6,6 +6,7 @@ vendor/
 admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
 admin/tool/usertours/amd/src/tour.js
 auth/cas/CAS/
+cache/stores/mongodb/MongoDB/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
index be8cec8..55b87b5 100644 (file)
@@ -9,6 +9,7 @@ vendor/
 admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
 admin/tool/usertours/amd/src/tour.js
 auth/cas/CAS/
+cache/stores/mongodb/MongoDB/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
diff --git a/admin/classes/form/testoutgoingmailconf_form.php b/admin/classes/form/testoutgoingmailconf_form.php
new file mode 100644 (file)
index 0000000..fbf8ac4
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Testing outgoing mail configuration form
+ *
+ * @package    core
+ * @copyright  2019 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_admin\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Test mail form
+ *
+ * @package    core
+ * @copyright 2019 Victor Deniz <victor@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testoutgoingmailconf_form extends \moodleform {
+    /**
+     * Add elements to form
+     */
+    public function definition() {
+        $mform = $this->_form;
+
+        // Recipient.
+        $options = ['maxlength' => '100', 'size' => '25'];
+        $mform->addElement('text', 'recipient', get_string('testoutgoingmailconf_toemail', 'admin'), $options);
+        $mform->setType('recipient', PARAM_EMAIL);
+        $mform->addRule('recipient', get_string('required'), 'required');
+
+        $buttonarray = array();
+        $buttonarray[] = $mform->createElement('submit', 'send', get_string('testoutgoingmailconf_sendtest', 'admin'));
+        $buttonarray[] = $mform->createElement('cancel');
+
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+
+    }
+}
index c10034a..9a43f61 100644 (file)
@@ -25,47 +25,101 @@ require_once(__DIR__ . '/../config.php');
 require_once($CFG->dirroot . '/message/lib.php');
 require_once($CFG->libdir.'/adminlib.php');
 
-// This is an admin page
+// This is an admin page.
 admin_externalpage_setup('managemessageoutputs');
 
-// Get the submitted params
-$disable    = optional_param('disable', 0, PARAM_INT);
-$enable     = optional_param('enable', 0, PARAM_INT);
+// Fetch processors.
+$allprocessors = get_message_processors();
+$processors = array_filter($allprocessors, function($processor) {
+    return $processor->enabled;
+});
+// Fetch message providers.
+$providers = get_message_providers();
+// Fetch the manage message outputs interface.
+$preferences = get_message_output_default_preferences();
 
-$headingtitle = get_string('managemessageoutputs', 'message');
+if (($form = data_submitted()) && confirm_sesskey()) {
+    $preferences = array();
+    // Prepare default message outputs settings.
+    foreach ($providers as $provider) {
+        $componentproviderbase = $provider->component.'_'.$provider->name;
+        $disableprovidersetting = $componentproviderbase.'_disable';
+        $providerdisabled = false;
+        if (!isset($form->$disableprovidersetting)) {
+            $providerdisabled = true;
+            $preferences[$disableprovidersetting] = 1;
+        } else {
+            $preferences[$disableprovidersetting] = 0;
+        }
 
-if (!empty($disable) && confirm_sesskey()) {
-    if (!$processor = $DB->get_record('message_processors', array('id'=>$disable))) {
-        print_error('outputdoesnotexist', 'message');
+        foreach (array('permitted', 'loggedin', 'loggedoff') as $setting) {
+            $value = null;
+            $componentprovidersetting = $componentproviderbase.'_'.$setting;
+            if ($setting == 'permitted') {
+                // If we deal with permitted select element, we need to create individual
+                // setting for each possible processor. Note that this block will
+                // always be processed first after entring parental foreach iteration
+                // so we can change form values on this stage.
+                foreach ($allprocessors as $processor) {
+                    $value = '';
+                    if (isset($form->{$componentprovidersetting}[$processor->name])) {
+                        $value = $form->{$componentprovidersetting}[$processor->name];
+                    }
+                    // Ensure that loggedin loggedoff options are set correctly for this permission.
+                    if (($value == 'disallowed') || $providerdisabled) {
+                        // It might be better to unset them, but I can't figure out why that cause error.
+                        $form->{$componentproviderbase.'_loggedin'}[$processor->name] = 0;
+                        $form->{$componentproviderbase.'_loggedoff'}[$processor->name] = 0;
+                    } else if ($value == 'forced') {
+                        $form->{$componentproviderbase.'_loggedin'}[$processor->name] = 1;
+                        $form->{$componentproviderbase.'_loggedoff'}[$processor->name] = 1;
+                    }
+                    // Record the site preference.
+                    $preferences[$processor->name.'_provider_'.$componentprovidersetting] = $value;
+                }
+            } else if (array_key_exists($componentprovidersetting, $form)) {
+                // We must be processing loggedin or loggedoff checkboxes. Store
+                // defained comma-separated processors as setting value.
+                // Using array_filter eliminates elements set to 0 above.
+                $value = join(',', array_keys(array_filter($form->{$componentprovidersetting})));
+                if (empty($value)) {
+                    $value = null;
+                }
+            }
+            if ($setting != 'permitted') {
+                // We have already recoded site preferences for 'permitted' type.
+                $preferences['message_provider_'.$componentprovidersetting] = $value;
+            }
+        }
+    }
+
+    // Update database.
+    $transaction = $DB->start_delegated_transaction();
+
+    // Save processors enabled/disabled status.
+    foreach ($allprocessors as $processor) {
+        $enabled = isset($form->{$processor->name});
+        \core_message\api::update_processor_status($processor, $enabled);
     }
-    \core_message\api::update_processor_status($processor, 0);     // Disable output.
-    core_plugin_manager::reset_caches();
-}
 
-if (!empty($enable) && confirm_sesskey()) {
-    if (!$processor = $DB->get_record('message_processors', array('id'=>$enable))) {
-        print_error('outputdoesnotexist', 'message');
+    foreach ($preferences as $name => $value) {
+        set_config($name, $value, 'message');
     }
-    \core_message\api::update_processor_status($processor, 1);      // Enable output.
+    $transaction->allow_commit();
+
     core_plugin_manager::reset_caches();
-}
 
-if ($disable || $enable) {
     $url = new moodle_url('message.php');
     redirect($url);
 }
+
 // Page settings
 $PAGE->set_context(context_system::instance());
+$PAGE->requires->js_init_call('M.core_message.init_defaultoutputs');
 
-// Grab the renderer
 $renderer = $PAGE->get_renderer('core', 'message');
 
-// Display the manage message outputs interface
-$processors = get_message_processors();
-$messageoutputs = $renderer->manage_messageoutputs($processors);
-
-// Display the page
+// Display the page.
 echo $OUTPUT->header();
-echo $OUTPUT->heading($headingtitle);
-echo $messageoutputs;
+echo $renderer->manage_messageoutput_settings($allprocessors, $processors, $providers, $preferences);
 echo $OUTPUT->footer();
index 5ac4c26..31929bd 100644 (file)
@@ -69,8 +69,11 @@ class core_role_check_users_selector extends user_selector_base {
 
         if ($coursecontext and $coursecontext != SITEID) {
             $sql1 = " FROM {user} u
-                      JOIN {user_enrolments} ue ON (ue.userid = u.id)
-                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid1)
+                      JOIN (SELECT DISTINCT subu.id
+                              FROM {user} subu
+                              JOIN {user_enrolments} ue ON (ue.userid = subu.id)
+                              JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid1)
+                           ) subq ON subq.id = u.id
                      WHERE $wherecondition";
             $params['courseid1'] = $coursecontext->instanceid;
 
diff --git a/admin/settings/messaging.php b/admin/settings/messaging.php
new file mode 100644 (file)
index 0000000..2221e72
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Adds messaging related settings links for Messaging category to admin tree.
+ *
+ * @copyright 2019 Amaia Anabitarte <amaia@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig) {
+    $temp = new admin_settingpage('messages', new lang_string('messagingssettings', 'admin'));
+    $temp->add(new admin_setting_configcheckbox('messaging',
+        new lang_string('messaging', 'admin'),
+        new lang_string('configmessaging', 'admin'),
+        1));
+    $temp->add(new admin_setting_configcheckbox('messagingallusers',
+            new lang_string('messagingallusers', 'admin'),
+            new lang_string('configmessagingallusers', 'admin'),
+             0)
+    );
+    $temp->add(new admin_setting_configcheckbox('messagingdefaultpressenter',
+            new lang_string('messagingdefaultpressenter', 'admin'),
+            new lang_string('configmessagingdefaultpressenter', 'admin'),
+            1)
+    );
+    $options = array(
+        DAYSECS => new lang_string('secondstotime86400'),
+        WEEKSECS => new lang_string('secondstotime604800'),
+        2620800 => new lang_string('nummonths', 'moodle', 1),
+        7862400 => new lang_string('nummonths', 'moodle', 3),
+        15724800 => new lang_string('nummonths', 'moodle', 6),
+        0 => new lang_string('never')
+    );
+    $temp->add(new admin_setting_configselect(
+            'messagingdeletereadnotificationsdelay',
+            new lang_string('messagingdeletereadnotificationsdelay', 'admin'),
+            new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'),
+            604800,
+            $options)
+    );
+    $temp->add(new admin_setting_configselect(
+            'messagingdeleteallnotificationsdelay',
+            new lang_string('messagingdeleteallnotificationsdelay', 'admin'),
+            new lang_string('configmessagingdeleteallnotificationsdelay', 'admin'),
+            2620800,
+            $options)
+    );
+    $temp->add(new admin_setting_configcheckbox('messagingallowemailoverride',
+        new lang_string('messagingallowemailoverride', 'admin'),
+        new lang_string('configmessagingallowemailoverride', 'admin'),
+        0));
+    $ADMIN->add('messaging', $temp);
+    $ADMIN->add('messaging', new admin_page_managemessageoutputs());
+
+    // Notification outputs plugins.
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('message');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
+        /** @var \core\plugininfo\message $plugin */
+        $plugin->load_settings($ADMIN, 'messaging', $hassiteconfig);
+    }
+}
index 485eb01..925829c 100644 (file)
@@ -81,17 +81,6 @@ if ($hassiteconfig) {
         $plugin->load_settings($ADMIN, 'blocksettings', $hassiteconfig);
     }
 
-    // message outputs
-    $ADMIN->add('modules', new admin_category('messageoutputs', new lang_string('messageoutputs', 'message')));
-    $ADMIN->add('messageoutputs', new admin_page_managemessageoutputs());
-    $ADMIN->add('messageoutputs', new admin_page_defaultmessageoutputs());
-    $plugins = core_plugin_manager::instance()->get_plugins_of_type('message');
-    core_collator::asort_objects_by_property($plugins, 'displayname');
-    foreach ($plugins as $plugin) {
-        /** @var \core\plugininfo\message $plugin */
-        $plugin->load_settings($ADMIN, 'messageoutputs', $hassiteconfig);
-    }
-
     // authentication plugins
     $ADMIN->add('modules', new admin_category('authsettings', new lang_string('authentication', 'admin')));
 
index 96c7871..8c47a56 100644 (file)
@@ -176,6 +176,8 @@ $ADMIN->add('server', $temp);
 
 $ADMIN->add('server', new admin_externalpage('environment', new lang_string('environment','admin'), "$CFG->wwwroot/$CFG->admin/environment.php"));
 $ADMIN->add('server', new admin_externalpage('phpinfo', new lang_string('phpinfo'), "$CFG->wwwroot/$CFG->admin/phpinfo.php"));
+$ADMIN->add('server', new admin_externalpage('testoutgoingmailconf', new lang_string('testoutgoingmailconf', 'admin'),
+            new moodle_url("$CFG->wwwroot/$CFG->admin/testoutgoingmailconf.php"), 'moodle/site:config', true));
 
 
 // "performance" settingpage
@@ -326,6 +328,10 @@ $temp->add(new admin_setting_configtextarea('allowedemaildomains',
         new lang_string('allowedemaildomains', 'admin'),
         new lang_string('configallowedemaildomains', 'admin'),
         ''));
+$url = new moodle_url('/admin/testoutgoingmailconf.php');
+$link = html_writer::link($url, get_string('testoutgoingmailconf', 'admin'));
+$temp->add(new admin_setting_heading('testoutgoinmailc', new lang_string('testoutgoingmailconf', 'admin'),
+        new lang_string('testoutgoingmaildetail', 'admin', $link)));
 $temp->add(new admin_setting_heading('emaildoesnotfit', new lang_string('doesnotfit', 'admin'),
         new lang_string('doesnotfitdetail', 'admin')));
 $charsets = get_list_of_charsets();
index b559de3..de5f75b 100644 (file)
@@ -13,45 +13,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablewebservices', new lang_string('enablewebservices', 'admin'), new lang_string('configenablewebservices', 'admin'), 0));
 
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messaging', new lang_string('messaging', 'admin'), new lang_string('configmessaging','admin'), 1));
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallusers',
-        new lang_string('messagingallusers', 'admin'),
-        new lang_string('configmessagingallusers', 'admin'),
-        0)
-    );
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingdefaultpressenter',
-        new lang_string('messagingdefaultpressenter', 'admin'),
-        new lang_string('configmessagingdefaultpressenter', 'admin'),
-        1)
-    );
-
-    $options = array(
-        DAYSECS => new lang_string('secondstotime86400'),
-        WEEKSECS => new lang_string('secondstotime604800'),
-        2620800 => new lang_string('nummonths', 'moodle', 1),
-        7862400 => new lang_string('nummonths', 'moodle', 3),
-        15724800 => new lang_string('nummonths', 'moodle', 6),
-        0 => new lang_string('never')
-    );
-    $optionalsubsystems->add(new admin_setting_configselect(
-        'messagingdeletereadnotificationsdelay',
-        new lang_string('messagingdeletereadnotificationsdelay', 'admin'),
-        new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'),
-        604800,
-        $options)
-    );
-    $optionalsubsystems->add(new admin_setting_configselect(
-        'messagingdeleteallnotificationsdelay',
-        new lang_string('messagingdeleteallnotificationsdelay', 'admin'),
-        new lang_string('configmessagingdeleteallnotificationsdelay', 'admin'),
-        2620800,
-        $options)
-    );
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallowemailoverride', new lang_string('messagingallowemailoverride', 'admin'), new lang_string('configmessagingallowemailoverride','admin'), 0));
-
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablestats', new lang_string('enablestats', 'admin'), new lang_string('configenablestats', 'admin'), 0));
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablerssfeeds', new lang_string('enablerssfeeds', 'admin'), new lang_string('configenablerssfeeds', 'admin'), 0));
index 102b758..32d91c6 100644 (file)
@@ -33,6 +33,7 @@ $ADMIN->add('root', new admin_category('competencies', new lang_string('competen
 $ADMIN->add('root', new admin_category('badges', new lang_string('badges'), empty($CFG->enablebadges)));
 $ADMIN->add('root', new admin_category('location', new lang_string('location','admin')));
 $ADMIN->add('root', new admin_category('language', new lang_string('language')));
+$ADMIN->add('root', new admin_category('messaging', new lang_string('messagingcategory', 'admin')));
 $ADMIN->add('root', new admin_category('modules', new lang_string('plugins', 'admin')));
 $ADMIN->add('root', new admin_category('security', new lang_string('security','admin')));
 $ADMIN->add('root', new admin_category('appearance', new lang_string('appearance','admin')));
diff --git a/admin/testoutgoingmailconf.php b/admin/testoutgoingmailconf.php
new file mode 100644 (file)
index 0000000..ce5857f
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Test output mail configuration page
+ *
+ * @copyright 2019 Victor Deniz <victor@moodle.com>, based on Michael Milette <michael.milette@tngconsulting.ca> code
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+// This is an admin page.
+admin_externalpage_setup('testoutgoingmailconf');
+
+$headingtitle = get_string('testoutgoingmailconf', 'admin');
+$homeurl = new moodle_url('/admin/category.php', array('category' => 'email'));
+$returnurl = new moodle_url('/admin/testoutgoingconf.php');
+
+$form = new core_admin\form\testoutgoingmailconf_form(null, ['returnurl' => $returnurl]);
+if ($form->is_cancelled()) {
+    redirect($homeurl);
+}
+
+// Display the page.
+echo $OUTPUT->header();
+echo $OUTPUT->heading($headingtitle);
+
+$data = $form->get_data();
+if ($data) {
+    $emailuser = new stdClass();
+    $emailuser->email = $data->recipient;
+    $emailuser->id = -99;
+
+    $subject = get_string('testoutgoingmailconf_subject', 'admin', $SITE->fullname);
+    $messagetext = get_string('testoutgoingmailconf_message', 'admin');
+
+    // Manage Moodle debugging options.
+    $debuglevel = $CFG->debug;
+    $debugdisplay = $CFG->debugdisplay;
+    $debugsmtp = $CFG->debugsmtp;
+    $CFG->debugdisplay = true;
+    $CFG->debugsmtp = true;
+    $CFG->debug = 15;
+
+    // Send test email.
+    ob_start();
+    $success = email_to_user($emailuser, $USER, $subject, $messagetext);
+    $smtplog = ob_get_contents();
+    ob_end_clean();
+
+    // Restore Moodle debugging options.
+    $CFG->debug = $debuglevel;
+    $CFG->debugdisplay = $debugdisplay;
+    $CFG->debugsmtp = $debugsmtp;
+
+    if ($success) {
+        $msgparams = new stdClass();
+        $msgparams->fromemail = $USER->email;
+        $msgparams->toemail = $emailuser->email;
+        $msg = get_string('testoutgoingmailconf_sentmail', 'admin', $msgparams);
+        $notificationtype = 'notifysuccess';
+    } else {
+        $notificationtype = 'notifyproblem';
+        // No communication between Moodle and the SMTP server - no error output.
+        if (trim($smtplog) == false) {
+            $msg = get_string('testoutgoingmailconf_errorcommunications', 'admin');
+        } else {
+            $msg = $smtplog;
+        }
+    }
+
+    // Show result.
+    echo $OUTPUT->notification($msg, $notificationtype);
+}
+
+$form->display();
+echo $OUTPUT->footer();
index b2fd070..2d365c4 100644 (file)
@@ -63,7 +63,7 @@ class behat_admin extends behat_base {
             $submitsearch = $this->find('css', 'form input[type=submit][name=search]');
             $submitsearch->press();
 
-            $this->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
+            $this->wait(self::get_timeout() * 1000, self::PAGE_READY_JS);
 
             // Admin settings does not use the same DOM structure than other moodle forms
             // but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
index c66254a..180805d 100644 (file)
Binary files a/admin/tool/analytics/amd/build/model.min.js and b/admin/tool/analytics/amd/build/model.min.js differ
index 2f0b605..f39c577 100644 (file)
@@ -20,8 +20,8 @@
  * @copyright  2017 David Monllao
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_factory', 'core/modal_events'],
-    function($, Str, log, Notification, ModalFactory, ModalEvents) {
+define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/templates'],
+    function($, Str, log, Notification, ModalFactory, ModalEvents, Templates) {
 
     /**
      * List of actions that require confirmation and confirmation message.
@@ -94,10 +94,65 @@ define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_facto
                     modal.getRoot().on(ModalEvents.save, function() {
                         window.location.href = a.attr('href');
                     });
+                    modal.show();
+                    return modal;
+                }).fail(Notification.exception);
+            });
+        },
+
+        /**
+         * Displays a select-evaluation-mode choice.
+         *
+         * @param  {String}  actionId
+         * @param  {Boolean} trainedOnlyExternally
+         */
+        selectEvaluationMode: function(actionId, trainedOnlyExternally) {
+            $('[data-action-id="' + actionId + '"]').on('click', function(ev) {
+                ev.preventDefault();
+
+                var a = $(ev.currentTarget);
+
+                if (!trainedOnlyExternally) {
+                    // We can not evaluate trained models if the model was trained using data from this site.
+                    // Default to evaluate the model configuration if that is the case.
+                    window.location.href = a.attr('href');
+                    return;
+                }
+
+                var stringsPromise = Str.get_strings([
+                    {
+                        key: 'evaluatemodel',
+                        component: 'tool_analytics'
+                    }, {
+                        key: 'evaluationmode',
+                        component: 'tool_analytics'
+                    }
+                ]);
+                var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});
+                var bodyPromise = Templates.render('tool_analytics/evaluation_mode_selection', {});
+
+                $.when(stringsPromise, modalPromise).then(function(strings, modal) {
+
+
+                    modal.getRoot().on(ModalEvents.hidden, modal.destroy.bind(modal));
+
+                    modal.setTitle(strings[1]);
+                    modal.setSaveButtonText(strings[0]);
+                    modal.setBody(bodyPromise);
+
+                    modal.getRoot().on(ModalEvents.save, function() {
+                        var evaluationMode = $("input[name='evaluationmode']:checked").val();
+                        if (evaluationMode == 'trainedmodel') {
+                            a.attr('href', a.attr('href') + '&mode=trainedmodel');
+                        }
+                        window.location.href = a.attr('href');
+                        return;
+                    });
+
                     modal.show();
                     return modal;
                 }).fail(Notification.exception);
             });
         }
     };
-});
+});
\ No newline at end of file
index df59038..2e3a552 100644 (file)
@@ -41,6 +41,11 @@ class model_logs extends \table_sql {
      */
     protected $model = null;
 
+    /**
+     * @var string|false
+     */
+    protected $evaluationmode = false;
+
     /**
      * Sets up the table_log parameters.
      *
@@ -57,21 +62,32 @@ class model_logs extends \table_sql {
         $this->set_attribute('class', 'modellog generaltable generalbox');
         $this->set_attribute('aria-live', 'polite');
 
-        $this->define_columns(array('time', 'version', 'indicators', 'timesplitting', 'accuracy', 'info', 'usermodified'));
+        $this->define_columns(array('time', 'version', 'evaluationmode', 'indicators', 'timesplitting',
+            'accuracy', 'info', 'usermodified'));
         $this->define_headers(array(
             get_string('time'),
             get_string('version'),
+            get_string('evaluationmode', 'tool_analytics'),
             get_string('indicators', 'tool_analytics'),
             get_string('timesplittingmethod', 'analytics'),
             get_string('accuracy', 'tool_analytics'),
             get_string('info', 'tool_analytics'),
             get_string('fullnameuser'),
         ));
+
+        $evaluationmodehelp = new \help_icon('evaluationmode', 'tool_analytics');
+        $this->define_help_for_headers([null, null, $evaluationmodehelp, null, null, null, null, null]);
+
         $this->pageable(true);
         $this->collapsible(false);
         $this->sortable(false);
         $this->is_downloadable(false);
 
+        $this->evaluationmode = optional_param('evaluationmode', false, PARAM_ALPHANUM);
+        if ($this->evaluationmode && $this->evaluationmode != 'configuration' && $this->evaluationmode != 'trainedmodel') {
+            $this->evaluationmode = '';
+        }
+
         $this->define_baseurl($PAGE->url);
     }
 
@@ -86,6 +102,15 @@ class model_logs extends \table_sql {
         return userdate($log->version, $recenttimestr);
     }
 
+    /**
+     * Generate the evaluation mode column.
+     *
+     * @param \stdClass $log log data.
+     * @return string HTML for the evaluationmode column
+     */
+    public function col_evaluationmode($log) {
+        return get_string('evaluationmodecol' . $log->evaluationmode, 'tool_analytics');
+    }
     /**
      * Generate the time column.
      *
index b3a1c43..351dae4 100644 (file)
@@ -187,15 +187,21 @@ class models_list implements \renderable, \templatable {
 
             // Evaluate machine-learning-based models.
             if (!$onlycli && $model->get_indicators() && !$model->is_static()) {
+
+                // Extra is_trained call as trained_locally returns false if the model has not been trained yet.
+                $trainedonlyexternally = !$model->trained_locally() && $model->is_trained();
+
+                $actionid = 'evaluate-' . $model->get_id();
+                $PAGE->requires->js_call_amd('tool_analytics/model', 'selectEvaluationMode', [$actionid, $trainedonlyexternally]);
                 $urlparams['action'] = 'evaluate';
                 $url = new \moodle_url('model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
-                    get_string('evaluate', 'tool_analytics'));
+                    get_string('evaluate', 'tool_analytics'), ['data-action-id' => $actionid]);
                 $actionsmenu->add($icon);
             }
 
             // Machine-learning-based models evaluation log.
-            if (!$model->is_static()) {
+            if (!$model->is_static() && $model->get_logs()) {
                 $urlparams['action'] = 'log';
                 $url = new \moodle_url('model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_analytics')),
index 25d182f..2836ca7 100644 (file)
@@ -35,6 +35,8 @@ Options:
 --non-interactive      Not interactive questions
 --timesplitting        Restrict the evaluation to 1 single time splitting method (Optional)
 --filter               Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional)
+--mode                 'configuration' or 'trainedmodel'. You can only use mode=trainedmodel when the trained" .
+    " model was imported" . "
 --reuse-prev-analysed  Reuse recently analysed courses instead of analysing the whole site. Set it to false while" .
     " coding indicators. Defaults to true (Optional)" . "
 -h, --help             Print out this help
@@ -50,6 +52,7 @@ list($options, $unrecognized) = cli_get_params(
         'modelid'               => false,
         'list'                  => false,
         'timesplitting'         => false,
+        'mode'                  => 'configuration',
         'reuse-prev-analysed'   => true,
         'non-interactive'       => false,
         'filter'                => false
@@ -64,16 +67,30 @@ if ($options['help']) {
     exit(0);
 }
 
-if ($options['list'] || $options['modelid'] === false) {
+if ($options['list']) {
     \tool_analytics\clihelper::list_models();
     exit(0);
 }
 
+if ($options['modelid'] === false) {
+    // All actions but --list require a modelid.
+    echo $help;
+    exit(0);
+}
+
 // Reformat them as an array.
 if ($options['filter'] !== false) {
     $options['filter'] = explode(',', $options['filter']);
 }
 
+if ($options['mode'] !== 'configuration' && $options['mode'] !== 'trainedmodel') {
+    cli_error('Error: The provided mode is not supported');
+}
+
+if ($options['mode'] == 'trainedmodel' && $options['timesplitting']) {
+    cli_error('Sorry, no time splitting method can be specified when using \'trainedmodel\' mode.');
+}
+
 // We need admin permissions.
 \core\session\manager::set_user(get_admin());
 
@@ -89,6 +106,7 @@ $analyseroptions = array(
     'filter' => $options['filter'],
     'timesplitting' => $options['timesplitting'],
     'reuseprevanalysed' => $options['reuse-prev-analysed'],
+    'mode' => $options['mode'],
 );
 // Evaluate its suitability to predict accurately.
 $results = $model->evaluate($analyseroptions);
index 47b40d0..36eed2f 100644 (file)
@@ -53,6 +53,18 @@ $string['erroronlycli'] = 'Execution only allowed via command line';
 $string['errortrainingdataexport'] = 'The model training data could not be exported';
 $string['evaluate'] = 'Evaluate';
 $string['evaluatemodel'] = 'Evaluate model';
+$string['evaluationmode'] = 'Evaluation mode';
+$string['evaluationmode_help'] = 'There are two evaluation modes:
+
+* Trained model -  Site data is used as testing data to evaluate the accuracy of the trained model.
+* Configuration - Site data is split into training and testing data, to both train and test the accuracy of the model configuration.
+
+Trained model is only available if a trained model has been imported into the site, and has not yet been re-trained using site data.';
+$string['evaluationmodeinfo'] = 'This model has been imported into the site. You can either evaluate the performance of the model, or you can evaluate the performance of the model configuration using site data.';
+$string['evaluationmodetrainedmodel'] = 'Evaluate the trained model';
+$string['evaluationmodecoltrainedmodel'] = 'Trained model';
+$string['evaluationmodecolconfiguration'] = 'Configuration';
+$string['evaluationmodeconfiguration'] = 'Evaluate the model configuration';
 $string['evaluationinbatches'] = 'The site contents are calculated and stored in batches. The evaluation process may be stopped at any time. The next time it is run, it will continue from the point when it was stopped.';
 $string['exportmodel'] = 'Export configuration';
 $string['exporttrainingdata'] = 'Export training data';
@@ -104,7 +116,7 @@ $string['trainingprocessfinished'] = 'Training process finished';
 $string['trainingresults'] = 'Training results';
 $string['trainmodels'] = 'Train models';
 $string['versionnotsame'] = 'Imported file was from a different moodle version ({$a->importedversion}) than the current one ({$a->version})';
-$string['viewlog'] = 'Log';
+$string['viewlog'] = 'Evaluation log';
 $string['weeksenddateautomaticallyset'] = 'End date automatically set based on start date and the number of sections';
 $string['weeksenddatedefault'] = 'End date automatically calculated from the course start date.';
 $string['privacy:metadata'] = 'The Analytic models plugin does not store any personal data.';
index 23d8454..71f303a 100644 (file)
@@ -169,7 +169,13 @@ switch ($action) {
         // Web interface is used by people who can not use CLI nor code stuff, always use
         // cached stuff as they will change the model through the web interface as well
         // which invalidates the previously analysed stuff.
-        $results = $model->evaluate(array('reuseprevanalysed' => true));
+        $options = ['reuseprevanalysed' => true];
+
+        $mode = optional_param('mode', false, PARAM_ALPHANUM);
+        if ($mode == 'trainedmodel') {
+            $options['mode'] = 'trainedmodel';
+        }
+        $results = $model->evaluate($options);
         $renderer = $PAGE->get_renderer('tool_analytics');
         echo $renderer->render_evaluate_results($results, $model->get_analyser()->get_logs());
         break;
diff --git a/admin/tool/analytics/templates/evaluation_mode_selection.mustache b/admin/tool/analytics/templates/evaluation_mode_selection.mustache
new file mode 100644 (file)
index 0000000..e9b32ce
--- /dev/null
@@ -0,0 +1,42 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_analytics/evaluation_mode_selector
+
+    Evaluation mode selector.
+
+    The purpose of this template is to render the evaluation mode radio button.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div class="box mb-4">{{#str}} evaluationmodeinfo, tool_analytics {{/str}}</div>
+<div class="form-check">
+    <input class="form-check-input" type="radio" name="evaluationmode" id="id-mode-trainedmodel" value="trainedmodel" checked>
+    <label class="form-check-label" for="id-mode-trainedmodel">{{#str}} evaluationmodetrainedmodel, tool_analytics {{/str}}</label>
+</div>
+<div class="form-check">
+    <input class="form-check-input" type="radio" name="evaluationmode" id="id-mode-configuration" value="configuration">
+    <label class="form-check-label" for="id-mode-configuration">{{#str}} evaluationmodeconfiguration, tool_analytics {{/str}}</label>
+</div>
\ No newline at end of file
diff --git a/admin/tool/customlang/classes/output/renderer.php b/admin/tool/customlang/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..b183daf
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderer class for tool customlang
+ *
+ * @package     tool_customlang
+ * @category    output
+ * @copyright   2019 Bas Brands <bas@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_customlang\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Renderer for the customlang tool.
+ *
+ * @copyright 2019 Bas Brands <bas@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+    /**
+     * Defer to template.
+     *
+     * @param tool_customlang_translator $translator
+     * @return string Html for the translator
+     */
+    protected function render_tool_customlang_translator(\tool_customlang_translator $translator) {
+        $renderabletranslator = new translator($translator);
+        $templatevars = $renderabletranslator->export_for_template($this);
+        return $this->render_from_template('tool_customlang/translator', $templatevars);
+    }
+
+    /**
+     * Defer to template.
+     *
+     * @param tool_customlang_menu $menu
+     * @return string html the customlang menu buttons
+     */
+    protected function render_tool_customlang_menu(\tool_customlang_menu $menu) {
+        $output = '';
+        foreach ($menu->get_items() as $item) {
+            $output .= $this->single_button($item->url, $item->title, $item->method);
+        }
+        return $this->box($output, 'menu');
+    }
+}
diff --git a/admin/tool/customlang/classes/output/translator.php b/admin/tool/customlang/classes/output/translator.php
new file mode 100644 (file)
index 0000000..9b7ac7d
--- /dev/null
@@ -0,0 +1,93 @@
+<?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/>.
+
+/**
+ * customlang specific renderers.
+ *
+ * @package   tool_customlang
+ * @copyright 2019 Moodle
+ * @author    Bas Brands
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_customlang\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use templatable;
+use renderer_base;
+use stdClass;
+
+/**
+ * Class containing data for customlang translator page
+ *
+ * @copyright  2019 Bas Brands
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class translator implements renderable, templatable {
+
+    /**
+     * @var tool_customlang_translator $translator object.
+     */
+    private $translator;
+
+    /**
+     * Construct this renderable.
+     *
+     * @param tool_customlang_translator $translator The translator object.
+     */
+    public function __construct(\tool_customlang_translator $translator) {
+        $this->translator = $translator;
+    }
+
+    /**
+     * Export the data.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+        $data = new stdClass();
+
+        $data->nostrings = $output->notification(get_string('nostringsfound', 'tool_customlang'));
+        $data->formurl = $this->translator->handler;
+        $data->currentpage = $this->translator->currentpage;
+        $data->sesskey = sesskey();
+        $data->strings = [];
+
+        if (!empty($this->translator->strings)) {
+            $data->hasstrings = true;
+            foreach ($this->translator->strings as $string) {
+                // Find strings that use placeholders.
+                if (preg_match('/\{\$a(->.+)?\}/', $string->master)) {
+                    $string->placeholderhelp = $output->help_icon('placeholder', 'tool_customlang',
+                            get_string('placeholderwarning', 'tool_customlang'));
+                }
+                if (!is_null($string->local) and $string->outdated) {
+                    $string->outdatedhelp = $output->help_icon('markinguptodate', 'tool_customlang');
+                    $string->checkupdated = true;
+                }
+                if ($string->original !== $string->master) {
+                    $string->showoriginalvsmaster = true;
+                }
+                $string->local = s($string->local);
+                $data->strings[] = $string;
+            }
+        }
+        return $data;
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/customlang/renderer.php b/admin/tool/customlang/renderer.php
deleted file mode 100644 (file)
index aea7fd6..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Output rendering of Language customization admin tool
- *
- * @package    tool
- * @subpackage customlang
- * @copyright  2010 David Mudrak <david@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Rendering methods for the tool widgets
- */
-class tool_customlang_renderer extends plugin_renderer_base {
-
-    /**
-     * Renders customlang tool menu
-     *
-     * @return string HTML
-     */
-    protected function render_tool_customlang_menu(tool_customlang_menu $menu) {
-        $output = '';
-        foreach ($menu->get_items() as $item) {
-            $output .= $this->single_button($item->url, $item->title, $item->method);
-        }
-        return $this->box($output, 'menu');
-    }
-
-    /**
-     * Renders customlang translation table
-     *
-     * @param tool_customlang_translator $translator
-     * @return string HTML
-     */
-    protected function render_tool_customlang_translator(tool_customlang_translator $translator) {
-        $output = '';
-
-        if (empty($translator->strings)) {
-            return $this->notification(get_string('nostringsfound', 'tool_customlang'));
-        }
-
-        $table = new html_table();
-        $table->id = 'translator';
-        $table->head = array(
-            get_string('headingcomponent', 'tool_customlang'),
-            get_string('headingstringid', 'tool_customlang'),
-            get_string('headingstandard', 'tool_customlang'),
-            get_string('headinglocal', 'tool_customlang'),
-        );
-
-        foreach ($translator->strings as $string) {
-            $cells = array();
-            // component name
-            $cells[0] = new html_table_cell($string->component);
-            $cells[0]->attributes['class'] = 'component';
-            // string identification code
-            $cells[1] = new html_table_cell(html_writer::tag('div', s($string->stringid), array('class' => 'stringid')));
-            $cells[1]->attributes['class'] = 'stringid';
-            // master translation of the string
-            $master = html_writer::tag('div', s($string->master), array('class' => 'preformatted'));
-            $minheight = strlen($string->master) / 200;
-            if (preg_match('/\{\$a(->.+)?\}/', $string->master)) {
-                $master .= html_writer::tag('div', $this->help_icon('placeholder', 'tool_customlang',
-                        get_string('placeholderwarning', 'tool_customlang')), array('class' => 'placeholderinfo'));
-            }
-            $cells[2] = new html_table_cell($master);
-            $cells[2]->attributes['class'] = 'standard master';
-            // local customization of the string
-            $textareaattributes = array('name'=>'cust['.$string->id.']', 'cols'=>40, 'rows'=>3);
-            if ($minheight>1) {
-               $textareaattributes['style'] = 'min-height:' . (int) 4*$minheight . 'em;';
-            }
-            $textarea = html_writer::tag('textarea', s($string->local), $textareaattributes);
-            $cells[3] = new html_table_cell($textarea);
-            if (!is_null($string->local) and $string->outdated) {
-                $mark  = html_writer::empty_tag('input', array('type' => 'checkbox', 'id' => 'update_' . $string->id,
-                                                               'name' => 'updates[]', 'value' => $string->id));
-                $help  = $this->help_icon('markinguptodate', 'tool_customlang');
-                $mark .= html_writer::tag('label', get_string('markuptodate', 'tool_customlang') . $help,
-                                          array('for' => 'update_' . $string->id));
-                $mark  = html_writer::tag('div', $mark, array('class' => 'uptodatewrapper'));
-            } else {
-                $mark  = '';
-            }
-            $cells[3] = new html_table_cell($textarea."\n".$mark);
-            $cells[3]->attributes['class'] = 'local';
-            $cells[3]->id = 'id_'.$string->id;
-            if (!is_null($string->local)) {
-                $cells[3]->attributes['class'] .= ' customized';
-            }
-            if ($string->outdated) {
-                $cells[3]->attributes['class'] .= ' outdated';
-            }
-            if ($string->modified) {
-                $cells[3]->attributes['class'] .= ' modified';
-            }
-
-            if ($string->original !== $string->master) {
-                $cells[0]->rowspan = $cells[1]->rowspan = $cells[3]->rowspan = 2;
-            }
-
-            $row = new html_table_row($cells);
-            $table->data[] = $row;
-
-            if ($string->original !== $string->master) {
-                $cells = array();
-                // original of the string
-                $cells[2] = new html_table_cell(html_writer::tag('div', s($string->original), array('class' => 'preformatted')));
-                $cells[2]->attributes['class'] = 'standard original';
-                $row = new html_table_row($cells);
-                $table->data[] = $row;
-            }
-        }
-
-        $output .= html_writer::start_tag('form', array('method'=>'post', 'action'=>$translator->handler->out()));
-        $output .= html_writer::start_tag('div');
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'translatorsubmitted', 'value'=>1));
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'p', 'value'=>$translator->currentpage));
-        $save1   = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecontinue',
-            'value' => get_string('savecontinue', 'tool_customlang'), 'class' => 'btn btn-secondary'));
-        $save2   = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecheckin',
-            'value' => get_string('savecheckin', 'tool_customlang'), 'class' => 'btn btn-secondary'));
-        $output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
-        $output .= html_writer::table($table);
-        $output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
-        $output .= html_writer::end_tag('div');
-        $output .= html_writer::end_tag('form');
-
-        return $output;
-    }
-}
diff --git a/admin/tool/customlang/styles.css b/admin/tool/customlang/styles.css
deleted file mode 100644 (file)
index 9f9fa98..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-.path-admin-tool-customlang .langselectorbox,
-.path-admin-tool-customlang fieldset.buttonsbar,
-.path-admin-tool-customlang .menu {
-    margin: 5px auto;
-    text-align: center;
-}
-
-.path-admin-tool-customlang .menu .singlebutton,
-.path-admin-tool-customlang .menu .singlebutton form,
-.path-admin-tool-customlang .menu .singlebutton form div {
-    display: inline;
-}
-
-.path-admin-tool-customlang .mform.filterform {
-    width: 70%;
-    margin-left: auto;
-    margin-right: auto;
-}
-
-.path-admin-tool-customlang .mform.filterform .fitem .fitemtitle {
-    width: 30%;
-}
-
-.path-admin-tool-customlang .mform.filterform .fitem .felement {
-    width: 60%;
-    margin-left: 31%;
-}
-
-.path-admin-tool-customlang #translator {
-    width: 100%;
-}
-
-.path-admin-tool-customlang #translator .standard,
-.path-admin-tool-customlang #translator .local {
-    min-width: 35%;
-}
-
-.path-admin-tool-customlang #translator .customized {
-    background-color: #e7f1c3;
-}
-
-.path-admin-tool-customlang #translator .customized.outdated {
-    background-color: #f3f2aa;
-}
-
-.path-admin-tool-customlang #translator .modified {
-    background-color: #ffd3d9;
-}
-
-.path-admin-tool-customlang #translator .customized.modified {
-    background-color: #d2ebff;
-}
-
-.path-admin-tool-customlang #translator textarea {
-    width: 100%;
-    min-height: 4em;
-}
-
-.path-admin-tool-customlang #translator .placeholderinfo {
-    text-align: center;
-    border: 1px dotted #ddd;
-    background-color: #f6f6f6;
-    margin-top: 0.5em;
-}
-
-#page-admin-tool-customlang-index .continuebutton {
-    margin-top: 1em;
-}
-
-.path-admin-tool-customlang #translator .standard.master.cell.c2 {
-    word-break: break-all;
-}
diff --git a/admin/tool/customlang/templates/translator.mustache b/admin/tool/customlang/templates/translator.mustache
new file mode 100644 (file)
index 0000000..8864f83
--- /dev/null
@@ -0,0 +1,150 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_customlang/translator
+
+    Template for the custom language translator page.
+
+    Classes required for JS:
+    -
+
+    Data attributes required for JS:
+    -
+
+    Context variables required for this template:
+    * strings
+
+    Example context (json):
+    {
+        "hasstrings": true,
+        "formurl": "admin/tool/customlang/edit.php?lng=en",
+        "currentpage": 0,
+        "sesskey" : "AZyeeQgmcs",
+        "strings": [
+            {
+                "id": 11,
+                "component": "core",
+                "componentid": 1,
+                "stringid": "course",
+                "original": "Course",
+                "master": "Cursus",
+                "local": "Hoofdstuk",
+                "outdated": 0,
+                "modified": 1
+            }
+        ]
+    }
+}}
+
+{{^hasstrings}}
+    {{{ nostrings }}}
+{{/hasstrings}}
+{{#hasstrings}}
+<form method="post" action="{{{formurl}}}">
+    <input type="hidden" name="translatorsubmitted" value="1">
+    <input type="hidden" name="sesskey" value="{{{ sesskey }}}">
+    <input type="hidden" name="p" value="{{ currentpage }}">
+
+    <fieldset class="m-a-1 m-3">
+        <button type="submit" name="savecontinue" class="btn btn-secondary">
+            {{#str}}savecontinue, tool_customlang{{/str}}
+        </button>
+        <button type="submit" name="savecheckin" class="btn btn-secondary">
+            {{#str}}savecheckin, tool_customlang{{/str}}
+        </button>
+    </fieldset>
+
+    <div class="list-group">
+        <div class="container-fluid d-none d-md-block list-group-item border-bottom-0">
+            <div class="row-fluid">
+                <div class="col-sm-4 col-md-2 span2">
+                    <strong>{{#str}}headingcomponent, tool_customlang{{/str}}</strong>
+                </div>
+                <div class="col-sm-4 col-md-2 span2">
+                    <strong>{{#str}}headingstringid, tool_customlang{{/str}}</strong>
+                </div>
+                <div class="col-sm-4 col-md-2 span2">
+                    <strong>{{#str}}headingstandard, tool_customlang{{/str}}</strong>
+                </div>
+                <div class="col-sm-12 col-md-6 span6">
+                    <span class="p-l-1 pl-3">
+                        <strong>{{#str}}headinglocal, tool_customlang{{/str}}</strong>
+                    </span>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="list-group">
+    {{#strings}}
+        <div class="container-fluid list-group-item
+                {{#local}}list-group-item-info{{/local}}
+                {{#outdated}}list-group-item-warning{{/outdated}}
+                {{#modified}}list-group-item-info{{/modified}}"
+            >
+            <div class="row-fluid ">
+                <div class="col-sm-4 col-md-2 span2">
+                    <div class="d-md-none">
+                        <strong>{{#str}}headingcomponent, tool_customlang{{/str}}</strong>
+                    </div>
+                    {{{ component }}}
+                </div>
+                <div class="col-sm-4 col-md-2 span2 text-break">
+                    <div class="d-md-none">
+                        <strong>{{#str}}headingstringid, tool_customlang{{/str}}</strong>
+                    </div>
+                    {{{ stringid }}}
+                </div>
+                <div class="col-sm-4 col-md-2 span2">
+                    <div class="d-md-none">
+                        <strong>{{#str}}headingstandard, tool_customlang{{/str}}</strong>
+                    </div>
+                    {{{ master }}}
+                    <div class="info">
+                        {{{ placeholderhelp }}}
+                        {{{ outdatedhelp}}}
+                    </div>
+                    {{#showoriginalvsmaster}}
+                    <div class="alert-secondary mt-3 m-t-1">
+                        {{{ original }}}
+                    </div>
+                    {{/showoriginalvsmaster}}
+                </div>
+                <div class="col-sm-12 col-md-6 mt-sm-3 mt-md-0 span6">
+                    <div class="d-md-none">
+                        <strong>{{#str}}headinglocal, tool_customlang{{/str}}</strong>
+                    </div>
+                    <div class="py-2 py-md-0 px-md-3">
+                        <textarea class="form-control w-100 border-box" name="cust[{{id}}]" cols="40" rows="3">{{{ local }}}</textarea>
+
+                        {{#checkupdated}}
+                        <div class="uptodatewrapper">
+                            <div class="form-check">
+                                <input id="update_{{id}}" class="form-check-input" name="updates[]" type="checkbox" value="{{id}}">
+                                <label for="update_{{id}}" class="form-check-label">{{#str}}markuptodate, tool_customlang{{/str}}</label>
+                                {{{ outdatedhelp }}}
+                            </div>
+                        </div>
+                        {{/checkupdated}}
+                    </div>
+                </div>
+            </div>
+        </div>
+    {{/strings}}
+    </div>
+</form>
+{{/hasstrings}}
diff --git a/admin/tool/dataprivacy/tests/coverage.php b/admin/tool/dataprivacy/tests/coverage.php
new file mode 100644 (file)
index 0000000..9af4b87
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Coverage information for the tool_dataprivacy plugin.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Coverage information for the tool_dataprivacy plugin.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+return new class extends phpunit_coverage_info {
+    /** @var array The list of folders relative to the plugin root to whitelist in coverage generation. */
+    protected $whitelistfolders = [
+        'classes',
+    ];
+
+    /** @var array The list of files relative to the plugin root to whitelist in coverage generation. */
+    protected $whitelistfiles = [];
+
+    /** @var array The list of folders relative to the plugin root to excludelist in coverage generation. */
+    protected $excludelistfolders = [
+    ];
+
+    /** @var array The list of files relative to the plugin root to excludelist in coverage generation. */
+    protected $excludelistfiles = [];
+};
index 9a62976..8681715 100644 (file)
@@ -301,6 +301,8 @@ class page_agreedocs implements renderable, templatable {
                 redirect(new moodle_url('/admin/tool/policy/view.php', $urlparams));
             }
         } else {
+            // Update the policyagreed for the user to avoid infinite loop because there are no policies to-be-accepted.
+            api::update_policyagreed($userid);
             $this->redirect_to_previous_url();
         }
     }
index b35a20e..c996d8b 100644 (file)
@@ -292,3 +292,23 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     When I press "Give consent"
     Then "Accepted on user's behalf" "text" should exist in the "User One" "table_row"
     And "Accepted on user's behalf" "text" should exist in the "User Two" "table_row"
+
+  Scenario: View acceptances made by users on their own after inactivating a policy
+    Given I log in as "user1"
+    And I should see "This site policy"
+    And I should not see "Course overview"
+    And I press "Next"
+    And I set the field "I agree to the This site policy" to "1"
+    And I press "Next"
+    And I should see "Course overview"
+    And I log out
+    And I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Manage policies" in site administration
+    And I click on "Actions" "link_or_button" in the "This privacy policy" "table_row"
+    And I click on "Set status to \"Active\"" "link" in the "This privacy policy" "table_row"
+    And I press "Continue"
+    And I click on "Set status to \"Inactive\"" "link" in the "This privacy policy" "table_row"
+    And I press "Continue"
+    And I log out
+    When I log in as "user1"
+    Then I should see "Course overview"
index be0d3a0..919e9f6 100644 (file)
@@ -63,7 +63,9 @@ interface classifier extends predictor {
      * @param int $niterations
      * @param \stored_file $dataset
      * @param string $outputdir
+     * @param  string $trainedmodeldir
      * @return \stdClass
      */
-    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
+    public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
+            $outputdir, $trainedmodeldir);
 }
index 3b6c6e4..d4a073b 100644 (file)
@@ -537,6 +537,29 @@ class model {
         }
 
         $options['evaluation'] = true;
+
+        if (empty($options['mode'])) {
+            $options['mode'] = 'configuration';
+        }
+
+        switch ($options['mode']) {
+            case 'trainedmodel':
+
+                // We are only interested on the time splitting method used by the trained model.
+                $options['timesplitting'] = $this->model->timesplitting;
+
+                // Provide the trained model directory to the ML backend if that is what we want to evaluate.
+                $trainedmodeldir = $this->get_output_dir(['execution']);
+                break;
+            case 'configuration':
+
+                $trainedmodeldir = false;
+                break;
+
+            default:
+                throw new \moodle_exception('errorunknownaction', 'analytics');
+        }
+
         $this->init_analyser($options);
 
         if (empty($this->get_indicators())) {
@@ -575,10 +598,10 @@ class model {
             // Evaluate the dataset, the deviation we accept in the results depends on the amount of iterations.
             if ($this->get_target()->is_linear()) {
                 $predictorresult = $predictor->evaluate_regression($this->get_unique_id(), self::ACCEPTED_DEVIATION,
-                self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+                    self::EVALUATION_ITERATIONS, $dataset, $outputdir, $trainedmodeldir);
             } else {
                 $predictorresult = $predictor->evaluate_classification($this->get_unique_id(), self::ACCEPTED_DEVIATION,
-                self::EVALUATION_ITERATIONS, $dataset, $outputdir);
+                    self::EVALUATION_ITERATIONS, $dataset, $outputdir, $trainedmodeldir);
             }
 
             $result->status = $predictorresult->status;
@@ -596,7 +619,7 @@ class model {
                 $dir = $predictorresult->dir;
             }
 
-            $result->logid = $this->log_result($timesplitting->get_id(), $result->score, $dir, $result->info);
+            $result->logid = $this->log_result($timesplitting->get_id(), $result->score, $dir, $result->info, $options['mode']);
 
             $results[$timesplitting->get_id()] = $result;
         }
@@ -1462,6 +1485,29 @@ class model {
         return \core_analytics\dataset_manager::export_training_data($this->get_id(), $timesplittingid);
     }
 
+    /**
+     * Has the model been trained using data from this site?
+     *
+     * This method is useful to determine if a trained model can be evaluated as
+     * we can not use the same data for training and for evaluation.
+     *
+     * @return bool
+     */
+    public function trained_locally() : bool {
+        global $DB;
+
+        if (!$this->is_trained() || $this->is_static()) {
+            // Early exit.
+            return false;
+        }
+
+        if ($DB->record_exists('analytics_train_samples', ['modelid' => $this->model->id])) {
+            return true;
+        }
+
+        return false;
+    }
+
     /**
      * Flag the provided file as used for training or prediction.
      *
@@ -1487,14 +1533,16 @@ class model {
      * @param float $score
      * @param string $dir
      * @param array $info
+     * @param string $evaluationmode
      * @return int The inserted log id
      */
-    protected function log_result($timesplittingid, $score, $dir = false, $info = false) {
+    protected function log_result($timesplittingid, $score, $dir = false, $info = false, $evaluationmode = 'configuration') {
         global $DB, $USER;
 
         $log = new \stdClass();
         $log->modelid = $this->get_id();
         $log->version = $this->model->version;
+        $log->evaluationmode = $evaluationmode;
         $log->target = $this->model->target;
         $log->indicators = $this->model->indicators;
         $log->timesplitting = $timesplittingid;
index c2d2a89..c8e0bcf 100644 (file)
@@ -63,7 +63,9 @@ interface regressor extends predictor {
      * @param int $niterations
      * @param \stored_file $dataset
      * @param string $outputdir
+     * @param  string $trainedmodeldir
      * @return \stdClass
      */
-    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
+    public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
+            $outputdir, $trainedmodeldir);
 }
diff --git a/analytics/classes/stats.php b/analytics/classes/stats.php
new file mode 100644 (file)
index 0000000..0caf975
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - https://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Provides the {@link \core_analytics\stats} class.
+ *
+ * @package     core_analytics
+ * @copyright   2019 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Provides stats and meta information about the analytics usage on this site.
+ *
+ * @copyright 2019 David Mudrák <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class stats {
+
+    /**
+     * Return the number of models enabled on this site.
+     *
+     * @return int
+     */
+    public static function enabled_models() : int {
+        return count(manager::get_all_models(true));
+    }
+
+    /**
+     * Return the number of predictions generated by the system.
+     *
+     * @return int
+     */
+    public static function predictions() : int {
+        global $DB;
+
+        return $DB->count_records('analytics_predictions');
+    }
+
+    /**
+     * Return the number of suggested actions executed by users.
+     *
+     * @return int
+     */
+    public static function actions() : int {
+        global $DB;
+
+        return $DB->count_records('analytics_prediction_actions');
+    }
+
+    /**
+     * Return the number of suggested actions flagged as not useful.
+     *
+     * @return int
+     */
+    public static function actions_not_useful() : int {
+        global $DB;
+
+        return $DB->count_records('analytics_prediction_actions', ['actionname' => prediction::ACTION_NOT_USEFUL]);
+    }
+}
index 18582c3..9b69093 100644 (file)
@@ -274,7 +274,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
      * test_ml_export_import
      *
      * @param string $predictionsprocessorclass The class name
-     * @dataProvider provider_ml_export_import
+     * @dataProvider provider_ml_processors
      */
     public function test_ml_export_import($predictionsprocessorclass) {
 
@@ -296,6 +296,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $model->update(true, false, '\core\analytics\time_splitting\quarters', get_class($predictionsprocessor));
 
         $model->train();
+        $this->assertTrue($model->trained_locally());
 
         $this->generate_courses(10, ['visible' => 0]);
 
@@ -314,16 +315,18 @@ class core_analytics_prediction_testcase extends advanced_testcase {
             $this->assertEquals($importedmodelresults->predictions[$sampleid]->prediction, $prediction->prediction);
         }
 
+        $this->assertFalse($importmodel->trained_locally());
+
         set_config('enabled_stores', '', 'tool_log');
         get_log_manager(true);
     }
 
     /**
-     * provider_ml_export_import
+     * provider_ml_processors
      *
      * @return array
      */
-    public function provider_ml_export_import() {
+    public function provider_ml_processors() {
         $cases = [
             'case' => [],
         ];
@@ -425,14 +428,14 @@ class core_analytics_prediction_testcase extends advanced_testcase {
     /**
      * Basic test to check that prediction processors work as expected.
      *
-     * @dataProvider provider_ml_test_evaluation
+     * @dataProvider provider_ml_test_evaluation_configuration
      * @param string $modelquality
      * @param int $ncourses
      * @param array $expected
      * @param string $predictionsprocessorclass
      * @return void
      */
-    public function test_ml_evaluation($modelquality, $ncourses, $expected, $predictionsprocessorclass) {
+    public function test_ml_evaluation_configuration($modelquality, $ncourses, $expected, $predictionsprocessorclass) {
         $this->resetAfterTest(true);
         $this->setAdminuser();
         set_config('enabled_stores', 'logstore_standard', 'tool_log');
@@ -473,6 +476,46 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         get_log_manager(true);
     }
 
+    /**
+     * Tests the evaluation of already trained models.
+     *
+     * @dataProvider provider_ml_processors
+     * @param  string $predictionsprocessorclass
+     * @return null
+     */
+    public function test_ml_evaluation_trained_model($predictionsprocessorclass) {
+        $this->resetAfterTest(true);
+        $this->setAdminuser();
+        set_config('enabled_stores', 'logstore_standard', 'tool_log');
+        set_config('timesplittings',
+            '\core\analytics\time_splitting\quarters,\core\analytics\time_splitting\quarters_accum', 'analytics');
+
+        $model = $this->add_perfect_model();
+
+        // Generate training data.
+        $this->generate_courses(50);
+
+        // We repeat the test for all prediction processors.
+        $predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
+        if ($predictionsprocessor->is_ready() !== true) {
+            $this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
+        }
+
+        $model->update(true, false, '\\core\\analytics\\time_splitting\\quarters', get_class($predictionsprocessor));
+        $model->train();
+
+        $zipfilename = 'model-zip-' . microtime() . '.zip';
+        $zipfilepath = $model->export_model($zipfilename);
+        $importmodel = \core_analytics\model::import_model($zipfilepath);
+
+        $results = $importmodel->evaluate(['mode' => 'trainedmodel']);
+        $this->assertEquals(0, $results['\\core\\analytics\\time_splitting\\quarters']->status);
+        $this->assertEquals(1, $results['\\core\\analytics\\time_splitting\\quarters']->score);
+
+        set_config('enabled_stores', '', 'tool_log');
+        get_log_manager(true);
+    }
+
     /**
      * test_read_indicator_calculations
      *
@@ -547,11 +590,11 @@ class core_analytics_prediction_testcase extends advanced_testcase {
     }
 
     /**
-     * provider_ml_test_evaluation
+     * provider_ml_test_evaluation_configuration
      *
      * @return array
      */
-    public function provider_ml_test_evaluation() {
+    public function provider_ml_test_evaluation_configuration() {
 
         $cases = array(
             'bad' => array(
diff --git a/analytics/tests/stats_test.php b/analytics/tests/stats_test.php
new file mode 100644 (file)
index 0000000..d92f403
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+// This file is part of Moodle - https://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Provides the {@link analytics_stats_testcase} class.
+ *
+ * @package     core_analytics
+ * @category    test
+ * @copyright   2019 David Mudrák <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
+require_once(__DIR__ . '/fixtures/test_target_shortname.php');
+
+/**
+ * Unit tests for the analytics stats.
+ *
+ * @copyright 2019 David Mudrák <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class analytics_stats_testcase extends advanced_testcase {
+
+    /**
+     * Set up the test environment.
+     */
+    public function setUp() {
+
+        $this->setAdminUser();
+    }
+
+    /**
+     * Test the {@link \core_analytics\stats::enabled_models()} implementation.
+     */
+    public function test_enabled_models() {
+
+        $this->resetAfterTest(true);
+
+        // By default, sites have {@link \core\analytics\target\no_teaching} enabled.
+        $this->assertEquals(1, \core_analytics\stats::enabled_models());
+
+        $model = \core_analytics\model::create(
+            \core_analytics\manager::get_target('\core\analytics\target\course_dropout'),
+            [
+                \core_analytics\manager::get_indicator('\core\analytics\indicator\any_write_action'),
+            ]
+        );
+
+        // Purely adding a new model does not make it included in the stats.
+        $this->assertEquals(1, \core_analytics\stats::enabled_models());
+
+        // New models must be enabled to have them counted.
+        $model->enable('\core\analytics\time_splitting\quarters');
+        $this->assertEquals(2, \core_analytics\stats::enabled_models());
+    }
+
+    /**
+     * Test the {@link \core_analytics\stats::predictions()} implementation.
+     */
+    public function test_predictions() {
+
+        $this->resetAfterTest(true);
+
+        $model = \core_analytics\model::create(
+            \core_analytics\manager::get_target('test_target_shortname'),
+            [
+                \core_analytics\manager::get_indicator('test_indicator_fullname'),
+            ]
+        );
+
+        $model->enable('\core\analytics\time_splitting\no_splitting');
+
+        // Train the model.
+        $this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]);
+        $this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]);
+        $model->train();
+
+        // No predictions yet.
+        $this->assertEquals(0, \core_analytics\stats::predictions());
+
+        // Get one new prediction.
+        $this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]);
+        $result = $model->predict();
+
+        $this->assertEquals(1, count($result->predictions));
+        $this->assertEquals(1, \core_analytics\stats::predictions());
+
+        // Nothing changes if there is no new prediction.
+        $result = $model->predict();
+        $this->assertFalse(isset($result->predictions));
+        $this->assertEquals(1, \core_analytics\stats::predictions());
+
+        // Get two more predictions, we have three in total now.
+        $this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]);
+        $this->getDataGenerator()->create_course(['shortname' => 'cc', 'fullname' => 'cc', 'visible' => 0]);
+
+        $result = $model->predict();
+        $this->assertEquals(2, count($result->predictions));
+        $this->assertEquals(3, \core_analytics\stats::predictions());
+    }
+
+    /**
+     * Test the {@link \core_analytics\stats::actions()} and {@link \core_analytics\stats::actions_not_useful()} implementation.
+     */
+    public function test_actions() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $model = \core_analytics\model::create(
+            \core_analytics\manager::get_target('test_target_shortname'),
+            [
+                \core_analytics\manager::get_indicator('test_indicator_fullname'),
+            ]
+        );
+
+        $model->enable('\core\analytics\time_splitting\no_splitting');
+
+        // Train the model.
+        $this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]);
+        $this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]);
+        $model->train();
+
+        // Generate two predictions.
+        $this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]);
+        $this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]);
+        $model->predict();
+
+        list($p1, $p2) = array_values($DB->get_records('analytics_predictions'));
+
+        $p1 = new \core_analytics\prediction($p1, []);
+        $p2 = new \core_analytics\prediction($p2, []);
+
+        // No actions executed at the start.
+        $this->assertEquals(0, \core_analytics\stats::actions());
+        $this->assertEquals(0, \core_analytics\stats::actions_not_useful());
+
+        // The user has acknowledged the first prediction.
+        $p1->action_executed(\core_analytics\prediction::ACTION_FIXED, $model->get_target());
+        $this->assertEquals(1, \core_analytics\stats::actions());
+        $this->assertEquals(0, \core_analytics\stats::actions_not_useful());
+
+        // The user has marked the other prediction as not useful.
+        $p2->action_executed(\core_analytics\prediction::ACTION_NOT_USEFUL, $model->get_target());
+        $this->assertEquals(2, \core_analytics\stats::actions());
+        $this->assertEquals(1, \core_analytics\stats::actions_not_useful());
+    }
+}
index 550a3da..3187365 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in analytics sub system,
 information provided here is intended especially for developers.
 
+=== 3.7 ===
+
+* \core_analytics\regressor::evaluate_regression and \core_analytics\classifier::evaluate_classification
+  have been updated to include a new $trainedmodeldir param. This new param will be used to evaluate the
+  existing trained model.
+
 === 3.5 ===
 
 * There are two new methods for analysers, processes_user_data() and join_sample_user(). You
index 77260f0..e0b38bd 100644 (file)
@@ -96,6 +96,7 @@ if (!has_any_capability(array(
         'moodle/badges:viewawarded',
         'moodle/badges:createbadge',
         'moodle/badges:awardbadge',
+        'moodle/badges:configurecriteria',
         'moodle/badges:configuremessages',
         'moodle/badges:configuredetails',
         'moodle/badges:deletebadge'), $PAGE->context)) {
index 910f9fb..6a79717 100644 (file)
@@ -48,7 +48,7 @@ $string['aria:sortingdropdown'] = 'Sorting drop-down menu';
 $string['card'] = 'Card';
 $string['cards'] = 'Cards';
 $string['courseprogress'] = 'Course progress:';
-$string['complete'] = 'complete';
+$string['completepercent'] = '{$a}% complete';
 $string['favourites'] = 'Starred';
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
@@ -93,4 +93,5 @@ $string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected
 $string['viewcourse'] = 'View course';
 
 // Deprecated since Moodle 3.7.
-$string['nocourses'] = 'No courses';
\ No newline at end of file
+$string['complete'] = 'complete';
+$string['nocourses'] = 'No courses';
index 4e22e76..fb6f86c 100644 (file)
@@ -13,4 +13,5 @@ sortbydates,block_myoverview
 timeline,block_myoverview
 viewcoursename,block_myoverview
 privacy:metadata:overviewlasttab,block_myoverview
-nocourses,block_myoverview
\ No newline at end of file
+nocourses,block_myoverview
+complete,block_myoverview
\ No newline at end of file
index 2567555..574b5e5 100644 (file)
 }}
 
 <div class="m-b-1 mr-1 d-flex align-items-center">
-    <div class="d-none d-md-inline-block mr-1">{{#str}} sortby, core {{/str}}</div>
     <div class="dropdown">
         <button id="sortingdropdown" type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown"  aria-haspopup="true" aria-expanded="false" aria-label="{{#str}} aria:sortingdropdown, block_myoverview {{/str}}">
-            <span data-active-item-text>
+            {{#pix}} t/sort_by {{/pix}}
+            <span class="d-sm-inline-block" data-active-item-text>
                 {{#title}}{{#str}} title, block_myoverview {{/str}}{{/title}}
                 {{#lastaccessed}}{{#str}} lastaccessed, block_myoverview {{/str}}{{/lastaccessed}}
             </span>
index 9bb0a57..02fa1f0 100644 (file)
@@ -30,5 +30,5 @@
 </div>
 <div class="small">
     <span class="sr-only">{{#str}}aria:courseprogress, block_myoverview{{/str}}</span>
-    <strong>{{progress}}%</strong> {{#str}}complete, block_myoverview{{/str}}
+    {{#str}}completepercent, block_myoverview, <strong>{{progress}}</strong>{{/str}}
 </div>
index 938763b..52d92a6 100644 (file)
     {}
 }}
 <div data-region="day-filter" class="dropdown">
-    <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+    <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
+            aria-label="{{#str}} ariadayfilter, block_timeline {{/str}}" aria-controls="menudayfilter">
         {{#pix}} i/duration {{/pix}}
-        <span class="sr-only">
-            {{#str}} ariadayfilter, block_timeline {{/str}}
-            <span data-active-item-text>{{#str}} next30days, block_timeline {{/str}}</span>
+        <span class="sr-only" data-active-item-text>
+            {{#all}} {{#str}} all, core {{/str}} {{/all}}
+            {{#overdue}} {{#str}} overdue, block_timeline {{/str}} {{/overdue}}
+            {{#next7days}} {{#str}}next7days, block_timeline {{/str}} {{/next7days}}
+            {{#next30days}} {{#str}}next30days, block_timeline {{/str}} {{/next30days}}
+            {{#next3months}} {{#str}}next3months, block_timeline {{/str}} {{/next3months}}
+            {{#next6months}} {{#str}}next6months, block_timeline {{/str}} {{/next6months}}
         </span>
     </button>
-    <div role="menu" class="dropdown-menu" data-show-active-item>
+    <div id="menudayfilter" role="menu" class="dropdown-menu" data-show-active-item>
         <a
             class="dropdown-item {{#all}} active {{/all}}"
             href="#"
             data-from="-14"
             data-filtername="all"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} all, core {{/str}}{{/str}}"
+            role="menuitem"
+            {{#all}}aria-current="true"{{/all}}
         >
             {{#str}} all, core {{/str}}
         </a>
             data-to="0"
             data-filtername="overdue"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} overdue, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#overdue}}aria-current="true"{{/overdue}}
         >
             {{#str}} overdue, block_timeline {{/str}}
         </a>
-        <div class="dropdown-divider"></div>
+        <div class="dropdown-divider" role="separator"></div>
         <h6 class="dropdown-header">{{#str}} duedate, block_timeline {{/str}}</h6>
         <a
             class="dropdown-item {{#next7days}} active {{/next7days}}"
@@ -59,6 +68,8 @@
             data-to="7"
             data-filtername="next7days"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#next7days}}aria-current="true"{{/next7days}}
         >
             {{#str}} next7days, block_timeline {{/str}}
         </a>
@@ -69,6 +80,8 @@
             data-to="30"
             data-filtername="next30days"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#next30days}}aria-current="true"{{/next30days}}
         >
             {{#str}} next30days, block_timeline {{/str}}
         </a>
@@ -79,6 +92,8 @@
             data-to="90"
             data-filtername="next3months"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#next3months}}aria-current="true"{{/next3months}}
         >
             {{#str}} next3months, block_timeline {{/str}}
         </a>
             data-to="180"
             data-filtername="next6months"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#next6months}}aria-current="true"{{/next6months}}
         >
             {{#str}} next6months, block_timeline {{/str}}
         </a>
index aa9d14d..106bf75 100644 (file)
     {}
 }}
 <div data-region="view-selector" class="btn-group">
-    <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+    <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
+            aria-label="{{#str}} ariaviewselector, block_timeline{{/str}}" aria-controls="menusortby">
         {{#pix}} t/sort_by {{/pix}}
-        <span class="sr-only">
-            {{#sorttimelinecourses}}<span data-active-item-text>{{/sorttimelinecourses}}{{#str}} ariaviewselector, block_timeline{{/str}}{{#sorttimelinecourses}}</span>{{/sorttimelinecourses}}
-            {{#sorttimelinedates}}<span data-active-item-text>{{/sorttimelinedates}}{{#str}} sortbydates, block_timeline {{/str}}{{#sorttimelinedates}}</span>{{/sorttimelinedates}}
+        <span class="sr-only" data-active-item-text>
+            {{#sorttimelinecourses}}{{#str}} sortbycourses, block_timeline{{/str}}{{/sorttimelinecourses}}
+            {{#sorttimelinedates}}{{#str}} sortbydates, block_timeline {{/str}}{{/sorttimelinedates}}
         </span>
     </button>
-    <div role="menu" class="dropdown-menu dropdown-menu-right list-group hidden" data-show-active-item data-skip-active-class="true" >
+    <div id="menusortby" role="menu" class="dropdown-menu dropdown-menu-right list-group hidden" data-show-active-item data-skip-active-class="true">
         <a
             class="dropdown-item {{#sorttimelinedates}}active{{/sorttimelinedates}}"
             href="#view_dates_{{uniqid}}"
             data-toggle="tab"
             data-filtername="sortbydates"
             aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbydates, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#sorttimelinedates}}aria-current="true"{{/sorttimelinedates}}
         >
             {{#str}} sortbydates, block_timeline {{/str}}
         </a>
@@ -46,6 +49,8 @@
             data-toggle="tab"
             data-filtername="sortbycourses"
             aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbycourses, block_timeline {{/str}}{{/str}}"
+            role="menuitem"
+            {{#sorttimelinecourses}}aria-current="true"{{/sorttimelinecourses}}
         >
             {{#str}} sortbycourses, block_timeline {{/str}}
         </a>
index d9a8a20..ad50e69 100644 (file)
@@ -36,7 +36,7 @@ Feature: The timeline block allows users to see upcoming courses
 
   Scenario: Next 30 days in course view
     Given I log in as "student1"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     When I click on "Sort by courses" "link" in the "Timeline" "block"
     Then I should see "Course 1" in the "Timeline" "block"
     And I should see "Course 2" in the "Timeline" "block"
@@ -52,9 +52,9 @@ Feature: The timeline block allows users to see upcoming courses
 
   Scenario: All in course view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     And I click on "All" "link" in the "Timeline" "block"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     And I click on "Sort by courses" "link" in the "Timeline" "block"
     When I click on "More courses" "button" in the "Timeline" "block"
     Then I should see "Course 3" in the "Timeline" "block"
@@ -73,9 +73,9 @@ Feature: The timeline block allows users to see upcoming courses
 
   Scenario: Persistent sort filter
     Given I log in as "student1"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     And I click on "Sort by dates" "link" in the "Timeline" "block"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     And I click on "Sort by courses" "link" in the "Timeline" "block"
     And I reload the page
     Then I should see "Course 1" in the "Timeline" "block"
index 29548a0..479aa84 100644 (file)
@@ -33,7 +33,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: Next 7 days in date view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     When I click on "Next 7 days" "link" in the "Timeline" "block"
     Then I should see "Test choice 1 closes" in the "Timeline" "block"
     And I should see "Test feedback 1 closes" in the "Timeline" "block"
@@ -44,7 +44,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: Overdue in date view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     When I click on "Overdue" "link" in the "Timeline" "block"
     Then I should see "Test assign 1 is due" in the "Timeline" "block"
     And I should not see "Test choice 2 closes" in the "Timeline" "block"
@@ -55,7 +55,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: All in date view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     When I click on "All" "link" in the "Timeline" "block"
     Then I should see "Test assign 1 is due" in the "Timeline" "block"
     And I should see "Test feedback 1 closes" in the "Timeline" "block"
@@ -75,7 +75,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: All in date view no next
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     And I click on "All" "link" in the "Timeline" "block"
     And I click on "5" "button" in the "Timeline" "block"
     When I click on "25" "link" in the "Timeline" "block"
@@ -89,7 +89,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: Persistent All in date view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     When I click on "All" "link" in the "Timeline" "block"
     And I reload the page
     Then I should see "Test assign 1 is due" in the "Timeline" "block"
@@ -110,7 +110,7 @@ Feature: The timeline block allows users to see upcoming activities
 
   Scenario: Persistent Overdue in date view
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     When I click on "Overdue" "link" in the "Timeline" "block"
     And I reload the page
     Then I should see "Test assign 1 is due" in the "Timeline" "block"
index c40bbf5..f08bc38 100644 (file)
@@ -35,9 +35,9 @@ Feature: The timeline block allows user persistence of their page limits
 
   Scenario: Toggle the page limit 5 - 25
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     And I click on "All" "link" in the "Timeline" "block"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     And I click on "Sort by dates" "link" in the "Timeline" "block"
     When I click on "5" "button" in the "Timeline" "block"
     And I click on "25" "link"
@@ -48,9 +48,9 @@ Feature: The timeline block allows user persistence of their page limits
 
   Scenario: Toggle the page limit 25 - 5
     Given I log in as "student1"
-    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "Filter timeline items" "button" in the "Timeline" "block"
     And I click on "All" "link" in the "Timeline" "block"
-    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort timeline items" "button" in the "Timeline" "block"
     And I click on "Sort by dates" "link" in the "Timeline" "block"
     When I click on "5" "button" in the "Timeline" "block"
     And I click on "25" "link"
diff --git a/cache/stores/mongodb/MongoDB/BulkWriteResult.php b/cache/stores/mongodb/MongoDB/BulkWriteResult.php
new file mode 100755 (executable)
index 0000000..cdf6654
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB;
+
+use MongoDB\Driver\WriteResult;
+use MongoDB\Exception\BadMethodCallException;
+
+/**
+ * Result class for a bulk write operation.
+ */
+class BulkWriteResult
+{
+    private $writeResult;
+    private $insertedIds;
+    private $isAcknowledged;
+
+    /**
+     * Constructor.
+     *
+     * @param WriteResult $writeResult
+     * @param mixed[]     $insertedIds
+     */
+    public function __construct(WriteResult $writeResult, array $insertedIds)
+    {
+        $this->writeResult = $writeResult;
+        $this->insertedIds = $insertedIds;
+        $this->isAcknowledged = $writeResult->isAcknowledged();
+    }
+
+    /**
+     * Return the number of documents that were deleted.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return integer
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getDeletedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getDeletedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return the number of documents that were inserted.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return integer
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getInsertedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getInsertedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return a map of the inserted documents' IDs.
+     *
+     * The index of each ID in the map corresponds to each document's position
+     * in the bulk operation. If a document had an ID prior to inserting (i.e.
+     * the driver did not generate an ID), the index will contain its "_id"
+     * field value. Any driver-generated ID will be a MongoDB\BSON\ObjectId
+     * instance.
+     *
+     * @return mixed[]
+     */
+    public function getInsertedIds()
+    {
+        return $this->insertedIds;
+    }
+
+    /**
+     * Return the number of documents that were matched by the filter.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return integer
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getMatchedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getMatchedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return the number of documents that were modified.
+     *
+     * This value is undefined (i.e. null) if the write executed as a legacy
+     * operation instead of command.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return integer|null
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getModifiedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getModifiedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return the number of documents that were upserted.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return integer
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getUpsertedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getUpsertedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return a map of the upserted documents' IDs.
+     *
+     * The index of each ID in the map corresponds to each document's position
+     * in bulk operation. If a document had an ID prior to upserting (i.e. the
+     * server did not need to generate an ID), this will contain its "_id". Any
+     * server-generated ID will be a MongoDB\BSON\ObjectId instance.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see BulkWriteResult::isAcknowledged()
+     * @return mixed[]
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getUpsertedIds()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getUpsertedIds();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return whether this update was acknowledged by the server.
+     *
+     * If the update was not acknowledged, other fields from the WriteResult
+     * (e.g. matchedCount) will be undefined.
+     *
+     * @return boolean
+     */
+    public function isAcknowledged()
+    {
+        return $this->isAcknowledged;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/ChangeStream.php b/cache/stores/mongodb/MongoDB/ChangeStream.php
new file mode 100755 (executable)
index 0000000..98a703f
--- /dev/null
@@ -0,0 +1,228 @@
+<?php
+/*
+ * Copyright 2017 MongoDB, 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.
+ */
+
+namespace MongoDB;
+
+use MongoDB\BSON\Serializable;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Exception\ConnectionException;
+use MongoDB\Driver\Exception\RuntimeException;
+use MongoDB\Driver\Exception\ServerException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\ResumeTokenException;
+use IteratorIterator;
+use Iterator;
+
+/**
+ * Iterator for a change stream.
+ *
+ * @api
+ * @see \MongoDB\Collection::watch()
+ * @see http://docs.mongodb.org/manual/reference/command/changeStream/
+ */
+class ChangeStream implements Iterator
+{
+    /**
+     * @deprecated 1.4
+     * @todo Remove this in 2.0 (see: PHPLIB-360)
+     */
+    const CURSOR_NOT_FOUND = 43;
+
+    private static $errorCodeCappedPositionLost = 136;
+    private static $errorCodeInterrupted = 11601;
+    private static $errorCodeCursorKilled = 237;
+
+    private $resumeToken;
+    private $resumeCallable;
+    private $csIt;
+    private $key = 0;
+    private $hasAdvanced = false;
+
+    /**
+     * Constructor.
+     *
+     * @internal
+     * @param Cursor $cursor
+     * @param callable $resumeCallable
+     */
+    public function __construct(Cursor $cursor, callable $resumeCallable)
+    {
+        $this->resumeCallable = $resumeCallable;
+        $this->csIt = new IteratorIterator($cursor);
+    }
+
+    /**
+     * @see http://php.net/iterator.current
+     * @return mixed
+     */
+    public function current()
+    {
+        return $this->csIt->current();
+    }
+
+    /**
+     * @return \MongoDB\Driver\CursorId
+     */
+    public function getCursorId()
+    {
+        return $this->csIt->getInnerIterator()->getId();
+    }
+
+    /**
+     * @see http://php.net/iterator.key
+     * @return mixed
+     */
+    public function key()
+    {
+        if ($this->valid()) {
+            return $this->key;
+        }
+        return null;
+    }
+
+    /**
+     * @see http://php.net/iterator.next
+     * @return void
+     */
+    public function next()
+    {
+        try {
+            $this->csIt->next();
+            if ($this->valid()) {
+                if ($this->hasAdvanced) {
+                    $this->key++;
+                }
+                $this->hasAdvanced = true;
+                $this->resumeToken = $this->extractResumeToken($this->csIt->current());
+            }
+            /* If the cursorId is 0, the server has invalidated the cursor so we
+             * will never perform another getMore. This means that we cannot
+             * resume and we can therefore unset the resumeCallable, which will
+             * free any reference to Watch. This will also free the only
+             * reference to an implicit session, since any such reference
+             * belongs to Watch. */
+            if ((string) $this->getCursorId() === '0') {
+                $this->resumeCallable = null;
+            }
+        } catch (RuntimeException $e) {
+            if ($this->isResumableError($e)) {
+                $this->resume();
+            }
+        }
+    }
+
+    /**
+     * @see http://php.net/iterator.rewind
+     * @return void
+     */
+    public function rewind()
+    {
+        try {
+            $this->csIt->rewind();
+            if ($this->valid()) {
+                $this->hasAdvanced = true;
+                $this->resumeToken = $this->extractResumeToken($this->csIt->current());
+            }
+            // As with next(), free the callable once we know it will never be used.
+            if ((string) $this->getCursorId() === '0') {
+                $this->resumeCallable = null;
+            }
+        } catch (RuntimeException $e) {
+            if ($this->isResumableError($e)) {
+                $this->resume();
+            }
+        }
+    }
+
+    /**
+     * @see http://php.net/iterator.valid
+     * @return boolean
+     */
+    public function valid()
+    {
+        return $this->csIt->valid();
+    }
+
+    /**
+     * Extracts the resume token (i.e. "_id" field) from the change document.
+     *
+     * @param array|document $document Change document
+     * @return mixed
+     * @throws InvalidArgumentException
+     * @throws ResumeTokenException if the resume token is not found or invalid
+     */
+    private function extractResumeToken($document)
+    {
+        if ( ! is_array($document) && ! is_object($document)) {
+            throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
+        }
+
+        if ($document instanceof Serializable) {
+            return $this->extractResumeToken($document->bsonSerialize());
+        }
+
+        $resumeToken = is_array($document)
+            ? (isset($document['_id']) ? $document['_id'] : null)
+            : (isset($document->_id) ? $document->_id : null);
+
+        if ( ! isset($resumeToken)) {
+            throw ResumeTokenException::notFound();
+        }
+
+        if ( ! is_array($resumeToken) && ! is_object($resumeToken)) {
+            throw ResumeTokenException::invalidType($resumeToken);
+        }
+
+        return $resumeToken;
+    }
+
+    /**
+     * Determines if an exception is a resumable error.
+     *
+     * @see https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.rst#resumable-error
+     * @param RuntimeException $exception
+     * @return boolean
+     */
+    private function isResumableError(RuntimeException $exception)
+    {
+        if ($exception instanceof ConnectionException) {
+            return true;
+        }
+
+        if ( ! $exception instanceof ServerException) {
+            return false;
+        }
+
+        if (in_array($exception->getCode(), [self::$errorCodeCappedPositionLost, self::$errorCodeCursorKilled, self::$errorCodeInterrupted])) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Creates a new changeStream after a resumable server error.
+     *
+     * @return void
+     */
+    private function resume()
+    {
+        $newChangeStream = call_user_func($this->resumeCallable, $this->resumeToken);
+        $this->csIt = $newChangeStream->csIt;
+        $this->csIt->rewind();
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Client.php b/cache/stores/mongodb/MongoDB/Client.php
new file mode 100755 (executable)
index 0000000..be26ec5
--- /dev/null
@@ -0,0 +1,307 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB;
+
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnexpectedValueException;
+use MongoDB\Exception\UnsupportedException;
+use MongoDB\Model\DatabaseInfoIterator;
+use MongoDB\Operation\DropDatabase;
+use MongoDB\Operation\ListDatabases;
+use MongoDB\Operation\Watch;
+
+class Client
+{
+    private static $defaultTypeMap = [
+        'array' => 'MongoDB\Model\BSONArray',
+        'document' => 'MongoDB\Model\BSONDocument',
+        'root' => 'MongoDB\Model\BSONDocument',
+    ];
+    private static $wireVersionForReadConcern = 4;
+    private static $wireVersionForWritableCommandWriteConcern = 5;
+
+    private $manager;
+    private $readConcern;
+    private $readPreference;
+    private $uri;
+    private $typeMap;
+    private $writeConcern;
+
+    /**
+     * Constructs a new Client instance.
+     *
+     * This is the preferred class for connecting to a MongoDB server or
+     * cluster of servers. It serves as a gateway for accessing individual
+     * databases and collections.
+     *
+     * Supported driver-specific options:
+     *
+     *  * typeMap (array): Default type map for cursors and BSON documents.
+     *
+     * Other options are documented in MongoDB\Driver\Manager::__construct().
+     *
+     * @see http://docs.mongodb.org/manual/reference/connection-string/
+     * @see http://php.net/manual/en/mongodb-driver-manager.construct.php
+     * @see http://php.net/manual/en/mongodb.persistence.php#mongodb.persistence.typemaps
+     * @param string $uri           MongoDB connection string
+     * @param array  $uriOptions    Additional connection string options
+     * @param array  $driverOptions Driver-specific options
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverInvalidArgumentException for parameter/option parsing errors in the driver
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function __construct($uri = 'mongodb://127.0.0.1/', array $uriOptions = [], array $driverOptions = [])
+    {
+        $driverOptions += ['typeMap' => self::$defaultTypeMap];
+
+        if (isset($driverOptions['typeMap']) && ! is_array($driverOptions['typeMap'])) {
+            throw InvalidArgumentException::invalidType('"typeMap" driver option', $driverOptions['typeMap'], 'array');
+        }
+
+        $this->uri = (string) $uri;
+        $this->typeMap = isset($driverOptions['typeMap']) ? $driverOptions['typeMap'] : null;
+
+        unset($driverOptions['typeMap']);
+
+        $this->manager = new Manager($uri, $uriOptions, $driverOptions);
+        $this->readConcern = $this->manager->getReadConcern();
+        $this->readPreference = $this->manager->getReadPreference();
+        $this->writeConcern = $this->manager->getWriteConcern();
+    }
+
+    /**
+     * Return internal properties for debugging purposes.
+     *
+     * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        return [
+            'manager' => $this->manager,
+            'uri' => $this->uri,
+            'typeMap' => $this->typeMap,
+            'writeConcern' => $this->writeConcern,
+        ];
+    }
+
+    /**
+     * Select a database.
+     *
+     * Note: databases whose names contain special characters (e.g. "-") may
+     * be selected with complex syntax (e.g. $client->{"that-database"}) or
+     * {@link selectDatabase()}.
+     *
+     * @see http://php.net/oop5.overloading#object.get
+     * @see http://php.net/types.string#language.types.string.parsing.complex
+     * @param string $databaseName Name of the database to select
+     * @return Database
+     */
+    public function __get($databaseName)
+    {
+        return $this->selectDatabase($databaseName);
+    }
+
+    /**
+     * Return the connection string (i.e. URI).
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->uri;
+    }
+
+    /**
+     * Drop a database.
+     *
+     * @see DropDatabase::__construct() for supported options
+     * @param string $databaseName Database name
+     * @param array  $options      Additional options
+     * @return array|object Command result document
+     * @throws UnsupportedException if options are unsupported on the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function dropDatabase($databaseName, array $options = [])
+    {
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new DropDatabase($databaseName, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Return the Manager.
+     *
+     * @return Manager
+     */
+    public function getManager()
+    {
+        return $this->manager;
+    }
+
+    /**
+     * Return the read concern for this client.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php
+     * @return ReadConcern
+     */
+    public function getReadConcern()
+    {
+        return $this->readConcern;
+    }
+
+    /**
+     * Return the read preference for this client.
+     *
+     * @return ReadPreference
+     */
+    public function getReadPreference()
+    {
+        return $this->readPreference;
+    }
+
+    /**
+     * Return the type map for this client.
+     *
+     * @return array
+     */
+    public function getTypeMap()
+    {
+        return $this->typeMap;
+    }
+
+    /**
+     * Return the write concern for this client.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php
+     * @return WriteConcern
+     */
+    public function getWriteConcern()
+    {
+        return $this->writeConcern;
+    }
+
+    /**
+     * List databases.
+     *
+     * @see ListDatabases::__construct() for supported options
+     * @return DatabaseInfoIterator
+     * @throws UnexpectedValueException if the command response was malformed
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function listDatabases(array $options = [])
+    {
+        $operation = new ListDatabases($options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Select a collection.
+     *
+     * @see Collection::__construct() for supported options
+     * @param string $databaseName   Name of the database containing the collection
+     * @param string $collectionName Name of the collection to select
+     * @param array  $options        Collection constructor options
+     * @return Collection
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function selectCollection($databaseName, $collectionName, array $options = [])
+    {
+        $options += ['typeMap' => $this->typeMap];
+
+        return new Collection($this->manager, $databaseName, $collectionName, $options);
+    }
+
+    /**
+     * Select a database.
+     *
+     * @see Database::__construct() for supported options
+     * @param string $databaseName Name of the database to select
+     * @param array  $options      Database constructor options
+     * @return Database
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function selectDatabase($databaseName, array $options = [])
+    {
+        $options += ['typeMap' => $this->typeMap];
+
+        return new Database($this->manager, $databaseName, $options);
+    }
+
+    /**
+     * Start a new client session.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-manager.startsession.php
+     * @param array  $options      Session options
+     * @return MongoDB\Driver\Session
+     */
+    public function startSession(array $options = [])
+    {
+        return $this->manager->startSession($options);
+    }
+
+    /**
+     * Create a change stream for watching changes to the cluster.
+     *
+     * @see Watch::__construct() for supported options
+     * @param array $pipeline List of pipeline operations
+     * @param array $options  Command options
+     * @return ChangeStream
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function watch(array $pipeline = [], array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $operation = new Watch($this->manager, null, null, $pipeline, $options);
+
+        return $operation->execute($server);
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Collection.php b/cache/stores/mongodb/MongoDB/Collection.php
new file mode 100755 (executable)
index 0000000..769e835
--- /dev/null
@@ -0,0 +1,1092 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB;
+
+use MongoDB\BSON\JavascriptInterface;
+use MongoDB\BSON\Serializable;
+use MongoDB\ChangeStream;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnexpectedValueException;
+use MongoDB\Exception\UnsupportedException;
+use MongoDB\Model\IndexInfo;
+use MongoDB\Model\IndexInfoIterator;
+use MongoDB\Operation\Aggregate;
+use MongoDB\Operation\BulkWrite;
+use MongoDB\Operation\CreateIndexes;
+use MongoDB\Operation\Count;
+use MongoDB\Operation\CountDocuments;
+use MongoDB\Operation\DeleteMany;
+use MongoDB\Operation\DeleteOne;
+use MongoDB\Operation\Distinct;
+use MongoDB\Operation\DropCollection;
+use MongoDB\Operation\DropIndexes;
+use MongoDB\Operation\EstimatedDocumentCount;
+use MongoDB\Operation\Explain;
+use MongoDB\Operation\Explainable;
+use MongoDB\Operation\Find;
+use MongoDB\Operation\FindOne;
+use MongoDB\Operation\FindOneAndDelete;
+use MongoDB\Operation\FindOneAndReplace;
+use MongoDB\Operation\FindOneAndUpdate;
+use MongoDB\Operation\InsertMany;
+use MongoDB\Operation\InsertOne;
+use MongoDB\Operation\ListIndexes;
+use MongoDB\Operation\MapReduce;
+use MongoDB\Operation\ReplaceOne;
+use MongoDB\Operation\UpdateMany;
+use MongoDB\Operation\UpdateOne;
+use MongoDB\Operation\Watch;
+use Traversable;
+
+class Collection
+{
+    private static $defaultTypeMap = [
+        'array' => 'MongoDB\Model\BSONArray',
+        'document' => 'MongoDB\Model\BSONDocument',
+        'root' => 'MongoDB\Model\BSONDocument',
+    ];
+    private static $wireVersionForFindAndModifyWriteConcern = 4;
+    private static $wireVersionForReadConcern = 4;
+    private static $wireVersionForWritableCommandWriteConcern = 5;
+
+    private $collectionName;
+    private $databaseName;
+    private $manager;
+    private $readConcern;
+    private $readPreference;
+    private $typeMap;
+    private $writeConcern;
+
+    /**
+     * Constructs new Collection instance.
+     *
+     * This class provides methods for collection-specific operations, such as
+     * CRUD (i.e. create, read, update, and delete) and index management.
+     *
+     * Supported options:
+     *
+     *  * readConcern (MongoDB\Driver\ReadConcern): The default read concern to
+     *    use for collection operations. Defaults to the Manager's read concern.
+     *
+     *  * readPreference (MongoDB\Driver\ReadPreference): The default read
+     *    preference to use for collection operations. Defaults to the Manager's
+     *    read preference.
+     *
+     *  * typeMap (array): Default type map for cursors and BSON documents.
+     *
+     *  * writeConcern (MongoDB\Driver\WriteConcern): The default write concern
+     *    to use for collection operations. Defaults to the Manager's write
+     *    concern.
+     *
+     * @param Manager $manager        Manager instance from the driver
+     * @param string  $databaseName   Database name
+     * @param string  $collectionName Collection name
+     * @param array   $options        Collection options
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function __construct(Manager $manager, $databaseName, $collectionName, array $options = [])
+    {
+        if (strlen($databaseName) < 1) {
+            throw new InvalidArgumentException('$databaseName is invalid: ' . $databaseName);
+        }
+
+        if (strlen($collectionName) < 1) {
+            throw new InvalidArgumentException('$collectionName is invalid: ' . $collectionName);
+        }
+
+        if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
+            throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+        }
+
+        if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+            throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+        }
+
+        if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+            throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+        }
+
+        if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+            throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+        }
+
+        $this->manager = $manager;
+        $this->databaseName = (string) $databaseName;
+        $this->collectionName = (string) $collectionName;
+        $this->readConcern = isset($options['readConcern']) ? $options['readConcern'] : $this->manager->getReadConcern();
+        $this->readPreference = isset($options['readPreference']) ? $options['readPreference'] : $this->manager->getReadPreference();
+        $this->typeMap = isset($options['typeMap']) ? $options['typeMap'] : self::$defaultTypeMap;
+        $this->writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : $this->manager->getWriteConcern();
+    }
+
+    /**
+     * Return internal properties for debugging purposes.
+     *
+     * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        return [
+            'collectionName' => $this->collectionName,
+            'databaseName' => $this->databaseName,
+            'manager' => $this->manager,
+            'readConcern' => $this->readConcern,
+            'readPreference' => $this->readPreference,
+            'typeMap' => $this->typeMap,
+            'writeConcern' => $this->writeConcern,
+        ];
+    }
+
+    /**
+     * Return the collection namespace (e.g. "db.collection").
+     *
+     * @see https://docs.mongodb.org/manual/faq/developers/#faq-dev-namespace
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->databaseName . '.' . $this->collectionName;
+    }
+
+    /**
+     * Executes an aggregation framework pipeline on the collection.
+     *
+     * Note: this method's return value depends on the MongoDB server version
+     * and the "useCursor" option. If "useCursor" is true, a Cursor will be
+     * returned; otherwise, an ArrayIterator is returned, which wraps the
+     * "result" array from the command response document.
+     *
+     * @see Aggregate::__construct() for supported options
+     * @param array $pipeline List of pipeline operations
+     * @param array $options  Command options
+     * @return Traversable
+     * @throws UnexpectedValueException if the command response was malformed
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function aggregate(array $pipeline, array $options = [])
+    {
+        $hasOutStage = \MongoDB\is_last_pipeline_operator_out($pipeline);
+
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        if ($hasOutStage) {
+            $options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        /* A "majority" read concern is not compatible with the $out stage, so
+         * avoid providing the Collection's read concern if it would conflict.
+         */
+        if ( ! isset($options['readConcern']) &&
+             ! ($hasOutStage && $this->readConcern->getLevel() === ReadConcern::MAJORITY) &&
+            \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        if ($hasOutStage && ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new Aggregate($this->databaseName, $this->collectionName, $pipeline, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Executes multiple write operations.
+     *
+     * @see BulkWrite::__construct() for supported options
+     * @param array[] $operations List of write operations
+     * @param array   $options    Command options
+     * @return BulkWriteResult
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function bulkWrite(array $operations, array $options = [])
+    {
+        if ( ! isset($options['writeConcern'])) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new BulkWrite($this->databaseName, $this->collectionName, $operations, $options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Gets the number of documents matching the filter.
+     *
+     * @see Count::__construct() for supported options
+     * @param array|object $filter  Query by which to filter documents
+     * @param array        $options Command options
+     * @return integer
+     * @throws UnexpectedValueException if the command response was malformed
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     *
+     * @deprecated 1.4
+     */
+    public function count($filter = [], array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        $operation = new Count($this->databaseName, $this->collectionName, $filter, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Gets the number of documents matching the filter.
+     *
+     * @see CountDocuments::__construct() for supported options
+     * @param array|object $filter  Query by which to filter documents
+     * @param array        $options Command options
+     * @return integer
+     * @throws UnexpectedValueException if the command response was malformed
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function countDocuments($filter = [], array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        $operation = new CountDocuments($this->databaseName, $this->collectionName, $filter, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Create a single index for the collection.
+     *
+     * @see Collection::createIndexes()
+     * @see CreateIndexes::__construct() for supported command options
+     * @param array|object $key     Document containing fields mapped to values,
+     *                              which denote order or an index type
+     * @param array        $options Index and command options
+     * @return string The name of the created index
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function createIndex($key, array $options = [])
+    {
+        $commandOptionKeys = ['maxTimeMS' => 1, 'session' => 1, 'writeConcern' => 1];
+        $indexOptions = array_diff_key($options, $commandOptionKeys);
+        $commandOptions = array_intersect_key($options, $commandOptionKeys);
+
+        return current($this->createIndexes([['key' => $key] + $indexOptions], $commandOptions));
+    }
+
+    /**
+     * Create one or more indexes for the collection.
+     *
+     * Each element in the $indexes array must have a "key" document, which
+     * contains fields mapped to an order or type. Other options may follow.
+     * For example:
+     *
+     *     $indexes = [
+     *         // Create a unique index on the "username" field
+     *         [ 'key' => [ 'username' => 1 ], 'unique' => true ],
+     *         // Create a 2dsphere index on the "loc" field with a custom name
+     *         [ 'key' => [ 'loc' => '2dsphere' ], 'name' => 'geo' ],
+     *     ];
+     *
+     * If the "name" option is unspecified, a name will be generated from the
+     * "key" document.
+     *
+     * @see http://docs.mongodb.org/manual/reference/command/createIndexes/
+     * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/
+     * @see CreateIndexes::__construct() for supported command options
+     * @param array[] $indexes List of index specifications
+     * @param array   $options Command options
+     * @return string[] The names of the created indexes
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function createIndexes(array $indexes, array $options = [])
+    {
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new CreateIndexes($this->databaseName, $this->collectionName, $indexes, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Deletes all documents matching the filter.
+     *
+     * @see DeleteMany::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/delete/
+     * @param array|object $filter  Query by which to delete documents
+     * @param array        $options Command options
+     * @return DeleteResult
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function deleteMany($filter, array $options = [])
+    {
+        if ( ! isset($options['writeConcern'])) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new DeleteMany($this->databaseName, $this->collectionName, $filter, $options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Deletes at most one document matching the filter.
+     *
+     * @see DeleteOne::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/delete/
+     * @param array|object $filter  Query by which to delete documents
+     * @param array        $options Command options
+     * @return DeleteResult
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function deleteOne($filter, array $options = [])
+    {
+        if ( ! isset($options['writeConcern'])) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new DeleteOne($this->databaseName, $this->collectionName, $filter, $options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Finds the distinct values for a specified field across the collection.
+     *
+     * @see Distinct::__construct() for supported options
+     * @param string $fieldName Field for which to return distinct values
+     * @param array|object $filter  Query by which to filter documents
+     * @param array        $options Command options
+     * @return mixed[]
+     * @throws UnexpectedValueException if the command response was malformed
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function distinct($fieldName, $filter = [], array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        $operation = new Distinct($this->databaseName, $this->collectionName, $fieldName, $filter, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Drop this collection.
+     *
+     * @see DropCollection::__construct() for supported options
+     * @param array $options Additional options
+     * @return array|object Command result document
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function drop(array $options = [])
+    {
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new DropCollection($this->databaseName, $this->collectionName, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Drop a single index in the collection.
+     *
+     * @see DropIndexes::__construct() for supported options
+     * @param string|IndexInfo $indexName Index name or model object
+     * @param array  $options   Additional options
+     * @return array|object Command result document
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function dropIndex($indexName, array $options = [])
+    {
+        $indexName = (string) $indexName;
+
+        if ($indexName === '*') {
+            throw new InvalidArgumentException('dropIndexes() must be used to drop multiple indexes');
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new DropIndexes($this->databaseName, $this->collectionName, $indexName, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Drop all indexes in the collection.
+     *
+     * @see DropIndexes::__construct() for supported options
+     * @param array $options Additional options
+     * @return array|object Command result document
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function dropIndexes(array $options = [])
+    {
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new DropIndexes($this->databaseName, $this->collectionName, '*', $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Gets an estimated number of documents in the collection using the collection metadata.
+     *
+     * @see EstimatedDocumentCount::__construct() for supported options
+     * @param array $options Command options
+     * @return integer
+     * @throws UnexpectedValueException if the command response was malformed
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function EstimatedDocumentCount(array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        $operation = new EstimatedDocumentCount($this->databaseName, $this->collectionName, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Explains explainable commands.
+     *
+     * @see Explain::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/explain/
+     * @param Explainable $explainable  Command on which to run explain
+     * @param array       $options      Additional options
+     * @return array|object
+     * @throws UnsupportedException if explainable or options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function explain(Explainable $explainable, array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        $operation = new Explain($this->databaseName, $explainable, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Finds documents matching the query.
+     *
+     * @see Find::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/core/read-operations-introduction/
+     * @param array|object $filter  Query by which to filter documents
+     * @param array        $options Additional options
+     * @return Cursor
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function find($filter = [], array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $operation = new Find($this->databaseName, $this->collectionName, $filter, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Finds a single document matching the query.
+     *
+     * @see FindOne::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/core/read-operations-introduction/
+     * @param array|object $filter  Query by which to filter documents
+     * @param array        $options Additional options
+     * @return array|object|null
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function findOne($filter = [], array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $operation = new FindOne($this->databaseName, $this->collectionName, $filter, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Finds a single document and deletes it, returning the original.
+     *
+     * The document to return may be null if no document matched the filter.
+     *
+     * @see FindOneAndDelete::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
+     * @param array|object $filter  Query by which to filter documents
+     * @param array        $options Command options
+     * @return array|object|null
+     * @throws UnexpectedValueException if the command response was malformed
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function findOneAndDelete($filter, array $options = [])
+    {
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $operation = new FindOneAndDelete($this->databaseName, $this->collectionName, $filter, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Finds a single document and replaces it, returning either the original or
+     * the replaced document.
+     *
+     * The document to return may be null if no document matched the filter. By
+     * default, the original document is returned. Specify
+     * FindOneAndReplace::RETURN_DOCUMENT_AFTER for the "returnDocument" option
+     * to return the updated document.
+     *
+     * @see FindOneAndReplace::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
+     * @param array|object $filter      Query by which to filter documents
+     * @param array|object $replacement Replacement document
+     * @param array        $options     Command options
+     * @return array|object|null
+     * @throws UnexpectedValueException if the command response was malformed
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function findOneAndReplace($filter, $replacement, array $options = [])
+    {
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $operation = new FindOneAndReplace($this->databaseName, $this->collectionName, $filter, $replacement, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Finds a single document and updates it, returning either the original or
+     * the updated document.
+     *
+     * The document to return may be null if no document matched the filter. By
+     * default, the original document is returned. Specify
+     * FindOneAndUpdate::RETURN_DOCUMENT_AFTER for the "returnDocument" option
+     * to return the updated document.
+     *
+     * @see FindOneAndReplace::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
+     * @param array|object $filter  Query by which to filter documents
+     * @param array|object $update  Update to apply to the matched document
+     * @param array        $options Command options
+     * @return array|object|null
+     * @throws UnexpectedValueException if the command response was malformed
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function findOneAndUpdate($filter, $update, array $options = [])
+    {
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $operation = new FindOneAndUpdate($this->databaseName, $this->collectionName, $filter, $update, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Return the collection name.
+     *
+     * @return string
+     */
+    public function getCollectionName()
+    {
+        return $this->collectionName;
+    }
+
+    /**
+     * Return the database name.
+     *
+     * @return string
+     */
+    public function getDatabaseName()
+    {
+        return $this->databaseName;
+    }
+
+    /**
+     * Return the Manager.
+     *
+     * @return Manager
+     */
+    public function getManager()
+    {
+        return $this->manager;
+    }
+
+    /**
+     * Return the collection namespace.
+     *
+     * @see https://docs.mongodb.org/manual/reference/glossary/#term-namespace
+     * @return string
+     */
+    public function getNamespace()
+    {
+        return $this->databaseName . '.' . $this->collectionName;
+    }
+
+    /**
+     * Return the read concern for this collection.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php
+     * @return ReadConcern
+     */
+    public function getReadConcern()
+    {
+        return $this->readConcern;
+    }
+
+    /**
+     * Return the read preference for this collection.
+     *
+     * @return ReadPreference
+     */
+    public function getReadPreference()
+    {
+        return $this->readPreference;
+    }
+
+    /**
+     * Return the type map for this collection.
+     *
+     * @return array
+     */
+    public function getTypeMap()
+    {
+        return $this->typeMap;
+    }
+
+    /**
+     * Return the write concern for this collection.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php
+     * @return WriteConcern
+     */
+    public function getWriteConcern()
+    {
+        return $this->writeConcern;
+    }
+
+    /**
+     * Inserts multiple documents.
+     *
+     * @see InsertMany::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/insert/
+     * @param array[]|object[] $documents The documents to insert
+     * @param array            $options   Command options
+     * @return InsertManyResult
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function insertMany(array $documents, array $options = [])
+    {
+        if ( ! isset($options['writeConcern'])) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new InsertMany($this->databaseName, $this->collectionName, $documents, $options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Inserts one document.
+     *
+     * @see InsertOne::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/insert/
+     * @param array|object $document The document to insert
+     * @param array        $options  Command options
+     * @return InsertOneResult
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function insertOne($document, array $options = [])
+    {
+        if ( ! isset($options['writeConcern'])) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new InsertOne($this->databaseName, $this->collectionName, $document, $options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Returns information for all indexes for the collection.
+     *
+     * @see ListIndexes::__construct() for supported options
+     * @return IndexInfoIterator
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function listIndexes(array $options = [])
+    {
+        $operation = new ListIndexes($this->databaseName, $this->collectionName, $options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Executes a map-reduce aggregation on the collection.
+     *
+     * @see MapReduce::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/mapReduce/
+     * @param JavascriptInterface $map            Map function
+     * @param JavascriptInterface $reduce         Reduce function
+     * @param string|array|object $out            Output specification
+     * @param array               $options        Command options
+     * @return MapReduceResult
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     * @throws UnexpectedValueException if the command response was malformed
+     */
+    public function mapReduce(JavascriptInterface $map, JavascriptInterface $reduce, $out, array $options = [])
+    {
+        $hasOutputCollection = ! \MongoDB\is_mapreduce_output_inline($out);
+
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        // Check if the out option is inline because we will want to coerce a primary read preference if not
+        if ($hasOutputCollection) {
+            $options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        /* A "majority" read concern is not compatible with inline output, so
+         * avoid providing the Collection's read concern if it would conflict.
+         */
+        if ( ! isset($options['readConcern']) && ! ($hasOutputCollection && $this->readConcern->getLevel() === ReadConcern::MAJORITY) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new MapReduce($this->databaseName, $this->collectionName, $map, $reduce, $out, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Replaces at most one document matching the filter.
+     *
+     * @see ReplaceOne::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/update/
+     * @param array|object $filter      Query by which to filter documents
+     * @param array|object $replacement Replacement document
+     * @param array        $options     Command options
+     * @return UpdateResult
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function replaceOne($filter, $replacement, array $options = [])
+    {
+        if ( ! isset($options['writeConcern'])) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new ReplaceOne($this->databaseName, $this->collectionName, $filter, $replacement, $options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Updates all documents matching the filter.
+     *
+     * @see UpdateMany::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/update/
+     * @param array|object $filter  Query by which to filter documents
+     * @param array|object $update  Update to apply to the matched documents
+     * @param array        $options Command options
+     * @return UpdateResult
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function updateMany($filter, $update, array $options = [])
+    {
+        if ( ! isset($options['writeConcern'])) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new UpdateMany($this->databaseName, $this->collectionName, $filter, $update, $options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Updates at most one document matching the filter.
+     *
+     * @see UpdateOne::__construct() for supported options
+     * @see http://docs.mongodb.org/manual/reference/command/update/
+     * @param array|object $filter  Query by which to filter documents
+     * @param array|object $update  Update to apply to the matched document
+     * @param array        $options Command options
+     * @return UpdateResult
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function updateOne($filter, $update, array $options = [])
+    {
+        if ( ! isset($options['writeConcern'])) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new UpdateOne($this->databaseName, $this->collectionName, $filter, $update, $options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Create a change stream for watching changes to the collection.
+     *
+     * @see Watch::__construct() for supported options
+     * @param array $pipeline List of pipeline operations
+     * @param array $options  Command options
+     * @return ChangeStream
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function watch(array $pipeline = [], array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        /* Although change streams require a newer version of the server than
+         * read concerns, perform the usual wire version check before inheriting
+         * the collection's read concern. In the event that the server is too
+         * old, this makes it more likely that users will encounter an error
+         * related to change streams being unsupported instead of an
+         * UnsupportedException regarding use of the "readConcern" option from
+         * the Aggregate operation class. */
+        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $operation = new Watch($this->manager, $this->databaseName, $this->collectionName, $pipeline, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Get a clone of this collection with different options.
+     *
+     * @see Collection::__construct() for supported options
+     * @param array $options Collection constructor options
+     * @return Collection
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function withOptions(array $options = [])
+    {
+        $options += [
+            'readConcern' => $this->readConcern,
+            'readPreference' => $this->readPreference,
+            'typeMap' => $this->typeMap,
+            'writeConcern' => $this->writeConcern,
+        ];
+
+        return new Collection($this->manager, $this->databaseName, $this->collectionName, $options);
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Database.php b/cache/stores/mongodb/MongoDB/Database.php
new file mode 100755 (executable)
index 0000000..3b8b0e7
--- /dev/null
@@ -0,0 +1,463 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB;
+
+use MongoDB\Collection;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+use MongoDB\GridFS\Bucket;
+use MongoDB\Model\CollectionInfoIterator;
+use MongoDB\Operation\CreateCollection;
+use MongoDB\Operation\DatabaseCommand;
+use MongoDB\Operation\DropCollection;
+use MongoDB\Operation\DropDatabase;
+use MongoDB\Operation\ListCollections;
+use MongoDB\Operation\ModifyCollection;
+use MongoDB\Operation\Watch;
+
+class Database
+{
+    private static $defaultTypeMap = [
+        'array' => 'MongoDB\Model\BSONArray',
+        'document' => 'MongoDB\Model\BSONDocument',
+        'root' => 'MongoDB\Model\BSONDocument',
+    ];
+    private static $wireVersionForReadConcern = 4;
+    private static $wireVersionForWritableCommandWriteConcern = 5;
+
+    private $databaseName;
+    private $manager;
+    private $readConcern;
+    private $readPreference;
+    private $typeMap;
+    private $writeConcern;
+
+    /**
+     * Constructs new Database instance.
+     *
+     * This class provides methods for database-specific operations and serves
+     * as a gateway for accessing collections.
+     *
+     * Supported options:
+     *
+     *  * readConcern (MongoDB\Driver\ReadConcern): The default read concern to
+     *    use for database operations and selected collections. Defaults to the
+     *    Manager's read concern.
+     *
+     *  * readPreference (MongoDB\Driver\ReadPreference): The default read
+     *    preference to use for database operations and selected collections.
+     *    Defaults to the Manager's read preference.
+     *
+     *  * typeMap (array): Default type map for cursors and BSON documents.
+     *
+     *  * writeConcern (MongoDB\Driver\WriteConcern): The default write concern
+     *    to use for database operations and selected collections. Defaults to
+     *    the Manager's write concern.
+     *
+     * @param Manager $manager      Manager instance from the driver
+     * @param string  $databaseName Database name
+     * @param array   $options      Database options
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function __construct(Manager $manager, $databaseName, array $options = [])
+    {
+        if (strlen($databaseName) < 1) {
+            throw new InvalidArgumentException('$databaseName is invalid: ' . $databaseName);
+        }
+
+        if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
+            throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+        }
+
+        if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+            throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+        }
+
+        if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+            throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+        }
+
+        if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+            throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+        }
+
+        $this->manager = $manager;
+        $this->databaseName = (string) $databaseName;
+        $this->readConcern = isset($options['readConcern']) ? $options['readConcern'] : $this->manager->getReadConcern();
+        $this->readPreference = isset($options['readPreference']) ? $options['readPreference'] : $this->manager->getReadPreference();
+        $this->typeMap = isset($options['typeMap']) ? $options['typeMap'] : self::$defaultTypeMap;
+        $this->writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : $this->manager->getWriteConcern();
+    }
+
+    /**
+     * Return internal properties for debugging purposes.
+     *
+     * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        return [
+            'databaseName' => $this->databaseName,
+            'manager' => $this->manager,
+            'readConcern' => $this->readConcern,
+            'readPreference' => $this->readPreference,
+            'typeMap' => $this->typeMap,
+            'writeConcern' => $this->writeConcern,
+        ];
+    }
+
+    /**
+     * Select a collection within this database.
+     *
+     * Note: collections whose names contain special characters (e.g. ".") may
+     * be selected with complex syntax (e.g. $database->{"system.profile"}) or
+     * {@link selectCollection()}.
+     *
+     * @see http://php.net/oop5.overloading#object.get
+     * @see http://php.net/types.string#language.types.string.parsing.complex
+     * @param string $collectionName Name of the collection to select
+     * @return Collection
+     */
+    public function __get($collectionName)
+    {
+        return $this->selectCollection($collectionName);
+    }
+
+    /**
+     * Return the database name.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->databaseName;
+    }
+
+    /**
+     * Execute a command on this database.
+     *
+     * @see DatabaseCommand::__construct() for supported options
+     * @param array|object $command Command document
+     * @param array        $options Options for command execution
+     * @return Cursor
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function command($command, array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $operation = new DatabaseCommand($this->databaseName, $command, $options);
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Create a new collection explicitly.
+     *
+     * @see CreateCollection::__construct() for supported options
+     * @param string $collectionName
+     * @param array  $options
+     * @return array|object Command result document
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function createCollection($collectionName, array $options = [])
+    {
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new CreateCollection($this->databaseName, $collectionName, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Drop this database.
+     *
+     * @see DropDatabase::__construct() for supported options
+     * @param array $options Additional options
+     * @return array|object Command result document
+     * @throws UnsupportedException if options are unsupported on the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function drop(array $options = [])
+    {
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new DropDatabase($this->databaseName, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Drop a collection within this database.
+     *
+     * @see DropCollection::__construct() for supported options
+     * @param string $collectionName Collection name
+     * @param array  $options        Additional options
+     * @return array|object Command result document
+     * @throws UnsupportedException if options are unsupported on the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function dropCollection($collectionName, array $options = [])
+    {
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new DropCollection($this->databaseName, $collectionName, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Returns the database name.
+     *
+     * @return string
+     */
+    public function getDatabaseName()
+    {
+        return $this->databaseName;
+    }
+
+    /**
+     * Return the Manager.
+     *
+     * @return Manager
+     */
+    public function getManager()
+    {
+        return $this->manager;
+    }
+
+    /**
+     * Return the read concern for this database.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php
+     * @return ReadConcern
+     */
+    public function getReadConcern()
+    {
+        return $this->readConcern;
+    }
+
+    /**
+     * Return the read preference for this database.
+     *
+     * @return ReadPreference
+     */
+    public function getReadPreference()
+    {
+        return $this->readPreference;
+    }
+
+    /**
+     * Return the type map for this database.
+     *
+     * @return array
+     */
+    public function getTypeMap()
+    {
+        return $this->typeMap;
+    }
+
+    /**
+     * Return the write concern for this database.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php
+     * @return WriteConcern
+     */
+    public function getWriteConcern()
+    {
+        return $this->writeConcern;
+    }
+
+    /**
+     * Returns information for all collections in this database.
+     *
+     * @see ListCollections::__construct() for supported options
+     * @param array $options
+     * @return CollectionInfoIterator
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function listCollections(array $options = [])
+    {
+        $operation = new ListCollections($this->databaseName, $options);
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Modifies a collection or view.
+     *
+     * @see ModifyCollection::__construct() for supported options
+     * @param string $collectionName    Collection or view to modify
+     * @param array  $collectionOptions Collection or view options to assign
+     * @param array  $options           Command options
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function modifyCollection($collectionName, array $collectionOptions, array $options = [])
+    {
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+
+        if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+            $options['writeConcern'] = $this->writeConcern;
+        }
+
+        $operation = new ModifyCollection($this->databaseName, $collectionName, $collectionOptions, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Select a collection within this database.
+     *
+     * @see Collection::__construct() for supported options
+     * @param string $collectionName Name of the collection to select
+     * @param array  $options        Collection constructor options
+     * @return Collection
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function selectCollection($collectionName, array $options = [])
+    {
+        $options += [
+            'readConcern' => $this->readConcern,
+            'readPreference' => $this->readPreference,
+            'typeMap' => $this->typeMap,
+            'writeConcern' => $this->writeConcern,
+        ];
+
+        return new Collection($this->manager, $this->databaseName, $collectionName, $options);
+    }
+
+    /**
+     * Select a GridFS bucket within this database.
+     *
+     * @see Bucket::__construct() for supported options
+     * @param array $options Bucket constructor options
+     * @return Bucket
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function selectGridFSBucket(array $options = [])
+    {
+        $options += [
+            'readConcern' => $this->readConcern,
+            'readPreference' => $this->readPreference,
+            'typeMap' => $this->typeMap,
+            'writeConcern' => $this->writeConcern,
+        ];
+
+        return new Bucket($this->manager, $this->databaseName, $options);
+    }
+
+    /**
+     * Create a change stream for watching changes to the database.
+     *
+     * @see Watch::__construct() for supported options
+     * @param array $pipeline List of pipeline operations
+     * @param array $options  Command options
+     * @return ChangeStream
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function watch(array $pipeline = [], array $options = [])
+    {
+        if ( ! isset($options['readPreference'])) {
+            $options['readPreference'] = $this->readPreference;
+        }
+
+        $server = $this->manager->selectServer($options['readPreference']);
+
+        if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            $options['readConcern'] = $this->readConcern;
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = $this->typeMap;
+        }
+
+        $operation = new Watch($this->manager, $this->databaseName, null, $pipeline, $options);
+
+        return $operation->execute($server);
+    }
+
+    /**
+     * Get a clone of this database with different options.
+     *
+     * @see Database::__construct() for supported options
+     * @param array $options Database constructor options
+     * @return Database
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function withOptions(array $options = [])
+    {
+        $options += [
+            'readConcern' => $this->readConcern,
+            'readPreference' => $this->readPreference,
+            'typeMap' => $this->typeMap,
+            'writeConcern' => $this->writeConcern,
+        ];
+
+        return new Database($this->manager, $this->databaseName, $options);
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/DeleteResult.php b/cache/stores/mongodb/MongoDB/DeleteResult.php
new file mode 100755 (executable)
index 0000000..a60137b
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB;
+
+use MongoDB\Driver\WriteResult;
+use MongoDB\Exception\BadMethodCallException;
+
+/**
+ * Result class for a delete operation.
+ */
+class DeleteResult
+{
+    private $writeResult;
+    private $isAcknowledged;
+
+    /**
+     * Constructor.
+     *
+     * @param WriteResult $writeResult
+     */
+    public function __construct(WriteResult $writeResult)
+    {
+        $this->writeResult = $writeResult;
+        $this->isAcknowledged = $writeResult->isAcknowledged();
+    }
+
+    /**
+     * Return the number of documents that were deleted.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see DeleteResult::isAcknowledged()
+     * @return integer
+     * @throws BadMethodCallException is the write result is unacknowledged
+     */
+    public function getDeletedCount()
+    {
+        if ($this->isAcknowledged) {
+            return $this->writeResult->getDeletedCount();
+        }
+
+        throw BadMethodCallException::unacknowledgedWriteResultAccess(__METHOD__);
+    }
+
+    /**
+     * Return whether this delete was acknowledged by the server.
+     *
+     * If the delete was not acknowledged, other fields from the WriteResult
+     * (e.g. deletedCount) will be undefined.
+     *
+     * @return boolean
+     */
+    public function isAcknowledged()
+    {
+        return $this->isAcknowledged;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Exception/BadMethodCallException.php b/cache/stores/mongodb/MongoDB/Exception/BadMethodCallException.php
new file mode 100755 (executable)
index 0000000..f1fe47a
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\Exception;
+
+class BadMethodCallException extends \BadMethodCallException implements Exception
+{
+    /**
+     * Thrown when a mutable method is invoked on an immutable object.
+     *
+     * @param string $class Class name
+     * @return self
+     */
+    public static function classIsImmutable($class)
+    {
+        return new static(sprintf('%s is immutable', $class));
+    }
+
+    /**
+     * Thrown when accessing a result field on an unacknowledged write result.
+     *
+     * @param string $method Method name
+     * @return self
+     */
+    public static function unacknowledgedWriteResultAccess($method)
+    {
+        return new static(sprintf('%s should not be called for an unacknowledged write result', $method));
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Exception/Exception.php b/cache/stores/mongodb/MongoDB/Exception/Exception.php
new file mode 100755 (executable)
index 0000000..703a44c
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\Exception;
+
+interface Exception extends \MongoDB\Driver\Exception\Exception
+{
+}
diff --git a/cache/stores/mongodb/MongoDB/Exception/InvalidArgumentException.php b/cache/stores/mongodb/MongoDB/Exception/InvalidArgumentException.php
new file mode 100755 (executable)
index 0000000..622215a
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\Exception;
+
+class InvalidArgumentException extends \MongoDB\Driver\Exception\InvalidArgumentException implements Exception
+{
+    /**
+     * Thrown when an argument or option has an invalid type.
+     *
+     * @param string $name         Name of the argument or option
+     * @param mixed  $value        Actual value (used to derive the type)
+     * @param string $expectedType Expected type
+     * @return self
+     */
+    public static function invalidType($name, $value, $expectedType)
+    {
+        return new static(sprintf('Expected %s to have type "%s" but found "%s"', $name, $expectedType, is_object($value) ? get_class($value) : gettype($value)));
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Exception/ResumeTokenException.php b/cache/stores/mongodb/MongoDB/Exception/ResumeTokenException.php
new file mode 100755 (executable)
index 0000000..4aeb655
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\Exception;
+
+class ResumeTokenException extends \Exception
+{
+    /**
+     * Thrown when a resume token has an invalid type.
+     *
+     * @param mixed $value Actual value (used to derive the type)
+     * @return self
+     */
+    public static function invalidType($value)
+    {
+        return new static(sprintf('Expected resume token to have type "array or object" but found "%s"', gettype($value)));
+    }
+
+    /**
+     * Thrown when a resume token is not found in a change document.
+     *
+     * @return self
+     */
+    public static function notFound()
+    {
+        return new static('Resume token not found in change document');
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Exception/RuntimeException.php b/cache/stores/mongodb/MongoDB/Exception/RuntimeException.php
new file mode 100755 (executable)
index 0000000..4d8c0b8
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\Exception;
+
+class RuntimeException extends \MongoDB\Driver\Exception\RuntimeException implements Exception
+{
+}
diff --git a/cache/stores/mongodb/MongoDB/Exception/UnexpectedValueException.php b/cache/stores/mongodb/MongoDB/Exception/UnexpectedValueException.php
new file mode 100755 (executable)
index 0000000..a65eaa7
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\Exception;
+
+class UnexpectedValueException extends \MongoDB\Driver\Exception\UnexpectedValueException implements Exception
+{
+}
diff --git a/cache/stores/mongodb/MongoDB/Exception/UnsupportedException.php b/cache/stores/mongodb/MongoDB/Exception/UnsupportedException.php
new file mode 100755 (executable)
index 0000000..91f99b2
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+/*
+ * Copyright 2015-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\Exception;
+
+class UnsupportedException extends RuntimeException
+{
+    /**
+     * Thrown when array filters are not supported by a server.
+     *
+     * @return self
+     */
+    public static function arrayFiltersNotSupported()
+    {
+        return new static('Array filters are not supported by the server executing this operation');
+    }
+
+    /**
+     * Thrown when collations are not supported by a server.
+     *
+     * @return self
+     */
+    public static function collationNotSupported()
+    {
+        return new static('Collations are not supported by the server executing this operation');
+    }
+
+    /**
+     * Thrown when explain is not supported by a server.
+     *
+     * @return self
+     */
+    public static function explainNotSupported()
+    {
+        return new static('Explain is not supported by the server executing this operation');
+    }
+
+    /**
+     * Thrown when a command's readConcern option is not supported by a server.
+     *
+     * @return self
+     */
+    public static function readConcernNotSupported()
+    {
+        return new static('Read concern is not supported by the server executing this command');
+    }
+
+    /**
+     * Thrown when a command's writeConcern option is not supported by a server.
+     *
+     * @return self
+     */
+    public static function writeConcernNotSupported()
+    {
+        return new static('Write concern is not supported by the server executing this command');
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/GridFS/Bucket.php b/cache/stores/mongodb/MongoDB/GridFS/Bucket.php
new file mode 100755 (executable)
index 0000000..243c331
--- /dev/null
@@ -0,0 +1,677 @@
+<?php
+/*
+ * Copyright 2016-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\GridFS;
+
+use MongoDB\Collection;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\GridFS\Exception\CorruptFileException;
+use MongoDB\GridFS\Exception\FileNotFoundException;
+use MongoDB\Operation\Find;
+use stdClass;
+
+/**
+ * Bucket provides a public API for interacting with the GridFS files and chunks
+ * collections.
+ *
+ * @api
+ */
+class Bucket
+{
+    private static $defaultBucketName = 'fs';
+    private static $defaultChunkSizeBytes = 261120;
+    private static $defaultTypeMap = [
+        'array' => 'MongoDB\Model\BSONArray',
+        'document' => 'MongoDB\Model\BSONDocument',
+        'root' => 'MongoDB\Model\BSONDocument',
+    ];
+    private static $streamWrapperProtocol = 'gridfs';
+
+    private $collectionWrapper;
+    private $databaseName;
+    private $manager;
+    private $bucketName;
+    private $disableMD5;
+    private $chunkSizeBytes;
+    private $readConcern;
+    private $readPreference;
+    private $typeMap;
+    private $writeConcern;
+
+    /**
+     * Constructs a GridFS bucket.
+     *
+     * Supported options:
+     *
+     *  * bucketName (string): The bucket name, which will be used as a prefix
+     *    for the files and chunks collections. Defaults to "fs".
+     *
+     *  * chunkSizeBytes (integer): The chunk size in bytes. Defaults to
+     *    261120 (i.e. 255 KiB).
+     *
+     *  * disableMD5 (boolean): When true, no MD5 sum will be generated for
+     *    each stored file. Defaults to "false".
+     *
+     *  * readConcern (MongoDB\Driver\ReadConcern): Read concern.
+     *
+     *  * readPreference (MongoDB\Driver\ReadPreference): Read preference.
+     *
+     *  * typeMap (array): Default type map for cursors and BSON documents.
+     *
+     *  * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+     *
+     * @param Manager $manager      Manager instance from the driver
+     * @param string  $databaseName Database name
+     * @param array   $options      Bucket options
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function __construct(Manager $manager, $databaseName, array $options = [])
+    {
+        $options += [
+            'bucketName' => self::$defaultBucketName,
+            'chunkSizeBytes' => self::$defaultChunkSizeBytes,
+            'disableMD5' => false,
+        ];
+
+        if (isset($options['bucketName']) && ! is_string($options['bucketName'])) {
+            throw InvalidArgumentException::invalidType('"bucketName" option', $options['bucketName'], 'string');
+        }
+
+        if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) {
+            throw InvalidArgumentException::invalidType('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer');
+        }
+
+        if (isset($options['chunkSizeBytes']) && $options['chunkSizeBytes'] < 1) {
+            throw new InvalidArgumentException(sprintf('Expected "chunkSizeBytes" option to be >= 1, %d given', $options['chunkSizeBytes']));
+        }
+
+        if (isset($options['disableMD5']) && ! is_bool($options['disableMD5'])) {
+            throw InvalidArgumentException::invalidType('"disableMD5" option', $options['disableMD5'], 'boolean');
+        }
+
+        if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
+            throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+        }
+
+        if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
+            throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+        }
+
+        if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+            throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+        }
+
+        if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+            throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+        }
+
+        $this->manager = $manager;
+        $this->databaseName = (string) $databaseName;
+        $this->bucketName = $options['bucketName'];
+        $this->chunkSizeBytes = $options['chunkSizeBytes'];
+        $this->disableMD5 = $options['disableMD5'];
+        $this->readConcern = isset($options['readConcern']) ? $options['readConcern'] : $this->manager->getReadConcern();
+        $this->readPreference = isset($options['readPreference']) ? $options['readPreference'] : $this->manager->getReadPreference();
+        $this->typeMap = isset($options['typeMap']) ? $options['typeMap'] : self::$defaultTypeMap;
+        $this->writeConcern = isset($options['writeConcern']) ? $options['writeConcern'] : $this->manager->getWriteConcern();
+
+        $collectionOptions = array_intersect_key($options, ['readConcern' => 1, 'readPreference' => 1, 'typeMap' => 1, 'writeConcern' => 1]);
+
+        $this->collectionWrapper = new CollectionWrapper($manager, $databaseName, $options['bucketName'], $collectionOptions);
+        $this->registerStreamWrapper();
+    }
+
+    /**
+     * Return internal properties for debugging purposes.
+     *
+     * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        return [
+            'bucketName' => $this->bucketName,
+            'databaseName' => $this->databaseName,
+            'manager' => $this->manager,
+            'chunkSizeBytes' => $this->chunkSizeBytes,
+            'readConcern' => $this->readConcern,
+            'readPreference' => $this->readPreference,
+            'typeMap' => $this->typeMap,
+            'writeConcern' => $this->writeConcern,
+        ];
+    }
+
+    /**
+     * Delete a file from the GridFS bucket.
+     *
+     * If the files collection document is not found, this method will still
+     * attempt to delete orphaned chunks.
+     *
+     * @param mixed $id File ID
+     * @throws FileNotFoundException if no file could be selected
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function delete($id)
+    {
+        $file = $this->collectionWrapper->findFileById($id);
+        $this->collectionWrapper->deleteFileAndChunksById($id);
+
+        if ($file === null) {
+            throw FileNotFoundException::byId($id, $this->getFilesNamespace());
+        }
+    }
+
+    /**
+     * Writes the contents of a GridFS file to a writable stream.
+     *
+     * @param mixed    $id          File ID
+     * @param resource $destination Writable Stream
+     * @throws FileNotFoundException if no file could be selected
+     * @throws InvalidArgumentException if $destination is not a stream
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function downloadToStream($id, $destination)
+    {
+        if ( ! is_resource($destination) || get_resource_type($destination) != "stream") {
+            throw InvalidArgumentException::invalidType('$destination', $destination, 'resource');
+        }
+
+        stream_copy_to_stream($this->openDownloadStream($id), $destination);
+    }
+
+    /**
+     * Writes the contents of a GridFS file, which is selected by name and
+     * revision, to a writable stream.
+     *
+     * Supported options:
+     *
+     *  * revision (integer): Which revision (i.e. documents with the same
+     *    filename and different uploadDate) of the file to retrieve. Defaults
+     *    to -1 (i.e. the most recent revision).
+     *
+     * Revision numbers are defined as follows:
+     *
+     *  * 0 = the original stored file
+     *  * 1 = the first revision
+     *  * 2 = the second revision
+     *  * etc…
+     *  * -2 = the second most recent revision
+     *  * -1 = the most recent revision
+     *
+     * @param string   $filename    Filename
+     * @param resource $destination Writable Stream
+     * @param array    $options     Download options
+     * @throws FileNotFoundException if no file could be selected
+     * @throws InvalidArgumentException if $destination is not a stream
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function downloadToStreamByName($filename, $destination, array $options = [])
+    {
+        if ( ! is_resource($destination) || get_resource_type($destination) != "stream") {
+            throw InvalidArgumentException::invalidType('$destination', $destination, 'resource');
+        }
+
+        stream_copy_to_stream($this->openDownloadStreamByName($filename, $options), $destination);
+    }
+
+    /**
+     * Drops the files and chunks collections associated with this GridFS
+     * bucket.
+     *
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function drop()
+    {
+        $this->collectionWrapper->dropCollections();
+    }
+
+    /**
+     * Finds documents from the GridFS bucket's files collection matching the
+     * query.
+     *
+     * @see Find::__construct() for supported options
+     * @param array|object $filter  Query by which to filter documents
+     * @param array        $options Additional options
+     * @return Cursor
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function find($filter = [], array $options = [])
+    {
+        return $this->collectionWrapper->findFiles($filter, $options);
+    }
+
+    /**
+     * Finds a single document from the GridFS bucket's files collection
+     * matching the query.
+     *
+     * @see FindOne::__construct() for supported options
+     * @param array|object $filter  Query by which to filter documents
+     * @param array        $options Additional options
+     * @return array|object|null
+     * @throws UnsupportedException if options are not supported by the selected server
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function findOne($filter = [], array $options = [])
+    {
+        return $this->collectionWrapper->findOneFile($filter, $options);
+    }
+
+    /**
+     * Return the bucket name.
+     *
+     * @return string
+     */
+    public function getBucketName()
+    {
+        return $this->bucketName;
+    }
+
+    /**
+     * Return the chunks collection.
+     *
+     * @return Collection
+     */
+    public function getChunksCollection()
+    {
+        return $this->collectionWrapper->getChunksCollection();
+    }
+
+    /**
+     * Return the chunk size in bytes.
+     *
+     * @return integer
+     */
+    public function getChunkSizeBytes()
+    {
+        return $this->chunkSizeBytes;
+    }
+
+    /**
+     * Return the database name.
+     *
+     * @return string
+     */
+    public function getDatabaseName()
+    {
+        return $this->databaseName;
+    }
+
+    /**
+     * Gets the file document of the GridFS file associated with a stream.
+     *
+     * @param resource $stream GridFS stream
+     * @return array|object
+     * @throws InvalidArgumentException if $stream is not a GridFS stream
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function getFileDocumentForStream($stream)
+    {
+        $file = $this->getRawFileDocumentForStream($stream);
+
+        // Filter the raw document through the specified type map
+        return \MongoDB\apply_type_map_to_document($file, $this->typeMap);
+    }
+
+    /**
+     * Gets the file document's ID of the GridFS file associated with a stream.
+     *
+     * @param resource $stream GridFS stream
+     * @return mixed
+     * @throws CorruptFileException if the file "_id" field does not exist
+     * @throws InvalidArgumentException if $stream is not a GridFS stream
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function getFileIdForStream($stream)
+    {
+        $file = $this->getRawFileDocumentForStream($stream);
+
+        /* Filter the raw document through the specified type map, but override
+         * the root type so we can reliably access the ID.
+         */
+        $typeMap = ['root' => 'stdClass'] + $this->typeMap;
+        $file = \MongoDB\apply_type_map_to_document($file, $typeMap);
+
+        if ( ! isset($file->_id) && ! property_exists($file, '_id')) {
+            throw new CorruptFileException('file._id does not exist');
+        }
+
+        return $file->_id;
+    }
+
+    /**
+     * Return the files collection.
+     *
+     * @return Collection
+     */
+    public function getFilesCollection()
+    {
+        return $this->collectionWrapper->getFilesCollection();
+    }
+
+    /**
+     * Return the read concern for this GridFS bucket.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-readconcern.isdefault.php
+     * @return ReadConcern
+     */
+    public function getReadConcern()
+    {
+        return $this->readConcern;
+    }
+
+    /**
+     * Return the read preference for this GridFS bucket.
+     *
+     * @return ReadPreference
+     */
+    public function getReadPreference()
+    {
+        return $this->readPreference;
+    }
+
+    /**
+     * Return the type map for this GridFS bucket.
+     *
+     * @return array
+     */
+    public function getTypeMap()
+    {
+        return $this->typeMap;
+    }
+
+    /**
+     * Return the write concern for this GridFS bucket.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-writeconcern.isdefault.php
+     * @return WriteConcern
+     */
+    public function getWriteConcern()
+    {
+        return $this->writeConcern;
+    }
+
+    /**
+     * Opens a readable stream for reading a GridFS file.
+     *
+     * @param mixed $id File ID
+     * @return resource
+     * @throws FileNotFoundException if no file could be selected
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function openDownloadStream($id)
+    {
+        $file = $this->collectionWrapper->findFileById($id);
+
+        if ($file === null) {
+            throw FileNotFoundException::byId($id, $this->getFilesNamespace());
+        }
+
+        return $this->openDownloadStreamByFile($file);
+    }
+
+    /**
+     * Opens a readable stream stream to read a GridFS file, which is selected
+     * by name and revision.
+     *
+     * Supported options:
+     *
+     *  * revision (integer): Which revision (i.e. documents with the same
+     *    filename and different uploadDate) of the file to retrieve. Defaults
+     *    to -1 (i.e. the most recent revision).
+     *
+     * Revision numbers are defined as follows:
+     *
+     *  * 0 = the original stored file
+     *  * 1 = the first revision
+     *  * 2 = the second revision
+     *  * etc…
+     *  * -2 = the second most recent revision
+     *  * -1 = the most recent revision
+     *
+     * @param string $filename Filename
+     * @param array  $options  Download options
+     * @return resource
+     * @throws FileNotFoundException if no file could be selected
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function openDownloadStreamByName($filename, array $options = [])
+    {
+        $options += ['revision' => -1];
+
+        $file = $this->collectionWrapper->findFileByFilenameAndRevision($filename, $options['revision']);
+
+        if ($file === null) {
+            throw FileNotFoundException::byFilenameAndRevision($filename, $options['revision'], $this->getFilesNamespace());
+        }
+
+        return $this->openDownloadStreamByFile($file);
+    }
+
+    /**
+     * Opens a writable stream for writing a GridFS file.
+     *
+     * Supported options:
+     *
+     *  * _id (mixed): File document identifier. Defaults to a new ObjectId.
+     *
+     *  * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the
+     *    bucket's chunk size.
+     *
+     *  * disableMD5 (boolean): When true, no MD5 sum will be generated for
+     *    the stored file. Defaults to "false".
+     *
+     *  * metadata (document): User data for the "metadata" field of the files
+     *    collection document.
+     *
+     * @param string $filename Filename
+     * @param array  $options  Upload options
+     * @return resource
+     */
+    public function openUploadStream($filename, array $options = [])
+    {
+        $options += ['chunkSizeBytes' => $this->chunkSizeBytes];
+
+        $path = $this->createPathForUpload();
+        $context = stream_context_create([
+            self::$streamWrapperProtocol => [
+                'collectionWrapper' => $this->collectionWrapper,
+                'filename' => $filename,
+                'options' => $options,
+            ],
+        ]);
+
+        return fopen($path, 'w', false, $context);
+    }
+
+    /**
+     * Renames the GridFS file with the specified ID.
+     *
+     * @param mixed  $id          File ID
+     * @param string $newFilename New filename
+     * @throws FileNotFoundException if no file could be selected
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function rename($id, $newFilename)
+    {
+        $updateResult = $this->collectionWrapper->updateFilenameForId($id, $newFilename);
+
+        if ($updateResult->getModifiedCount() === 1) {
+            return;
+        }
+
+        /* If the update resulted in no modification, it's possible that the
+         * file did not exist, in which case we must raise an error. Checking
+         * the write result's matched count will be most efficient, but fall
+         * back to a findOne operation if necessary (i.e. legacy writes).
+         */
+        $found = $updateResult->getMatchedCount() !== null
+            ? $updateResult->getMatchedCount() === 1
+            : $this->collectionWrapper->findFileById($id) !== null;
+
+        if ( ! $found) {
+            throw FileNotFoundException::byId($id, $this->getFilesNamespace());
+        }
+    }
+
+    /**
+     * Writes the contents of a readable stream to a GridFS file.
+     *
+     * Supported options:
+     *
+     *  * _id (mixed): File document identifier. Defaults to a new ObjectId.
+     *
+     *  * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the
+     *    bucket's chunk size.
+     *
+     *  * disableMD5 (boolean): When true, no MD5 sum will be generated for
+     *    the stored file. Defaults to "false".
+     *
+     *  * metadata (document): User data for the "metadata" field of the files
+     *    collection document.
+     *
+     * @param string   $filename Filename
+     * @param resource $source   Readable stream
+     * @param array    $options  Stream options
+     * @return mixed ID of the newly created GridFS file
+     * @throws InvalidArgumentException if $source is not a GridFS stream
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function uploadFromStream($filename, $source, array $options = [])
+    {
+        if ( ! is_resource($source) || get_resource_type($source) != "stream") {
+            throw InvalidArgumentException::invalidType('$source', $source, 'resource');
+        }
+
+        $destination = $this->openUploadStream($filename, $options);
+        stream_copy_to_stream($source, $destination);
+
+        return $this->getFileIdForStream($destination);
+    }
+
+    /**
+     * Creates a path for an existing GridFS file.
+     *
+     * @param stdClass $file GridFS file document
+     * @return string
+     */
+    private function createPathForFile(stdClass $file)
+    {
+        if ( ! is_object($file->_id) || method_exists($file->_id, '__toString')) {
+            $id = (string) $file->_id;
+        } else {
+            $id = \MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP(['_id' => $file->_id]));
+        }
+
+        return sprintf(
+            '%s://%s/%s.files/%s',
+            self::$streamWrapperProtocol,
+            urlencode($this->databaseName),
+            urlencode($this->bucketName),
+            urlencode($id)
+        );
+    }
+
+    /**
+     * Creates a path for a new GridFS file, which does not yet have an ID.
+     *
+     * @return string
+     */
+    private function createPathForUpload()
+    {
+        return sprintf(
+            '%s://%s/%s.files',
+            self::$streamWrapperProtocol,
+            urlencode($this->databaseName),
+            urlencode($this->bucketName)
+        );
+    }
+
+    /**
+     * Returns the names of the files collection.
+     *
+     * @return string
+     */
+    private function getFilesNamespace()
+    {
+        return sprintf('%s.%s.files', $this->databaseName, $this->bucketName);
+    }
+
+    /**
+     * Gets the file document of the GridFS file associated with a stream.
+     *
+     * This returns the raw document from the StreamWrapper, which does not
+     * respect the Bucket's type map.
+     *
+     * @param resource $stream GridFS stream
+     * @return stdClass
+     * @throws InvalidArgumentException
+     */
+    private function getRawFileDocumentForStream($stream)
+    {
+        if ( ! is_resource($stream) || get_resource_type($stream) != "stream") {
+            throw InvalidArgumentException::invalidType('$stream', $stream, 'resource');
+        }
+
+        $metadata = stream_get_meta_data($stream);
+
+        if ( ! isset ($metadata['wrapper_data']) || ! $metadata['wrapper_data'] instanceof StreamWrapper) {
+            throw InvalidArgumentException::invalidType('$stream wrapper data', isset($metadata['wrapper_data']) ? $metadata['wrapper_data'] : null, 'MongoDB\Driver\GridFS\StreamWrapper');
+        }
+
+        return $metadata['wrapper_data']->getFile();
+    }
+
+    /**
+     * Opens a readable stream for the GridFS file.
+     *
+     * @param stdClass $file GridFS file document
+     * @return resource
+     */
+    private function openDownloadStreamByFile(stdClass $file)
+    {
+        $path = $this->createPathForFile($file);
+        $context = stream_context_create([
+            self::$streamWrapperProtocol => [
+                'collectionWrapper' => $this->collectionWrapper,
+                'file' => $file,
+            ],
+        ]);
+
+        return fopen($path, 'r', false, $context);
+    }
+
+    /**
+     * Registers the GridFS stream wrapper if it is not already registered.
+     */
+    private function registerStreamWrapper()
+    {
+        if (in_array(self::$streamWrapperProtocol, stream_get_wrappers())) {
+            return;
+        }
+
+        StreamWrapper::register(self::$streamWrapperProtocol);
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/GridFS/CollectionWrapper.php b/cache/stores/mongodb/MongoDB/GridFS/CollectionWrapper.php
new file mode 100755 (executable)
index 0000000..754fd4b
--- /dev/null
@@ -0,0 +1,338 @@
+<?php
+/*
+ * Copyright 2016-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\GridFS;
+
+use MongoDB\Collection;
+use MongoDB\UpdateResult;
+use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Manager;
+use MongoDB\Driver\ReadPreference;
+use stdClass;
+
+/**
+ * CollectionWrapper abstracts the GridFS files and chunks collections.
+ *
+ * @internal
+ */
+class CollectionWrapper
+{
+    private $bucketName;
+    private $chunksCollection;
+    private $databaseName;
+    private $checkedIndexes = false;
+    private $filesCollection;
+
+    /**
+     * Constructs a GridFS collection wrapper.
+     *
+     * @see Collection::__construct() for supported options
+     * @param Manager $manager           Manager instance from the driver
+     * @param string  $databaseName      Database name
+     * @param string  $bucketName        Bucket name
+     * @param array   $collectionOptions Collection options
+     * @throws InvalidArgumentException
+     */
+    public function __construct(Manager $manager, $databaseName, $bucketName, array $collectionOptions = [])
+    {
+        $this->databaseName = (string) $databaseName;
+        $this->bucketName = (string) $bucketName;
+
+        $this->filesCollection = new Collection($manager, $databaseName, sprintf('%s.files', $bucketName), $collectionOptions);
+        $this->chunksCollection = new Collection($manager, $databaseName, sprintf('%s.chunks', $bucketName), $collectionOptions);
+    }
+
+    /**
+     * Deletes all GridFS chunks for a given file ID.
+     *
+     * @param mixed $id
+     */
+    public function deleteChunksByFilesId($id)
+    {
+        $this->chunksCollection->deleteMany(['files_id' => $id]);
+    }
+
+    /**
+     * Deletes a GridFS file and related chunks by ID.
+     *
+     * @param mixed $id
+     */
+    public function deleteFileAndChunksById($id)
+    {
+        $this->filesCollection->deleteOne(['_id' => $id]);
+        $this->chunksCollection->deleteMany(['files_id' => $id]);
+    }
+
+    /**
+     * Drops the GridFS files and chunks collections.
+     */
+    public function dropCollections()
+    {
+        $this->filesCollection->drop(['typeMap' => []]);
+        $this->chunksCollection->drop(['typeMap' => []]);
+    }
+
+    /**
+     * Finds GridFS chunk documents for a given file ID and optional offset.
+     *
+     * @param mixed   $id        File ID
+     * @param integer $fromChunk Starting chunk (inclusive)
+     * @return Cursor
+     */
+    public function findChunksByFileId($id, $fromChunk = 0)
+    {
+        return $this->chunksCollection->find(
+            [
+                'files_id' => $id,
+                'n' => ['$gte' => $fromChunk],
+            ],
+            [
+                'sort' => ['n' => 1],
+                'typeMap' => ['root' => 'stdClass'],
+            ]
+        );
+    }
+
+    /**
+     * Finds a GridFS file document for a given filename and revision.
+     *
+     * Revision numbers are defined as follows:
+     *
+     *  * 0 = the original stored file
+     *  * 1 = the first revision
+     *  * 2 = the second revision
+     *  * etc…
+     *  * -2 = the second most recent revision
+     *  * -1 = the most recent revision
+     *
+     * @see Bucket::downloadToStreamByName()
+     * @see Bucket::openDownloadStreamByName()
+     * @param string $filename
+     * @param integer $revision
+     * @return stdClass|null
+     */
+    public function findFileByFilenameAndRevision($filename, $revision)
+    {
+        $filename = (string) $filename;
+        $revision = (integer) $revision;
+
+        if ($revision < 0) {
+            $skip = abs($revision) - 1;
+            $sortOrder = -1;
+        } else {
+            $skip = $revision;
+            $sortOrder = 1;
+        }
+
+        return $this->filesCollection->findOne(
+            ['filename' => $filename],
+            [
+                'skip' => $skip,
+                'sort' => ['uploadDate' => $sortOrder],
+                'typeMap' => ['root' => 'stdClass'],
+            ]
+        );
+    }
+
+    /**
+     * Finds a GridFS file document for a given ID.
+     *
+     * @param mixed $id
+     * @return stdClass|null
+     */
+    public function findFileById($id)
+    {
+        return $this->filesCollection->findOne(
+            ['_id' => $id],
+            ['typeMap' => ['root' => 'stdClass']]
+        );
+    }
+
+    /**
+     * Finds documents from the GridFS bucket's files collection.
+     *
+     * @see Find::__construct() for supported options
+     * @param array|object $filter  Query by which to filter documents
+     * @param array        $options Additional options
+     * @return Cursor
+     */
+    public function findFiles($filter, array $options = [])
+    {
+        return $this->filesCollection->find($filter, $options);
+    }
+
+    /**
+     * Finds a single document from the GridFS bucket's files collection.
+     *
+     * @param array|object $filter  Query by which to filter documents
+     * @param array        $options Additional options
+     * @return array|object|null
+     */
+    public function findOneFile($filter, array $options = [])
+    {
+        return $this->filesCollection->findOne($filter, $options);
+    }
+
+    /**
+     * Return the bucket name.
+     *
+     * @return string
+     */
+    public function getBucketName()
+    {
+        return $this->bucketName;
+    }
+
+    /**
+     * Return the chunks collection.
+     *
+     * @return Collection
+     */
+    public function getChunksCollection()
+    {
+        return $this->chunksCollection;
+    }
+
+    /**
+     * Return the database name.
+     *
+     * @return string
+     */
+    public function getDatabaseName()
+    {
+        return $this->databaseName;
+    }
+
+    /**
+     * Return the files collection.
+     *
+     * @return Collection
+     */
+    public function getFilesCollection()
+    {
+        return $this->filesCollection;
+    }
+
+    /**
+     * Inserts a document into the chunks collection.
+     *
+     * @param array|object $chunk Chunk document
+     */
+    public function insertChunk($chunk)
+    {
+        if ( ! $this->checkedIndexes) {
+            $this->ensureIndexes();
+        }
+
+        $this->chunksCollection->insertOne($chunk);
+    }
+
+    /**
+     * Inserts a document into the files collection.
+     *
+     * The file document should be inserted after all chunks have been inserted.
+     *
+     * @param array|object $file File document
+     */
+    public function insertFile($file)
+    {
+        if ( ! $this->checkedIndexes) {
+            $this->ensureIndexes();
+        }
+
+        $this->filesCollection->insertOne($file);
+    }
+
+    /**
+     * Updates the filename field in the file document for a given ID.
+     *
+     * @param mixed  $id
+     * @param string $filename 
+     * @return UpdateResult
+     */
+    public function updateFilenameForId($id, $filename)
+    {
+        return $this->filesCollection->updateOne(
+            ['_id' => $id],
+            ['$set' => ['filename' => (string) $filename]]
+        );
+    }
+
+    /**
+     * Create an index on the chunks collection if it does not already exist.
+     */
+    private function ensureChunksIndex()
+    {
+        foreach ($this->chunksCollection->listIndexes() as $index) {
+            if ($index->isUnique() && $index->getKey() === ['files_id' => 1, 'n' => 1]) {
+                return;
+            }
+        }
+
+        $this->chunksCollection->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
+    }
+
+    /**
+     * Create an index on the files collection if it does not already exist.
+     */
+    private function ensureFilesIndex()
+    {
+        foreach ($this->filesCollection->listIndexes() as $index) {
+            if ($index->getKey() === ['filename' => 1, 'uploadDate' => 1]) {
+                return;
+            }
+        }
+
+        $this->filesCollection->createIndex(['filename' => 1, 'uploadDate' => 1]);
+    }
+
+    /**
+     * Ensure indexes on the files and chunks collections exist.
+     *
+     * This method is called once before the first write operation on a GridFS
+     * bucket. Indexes are only be created if the files collection is empty.
+     */
+    private function ensureIndexes()
+    {
+        if ($this->checkedIndexes) {
+            return;
+        }
+
+        $this->checkedIndexes = true;
+
+        if ( ! $this->isFilesCollectionEmpty()) {
+            return;
+        }
+
+        $this->ensureFilesIndex();
+        $this->ensureChunksIndex();
+    }
+
+    /**
+     * Returns whether the files collection is empty.
+     *
+     * @return boolean
+     */
+    private function isFilesCollectionEmpty()
+    {
+        return null === $this->filesCollection->findOne([], [
+            'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY),
+            'projection' => ['_id' => 1],
+            'typeMap' => [],
+        ]);
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/GridFS/Exception/CorruptFileException.php b/cache/stores/mongodb/MongoDB/GridFS/Exception/CorruptFileException.php
new file mode 100755 (executable)
index 0000000..787c9b8
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/*
+ * Copyright 2016-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\GridFS\Exception;
+
+use MongoDB\Exception\RuntimeException;
+
+class CorruptFileException extends RuntimeException
+{
+    /**
+     * Thrown when a chunk is not found for an expected index.
+     *
+     * @param integer $expectedIndex Expected index number
+     * @return self
+     */
+    public static function missingChunk($expectedIndex)
+    {
+        return new static(sprintf('Chunk not found for index "%d"', $expectedIndex));
+    }
+
+    /**
+     * Thrown when a chunk has an unexpected index number.
+     *
+     * @param integer $index         Actual index number (i.e. "n" field)
+     * @param integer $expectedIndex Expected index number
+     * @return self
+     */
+    public static function unexpectedIndex($index, $expectedIndex)
+    {
+        return new static(sprintf('Expected chunk to have index "%d" but found "%d"', $expectedIndex, $index));
+    }
+
+    /**
+     * Thrown when a chunk has an unexpected data size.
+     *
+     * @param integer $size         Actual size (i.e. "data" field length)
+     * @param integer $expectedSize Expected size
+     * @return self
+     */
+    public static function unexpectedSize($size, $expectedSize)
+    {
+        return new static(sprintf('Expected chunk to have size "%d" but found "%d"', $expectedSize, $size));
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/GridFS/Exception/FileNotFoundException.php b/cache/stores/mongodb/MongoDB/GridFS/Exception/FileNotFoundException.php
new file mode 100755 (executable)
index 0000000..2d3d037
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+/*
+ * Copyright 2016-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\GridFS\Exception;
+
+use MongoDB\Exception\RuntimeException;
+
+class FileNotFoundException extends RuntimeException
+{
+    /**
+     * Thrown when a file cannot be found by its filename and revision.
+     *
+     * @param string  $filename  Filename
+     * @param integer $revision  Revision
+     * @param string  $namespace Namespace for the files collection
+     * @return self
+     */
+    public static function byFilenameAndRevision($filename, $revision, $namespace)
+    {
+        return new static(sprintf('File with name "%s" and revision "%d" not found in "%s"', $filename, $revision, $namespace));
+    }
+
+    /**
+     * Thrown when a file cannot be found by its ID.
+     *
+     * @param mixed  $id        File ID
+     * @param string $namespace Namespace for the files collection
+     * @return self
+     */
+    public static function byId($id, $namespace)
+    {
+        $json = \MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP(['_id' => $id]));
+
+        return new static(sprintf('File "%s" not found in "%s"', $json, $namespace));
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/GridFS/ReadableStream.php b/cache/stores/mongodb/MongoDB/GridFS/ReadableStream.php
new file mode 100755 (executable)
index 0000000..d49b213
--- /dev/null
@@ -0,0 +1,296 @@
+<?php
+/*
+ * Copyright 2016-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\GridFS;
+
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\GridFS\Exception\CorruptFileException;
+use IteratorIterator;
+use stdClass;
+
+/**
+ * ReadableStream abstracts the process of reading a GridFS file.
+ *
+ * @internal
+ */
+class ReadableStream
+{
+    private $buffer;
+    private $bufferOffset = 0;
+    private $chunkSize;
+    private $chunkOffset = 0;
+    private $chunksIterator;
+    private $collectionWrapper;
+    private $expectedLastChunkSize = 0;
+    private $file;
+    private $length;
+    private $numChunks = 0;
+
+    /**
+     * Constructs a readable GridFS stream.
+     *
+     * @param CollectionWrapper $collectionWrapper GridFS collection wrapper
+     * @param stdClass          $file              GridFS file document
+     * @throws CorruptFileException
+     */
+    public function __construct(CollectionWrapper $collectionWrapper, stdClass $file)
+    {
+        if ( ! isset($file->chunkSize) || ! is_integer($file->chunkSize) || $file->chunkSize < 1) {
+            throw new CorruptFileException('file.chunkSize is not an integer >= 1');
+        }
+
+        if ( ! isset($file->length) || ! is_integer($file->length) || $file->length < 0) {
+            throw new CorruptFileException('file.length is not an integer > 0');
+        }
+
+        if ( ! isset($file->_id) && ! property_exists($file, '_id')) {
+            throw new CorruptFileException('file._id does not exist');
+        }
+
+        $this->file = $file;
+        $this->chunkSize = (integer) $file->chunkSize;
+        $this->length = (integer) $file->length;
+
+        $this->collectionWrapper = $collectionWrapper;
+
+        if ($this->length > 0) {
+            $this->numChunks = (integer) ceil($this->length / $this->chunkSize);
+            $this->expectedLastChunkSize = ($this->length - (($this->numChunks - 1) * $this->chunkSize));
+        }
+    }
+
+    /**
+     * Return internal properties for debugging purposes.
+     *
+     * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        return [
+            'bucketName' => $this->collectionWrapper->getBucketName(),
+            'databaseName' => $this->collectionWrapper->getDatabaseName(),
+            'file' => $this->file,
+        ];
+    }
+
+    public function close()
+    {
+        // Nothing to do
+    }
+
+    /**
+     * Return the stream's file document.
+     *
+     * @return stdClass
+     */
+    public function getFile()
+    {
+        return $this->file;
+    }
+
+    /**
+     * Return the stream's size in bytes.
+     *
+     * @return integer
+     */
+    public function getSize()
+    {
+        return $this->length;
+    }
+
+    /**
+     * Return whether the current read position is at the end of the stream.
+     *
+     * @return boolean
+     */
+    public function isEOF()
+    {
+        if ($this->chunkOffset === $this->numChunks - 1) {
+            return $this->bufferOffset >= $this->expectedLastChunkSize;
+        }
+
+        return $this->chunkOffset >= $this->numChunks;
+    }
+
+    /**
+     * Read bytes from the stream.
+     *
+     * Note: this method may return a string smaller than the requested length
+     * if data is not available to be read.
+     *
+     * @param integer $length Number of bytes to read
+     * @return string
+     * @throws InvalidArgumentException if $length is negative
+     */
+    public function readBytes($length)
+    {
+        if ($length < 0) {
+            throw new InvalidArgumentException(sprintf('$length must be >= 0; given: %d', $length));
+        }
+
+        if ($this->chunksIterator === null) {
+            $this->initChunksIterator();
+        }
+
+        if ($this->buffer === null && ! $this->initBufferFromCurrentChunk()) {
+            return '';
+        }
+
+        $data = '';
+
+        while (strlen($data) < $length) {
+            if ($this->bufferOffset >= strlen($this->buffer) && ! $this->initBufferFromNextChunk()) {
+                break;
+            }
+
+            $initialDataLength = strlen($data);
+            $data .= substr($this->buffer, $this->bufferOffset, $length - $initialDataLength);
+            $this->bufferOffset += strlen($data) - $initialDataLength;
+        }
+
+        return $data;
+    }
+
+    /**
+     * Seeks the chunk and buffer offsets for the next read operation.
+     *
+     * @param integer $offset
+     * @throws InvalidArgumentException if $offset is out of range
+     */
+    public function seek($offset)
+    {
+        if ($offset < 0 || $offset > $this->file->length) {
+            throw new InvalidArgumentException(sprintf('$offset must be >= 0 and <= %d; given: %d', $this->file->length, $offset));
+        }
+
+        /* Compute the offsets for the chunk and buffer (i.e. chunk data) from
+         * which we will expect to read after seeking. If the chunk offset
+         * changed, we'll also need to reset the buffer.
+         */
+        $lastChunkOffset = $this->chunkOffset;
+        $this->chunkOffset = (integer) floor($offset / $this->chunkSize);
+        $this->bufferOffset = $offset % $this->chunkSize;
+
+        if ($lastChunkOffset === $this->chunkOffset) {
+            return;
+        }
+
+        if ($this->chunksIterator === null) {
+            return;
+        }
+
+        // Clear the buffer since the current chunk will be changed
+        $this->buffer = null;
+
+        /* If we are seeking to a previous chunk, we need to reinitialize the
+         * chunk iterator.
+         */
+        if ($lastChunkOffset > $this->chunkOffset) {
+            $this->chunksIterator = null;
+            return;
+        }
+
+        /* If we are seeking to a subsequent chunk, we do not need to
+         * reinitalize the chunk iterator. Instead, we can simply move forward
+         * to $this->chunkOffset.
+         */
+        $numChunks = $this->chunkOffset - $lastChunkOffset;
+        for ($i = 0; $i < $numChunks; $i++) {
+            $this->chunksIterator->next();
+        }
+    }
+
+    /**
+     * Return the current position of the stream.
+     *
+     * This is the offset within the stream where the next byte would be read.
+     *
+     * @return integer
+     */
+    public function tell()
+    {
+        return ($this->chunkOffset * $this->chunkSize) + $this->bufferOffset;
+    }
+
+    /**
+     * Initialize the buffer to the current chunk's data.
+     *
+     * @return boolean Whether there was a current chunk to read
+     * @throws CorruptFileException if an expected chunk could not be read successfully
+     */
+    private function initBufferFromCurrentChunk()
+    {
+        if ($this->chunkOffset === 0 && $this->numChunks === 0) {
+            return false;
+        }
+
+        if ( ! $this->chunksIterator->valid()) {
+            throw CorruptFileException::missingChunk($this->chunkOffset);
+        }
+
+        $currentChunk = $this->chunksIterator->current();
+
+        if ($currentChunk->n !== $this->chunkOffset) {
+            throw CorruptFileException::unexpectedIndex($currentChunk->n, $this->chunkOffset);
+        }
+
+        $this->buffer = $currentChunk->data->getData();
+
+        $actualChunkSize = strlen($this->buffer);
+
+        $expectedChunkSize = ($this->chunkOffset === $this->numChunks - 1)
+            ? $this->expectedLastChunkSize
+            : $this->chunkSize;
+
+        if ($actualChunkSize !== $expectedChunkSize) {
+            throw CorruptFileException::unexpectedSize($actualChunkSize, $expectedChunkSize);
+        }
+
+        return true;
+    }
+
+    /**
+     * Advance to the next chunk and initialize the buffer to its data.
+     *
+     * @return boolean Whether there was a next chunk to read
+     * @throws CorruptFileException if an expected chunk could not be read successfully
+     */
+    private function initBufferFromNextChunk()
+    {
+        if ($this->chunkOffset === $this->numChunks - 1) {
+            return false;
+        }
+
+        $this->bufferOffset = 0;
+        $this->chunkOffset++;
+        $this->chunksIterator->next();
+
+        return $this->initBufferFromCurrentChunk();
+    }
+
+    /**
+     * Initializes the chunk iterator starting from the current offset.
+     */
+    private function initChunksIterator()
+    {
+        $cursor = $this->collectionWrapper->findChunksByFileId($this->file->_id, $this->chunkOffset);
+
+        $this->chunksIterator = new IteratorIterator($cursor);
+        $this->chunksIterator->rewind();
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/GridFS/StreamWrapper.php b/cache/stores/mongodb/MongoDB/GridFS/StreamWrapper.php
new file mode 100755 (executable)
index 0000000..29a2be6
--- /dev/null
@@ -0,0 +1,308 @@
+<?php
+/*
+ * Copyright 2016-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\GridFS;
+
+use MongoDB\BSON\UTCDateTime;
+use Exception;
+use stdClass;
+
+/**
+ * Stream wrapper for reading and writing a GridFS file.
+ *
+ * @internal
+ * @see Bucket::openUploadStream()
+ * @see Bucket::openDownloadStream()
+ */
+class StreamWrapper
+{
+    /**
+     * @var resource|null Stream context (set by PHP)
+     */
+    public $context;
+
+    private $mode;
+    private $protocol;
+    private $stream;
+
+    /**
+     * Return the stream's file document.
+     *
+     * @return stdClass
+     */
+    public function getFile()
+    {
+        return $this->stream->getFile();
+    }
+
+    /**
+     * Register the GridFS stream wrapper.
+     *
+     * @param string $protocol Protocol to use for stream_wrapper_register()
+     */
+    public static function register($protocol = 'gridfs')
+    {
+        if (in_array($protocol, stream_get_wrappers())) {
+            stream_wrapper_unregister($protocol);
+        }
+
+        stream_wrapper_register($protocol, get_called_class(), \STREAM_IS_URL);
+    }
+
+    /**
+     * Closes the stream.
+     *
+     * @see http://php.net/manual/en/streamwrapper.stream-close.php
+     */
+    public function stream_close()
+    {
+        $this->stream->close();
+    }
+
+    /**
+     * Returns whether the file pointer is at the end of the stream.
+     *
+     * @see http://php.net/manual/en/streamwrapper.stream-eof.php
+     * @return boolean
+     */
+    public function stream_eof()
+    {
+        if ( ! $this->stream instanceof ReadableStream) {
+            return false;
+        }
+
+        return $this->stream->isEOF();
+    }
+
+    /**
+     * Opens the stream.
+     *
+     * @see http://php.net/manual/en/streamwrapper.stream-open.php
+     * @param string  $path       Path to the file resource
+     * @param string  $mode       Mode used to open the file (only "r" and "w" are supported)
+     * @param integer $options    Additional flags set by the streams API
+     * @param string  $openedPath Not used
+     */
+    public function stream_open($path, $mode, $options, &$openedPath)
+    {
+        $this->initProtocol($path);
+        $this->mode = $mode;
+
+        if ($mode === 'r') {
+            return $this->initReadableStream();
+        }
+
+        if ($mode === 'w') {
+            return $this->initWritableStream();
+        }
+
+        return false;
+    }
+
+    /**
+     * Read bytes from the stream.
+     *
+     * Note: this method may return a string smaller than the requested length
+     * if data is not available to be read.
+     *
+     * @see http://php.net/manual/en/streamwrapper.stream-read.php
+     * @param integer $length Number of bytes to read
+     * @return string
+     */
+    public function stream_read($length)
+    {
+        if ( ! $this->stream instanceof ReadableStream) {
+            return '';
+        }
+
+        try {
+            return $this->stream->readBytes($length);
+        } catch (Exception $e) {
+            trigger_error(sprintf('%s: %s', get_class($e), $e->getMessage()), \E_USER_WARNING);
+            return false;
+        }
+    }
+
+    /**
+     * Return the current position of the stream.
+     *
+     * @see http://php.net/manual/en/streamwrapper.stream-seek.php
+     * @param integer $offset Stream offset to seek to
+     * @param integer $whence One of SEEK_SET, SEEK_CUR, or SEEK_END
+     * @return boolean True if the position was updated and false otherwise
+     */
+    public function stream_seek($offset, $whence = \SEEK_SET)
+    {
+        $size = $this->stream->getSize();
+
+        if ($whence === \SEEK_CUR) {
+            $offset += $this->stream->tell();
+        }
+
+        if ($whence === \SEEK_END) {
+            $offset += $size;
+        }
+
+        // WritableStreams are always positioned at the end of the stream
+        if ($this->stream instanceof WritableStream) {
+            return $offset === $size;
+        }
+
+        if ($offset < 0 || $offset > $size) {
+            return false;
+        }
+
+        $this->stream->seek($offset);
+
+        return true;
+    }
+
+    /**
+     * Return information about the stream.
+     *
+     * @see http://php.net/manual/en/streamwrapper.stream-stat.php
+     * @return array
+     */
+    public function stream_stat()
+    {
+        $stat = $this->getStatTemplate();
+
+        $stat[2] = $stat['mode'] = $this->stream instanceof ReadableStream
+            ? 0100444  // S_IFREG & S_IRUSR & S_IRGRP & S_IROTH
+            : 0100222; // S_IFREG & S_IWUSR & S_IWGRP & S_IWOTH
+        $stat[7] = $stat['size'] = $this->stream->getSize();
+
+        $file = $this->stream->getFile();
+
+        if (isset($file->uploadDate) && $file->uploadDate instanceof UTCDateTime) {
+            $timestamp = $file->uploadDate->toDateTime()->getTimestamp();
+            $stat[9] = $stat['mtime'] = $timestamp;
+            $stat[10] = $stat['ctime'] = $timestamp;
+        }
+
+        if (isset($file->chunkSize) && is_integer($file->chunkSize)) {
+            $stat[11] = $stat['blksize'] = $file->chunkSize;
+        }
+
+        return $stat;
+    }
+
+    /**
+     * Return the current position of the stream.
+     *
+     * @see http://php.net/manual/en/streamwrapper.stream-tell.php
+     * @return integer The current position of the stream
+     */
+    public function stream_tell()
+    {
+        return $this->stream->tell();
+    }
+
+    /**
+     * Write bytes to the stream.
+     *
+     * @see http://php.net/manual/en/streamwrapper.stream-write.php
+     * @param string $data Data to write
+     * @return integer The number of bytes written
+     */
+    public function stream_write($data)
+    {
+        if ( ! $this->stream instanceof WritableStream) {
+            return 0;
+        }
+
+        try {
+            return $this->stream->writeBytes($data);
+        } catch (Exception $e) {
+            trigger_error(sprintf('%s: %s', get_class($e), $e->getMessage()), \E_USER_WARNING);
+            return false;
+        }
+    }
+
+    /**
+     * Returns a stat template with default values.
+     *
+     * @return array
+     */
+    private function getStatTemplate()
+    {
+        return [
+            0  => 0,  'dev'     => 0,
+            1  => 0,  'ino'     => 0,
+            2  => 0,  'mode'    => 0,
+            3  => 0,  'nlink'   => 0,
+            4  => 0,  'uid'     => 0,
+            5  => 0,  'gid'     => 0,
+            6  => -1, 'rdev'    => -1,
+            7  => 0,  'size'    => 0,
+            8  => 0,  'atime'   => 0,
+            9  => 0,  'mtime'   => 0,
+            10 => 0,  'ctime'   => 0,
+            11 => -1, 'blksize' => -1,
+            12 => -1, 'blocks'  => -1,
+        ];
+    }
+
+    /**
+     * Initialize the protocol from the given path.
+     *
+     * @see StreamWrapper::stream_open()
+     * @param string $path
+     */
+    private function initProtocol($path)
+    {
+        $parts = explode('://', $path, 2);
+        $this->protocol = $parts[0] ?: 'gridfs';
+    }
+
+    /**
+     * Initialize the internal stream for reading.
+     *
+     * @see StreamWrapper::stream_open()
+     * @return boolean
+     */
+    private function initReadableStream()
+    {
+        $context = stream_context_get_options($this->context);
+
+        $this->stream = new ReadableStream(
+            $context[$this->protocol]['collectionWrapper'],
+            $context[$this->protocol]['file']
+        );
+
+        return true;
+    }
+
+    /**
+     * Initialize the internal stream for writing.
+     *
+     * @see StreamWrapper::stream_open()
+     * @return boolean
+     */
+    private function initWritableStream()
+    {
+        $context = stream_context_get_options($this->context);
+
+        $this->stream = new WritableStream(
+            $context[$this->protocol]['collectionWrapper'],
+            $context[$this->protocol]['filename'],
+            $context[$this->protocol]['options']
+        );
+
+        return true;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/GridFS/WritableStream.php b/cache/stores/mongodb/MongoDB/GridFS/WritableStream.php
new file mode 100755 (executable)
index 0000000..1fed3f9
--- /dev/null
@@ -0,0 +1,283 @@
+<?php
+/*
+ * Copyright 2016-2017 MongoDB, 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.
+ */
+
+namespace MongoDB\GridFS;
+
+use MongoDB\BSON\Binary;
+use MongoDB\BSON\ObjectId;
+use MongoDB\BSON\UTCDateTime;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use stdClass;
+
+/**
+ * WritableStream abstracts the process of writing a GridFS file.
+ *
+ * @internal
+ */
+class WritableStream
+{
+    private static $defaultChunkSizeBytes = 261120;
+
+    private $buffer = '';
+    private $chunkOffset = 0;
+    private $chunkSize;
+    private $disableMD5;
+    private $collectionWrapper;
+    private $file;
+    private $hashCtx;
+    private $isClosed = false;
+    private $length = 0;
+
+    /**
+     * Constructs a writable GridFS stream.
+     *
+     * Supported options:
+     *
+     *  * _id (mixed): File document identifier. Defaults to a new ObjectId.
+     *
+     *  * aliases (array of strings): DEPRECATED An array of aliases.
+     *    Applications wishing to store aliases should add an aliases field to
+     *    the metadata document instead.
+     *
+     *  * chunkSizeBytes (integer): The chunk size in bytes. Defaults to
+     *    261120 (i.e. 255 KiB).
+     *
+     *  * disableMD5 (boolean): When true, no MD5 sum will be generated.
+     *    Defaults to "false".
+     *
+     *  * contentType (string): DEPRECATED content type to be stored with the
+     *    file. This information should now be added to the metadata.
+     *
+     *  * metadata (document): User data for the "metadata" field of the files
+     *    collection document.
+     *
+     * @param CollectionWrapper $collectionWrapper GridFS collection wrapper
+     * @param string            $filename          Filename
+     * @param array             $options           Upload options
+     * @throws InvalidArgumentException
+     */
+    public function __construct(CollectionWrapper $collectionWrapper, $filename, array $options = [])
+    {
+        $options += [
+            '_id' => new ObjectId,
+            'chunkSizeBytes' => self::$defaultChunkSizeBytes,
+            'disableMD5' => false,
+        ];
+
+        if (isset($options['aliases']) && ! \MongoDB\is_string_array($options['aliases'])) {
+            throw InvalidArgumentException::invalidType('"aliases" option', $options['aliases'], 'array of strings');
+        }
+
+        if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) {
+            throw InvalidArgumentException::invalidType('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer');
+        }
+
+        if (isset($options['chunkSizeBytes']) && $options['chunkSizeBytes'] < 1) {
+            throw new InvalidArgumentException(sprintf('Expected "chunkSizeBytes" option to be >= 1, %d given', $options['chunkSizeBytes']));
+        }
+
+        if (isset($options['disableMD5']) && ! is_bool($options['disableMD5'])) {
+            throw InvalidArgumentException::invalidType('"disableMD5" option', $options['disableMD5'], 'boolean');
+        }
+
+        if (isset($options['contentType']) && ! is_string($opti