Merge branch 'MDL-60340_master' of git://github.com/dmonllao/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 19 Mar 2019 23:36:51 +0000 (00:36 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 19 Mar 2019 23:36:51 +0000 (00:36 +0100)
146 files changed:
.eslintignore
.stylelintignore
admin/tests/behat/behat_admin.php
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/policy/classes/output/page_agreedocs.php
admin/tool/policy/tests/behat/acceptances.feature
admin/tool/usertours/classes/manager.php
admin/tool/usertours/db/upgrade.php
admin/tool/usertours/version.php
badges/index.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lang/en/deprecated.txt
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
grade/import/csv/classes/load_data.php
grade/import/csv/tests/load_data_test.php
group/tests/behat/behat_groups.php
lang/en/grades.php
lib/amd/build/templates.min.js
lib/amd/src/templates.js
lib/behat/behat_base.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/output/external.php
lib/classes/output/mustache_template_source_loader.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/upgrade.txt
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/workshop/allocation/manual/tests/behat/behat_workshopallocation_manual.php
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/style/moodle.css
theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache
theme/bootstrapbase/templates/block_timeline/nav-view-selector.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

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.*
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.
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}}
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 64955f2..f29f481 100644 (file)
@@ -796,8 +796,7 @@ class manager {
         // the format filename => version. The version value needs to
         // be increased if the tour has been updated.
         $shippedtours = [
-            '36_dashboard.json' => 3,
-            '36_messaging.json' => 3,
+            '36_dashboard.json' => 3
         ];
 
         // These are tours that we used to ship but don't ship any longer.
@@ -807,6 +806,12 @@ class manager {
             'boost_course_view.json' => 1,
         ];
 
+        if ($CFG->messaging) {
+            $shippedtours['36_messaging.json'] = 3;
+        } else {
+            $unshippedtours['36_messaging.json'] = 3;
+        }
+
         $existingtourrecords = $DB->get_recordset('tool_usertours_tours');
 
         // Get all of the existing shipped tours and check if they need to be
index a8582de..47e4994 100644 (file)
@@ -54,5 +54,12 @@ function xmldb_tool_usertours_upgrade($oldversion) {
     // Automatically generated Moodle v3.6.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2019030600) {
+        // Update the tours shipped with Moodle.
+        manager::update_shipped_tours();
+
+        upgrade_plugin_savepoint(true, 2019030600, 'tool', 'usertours');
+    }
+
     return true;
 }
index 2f00da1..c678cb9 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;            // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2019030600;            // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2018112800;            // Requires this Moodle version.
 $plugin->component = 'tool_usertours';      // Full name of the plugin (used for diagnostics).
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 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($options['contentType'])) {
+            throw InvalidArgumentException::invalidType('"contentType" option', $options['contentType'], 'string');
+        }
+
+        if (isset($options['metadata']) && ! is_array($options['metadata']) && ! is_object($options['metadata'])) {
+            throw InvalidArgumentException::invalidType('"metadata" option', $options['metadata'], 'array or object');
+        }
+
+        $this->chunkSize = $options['chunkSizeBytes'];
+        $this->collectionWrapper = $collectionWrapper;
+        $this->disableMD5 = $options['disableMD5'];
+
+        if ( ! $this->disableMD5) {
+            $this->hashCtx = hash_init('md5');
+        }
+
+        $this->file = [
+            '_id' => $options['_id'],
+            'chunkSize' => $this->chunkSize,
+            'filename' => (string) $filename,
+        ] + array_intersect_key($options, ['aliases' => 1, 'contentType' => 1, 'metadata' => 1]);
+    }
+
+    /**
+     * 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,
+        ];
+    }
+
+    /**
+     * Closes an active stream and flushes all buffered data to GridFS.
+     */
+    public function close()
+    {
+        if ($this->isClosed) {
+            // TODO: Should this be an error condition? e.g. BadMethodCallException
+            return;
+        }
+
+        if (strlen($this->buffer) > 0) {
+            $this->insertChunkFromBuffer();
+        }
+
+        $this->fileCollectionInsert();
+        $this->isClosed = true;
+    }
+
+    /**
+     * Return the stream's file document.
+     *
+     * @return stdClass
+     */
+    public function getFile()
+    {
+        return (object) $this->file;
+    }
+
+    /**
+     * Return the stream's size in bytes.
+     *
+     * Note: this value will increase as more data is written to the stream.
+     *
+     * @return integer
+     */
+    public function getSize()
+    {
+        return $this->length + strlen($this->buffer);
+    }
+
+    /**
+     * Return the current position of the stream.
+     *
+     * This is the offset within the stream where the next byte would be
+     * written. Since seeking is not supported and writes are appended, this is
+     * always the end of the stream.
+     *
+     * @see WritableStream::getSize()
+     * @return integer
+     */
+    public function tell()
+    {
+        return $this->getSize();
+    }
+
+    /**
+     * Inserts binary data into GridFS via chunks.
+     *
+     * Data will be buffered internally until chunkSizeBytes are accumulated, at
+     * which point a chunk document will be inserted and the buffer reset.
+     *
+     * @param string $data Binary data to write
+     * @return integer
+     */
+    public function writeBytes($data)
+    {
+        if ($this->isClosed) {
+            // TODO: Should this be an error condition? e.g. BadMethodCallException
+            return;
+        }
+
+        $bytesRead = 0;
+
+        while ($bytesRead != strlen($data)) {
+            $initialBufferLength = strlen($this->buffer);
+            $this->buffer .= substr($data, $bytesRead, $this->chunkSize - $initialBufferLength);
+            $bytesRead += strlen($this->buffer) - $initialBufferLength;
+
+            if (strlen($this->buffer) == $this->chunkSize) {
+                $this->insertChunkFromBuffer();
+            }
+        }
+
+        return $bytesRead;
+    }
+
+    private function abort()
+    {
+        try {
+            $this->collectionWrapper->deleteChunksByFilesId($this->file['_id']);
+        } catch (DriverRuntimeException $e) {
+            // We are already handling an error if abort() is called, so suppress this
+        }
+
+        $this->isClosed = true;
+    }
+
+    private function fileCollectionInsert()
+    {
+        $this->file['length'] = $this->length;
+        $this->file['uploadDate'] = new UTCDateTime;
+
+        if ( ! $this->disableMD5) {
+            $this->file['md5'] = hash_final($this->hashCtx);
+        }
+
+        try {
+            $this->collectionWrapper->insertFile($this->file);
+        } catch (DriverRuntimeException $e) {
+            $this->abort();
+
+            throw $e;
+        }
+
+        return $this->file['_id'];
+    }
+
+    private function insertChunkFromBuffer()
+    {
+        if (strlen($this->buffer) == 0) {
+            return;
+        }
+
+        $data = $this->buffer;
+        $this->buffer = '';
+
+        $chunk = [
+            'files_id' => $this->file['_id'],
+            'n' => $this->chunkOffset,
+            'data' => new Binary($data, Binary::TYPE_GENERIC),
+        ];
+
+        if ( ! $this->disableMD5) {
+            hash_update($this->hashCtx, $data);
+        }
+
+        try {
+            $this->collectionWrapper->insertChunk($chunk);
+        } catch (DriverRuntimeException $e) {
+            $this->abort();
+
+            throw $e;
+        }
+
+        $this->length += strlen($data);
+        $this->chunkOffset++;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/InsertManyResult.php b/cache/stores/mongodb/MongoDB/InsertManyResult.php
new file mode 100755 (executable)
index 0000000..a9ecb69
--- /dev/null
@@ -0,0 +1,91 @@
+<?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 multi-document insert operation.
+ */
+class InsertManyResult
+{
+    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 inserted.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see InsertManyResult::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 whether this insert result was acknowledged by the server.
+     *
+     * If the insert was not acknowledged, other fields from the WriteResult
+     * (e.g. insertedCount) will be undefined.
+     *
+     * @return boolean
+     */
+    public function isAcknowledged()
+    {
+        return $this->writeResult->isAcknowledged();
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/InsertOneResult.php b/cache/stores/mongodb/MongoDB/InsertOneResult.php
new file mode 100755 (executable)
index 0000000..5a580bd
--- /dev/null
@@ -0,0 +1,93 @@
+<?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 single-document insert operation.
+ */
+class InsertOneResult
+{
+    private $writeResult;
+    private $insertedId;
+    private $isAcknowledged;
+
+    /**
+     * Constructor.
+     *
+     * @param WriteResult $writeResult
+     * @param mixed       $insertedId
+     */
+    public function __construct(WriteResult $writeResult, $insertedId)
+    {
+        $this->writeResult = $writeResult;
+        $this->insertedId = $insertedId;
+        $this->isAcknowledged = $writeResult->isAcknowledged();
+    }
+
+    /**
+     * Return the number of documents that were inserted.
+     *
+     * This method should only be called if the write was acknowledged.
+     *
+     * @see InsertOneResult::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 the inserted document's ID.
+     *
+     * If the document had an ID prior to inserting (i.e. the driver did not
+     * need to generate an ID), this will contain its "_id". Any
+     * driver-generated ID will be a MongoDB\BSON\ObjectId instance.
+     *
+     * @return mixed
+     */
+    public function getInsertedId()
+    {
+        return $this->insertedId;
+    }
+
+    /**
+     * Return whether this insert was acknowledged by the server.
+     *
+     * If the insert was not acknowledged, other fields from the WriteResult
+     * (e.g. insertedCount) will be undefined.
+     *
+     * If the insert was not acknowledged, other fields from the WriteResult
+     * (e.g. insertedCount) will be undefined and their getter methods should
+     * not be invoked.
+     *
+     * @return boolean
+     */
+    public function isAcknowledged()
+    {
+        return $this->writeResult->isAcknowledged();
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/MapReduceResult.php b/cache/stores/mongodb/MongoDB/MapReduceResult.php
new file mode 100755 (executable)
index 0000000..37ba38f
--- /dev/null
@@ -0,0 +1,100 @@
+<?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 IteratorAggregate;
+use stdClass;
+use Traversable;
+
+/**
+ * Result class for mapReduce command results.
+ *
+ * This class allows for iteration of mapReduce results irrespective of the
+ * output method (e.g. inline, collection) via the IteratorAggregate interface.
+ * It also provides access to command statistics.
+ *
+ * @api
+ * @see \MongoDB\Collection::mapReduce()
+ * @see https://docs.mongodb.com/manual/reference/command/mapReduce/
+ */
+class MapReduceResult implements IteratorAggregate
+{
+    private $getIterator;
+    private $executionTimeMS;
+    private $counts;
+    private $timing;
+
+    /**
+     * Constructor.
+     *
+     * @internal
+     * @param callable $getIterator Callback that returns a Traversable for mapReduce results
+     * @param stdClass $result      Result document from the mapReduce command
+     */
+    public function __construct(callable $getIterator, stdClass $result)
+    {
+        $this->getIterator = $getIterator;
+        $this->executionTimeMS = (integer) $result->timeMillis;
+        $this->counts = (array) $result->counts;
+        $this->timing = isset($result->timing) ? (array) $result->timing : [];
+    }
+
+    /**
+     * Returns various count statistics from the mapReduce command.
+     *
+     * @return array
+     */
+    public function getCounts()
+    {
+        return $this->counts;
+    }
+
+    /**
+     * Return the command execution time in milliseconds.
+     *
+     * @return integer
+     */
+    public function getExecutionTimeMS()
+    {
+        return $this->executionTimeMS;
+    }
+
+    /**
+     * Return the mapReduce results as a Traversable.
+     *
+     * @see http://php.net/iteratoraggregate.getiterator
+     * @return Traversable
+     */
+    public function getIterator()
+    {
+        return call_user_func($this->getIterator);
+    }
+
+    /**
+     * Returns various timing statistics from the mapReduce command.
+     *
+     * Note: timing statistics are only available if the mapReduce command's
+     * "verbose" option was true; otherwise, an empty array will be returned.
+     *
+     * @return array
+     */
+    public function getTiming()
+    {
+        return $this->timing;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/BSONArray.php b/cache/stores/mongodb/MongoDB/Model/BSONArray.php
new file mode 100755 (executable)
index 0000000..61dc9b4
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+/*
+ * Copyright 2016-present 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\Model;
+
+use MongoDB\BSON\Serializable;
+use MongoDB\BSON\Unserializable;
+use ArrayObject;
+use JsonSerializable;
+
+/**
+ * Model class for a BSON array.
+ *
+ * The internal data will be filtered through array_values() during BSON
+ * serialization to ensure that it becomes a BSON array.
+ *
+ * @api
+ */
+class BSONArray extends ArrayObject implements JsonSerializable, Serializable, Unserializable
+{
+    /**
+     * Clone this BSONArray.
+     */
+    public function __clone()
+    {
+        foreach ($this as $key => $value) {
+            $this[$key] = \MongoDB\recursive_copy($value);
+        }
+    }
+
+    /**
+     * Factory method for var_export().
+     *
+     * @see http://php.net/oop5.magic#object.set-state
+     * @see http://php.net/var-export
+     * @param array $properties
+     * @return self
+     */
+    public static function __set_state(array $properties)
+    {
+        $array = new static;
+        $array->exchangeArray($properties);
+
+        return $array;
+    }
+
+    /**
+     * Serialize the array to BSON.
+     *
+     * The array data will be numerically reindexed to ensure that it is stored
+     * as a BSON array.
+     *
+     * @see http://php.net/mongodb-bson-serializable.bsonserialize
+     * @return array
+     */
+    public function bsonSerialize()
+    {
+        return array_values($this->getArrayCopy());
+    }
+
+    /**
+     * Unserialize the document to BSON.
+     *
+     * @see http://php.net/mongodb-bson-unserializable.bsonunserialize
+     * @param array $data Array data
+     */
+    public function bsonUnserialize(array $data)
+    {
+        self::__construct($data);
+    }
+
+    /**
+     * Serialize the array to JSON.
+     *
+     * The array data will be numerically reindexed to ensure that it is stored
+     * as a JSON array.
+     *
+     * @see http://php.net/jsonserializable.jsonserialize
+     * @return array
+     */
+    public function jsonSerialize()
+    {
+        return array_values($this->getArrayCopy());
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/BSONDocument.php b/cache/stores/mongodb/MongoDB/Model/BSONDocument.php
new file mode 100755 (executable)
index 0000000..90ea887
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+/*
+ * Copyright 2016-present 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\Model;
+
+use MongoDB\BSON\Serializable;
+use MongoDB\BSON\Unserializable;
+use ArrayObject;
+use JsonSerializable;
+
+/**
+ * Model class for a BSON document.
+ *
+ * The internal data will be cast to an object during BSON serialization to
+ * ensure that it becomes a BSON document.
+ *
+ * @api
+ */
+class BSONDocument extends ArrayObject implements JsonSerializable, Serializable, Unserializable
+{
+    /**
+     * Deep clone this BSONDocument.
+     */
+    public function __clone()
+    {
+        foreach ($this as $key => $value) {
+            $this[$key] = \MongoDB\recursive_copy($value);
+        }
+    }
+
+    /**
+     * Constructor.
+     *
+     * This overrides the parent constructor to allow property access of entries
+     * by default.
+     *
+     * @see http://php.net/arrayobject.construct
+     */
+    public function __construct($input = [], $flags = ArrayObject::ARRAY_AS_PROPS, $iterator_class = 'ArrayIterator')
+    {
+        parent::__construct($input, $flags, $iterator_class);
+    }
+
+    /**
+     * Factory method for var_export().
+     *
+     * @see http://php.net/oop5.magic#object.set-state
+     * @see http://php.net/var-export
+     * @param array $properties
+     * @return self
+     */
+    public static function __set_state(array $properties)
+    {
+        $document = new static;
+        $document->exchangeArray($properties);
+
+        return $document;
+    }
+
+    /**
+     * Serialize the document to BSON.
+     *
+     * @see http://php.net/mongodb-bson-serializable.bsonserialize
+     * @return object
+     */
+    public function bsonSerialize()
+    {
+        return (object) $this->getArrayCopy();
+    }
+
+    /**
+     * Unserialize the document to BSON.
+     *
+     * @see http://php.net/mongodb-bson-unserializable.bsonunserialize
+     * @param array $data Array data
+     */
+    public function bsonUnserialize(array $data)
+    {
+        parent::__construct($data, ArrayObject::ARRAY_AS_PROPS);
+    }
+
+    /**
+     * Serialize the array to JSON.
+     *
+     * @see http://php.net/jsonserializable.jsonserialize
+     * @return object
+     */
+    public function jsonSerialize()
+    {
+        return (object) $this->getArrayCopy();
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/BSONIterator.php b/cache/stores/mongodb/MongoDB/Model/BSONIterator.php
new file mode 100755 (executable)
index 0000000..f082794
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+/*
+ * Copyright 2018 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\Model;
+
+use MongoDB\Exception\UnexpectedValueException;
+use MongoDB\Model\BSONDocument;
+use Iterator;
+
+/**
+ * Iterator for BSON documents.
+ */
+class BSONIterator implements Iterator
+{
+    private static $bsonSize = 4;
+
+    private $buffer;
+    private $bufferLength;
+    private $current;
+    private $key = 0;
+    private $position = 0;
+    private $options;
+
+    /**
+     * Constructs a BSON Iterator.
+     *
+     * Supported options:
+     *
+     *  * typeMap (array): Type map for BSON deserialization.
+     *
+     * @internal
+     * @see http://php.net/manual/en/function.mongodb.bson-tophp.php
+     * @param string $data    Concatenated, valid, BSON-encoded documents
+     * @param array  $options Iterator options
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function __construct($data, array $options = [])
+    {
+        if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+            throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+        }
+
+        if ( ! isset($options['typeMap'])) {
+            $options['typeMap'] = [];
+        }
+
+        $this->buffer = $data;
+        $this->bufferLength = strlen($data);
+        $this->options = $options;
+    }
+
+    /**
+     * @see http://php.net/iterator.current
+     * @return mixed
+     */
+    public function current()
+    {
+        return $this->current;
+    }
+
+    /**
+     * @see http://php.net/iterator.key
+     * @return mixed
+     */
+    public function key()
+    {
+        return $this->key;
+    }
+
+    /**
+     * @see http://php.net/iterator.next
+     * @return void
+     */
+    public function next()
+    {
+        $this->key++;
+        $this->current = null;
+        $this->advance();
+    }
+
+    /**
+     * @see http://php.net/iterator.rewind
+     * @return void
+     */
+    public function rewind()
+    {
+        $this->key = 0;
+        $this->position = 0;
+        $this->current = null;
+        $this->advance();
+    }
+
+    /**
+     * @see http://php.net/iterator.valid
+     * @return boolean
+     */
+    public function valid()
+    {
+        return $this->current !== null;
+    }
+
+    private function advance()
+    {
+        if ($this->position === $this->bufferLength) {
+            return;
+        }
+
+        if (($this->bufferLength - $this->position) < self::$bsonSize) {
+            throw new UnexpectedValueException(sprintf('Expected at least %d bytes; %d remaining', self::$bsonSize, $this->bufferLength - $this->position));
+        }
+
+        list(,$documentLength) = unpack('V', substr($this->buffer, $this->position, self::$bsonSize));
+
+        if (($this->bufferLength - $this->position) < $documentLength) {
+            throw new UnexpectedValueException(sprintf('Expected %d bytes; %d remaining', $documentLength, $this->bufferLength - $this->position));
+        }
+
+        $this->current = \MongoDB\BSON\toPHP(substr($this->buffer, $this->position, $documentLength), $this->options['typeMap']);
+        $this->position += $documentLength;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/CachingIterator.php b/cache/stores/mongodb/MongoDB/Model/CachingIterator.php
new file mode 100755 (executable)
index 0000000..adad231
--- /dev/null
@@ -0,0 +1,165 @@
+<?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\Model;
+
+use Countable;
+use Generator;
+use Iterator;
+use Traversable;
+
+/**
+ * Iterator for wrapping a Traversable and caching its results.
+ *
+ * By caching results, this iterators allows a Traversable to be counted and
+ * rewound multiple times, even if the wrapped object does not natively support
+ * those operations (e.g. MongoDB\Driver\Cursor).
+ *
+ * @internal
+ */
+class CachingIterator implements Countable, Iterator
+{
+    private $items = [];
+    private $iterator;
+    private $iteratorAdvanced = false;
+    private $iteratorExhausted = false;
+
+    /**
+     * Constructor.
+     *
+     * Initialize the iterator and stores the first item in the cache. This
+     * effectively rewinds the Traversable and the wrapping Generator, which
+     * will execute up to its first yield statement. Additionally, this mimics
+     * behavior of the SPL iterators and allows users to omit an explicit call
+     * to rewind() before using the other methods.
+     *
+     * @param Traversable $traversable
+     */
+    public function __construct(Traversable $traversable)
+    {
+        $this->iterator = $this->wrapTraversable($traversable);
+        $this->storeCurrentItem();
+    }
+
+    /**
+     * @see http://php.net/countable.count
+     * @return integer
+     */
+    public function count()
+    {
+        $this->exhaustIterator();
+
+        return count($this->items);
+    }
+
+    /**
+     * @see http://php.net/iterator.current
+     * @return mixed
+     */
+    public function current()
+    {
+        return current($this->items);
+    }
+
+    /**
+     * @see http://php.net/iterator.key
+     * @return mixed
+     */
+    public function key()
+    {
+        return key($this->items);
+    }
+
+    /**
+     * @see http://php.net/iterator.next
+     * @return void
+     */
+    public function next()
+    {
+        if ( ! $this->iteratorExhausted) {
+            $this->iterator->next();
+            $this->storeCurrentItem();
+        }
+
+        next($this->items);
+    }
+
+    /**
+     * @see http://php.net/iterator.rewind
+     * @return void
+     */
+    public function rewind()
+    {
+        /* If the iterator has advanced, exhaust it now so that future iteration
+         * can rely on the cache.
+         */
+        if ($this->iteratorAdvanced) {
+            $this->exhaustIterator();
+        }
+
+        reset($this->items);
+    }
+
+    /**
+     * @see http://php.net/iterator.valid
+     * @return boolean
+     */
+    public function valid()
+    {
+        return $this->key() !== null;
+    }
+
+    /**
+     * Ensures that the inner iterator is fully consumed and cached.
+     */
+    private function exhaustIterator()
+    {
+        while ( ! $this->iteratorExhausted) {
+            $this->next();
+        }
+    }
+
+    /**
+     * Stores the current item in the cache.
+     */
+    private function storeCurrentItem()
+    {
+        $key = $this->iterator->key();
+
+        if ($key === null) {
+            return;
+        }
+
+        $this->items[$key] = $this->iterator->current();
+    }
+
+    /**
+     * Wraps the Traversable with a Generator.
+     *
+     * @param Traversable $traversable
+     * @return Generator
+     */
+    private function wrapTraversable(Traversable $traversable)
+    {
+        foreach ($traversable as $key => $value) {
+            yield $key => $value;
+            $this->iteratorAdvanced = true;
+        }
+
+        $this->iteratorExhausted = true;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/CollectionInfo.php b/cache/stores/mongodb/MongoDB/Model/CollectionInfo.php
new file mode 100755 (executable)
index 0000000..39b20c4
--- /dev/null
@@ -0,0 +1,154 @@
+<?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\Model;
+
+use MongoDB\Exception\BadMethodCallException;
+use ArrayAccess;
+
+/**
+ * Collection information model class.
+ *
+ * This class models the collection information returned by the listCollections
+ * command or, for legacy servers, queries on the "system.namespaces"
+ * collection. It provides methods to access options for the collection.
+ *
+ * @api
+ * @see \MongoDB\Database::listCollections()
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-collections.rst
+ */
+class CollectionInfo implements ArrayAccess
+{
+    private $info;
+
+    /**
+     * Constructor.
+     *
+     * @param array $info Collection info
+     */
+    public function __construct(array $info)
+    {
+        $this->info = $info;
+    }
+
+    /**
+     * Return the collection info as an array.
+     *
+     * @see http://php.net/oop5.magic#language.oop5.magic.debuginfo
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        return $this->info;
+    }
+
+    /**
+     * Return the maximum number of documents to keep in the capped collection.
+     *
+     * @return integer|null
+     */
+    public function getCappedMax()
+    {
+        return isset($this->info['options']['max']) ? (integer) $this->info['options']['max'] : null;
+    }
+
+    /**
+     * Return the maximum size (in bytes) of the capped collection.
+     *
+     * @return integer|null
+     */
+    public function getCappedSize()
+    {
+        return isset($this->info['options']['size']) ? (integer) $this->info['options']['size'] : null;
+    }
+
+    /**
+     * Return the collection name.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return (string) $this->info['name'];
+    }
+
+    /**
+     * Return the collection options.
+     *
+     * @return array
+     */
+    public function getOptions()
+    {
+        return isset($this->info['options']) ? (array) $this->info['options'] : [];
+    }
+
+    /**
+     * Return whether the collection is a capped collection.
+     *
+     * @return boolean
+     */
+    public function isCapped()
+    {
+        return ! empty($this->info['options']['capped']);
+    }
+
+    /**
+     * Check whether a field exists in the collection information.
+     *
+     * @see http://php.net/arrayaccess.offsetexists
+     * @param mixed $key
+     * @return boolean
+     */
+    public function offsetExists($key)
+    {
+        return array_key_exists($key, $this->info);
+    }
+
+    /**
+     * Return the field's value from the collection information.
+     *
+     * @see http://php.net/arrayaccess.offsetget
+     * @param mixed $key
+     * @return mixed
+     */
+    public function offsetGet($key)
+    {
+        return $this->info[$key];
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayaccess.offsetset
+     * @throws BadMethodCallException
+     */
+    public function offsetSet($key, $value)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayaccess.offsetunset
+     * @throws BadMethodCallException
+     */
+    public function offsetUnset($key)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/CollectionInfoCommandIterator.php b/cache/stores/mongodb/MongoDB/Model/CollectionInfoCommandIterator.php
new file mode 100755 (executable)
index 0000000..c4b7b8d
--- /dev/null
@@ -0,0 +1,46 @@
+<?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\Model;
+
+use IteratorIterator;
+
+/**
+ * CollectionInfoIterator for listCollections command results.
+ *
+ * This iterator may be used to wrap a Cursor returned by the listCollections
+ * command.
+ *
+ * @internal
+ * @see \MongoDB\Database::listCollections()
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-collections.rst
+ * @see http://docs.mongodb.org/manual/reference/command/listCollections/
+ */
+class CollectionInfoCommandIterator extends IteratorIterator implements CollectionInfoIterator
+{
+    /**
+     * Return the current element as a CollectionInfo instance.
+     *
+     * @see CollectionInfoIterator::current()
+     * @see http://php.net/iterator.current
+     * @return CollectionInfo
+     */
+    public function current()
+    {
+        return new CollectionInfo(parent::current());
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/CollectionInfoIterator.php b/cache/stores/mongodb/MongoDB/Model/CollectionInfoIterator.php
new file mode 100755 (executable)
index 0000000..999ee9a
--- /dev/null
@@ -0,0 +1,38 @@
+<?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\Model;
+
+use Iterator;
+
+/**
+ * CollectionInfoIterator interface.
+ *
+ * This iterator is used for enumerating collections in a database.
+ *
+ * @api
+ * @see \MongoDB\Database::listCollections()
+ */
+interface CollectionInfoIterator extends Iterator
+{
+    /**
+     * Return the current element as a CollectionInfo instance.
+     *
+     * @return CollectionInfo
+     */
+    public function current();
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/DatabaseInfo.php b/cache/stores/mongodb/MongoDB/Model/DatabaseInfo.php
new file mode 100755 (executable)
index 0000000..e93c5ca
--- /dev/null
@@ -0,0 +1,132 @@
+<?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\Model;
+
+use MongoDB\Exception\BadMethodCallException;
+use ArrayAccess;
+/**
+ * Database information model class.
+ *
+ * This class models the database information returned by the listDatabases
+ * command. It provides methods to access common database properties.
+ *
+ * @api
+ * @see \MongoDB\Client::listDatabases()
+ * @see http://docs.mongodb.org/manual/reference/command/listDatabases/
+ */
+class DatabaseInfo implements ArrayAccess
+{
+    private $info;
+
+    /**
+     * Constructor.
+     *
+     * @param array $info Database info
+     */
+    public function __construct(array $info)
+    {
+        $this->info = $info;
+    }
+
+    /**
+     * Return the collection info as an array.
+     *
+     * @see http://php.net/oop5.magic#language.oop5.magic.debuginfo
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        return $this->info;
+    }
+
+    /**
+     * Return the database name.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return (string) $this->info['name'];
+    }
+
+    /**
+     * Return the databases size on disk (in bytes).
+     *
+     * @return integer
+     */
+    public function getSizeOnDisk()
+    {
+        return (integer) $this->info['sizeOnDisk'];
+    }
+
+    /**
+     * Return whether the database is empty.
+     *
+     * @return boolean
+     */
+    public function isEmpty()
+    {
+        return (boolean) $this->info['empty'];
+    }
+
+    /**
+     * Check whether a field exists in the database information.
+     *
+     * @see http://php.net/arrayaccess.offsetexists
+     * @param mixed $key
+     * @return boolean
+     */
+    public function offsetExists($key)
+    {
+        return array_key_exists($key, $this->info);
+    }
+
+    /**
+     * Return the field's value from the database information.
+     *
+     * @see http://php.net/arrayaccess.offsetget
+     * @param mixed $key
+     * @return mixed
+     */
+    public function offsetGet($key)
+    {
+        return $this->info[$key];
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayaccess.offsetset
+     * @throws BadMethodCallException
+     */
+    public function offsetSet($key, $value)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayaccess.offsetunset
+     * @throws BadMethodCallException
+     */
+    public function offsetUnset($key)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/DatabaseInfoIterator.php b/cache/stores/mongodb/MongoDB/Model/DatabaseInfoIterator.php
new file mode 100755 (executable)
index 0000000..91576eb
--- /dev/null
@@ -0,0 +1,38 @@
+<?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\Model;
+
+use Iterator;
+
+/**
+ * DatabaseInfoIterator interface.
+ *
+ * This iterator is used for enumerating databases on a server.
+ *
+ * @api
+ * @see \MongoDB\Client::listDatabases()
+ */
+interface DatabaseInfoIterator extends Iterator
+{
+    /**
+     * Return the current element as a DatabaseInfo instance.
+     *
+     * @return DatabaseInfo
+     */
+    public function current();
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/DatabaseInfoLegacyIterator.php b/cache/stores/mongodb/MongoDB/Model/DatabaseInfoLegacyIterator.php
new file mode 100755 (executable)
index 0000000..04be47e
--- /dev/null
@@ -0,0 +1,97 @@
+<?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\Model;
+
+/**
+ * DatabaseInfoIterator for inline listDatabases command results.
+ *
+ * This iterator may be used to wrap the array returned within the listDatabases
+ * command's single-document result.
+ *
+ * @internal
+ * @see \MongoDB\Client::listDatabases()
+ * @see http://docs.mongodb.org/manual/reference/command/listDatabases/
+ */
+class DatabaseInfoLegacyIterator implements DatabaseInfoIterator
+{
+    private $databases;
+
+    /**
+     * Constructor.
+     *
+     * @param array $databases
+     */
+    public function __construct(array $databases)
+    {
+        $this->databases = $databases;
+    }
+
+    /**
+     * Return the current element as a DatabaseInfo instance.
+     *
+     * @see DatabaseInfoIterator::current()
+     * @see http://php.net/iterator.current
+     * @return DatabaseInfo
+     */
+    public function current()
+    {
+        return new DatabaseInfo(current($this->databases));
+    }
+
+    /**
+     * Return the key of the current element.
+     *
+     * @see http://php.net/iterator.key
+     * @return integer
+     */
+    public function key()
+    {
+        return key($this->databases);
+    }
+
+    /**
+     * Move forward to next element.
+     *
+     * @see http://php.net/iterator.next
+     */
+    public function next()
+    {
+        next($this->databases);
+    }
+
+    /**
+     * Rewind the Iterator to the first element.
+     *
+     * @see http://php.net/iterator.rewind
+     */
+    public function rewind()
+    {
+        reset($this->databases);
+    }
+
+    /**
+     * Checks if current position is valid.
+     *
+     * @see http://php.net/iterator.valid
+     * @return boolean
+     */
+    public function valid()
+    {
+        return key($this->databases) !== null;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/IndexInfo.php b/cache/stores/mongodb/MongoDB/Model/IndexInfo.php
new file mode 100755 (executable)
index 0000000..7491f34
--- /dev/null
@@ -0,0 +1,226 @@
+<?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\Model;
+
+use MongoDB\Exception\BadMethodCallException;
+use ArrayAccess;
+
+/**
+ * Index information model class.
+ *
+ * This class models the index information returned by the listIndexes command
+ * or, for legacy servers, queries on the "system.indexes" collection. It
+ * provides methods to access common index options, and allows access to other
+ * options through the ArrayAccess interface (write methods are not supported).
+ * For information on keys and index options, see the referenced
+ * db.collection.createIndex() documentation.
+ *
+ * @api
+ * @see \MongoDB\Collection::listIndexes()
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst
+ * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/
+ */
+class IndexInfo implements ArrayAccess
+{
+    private $info;
+
+    /**
+     * Constructor.
+     *
+     * @param array $info Index info
+     */
+    public function __construct(array $info)
+    {
+        $this->info = $info;
+    }
+
+    /**
+     * Return the collection info as an array.
+     *
+     * @see http://php.net/oop5.magic#language.oop5.magic.debuginfo
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        return $this->info;
+    }
+
+    /**
+     * Return the index name to allow casting IndexInfo to string.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->getName();
+    }
+
+    /**
+     * Return the index key.
+     *
+     * @return array
+     */
+    public function getKey()
+    {
+        return (array) $this->info['key'];
+    }
+
+    /**
+     * Return the index name.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return (string) $this->info['name'];
+    }
+
+    /**
+     * Return the index namespace (e.g. "db.collection").
+     *
+     * @return string
+     */
+    public function getNamespace()
+    {
+        return (string) $this->info['ns'];
+    }
+
+    /**
+     * Return the index version.
+     *
+     * @return integer
+     */
+    public function getVersion()
+    {
+        return (integer) $this->info['v'];
+    }
+
+    /**
+     * Return whether or not this index is of type 2dsphere.
+     *
+     * @return boolean
+     */
+    public function is2dSphere()
+    {
+        return array_search('2dsphere', $this->getKey(), true) !== false;
+    }
+
+    /**
+     * Return whether or not this index is of type geoHaystack.
+     *
+     * @return boolean
+     */
+    public function isGeoHaystack()
+    {
+        return array_search('geoHaystack', $this->getKey(), true) !== false;
+    }
+
+    /**
+     * Return whether this is a sparse index.
+     *
+     * @see http://docs.mongodb.org/manual/core/index-sparse/
+     * @return boolean
+     */
+    public function isSparse()
+    {
+        return ! empty($this->info['sparse']);
+    }
+
+    /**
+     * Return whether or not this index is of type text.
+     *
+     * @return boolean
+     */
+    public function isText()
+    {
+        return array_search('text', $this->getKey(), true) !== false;
+    }
+
+    /**
+     * Return whether this is a TTL index.
+     *
+     * @see http://docs.mongodb.org/manual/core/index-ttl/
+     * @return boolean
+     */
+    public function isTtl()
+    {
+        return array_key_exists('expireAfterSeconds', $this->info);
+    }
+
+    /**
+     * Return whether this is a unique index.
+     *
+     * @see http://docs.mongodb.org/manual/core/index-unique/
+     * @return boolean
+     */
+    public function isUnique()
+    {
+        return ! empty($this->info['unique']);
+    }
+
+    /**
+     * Check whether a field exists in the index information.
+     *
+     * @see http://php.net/arrayaccess.offsetexists
+     * @param mixed $key
+     * @return boolean
+     */
+    public function offsetExists($key)
+    {
+        return array_key_exists($key, $this->info);
+    }
+
+    /**
+     * Return the field's value from the index information.
+     *
+     * This method satisfies the Enumerating Indexes specification's requirement
+     * that index fields be made accessible under their original names. It may
+     * also be used to access fields that do not have a helper method.
+     *
+     * @see http://php.net/arrayaccess.offsetget
+     * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst#getting-full-index-information
+     * @param mixed $key
+     * @return mixed
+     */
+    public function offsetGet($key)
+    {
+        return $this->info[$key];
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayaccess.offsetset
+     * @throws BadMethodCallException
+     */
+    public function offsetSet($key, $value)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayaccess.offsetunset
+     * @throws BadMethodCallException
+     */
+    public function offsetUnset($key)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/IndexInfoIterator.php b/cache/stores/mongodb/MongoDB/Model/IndexInfoIterator.php
new file mode 100755 (executable)
index 0000000..5195172
--- /dev/null
@@ -0,0 +1,38 @@
+<?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\Model;
+
+use Iterator;
+
+/**
+ * IndexInfoIterator interface.
+ *
+ * This iterator is used for enumerating indexes in a collection.
+ *
+ * @api
+ * @see \MongoDB\Collection::listIndexes()
+ */
+interface IndexInfoIterator extends Iterator
+{
+    /**
+     * Return the current element as a IndexInfo instance.
+     *
+     * @return IndexInfo
+     */
+    public function current();
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/IndexInfoIteratorIterator.php b/cache/stores/mongodb/MongoDB/Model/IndexInfoIteratorIterator.php
new file mode 100755 (executable)
index 0000000..7eb3b6b
--- /dev/null
@@ -0,0 +1,48 @@
+<?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\Model;
+
+use IteratorIterator;
+
+/**
+ * IndexInfoIterator for both listIndexes command and legacy query results.
+ *
+ * This common iterator may be used to wrap a Cursor returned by both the
+ * listIndexes command and, for legacy servers, queries on the "system.indexes"
+ * collection.
+ *
+ * @internal
+ * @see \MongoDB\Collection::listIndexes()
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst
+ * @see http://docs.mongodb.org/manual/reference/command/listIndexes/
+ * @see http://docs.mongodb.org/manual/reference/system-collections/
+ */
+class IndexInfoIteratorIterator extends IteratorIterator implements IndexInfoIterator
+{
+    /**
+     * Return the current element as an IndexInfo instance.
+     *
+     * @see IndexInfoIterator::current()
+     * @see http://php.net/iterator.current
+     * @return IndexInfo
+     */
+    public function current()
+    {
+        return new IndexInfo(parent::current());
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/IndexInput.php b/cache/stores/mongodb/MongoDB/Model/IndexInput.php
new file mode 100755 (executable)
index 0000000..b306fa0
--- /dev/null
@@ -0,0 +1,99 @@
+<?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\Model;
+
+use MongoDB\BSON\Serializable;
+use MongoDB\Exception\InvalidArgumentException;
+
+/**
+ * Index input model class.
+ *
+ * This class is used to validate user input for index creation.
+ *
+ * @internal
+ * @see \MongoDB\Collection::createIndexes()
+ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst
+ * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/
+ */
+class IndexInput implements Serializable
+{
+    private $index;
+
+    /**
+     * Constructor.
+     *
+     * @param array $index Index specification
+     * @throws InvalidArgumentException
+     */
+    public function __construct(array $index)
+    {
+        if ( ! isset($index['key'])) {
+            throw new InvalidArgumentException('Required "key" document is missing from index specification');
+        }
+
+        if ( ! is_array($index['key']) && ! is_object($index['key'])) {
+            throw InvalidArgumentException::invalidType('"key" option', $index['key'], 'array or object');
+        }
+
+        foreach ($index['key'] as $fieldName => $order) {
+            if ( ! is_int($order) && ! is_float($order) && ! is_string($order)) {
+                throw InvalidArgumentException::invalidType(sprintf('order value for "%s" field within "key" option', $fieldName), $order, 'numeric or string');
+            }
+        }
+
+        if ( ! isset($index['ns'])) {
+            throw new InvalidArgumentException('Required "ns" option is missing from index specification');
+        }
+
+        if ( ! is_string($index['ns'])) {
+            throw InvalidArgumentException::invalidType('"ns" option', $index['ns'], 'string');
+        }
+
+        if ( ! isset($index['name'])) {
+            $index['name'] = \MongoDB\generate_index_name($index['key']);
+        }
+
+        if ( ! is_string($index['name'])) {
+            throw InvalidArgumentException::invalidType('"name" option', $index['name'], 'string');
+        }
+
+        $this->index = $index;
+    }
+
+    /**
+     * Return the index name.
+     *
+     * @param string
+     */
+    public function __toString()
+    {
+        return $this->index['name'];
+    }
+
+    /**
+     * Serialize the index information to BSON for index creation.
+     *
+     * @see \MongoDB\Collection::createIndexes()
+     * @see http://php.net/mongodb-bson-serializable.bsonserialize
+     * @return array
+     */
+    public function bsonSerialize()
+    {
+        return $this->index;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Model/TypeMapArrayIterator.php b/cache/stores/mongodb/MongoDB/Model/TypeMapArrayIterator.php
new file mode 100755 (executable)
index 0000000..b30a0ec
--- /dev/null
@@ -0,0 +1,166 @@
+<?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\Model;
+
+use ArrayIterator;
+use MongoDB\Exception\BadMethodCallException;
+
+/**
+ * Iterator for applying a type map to documents in inline command results.
+ *
+ * @internal
+ */
+class TypeMapArrayIterator extends ArrayIterator
+{
+    private $typeMap;
+
+    /**
+     * Constructor.
+     *
+     * @param array $documents
+     * @param array $typeMap
+     */
+    public function __construct(array $documents = [], array $typeMap)
+    {
+        parent::__construct($documents);
+
+        $this->typeMap = $typeMap;
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayiterator.append
+     * @throws BadMethodCallException
+     */
+    public function append($value)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayiterator.asort
+     * @throws BadMethodCallException
+     */
+    public function asort()
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Return the current element with the type map applied to it.
+     *
+     * @see http://php.net/arrayiterator.current
+     * @return array|object
+     */
+    public function current()
+    {
+        return \MongoDB\apply_type_map_to_document(parent::current(), $this->typeMap);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayiterator.ksort
+     * @throws BadMethodCallException
+     */
+    public function ksort()
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayiterator.natcasesort
+     * @throws BadMethodCallException
+     */
+    public function natcasesort()
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayiterator.natsort
+     * @throws BadMethodCallException
+     */
+    public function natsort()
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Return the value from the provided offset with the type map applied.
+     *
+     * @see http://php.net/arrayiterator.offsetget
+     * @param mixed $offset
+     * @return array|object
+     */
+    public function offsetGet($offset)
+    {
+        return \MongoDB\apply_type_map_to_document(parent::offsetGet($offset), $this->typeMap);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayiterator.offsetset
+     * @throws BadMethodCallException
+     */
+    public function offsetSet($index, $newval)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayiterator.offsetunset
+     * @throws BadMethodCallException
+     */
+    public function offsetUnset($index)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayiterator.uasort
+     * @throws BadMethodCallException
+     */
+    public function uasort($cmp_function)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+
+    /**
+     * Not supported.
+     *
+     * @see http://php.net/arrayiterator.uksort
+     * @throws BadMethodCallException
+     */
+    public function uksort($cmp_function)
+    {
+        throw BadMethodCallException::classIsImmutable(__CLASS__);
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Operation/Aggregate.php b/cache/stores/mongodb/MongoDB/Operation/Aggregate.php
new file mode 100755 (executable)
index 0000000..2ddba68
--- /dev/null
@@ -0,0 +1,363 @@
+<?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\Operation;
+
+use MongoDB\Driver\Command;
+use MongoDB\Driver\ReadConcern;
+use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+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\TypeMapArrayIterator;
+use ArrayIterator;
+use stdClass;
+use Traversable;
+
+/**
+ * Operation for the aggregate command.
+ *
+ * @api
+ * @see \MongoDB\Collection::aggregate()
+ * @see http://docs.mongodb.org/manual/reference/command/aggregate/
+ */
+class Aggregate implements Executable
+{
+    private static $wireVersionForCollation = 5;
+    private static $wireVersionForDocumentLevelValidation = 4;
+    private static $wireVersionForReadConcern = 4;
+    private static $wireVersionForWriteConcern = 5;
+
+    private $databaseName;
+    private $collectionName;
+    private $pipeline;
+    private $options;
+
+    /**
+     * Constructs an aggregate command.
+     *
+     * Supported options:
+     *
+     *  * allowDiskUse (boolean): Enables writing to temporary files. When set
+     *    to true, aggregation stages can write data to the _tmp sub-directory
+     *    in the dbPath directory. The default is false.
+     *
+     *  * batchSize (integer): The number of documents to return per batch.
+     *
+     *  * bypassDocumentValidation (boolean): If true, allows the write to
+     *    circumvent document level validation. This only applies when the $out
+     *    stage is specified.
+     *
+     *    For servers < 3.2, this option is ignored as document level validation
+     *    is not available.
+     *
+     *  * collation (document): Collation specification.
+     *
+     *    This is not supported for server versions < 3.4 and will result in an
+     *    exception at execution time if used.
+     *
+     *  * comment (string): An arbitrary string to help trace the operation
+     *    through the database profiler, currentOp, and logs.
+     *
+     *  * explain (boolean): Specifies whether or not to return the information
+     *    on the processing of the pipeline.
+     *
+     *  * hint (string|document): The index to use. Specify either the index
+     *    name as a string or the index key pattern as a document. If specified,
+     *    then the query system will only consider plans using the hinted index.
+     *
+     *  * maxTimeMS (integer): The maximum amount of time to allow the query to
+     *    run.
+     *
+     *  * readConcern (MongoDB\Driver\ReadConcern): Read concern. Note that a
+     *    "majority" read concern is not compatible with the $out stage.
+     *
+     *    This is not supported for server versions < 3.2 and will result in an
+     *    exception at execution time if used.
+     *
+     *  * readPreference (MongoDB\Driver\ReadPreference): Read preference.
+     *
+     *    This option is ignored if the $out stage is specified.
+     *
+     *  * session (MongoDB\Driver\Session): Client session.
+     *
+     *    Sessions are not supported for server versions < 3.6.
+     *
+     *  * typeMap (array): Type map for BSON deserialization. This will be
+     *    applied to the returned Cursor (it is not sent to the server).
+     *
+     *  * useCursor (boolean): Indicates whether the command will request that
+     *    the server provide results using a cursor. The default is true.
+     *
+     *    This option allows users to turn off cursors if necessary to aid in
+     *    mongod/mongos upgrades.
+     *
+     *  * writeConcern (MongoDB\Driver\WriteConcern): Write concern. This only
+     *    applies when the $out stage is specified.
+     *
+     *    This is not supported for server versions < 3.4 and will result in an
+     *    exception at execution time if used.
+     *
+     * Note: Collection-agnostic commands (e.g. $currentOp) may be executed by
+     * specifying null for the collection name.
+     *
+     * @param string      $databaseName   Database name
+     * @param string|null $collectionName Collection name
+     * @param array       $pipeline       List of pipeline operations
+     * @param array       $options        Command options
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function __construct($databaseName, $collectionName, array $pipeline, array $options = [])
+    {
+        $expectedIndex = 0;
+
+        foreach ($pipeline as $i => $operation) {
+            if ($i !== $expectedIndex) {
+                throw new InvalidArgumentException(sprintf('$pipeline is not a list (unexpected index: "%s")', $i));
+            }
+
+            if ( ! is_array($operation) && ! is_object($operation)) {
+                throw InvalidArgumentException::invalidType(sprintf('$pipeline[%d]', $i), $operation, 'array or object');
+            }
+
+            $expectedIndex += 1;
+        }
+
+        $options += [
+            'allowDiskUse' => false,
+            'useCursor' => true,
+        ];
+
+        if ( ! is_bool($options['allowDiskUse'])) {
+            throw InvalidArgumentException::invalidType('"allowDiskUse" option', $options['allowDiskUse'], 'boolean');
+        }
+
+        if (isset($options['batchSize']) && ! is_integer($options['batchSize'])) {
+            throw InvalidArgumentException::invalidType('"batchSize" option', $options['batchSize'], 'integer');
+        }
+
+        if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) {
+            throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean');
+        }
+
+        if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) {
+            throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object');
+        }
+
+        if (isset($options['comment']) && ! is_string($options['comment'])) {
+            throw InvalidArgumentException::invalidType('"comment" option', $options['comment'], 'string');
+        }
+
+        if (isset($options['explain']) && ! is_bool($options['explain'])) {
+            throw InvalidArgumentException::invalidType('"explain" option', $options['explain'], 'boolean');
+        }
+
+        if (isset($options['hint']) && ! is_string($options['hint']) && ! is_array($options['hint']) && ! is_object($options['hint'])) {
+            throw InvalidArgumentException::invalidType('"hint" option', $options['hint'], 'string or array or object');
+        }
+
+        if (isset($options['maxAwaitTimeMS']) && ! is_integer($options['maxAwaitTimeMS'])) {
+            throw InvalidArgumentException::invalidType('"maxAwaitTimeMS" option', $options['maxAwaitTimeMS'], 'integer');
+        }
+
+        if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) {
+            throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
+        }
+
+        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['session']) && ! $options['session'] instanceof Session) {
+            throw InvalidArgumentException::invalidType('"session" option', $options['session'], 'MongoDB\Driver\Session');
+        }
+
+        if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
+            throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
+        }
+
+        if ( ! is_bool($options['useCursor'])) {
+            throw InvalidArgumentException::invalidType('"useCursor" option', $options['useCursor'], 'boolean');
+        }
+
+        if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
+            throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+        }
+
+        if (isset($options['batchSize']) && ! $options['useCursor']) {
+            throw new InvalidArgumentException('"batchSize" option should not be used if "useCursor" is false');
+        }
+
+        if (isset($options['readConcern']) && $options['readConcern']->isDefault()) {
+            unset($options['readConcern']);
+        }
+
+        if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
+            unset($options['writeConcern']);
+        }
+
+        if ( ! empty($options['explain'])) {
+            $options['useCursor'] = false;
+        }
+
+        $this->databaseName = (string) $databaseName;
+        $this->collectionName = isset($collectionName) ? (string) $collectionName : null;
+        $this->pipeline = $pipeline;
+        $this->options = $options;
+    }
+
+    /**
+     * Execute the operation.
+     *
+     * @see Executable::execute()
+     * @param Server $server
+     * @return Traversable
+     * @throws UnexpectedValueException if the command response was malformed
+     * @throws UnsupportedException if collation, read concern, or write concern is used and unsupported
+     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
+     */
+    public function execute(Server $server)
+    {
+        if (isset($this->options['collation']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForCollation)) {
+            throw UnsupportedException::collationNotSupported();
+        }
+
+        if (isset($this->options['readConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+            throw UnsupportedException::readConcernNotSupported();
+        }
+
+        if (isset($this->options['writeConcern']) && ! \MongoDB\server_supports_feature($server, self::$wireVersionForWriteConcern)) {
+            throw UnsupportedException::writeConcernNotSupported();
+        }
+
+        $hasExplain = ! empty($this->options['explain']);
+        $hasOutStage = \MongoDB\is_last_pipeline_operator_out($this->pipeline);
+
+        $command = $this->createCommand($server);
+        $options = $this->createOptions($hasOutStage, $hasExplain);
+
+        $cursor = ($hasOutStage && ! $hasExplain)
+            ? $server->executeReadWriteCommand($this->databaseName, $command, $options)
+            : $server->executeReadCommand($this->databaseName, $command, $options);
+
+        if ($this->options['useCursor'] || $hasExplain) {
+            if (isset($this->options['typeMap'])) {
+                $cursor->setTypeMap($this->options['typeMap']);
+            }
+
+            return $cursor;
+        }
+
+        $result = current($cursor->toArray());
+
+        if ( ! isset($result->result) || ! is_array($result->result)) {
+            throw new UnexpectedValueException('aggregate command did not return a "result" array');
+        }
+
+        if (isset($this->options['typeMap'])) {
+            return new TypeMapArrayIterator($result->result, $this->options['typeMap']);
+        }
+
+        return new ArrayIterator($result->result);
+    }
+
+    /**
+     * Create the aggregate command.
+     *
+     * @param Server  $server
+     * @return Command
+     */
+    private function createCommand(Server $server)
+    {
+        $cmd = [
+            'aggregate' => isset($this->collectionName) ? $this->collectionName : 1,
+            'pipeline' => $this->pipeline,
+        ];
+        $cmdOptions = [];
+
+        $cmd['allowDiskUse'] = $this->options['allowDiskUse'];
+
+        if (isset($this->options['bypassDocumentValidation']) && \MongoDB\server_supports_feature($server, self::$wireVersionForDocumentLevelValidation)) {
+            $cmd['bypassDocumentValidation'] = $this->options['bypassDocumentValidation'];
+        }
+
+        foreach (['comment', 'explain', 'maxTimeMS'] as $option) {
+            if (isset($this->options[$option])) {
+                $cmd[$option] = $this->options[$option];
+            }
+        }
+
+        if (isset($this->options['collation'])) {
+            $cmd['collation'] = (object) $this->options['collation'];
+        }
+
+        if (isset($this->options['hint'])) {
+            $cmd['hint'] = is_array($this->options['hint']) ? (object) $this->options['hint'] : $this->options['hint'];
+        }
+
+        if (isset($this->options['maxAwaitTimeMS'])) {
+            $cmdOptions['maxAwaitTimeMS'] = $this->options['maxAwaitTimeMS'];
+        }
+
+        if ($this->options['useCursor']) {
+            $cmd['cursor'] = isset($this->options["batchSize"])
+                ? ['batchSize' => $this->options["batchSize"]]
+                : new stdClass;
+        }
+
+        return new Command($cmd, $cmdOptions);
+    }
+
+    /**
+     * Create options for executing the command.
+     *
+     * @see http://php.net/manual/en/mongodb-driver-server.executereadcommand.php
+     * @see http://php.net/manual/en/mongodb-driver-server.executereadwritecommand.php
+     * @param boolean $hasOutStage
+     * @return array
+     */
+    private function createOptions($hasOutStage, $hasExplain)
+    {
+        $options = [];
+
+        if (isset($this->options['readConcern'])) {
+            $options['readConcern'] = $this->options['readConcern'];
+        }
+
+        if ( ! $hasOutStage && isset($this->options['readPreference'])) {
+            $options['readPreference'] = $this->options['readPreference'];
+        }
+
+        if (isset($this->options['session'])) {
+            $options['session'] = $this->options['session'];
+        }
+
+        if ($hasOutStage && ! $hasExplain && isset($this->options['writeConcern'])) {
+            $options['writeConcern'] = $this->options['writeConcern'];
+        }
+
+        return $options;
+    }
+}
diff --git a/cache/stores/mongodb/MongoDB/Operation/BulkWrite.php b/cache/stores/mongodb/MongoDB/Operation/BulkWrite.php
new file mode 100755 (executable)
index 0000000..82ef082
--- /dev/null
@@ -0,0 +1,380 @@
+<?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\Operation;
+
+use MongoDB\BulkWriteResult;
+use MongoDB\Driver\BulkWrite as Bulk;
+use MongoDB\Driver\Server;
+use MongoDB\Driver\Session;
+use MongoDB\Driver\WriteConcern;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
+use MongoDB\Exception\InvalidArgumentException;
+use MongoDB\Exception\UnsupportedException;
+
+/**
+ * Operation for executing multiple write operations.
+ *
+ * @api
+ * @see \MongoDB\Collection::bulkWrite()
+ */
+class BulkWrite implements Executable
+{
+    const DELETE_MANY = 'deleteMany';
+    const DELETE_ONE  = 'deleteOne';
+    const INSERT_ONE  = 'insertOne';
+    const REPLACE_ONE = 'replaceOne';
+    const UPDATE_MANY = 'updateMany';
+    const UPDATE_ONE  = 'updateOne';
+
+    private static $wireVersionForArrayFilters = 6;
+    private static $wireVersionForCollation = 5;
+    private static $wireVersionForDocumentLevelValidation = 4;
+
+    private $databaseName;
+    private $collectionName;
+    private $operations;
+    private $options;
+    private $isArrayFiltersUsed = false;
+    private $isCollationUsed = false;
+
+    /**
+     * Constructs a bulk write operation.
+     *
+     * Example array structure for all supported operation types:
+     *
+     *  [
+     *    [ 'deleteMany' => [ $filter, $options ] ],
+     *    [ 'deleteOne'  => [ $filter, $options ] ],
+     *    [ 'insertOne'  => [ $document ] ],
+     *    [ 'replaceOne' => [ $filter, $replacement, $options ] ],
+     *    [ 'updateMany' => [ $filter, $update, $options ] ],
+     *    [ 'updateOne'  => [ $filter, $update, $options ] ],
+     *  ]
+     *
+     * Arguments correspond to the respective Operation classes; however, the
+     * writeConcern option is specified for the top-level bulk write operation
+     * instead of each individual operation.
+     *
+     * Supported options for deleteMany and deleteOne operations:
+     *
+     *  * collation (document): Collation specification.
+     *
+     *    This is not supported for server versions < 3.4 and will result in an
+     *    exception at execution time if used.
+     *
+     * Supported options for replaceOne, updateMany, and updateOne operations:
+     *
+     *  * collation (document): Collation specification.
+     *
+     *    This is not supported for server versions < 3.4 and will result in an
+     *    exception at execution time if used.
+     *
+     *  * upsert (boolean): When true, a new document is created if no document
+     *    matches the query. The default is false.
+     *
+     * Supported options for updateMany and updateOne operations:
+     *
+     *  * arrayFilters (document array): A set of filters specifying to which
+     *    array elements an update should apply.
+     *
+     *    This is not supported for server versions < 3.6 and will result in an
+     *    exception at execution time if used.
+     *
+     * Supported options for the bulk write operation:
+     *
+     *  * bypassDocumentValidation (boolean): If true, allows the write to
+     *    circumvent document level validation. The default is false.
+     *
+     *    For servers < 3.2, this option is ignored as document level validation
+     *    is not available.
+     *
+     *  * ordered (boolean): If true, when an insert fails, return without
+     *    performing the remaining writes. If false, when a write fails,
+     *    continue with the remaining writes, if any. The default is true.
+     *
+     *  * session (MongoDB\Driver\Session): Client session.
+     *
+     *    Sessions are not supported for server versions < 3.6.
+     *
+     *  * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
+     *
+     * @param string  $databaseName   Database name
+     * @param string  $collectionName Collection name
+     * @param array[] $operations     List of write operations
+     * @param array   $options        Command options
+     * @throws InvalidArgumentException for parameter/option parsing errors
+     */
+    public function __construct($databaseName, $collectionName, array $operations, array $options = [])
+    {
+        if (empty($operations)) {
+            throw new InvalidArgumentException('$operations is empty');
+        }
+
+        $expectedIndex = 0;
+
+        foreach ($operations as $i => $operation) {
+            if ($i !== $expectedIndex) {
+                throw new InvalidArgumentException(sprintf('$operations is not a list (unexpected index: "%s")', $i));
+            }
+
+            if ( ! is_array($operation)) {
+                throw InvalidArgumentException::invalidType(sprintf('$operations[%d]', $i), $operation, 'array');
+            }
+
+            if (count($operation) !== 1) {
+                throw new InvalidArgumentException(sprintf('Expected one element in $operation[%d], actually: %d', $i, count($operation)));
+            }
+
+            $type = key($operation);
+            $args = current($operation);
+
+            if ( ! isset($args[0]) && ! array_key_exists(0, $args)) {
+                throw new InvalidArgumentException(sprintf('Missing first argument for $operations[%d]["%s"]', $i, $type));
+            }
+
+            if ( ! is_array($args[0]) && ! is_object($args[0])) {
+                throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][0]', $i, $type), $args[0], 'array or object');
+            }
+
+            switch ($type) {
+                case self::INSERT_ONE:
+                    break;
+
+                case self::DELETE_MANY:
+                case self::DELETE_ONE:
+                    if ( ! isset($args[1])) {
+                        $args[1] = [];
+                    }
+
+                    if ( ! is_array($args[1])) {
+                        throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][1]', $i, $type), $args[1], 'array');
+                    }
+
+                    $args[1]['limit'] = ($type === self::DELETE_ONE ? 1 : 0);
+
+                    if (isset($args[1]['collation'])) {
+                        $this->isCollationUsed = true;
+
+                        if ( ! is_array($args[1]['collation']) && ! is_object($args[1]['collation'])) {
+                            throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][1]["collation"]', $i, $type), $args[1]['collation'], 'array or object');
+                        }
+                    }
+
+                    $operations[$i][$type][1] = $args[1];
+
+                    break;
+
+                case self::REPLACE_ONE:
+                    if ( ! isset($args[1]) && ! array_key_exists(1, $args)) {
+                        throw new InvalidArgumentException(sprintf('Missing second argument for $operations[%d]["%s"]', $i, $type));
+                    }
+
+                    if ( ! is_array($args[1]) && ! is_object($args[1])) {
+                        throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][1]', $i, $type), $args[1], 'array or object');
+                    }
+
+                    if (\MongoDB\is_first_key_operator($args[1])) {
+                        throw new InvalidArgumentException(sprintf('First key in $operations[%d]["%s"][1] is an update operator', $i, $type));
+                    }
+
+                    if ( ! isset($args[2])) {
+                        $args[2] = [];
+                    }
+
+                    if ( ! is_array($args[2])) {
+                        throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]', $i, $type), $args[2], 'array');
+                    }
+
+                    $args[2]['multi'] = false;
+                    $args[2] += ['upsert' => false];
+
+                    if (isset($args[2]['collation'])) {
+                        $this->isCollationUsed = true;
+
+                        if ( ! is_array($args[2]['collation']) && ! is_object($args[2]['collation'])) {
+                            throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]["collation"]', $i, $type), $args[2]['collation'], 'array or object');
+                        }
+                    }
+
+                    if ( ! is_bool($args[2]['upsert'])) {
+                        throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][2]["upsert"]', $i, $type), $args[2]['upsert'], 'boolean');
+                    }
+
+                    $operations[$i][$type][2] = $args[2];
+
+                    break;
+
+                case self::UPDATE_MANY:
+                case self::UPDATE_ONE:
+                    if ( ! isset($args[1]) && ! array_key_exists(1, $args)) {
+                        throw new InvalidArgumentException(sprintf('Missing second argument for $operations[%d]["%s"]', $i, $type));
+                    }
+
+                    if ( ! is_array($args[1]) && ! is_object($args[1])) {
+                        throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s"][1]', $i, $type), $args[1], 'array or object');
+                    }
+
+                    if ( ! \MongoDB\is_first_key_operator($args[1])) {
+                        throw new InvalidArgumentException(sprintf('First key in $operations[%d]["%s"][1] is not an update operator', $i, $type));
+                    }
+
+                    if ( ! isset($args[2])) {
+                        $args[2] = [];
+                    }
+
+                    if ( ! is_array($args[2])) {
+                        throw InvalidArgumentException::invalidType(sprintf('$operations[%d]["%s&q