Merge branch 'MDL-68935-310' of git://github.com/jleyva/moodle into MOODLE_310_STABLE
authorSara Arjona <sara@moodle.com>
Mon, 28 Sep 2020 09:41:01 +0000 (11:41 +0200)
committerSara Arjona <sara@moodle.com>
Mon, 28 Sep 2020 09:41:01 +0000 (11:41 +0200)
166 files changed:
.eslintignore
.stylelintignore
admin/admin_settings_search_form.php [deleted file]
admin/search.php
admin/settings/courses.php
admin/tests/behat/behat_admin.php
admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-debug.js
admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js
admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search.js
admin/tool/capability/yui/src/search/js/search.js
admin/tool/lp/classes/form/template_cohorts.php
admin/tool/lp/template_cohorts.php
admin/tool/mobile/classes/api.php
admin/tool/templatelibrary/amd/build/search.min.js
admin/tool/templatelibrary/amd/build/search.min.js.map
admin/tool/templatelibrary/amd/src/search.js
admin/tool/templatelibrary/templates/list_templates_page.mustache
admin/tool/usertours/classes/tour.php
admin/tool/usertours/db/upgrade.php
admin/tool/usertours/tests/tour_test.php
admin/tool/usertours/version.php
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_course_task.class.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_controller_dbops.class.php
backup/util/ui/renderer.php
blocks/blog_menu/block_blog_menu.php
blocks/blog_menu/tests/behat/block_blog_menu_activity.feature
blocks/blog_menu/tests/behat/block_blog_menu_course.feature
blocks/globalsearch/block_globalsearch.php
blocks/search_forums/classes/output/search_form.php
blocks/search_forums/templates/search_form.mustache
blocks/search_forums/tests/behat/block_search_forums_course.feature
blocks/search_forums/tests/behat/block_search_forums_frontpage.feature
blocks/settings/renderer.php
blocks/social_activities/tests/behat/edit_activities.feature
cache/classes/factory.php
cache/classes/loaders.php
cache/disabledlib.php
cache/tests/cache_test.php
cohort/index.php
competency/classes/external/competency_framework_exporter.php
contentbank/amd/build/search.min.js
contentbank/amd/build/search.min.js.map
contentbank/amd/build/selectors.min.js
contentbank/amd/build/selectors.min.js.map
contentbank/amd/build/sort.min.js
contentbank/amd/build/sort.min.js.map
contentbank/amd/src/search.js
contentbank/amd/src/selectors.js
contentbank/amd/src/sort.js
contentbank/templates/bankcontent.mustache
contentbank/templates/bankcontent/search.mustache
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/build/local/activitychooser/selectors.min.js
course/amd/build/local/activitychooser/selectors.min.js.map
course/amd/src/local/activitychooser/dialogue.js
course/amd/src/local/activitychooser/selectors.js
course/classes/management_renderer.php
course/format/social/lib.php
course/format/upgrade.txt
course/management.php
course/renderer.php
course/search.php
course/templates/local/activitychooser/search.mustache
course/tests/behat/course_search.feature
h5p/classes/editor.php
h5p/classes/file_storage.php
h5p/lib.php
h5p/tests/editor_test.php
h5p/tests/generator/lib.php
h5p/tests/h5p_file_storage_test.php
lang/en/backup.php
lang/en/contentbank.php
lang/en/moodle.php
lib/amd/build/search-input.min.js [deleted file]
lib/amd/build/search-input.min.js.map [deleted file]
lib/amd/src/search-input.js [deleted file]
lib/classes/component.php
lib/db/caches.php
lib/db/install.xml
lib/db/upgrade.php
lib/externallib.php
lib/http-message/LICENSE [new file with mode: 0644]
lib/http-message/readme_moodle.txt [new file with mode: 0644]
lib/http-message/src/MessageInterface.php [new file with mode: 0644]
lib/http-message/src/RequestInterface.php [new file with mode: 0644]
lib/http-message/src/ResponseInterface.php [new file with mode: 0644]
lib/http-message/src/ServerRequestInterface.php [new file with mode: 0644]
lib/http-message/src/StreamInterface.php [new file with mode: 0644]
lib/http-message/src/UploadedFileInterface.php [new file with mode: 0644]
lib/http-message/src/UriInterface.php [new file with mode: 0644]
lib/licenselib.php
lib/moodlelib.php
lib/myprofilelib.php
lib/outputrenderers.php
lib/php-enum/LICENSE [new file with mode: 0644]
lib/php-enum/readme_moodle.txt [new file with mode: 0644]
lib/php-enum/src/Enum.php [new file with mode: 0644]
lib/setuplib.php
lib/templates/checkbox.mustache [new file with mode: 0644]
lib/templates/popover_region.mustache
lib/templates/search_input.mustache [new file with mode: 0644]
lib/templates/search_input_auto.mustache [new file with mode: 0644]
lib/templates/search_input_navbar.mustache [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/setuplib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/zipstream/LICENSE [new file with mode: 0644]
lib/zipstream/readme_moodle.txt [new file with mode: 0644]
lib/zipstream/src/Bigint.php [new file with mode: 0644]
lib/zipstream/src/DeflateStream.php [new file with mode: 0644]
lib/zipstream/src/Exception.php [new file with mode: 0644]
lib/zipstream/src/Exception/EncodingException.php [new file with mode: 0644]
lib/zipstream/src/Exception/FileNotFoundException.php [new file with mode: 0644]
lib/zipstream/src/Exception/FileNotReadableException.php [new file with mode: 0644]
lib/zipstream/src/Exception/IncompatibleOptionsException.php [new file with mode: 0644]
lib/zipstream/src/Exception/OverflowException.php [new file with mode: 0644]
lib/zipstream/src/Exception/StreamNotReadableException.php [new file with mode: 0644]
lib/zipstream/src/File.php [new file with mode: 0644]
lib/zipstream/src/Option/Archive.php [new file with mode: 0644]
lib/zipstream/src/Option/File.php [new file with mode: 0644]
lib/zipstream/src/Option/Method.php [new file with mode: 0644]
lib/zipstream/src/Option/Version.php [new file with mode: 0644]
lib/zipstream/src/Stream.php [new file with mode: 0644]
lib/zipstream/src/ZipStream.php [new file with mode: 0644]
message/templates/message_drawer_view_overview_header.mustache
message/templates/message_drawer_view_search_header.mustache
message/templates/message_popover.mustache
mod/forum/classes/output/quick_search_form.php
mod/forum/templates/quick_search_form.mustache
mod/glossary/view.php
mod/lti/locallib.php
mod/lti/templates/tool_card.mustache
mod/lti/tests/locallib_test.php
mod/quiz/accessrule/seb/db/install.php
mod/quiz/attemptlib.php
mod/quiz/module.js
mod/quiz/renderer.php
mod/quiz/styles.css
mod/quiz/templates/timer.mustache [moved from blocks/settings/templates/search_form.mustache with 52% similarity]
mod/wiki/lib.php
search/tests/behat/behat_search.php
tag/manage.php
theme/boost/scss/moodle.scss
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/atto.scss [moved from lib/editor/atto/styles.css with 85% similarity]
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/filemanager.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/popover-region.scss
theme/boost/scss/moodle/search.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/navbar.mustache
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
theme/classic/templates/navbar.mustache
user/tests/behat/view_full_profile.feature
version.php

index e4546c5..0d0a6ea 100644 (file)
@@ -67,6 +67,9 @@ lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
 lib/plist/
+lib/zipstream/
+lib/php-enum/
+lib/http-message/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index 5fae1d4..0207e41 100644 (file)
@@ -68,6 +68,9 @@ lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
 lib/plist/
+lib/zipstream/
+lib/php-enum/
+lib/http-message/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
diff --git a/admin/admin_settings_search_form.php b/admin/admin_settings_search_form.php
deleted file mode 100644 (file)
index ad42300..0000000
+++ /dev/null
@@ -1,48 +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/>.
-
-/**
- * Admin settings search form
- *
- * @package    admin
- * @copyright  2016 Damyon Wiese
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once $CFG->libdir.'/formslib.php';
-
-/**
- * Admin settings search form
- *
- * @package    admin
- * @copyright  2016 Damyon Wiese
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class admin_settings_search_form extends moodleform {
-    function definition () {
-        $mform = $this->_form;
-
-        //$mform->addElement('header', 'settingsheader', get_string('search', 'admin'));
-        $elements = [];
-        $elements[] = $mform->createElement('text', 'query', get_string('query', 'admin'));
-        $elements[] = $mform->createElement('submit', 'search', get_string('search'));
-        $mform->addGroup($elements);
-        $mform->setType('query', PARAM_RAW);
-        $mform->setDefault('query', optional_param('query', '', PARAM_RAW));
-    }
-}
index 5539517..98ec900 100644 (file)
@@ -68,9 +68,16 @@ if ($errormsg !== '') {
 $showsettingslinks = true;
 
 if ($hassiteconfig) {
-    require_once("admin_settings_search_form.php");
-    $form = new admin_settings_search_form();
-    $form->display();
+    $data = [
+        'action' => new moodle_url('/admin/search.php'),
+        'btnclass' => 'btn-primary',
+        'inputname' => 'query',
+        'searchstring' => get_string('search'),
+        'query' => $query,
+        'extraclasses' => 'd-flex justify-content-center'
+    ];
+    echo $OUTPUT->render_from_template('core/search_input', $data);
+
     echo '<hr>';
     if ($query) {
         echo admin_search_settings_html($query);
index e7d9bf9..6b53eb0 100644 (file)
@@ -285,6 +285,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
         ['value' => 1, 'locked' => 0])
     );
 
+    $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_legacyfiles',
+        new lang_string('generallegacyfiles', 'backup'),
+        new lang_string('configlegacyfiles', 'backup'), array('value' => 1, 'locked' => 0)));
     $ADMIN->add('backups', $temp);
 
     // Create a page for general import configuration and defaults.
@@ -311,6 +314,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
         new lang_string('configgeneralcontentbankcontent', 'backup'),
         ['value' => 1, 'locked' => 0])
     );
+    $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_legacyfiles',
+        new lang_string('generallegacyfiles', 'backup'),
+        new lang_string('configlegacyfiles', 'backup'), array('value' => 1, 'locked' => 0)));
 
     $ADMIN->add('backups', $temp);
 
@@ -437,6 +443,10 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
         1)
     );
 
+    $temp->add(new admin_setting_configcheckbox('backup/backup_auto_legacyfiles',
+        new lang_string('generallegacyfiles', 'backup'),
+        new lang_string('configlegacyfiles', 'backup'), 1));
+
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_messages', new lang_string('messages', 'message'), new lang_string('backupmessageshelp','message'), 0));
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_blogs', new lang_string('blogs', 'blog'), new lang_string('backupblogshelp','blog'), 0));
 
@@ -499,6 +509,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_contentbankcontent',
         new lang_string('generalcontentbankcontent', 'backup'),
         new lang_string('configrestorecontentbankcontent', 'backup'), array('value' => 1, 'locked' => 0)));
+    $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_legacyfiles',
+        new lang_string('generallegacyfiles', 'backup'),
+        new lang_string('configlegacyfiles', 'backup'), array('value' => 1, 'locked' => 0)));
 
     // Restore defaults when merging into another course.
     $temp->add(new admin_setting_heading('mergerestoredefaults', new lang_string('mergerestoredefaults', 'backup'), ''));
index af027de..98f5d91 100644 (file)
@@ -56,7 +56,7 @@ class behat_admin extends behat_base {
             $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', [get_string('administrationsite')]);
 
             // Search by label.
-            $this->execute('behat_forms::i_set_the_field_to', [get_string('query', 'admin'), $label]);
+            $this->execute('behat_forms::i_set_the_field_to', [get_string('search'), $label]);
             $this->execute("behat_forms::press_button", get_string('search', 'admin'));
 
             // Admin settings does not use the same DOM structure than other moodle forms
index 1c05534..ed3d79c 100644 (file)
Binary files a/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-debug.js and b/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-debug.js differ
index 6520ca2..30e97fc 100644 (file)
Binary files a/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js and b/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js differ
index 1c05534..ed3d79c 100644 (file)
Binary files a/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search.js and b/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search.js differ
index 1e4b96c..71bd152 100644 (file)
@@ -51,6 +51,13 @@ SEARCH.prototype = {
      * @protected
      */
     button: null,
+    /**
+     * The cancel button for the form.
+     * @property button
+     * @type Node
+     * @protected
+     */
+    cancel: null,
     /**
      * The last search node if there is one.
      * If there is a last search node then the last search term will be persisted between requests.
@@ -74,17 +81,28 @@ SEARCH.prototype = {
         this.button = this.form.all('input[type=submit]');
         this.lastsearch = this.form.one('input[name=search]');
 
-        var div = Y.Node.create('<div id="capabilitysearchui" data-fieldtype="text"></div>'),
-            label = Y.Node.create('<label for="capabilitysearch">' + this.get('strsearch') + '</label>');
-        this.input = Y.Node.create('<input type="text" id="capabilitysearch" />');
+        var div = Y.Node.create('<div id="capabilitysearchui" class="input-group simplesearchform mb-2"' +
+            'data-fieldtype="text"></div>'),
+            label = Y.Node.create('<label for="capabilitysearch"><span class="sr-only"' +
+                this.get('strsearch') + '</span></label>');
+        this.cancel = Y.Node.create('<a class="btn btn-clear d-none icon-no-margin">' +
+                '<i class="icon fa fa-times fa-fw " aria-hidden="true"></i>' +
+                '</a>');
+        this.input = Y.Node.create('<input type="text" class="form-control withclear" placeholder="' +
+            this.get('strsearch') + '"id="capabilitysearch" />');
 
-        div.append(label).append(this.input);
+        div.append(label).append(this.input).append(this.cancel);
 
         this.select.insert(div, 'before');
 
         this.input.on('keyup', this.typed, this);
         this.select.on('change', this.validate, this);
 
+        this.cancel.on('click', function() {
+            this.input.set('value', '');
+            this.typed();
+        }, this);
+
         if (this.lastsearch) {
             this.input.set('value', this.lastsearch.get('value'));
             this.typed();
@@ -131,6 +149,11 @@ SEARCH.prototype = {
                 last.set('selected', true);
             }
         }
+        if (search !== '') {
+            this.cancel.removeClass("d-none");
+        } else {
+            this.cancel.addClass("d-none");
+        }
         this.validate();
     }
 };
index bd61cc7..e4937f3 100644 (file)
@@ -26,7 +26,6 @@ namespace tool_lp\form;
 defined('MOODLE_INTERNAL') || die();
 
 use moodleform;
-use core\form\persistent;
 
 require_once($CFG->libdir . '/formslib.php');
 
@@ -39,17 +38,20 @@ require_once($CFG->libdir . '/formslib.php');
  */
 class template_cohorts extends moodleform {
 
+    /**
+     * Form definition
+     *
+     * @return void
+     */
     public function definition() {
         $mform = $this->_form;
 
         $options = array(
-            'ajax' => 'tool_lp/form-cohort-selector',
             'multiple' => true,
-            'data-contextid' => $this->_customdata['pagecontextid'],
-            'data-includes' => 'parents'
+            'exclude' => implode(',', $this->_customdata['excludecohorts']),
+            'contextid' => $this->_customdata['pagecontextid'],
         );
-        $mform->addElement('autocomplete', 'cohorts', get_string('selectcohortstosync', 'tool_lp'), array(), $options);
+        $mform->addElement('cohort', 'cohorts', get_string('selectcohortstosync', 'tool_lp'), $options);
         $mform->addElement('submit', 'submit', get_string('addcohorts', 'tool_lp'));
     }
-
 }
index 5667f3f..ad59ebe 100644 (file)
@@ -54,7 +54,19 @@ if ($canmanagetemplate && ($removecohort = optional_param('removecohort', false,
 }
 
 // Capture the form submission.
-$form = new \tool_lp\form\template_cohorts($url->out(false), array('pagecontextid' => $pagecontextid));
+$existingcohortsql =
+    'SELECT c.id
+       FROM {' . \core_competency\template_cohort::TABLE . '} tc
+       JOIN {cohort} c ON c.id = tc.cohortid
+      WHERE tc.templateid = :templateid';
+
+$existingcohorts = $DB->get_records_sql_menu($existingcohortsql, ['templateid' => $template->get('id')]);
+
+$form = new \tool_lp\form\template_cohorts($url->out(false), [
+    'pagecontextid' => $pagecontextid,
+    'excludecohorts' => array_keys($existingcohorts),
+]);
+
 if ($canmanagetemplate && ($data = $form->get_data()) && !empty($data->cohorts)) {
     $maxtocreate = 50;
     $maxreached = false;
index 006d917..0fd5f72 100644 (file)
@@ -488,6 +488,7 @@ class api {
                 '$mmSideMenuDelegate_mmaCompetency' => new lang_string('myplans', 'tool_lp'),
                 'CoreMainMenuDelegate_AddonBlog' => new lang_string('blog', 'blog'),
                 '$mmSideMenuDelegate_mmaFiles' => new lang_string('files'),
+                'CoreMainMenuDelegate_CoreTag' => new lang_string('tags'),
                 '$mmSideMenuDelegate_website' => new lang_string('webpage'),
                 '$mmSideMenuDelegate_help' => new lang_string('help'),
                 'CoreMainMenuDelegate_QrReader' => new lang_string('scanqrcode', 'tool_mobile'),
index ea1aac5..74f8c50 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/search.min.js and b/admin/tool/templatelibrary/amd/build/search.min.js differ
index 1611c7e..0245bb8 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/search.min.js.map and b/admin/tool/templatelibrary/amd/build/search.min.js.map differ
index 18623df..9caf344 100644 (file)
@@ -45,8 +45,13 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
      */
     var refreshSearch = function(themename) {
         var componentStr = $('[data-field="component"]').val();
-        var searchStr = $('[data-field="search"]').val();
+        var searchStr = $('[data-region="list-templates"] [data-region="input"]').val();
 
+        if (searchStr !== '') {
+            $('[data-region="list-templates"] [data-action="clearsearch"]').removeClass('d-none');
+        } else {
+            $('[data-region="list-templates"] [data-action="clearsearch"]').addClass('d-none');
+        }
         // Trigger the search.
         document.location.hash = searchStr;
 
@@ -84,9 +89,14 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
     };
     // Add change handlers to refresh the list.
     $('[data-region="list-templates"]').on('change', '[data-field="component"]', changeHandler);
-    $('[data-region="list-templates"]').on('input', '[data-field="search"]', changeHandler);
+    $('[data-region="list-templates"]').on('input', '[data-region="input"]', changeHandler);
+    $('[data-action="clearsearch"]').on('click', function() {
+        $('[data-region="input"]').val('');
+        refreshSearch(config.theme);
+        $(this).addClass('d-none');
+    });
 
-    $('[data-field="search"]').val(document.location.hash.replace('#', ''));
+    $('[data-region="input"]').val(document.location.hash.replace('#', ''));
     refreshSearch(config.theme);
     return {};
 });
index 0526c94..c5a454e 100644 (file)
     Context variables required for this template:
     * allcomponents - array of components containing templates. Each component has a name and a component attribute.
 
+}}
+{{!
+    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_templatelibrary/list_templates_page
+
+    Moodle template to the template library
+
+    The purpose of this template is build the entire page for the template library (by including smaller templates).
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * data-region, data-field
+
+    Context variables required for this template:
+    * allcomponents - array of components containing templates. Each component has a name and a component attribute.
+
 }}
 <div data-region="list-templates">
     <form class="form-horizontal">
-        <div class="control-group">
-            <label for="selectcomponent" class="control-label">{{#str}}component, tool_templatelibrary{{/str}}</label>
-            <div class="controls">
-                <select id="selectcomponent" data-field="component">
-                    <option value="">{{#str}}all, tool_templatelibrary{{/str}}</option>
-                    {{#allcomponents}}
-                        <option value="{{component}}">{{name}}</option>
-                    {{/allcomponents}}
-                </select>
-            </div>
-        </div>
-        <div class="control-group">
-            <label for="search" class="control-label">{{#str}}search, tool_templatelibrary{{/str}}</label>
-            <div class="controls">
-                <input type="text" id="search" data-field="search"/>
-            </div>
-        </div>
+    {{< core_form/element-template }}
+        {{$label}}
+            <div class="col-form-label">{{#str}}component, tool_templatelibrary{{/str}}</div>
+        {{/label}}
+
+        {{$element}}
+            <select id="selectcomponent" class="form-control" data-field="component">
+                <option value="">{{#str}}all, tool_templatelibrary{{/str}}</option>
+                {{#allcomponents}}
+                    <option value="{{component}}">{{name}}</option>
+                {{/allcomponents}}
+            </select>
+        {{/element}}
+    {{/ core_form/element-template }}
+
+    {{< core_form/element-template }}
+        {{$element}}
+            {{< core/search_input_auto }}
+                {{$label}}{{{ searchstring }}}{{/label}}
+                {{$placeholder}}{{#str}}
+                    search, core
+                {{/str}}{{/placeholder}}
+            {{/ core/search_input_auto }}
+        {{/element}}
+    {{/ core_form/element-template }}
     </form>
     <hr/>
     {{> tool_templatelibrary/search_results }}
index 4c4a201..0765ee3 100644 (file)
@@ -557,9 +557,10 @@ class tour {
 
         // Remove the configuration for the tour.
         $DB->delete_records('tool_usertours_tours', array('id' => $this->id));
-
         helper::reset_tour_sortorder();
 
+        $this->remove_user_preferences();
+
         return null;
     }
 
@@ -585,6 +586,16 @@ class tour {
         return $this;
     }
 
+    /**
+     * Remove stored user preferences for the tour
+     */
+    protected function remove_user_preferences(): void {
+        global $DB;
+
+        $DB->delete_records('user_preferences', ['name' => self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id()]);
+        $DB->delete_records('user_preferences', ['name' => self::TOUR_REQUESTED_BY_USER . $this->get_id()]);
+    }
+
     /**
      * Whether this tour should be displayed to the user.
      *
@@ -665,11 +676,9 @@ class tour {
      * @return  $this
      */
     public function mark_major_change() {
-        global $DB;
-
         // Clear old reset and completion notes.
-        $DB->delete_records('user_preferences', ['name' => self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id()]);
-        $DB->delete_records('user_preferences', ['name' => self::TOUR_REQUESTED_BY_USER . $this->get_id()]);
+        $this->remove_user_preferences();
+
         $this->set_config('majorupdatetime', time());
         $this->persist();
 
index 6769e01..e3c0fc9 100644 (file)
@@ -25,6 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 use tool_usertours\manager;
+use tool_usertours\tour;
 
 /**
  * Upgrade the user tours plugin.
@@ -57,5 +58,26 @@ function xmldb_tool_usertours_upgrade($oldversion) {
     // Automatically generated Moodle v3.9.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2020082700) {
+        // Clean up user preferences of deleted tours.
+        $select = $DB->sql_like('name', ':lastcompleted') . ' OR ' . $DB->sql_like('name', ':requested');
+        $params = [
+            'lastcompleted' => tour::TOUR_LAST_COMPLETED_BY_USER . '%',
+            'requested' => tour::TOUR_REQUESTED_BY_USER . '%',
+        ];
+
+        $preferences = $DB->get_records_select('user_preferences', $select, $params, '', 'DISTINCT name');
+        foreach ($preferences as $preference) {
+            // Match tour ID at the end of the preference name, remove all of that preference type if tour ID doesn't exist.
+            if (preg_match('/(?<tourid>\d+)$/', $preference->name, $matches) &&
+                    !$DB->record_exists('tool_usertours_tours', ['id' => $matches['tourid']])) {
+
+                $DB->delete_records('user_preferences', ['name' => $preference->name]);
+            }
+        }
+
+        upgrade_plugin_savepoint(true, 2020082700, 'tool', 'usertours');
+    }
+
     return true;
 }
index d08a9df..c5bf23f 100644 (file)
@@ -64,7 +64,7 @@ class tour_testcase extends advanced_testcase {
     /**
      * Helper to mock the database.
      *
-     * @return moodle_database
+     * @return \PHPUnit\Framework\MockObject\MockObject
      */
     public function mock_database() {
         global $DB;
@@ -569,9 +569,14 @@ class tour_testcase extends advanced_testcase {
 
         // Mock the database.
         $DB = $this->mock_database();
-        $DB->expects($this->once())
+
+        $DB->expects($this->exactly(3))
             ->method('delete_records')
-            ->with($this->equalTo('tool_usertours_tours'), $this->equalTo(['id' => $id]))
+            ->withConsecutive(
+                [$this->equalTo('tool_usertours_tours'), $this->equalTo(['id' => $id])],
+                [$this->equalTo('user_preferences'), $this->equalTo(['name' => tour::TOUR_LAST_COMPLETED_BY_USER . $id])],
+                [$this->equalTo('user_preferences'), $this->equalTo(['name' => tour::TOUR_REQUESTED_BY_USER . $id])]
+            )
             ->willReturn(null)
             ;
 
index c477167..e34cc47 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2020061501;            // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2020082700;            // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2020060900;            // Requires this Moodle version.
 $plugin->component = 'tool_usertours';      // Full name of the plugin (used for diagnostics).
index 4771c39..478ae2a 100644 (file)
@@ -184,5 +184,10 @@ class backup_root_task extends backup_task {
         $contentbank = new backup_contentbankcontent_setting('contentbankcontent', base_setting::IS_BOOLEAN, true);
         $contentbank->set_ui(new backup_setting_ui_checkbox($contentbank, get_string('rootsettingcontentbankcontent', 'backup')));
         $this->add_setting($contentbank);
+
+        // Define legacy file inclusion setting.
+        $legacyfiles = new backup_generic_setting('legacyfiles', base_setting::IS_BOOLEAN, true);
+        $legacyfiles->set_ui(new backup_setting_ui_checkbox($legacyfiles, get_string('rootsettinglegacyfiles', 'backup')));
+        $this->add_setting($legacyfiles);
     }
 }
index 8c84801..6a66e3f 100644 (file)
@@ -488,7 +488,10 @@ class backup_course_structure_step extends backup_structure_step {
 
         $course->annotate_files('course', 'summary', null);
         $course->annotate_files('course', 'overviewfiles', null);
-        $course->annotate_files('course', 'legacy', null);
+
+        if ($this->get_setting_value('legacyfiles')) {
+            $course->annotate_files('course', 'legacy', null);
+        }
 
         // Return root element ($course)
 
index be4f3f9..f39b766 100644 (file)
@@ -76,7 +76,9 @@ class restore_course_task extends restore_task {
             }
         }
 
-        $this->add_step(new restore_course_legacy_files_step('legacy_files'));
+        if ($this->get_setting_value('legacyfiles')) {
+            $this->add_step(new restore_course_legacy_files_step('legacy_files'));
+        }
 
         // Deal with enrolment methods and user enrolments.
         if ($this->plan->get_mode() == backup::MODE_IMPORT) {
index 632985f..3b37acc 100644 (file)
@@ -302,5 +302,13 @@ class restore_root_task extends restore_task {
         $contents->set_ui(new backup_setting_ui_checkbox($contents, get_string('rootsettingcontentbankcontent', 'backup')));
         $contents->get_ui()->set_changeable($changeable);
         $this->add_setting($contents);
+
+        // Include legacy files.
+        $defaultvalue = true;
+        $changeable = true;
+        $legacyfiles = new restore_generic_setting('legacyfiles', base_setting::IS_BOOLEAN, $defaultvalue);
+        $legacyfiles->set_ui(new backup_setting_ui_checkbox($legacyfiles, get_string('rootsettinglegacyfiles', 'backup')));
+        $legacyfiles->get_ui()->set_changeable($changeable);
+        $this->add_setting($legacyfiles);
     }
 }
index 7e9e325..2b28e74 100644 (file)
@@ -1881,6 +1881,11 @@ class restore_course_structure_step extends restore_structure_step {
             $data->idnumber = '';
         }
 
+        // If we restore a course from this site, let's capture the original course id.
+        if ($isnewcourse && $this->get_task()->is_samesite()) {
+            $data->originalcourseid = $this->get_task()->get_old_courseid();
+        }
+
         // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
         // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
         if (empty($data->hiddensections)) {
index 9e09f55..e5c0163 100644 (file)
@@ -566,6 +566,7 @@ abstract class backup_controller_dbops extends backup_dbops {
                         'backup_general_groups'             => 'groups',
                         'backup_general_competencies'       => 'competencies',
                         'backup_general_contentbankcontent' => 'contentbankcontent',
+                        'backup_general_legacyfiles'        => 'legacyfiles'
                 );
                 self::apply_admin_config_defaults($controller, $settings, true);
                 break;
@@ -580,6 +581,7 @@ abstract class backup_controller_dbops extends backup_dbops {
                         'backup_import_groups'             => 'groups',
                         'backup_import_competencies'       => 'competencies',
                         'backup_import_contentbankcontent' => 'contentbankcontent',
+                        'backup_import_legacyfiles'        => 'legacyfiles'
                 );
                 self::apply_admin_config_defaults($controller, $settings, true);
                 if ((!$controller->get_interactive()) &&
@@ -611,7 +613,8 @@ abstract class backup_controller_dbops extends backup_dbops {
                         'backup_auto_questionbank'       => 'questionbank',
                         'backup_auto_groups'             => 'groups',
                         'backup_auto_competencies'       => 'competencies',
-                        'backup_auto_contentbankcontent' => 'contentbankcontent'
+                        'backup_auto_contentbankcontent' => 'contentbankcontent',
+                        'backup_auto_legacyfiles'        => 'legacyfiles'
                 );
                 self::apply_admin_config_defaults($controller, $settings, false);
                 break;
index 148655b..216da03 100644 (file)
@@ -158,7 +158,8 @@ abstract class restore_controller_dbops extends restore_dbops {
             'restore_general_questionbank'       => 'questionbank',
             'restore_general_groups'             => 'groups',
             'restore_general_competencies'       => 'competencies',
-            'restore_general_contentbankcontent' => 'contentbankcontent'
+            'restore_general_contentbankcontent' => 'contentbankcontent',
+            'restore_general_legacyfiles'        => 'legacyfiles'
         );
         self::apply_admin_config_defaults($controller, $settings, true);
 
index d5b44ce..4a1f187 100644 (file)
@@ -720,7 +720,7 @@ class core_backup_renderer extends plugin_renderer_base {
     public function render_restore_course_search(restore_course_search $component) {
         $url = $component->get_url();
 
-        $output = html_writer::start_tag('div', array('class' => 'restore-course-search form-inline mb-1'));
+        $output = html_writer::start_tag('div', array('class' => 'restore-course-search mb-1'));
         $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
 
         $table = new html_table();
@@ -759,22 +759,14 @@ class core_backup_renderer extends plugin_renderer_base {
         $output .= html_writer::table($table);
         $output .= html_writer::end_tag('div');
 
-        $output .= html_writer::start_tag('div', array('class' => 'rcs-search'));
-        $attrs = array(
-            'type' => 'text',
-            'name' => restore_course_search::$VAR_SEARCH,
-            'value' => $component->get_search(),
-            'class' => 'form-control'
-        );
-        $output .= html_writer::empty_tag('input', $attrs);
-        $attrs = array(
-            'type' => 'submit',
-            'name' => 'searchcourses',
-            'value' => get_string('search'),
-            'class' => 'btn btn-secondary'
-        );
-        $output .= html_writer::empty_tag('input', $attrs);
-        $output .= html_writer::end_tag('div');
+        $data = [
+            'inform' => true,
+            'extraclasses' => 'rcs-search mb-3 w-25',
+            'inputname' => restore_course_search::$VAR_SEARCH,
+            'searchstring' => get_string('searchcourses'),
+            'query' => $component->get_search(),
+        ];
+        $output .= $this->output->render_from_template('core/search_input', $data);
 
         $output .= html_writer::end_tag('div');
         return $output;
@@ -880,7 +872,7 @@ class core_backup_renderer extends plugin_renderer_base {
     public function render_restore_category_search(restore_category_search $component) {
         $url = $component->get_url();
 
-        $output = html_writer::start_tag('div', array('class' => 'restore-course-search form-inline mb-1'));
+        $output = html_writer::start_tag('div', array('class' => 'restore-course-search mb-1'));
         $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
 
         $table = new html_table();
@@ -922,22 +914,14 @@ class core_backup_renderer extends plugin_renderer_base {
         $output .= html_writer::table($table);
         $output .= html_writer::end_tag('div');
 
-        $output .= html_writer::start_tag('div', array('class' => 'rcs-search'));
-        $attrs = array(
-            'type' => 'text',
-            'name' => restore_category_search::$VAR_SEARCH,
-            'value' => $component->get_search(),
-            'class' => 'form-control'
-        );
-        $output .= html_writer::empty_tag('input', $attrs);
-        $attrs = array(
-            'type' => 'submit',
-            'name' => 'searchcourses',
-            'value' => get_string('search'),
-            'class' => 'btn btn-secondary'
-        );
-        $output .= html_writer::empty_tag('input', $attrs);
-        $output .= html_writer::end_tag('div');
+        $data = [
+            'inform' => true,
+            'extraclasses' => 'rcs-search mb-3 w-25',
+            'inputname' => restore_category_search::$VAR_SEARCH,
+            'searchstring' => get_string('search'),
+            'query' => $component->get_search(),
+        ];
+        $output .= $this->output->render_from_template('core/search_input', $data);
 
         $output .= html_writer::end_tag('div');
         return $output;
index 7bc53b1..3c4d9c9 100644 (file)
@@ -50,7 +50,7 @@ class block_blog_menu extends block_base {
     }
 
     function get_content() {
-        global $CFG;
+        global $CFG, $OUTPUT;
 
         // detect if blog enabled
         if ($this->content !== NULL) {
@@ -98,15 +98,14 @@ class block_blog_menu extends block_base {
 
         // Prepare the footer for this block
         if (has_capability('moodle/blog:search', context_system::instance())) {
-            // Full-text search field
-            $form  = html_writer::tag('label', get_string('search', 'admin'), array('for' => 'blogsearchquery',
-                'class' => 'accesshide'));
-            $form .= html_writer::empty_tag('input', array('id' => 'blogsearchquery', 'class' => 'form-control mr-1',
-                'type' => 'text', 'name' => 'search'));
-            $form .= html_writer::empty_tag('input', array('type' => 'submit', 'class' => 'btn btn-secondary',
-                'value' => get_string('search')));
-            $this->content->footer = html_writer::tag('form', html_writer::tag('div', $form), array(
-                'class' => 'blogsearchform form-inline', 'method' => 'get', 'action' => new moodle_url('/blog/index.php')));
+
+            $data = [
+                'action' => new moodle_url('/blog/index.php'),
+                'inputname' => 'search',
+                'searchstring' => get_string('search', 'admin'),
+                'extraclasses' => 'mt-3'
+            ];
+            $this->content->footer = $OUTPUT->render_from_template('core/search_input', $data);
         } else {
             // No footer to display
             $this->content->footer = '';
index 9a342d2..586e00c 100644 (file)
@@ -206,7 +206,7 @@ Feature: Enable Block blog menu in an activity
     When I log in as "teacher1"
     And I am on "Course 1" course homepage
     And I follow "Test assignment 1"
-    And I set the field "blogsearchquery" to "First"
+    And I set the field "Search" to "First"
     And I press "Search"
     Then I should see "S1 First Blog"
     And I should see "S2 First Blog"
index 39821d2..11d9625 100644 (file)
@@ -183,7 +183,7 @@ Feature: Students can use block blog menu in a course
     And I log out
     When I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I set the field "blogsearchquery" to "First"
+    And I set the field "Search" to "First"
     And I press "Search"
     Then I should see "S1 First Blog"
     And I should see "S2 First Blog"
index d3d96b2..91892ae 100644 (file)
@@ -64,32 +64,17 @@ class block_globalsearch extends block_base {
             return $this->content;
         }
 
-        $url = new moodle_url('/search/index.php');
-        $this->content->footer .= html_writer::link($url, get_string('advancedsearch', 'search'));
+        $data = [
+            'action' => new moodle_url('/search/index.php'),
+            'inputname' => 'q',
+            'searchstring' => get_string('search'),
+        ];
 
-        $this->content->text  = html_writer::start_tag('div', array('class' => 'searchform'));
-        $this->content->text .= html_writer::start_tag('form', array('action' => $url->out()));
-        $this->content->text .= html_writer::start_tag('fieldset', array('action' => 'invisiblefieldset'));
-
-        // Input.
-        $this->content->text .= html_writer::tag('label', get_string('search', 'search'),
-            array('for' => 'searchform_search', 'class' => 'accesshide'));
-        $inputoptions = array('id' => 'searchform_search', 'name' => 'q', 'class' => 'form-control',
-            'type' => 'text', 'size' => '15');
-        $this->content->text .= html_writer::empty_tag('input', $inputoptions);
-
-        // Context id.
         if ($this->page->context && $this->page->context->contextlevel !== CONTEXT_SYSTEM) {
-            $this->content->text .= html_writer::empty_tag('input', ['type' => 'hidden',
-                    'name' => 'context', 'value' => $this->page->context->id]);
+            $data['hiddenfields'] = (object) ['name' => 'context', 'value' => $this->page->context->id];
         }
 
-        // Search button.
-        $this->content->text .= html_writer::tag('button', get_string('search', 'search'),
-            array('id' => 'searchform_button', 'type' => 'submit', 'title' => 'globalsearch', 'class' => 'btn btn-secondary'));
-        $this->content->text .= html_writer::end_tag('fieldset');
-        $this->content->text .= html_writer::end_tag('form');
-        $this->content->text .= html_writer::end_tag('div');
+        $this->content->text = $OUTPUT->render_from_template('core/search_input', $data);
 
         return $this->content;
     }
index 8213abe..76b41a0 100644 (file)
@@ -44,8 +44,6 @@ class search_form implements renderable, templatable {
     protected $courseid;
     /** @var moodle_url The form action URL. */
     protected $actionurl;
-    /** @var moodle_url The advanced search URL. */
-    protected $advancedsearchurl;
     /** @var help_icon The help icon. */
     protected $helpicon;
 
@@ -56,17 +54,17 @@ class search_form implements renderable, templatable {
      */
     public function __construct($courseid) {
         $this->courseid = $courseid;
-        $this->actionurl = new moodle_url('/mod/forum/search.php');
-        $this->advancedsearchurl = new moodle_url('/mod/forum/search.php', ['id' => $this->courseid]);
+        $this->actionurl = new moodle_url('/mod/forum/search.php', ['id' => $courseid]);
         $this->helpicon = new help_icon('search', 'core');
     }
 
     public function export_for_template(renderer_base $output) {
         $data = [
-            'actionurl' => $this->actionurl->out(false),
-            'courseid' => $this->courseid,
-            'advancedsearchurl' => $this->advancedsearchurl->out(false),
+            'action' => $this->actionurl,
             'helpicon' => $this->helpicon->export_for_template($output),
+            'hiddenfields' => (object) ['name' => 'id', 'value' => $this->courseid],
+            'inputname' => 'search',
+            'searchstring' => get_string('search')
         ];
         return $data;
     }
index b5f8133..320a546 100644 (file)
 
     Example context (json):
     {
-        "actionurl": "https://domain.example/mod/forum/search.php",
-        "courseid": "2",
-        "advancedsearchurl": "https://domain.example/mod/forum/search.php?id=2",
+        "action": "https://moodle.local/admin/search.php",
+        "inputname": "search",
+        "searchstring": "Search settings",
+        "value": "policy",
+        "hiddenfields": [
+            {
+                "name": "course",
+                "value": "11"
+            }
+        ],
         "helpicon": "<a class='btn'><i class='icon fa fa-question-circle'></i></a>"
     }
 }}
 <div class="searchform">
-    <form action="{{actionurl}}" class="form-inline">
-        <input type="hidden" name="id" value="{{courseid}}">
-        <div class="input-group w-100">
-            <label class="sr-only" for="searchform_search">{{#str}}search{{/str}}</label>
-            <input id="searchform_search" name="search" type="text" class="form-control" size="10">
-            <div class="input-group-append">
-                <button class="btn btn-secondary" id="searchform_button" type="submit">{{#str}}go{{/str}}</button>
-            </div>
-        </div>
-    </form>
+    {{>core/search_input}}
     <div class="mt-3">
-        <a href="{{advancedsearchurl}}">{{#str}}advancedsearch, block_search_forums{{/str}}</a>
+        <a href="{{action}}">{{#str}}advancedsearch, block_search_forums{{/str}}</a>
         {{#helpicon}}
             {{>core/help_icon}}
         {{/helpicon}}
index dfdeb6f..9444846 100644 (file)
@@ -29,9 +29,8 @@ Feature: The search forums block allows users to search for forum posts on cours
   Scenario: Use the search forum block in a course without any forum posts
     Given I log in as "student1"
     And I am on "Course 1" course homepage
-    When I set the following fields to these values:
-      | searchform_search | Moodle |
-    And I press "Go"
+    When I set the field "Search" to "Moodle"
+    And I press "Search"
     Then I should see "No posts"
 
   Scenario: Use the search forum block in a course with a hidden forum and search for posts
@@ -50,9 +49,8 @@ Feature: The search forums block allows users to search for forum posts on cours
     When I log in as "student1"
     And I am on "Course 1" course homepage
     And "Search forums" "block" should exist
-    And I set the following fields to these values:
-      | searchform_search | message |
-    And I press "Go"
+    When I set the field "Search" to "message"
+    And I press "Search"
     Then I should see "No posts"
 
   Scenario: Use the search forum block in a course and search for posts
@@ -65,7 +63,6 @@ Feature: The search forums block allows users to search for forum posts on cours
     When I log in as "student1"
     And I am on "Course 1" course homepage
     And "Search forums" "block" should exist
-    And I set the following fields to these values:
-      | searchform_search | message |
-    And I press "Go"
+    When I set the field "Search" to "message"
+    And I press "Search"
     Then I should see "My subject"
index 53f3ba9..31d8683 100644 (file)
@@ -17,15 +17,13 @@ Feature: The search forums block allows users to search for forum posts on front
   Scenario: Use the search forum block on the frontpage and search for posts as a user
     Given I log in as "student1"
     And I am on site homepage
-    When I set the following fields to these values:
-      | searchform_search | Moodle |
-    And I press "Go"
+    When I set the field "Search" to "Moodle"
+    And I press "Search"
     Then I should see "No posts"
 
   Scenario: Use the search forum block on the frontpage and search for posts as a guest
     Given I log in as "guest"
     And I am on site homepage
-    When I set the following fields to these values:
-      | searchform_search | Moodle |
-    And I press "Go"
+    When I set the field "Search" to "Moodle"
+    And I press "Search"
     Then I should see "No posts"
index bceefd8..3e92848 100644 (file)
@@ -144,11 +144,12 @@ class block_settings_renderer extends plugin_renderer_base {
 
     public function search_form(moodle_url $formtarget, $searchvalue) {
         $data = [
-                'action' => $formtarget->out(false),
-                'label' => get_string('searchinsettings', 'admin'),
-                'searchvalue' => $searchvalue
+            'action' => $formtarget,
+            'inputname' => 'query',
+            'searchstring' => get_string('searchinsettings', 'admin'),
+            'query' => $searchvalue
         ];
-        return $this->render_from_template('block_settings/search_form', $data);
+        return $this->render_from_template('core/search_input', $data);
     }
 
 }
index ad67635..2cf87a5 100644 (file)
@@ -18,10 +18,11 @@ Feature: Edit activities in social activities block
       | student1 | C1 | student |
 
   @javascript
-  Scenario: Edit name of acitivity in-place in social activities block
+  Scenario: Edit name of activity in-place in social activities block
     Given I log in as "user1"
     And I am on "Course 1" course homepage with editing mode on
-    And I set the field "Add an activity to section 'section 0'" to "Forum"
+    And I press "Add an activity or resource"
+    And I click on "Add a new Forum" "link" in the "Add an activity or resource" "dialogue"
     And I set the field "Forum name" to "My forum name"
     And I press "Save and return to course"
     And I click on "Edit title" "link" in the "My forum name" activity in social activities block
@@ -42,7 +43,8 @@ Feature: Edit activities in social activities block
     And I log in as "user1"
     And I am on "Course 1" course homepage with editing mode on
     And I add the "Recent activity" block
-    And I set the field "Add an activity to section 'section 0'" to "Forum"
+    And I press "Add an activity or resource"
+    And I click on "Add a new Forum" "link" in the "Add an activity or resource" "dialogue"
     And I set the field "Forum name" to "My forum name"
     And I press "Save and return to course"
     And "My forum name" activity in social activities block should have "Hide" editing icon
index 9abfe0e..a974377 100644 (file)
@@ -449,7 +449,8 @@ class cache_factory {
                         $definition = $instance->get_definition_by_id($id);
                         if (!$definition) {
                             throw new coding_exception('The requested cache definition does not exist.'. $id, $id);
-                        } else if (!$this->is_disabled()) {
+                        }
+                        if (!$this->is_disabled()) {
                             debugging('Cache definitions reparsed causing cache reset in order to locate definition.
                                 You should bump the version number to ensure definitions are reprocessed.', DEBUG_DEVELOPER);
                         }
index 2b86132..a557a3e 100644 (file)
@@ -223,11 +223,9 @@ class cache implements cache_loader {
         $this->storetype = get_class($store);
         $this->perfdebug = (!empty($CFG->perfdebug) and $CFG->perfdebug > 7);
         if ($loader instanceof cache_loader) {
-            $this->loader = $loader;
-            // Mark the loader as a sub (chained) loader.
-            $this->loader->set_is_sub_loader(true);
+            $this->set_loader($loader);
         } else if ($loader instanceof cache_data_source) {
-            $this->datasource = $loader;
+            $this->set_data_source($loader);
         }
         $this->definition->generate_definition_hash();
         $this->staticacceleration = $this->definition->use_static_acceleration();
@@ -237,6 +235,27 @@ class cache implements cache_loader {
         $this->hasattl = ($this->definition->get_ttl() > 0);
     }
 
+    /**
+     * Set the loader for this cache.
+     *
+     * @param   cache_loader $loader
+     */
+    protected function set_loader(cache_loader $loader): void {
+        $this->loader = $loader;
+
+        // Mark the loader as a sub (chained) loader.
+        $this->loader->set_is_sub_loader(true);
+    }
+
+    /**
+     * Set the data source for this cache.
+     *
+     * @param   cache_data_source $datasource
+     */
+    protected function set_data_source(cache_data_source $datasource): void {
+        $this->datasource = $datasource;
+    }
+
     /**
      * Used to inform the loader of its state as a sub loader, or as the top of the chain.
      *
index 2bcc1ca..88af42d 100644 (file)
@@ -49,7 +49,12 @@ class cache_disabled extends cache {
      * @param null $loader Unused.
      */
     public function __construct(cache_definition $definition, cache_store $store, $loader = null) {
-        // Nothing to do here.
+        if ($loader instanceof cache_data_source) {
+            // Set the data source to allow data sources to work when caching is entirely disabled.
+            $this->set_data_source($loader);
+        }
+
+        // No other features are handled.
     }
 
     /**
@@ -60,6 +65,10 @@ class cache_disabled extends cache {
      * @return bool
      */
     public function get($key, $strictness = IGNORE_MISSING) {
+        if ($this->get_datasource() !== false) {
+            return $this->get_datasource()->load_for_cache($key);
+        }
+
         return false;
     }
 
@@ -71,11 +80,11 @@ class cache_disabled extends cache {
      * @return array
      */
     public function get_many(array $keys, $strictness = IGNORE_MISSING) {
-        $return = array();
-        foreach ($keys as $key) {
-            $return[$key] = false;
+        if ($this->get_datasource() !== false) {
+            return $this->get_datasource()->load_many_for_cache($keys);
         }
-        return $return;
+
+        return array_combine($keys, array_fill(0, count($keys), false));
     }
 
     /**
@@ -129,7 +138,9 @@ class cache_disabled extends cache {
      * @return bool
      */
     public function has($key, $tryloadifpossible = false) {
-        return false;
+        $result = $this->get($key);
+
+        return $result !== false;
     }
 
     /**
@@ -138,7 +149,16 @@ class cache_disabled extends cache {
      * @return bool
      */
     public function has_all(array $keys) {
-        return false;
+        if (!$this->get_datasource()) {
+            return false;
+        }
+
+        foreach ($keys as $key) {
+            if (!$this->has($key)) {
+                return false;
+            }
+        }
+        return true;
     }
 
     /**
@@ -148,6 +168,12 @@ class cache_disabled extends cache {
      * @return bool
      */
     public function has_any(array $keys) {
+        foreach ($keys as $key) {
+            if ($this->has($key)) {
+                return true;
+            }
+        }
+
         return false;
     }
 
@@ -189,6 +215,11 @@ class cache_factory_disabled extends cache_factory {
      * @return cache_definition
      */
     public function create_definition($component, $area, $unused = null) {
+        $definition = parent::create_definition($component, $area);
+        if ($definition->has_data_source()) {
+            return $definition;
+        }
+
         return cache_definition::load_adhoc(cache_store::MODE_REQUEST, $component, $area);
     }
 
@@ -200,7 +231,11 @@ class cache_factory_disabled extends cache_factory {
      * @throws coding_exception
      */
     public function create_cache(cache_definition $definition) {
-        return new cache_disabled($definition, $this->create_dummy_store($definition));
+        $loader = null;
+        if ($definition->has_data_source()) {
+            $loader = $definition->get_data_source();
+        }
+        return new cache_disabled($definition, $this->create_dummy_store($definition), $loader);
     }
 
     /**
@@ -292,6 +327,15 @@ class cache_factory_disabled extends cache_factory {
         // Return the instance.
         return $this->configs[$class];
     }
+
+    /**
+     * Returns true if the cache API has been disabled.
+     *
+     * @return bool
+     */
+    public function is_disabled() {
+        return true;
+    }
 }
 
 /**
@@ -484,4 +528,4 @@ class cache_config_disabled extends cache_config_writer {
     public function set_definition_mappings($definition, $mappings) {
         // Nothing to do here.
     }
-}
\ No newline at end of file
+}
index a23672e..faafe51 100644 (file)
@@ -1335,15 +1335,13 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertInstanceOf('cache_config_disabled', $config);
 
         // Check we get the expected disabled caches.
-        $cache = cache::make('phpunit', 'disable');
+        $cache = cache::make('core', 'string');
         $this->assertInstanceOf('cache_disabled', $cache);
 
         // Test an application cache.
         $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'phpunit', 'disable');
         $this->assertInstanceOf('cache_disabled', $cache);
 
-        $this->assertFalse(file_exists($configfile));
-
         $this->assertFalse($cache->get('test'));
         $this->assertFalse($cache->set('test', 'test'));
         $this->assertFalse($cache->delete('test'));
@@ -1353,8 +1351,6 @@ class core_cache_testcase extends advanced_testcase {
         $cache = cache::make_from_params(cache_store::MODE_SESSION, 'phpunit', 'disable');
         $this->assertInstanceOf('cache_disabled', $cache);
 
-        $this->assertFalse(file_exists($configfile));
-
         $this->assertFalse($cache->get('test'));
         $this->assertFalse($cache->set('test', 'test'));
         $this->assertFalse($cache->delete('test'));
@@ -1364,8 +1360,6 @@ class core_cache_testcase extends advanced_testcase {
         $cache = cache::make_from_params(cache_store::MODE_REQUEST, 'phpunit', 'disable');
         $this->assertInstanceOf('cache_disabled', $cache);
 
-        $this->assertFalse(file_exists($configfile));
-
         $this->assertFalse($cache->get('test'));
         $this->assertFalse($cache->set('test', 'test'));
         $this->assertFalse($cache->delete('test'));
index 0341bf9..02d1434 100644 (file)
@@ -103,19 +103,21 @@ if ($editcontrols = cohort_edit_controls($context, $baseurl)) {
 }
 
 // Add search form.
-$search  = html_writer::start_tag('form', array('id'=>'searchcohortquery', 'method'=>'get', 'class' => 'form-inline search-cohort'));
-$search .= html_writer::start_div('mb-1');
-$search .= html_writer::label(get_string('searchcohort', 'cohort'), 'cohort_search_q', true,
-        array('class' => 'mr-1')); // No : in form labels!
-$search .= html_writer::empty_tag('input', array('id' => 'cohort_search_q', 'type' => 'text', 'name' => 'search',
-        'value' => $searchquery, 'class' => 'form-control mr-1'));
-$search .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('search', 'cohort'),
-        'class' => 'btn btn-secondary'));
-$search .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'contextid', 'value'=>$contextid));
-$search .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'showall', 'value'=>$showall));
-$search .= html_writer::end_div();
-$search .= html_writer::end_tag('form');
-echo $search;
+$hiddenfields = [
+    (object) ['name' => 'contextid', 'value' => $contextid],
+    (object) ['name' => 'showall', 'value' => $showall]
+];
+
+$data = [
+    'action' => new moodle_url('/cohort/index.php'),
+    'inputname' => 'search',
+    'searchstring' => get_string('search', 'cohort'),
+    'query' => $searchquery,
+    'hiddenfields' => $hiddenfields,
+    'extraclasses' => 'mb-3'
+];
+
+echo $OUTPUT->render_from_template('core/search_input', $data);
 
 // Output pagination bar.
 echo $OUTPUT->paging_bar($cohorts['totalcohorts'], $page, 25, $baseurl);
index be279e7..9f0c2a1 100644 (file)
@@ -55,7 +55,7 @@ class competency_framework_exporter extends \core\external\persistent_exporter {
         $context = $this->persistent->get_context();
         $competenciescount = 0;
         try {
-            api::count_competencies($filters);
+            $competenciescount = api::count_competencies($filters);
         } catch (\required_capability_exception $re) {
             $competenciescount = 0;
         }
index 16c80b0..8309c24 100644 (file)
Binary files a/contentbank/amd/build/search.min.js and b/contentbank/amd/build/search.min.js differ
index 0e06258..ae3bad7 100644 (file)
Binary files a/contentbank/amd/build/search.min.js.map and b/contentbank/amd/build/search.min.js.map differ
index 26786ec..5aba987 100644 (file)
Binary files a/contentbank/amd/build/selectors.min.js and b/contentbank/amd/build/selectors.min.js differ
index 2efa6a5..62be444 100644 (file)
Binary files a/contentbank/amd/build/selectors.min.js.map and b/contentbank/amd/build/selectors.min.js.map differ
index 2fd3317..4660de4 100644 (file)
Binary files a/contentbank/amd/build/sort.min.js and b/contentbank/amd/build/sort.min.js differ
index a1b8557..06c091c 100644 (file)
Binary files a/contentbank/amd/build/sort.min.js.map and b/contentbank/amd/build/sort.min.js.map differ
index e604abc..40a99a5 100644 (file)
@@ -80,8 +80,7 @@ const registerListenerEvents = (root) => {
  * @param {String} searchQuery The search query.
  */
 const toggleSearchResultsView = async(body, searchQuery) => {
-    const clearSearchButton = body.find(selectors.elements.clearsearch)[0];
-    const searchIcon = body.find(selectors.elements.searchicon)[0];
+    const clearSearchButton = body.find(selectors.actions.clearSearch)[0];
 
     const navbarBreadcrumb = body.find(selectors.elements.cbnavbarbreadcrumb)[0];
     const navbarTotal = body.find(selectors.elements.cbnavbartotalsearch)[0];
@@ -91,7 +90,6 @@ const toggleSearchResultsView = async(body, searchQuery) => {
         // As the search query is present, search results should be displayed.
 
         // Display the "clear" search button in the activity chooser search bar.
-        searchIcon.classList.add('d-none');
         clearSearchButton.classList.remove('d-none');
 
         // Change the cb-navbar to display total items found.
@@ -103,7 +101,6 @@ const toggleSearchResultsView = async(body, searchQuery) => {
 
         // Hide the "clear" search button in the activity chooser search bar.
         clearSearchButton.classList.add('d-none');
-        searchIcon.classList.remove('d-none');
 
         // Display again the breadcrumb in the navbar.
         navbarBreadcrumb.classList.remove('d-none');
index 60fe955..f6de1b2 100644 (file)
@@ -42,7 +42,7 @@ export default {
     },
     actions: {
         search: getDataSelector('action', 'searchcontent'),
-        clearSearch: getDataSelector('action', 'clearsearchcontent'),
+        clearSearch: getDataSelector('action', 'clearsearch'),
         viewgrid: getDataSelector('action', 'viewgrid'),
         viewlist: getDataSelector('action', 'viewlist'),
         sortname: getDataSelector('action', 'sortname'),
@@ -55,8 +55,6 @@ export default {
         listitem: '.cb-listitem',
         cbnavbarbreadcrumb: '.cb-navbar-breadbrumb',
         cbnavbartotalsearch: '.cb-navbar-totalsearch',
-        clearsearch: '.input-group-append .clear-icon',
-        searchicon: '.input-group-append .search-icon',
         searchinput: '#searchinput',
         sortbutton: '.cb-btnsort'
     },
index 34c282b..ed67de5 100644 (file)
@@ -48,63 +48,78 @@ export const init = () => {
  */
 const registerListenerEvents = (contentBank) => {
 
-    // The search.
-    const fileArea = document.querySelector(selectors.regions.filearea);
-    const shownItems = fileArea.querySelectorAll(selectors.elements.listitem);
-
-    // The view buttons.
-    const viewGrid = contentBank.querySelector(selectors.actions.viewgrid);
-    const viewList = contentBank.querySelector(selectors.actions.viewlist);
-
-    viewGrid.addEventListener('click', () => {
-        contentBank.classList.remove('view-list');
-        contentBank.classList.add('view-grid');
-        viewGrid.classList.add('active');
-        viewList.classList.remove('active');
-        setViewListPreference(false);
-    });
-
-    viewList.addEventListener('click', () => {
-        contentBank.classList.remove('view-grid');
-        contentBank.classList.add('view-list');
-        viewList.classList.add('active');
-        viewGrid.classList.remove('active');
-        setViewListPreference(true);
-    });
-
-    // Sort by file name alphabetical
-    const sortByName = contentBank.querySelector(selectors.actions.sortname);
-    sortByName.addEventListener('click', () => {
-        const ascending = updateSortButtons(contentBank, sortByName);
-        updateSortOrder(fileArea, shownItems, 'data-file', ascending);
-    });
-
-    // Sort by date.
-    const sortByDate = contentBank.querySelector(selectors.actions.sortdate);
-    sortByDate.addEventListener('click', () => {
-        const ascending = updateSortButtons(contentBank, sortByDate);
-        updateSortOrder(fileArea, shownItems, 'data-timemodified', ascending);
-    });
+    contentBank.addEventListener('click', e => {
+        const viewList = contentBank.querySelector(selectors.actions.viewlist);
+        const viewGrid = contentBank.querySelector(selectors.actions.viewgrid);
+
+        // View as Grid button.
+        if (e.target.closest(selectors.actions.viewgrid)) {
+            contentBank.classList.remove('view-list');
+            contentBank.classList.add('view-grid');
+            viewGrid.classList.add('active');
+            viewList.classList.remove('active');
+            setViewListPreference(false);
+
+            return;
+        }
 
-    // Sort by size.
-    const sortBySize = contentBank.querySelector(selectors.actions.sortsize);
-    sortBySize.addEventListener('click', () => {
-        const ascending = updateSortButtons(contentBank, sortBySize);
-        updateSortOrder(fileArea, shownItems, 'data-bytes', ascending);
-    });
+        // View as List button.
+        if (e.target.closest(selectors.actions.viewlist)) {
+            contentBank.classList.remove('view-grid');
+            contentBank.classList.add('view-list');
+            viewList.classList.add('active');
+            viewGrid.classList.remove('active');
+            setViewListPreference(true);
 
-    // Sort by type.
-    const sortByType = contentBank.querySelector(selectors.actions.sorttype);
-    sortByType.addEventListener('click', () => {
-        const ascending = updateSortButtons(contentBank, sortByType);
-        updateSortOrder(fileArea, shownItems, 'data-type', ascending);
-    });
+            return;
+        }
 
-    // Sort by author.
-    const sortByAuthor = contentBank.querySelector(selectors.actions.sortauthor);
-    sortByAuthor.addEventListener('click', () => {
-        const ascending = updateSortButtons(contentBank, sortByAuthor);
-        updateSortOrder(fileArea, shownItems, 'data-author', ascending);
+        // TODO: This should _not_ use `document`. Every query should be constrained to the content bank container.
+        const fileArea = document.querySelector(selectors.regions.filearea);
+        const shownItems = fileArea.querySelectorAll(selectors.elements.listitem);
+
+        if (fileArea && shownItems) {
+
+            // Sort by file name alphabetical
+            const sortByName = e.target.closest(selectors.actions.sortname);
+            if (sortByName) {
+                const ascending = updateSortButtons(contentBank, sortByName);
+                updateSortOrder(fileArea, shownItems, 'data-file', ascending);
+                return;
+            }
+
+            // Sort by date.
+            const sortByDate = e.target.closest(selectors.actions.sortdate);
+            if (sortByDate) {
+                const ascending = updateSortButtons(contentBank, sortByDate);
+                updateSortOrder(fileArea, shownItems, 'data-timemodified', ascending);
+                return;
+            }
+
+            // Sort by size.
+            const sortBySize = e.target.closest(selectors.actions.sortsize);
+            if (sortBySize) {
+                const ascending = updateSortButtons(contentBank, sortBySize);
+                updateSortOrder(fileArea, shownItems, 'data-bytes', ascending);
+                return;
+            }
+
+            // Sort by type.
+            const sortByType = e.target.closest(selectors.actions.sorttype);
+            if (sortByType) {
+                const ascending = updateSortButtons(contentBank, sortByType);
+                updateSortOrder(fileArea, shownItems, 'data-type', ascending);
+                return;
+            }
+
+            // Sort by author.
+            const sortByAuthor = e.target.closest(selectors.actions.sortauthor);
+            if (sortByAuthor) {
+                const ascending = updateSortButtons(contentBank, sortByAuthor);
+                updateSortOrder(fileArea, shownItems, 'data-author', ascending);
+            }
+            return;
+        }
     });
 };
 
index a62beff..f5c800e 100644 (file)
@@ -95,87 +95,96 @@ data-region="contentbank">
                 <div class="cb-navbar-totalsearch d-none">
                 </div>
             </div>
-            <div class="cb-content-wrapper d-flex px-2" data-region="filearea">
-                <div class="cb-heading bg-white">
-                    <div class="cb-file cb-column d-flex">
-                        <div class="title">{{#str}} contentname, contentbank {{/str}}</div>
-                        <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="contentname" data-action="sortname"
-                            title="{{#str}} sortbyx, core, {{#str}} contentname, contentbank {{/str}} {{/str}}">
-                            <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
-                            <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
-                            <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
-                        </button>
-                    </div>
-                    <div class="cb-date cb-column d-flex">
-                        <div class="title">{{#str}} lastmodified, contentbank {{/str}}</div>
-                        <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="lastmodified" data-action="sortdate"
-                        title="{{#str}} sortbyx, core, {{#str}} lastmodified, contentbank {{/str}} {{/str}}">
-                            <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
-                            <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
-                            <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
-                        </button>
-                    </div>
-                    <div class="cb-size cb-column d-flex">
-                        <div class="title">{{#str}} size, contentbank {{/str}}</div>
-                        <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="size" data-action="sortsize"
-                        title="{{#str}} sortbyx, core, {{#str}} size, contentbank {{/str}} {{/str}}">
-                            <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
-                            <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
-                            <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
-                        </button>
-                    </div>
-                    <div class="cb-type cb-column d-flex">
-                        <div class="title">{{#str}} type, contentbank {{/str}}</div>
-                        <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="type" data-action="sorttype"
-                        title="{{#str}} sortbyx, core, {{#str}} type, contentbank {{/str}} {{/str}}">
-                            <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
-                            <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
-                            <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
-                        </button>
-                    </div>
-                    <div class="cb-author cb-column d-flex last">
-                        <div class="title">{{#str}} author, contentbank {{/str}}</div>
-                        <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="author" data-action="sortauthor"
-                        title="{{#str}} sortbyx, core, {{#str}} author, contentbank {{/str}} {{/str}}">
-                            <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
-                            <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
-                            <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
-                        </button>
-                    </div>
-                </div>
-            {{#contents}}
-                <div class="cb-listitem"
-                    data-file="{{{ title }}}"
-                    data-name="{{{ name }}}"
-                    data-bytes="{{ bytes }}"
-                    data-timemodified="{{ timemodified }}"
-                    data-type="{{{ type }}}"
-                    data-author="{{{ author }}}">
-                    <div class="cb-file cb-column position-relative">
-                        <div class="cb-thumbnail" role="img" aria-label="{{{ name }}}"
-                        style="background-image: url('{{{ icon }}}');">
+            {{#contents.0}}
+                <div class="cb-content-wrapper d-flex px-2" data-region="filearea">
+                    <div class="cb-heading bg-white">
+                        <div class="cb-file cb-column d-flex">
+                            <div class="title">{{#str}} contentname, contentbank {{/str}}</div>
+                            <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="contentname" data-action="sortname"
+                                title="{{#str}} sortbyx, core, {{#str}} contentname, contentbank {{/str}} {{/str}}">
+                                <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
+                                <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
+                                <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
+                            </button>
+                        </div>
+                        <div class="cb-date cb-column d-flex">
+                            <div class="title">{{#str}} lastmodified, contentbank {{/str}}</div>
+                            <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="lastmodified" data-action="sortdate"
+                            title="{{#str}} sortbyx, core, {{#str}} lastmodified, contentbank {{/str}} {{/str}}">
+                                <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
+                                <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
+                                <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
+                            </button>
+                        </div>
+                        <div class="cb-size cb-column d-flex">
+                            <div class="title">{{#str}} size, contentbank {{/str}}</div>
+                            <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="size" data-action="sortsize"
+                            title="{{#str}} sortbyx, core, {{#str}} size, contentbank {{/str}} {{/str}}">
+                                <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
+                                <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
+                                <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
+                            </button>
+                        </div>
+                        <div class="cb-type cb-column d-flex">
+                            <div class="title">{{#str}} type, contentbank {{/str}}</div>
+                            <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="type" data-action="sorttype"
+                            title="{{#str}} sortbyx, core, {{#str}} type, contentbank {{/str}} {{/str}}">
+                                <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
+                                <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
+                                <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
+                            </button>
+                        </div>
+                        <div class="cb-author cb-column d-flex last">
+                            <div class="title">{{#str}} author, contentbank {{/str}}</div>
+                            <button class="btn btn-sm cb-btnsort dir-none ml-auto" data-string="author" data-action="sortauthor"
+                            title="{{#str}} sortbyx, core, {{#str}} author, contentbank {{/str}} {{/str}}">
+                                <span class="default">{{#pix}} t/sort, core, {{#str}}sort, core {{/str}} {{/pix}}</span>
+                                <span class="desc">{{#pix}} t/sort_desc, core, {{#str}}desc, core{{/str}} {{/pix}}</span>
+                                <span class="asc">{{#pix}} t/sort_asc, core, {{#str}}asc, core{{/str}} {{/pix}}</span>
+                            </button>
                         </div>
-                        <a href="{{{ link }}}" class="cb-link stretched-link" title="{{{ name }}}">
-                            <span class="cb-name word-break-all clamp-2" data-region="cb-content-name">
-                                {{{ name }}}
-                            </span>
-                        </a>
-                    </div>
-                    <div class="cb-date cb-column small">
-                        {{#userdate}} {{ timemodified }}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}} {{/userdate}}
-                    </div>
-                    <div class="cb-size cb-column small">
-                        {{ size }}
                     </div>
-                    <div class="cb-type cb-column small">
-                        {{{ type }}}
+                {{#contents}}
+                    <div class="cb-listitem"
+                        data-file="{{{ title }}}"
+                        data-name="{{{ name }}}"
+                        data-bytes="{{ bytes }}"
+                        data-timemodified="{{ timemodified }}"
+                        data-type="{{{ type }}}"
+                        data-author="{{{ author }}}">
+                        <div class="cb-file cb-column position-relative">
+                            <div class="cb-thumbnail" role="img" aria-label="{{{ name }}}"
+                            style="background-image: url('{{{ icon }}}');">
+                            </div>
+                            <a href="{{{ link }}}" class="cb-link stretched-link" title="{{{ name }}}">
+                                <span class="cb-name word-break-all clamp-2" data-region="cb-content-name">
+                                    {{{ name }}}
+                                </span>
+                            </a>
+                        </div>
+                        <div class="cb-date cb-column small">
+                            {{#userdate}} {{ timemodified }}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}} {{/userdate}}
+                        </div>
+                        <div class="cb-size cb-column small">
+                            {{ size }}
+                        </div>
+                        <div class="cb-type cb-column small">
+                            {{{ type }}}
+                        </div>
+                        <div class="cb-type cb-column last small">
+                            {{{ author }}}
+                        </div>
                     </div>
-                    <div class="cb-type cb-column last small">
-                        {{{ author }}}
+                {{/contents}}
+                </div>
+            {{/contents.0}}
+            {{^contents.0}}
+                <div class="cb-content-wrapper d-flex flex-wrap p-2" data-region="filearea">
+                    <div class="w-100 p-3 text-center text-muted">
+                        {{#str}} nocontentavailable, core_contentbank {{/str}}
                     </div>
                 </div>
-            {{/contents}}
-            </div>
+            {{/contents.0}}
         </div>
     </div>
 </div>
index 8d02863..6fc2e58 100644 (file)
     @template core_contentbank/bankcontent/search
 
     Example context (json):
-    {}
+    {
+    }
 
 }}
-<div class="searchbar input-group" role="search">
-    <label for="searchinput">
-        <span class="sr-only">{{#str}} searchcontentbankbyname, contentbank {{/str}}</span>
-    </label>
-    <input type="text"
-           id="searchinput"
-           class="form-control searchinput border-right-0"
-           placeholder="{{#str}} search, core {{/str}}"
-           name="search"
-           autocomplete="off"
-    >
-    <div class="input-group-append">
-        <div class="input-group-text bg-transparent">
-            <div class="search-icon">
-                <button class="btn p-0 align-baseline icon-no-margin" data-action="searchcontent"
-                    aria-label="{{#str}} search, core {{/str}}">
-                    <span class="d-flex" aria-hidden="true">{{#pix}} a/search, core {{/pix}}</span>
-                </button>
-            </div>
-            <div class="clear-icon d-none">
-                <button class="btn p-0 align-baseline icon-no-margin" data-action="clearsearchcontent"
-                    aria-label="{{#str}} clearsearch, core {{/str}}">
-                    <span class="d-flex" aria-hidden="true">{{#pix}} e/cancel_solid_circle, core {{/pix}}</span>
-                </button>
-            </div>
-        </div>
-    </div>
-</div>
\ No newline at end of file
+
+{{< core/search_input_auto }}
+    {{$label}}{{#str}}
+        searchcontentbankbyname, contentbank
+    {{/str}}{{/label}}
+    {{$placeholder}}{{#str}}
+        search, core
+    {{/str}}{{/placeholder}}
+{{/ core/search_input_auto }}
index dbb33f4..d517d5b 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index 5c2bb8f..740fe3f 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index 0b4a6ed..45f8f4f 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js and b/course/amd/build/local/activitychooser/selectors.min.js differ
index 177b53e..13f9e9f 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js.map and b/course/amd/build/local/activitychooser/selectors.min.js.map differ
index cd045a3..a3bde76 100644 (file)
@@ -466,8 +466,7 @@ const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {
     const modalBody = modal.getBody()[0];
     const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);
     const chooserContainer = modalBody.querySelector(selectors.regions.chooser);
-    const clearSearchButton = modalBody.querySelector(selectors.elements.clearsearch);
-    const searchIcon = modalBody.querySelector(selectors.elements.searchicon);
+    const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);
 
     if (searchQuery.length > 0) { // Search query is present.
         const searchResultsData = searchModules(mappedModules, searchQuery);
@@ -481,7 +480,6 @@ const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {
             initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);
         }
         // Display the "clear" search button in the activity chooser search bar.
-        searchIcon.classList.add('d-none');
         clearSearchButton.classList.remove('d-none');
         // Hide the default chooser options container.
         chooserContainer.setAttribute('hidden', 'hidden');
@@ -490,7 +488,6 @@ const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {
     } else { // Search query is not present.
         // Hide the "clear" search button in the activity chooser search bar.
         clearSearchButton.classList.add('d-none');
-        searchIcon.classList.remove('d-none');
         // Hide the search results container.
         searchResultsContainer.setAttribute('hidden', 'hidden');
         // Display the default chooser options container.
index 6b54bbc..dd20e75 100644 (file)
@@ -86,8 +86,6 @@ export default {
         sitetopic: 'div.sitetopic',
         tab: 'a[data-toggle="tab"]',
         activetab: 'a[data-toggle="tab"][aria-selected="true"]',
-        visibletabs: 'a[data-toggle="tab"]:not(.d-none)',
-        searchicon: '.input-group-append .search-icon',
-        clearsearch: '.input-group-append .clear'
+        visibletabs: 'a[data-toggle="tab"]:not(.d-none)'
     },
 };
index 25340f8..3de3c7a 100644 (file)
@@ -1291,56 +1291,19 @@ class core_course_management_renderer extends plugin_renderer_base {
      * Renders html to display a course search form
      *
      * @param string $value default value to populate the search field
-     * @param string $format display format - 'plain' (default), 'short' or 'navbar'
      * @return string
      */
-    public function course_search_form($value = '', $format = 'plain') {
-        static $count = 0;
-        $formid = 'coursesearch';
-        if ((++$count) > 1) {
-            $formid .= $count;
-        }
-
-        switch ($format) {
-            case 'navbar' :
-                $formid = 'coursesearchnavbar';
-                $inputid = 'navsearchbox';
-                $inputsize = 20;
-                break;
-            case 'short' :
-                $inputid = 'shortsearchbox';
-                $inputsize = 12;
-                break;
-            default :
-                $inputid = 'coursesearchbox';
-                $inputsize = 30;
-        }
-
-        $strsearchcourses = get_string("searchcourses");
-        $searchurl = new moodle_url('/course/management.php');
-
-        $output = html_writer::start_div('row');
-        $output .= html_writer::start_div('col-md-12');
-        $output .= html_writer::start_tag('form', array('class' => 'card', 'id' => $formid,
-                'action' => $searchurl, 'method' => 'get'));
-        $output .= html_writer::start_tag('fieldset', array('class' => 'coursesearchbox invisiblefieldset'));
-        $output .= html_writer::tag('legend', $this->output->heading($strsearchcourses.': ', 2, 'm-0'),
-                array('class' => 'card-header'));
-        $output .= html_writer::start_div('card-body');
-        $output .= html_writer::start_div('input-group col-sm-6 col-lg-4 m-auto');
-        $output .= html_writer::empty_tag('input', array('class' => 'form-control', 'type' => 'text', 'id' => $inputid,
-                'size' => $inputsize, 'name' => 'search', 'value' => s($value), 'aria-label' => get_string('searchcourses')));
-        $output .= html_writer::start_tag('span', array('class' => 'input-group-btn'));
-        $output .= html_writer::tag('button', get_string('go'), array('class' => 'btn btn-primary', 'type' => 'submit'));
-        $output .= html_writer::end_tag('span');
-        $output .= html_writer::end_div();
-        $output .= html_writer::end_div();
-        $output .= html_writer::end_tag('fieldset');
-        $output .= html_writer::end_tag('form');
-        $output .= html_writer::end_div();
-        $output .= html_writer::end_div();
-
-        return $output;
+    public function course_search_form($value = '') {
+
+        $data = [
+            'action' => new moodle_url('/course/management.php'),
+            'btnclass' => 'btn-primary',
+            'extraclasses' => 'my-3 d-flex justify-content-center',
+            'inputname' => 'search',
+            'searchstring' => get_string('searchcourses'),
+            'value' => $value
+        ];
+        return $this->render_from_template('core/search_input', $data);
     }
 
     /**
index 972f30c..088719a 100644 (file)
@@ -131,4 +131,19 @@ class format_social extends format_base {
         // Return everything (nothing to hide).
         return $this->get_format_options();
     }
+
+    /**
+     * Returns the information about the ajax support in the given source format.
+     *
+     * The returned object's property (boolean)capable indicates that
+     * the course format supports Moodle course ajax features.
+     *
+     * @return stdClass
+     */
+    public function supports_ajax() {
+        $ajaxsupport = new stdClass();
+        $ajaxsupport->capable = true;
+        return $ajaxsupport;
+    }
+
 }
index ebb0e4a..948497e 100644 (file)
@@ -2,6 +2,9 @@ This files describes API changes for course formats
 
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
+=== 3.10 ===
+* Added the missing callback supports_ajax() to format_social.
+
 === 3.9 ===
 
 * The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info:
index 39ad5a7..5afc685 100644 (file)
@@ -487,6 +487,9 @@ if (count($notificationsfail) > 0) {
 }
 
 // Start the management form.
+
+echo $renderer->course_search_form($search);
+
 echo $renderer->management_form_start();
 
 echo $renderer->accessible_skipto_links($displaycategorylisting, $displaycourselisting, $displaycoursedetail);
@@ -518,6 +521,5 @@ echo $renderer->grid_end();
 
 // End of the management form.
 echo $renderer->management_form_end();
-echo $renderer->course_search_form($search);
 
 echo $renderer->footer();
index da82415..89b7e0c 100644 (file)
@@ -399,48 +399,22 @@ class core_course_renderer extends plugin_renderer_base {
     }
 
     /**
-     * Renders html to display a course search form.
+     * Renders html to display a course search form
      *
      * @param string $value default value to populate the search field
-     * @param string $format display format - 'plain' (default), 'short' or 'navbar'
      * @return string
      */
-    public function course_search_form($value = '', $format = 'plain') {
-        static $count = 0;
-        $formid = 'coursesearch';
-        if ((++$count) > 1) {
-            $formid .= $count;
-        }
-
-        switch ($format) {
-            case 'navbar' :
-                $formid = 'coursesearchnavbar';
-                $inputid = 'navsearchbox';
-                $inputsize = 20;
-                break;
-            case 'short' :
-                $inputid = 'shortsearchbox';
-                $inputsize = 12;
-                break;
-            default :
-                $inputid = 'coursesearchbox';
-                $inputsize = 30;
-        }
-
-        $data = new stdClass();
-        $data->searchurl = \core_search\manager::get_course_search_url()->out(false);
-        $data->id = $formid;
-        $data->inputid = $inputid;
-        $data->inputsize = $inputsize;
-        $data->value = $value;
-        $data->areaids = 'core_course-course';
+    public function course_search_form($value = '') {
 
-        if ($format != 'navbar') {
-            $helpicon = new \help_icon('coursesearch', 'core');
-            $data->helpicon = $helpicon->export_for_template($this);
-        }
-
-        return $this->render_from_template('core_course/course_search_form', $data);
+        $data = [
+            'action' => \core_search\manager::get_course_search_url(),
+            'btnclass' => 'btn-primary',
+            'inputname' => 'q',
+            'searchstring' => get_string('searchcourses'),
+            'hiddenfields' => (object) ['name' => 'areaids', 'value' => 'core_course-course'],
+            'query' => $value
+        ];
+        return $this->render_from_template('core/search_input', $data);
     }
 
     /**
@@ -1890,6 +1864,13 @@ class core_course_renderer extends plugin_renderer_base {
     public function search_courses($searchcriteria) {
         global $CFG;
         $content = '';
+
+        $search = '';
+        if (!empty($searchcriteria['search'])) {
+            $search = $searchcriteria['search'];
+        }
+        $content .= $this->course_search_form($search);
+
         if (!empty($searchcriteria)) {
             // print search results
 
@@ -1931,18 +1912,6 @@ class core_course_renderer extends plugin_renderer_base {
                 $content .= $this->heading(get_string('searchresults'). ": $totalcount");
                 $content .= $courseslist;
             }
-
-            if (!empty($searchcriteria['search'])) {
-                // print search form only if there was a search by search string, otherwise it is confusing
-                $content .= $this->box_start('generalbox mdl-align');
-                $content .= $this->course_search_form($searchcriteria['search']);
-                $content .= $this->box_end();
-            }
-        } else {
-            // just print search form
-            $content .= $this->box_start('generalbox mdl-align');
-            $content .= $this->course_search_form();
-            $content .= $this->box_end();
         }
         return $content;
     }
@@ -2464,7 +2433,7 @@ class core_course_renderer extends plugin_renderer_base {
                     break;
 
                 case FRONTPAGECOURSESEARCH:
-                    $output .= $this->box($this->course_search_form('', 'short'), 'mdl-align');
+                    $output .= $this->box($this->course_search_form(''), 'd-flex justify-content-center');
                     break;
 
             }
index 144767d..ccaf4b3 100644 (file)
@@ -95,7 +95,7 @@ if (empty($searchcriteria)) {
         $aurl = new moodle_url('/course/management.php', $searchcriteria);
         $searchform = $OUTPUT->single_button($aurl, get_string('managecourses'), 'get');
     } else {
-        $searchform = $courserenderer->course_search_form($search, 'navbar');
+        $searchform = $courserenderer->course_search_form($search);
     }
     $PAGE->set_button($searchform);
 
index 9283980..70dfac6 100644 (file)
     Example context (json):
     {}
 }}
-<div class="searchbar input-group" role="search">
-    <label for="searchinput">
-        <span class="sr-only">{{#str}} searchactivities, core {{/str}}</span>
-    </label>
-    <input type="text"
-           data-action="search"
-           id="searchinput"
-           class="form-control searchinput h-auto border-right-0 rounded-left px-3 py-2"
-           placeholder="{{#str}} search, core {{/str}}"
-           name="search"
-           autocomplete="off"
-    >
-    <div class="input-group-append">
-        <div class="input-group-text border-left-0 rounded-right bg-transparent px-3 py-2">
-            <div class="search-icon">
-                {{#pix}} a/search, core {{/pix}}
-            </div>
-            <div class="clear d-none">
-                <button class="btn p-0" data-action="clearsearch">
-                    <span class="d-flex" aria-hidden="true">{{#pix}} e/cancel_solid_circle, core {{/pix}}</span>
-                    <span class="sr-only">{{#str}} clearsearch, core {{/str}}</span>
-                </button>
-            </div>
-        </div>
-    </div>
-</div>
+{{< core/search_input_auto }}
+    {{$label}}{{#str}}
+        searchactivities, core
+    {{/str}}{{/label}}
+    {{$placeholder}}{{#str}}
+        search, core
+    {{/str}}{{/placeholder}}
+{{/ core/search_input_auto }}
+
 <div class="searchresultscontainer" data-region="search-results-container" hidden="hidden" aria-live="polite">
 </div>
index 635bc2d..bd5c99c 100644 (file)
@@ -20,8 +20,8 @@ Feature: Courses can be searched for and moved in bulk.
   Scenario: Search courses finds correct results
     Given I log in as "admin"
     And I go to the courses management page
-    When I set the field "coursesearchbox" to "Biology"
-    And I press "Go"
+    When I set the field "Search" to "Biology"
+    And I press "Search"
     Then I should see "Biology Y1"
     And I should see "Biology Y2"
     And I should not see "English Y1"
@@ -31,8 +31,8 @@ Feature: Courses can be searched for and moved in bulk.
   Scenario: Search courses and move results in bulk
     Given I log in as "admin"
     And I go to the courses management page
-    And I set the field "coursesearchbox" to "Biology"
-    And I press "Go"
+    And I set the field "Search" to "Biology"
+    And I press "Search"
     When I select course "Biology Y1" in the management interface
     And I select course "Biology Y2" in the management interface
     And I set the field "menumovecoursesto" to "Science"
index a4b4af7..f46edc4 100644 (file)
@@ -382,7 +382,7 @@ class editor {
 
         // Add JavaScript settings.
         $root = $CFG->wwwroot;
-        $filespathbase = "{$root}/pluginfile.php/{$context->id}/core_h5p/";
+        $filespathbase = \moodle_url::make_draftfile_url(0, '', '');
 
         $factory = new factory();
         $contentvalidator = $factory->get_content_validator();
@@ -390,7 +390,7 @@ class editor {
         $editorajaxtoken = core::createToken(editor_ajax::EDITOR_AJAX_TOKEN);
         $sesskey = sesskey();
         $settings['editor'] = [
-            'filesPath' => $filespathbase . 'editor',
+            'filesPath' => $filespathbase->out(),
             'fileIcon' => [
                 'path' => $url . 'images/binary-file.png',
                 'width' => 50,
index c1e24ad..ba032a4 100644 (file)
@@ -48,7 +48,12 @@ class file_storage implements \H5PFileStorage {
     public const EXPORT_FILEAREA = 'export';
     /** The icon filename */
     public const ICON_FILENAME = 'icon.svg';
-    /** The editor file area */
+
+    /**
+     * The editor file area.
+     * @deprecated since Moodle 3.10 MDL-68909. Please do not use this constant any more.
+     * @todo MDL-69530 This will be deleted in Moodle 4.2.
+     */
     public const EDITOR_FILEAREA = 'editor';
 
     /**
@@ -331,10 +336,22 @@ class file_storage implements \H5PFileStorage {
      * @return int The id of the saved file.
      */
     public function saveFile($file, $contentid) {
+        global $USER;
+
+        $context = $this->context->id;
+        $component = self::COMPONENT;
+        $filearea = self::CONTENT_FILEAREA;
+        if ($contentid === 0) {
+            $usercontext = \context_user::instance($USER->id);
+            $context = $usercontext->id;
+            $component = 'user';
+            $filearea = 'draft';
+        }
+
         $record = array(
-            'contextid' => $this->context->id,
-            'component' => self::COMPONENT,
-            'filearea' => $contentid === 0 ? self::EDITOR_FILEAREA : self::CONTENT_FILEAREA,
+            'contextid' => $context,
+            'component' => $component,
+            'filearea' => $filearea,
             'itemid' => $contentid,
             'filepath' => '/' . $file->getType() . 's/',
             'filename' => $file->getName()
@@ -357,8 +374,8 @@ class file_storage implements \H5PFileStorage {
      */
     public function cloneContentFile($file, $fromid, $tocontent): void {
         // Determine source filearea and itemid.
-        if ($fromid === self::EDITOR_FILEAREA) {
-            $sourcefilearea = self::EDITOR_FILEAREA;
+        if ($fromid === 'editor') {
+            $sourcefilearea = 'draft';
             $sourceitemid = 0;
         } else {
             $sourcefilearea = self::CONTENT_FILEAREA;
@@ -791,15 +808,22 @@ class file_storage implements \H5PFileStorage {
      * @return stored_file|null
      */
     private function get_file(string $filearea, int $itemid, string $file): ?stored_file {
-        if ($filearea === 'editor') {
+        global $USER;
+
+        $component = self::COMPONENT;
+        $context = $this->context->id;
+        if ($filearea === 'draft') {
             $itemid = 0;
+            $component = 'user';
+            $usercontext = \context_user::instance($USER->id);
+            $context = $usercontext->id;
         }
 
         $filepath = '/'. dirname($file). '/';
         $filename = basename($file);
 
         // Load file.
-        $existingfile = $this->fs->get_file($this->context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename);
+        $existingfile = $this->fs->get_file($context, $component, $filearea, $itemid, $filepath, $filename);
         if (!$existingfile) {
             return null;
         }
@@ -824,8 +848,8 @@ class file_storage implements \H5PFileStorage {
         // Create file record for content.
         $record = array(
             'contextid' => $this->context->id,
-            'component' => self::COMPONENT,
-            'filearea' => $contentid > 0 ? self::CONTENT_FILEAREA : self::EDITOR_FILEAREA,
+            'component' => $contentid > 0 ? self::COMPONENT : 'user',
+            'filearea' => $contentid > 0 ? self::CONTENT_FILEAREA : 'draft',
             'itemid' => $contentid > 0 ? $contentid : 0,
             'filepath' => '/' . $foldername . '/',
             'filename' => $filename
index 025c58b..b640925 100644 (file)
@@ -94,7 +94,6 @@ function core_h5p_pluginfile($course, $cm, $context, string $filearea, array $ar
             }
             $itemid = array_shift($args);
             break;
-        case \core_h5p\file_storage::EDITOR_FILEAREA:
         case \core_h5p\file_storage::CACHED_ASSETS_FILEAREA:
         case \core_h5p\file_storage::EXPORT_FILEAREA:
             $itemid = 0;
index dff6022..8145ab5 100644 (file)
@@ -182,6 +182,9 @@ class editor_testcase extends advanced_testcase {
     public function test_add_editor_to_form() {
         global $PAGE, $CFG;
 
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
         // Get form data.
         $form = $this->get_test_form();
         $mform = $form->getform();
index 88c229d..72e8087 100644 (file)
@@ -414,6 +414,8 @@ class core_h5p_generator extends \component_generator_base {
      * @throws coding_exception
      */
     public function create_content_file(string $file, string $filearea, int $contentid = 0): stored_file {
+        global $USER;
+
         $filepath = '/'.dirname($file).'/';
         $filename = basename($file);
 
@@ -421,15 +423,25 @@ class core_h5p_generator extends \component_generator_base {
             throw new coding_exception('Files belonging to an H5P content must specify the H5P content id');
         }
 
-        $content = 'fake content';
+        if ($filearea === 'draft') {
+            $usercontext = \context_user::instance($USER->id);
+            $context = $usercontext->id;
+            $component = 'user';
+            $itemid = 0;
+        } else {
+            $systemcontext = context_system::instance();
+            $context = $systemcontext->id;
+            $component = \core_h5p\file_storage::COMPONENT;
+            $itemid = $contentid;
+        }
 
-        $systemcontext = context_system::instance();
+        $content = 'fake content';
 
         $filerecord = array(
-            'contextid' => $systemcontext->id,
-            'component' => \core_h5p\file_storage::COMPONENT,
+            'contextid' => $context,
+            'component' => $component,
             'filearea'  => $filearea,
-            'itemid'    => ($filearea === 'editor') ? 0 : $contentid,
+            'itemid'    => $itemid,
             'filepath'  => $filepath,
             'filename'  => $filename,
         );
index d94d395..8cce6c2 100644 (file)
@@ -625,6 +625,7 @@ class h5p_file_storage_testcase extends \advanced_testcase {
      */
     public function test_get_file(): void {
 
+        $this->setAdminUser();
         $file = 'img/fake.png';
         $h5pcontentid = 3;
 
@@ -641,9 +642,9 @@ class h5p_file_storage_testcase extends \advanced_testcase {
         $this->assertInstanceOf('stored_file', $contentfile);
 
         // Add a file to editor.
-        $this->h5p_generator->create_content_file($file, file_storage::EDITOR_FILEAREA, $h5pcontentid);
+        $this->h5p_generator->create_content_file($file, 'draft', $h5pcontentid);
 
-        $editorfile = $method->invoke(new file_storage(), file_storage::EDITOR_FILEAREA, $h5pcontentid, $file);
+        $editorfile = $method->invoke(new file_storage(), 'draft', $h5pcontentid, $file);
 
         // Check that it returns an instance of store_file.
         $this->assertInstanceOf('stored_file', $editorfile);
@@ -692,6 +693,9 @@ class h5p_file_storage_testcase extends \advanced_testcase {
      */
     public function test_cloneContentFile(): void {
 
+        $admin = get_admin();
+        $usercontext = \context_user::instance($admin->id);
+        $this->setUser($admin);
         // Upload a file to the editor.
         $file = 'images/fake.jpg';
         $filepath = '/'.dirname($file).'/';
@@ -700,9 +704,9 @@ class h5p_file_storage_testcase extends \advanced_testcase {
         $content = 'abcd';
 
         $filerecord = array(
-            'contextid' => $this->h5p_fs_context->id,
-            'component' => file_storage::COMPONENT,
-            'filearea'  => file_storage::EDITOR_FILEAREA,
+            'contextid' => $usercontext->id,
+            'component' => 'user',
+            'filearea'  => 'draft',
             'itemid'    => 0,
             'filepath'  => $filepath,
             'filename'  => $filename,
@@ -731,7 +735,9 @@ class h5p_file_storage_testcase extends \advanced_testcase {
         $filename = basename($file);
 
         $sourcecontentid = 111;
-        $filerecord['filearea'] = 'content';
+        $filerecord['contextid'] = $this->h5p_fs_context->id;
+        $filerecord['component'] = file_storage::COMPONENT;
+        $filerecord['filearea'] = file_storage::CONTENT_FILEAREA;
         $filerecord['itemid'] = $sourcecontentid;
         $filerecord['filepath'] = $filepath;
         $filerecord['filename'] = $filename;
index f13cc48..7ff69a1 100644 (file)
@@ -137,6 +137,7 @@ $string['configgeneralgroups'] = 'Sets the default for including groups and grou
 $string['configgeneralroleassignments'] = 'If enabled by default roles assignments will also be backed up.';
 $string['configgeneraluserscompletion'] = 'If enabled user completion information will be included in backups by default.';
 $string['configgeneralusers'] = 'Sets the default for whether to include users in backups.';
+$string['configlegacyfiles'] = 'If disabled, legacy course files will not be included';
 $string['configloglifetime'] = 'This specifies the length of time you want to keep backup logs information. Logs that are older than this age are automatically deleted. It is recommended to keep this value small, because backup logged information can be huge.';
 $string['configrestoreactivities'] = 'Sets the default for restoring activities.';
 $string['configrestorebadges'] = 'Sets the default for restoring badges.';
@@ -227,6 +228,7 @@ $string['generalfiles'] = 'Include files';
 $string['generalfilters'] = 'Include filters';
 $string['generalhistories'] = 'Include histories';
 $string['generalgradehistories'] = 'Include histories';
+$string['generallegacyfiles'] = 'Include legacy course files';
 $string['generallogs'] = 'Include logs';
 $string['generalquestionbank'] = 'Include question bank';
 $string['generalgroups'] = 'Include groups and groupings';
@@ -359,6 +361,7 @@ $string['rootsettingcalendarevents'] = 'Include calendar events';
 $string['rootsettingcontentbankcontent'] = 'Include content bank content';
 $string['rootsettinguserscompletion'] = 'Include user completion details';
 $string['rootsettingquestionbank'] = 'Include question bank';
+$string['rootsettinglegacyfiles'] = 'Include legacy course files';
 $string['rootsettinglogs'] = 'Include course logs';
 $string['rootsettinggradehistories'] = 'Include grade history';
 $string['rootsettinggroups'] = 'Include groups and groupings';
index 6535c93..4e073a6 100644 (file)
@@ -51,6 +51,7 @@ $string['file_help'] = 'Files may be stored in the content bank for use in cours
 $string['itemsfound'] = '{$a} items found';
 $string['lastmodified'] = 'Last modified';
 $string['name'] = 'Content';
+$string['nocontentavailable'] = 'No content available';
 $string['nocontenttypes'] = 'No content types available';
 $string['nopermissiontodelete'] = 'You do not have permission to delete content.';
 $string['nopermissiontomanage'] = 'You do not have permission to manage content.';
index 2d8bd72..f5a35c1 100644 (file)
@@ -1936,6 +1936,8 @@ $string['sizeb'] = 'bytes';
 $string['sizegb'] = 'GB';
 $string['sizekb'] = 'KB';
 $string['sizemb'] = 'MB';
+$string['sizepb'] = 'PB';
+$string['sizetb'] = 'TB';
 $string['skipped'] = 'Skipped';
 $string['skiptocategorylisting'] = 'Skip to the category listings';
 $string['skiptocourselisting'] = 'Skip to the course listings';
@@ -2096,6 +2098,7 @@ $string['today'] = 'Today';
 $string['todaylogs'] = 'Today\'s logs';
 $string['toeveryone'] = 'to everyone';
 $string['toggleemojipicker'] = 'Toggle emoji picker';
+$string['togglesearch'] = 'Toggle search input';
 $string['toomanybounces'] = 'That email address has had too many bounces. You <b>must</b> change it to continue.';
 $string['toomanytags'] = 'This search included too many tags; some will have been ignored.';
 $string['toomanytoshow'] = 'There are too many users to show.';
diff --git a/lib/amd/build/search-input.min.js b/lib/amd/build/search-input.min.js
deleted file mode 100644 (file)
index 5e2f8db..0000000
Binary files a/lib/amd/build/search-input.min.js and /dev/null differ
diff --git a/lib/amd/build/search-input.min.js.map b/lib/amd/build/search-input.min.js.map
deleted file mode 100644 (file)
index f6a55ed..0000000
Binary files a/lib/amd/build/search-input.min.js.map and /dev/null differ
diff --git a/lib/amd/src/search-input.js b/lib/amd/src/search-input.js
deleted file mode 100644 (file)
index 85f9bb1..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-// 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/>.
-
-/**
- * Search box.
- *
- * @module     core/search-input
- * @class      search-input
- * @package    core
- * @copyright  2016 David Monllao {@link http://www.davidmonllao.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since      Moodle 3.1
- */
-define(['jquery'], function($) {
-
-    /**
-     * This search box div node.
-     *
-     * @private
-     */
-    var wrapper = null;
-
-    /**
-     * Toggles the form visibility.
-     *
-     * @param {Event} ev
-     * @method toggleForm
-     * @private
-     */
-    var toggleForm = function(ev) {
-
-        if (wrapper.hasClass('expanded')) {
-            hideForm();
-        } else {
-            showForm(ev);
-        }
-    };
-
-    /**
-     * Shows the form or submits it depending on the window size.
-     *
-     * @param {Event} ev
-     * @method showForm
-     * @private
-     */
-    var showForm = function(ev) {
-
-        var windowWidth = $(document).width();
-
-        // We are only interested in enter and space keys (accessibility).
-        if (ev.type === 'keydown' && ev.keyCode !== 13 && ev.keyCode !== 32) {
-            return;
-        }
-
-        if (windowWidth <= 767 && (ev.type === 'click' || ev.type === 'keydown')) {
-            // Move to the search page when using small window sizes as the input requires too much space.
-            submitForm();
-            return;
-        } else if (windowWidth <= 767) {
-            // Ignore mousedown events in while using small window sizes.
-            return;
-        }
-
-        if (ev.type === 'keydown') {
-            // We don't want to submit the form unless the user hits enter.
-            ev.preventDefault();
-        }
-
-        wrapper.addClass('expanded');
-        wrapper.find('form').addClass('expanded');
-        wrapper.find('input').focus();
-    };
-
-    /**
-     * Hides the form.
-     *
-     * @method hideForm
-     * @private
-     */
-    var hideForm = function() {
-        wrapper.removeClass('expanded');
-        wrapper.find('form').removeClass('expanded');
-    };
-
-    /**
-     * Submits the form.
-     *
-     * @param {Event} ev
-     * @method submitForm
-     * @private
-     */
-    var submitForm = function() {
-        wrapper.find('form').submit();
-    };
-
-    return /** @alias module:core/search-input */ {
-        // Public variables and functions.
-
-        /**
-         * Assigns listeners to the requested select box.
-         *
-         * @method init
-         * @param {Number} id The search wrapper div id
-         */
-        init: function(id) {
-            wrapper = $('#' + id);
-            wrapper.on('click mouseover keydown', 'div', toggleForm);
-        }
-    };
-});
index 2a6eae8..fd96e72 100644 (file)
@@ -104,6 +104,9 @@ class core_component {
         'RedeyeVentures\\GeoPattern' => 'lib/geopattern-php/GeoPattern',
         'MongoDB' => 'cache/stores/mongodb/MongoDB',
         'Firebase\\JWT' => 'lib/php-jwt/src',
+        'ZipStream' => 'lib/zipstream/src/',
+        'MyCLabs\\Enum' => 'lib/php-enum/src',
+        'Psr\\Http\\Message' => 'lib/http-message/src',
     );
 
     /**
index 66dbe76..2861806 100644 (file)
@@ -473,7 +473,7 @@ $definitions = array(
     // Cache for licenses.
     'license' => [
         'mode' => cache_store::MODE_APPLICATION,
-        'simplekeys' => false,
-        'simpledata' => false
+        'simplekeys' => true,
+        'simpledata' => false,
     ],
 );
index 43e82bc..0643897 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20200804" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20200911" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="enablecompletion" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="1 = allow use of 'completion' progress-tracking on this course. 0 = disable completion tracking on this course."/>
         <FIELD NAME="completionnotify" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Notify users when they complete this course"/>
         <FIELD NAME="cacherev" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Incrementing revision for validating the course content cache"/>
+        <FIELD NAME="originalcourseid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="the id of the source course when a new course originates from a restore of another course on the same site."/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index 60ac970..cbfdccf 100644 (file)
@@ -2684,5 +2684,29 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2020082200.03);
     }
 
+    if ($oldversion < 2020091000.02) {
+        // Remove all the files with component='core_h5p' and filearea='editor' because they won't be used anymore.
+        $fs = get_file_storage();
+        $syscontext = context_system::instance();
+        $fs->delete_area_files($syscontext->id, 'core_h5p', 'editor');
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2020091000.02);
+    }
+
+    if ($oldversion < 2020091800.01) {
+        // Copy From id captures the id of the source course when a new course originates from a restore
+        // of another course on the same site.
+        $table = new xmldb_table('course');
+        $field = new xmldb_field('originalcourseid', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2020091800.01);
+    }
+
     return true;
 }
index 50cd9da..7f33390 100644 (file)
@@ -250,7 +250,7 @@ class external_api {
                 foreach ($plugins as $plugin => $callback) {
                     $result = $callback($externalfunctioninfo, $params);
                     if ($result !== false) {
-                        break;
+                        break 2;
                     }
                 }
             }
diff --git a/lib/http-message/LICENSE b/lib/http-message/LICENSE
new file mode 100644 (file)
index 0000000..c2d8e45
--- /dev/null
@@ -0,0 +1,19 @@
+Copyright (c) 2014 PHP Framework Interoperability Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy 
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights 
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
+copies of the Software, and to permit persons to whom the Software is 
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in 
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/lib/http-message/readme_moodle.txt b/lib/http-message/readme_moodle.txt
new file mode 100644 (file)
index 0000000..d8ec8aa
--- /dev/null
@@ -0,0 +1,5 @@
+Instructions to import http-message into Moodle:
+
+1/ Download from https://github.com/php-fig/http-message/releases
+
+2/ Copy the LICENSE file and the src folder into the lib/http-message folder
diff --git a/lib/http-message/src/MessageInterface.php b/lib/http-message/src/MessageInterface.php
new file mode 100644 (file)
index 0000000..dd46e5e
--- /dev/null
@@ -0,0 +1,187 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * HTTP messages consist of requests from a client to a server and responses
+ * from a server to a client. This interface defines the methods common to
+ * each.
+ *
+ * Messages are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ *
+ * @link http://www.ietf.org/rfc/rfc7230.txt
+ * @link http://www.ietf.org/rfc/rfc7231.txt
+ */
+interface MessageInterface
+{
+    /**
+     * Retrieves the HTTP protocol version as a string.
+     *
+     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
+     *
+     * @return string HTTP protocol version.
+     */
+    public function getProtocolVersion();
+
+    /**
+     * Return an instance with the specified HTTP protocol version.
+     *
+     * The version string MUST contain only the HTTP version number (e.g.,
+     * "1.1", "1.0").
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new protocol version.
+     *
+     * @param string $version HTTP protocol version
+     * @return static
+     */
+    public function withProtocolVersion($version);
+
+    /**
+     * Retrieves all message header values.
+     *
+     * The keys represent the header name as it will be sent over the wire, and
+     * each value is an array of strings associated with the header.
+     *
+     *     // Represent the headers as a string
+     *     foreach ($message->getHeaders() as $name => $values) {
+     *         echo $name . ": " . implode(", ", $values);
+     *     }
+     *
+     *     // Emit headers iteratively:
+     *     foreach ($message->getHeaders() as $name => $values) {
+     *         foreach ($values as $value) {
+     *             header(sprintf('%s: %s', $name, $value), false);
+     *         }
+     *     }
+     *
+     * While header names are not case-sensitive, getHeaders() will preserve the
+     * exact case in which headers were originally specified.
+     *
+     * @return string[][] Returns an associative array of the message's headers. Each
+     *     key MUST be a header name, and each value MUST be an array of strings
+     *     for that header.
+     */
+    public function getHeaders();
+
+    /**
+     * Checks if a header exists by the given case-insensitive name.
+     *
+     * @param string $name Case-insensitive header field name.
+     * @return bool Returns true if any header names match the given header
+     *     name using a case-insensitive string comparison. Returns false if
+     *     no matching header name is found in the message.
+     */
+    public function hasHeader($name);
+
+    /**
+     * Retrieves a message header value by the given case-insensitive name.
+     *
+     * This method returns an array of all the header values of the given
+     * case-insensitive header name.
+     *
+     * If the header does not appear in the message, this method MUST return an
+     * empty array.
+     *
+     * @param string $name Case-insensitive header field name.
+     * @return string[] An array of string values as provided for the given
+     *    header. If the header does not appear in the message, this method MUST
+     *    return an empty array.
+     */
+    public function getHeader($name);
+
+    /**
+     * Retrieves a comma-separated string of the values for a single header.
+     *
+     * This method returns all of the header values of the given
+     * case-insensitive header name as a string concatenated together using
+     * a comma.
+     *
+     * NOTE: Not all header values may be appropriately represented using
+     * comma concatenation. For such headers, use getHeader() instead
+     * and supply your own delimiter when concatenating.
+     *
+     * If the header does not appear in the message, this method MUST return
+     * an empty string.
+     *
+     * @param string $name Case-insensitive header field name.
+     * @return string A string of values as provided for the given header
+     *    concatenated together using a comma. If the header does not appear in
+     *    the message, this method MUST return an empty string.
+     */
+    public function getHeaderLine($name);
+
+    /**
+     * Return an instance with the provided value replacing the specified header.
+     *
+     * While header names are case-insensitive, the casing of the header will
+     * be preserved by this function, and returned from getHeaders().
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new and/or updated header and value.
+     *
+     * @param string $name Case-insensitive header field name.
+     * @param string|string[] $value Header value(s).
+     * @return static
+     * @throws \InvalidArgumentException for invalid header names or values.
+     */
+    public function withHeader($name, $value);
+
+    /**
+     * Return an instance with the specified header appended with the given value.
+     *
+     * Existing values for the specified header will be maintained. The new
+     * value(s) will be appended to the existing list. If the header did not
+     * exist previously, it will be added.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new header and/or value.
+     *
+     * @param string $name Case-insensitive header field name to add.
+     * @param string|string[] $value Header value(s).
+     * @return static
+     * @throws \InvalidArgumentException for invalid header names or values.
+     */
+    public function withAddedHeader($name, $value);
+
+    /**
+     * Return an instance without the specified header.
+     *
+     * Header resolution MUST be done without case-sensitivity.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that removes
+     * the named header.
+     *
+     * @param string $name Case-insensitive header field name to remove.
+     * @return static
+     */
+    public function withoutHeader($name);
+
+    /**
+     * Gets the body of the message.
+     *
+     * @return StreamInterface Returns the body as a stream.
+     */
+    public function getBody();
+
+    /**
+     * Return an instance with the specified message body.
+     *
+     * The body MUST be a StreamInterface object.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return a new instance that has the
+     * new body stream.
+     *
+     * @param StreamInterface $body Body.
+     * @return static
+     * @throws \InvalidArgumentException When the body is not valid.
+     */
+    public function withBody(StreamInterface $body);
+}
diff --git a/lib/http-message/src/RequestInterface.php b/lib/http-message/src/RequestInterface.php
new file mode 100644 (file)
index 0000000..a96d4fd
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Representation of an outgoing, client-side request.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - HTTP method
+ * - URI
+ * - Headers
+ * - Message body
+ *
+ * During construction, implementations MUST attempt to set the Host header from
+ * a provided URI if no Host header is provided.
+ *
+ * Requests are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+interface RequestInterface extends MessageInterface
+{
+    /**
+     * Retrieves the message's request target.
+     *
+     * Retrieves the message's request-target either as it will appear (for
+     * clients), as it appeared at request (for servers), or as it was
+     * specified for the instance (see withRequestTarget()).
+     *
+     * In most cases, this will be the origin-form of the composed URI,
+     * unless a value was provided to the concrete implementation (see
+     * withRequestTarget() below).
+     *
+     * If no URI is available, and no request-target has been specifically
+     * provided, this method MUST return the string "/".
+     *
+     * @return string
+     */
+    public function getRequestTarget();
+
+    /**
+     * Return an instance with the specific request-target.
+     *
+     * If the request needs a non-origin-form request-target — e.g., for
+     * specifying an absolute-form, authority-form, or asterisk-form —
+     * this method may be used to create an instance with the specified
+     * request-target, verbatim.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * changed request target.
+     *
+     * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
+     *     request-target forms allowed in request messages)
+     * @param mixed $requestTarget
+     * @return static
+     */
+    public function withRequestTarget($requestTarget);
+
+    /**
+     * Retrieves the HTTP method of the request.
+     *
+     * @return string Returns the request method.
+     */
+    public function getMethod();
+
+    /**
+     * Return an instance with the provided HTTP method.
+     *
+     * While HTTP method names are typically all uppercase characters, HTTP
+     * method names are case-sensitive and thus implementations SHOULD NOT
+     * modify the given string.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * changed request method.
+     *
+     * @param string $method Case-sensitive method.
+     * @return static
+     * @throws \InvalidArgumentException for invalid HTTP methods.
+     */
+    public function withMethod($method);
+
+    /**
+     * Retrieves the URI instance.
+     *
+     * This method MUST return a UriInterface instance.
+     *
+     * @link http://tools.ietf.org/html/rfc3986#section-4.3
+     * @return UriInterface Returns a UriInterface instance
+     *     representing the URI of the request.
+     */
+    public function getUri();
+
+    /**
+     * Returns an instance with the provided URI.
+     *
+     * This method MUST update the Host header of the returned request by
+     * default if the URI contains a host component. If the URI does not
+     * contain a host component, any pre-existing Host header MUST be carried
+     * over to the returned request.
+     *
+     * You can opt-in to preserving the original state of the Host header by
+     * setting `$preserveHost` to `true`. When `$preserveHost` is set to
+     * `true`, this method interacts with the Host header in the following ways:
+     *
+     * - If the Host header is missing or empty, and the new URI contains
+     *   a host component, this method MUST update the Host header in the returned
+     *   request.
+     * - If the Host header is missing or empty, and the new URI does not contain a
+     *   host component, this method MUST NOT update the Host header in the returned
+     *   request.
+     * - If a Host header is present and non-empty, this method MUST NOT update
+     *   the Host header in the returned request.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new UriInterface instance.
+     *
+     * @link http://tools.ietf.org/html/rfc3986#section-4.3
+     * @param UriInterface $uri New request URI to use.
+     * @param bool $preserveHost Preserve the original state of the Host header.
+     * @return static
+     */
+    public function withUri(UriInterface $uri, $preserveHost = false);
+}
diff --git a/lib/http-message/src/ResponseInterface.php b/lib/http-message/src/ResponseInterface.php
new file mode 100644 (file)
index 0000000..c306514
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Representation of an outgoing, server-side response.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - Status code and reason phrase
+ * - Headers
+ * - Message body
+ *
+ * Responses are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+interface ResponseInterface extends MessageInterface
+{
+    /**
+     * Gets the response status code.
+     *
+     * The status code is a 3-digit integer result code of the server's attempt
+     * to understand and satisfy the request.
+     *
+     * @return int Status code.
+     */
+    public function getStatusCode();
+
+    /**
+     * Return an instance with the specified status code and, optionally, reason phrase.
+     *
+     * If no reason phrase is specified, implementations MAY choose to default
+     * to the RFC 7231 or IANA recommended reason phrase for the response's
+     * status code.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated status and reason phrase.
+     *
+     * @link http://tools.ietf.org/html/rfc7231#section-6
+     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+     * @param int $code The 3-digit integer result code to set.
+     * @param string $reasonPhrase The reason phrase to use with the
+     *     provided status code; if none is provided, implementations MAY
+     *     use the defaults as suggested in the HTTP specification.
+     * @return static
+     * @throws \InvalidArgumentException For invalid status code arguments.
+     */
+    public function withStatus($code, $reasonPhrase = '');
+
+    /**
+     * Gets the response reason phrase associated with the status code.
+     *
+     * Because a reason phrase is not a required element in a response
+     * status line, the reason phrase value MAY be null. Implementations MAY
+     * choose to return the default RFC 7231 recommended reason phrase (or those
+     * listed in the IANA HTTP Status Code Registry) for the response's
+     * status code.
+     *
+     * @link http://tools.ietf.org/html/rfc7231#section-6
+     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+     * @return string Reason phrase; must return an empty string if none present.
+     */
+    public function getReasonPhrase();
+}
diff --git a/lib/http-message/src/ServerRequestInterface.php b/lib/http-message/src/ServerRequestInterface.php
new file mode 100644 (file)
index 0000000..0251234
--- /dev/null
@@ -0,0 +1,261 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Representation of an incoming, server-side HTTP request.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - HTTP method
+ * - URI
+ * - Headers
+ * - Message body
+ *
+ * Additionally, it encapsulates all data as it has arrived to the
+ * application from the CGI and/or PHP environment, including:
+ *
+ * - The values represented in $_SERVER.
+ * - Any cookies provided (generally via $_COOKIE)
+ * - Query string arguments (generally via $_GET, or as parsed via parse_str())
+ * - Upload files, if any (as represented by $_FILES)
+ * - Deserialized body parameters (generally from $_POST)
+ *
+ * $_SERVER values MUST be treated as immutable, as they represent application
+ * state at the time of request; as such, no methods are provided to allow
+ * modification of those values. The other values provide such methods, as they
+ * can be restored from $_SERVER or the request body, and may need treatment
+ * during the application (e.g., body parameters may be deserialized based on
+ * content type).
+ *
+ * Additionally, this interface recognizes the utility of introspecting a
+ * request to derive and match additional parameters (e.g., via URI path
+ * matching, decrypting cookie values, deserializing non-form-encoded body
+ * content, matching authorization headers to users, etc). These parameters
+ * are stored in an "attributes" property.
+ *
+ * Requests are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+interface ServerRequestInterface extends RequestInterface
+{
+    /**
+     * Retrieve server parameters.
+     *
+     * Retrieves data related to the incoming request environment,
+     * typically derived from PHP's $_SERVER superglobal. The data IS NOT
+     * REQUIRED to originate from $_SERVER.
+     *
+     * @return array
+     */
+    public function getServerParams();
+
+    /**
+     * Retrieve cookies.
+     *
+     * Retrieves cookies sent by the client to the server.
+     *
+     * The data MUST be compatible with the structure of the $_COOKIE
+     * superglobal.
+     *
+     * @return array
+     */
+    public function getCookieParams();
+
+    /**
+     * Return an instance with the specified cookies.
+     *
+     * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
+     * be compatible with the structure of $_COOKIE. Typically, this data will
+     * be injected at instantiation.
+     *
+     * This method MUST NOT update the related Cookie header of the request
+     * instance, nor related values in the server params.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated cookie values.
+     *
+     * @param array $cookies Array of key/value pairs representing cookies.
+     * @return static
+     */
+    public function withCookieParams(array $cookies);
+
+    /**
+     * Retrieve query string arguments.
+     *
+     * Retrieves the deserialized query string arguments, if any.
+     *
+     * Note: the query params might not be in sync with the URI or server
+     * params. If you need to ensure you are only getting the original
+     * values, you may need to parse the query string from `getUri()->getQuery()`
+     * or from the `QUERY_STRING` server param.
+     *
+     * @return array
+     */
+    public function getQueryParams();
+
+    /**
+     * Return an instance with the specified query string arguments.
+     *
+     * These values SHOULD remain immutable over the course of the incoming
+     * request. They MAY be injected during instantiation, such as from PHP's
+     * $_GET superglobal, or MAY be derived from some other value such as the
+     * URI. In cases where the arguments are parsed from the URI, the data
+     * MUST be compatible with what PHP's parse_str() would return for
+     * purposes of how duplicate query parameters are handled, and how nested
+     * sets are handled.
+     *
+     * Setting query string arguments MUST NOT change the URI stored by the
+     * request, nor the values in the server params.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated query string arguments.
+     *
+     * @param array $query Array of query string arguments, typically from
+     *     $_GET.
+     * @return static
+     */
+    public function withQueryParams(array $query);
+
+    /**
+     * Retrieve normalized file upload data.
+     *
+     * This method returns upload metadata in a normalized tree, with each leaf
+     * an instance of Psr\Http\Message\UploadedFileInterface.
+     *
+     * These values MAY be prepared from $_FILES or the message body during
+     * instantiation, or MAY be injected via withUploadedFiles().
+     *
+     * @return array An array tree of UploadedFileInterface instances; an empty
+     *     array MUST be returned if no data is present.
+     */
+    public function getUploadedFiles();
+
+    /**
+     * Create a new instance with the specified uploaded files.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated body parameters.
+     *
+     * @param array $uploadedFiles An array tree of UploadedFileInterface instances.
+     * @return static
+     * @throws \InvalidArgumentException if an invalid structure is provided.
+     */
+    public function withUploadedFiles(array $uploadedFiles);
+
+    /**
+     * Retrieve any parameters provided in the request body.
+     *
+     * If the request Content-Type is either application/x-www-form-urlencoded
+     * or multipart/form-data, and the request method is POST, this method MUST
+     * return the contents of $_POST.
+     *
+     * Otherwise, this method may return any results of deserializing
+     * the request body content; as parsing returns structured content, the
+     * potential types MUST be arrays or objects only. A null value indicates
+     * the absence of body content.
+     *
+     * @return null|array|object The deserialized body parameters, if any.
+     *     These will typically be an array or object.
+     */
+    public function getParsedBody();
+
+    /**
+     * Return an instance with the specified body parameters.
+     *
+     * These MAY be injected during instantiation.
+     *
+     * If the request Content-Type is either application/x-www-form-urlencoded
+     * or multipart/form-data, and the request method is POST, use this method
+     * ONLY to inject the contents of $_POST.
+     *
+     * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
+     * deserializing the request body content. Deserialization/parsing returns
+     * structured data, and, as such, this method ONLY accepts arrays or objects,
+     * or a null value if nothing was available to parse.
+     *
+     * As an example, if content negotiation determines that the request data
+     * is a JSON payload, this method could be used to create a request
+     * instance with the deserialized parameters.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated body parameters.
+     *
+     * @param null|array|object $data The deserialized body data. This will
+     *     typically be in an array or object.
+     * @return static
+     * @throws \InvalidArgumentException if an unsupported argument type is
+     *     provided.
+     */
+    public function withParsedBody($data);
+
+    /**
+     * Retrieve attributes derived from the request.
+     *
+     * The request "attributes" may be used to allow injection of any
+     * parameters derived from the request: e.g., the results of path
+     * match operations; the results of decrypting cookies; the results of
+     * deserializing non-form-encoded message bodies; etc. Attributes
+     * will be application and request specific, and CAN be mutable.
+     *
+     * @return array Attributes derived from the request.
+     */
+    public function getAttributes();
+
+    /**
+     * Retrieve a single derived request attribute.
+     *
+     * Retrieves a single derived request attribute as described in
+     * getAttributes(). If the attribute has not been previously set, returns
+     * the default value as provided.
+     *
+     * This method obviates the need for a hasAttribute() method, as it allows
+     * specifying a default value to return if the attribute is not found.
+     *
+     * @see getAttributes()
+     * @param string $name The attribute name.
+     * @param mixed $default Default value to return if the attribute does not exist.
+     * @return mixed
+     */
+    public function getAttribute($name, $default = null);
+
+    /**
+     * Return an instance with the specified derived request attribute.
+     *
+     * This method allows setting a single derived request attribute as
+     * described in getAttributes().
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated attribute.
+     *
+     * @see getAttributes()
+     * @param string $name The attribute name.
+     * @param mixed $value The value of the attribute.
+     * @return static
+     */
+    public function withAttribute($name, $value);
+
+    /**
+     * Return an instance that removes the specified derived request attribute.
+     *
+     * This method allows removing a single derived request attribute as
+     * described in getAttributes().
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that removes
+     * the attribute.
+     *
+     * @see getAttributes()
+     * @param string $name The attribute name.
+     * @return static
+     */
+    public function withoutAttribute($name);
+}
diff --git a/lib/http-message/src/StreamInterface.php b/lib/http-message/src/StreamInterface.php
new file mode 100644 (file)
index 0000000..f68f391
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Describes a data stream.
+ *
+ * Typically, an instance will wrap a PHP stream; this interface provides
+ * a wrapper around the most common operations, including serialization of
+ * the entire stream to a string.
+ */
+interface StreamInterface
+{
+    /**
+     * Reads all data from the stream into a string, from the beginning to end.
+     *
+     * This method MUST attempt to seek to the beginning of the stream before
+     * reading data and read the stream until the end is reached.
+     *
+     * Warning: This could attempt to load a large amount of data into memory.
+     *
+     * This method MUST NOT raise an exception in order to conform with PHP's
+     * string casting operations.
+     *
+     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
+     * @return string
+     */
+    public function __toString();
+
+    /**
+     * Closes the stream and any underlying resources.
+     *
+     * @return void
+     */
+    public function close();
+
+    /**
+     * Separates any underlying resources from the stream.
+     *
+     * After the stream has been detached, the stream is in an unusable state.
+     *
+     * @return resource|null Underlying PHP stream, if any
+     */
+    public function detach();
+
+    /**
+     * Get the size of the stream if known.
+     *
+     * @return int|null Returns the size in bytes if known, or null if unknown.
+     */
+    public function getSize();
+
+    /**
+     * Returns the current position of the file read/write pointer
+     *
+     * @return int Position of the file pointer
+     * @throws \RuntimeException on error.
+     */
+    public function tell();
+
+    /**
+     * Returns true if the stream is at the end of the stream.
+     *
+     * @return bool
+     */
+    public function eof();
+
+    /**
+     * Returns whether or not the stream is seekable.
+     *
+     * @return bool
+     */
+    public function isSeekable();
+
+    /**
+     * Seek to a position in the stream.
+     *
+     * @link http://www.php.net/manual/en/function.fseek.php
+     * @param int $offset Stream offset
+     * @param int $whence Specifies how the cursor position will be calculated
+     *     based on the seek offset. Valid values are identical to the built-in
+     *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
+     *     offset bytes SEEK_CUR: Set position to current location plus offset
+     *     SEEK_END: Set position to end-of-stream plus offset.
+     * @throws \RuntimeException on failure.
+     */
+    public function seek($offset, $whence = SEEK_SET);
+
+    /**
+     * Seek to the beginning of the stream.
+     *
+     * If the stream is not seekable, this method will raise an exception;
+     * otherwise, it will perform a seek(0).
+     *
+     * @see seek()
+     * @link http://www.php.net/manual/en/function.fseek.php
+     * @throws \RuntimeException on failure.
+     */
+    public function rewind();
+
+    /**
+     * Returns whether or not the stream is writable.
+     *
+     * @return bool
+     */
+    public function isWritable();
+
+    /**
+     * Write data to the stream.
+     *
+     * @param string $string The string that is to be written.
+     * @return int Returns the number of bytes written to the stream.
+     * @throws \RuntimeException on failure.
+     */
+    public function write($string);
+
+    /**
+     * Returns whether or not the stream is readable.
+     *
+     * @return bool
+     */
+    public function isReadable();
+
+    /**
+     * Read data from the stream.
+     *
+     * @param int $length Read up to $length bytes from the object and return
+     *     them. Fewer than $length bytes may be returned if underlying stream
+     *     call returns fewer bytes.
+     * @return string Returns the data read from the stream, or an empty string
+     *     if no bytes are available.
+     * @throws \RuntimeException if an error occurs.
+     */
+    public function read($length);
+
+    /**
+     * Returns the remaining contents in a string
+     *
+     * @return string
+     * @throws \RuntimeException if unable to read or an error occurs while
+     *     reading.
+     */
+    public function getContents();
+
+    /**
+     * Get stream metadata as an associative array or retrieve a specific key.
+     *
+     * The keys returned are identical to the keys returned from PHP's
+     * stream_get_meta_data() function.
+     *
+     * @link http://php.net/manual/en/function.stream-get-meta-data.php
+     * @param string $key Specific metadata to retrieve.
+     * @return array|mixed|null Returns an associative array if no key is
+     *     provided. Returns a specific key value if a key is provided and the
+     *     value is found, or null if the key is not found.
+     */
+    public function getMetadata($key = null);
+}
diff --git a/lib/http-message/src/UploadedFileInterface.php b/lib/http-message/src/UploadedFileInterface.php
new file mode 100644 (file)
index 0000000..f8a6901
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Value object representing a file uploaded through an HTTP request.
+ *
+ * Instances of this interface are considered immutable; all methods that
+ * might change state MUST be implemented such that they retain the internal
+ * state of the current instance and return an instance that contains the
+ * changed state.
+ */
+interface UploadedFileInterface
+{
+    /**
+     * Retrieve a stream representing the uploaded file.
+     *
+     * This method MUST return a StreamInterface instance, representing the
+     * uploaded file. The purpose of this method is to allow utilizing native PHP
+     * stream functionality to manipulate the file upload, such as
+     * stream_copy_to_stream() (though the result will need to be decorated in a
+     * native PHP stream wrapper to work with such functions).
+     *
+     * If the moveTo() method has been called previously, this method MUST raise
+     * an exception.
+     *
+     * @return StreamInterface Stream representation of the uploaded file.
+     * @throws \RuntimeException in cases when no stream is available or can be
+     *     created.
+     */
+    public function getStream();
+
+    /**
+     * Move the uploaded file to a new location.
+     *
+     * Use this method as an alternative to move_uploaded_file(). This method is
+     * guaranteed to work in both SAPI and non-SAPI environments.
+     * Implementations must determine which environment they are in, and use the
+     * appropriate method (move_uploaded_file(), rename(), or a stream
+     * operation) to perform the operation.
+     *
+     * $targetPath may be an absolute path, or a relative path. If it is a
+     * relative path, resolution should be the same as used by PHP's rename()
+     * function.
+     *
+     * The original file or stream MUST be removed on completion.
+     *
+     * If this method is called more than once, any subsequent calls MUST raise
+     * an exception.
+     *
+     * When used in an SAPI environment where $_FILES is populated, when writing
+     * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
+     * used to ensure permissions and upload status are verified correctly.
+     *
+     * If you wish to move to a stream, use getStream(), as SAPI operations
+     * cannot guarantee writing to stream destinations.
+     *
+     * @see http://php.net/is_uploaded_file
+     * @see http://php.net/move_uploaded_file
+     * @param string $targetPath Path to which to move the uploaded file.
+     * @throws \InvalidArgumentException if the $targetPath specified is invalid.
+     * @throws \RuntimeException on any error during the move operation, or on
+     *     the second or subsequent call to the method.
+     */
+    public function moveTo($targetPath);
+    
+    /**
+     * Retrieve the file size.
+     *
+     * Implementations SHOULD return the value stored in the "size" key of
+     * the file in the $_FILES array if available, as PHP calculates this based
+     * on the actual size transmitted.
+     *
+     * @return int|null The file size in bytes or null if unknown.
+     */
+    public function getSize();
+    
+    /**
+     * Retrieve the error associated with the uploaded file.
+     *
+     * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
+     *
+     * If the file was uploaded successfully, this method MUST return
+     * UPLOAD_ERR_OK.
+     *
+     * Implementations SHOULD return the value stored in the "error" key of
+     * the file in the $_FILES array.
+     *
+     * @see http://php.net/manual/en/features.file-upload.errors.php
+     * @return int One of PHP's UPLOAD_ERR_XXX constants.
+     */
+    public function getError();
+    
+    /**
+     * Retrieve the filename sent by the client.
+     *
+     * Do not trust the value returned by this method. A client could send
+     * a malicious filename with the intention to corrupt or hack your
+     * application.
+     *
+     * Implementations SHOULD return the value stored in the "name" key of
+     * the file in the $_FILES array.
+     *
+     * @return string|null The filename sent by the client or null if none
+     *     was provided.
+     */
+    public function getClientFilename();
+    
+    /**
+     * Retrieve the media type sent by the client.
+     *
+     * Do not trust the value returned by this method. A client could send
+     * a malicious media type with the intention to corrupt or hack your
+     * application.
+     *
+     * Implementations SHOULD return the value stored in the "type" key of
+     * the file in the $_FILES array.
+     *
+     * @return string|null The media type sent by the client or null if none
+     *     was provided.
+     */
+    public function getClientMediaType();
+}
diff --git a/lib/http-message/src/UriInterface.php b/lib/http-message/src/UriInterface.php
new file mode 100644 (file)
index 0000000..9d7ab9e
--- /dev/null
@@ -0,0 +1,323 @@
+<?php
+namespace Psr\Http\Message;
+
+/**
+ * Value object representing a URI.
+ *
+ * This interface is meant to represent URIs according to RFC 3986 and to
+ * provide methods for most common operations. Additional functionality for
+ * working with URIs can be provided on top of the interface or externally.
+ * Its primary use is for HTTP requests, but may also be used in other
+ * contexts.
+ *
+ * Instances of this interface are considered immutable; all methods that
+ * might change state MUST be implemented such that they retain the internal
+ * state of the current instance and return an instance that contains the
+ * changed state.
+ *
+ * Typically the Host header will be also be present in the request message.
+ * For server-side requests, the scheme will typically be discoverable in the
+ * server parameters.
+ *
+ * @link http://tools.ietf.org/html/rfc3986 (the URI specification)
+ */
+interface UriInterface
+{
+    /**
+     * Retrieve the scheme component of the URI.
+     *
+     * If no scheme is present, this method MUST return an empty string.
+     *
+     * The value returned MUST be normalized to lowercase, per RFC 3986
+     * Section 3.1.
+     *
+     * The trailing ":" character is not part of the scheme and MUST NOT be
+     * added.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-3.1
+     * @return string The URI scheme.
+     */
+    public function getScheme();
+
+    /**
+     * Retrieve the authority component of the URI.
+     *
+     * If no authority information is present, this method MUST return an empty
+     * string.
+     *
+     * The authority syntax of the URI is:
+     *
+     * <pre>
+     * [user-info@]host[:port]
+     * </pre>
+     *
+     * If the port component is not set or is the standard port for the current
+     * scheme, it SHOULD NOT be included.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-3.2
+     * @return string The URI authority, in "[user-info@]host[:port]" format.
+     */
+    public function getAuthority();
+
+    /**
+     * Retrieve the user information component of the URI.
+     *
+     * If no user information is present, this method MUST return an empty
+     * string.
+     *
+     * If a user is present in the URI, this will return that value;
+     * additionally, if the password is also present, it will be appended to the
+     * user value, with a colon (":") separating the values.
+     *
+     * The trailing "@" character is not part of the user information and MUST
+     * NOT be added.
+     *
+     * @return string The URI user information, in "username[:password]" format.
+     */
+    public function getUserInfo();
+
+    /**
+     * Retrieve the host component of the URI.
+     *
+     * If no host is present, this method MUST return an empty string.
+     *
+     * The value returned MUST be normalized to lowercase, per RFC 3986
+     * Section 3.2.2.
+     *
+     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
+     * @return string The URI host.
+     */
+    public function getHost();
+
+    /**
+     * Retrieve the port component of the URI.
+     *
+     * If a port is present, and it is non-standard for the current scheme,
+     * this method MUST return it as an integer. If the port is the standard port
+     * used with the current scheme, this method SHOULD return null.
+     *
+     * If no port is present, and no scheme is present, this method MUST return
+     * a null value.
+     *
+     * If no port is present, but a scheme is present, this method MAY return
+     * the standard port for that scheme, but SHOULD return null.
+     *
+     * @return null|int The URI port.
+     */
+    public function getPort();
+
+    /**
+     * Retrieve the path component of the URI.
+     *
+     * The path can either be empty or absolute (starting with a slash) or
+     * rootless (not starting with a slash). Implementations MUST support all
+     * three syntaxes.
+     *
+     * Normally, the empty path "" and absolute path "/" are considered equal as
+     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
+     * do this normalization because in contexts with a trimmed base path, e.g.
+     * the front controller, this difference becomes significant. It's the task
+     * of the user to handle both "" and "/".
+     *
+     * The value returned MUST be percent-encoded, but MUST NOT double-encode
+     * any characters. To determine what characters to encode, please refer to
+     * RFC 3986, Sections 2 and 3.3.
+     *
+     * As an example, if the value should include a slash ("/") not intended as
+     * delimiter between path segments, that value MUST be passed in encoded
+     * form (e.g., "%2F") to the instance.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-2
+     * @see https://tools.ietf.org/html/rfc3986#section-3.3
+     * @return string The URI path.
+     */
+    public function getPath();
+
+    /**
+     * Retrieve the query string of the URI.
+     *
+     * If no query string is present, this method MUST return an empty string.
+     *
+     * The leading "?" character is not part of the query and MUST NOT be
+     * added.
+     *
+     * The value returned MUST be percent-encoded, but MUST NOT double-encode
+     * any characters. To determine what characters to encode, please refer to
+     * RFC 3986, Sections 2 and 3.4.
+     *
+     * As an example, if a value in a key/value pair of the query string should
+     * include an ampersand ("&") not intended as a delimiter between values,
+     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-2
+     * @see https://tools.ietf.org/html/rfc3986#section-3.4
+     * @return string The URI query string.
+     */
+    public function getQuery();
+
+    /**
+     * Retrieve the fragment component of the URI.
+     *
+     * If no fragment is present, this method MUST return an empty string.
+     *
+     * The leading "#" character is not part of the fragment and MUST NOT be
+     * added.
+     *
+     * The value returned MUST be percent-encoded, but MUST NOT double-encode
+     * any characters. To determine what characters to encode, please refer to
+     * RFC 3986, Sections 2 and 3.5.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-2
+     * @see https://tools.ietf.org/html/rfc3986#section-3.5
+     * @return string The URI fragment.
+     */
+    public function getFragment();
+
+    /**
+     * Return an instance with the specified scheme.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified scheme.
+     *
+     * Implementations MUST support the schemes "http" and "https" case
+     * insensitively, and MAY accommodate other schemes if required.
+     *
+     * An empty scheme is equivalent to removing the scheme.
+     *
+     * @param string $scheme The scheme to use with the new instance.
+     * @return static A new instance with the specified scheme.
+     * @throws \InvalidArgumentException for invalid or unsupported schemes.
+     */
+    public function withScheme($scheme);
+
+    /**
+     * Return an instance with the specified user information.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified user information.
+     *
+     * Password is optional, but the user information MUST include the
+     * user; an empty string for the user is equivalent to removing user
+     * information.
+     *
+     * @param string $user The user name to use for authority.
+     * @param null|string $password The password associated with $user.
+     * @return static A new instance with the specified user information.
+     */
+    public function withUserInfo($user, $password = null);
+
+    /**
+     * Return an instance with the specified host.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified host.
+     *
+     * An empty host value is equivalent to removing the host.
+     *
+     * @param string $host The hostname to use with the new instance.
+     * @return static A new instance with the specified host.
+     * @throws \InvalidArgumentException for invalid hostnames.
+     */
+    public function withHost($host);
+
+    /**
+     * Return an instance with the specified port.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified port.
+     *
+     * Implementations MUST raise an exception for ports outside the
+     * established TCP and UDP port ranges.
+     *
+     * A null value provided for the port is equivalent to removing the port
+     * information.
+     *
+     * @param null|int $port The port to use with the new instance; a null value
+     *     removes the port information.
+     * @return static A new instance with the specified port.
+     * @throws \InvalidArgumentException for invalid ports.
+     */
+    public function withPort($port);
+
+    /**
+     * Return an instance with the specified path.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified path.
+     *
+     * The path can either be empty or absolute (starting with a slash) or
+     * rootless (not starting with a slash). Implementations MUST support all
+     * three syntaxes.
+     *
+     * If the path is intended to be domain-relative rather than path relative then
+     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
+     * are assumed to be relative to some base path known to the application or
+     * consumer.
+     *
+     * Users can provide both encoded and decoded path characters.
+     * Implementations ensure the correct encoding as outlined in getPath().
+     *
+     * @param string $path The path to use with the new instance.
+     * @return static A new instance with the specified path.
+     * @throws \InvalidArgumentException for invalid paths.
+     */
+    public function withPath($path);
+
+    /**
+     * Return an instance with the specified query string.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified query string.
+     *
+     * Users can provide both encoded and decoded query characters.
+     * Implementations ensure the correct encoding as outlined in getQuery().
+     *
+     * An empty query string value is equivalent to removing the query string.
+     *
+     * @param string $query The query string to use with the new instance.
+     * @return static A new instance with the specified query string.
+     * @throws \InvalidArgumentException for invalid query strings.
+     */
+    public function withQuery($query);
+
+    /**
+     * Return an instance with the specified URI fragment.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified URI fragment.
+     *
+     * Users can provide both encoded and decoded fragment characters.
+     * Implementations ensure the correct encoding as outlined in getFragment().
+     *
+     * An empty fragment value is equivalent to removing the fragment.
+     *
+     * @param string $fragment The fragment to use with the new instance.
+     * @return static A new instance with the specified fragment.
+     */
+    public function withFragment($fragment);
+
+    /**
+     * Return the string representation as a URI reference.
+     *
+     * Depending on which components of the URI are present, the resulting
+     * string is either a full URI or relative reference according to RFC 3986,
+     * Section 4.1. The method concatenates the various components of the URI,
+     * using the appropriate delimiters:
+     *
+     * - If a scheme is present, it MUST be suffixed by ":".
+     * - If an authority is present, it MUST be prefixed by "//".
+     * - The path can be concatenated without delimiters. But there are two
+     *   cases where the path has to be adjusted to make the URI reference
+     *   valid as PHP does not allow to throw an exception in __toString():
+     *     - If the path is rootless and an authority is present, the path MUST
+     *       be prefixed by "/".
+     *     - If the path is starting with more than one "/" and no authority is
+     *       present, the starting slashes MUST be reduced to one.
+     * - If a query is present, it MUST be prefixed by "?".
+     * - If a fragment is present, it MUST be prefixed by "#".
+     *
+     * @see http://tools.ietf.org/html/rfc3986#section-4.1
+     * @return string
+     */
+    public function __toString();
+}
index 3d14013..017ffc0 100644 (file)
@@ -227,21 +227,24 @@ class license_manager {
         $cache = \cache::make('core', 'license');
         $licenses = $cache->get('licenses');
 
-        if (empty($licenses)) {
+        if ($licenses === false) {
             $licenses = [];
             $records = $DB->get_records_select('license', null, null, 'sortorder ASC');
             foreach ($records as $license) {
-                // Interpret core license strings for internationalisation.
-                if ($license->custom == self::CORE_LICENSE) {
-                    $license->fullname = get_string($license->shortname, 'license');
-                } else {
-                    $license->fullname = format_string($license->fullname);
-                }
                 $licenses[$license->shortname] = $license;
             }
             $cache->set('licenses', $licenses);
         }
 
+        foreach ($licenses as $license) {
+            // Localise the license names.
+            if ($license->custom == self::CORE_LICENSE) {
+                $license->fullname = get_string($license->shortname, 'core_license');
+            } else {
+                $license->fullname = format_string($license->fullname);
+            }
+        }
+
         return $licenses;
     }
 
index c15e8d6..d1270bc 100644 (file)
@@ -7104,27 +7104,33 @@ function get_directory_size($rootdir, $excludefile='') {
  */
 function display_size($size) {
 
-    static $gb, $mb, $kb, $b;
+    static $units;
 
     if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
         return get_string('unlimited');
     }
 
-    if (empty($gb)) {
-        $gb = get_string('sizegb');
-        $mb = get_string('sizemb');
-        $kb = get_string('sizekb');
-        $b  = get_string('sizeb');
-    }
-
-    if ($size >= 1073741824) {
-        $size = round($size / 1073741824 * 10) / 10 . $gb;
-    } else if ($size >= 1048576) {
-        $size = round($size / 1048576 * 10) / 10 . $mb;
-    } else if ($size >= 1024) {
-        $size = round($size / 1024 * 10) / 10 . $kb;
+    if (empty($units)) {
+        $units[] = get_string('sizeb');
+        $units[] = get_string('sizekb');
+        $units[] = get_string('sizemb');
+        $units[] = get_string('sizegb');
+        $units[] = get_string('sizetb');
+        $units[] = get_string('sizepb');
+    }
+
+    if ($size >= 1024 ** 5) {
+        $size = round($size / 1024 ** 5 * 10) / 10 . $units[5];
+    } else if ($size >= 1024 ** 4) {
+        $size = round($size / 1024 ** 4 * 10) / 10 . $units[4];
+    } else if ($size >= 1024 ** 3) {
+        $size = round($size / 1024 ** 3 * 10) / 10 . $units[3];
+    } else if ($size >= 1024 ** 2) {
+        $size = round($size / 1024 ** 2 * 10) / 10 . $units[2];
+    } else if ($size >= 1024 ** 1) {
+        $size = round($size / 1024 ** 1 * 10) / 10 . $units[1];
     } else {
-        $size = intval($size) .' '. $b; // File sizes over 2GB can not work in 32bit PHP anyway.
+        $size = intval($size) .' '. $units[0]; // File sizes over 2GB can not work in 32bit PHP anyway.
     }
     return $size;
 }
index 730ef6e..19c3675 100644 (file)
@@ -62,9 +62,7 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
     // Add core nodes.
     // Full profile node.
     if (!empty($course)) {
-        if (empty($CFG->forceloginforprofiles) || $iscurrentuser ||
-            has_capability('moodle/user:viewdetails', $usercontext)
-            || has_coursecontact_role($user->id)) {
+        if (user_can_view_profile($user, null, $usercontext)) {
             $url = new moodle_url('/user/profile.php', array('id' => $user->id));
             $node = new core_user\output\myprofile\node('miscellaneous', 'fullprofile', get_string('fullprofile'), null, $url);
             $tree->add_node($node);
index 74e7c80..dd9bd24 100644 (file)
@@ -1385,15 +1385,20 @@ class core_renderer extends renderer_base {
     public function footer() {
         global $CFG, $DB;
 
+        $output = '';
+
         // Give plugins an opportunity to touch the page before JS is finalized.
         $pluginswithfunction = get_plugins_with_function('before_footer', 'lib.php');
         foreach ($pluginswithfunction as $plugins) {
             foreach ($plugins as $function) {
-                $function();
+                $extrafooter = $function();
+                if (is_string($extrafooter)) {
+                    $output .= $extrafooter;
+                }
             }
         }
 
-        $output = $this->container_end_all(true);
+        $output .= $this->container_end_all(true);
 
         $footer = $this->opencontainers->pop('header/footer');
 
@@ -2691,7 +2696,7 @@ $iconprogress
 EOD;
         if ($options->env != 'url') {
             $html .= <<<EOD
-    <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist border" style="position: relative">
+    <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist" style="position: relative">
     <div class="filepicker-filename">
         <div class="filepicker-container">$currentfile<div class="dndupload-message">$strdndenabled <br/><div class="dndupload-arrow"></div></div></div>
         <div class="dndupload-progressbars"></div>
@@ -3217,31 +3222,13 @@ EOD;
             return '';
         }
 
-        if ($id == false) {
-            $id = uniqid();
-        } else {
-            // Needs to be cleaned, we use it for the input id.
-            $id = clean_param($id, PARAM_ALPHANUMEXT);
-        }
-
-        // JS to animate the form.
-        $this->page->requires->js_call_amd('core/search-input', 'init', array($id));
-
-        $searchicon = html_writer::tag('div', $this->pix_icon('a/search', get_string('search', 'search'), 'moodle'),
-            array('role' => 'button', 'tabindex' => 0));
-        $formattrs = array('class' => 'search-input-form', 'action' => $CFG->wwwroot . '/search/index.php');
-        $inputattrs = array('type' => 'text', 'name' => 'q', 'placeholder' => get_string('search', 'search'),
-            'size' => 13, 'tabindex' => -1, 'id' => 'id_q_' . $id, 'class' => 'form-control');
-
-        $contents = html_writer::tag('label', get_string('enteryoursearchquery', 'search'),
-            array('for' => 'id_q_' . $id, 'class' => 'accesshide')) . html_writer::empty_tag('input', $inputattrs);
-        if ($this->page->context && $this->page->context->contextlevel !== CONTEXT_SYSTEM) {
-            $contents .= html_writer::empty_tag('input', ['type' => 'hidden',
-                    'name' => 'context', 'value' => $this->page->context->id]);
-        }
-        $searchinput = html_writer::tag('form', $contents, $formattrs);
-
-        return html_writer::tag('div', $searchicon . $searchinput, array('class' => 'search-input-wrapper nav-link', 'id' => $id));
+        $data = [
+            'action' => new moodle_url('/search/index.php'),
+            'hiddenfields' => (object) ['name' => 'context', 'value' => $this->page->context->id],
+            'inputname' => 'q',
+            'searchstring' => get_string('search'),
+            ];
+        return $this->render_from_template('core/search_input_navbar', $data);
     }
 
     /**
diff --git a/lib/php-enum/LICENSE b/lib/php-enum/LICENSE
new file mode 100644 (file)
index 0000000..2a8cf22
--- /dev/null
@@ -0,0 +1,18 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 My C-Labs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial
+portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/php-enum/readme_moodle.txt b/lib/php-enum/readme_moodle.txt
new file mode 100644 (file)
index 0000000..63b65d7
--- /dev/null
@@ -0,0 +1,7 @@
+Instructions to import php-enum into Moodle:
+
+1/ Download from https://github.com/myclabs/php-enum/releases
+
+2/ Copy the LICENSE file and the src folder into the lib/php-enum folder
+
+3/ Remove the src/PHPUnit folder, as it is not required
diff --git a/lib/php-enum/src/Enum.php b/lib/php-enum/src/Enum.php
new file mode 100644 (file)
index 0000000..f5a5940
--- /dev/null
@@ -0,0 +1,239 @@
+<?php
+/**
+ * @link    http://github.com/myclabs/php-enum
+ * @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
+ */
+
+namespace MyCLabs\Enum;
+
+/**
+ * Base Enum class
+ *
+ * Create an enum by implementing this class and adding class constants.
+ *
+ * @author Matthieu Napoli <matthieu@mnapoli.fr>
+ * @author Daniel Costa <danielcosta@gmail.com>
+ * @author Mirosław Filip <mirfilip@gmail.com>
+ *
+ * @psalm-template T
+ * @psalm-immutable
+ */
+abstract class Enum implements \JsonSerializable
+{
+    /**
+     * Enum value
+     *
+     * @var mixed
+     * @psalm-var T
+     */
+    protected $value;
+
+    /**
+     * Store existing constants in a static cache per object.
+     *
+     *
+     * @var array
+     * @psalm-var array<class-string, array<string, mixed>>
+     */
+    protected static $cache = [];
+
+    /**
+     * Creates a new value of some type
+     *
+     * @psalm-pure
+     * @param mixed $value
+     *
+     * @psalm-param static<T>|T $value
+     * @throws \UnexpectedValueException if incompatible type is given.
+     */
+    public function __construct($value)
+    {
+        if ($value instanceof static) {
+           /** @psalm-var T */
+            $value = $value->getValue();
+        }
+
+        if (!$this->isValid($value)) {
+            /** @psalm-suppress InvalidCast */
+            throw new \UnexpectedValueException("Value '$value' is not part of the enum " . static::class);
+        }
+
+        /** @psalm-var T */
+        $this->value = $value;
+    }
+
+    /**
+     * @psalm-pure
+     * @return mixed
+     * @psalm-return T
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * Returns the enum key (i.e. the constant name).
+     *
+     * @psalm-pure
+     * @return mixed
+     */
+    public function getKey()
+    {
+        return static::search($this->value);
+    }
+
+    /**
+     * @psalm-pure
+     * @psalm-suppress InvalidCast
+     * @return string
+     */
+    public function __toString()
+    {
+        return (string)$this->value;
+    }
+
+    /**
+     * Determines if Enum should be considered equal with the variable passed as a parameter.
+     * Returns false if an argument is an object of different class or not an object.
+     *
+     * This method is final, for more information read https://github.com/myclabs/php-enum/issues/4
+     *
+     * @psalm-pure
+     * @psalm-param mixed $variable
+     * @return bool
+     */
+    final public function equals($variable = null): bool
+    {
+        return $variable instanceof self
+            && $this->getValue() === $variable->getValue()
+            && static::class === \get_class($variable);
+    }
+
+    /**
+     * Returns the names (keys) of all constants in the Enum class
+     *
+     * @psalm-pure
+     * @psalm-return list<string>
+     * @return array
+     */
+    public static function keys()
+    {
+        return \array_keys(static::toArray());
+    }
+
+    /**
+     * Returns instances of the Enum class of all Enum constants
+     *
+     * @psalm-pure
+     * @psalm-return array<string, static>
+     * @return static[] Constant name in key, Enum instance in value
+     */
+    public static function values()
+    {
+        $values = array();
+
+        /** @psalm-var T $value */
+        foreach (static::toArray() as $key => $value) {
+            $values[$key] = new static($value);
+        }
+
+        return $values;
+    }
+
+    /**
+     * Returns all possible values as an array
+     *
+     * @psalm-pure
+     * @psalm-suppress ImpureStaticProperty
+     *
+     * @psalm-return array<string, mixed>
+     * @return array Constant name in key, constant value in value
+     */
+    public static function toArray()
+    {
+        $class = static::class;
+
+        if (!isset(static::$cache[$class])) {
+            $reflection            = new \ReflectionClass($class);
+            static::$cache[$class] = $reflection->getConstants();
+        }
+
+        return static::$cache[$class];
+    }
+
+    /**
+     * Check if is valid enum value
+     *
+     * @param $value
+     * @psalm-param mixed $value
+     * @psalm-pure
+     * @return bool
+     */
+    public static function isValid($value)
+    {
+        return \in_array($value, static::toArray(), true);
+    }
+
+    /**
+     * Check if is valid enum key
+     *
+     * @param $key
+     * @psalm-param string $key
+     * @psalm-pure
+     * @return bool
+     */
+    public static function isValidKey($key)
+    {
+        $array = static::toArray();
+
+        return isset($array[$key]) || \array_key_exists($key, $array);
+    }
+
+    /**
+     * Return key for value
+     *
+     * @param $value
+     *
+     * @psalm-param mixed $value
+     * @psalm-pure
+     * @return mixed
+     */
+    public static function search($value)
+    {
+        return \array_search($value, static::toArray(), true);
+    }
+
+    /**
+     * Returns a value when called statically like so: MyEnum::SOME_VALUE() given SOME_VALUE is a class constant
+     *
+     * @param string $name
+     * @param array  $arguments
+     *
+     * @return static
+     * @psalm-pure
+     * @throws \BadMethodCallException
+     */
+    public static function __callStatic($name, $arguments)
+    {
+        $array = static::toArray();
+        if (isset($array[$name]) || \array_key_exists($name, $array)) {
+            return new static($array[$name]);
+        }
+
+        throw new \BadMethodCallException("No static method or enum constant '$name' in class " . static::class);
+    }
+
+    /**
+     * Specify data which should be serialized to JSON. This method returns data that can be serialized by json_encode()
+     * natively.
+     *
+     * @return mixed
+     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
+     * @psalm-pure
+     */
+    public function jsonSerialize()
+    {
+        return $this->getValue();
+    }
+}
index 5a2b1ef..2133479 100644 (file)
@@ -1325,17 +1325,19 @@ function get_real_size($size = 0) {
     }
 
     static $binaryprefixes = array(
-        'K' => 1024,
-        'k' => 1024,
-        'M' => 1048576,
-        'm' => 1048576,
-        'G' => 1073741824,
-        'g' => 1073741824,
-        'T' => 1099511627776,
-        't' => 1099511627776,
+        'K' => 1024 ** 1,
+        'k' => 1024 ** 1,
+        'M' => 1024 ** 2,
+        'm' => 1024 ** 2,
+        'G' => 1024 ** 3,
+        'g' => 1024 ** 3,
+        'T' => 1024 ** 4,
+        't' => 1024 ** 4,
+        'P' => 1024 ** 5,
+        'p' => 1024 ** 5,
     );
 
-    if (preg_match('/^([0-9]+)([KMGT])/i', $size, $matches)) {
+    if (preg_match('/^([0-9]+)([KMGTP])/i', $size, $matches)) {
         return $matches[1] * $binaryprefixes[$matches[2]];
     }
 
diff --git a/lib/templates/checkbox.mustache b/lib/templates/checkbox.mustache
new file mode 100644 (file)
index 0000000..518e662
--- /dev/null
@@ -0,0 +1,34 @@
+{{!
+    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 core/checkbox
+
+    Chooser search template.
+
+    Example context (json):
+    {
+        "name": "fullsearch",
+        "id": "fullsearch",
+        "checked": true,
+        "value": "1",
+        "label": "Reset options"
+    }
+}}
+<div class="custom-control custom-checkbox">
+  <input type="checkbox" name={{{ name }}} class="custom-control-input" value={{{ value }}} id="{{{ id }}}" {{#checked}} checked="checked" {{/checked}}>
+  <label class="custom-control-label" for="{{{ id }}}">{{{ label }}}</label>
+</div>
\ No newline at end of file
index 2047541..610681e 100644 (file)
@@ -36,7 +36,7 @@
 <div class="popover-region collapsed {{$classes}}{{/classes}}"
     {{$attributes}}{{/attributes}}
     data-region="popover-region">
-    <div class="popover-region-toggle nav-link"
+    <div class="popover-region-toggle nav-link icon-no-margin"
         data-region="popover-region-toggle"
         role="button"
         aria-controls="popover-region-container-{{uniqid}}"
diff --git a/lib/templates/search_input.mustache b/lib/templates/search_input.mustache
new file mode 100644 (file)
index 0000000..f494a26
--- /dev/null
@@ -0,0 +1,75 @@
+{{!
+    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 core/search_input
+
+    Simple search input.
+
+    Example context (json):
+    {
+        "action": "https://moodle.local/admin/search.php",
+        "extraclasses": "my-2",
+        "inputname": "search",
+        "inform": false,
+        "searchstring": "Search settings",
+        "value": "policy",
+        "btnclass": "primary",
+        "query": "themedesigner",
+        "hiddenfields": [
+            {
+                "name": "context",
+                "value": "11"
+            }
+        ]
+    }
+}}
+<div class="simplesearchform {{{ extraclasses }}}">
+    {{^inform}}
+    <form autocomplete="off" action="{{{ action }}}" method="get" accept-charset="utf-8" class="mform form-inline simplesearchform">
+    {{/inform}}
+    {{#hiddenfields}}
+        <input type="hidden" name="{{ name }}" value="{{ value }}">
+    {{/hiddenfields}}
+    <div class="input-group">
+        <label for="searchinput-{{uniqid}}">
+            <span class="sr-only">{{{ searchstring }}}</span>
+        </label>
+        <input type="text"
+           id="searchinput-{{uniqid}}"
+           class="form-control"
+           placeholder="{{{ searchstring }}}"
+           aria-label="{{{ searchstring }}}"
+           name="{{{ inputname }}}"
+           data-region="input"
+           autocomplete="off"
+           value="{{{ query }}}"
+        >
+        <div class="input-group-append">
+            <button type="submit" class="btn {{^btnclass}}btn-submit{{/btnclass}} {{{ btnclass }}} search-icon">
+                {{#pix}} a/search, core {{/pix}}
+                <span class="sr-only">{{{ searchstring }}}</span>
+            </button>
+        </div>
+
+    </div>
+    {{#otherfields}}
+        <div  class="ml-2">{{{ otherfields }}}</div>
+    {{/otherfields}}
+{{^inform}}
+    </form>
+{{/inform}}
+</div>
\ No newline at end of file
diff --git a/lib/templates/search_input_auto.mustache b/lib/templates/search_input_auto.mustache
new file mode 100644 (file)
index 0000000..7e89b09
--- /dev/null
@@ -0,0 +1,63 @@
+{{!
+    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 core/search_input_auto
+
+    Search input that auto searches.
+
+    Example context (json):
+    {
+        "action": "https://moodle.local/admin/search.php",
+        "extraclasses": "my-2",
+        "inputname": "search",
+        "searchstring": "Search settings",
+        "sesskey": "sesskey",
+        "value": "policy",
+        "btnclass": "primary",
+        "hiddenfields": [
+            {
+                "name": "course",
+                "value": "11"
+            }
+        ]
+    }
+}}
+<div id="searchform-auto-{{uniqid}}" class="form-inline simplesearchform">
+    <div class="input-group searchbar" role="search">
+        <label for="searchinput">
+            <span class="sr-only">{{$label}}{{#str}} search, core {{/str}}{{/label}}</span>
+        </label>
+        <input
+           type="text"
+           data-region="input"
+           data-action="search"
+           id="searchinput"
+           class="form-control withclear"
+           placeholder="{{$placeholder}}{{#str}} search, core {{/str}}{{/placeholder}}"
+           name="search"
+           autocomplete="off"
+        >
+        <button
+            class="btn btn-clear d-none"
+            data-action="clearsearch"
+            type="button"
+        >
+           {{#pix}} e/cancel, core {{/pix}}
+            <span class="sr-only">{{#str}} clearsearch, core {{/str}}</span>
+        </button>
+    </div>
+</div>
diff --git a/lib/templates/search_input_navbar.mustache b/lib/templates/search_input_navbar.mustache
new file mode 100644 (file)
index 0000000..4f07bc4
--- /dev/null
@@ -0,0 +1,116 @@
+{{!
+    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 core/search_input_navbar
+
+    Navbar search input template.
+
+    Example context (json):
+    {
+        "action": "https://moodle.local/admin/search.php",
+        "inputname": "search",
+        "searchstring": "Search",
+        "hiddenfields": [
+            {
+                "name": "cmid",
+                "value": "11"
+            }
+        ]
+    }
+}}
+<div id="searchinput-navbar-{{uniqid}}" class="simplesearchform">
+    <div class="collapse" id="searchform-navbar">
+        <form autocomplete="off" action="{{{ action }}}" method="get" accept-charset="utf-8" class="mform form-inline searchform-navbar">
+            {{#hiddenfields}}
+                <input type="hidden" name="{{ name }}" value="{{ value }}">
+            {{/hiddenfields}}
+            <div class="input-group">
+                <label for="searchinput-{{uniqid}}">
+                    <span class="sr-only">{{{ searchstring }}}</span>
+                </label>
+                    <input type="text"
+                       id="searchinput-{{uniqid}}"
+                       class="form-control withclear"
+                       placeholder="{{{ searchstring }}}"
+                       aria-label="{{{ searchstring }}}"
+                       name="{{{ inputname }}}"
+                       data-region="input"
+                       autocomplete="off"
+                    >
+                    <a class="btn btn-close"
+                        data-action="closesearch"
+                        data-toggle="collapse"
+                        href="#searchform-navbar"
+                        role="button"
+                    >
+                        {{#pix}} e/cancel, core {{/pix}}
+                        <span class="sr-only">{{#str}} closebuttontitle {{/str}}</span>
+                    </a>
+                <div class="input-group-append">
+                    <button type="submit" class="btn btn-submit" data-action="submit">
+                        {{#pix}} a/search, core {{/pix}}
+                        <span class="sr-only">{{{ searchstring }}}</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </div>
+    <a
+        class="btn btn-open"
+        data-toggle="collapse"
+        data-action="opensearch"
+        href="#searchform-navbar"
+        role="button"
+        aria-expanded="false"
+        aria-controls="searchform-navbar"
+    >
+        <i class="icon fa fa-search fa-fw " aria-hidden="true"></i>
+        <span class="sr-only">{{#str}} togglesearch {{/str}}</span>
+    </a>
+</div>
+
+{{#js}}
+require(
+[
+    'jquery',
+],
+function(
+    $
+) {
+    var uniqid = "{{uniqid}}";
+    var container = $('#searchinput-navbar-' + uniqid);
+    var opensearch = container.find('[data-action="opensearch"]');
+    var input = container.find('[data-region="input"]');
+    var submit = container.find('[data-action="submit"');
+
+    submit.on('click', function(e) {
+        if (input.val() === '') {
+            e.preventDefault();
+        }
+    });
+    container.on('hidden.bs.collapse', function() {
+        opensearch.removeClass('d-none');
+        input.val('');
+    });
+    container.on('show.bs.collapse', function() {
+        opensearch.addClass('d-none');
+    });
+    container.on('shown.bs.collapse', function() {
+        input.focus();
+    });
+});
+{{/js}}
\ No newline at end of file
index cbb7dc2..0b89adb 100644 (file)
@@ -4776,4 +4776,45 @@ class core_moodlelib_testcase extends advanced_testcase {
         $file = $CFG->dataroot . '/argh.txt';
         $this->assertFalse(rename_to_unused_name($file));
     }
+
+    /**
+     * Provider for display_size
+     *
+     * @return array of ($size, $expected)
+     */
+    public function display_size_provider() {
+
+        return [
+            [0,     '0 bytes'    ],
+            [1,     '1 bytes'    ],
+            [1023,  '1023 bytes' ],
+            [1024,      '1KB'    ],
+            [2222,      '2.2KB'  ],
+            [33333,     '32.6KB' ],
+            [444444,    '434KB'  ],
+            [5555555,       '5.3MB'  ],
+            [66666666,      '63.6MB' ],
+            [777777777,     '741.7MB'],
+            [8888888888,        '8.3GB'  ],
+            [99999999999,       '93.1GB' ],
+            [111111111111,      '103.5GB'],
+            [2222222222222,         '2TB'    ],
+            [33333333333333,        '30.3TB' ],
+            [444444444444444,       '404.2TB'],
+            [5555555555555555,          '4.9PB'  ],
+            [66666666666666666,         '59.2PB' ],
+            [777777777777777777,        '690.8PB'],
+        ];
+    }
+
+    /**
+     * Test display_size
+     * @dataProvider display_size_provider
+     * @param int $size the size in bytes
+     * @param string $expected the expected string.
+     */
+    public function test_display_size($size, $expected) {
+        $result = display_size($size);
+        $this->assertEquals($expected, $result);
+    }
 }
index 3e00f6b..bed8fc7 100644 (file)
@@ -450,17 +450,23 @@ class core_setuplib_testcase extends advanced_testcase {
      */
     public function data_for_test_get_real_size() {
         return array(
-            array('8KB', 8192),
-            array('8Kb', 8192),
-            array('8K', 8192),
-            array('8k', 8192),
-            array('50MB', 52428800),
-            array('50Mb', 52428800),
-            array('50M', 52428800),
-            array('50m', 52428800),
-            array('8Gb', 8589934592),
-            array('8GB', 8589934592),
-            array('8G', 8589934592),
+            array('8KB',    8192),
+            array('8Kb',    8192),
+            array('8K',     8192),
+            array('8k',     8192),
+            array('50MB',   52428800),
+            array('50Mb',   52428800),
+            array('50M',    52428800),
+            array('50m',    52428800),
+            array('8GB',    8589934592),
+            array('8Gb',    8589934592),
+            array('8G',     8589934592),
+            array('7T',     7696581394432),
+            array('7TB',    7696581394432),
+            array('7Tb',    7696581394432),
+            array('6P',     6755399441055744),
+            array('6PB',    6755399441055744),
+            array('6Pb',    6755399441055744),
         );
     }
 
index 812cf83..2f2733e 100644 (file)
     <license>MIT</license>
     <version>2.0.1</version>
   </library>
+  <library>
+    <location>zipstream</location>
+    <name>ZipStream-PHP</name>
+    <license>MIT</license>
+    <version>2.1.0</version>
+  </library>
+  <library>
+    <location>php-enum</location>
+    <name>php-enum</name>
+    <license>MIT</license>
+    <version>1.7.6</version>
+  </library>
+  <library>
+    <location>http-message</location>
+    <name>http-message</name>
+    <license>MIT</license>
+    <version>1.0.1</version>
+  </library>
 </libraries>
index 23c54ee..1e1f294 100644 (file)
@@ -2,6 +2,7 @@ This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
 === 3.10 ===
+* Retains the source course id when a course is copied from another course on the same site.
 * Added function setScrollable in core/modal. This function can be used to set the modal's body to be scrollable or not
   when the modal's height exceeds the browser's height. This is also supported in core/modal_factory through the
   'scrollable' config parameter which can be set to either true or false. If not explicitly defined, the default value
@@ -42,6 +43,11 @@ information provided here is intended especially for developers.
   be called before executing a task, and a new function \core\task\manager::get_running_tasks()
   returns information about currently-running tasks.
 * New library function rename_to_unused_name() to rename a file within its current location.
+* Constant \core_h5p\file_storage::EDITOR_FILEAREA has been deprecated
+  because it's not required any more.
+* The ZipStream-PHP library has been added to Moodle core in /lib/zipstream.
+* The php-enum library has been added to Moodle core in /lib/php-enum.
+* The http-message library has been added to Moodle core in /lib/http-message.
 
 === 3.9 ===
 * Following function has been deprecated, please use \core\task\manager::run_from_cli().
diff --git a/lib/zipstream/LICENSE b/lib/zipstream/LICENSE
new file mode 100644 (file)
index 0000000..ebe7fe2
--- /dev/null
@@ -0,0 +1,24 @@
+MIT License
+
+Copyright (C) 2007-2009 Paul Duncan <pabs@pablotron.org>
+Copyright (C) 2014 Jonatan Männchen <jonatan@maennchen.ch>
+Copyright (C) 2014 Jesse G. Donat <donatj@gmail.com>
+Copyright (C) 2018 Nicolas CARPi <nicolas.carpi@curie.fr>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/lib/zipstream/readme_moodle.txt b/lib/zipstream/readme_moodle.txt
new file mode 100644 (file)
index 0000000..dcdf816
--- /dev/null
@@ -0,0 +1,8 @@
+Instructions to import ZipStream into Moodle:
+
+1/ Download from https://github.com/maennchen/ZipStream-PHP/releases/
+
+2/ Copy the LICENSE file and the src folder into the lib/zipstream folder
+
+3/ Ensure any dependencies are also imported (eg psr/http-message and myclabs/php-enum).
+   The dependencies will be listed in the "require" section of the library's composer.json file
diff --git a/lib/zipstream/src/Bigint.php b/lib/zipstream/src/Bigint.php
new file mode 100644 (file)
index 0000000..52ccfd2
--- /dev/null
@@ -0,0 +1,172 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream;
+
+use OverflowException;
+
+class Bigint
+{
+    /**
+     * @var int[]
+     */
+    private $bytes = [0, 0, 0, 0, 0, 0, 0, 0];
+
+    /**
+     * Initialize the bytes array
+     *
+     * @param int $value
+     */
+    public function __construct(int $value = 0)
+    {
+        $this->fillBytes($value, 0, 8);
+    }
+
+    /**
+     * Fill the bytes field with int
+     *
+     * @param int $value
+     * @param int $start
+     * @param int $count
+     * @return void
+     */
+    protected function fillBytes(int $value, int $start, int $count): void
+    {
+        for ($i = 0; $i < $count; $i++) {
+            $this->bytes[$start + $i] = $i >= PHP_INT_SIZE ? 0 : $value & 0xFF;
+            $value >>= 8;
+        }
+    }
+
+    /**
+     * Get an instance
+     *
+     * @param int $value
+     * @return Bigint
+     */
+    public static function init(int $value = 0): self
+    {
+        return new self($value);
+    }
+
+    /**
+     * Fill bytes from low to high
+     *
+     * @param int $low
+     * @param int $high
+     * @return Bigint
+     */
+    public static function fromLowHigh(int $low, int $high): self
+    {
+        $bigint = new Bigint();
+        $bigint->fillBytes($low, 0, 4);
+        $bigint->fillBytes($high, 4, 4);
+        return $bigint;
+    }
+
+    /**
+     * Get high 32
+     *
+     * @return int
+     */
+    public function getHigh32(): int
+    {
+        return $this->getValue(4, 4);
+    }
+
+    /**
+     * Get value from bytes array
+     *
+     * @param int $end
+     * @param int $length
+     * @return int
+     */
+    public function getValue(int $end = 0, int $length = 8): int
+    {
+        $result = 0;
+        for ($i = $end + $length - 1; $i >= $end; $i--) {
+            $result <<= 8;
+            $result |= $this->bytes[$i];
+        }
+        return $result;
+    }
+
+    /**
+     * Get low FF
+     *
+     * @param bool $force
+     * @return float
+     */
+    public function getLowFF(bool $force = false): float
+    {
+        if ($force || $this->isOver32()) {
+            return (float)0xFFFFFFFF;
+        }
+        return (float)$this->getLow32();
+    }
+
+    /**
+     * Check if is over 32
+     *
+     * @param bool $force
+     * @return bool
+     */
+    public function isOver32(bool $force = false): bool
+    {
+        // value 0xFFFFFFFF already needs a Zip64 header
+        return $force ||
+            max(array_slice($this->bytes, 4, 4)) > 0 ||
+            min(array_slice($this->bytes, 0, 4)) === 0xFF;
+    }
+
+    /**
+     * Get low 32
+     *
+     * @return int
+     */
+    public function getLow32(): int
+    {
+        return $this->getValue(0, 4);
+    }
+
+    /**
+     * Get hexadecimal
+     *
+     * @return string
+     */
+    public function getHex64(): string
+    {
+        $result = '0x';
+        for ($i = 7; $i >= 0; $i--) {
+            $result .= sprintf('%02X', $this->bytes[$i]);
+        }
+        return $result;
+    }
+
+    /**
+     * Add
+     *
+     * @param Bigint $other
+     * @return Bigint
+     */
+    public function add(Bigint $other): Bigint
+    {
+        $result = clone $this;
+        $overflow = false;
+        for ($i = 0; $i < 8; $i++) {
+            $result->bytes[$i] += $other->bytes[$i];
+            if ($overflow) {
+                $result->bytes[$i]++;
+                $overflow = false;
+            }
+            if ($result->bytes[$i] & 0x100) {
+                $overflow = true;
+                $result->bytes[$i] &= 0xFF;
+            }
+        }
+        if ($overflow) {
+            throw new OverflowException;
+        }
+        return $result;
+    }
+}
diff --git a/lib/zipstream/src/DeflateStream.php b/lib/zipstream/src/DeflateStream.php
new file mode 100644 (file)
index 0000000..d6c2728
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream;
+
+class DeflateStream extends Stream
+{
+    protected $filter;
+
+    /**
+     * @var Option\File
+     */
+    protected $options;
+
+    /**
+     * Rewind stream
+     *
+     * @return void
+     */
+    public function rewind(): void
+    {
+        // deflate filter needs to be removed before rewind
+        if ($this->filter) {
+            $this->removeDeflateFilter();
+            $this->seek(0);
+            $this->addDeflateFilter($this->options);
+        } else {
+            rewind($this->stream);
+        }
+    }
+
+    /**
+     * Remove the deflate filter
+     *
+     * @return void
+     */
+    public function removeDeflateFilter(): void
+    {
+        if (!$this->filter) {
+            return;
+        }
+        stream_filter_remove($this->filter);
+        $this->filter = null;
+    }
+
+    /**
+     * Add a deflate filter
+     *
+     * @param Option\File $options
+     * @return void
+     */
+    public function addDeflateFilter(Option\File $options): void
+    {
+        $this->options = $options;
+        // parameter 4 for stream_filter_append expects array
+        // so we convert the option object in an array
+        $optionsArr = [
+            'comment' => $options->getComment(),
+            'method' => $options->getMethod(),
+            'deflateLevel' => $options->getDeflateLevel(),
+            'time' => $options->getTime()
+        ];
+        $this->filter = stream_filter_append(
+            $this->stream,
+            'zlib.deflate',
+            STREAM_FILTER_READ,
+            $optionsArr
+        );
+    }
+}
diff --git a/lib/zipstream/src/Exception.php b/lib/zipstream/src/Exception.php
new file mode 100644 (file)
index 0000000..18ccfbb
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream;
+
+/**
+ * This class is only for inheriting
+ */
+abstract class Exception extends \Exception
+{
+}
diff --git a/lib/zipstream/src/Exception/EncodingException.php b/lib/zipstream/src/Exception/EncodingException.php
new file mode 100644 (file)
index 0000000..5134ea8
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream\Exception;
+
+use ZipStream\Exception;
+
+/**
+ * This Exception gets invoked if file or comment encoding is incorrect
+ */
+class EncodingException extends Exception
+{
+}
diff --git a/lib/zipstream/src/Exception/FileNotFoundException.php b/lib/zipstream/src/Exception/FileNotFoundException.php
new file mode 100644 (file)
index 0000000..ac1a4d5
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream\Exception;
+
+use ZipStream\Exception;
+
+/**
+ * This Exception gets invoked if a file wasn't found
+ */
+class FileNotFoundException extends Exception
+{
+    /**
+     * Constructor of the Exception
+     *
+     * @param String $path - The path which wasn't found
+     */
+    public function __construct(string $path)
+    {
+        parent::__construct("The file with the path $path wasn't found.");
+    }
+}
diff --git a/lib/zipstream/src/Exception/FileNotReadableException.php b/lib/zipstream/src/Exception/FileNotReadableException.php
new file mode 100644 (file)
index 0000000..e8b4e5e
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream\Exception;
+
+use ZipStream\Exception;
+
+/**
+ * This Exception gets invoked if a file wasn't found
+ */
+class FileNotReadableException extends Exception
+{
+    /**
+     * Constructor of the Exception
+     *
+     * @param String $path - The path which wasn't found
+     */
+    public function __construct(string $path)
+    {
+        parent::__construct("The file with the path $path isn't readable.");
+    }
+}
diff --git a/lib/zipstream/src/Exception/IncompatibleOptionsException.php b/lib/zipstream/src/Exception/IncompatibleOptionsException.php
new file mode 100644 (file)
index 0000000..ebed364
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream\Exception;
+
+use ZipStream\Exception;
+
+/**
+ * This Exception gets invoked if options are incompatible
+ */
+class IncompatibleOptionsException extends Exception
+{
+}
diff --git a/lib/zipstream/src/Exception/OverflowException.php b/lib/zipstream/src/Exception/OverflowException.php
new file mode 100644 (file)
index 0000000..c46cbef
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream\Exception;
+
+use ZipStream\Exception;
+
+/**
+ * This Exception gets invoked if a counter value exceeds storage size
+ */
+class OverflowException extends Exception
+{
+    public function __construct()
+    {
+        parent::__construct('File size exceeds limit of 32 bit integer. Please enable "zip64" option.');
+    }
+}
diff --git a/lib/zipstream/src/Exception/StreamNotReadableException.php b/lib/zipstream/src/Exception/StreamNotReadableException.php
new file mode 100644 (file)
index 0000000..c4ed251
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream\Exception;
+
+use ZipStream\Exception;
+
+/**
+ * This Exception gets invoked if `fread` fails on a stream.
+ */
+class StreamNotReadableException extends Exception
+{
+    /**
+     * Constructor of the Exception
+     *
+     * @param string $fileName - The name of the file which the stream belongs to.
+     */
+    public function __construct(string $fileName)
+    {
+        parent::__construct("The stream for $fileName could not be read.");
+    }
+}
diff --git a/lib/zipstream/src/File.php b/lib/zipstream/src/File.php
new file mode 100644 (file)
index 0000000..2200a36
--- /dev/null
@@ -0,0 +1,477 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream;
+
+use Psr\Http\Message\StreamInterface;
+use ZipStream\Exception\EncodingException;
+use ZipStream\Exception\FileNotFoundException;
+use ZipStream\Exception\FileNotReadableException;
+use ZipStream\Exception\OverflowException;
+use ZipStream\Option\File as FileOptions;
+use ZipStream\Option\Method;
+use ZipStream\Option\Version;
+
+class File
+{
+    const HASH_ALGORITHM = 'crc32b';
+
+    const BIT_ZERO_HEADER = 0x0008;
+    const BIT_EFS_UTF8 = 0x0800;
+
+    const COMPUTE = 1;
+    const SEND = 2;
+
+    private const CHUNKED_READ_BLOCK_SIZE = 1048576;
+
+    /**
+     * @var string
+     */
+    public $name;
+
+    /**
+     * @var FileOptions
+     */
+    public $opt;
+
+    /**
+     * @var Bigint
+     */
+    public $len;
+    /**
+     * @var Bigint
+     */
+    public $zlen;
+
+    /** @var  int */
+    public $crc;
+
+    /**
+     * @var Bigint
+     */
+    public $hlen;
+
+    /**
+     * @var Bigint
+     */
+    public $ofs;
+
+    /**
+     * @var int
+     */
+    public $bits;
+
+    /**
+     * @var Version
+     */
+    public $version;
+
+    /**
+     * @var ZipStream
+     */
+    public $zip;
+
+    /**
+     * @var resource
+     */
+    private $deflate;
+    /**
+     * @var resource
+     */
+    private $hash;
+
+    /**
+     * @var Method
+     */
+    private $method;
+
+    /**
+     * @var Bigint
+     */
+    private $totalLength;
+
+    public function __construct(ZipStream $zip, string $name, ?FileOptions $opt = null)
+    {
+        $this->zip = $zip;
+
+        $this->name = $name;
+        $this->opt = $opt ?: new FileOptions();
+        $this->method = $this->opt->getMethod();
+        $this->version = Version::STORE();
+        $this->ofs = new Bigint();
+    }
+
+    public function processPath(string $path): void
+    {
+        if (!is_readable($path)) {
+            if (!file_exists($path)) {
+                throw new FileNotFoundException($path);
+            }
+            throw new FileNotReadableException($path);
+        }
+        if ($this->zip->isLargeFile($path) === false) {
+            $data = file_get_contents($path);
+            $this->processData($data);
+        } else {
+            $this->method = $this->zip->opt->getLargeFileMethod();
+
+            $stream = new DeflateStream(fopen($path, 'rb'));
+            $this->processStream($stream);
+            $stream->close();
+        }
+    }
+
+    public function processData(string $data): void
+    {
+        $this->len = new Bigint(strlen($data));
+        $this->crc = crc32($data);
+
+        // compress data if needed
+        if ($this->method->equals(Method::DEFLATE())) {
+            $data = gzdeflate($data);
+        }
+
+        $this->zlen = new Bigint(strlen($data));
+        $this->addFileHeader();
+        $this->zip->send($data);
+        $this->addFileFooter();
+    }
+
+    /**
+     * Create and send zip header for this file.
+     *
+     * @return void
+     * @throws \ZipStream\Exception\EncodingException
+     */
+    public function addFileHeader(): void
+    {
+        $name = static::filterFilename($this->name);
+
+        // calculate name length
+        $nameLength = strlen($name);
+
+        // create dos timestamp
+        $time = static::dosTime($this->opt->getTime()->getTimestamp());
+
+        $comment = $this->opt->getComment();
+
+        if (!mb_check_encoding($name, 'ASCII') ||
+            !mb_check_encoding($comment, 'ASCII')) {
+            // Sets Bit 11: Language encoding flag (EFS).  If this bit is set,
+            // the filename and comment fields for this file
+            // MUST be encoded using UTF-8. (see APPENDIX D)
+            if (!mb_check_encoding($name, 'UTF-8') ||
+                !mb_check_encoding($comment, 'UTF-8')) {
+                throw new EncodingException(
+                    'File name and comment should use UTF-8 ' .
+                    'if one of them does not fit into ASCII range.'
+                );
+            }
+            $this->bits |= self::BIT_EFS_UTF8;
+        }
+
+        if ($this->method->equals(Method::DEFLATE())) {
+            $this->version = Version::DEFLATE();
+        }
+
+        $force = (boolean)($this->bits & self::BIT_ZERO_HEADER) &&
+            $this->zip->opt->isEnableZip64();
+
+        $footer = $this->buildZip64ExtraBlock($force);
+
+        // If this file will start over 4GB limit in ZIP file,
+        // CDR record will have to use Zip64 extension to describe offset
+        // to keep consistency we use the same value here
+        if ($this->zip->ofs->isOver32()) {
+            $this->version = Version::ZIP64();
+        }
+
+        $fields = [
+            ['V', ZipStream::FILE_HEADER_SIGNATURE],
+            ['v', $this->version->getValue()],      // Version needed to Extract
+            ['v', $this->bits],                     // General purpose bit flags - data descriptor flag set
+            ['v', $this->method->getValue()],       // Compression method
+            ['V', $time],                           // Timestamp (DOS Format)
+            ['V', $this->crc],                      // CRC32 of data (0 -> moved to data descriptor footer)
+            ['V', $this->zlen->getLowFF($force)],   // Length of compressed data (forced to 0xFFFFFFFF for zero header)
+            ['V', $this->len->getLowFF($force)],    // Length of original data (forced to 0xFFFFFFFF for zero header)
+            ['v', $nameLength],                     // Length of filename
+            ['v', strlen($footer)],                 // Extra data (see above)
+        ];
+
+        // pack fields and calculate "total" length
+        $header = ZipStream::packFields($fields);
+
+        // print header and filename
+        $data = $header . $name . $footer;
+        $this->zip->send($data);
+
+        // save header length
+        $this->hlen = Bigint::init(strlen($data));
+    }
+
+    /**
+     * Strip characters that are not legal in Windows filenames
+     * to prevent compatibility issues
+     *
+     * @param string $filename Unprocessed filename
+     * @return string
+     */
+    public static function filterFilename(string $filename): string
+    {
+        // strip leading slashes from file name
+        // (fixes bug in windows archive viewer)
+        $filename = preg_replace('/^\\/+/', '', $filename);
+
+        return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $filename);
+    }
+
+    /**
+     * Convert a UNIX timestamp to a DOS timestamp.
+     *
+     * @param int $when
+     * @return int DOS Timestamp
+     */
+    final protected static function dosTime(int $when): int
+    {
+        // get date array for timestamp
+        $d = getdate($when);
+
+        // set lower-bound on dates
+        if ($d['year'] < 1980) {
+            $d = array(
+                'year' => 1980,
+                'mon' => 1,
+                'mday' => 1,
+                'hours' => 0,
+                'minutes' => 0,
+                'seconds' => 0
+            );
+        }
+
+        // remove extra years from 1980
+        $d['year'] -= 1980;
+
+        // return date string
+        return
+            ($d['year'] << 25) |
+            ($d['mon'] << 21) |
+            ($d['mday'] << 16) |
+            ($d['hours'] << 11) |
+            ($d['minutes'] << 5) |
+            ($d['seconds'] >> 1);
+    }
+
+    protected function buildZip64ExtraBlock(bool $force = false): string
+    {
+
+        $fields = [];
+        if ($this->len->isOver32($force)) {
+            $fields[] = ['P', $this->len];          // Length of original data
+        }
+
+        if ($this->len->isOver32($force)) {
+            $fields[] = ['P', $this->zlen];         // Length of compressed data
+        }
+
+        if ($this->ofs->isOver32()) {
+            $fields[] = ['P', $this->ofs];          // Offset of local header record
+        }
+
+        if (!empty($fields)) {
+            if (!$this->zip->opt->isEnableZip64()) {
+                throw new OverflowException();
+            }
+
+            array_unshift(
+                $fields,
+                ['v', 0x0001],                      // 64 bit extension
+                ['v', count($fields) * 8]             // Length of data block
+            );
+            $this->version = Version::ZIP64();
+        }
+
+        return ZipStream::packFields($fields);
+    }
+
+    /**
+     * Create and send data descriptor footer for this file.
+     *
+     * @return void
+     */
+
+    public function addFileFooter(): void
+    {
+
+        if ($this->bits & self::BIT_ZERO_HEADER) {
+            // compressed and uncompressed size
+            $sizeFormat = 'V';
+            if ($this->zip->opt->isEnableZip64()) {
+                $sizeFormat = 'P';
+            }
+            $fields = [
+                ['V', ZipStream::DATA_DESCRIPTOR_SIGNATURE],
+                ['V', $this->crc],              // CRC32
+                [$sizeFormat, $this->zlen],     // Length of compressed data
+                [$sizeFormat, $this->len],      // Length of original data
+            ];
+
+            $footer = ZipStream::packFields($fields);
+            $this->zip->send($footer);
+        } else {
+            $footer = '';
+        }
+        $this->totalLength = $this->hlen->add($this->zlen)->add(Bigint::init(strlen($footer)));
+        $this->zip->addToCdr($this);
+    }
+
+    public function processStream(StreamInterface $stream): void
+    {
+        $this->zlen = new Bigint();
+        $this->len = new Bigint();
+
+        if ($this->zip->opt->isZeroHeader()) {
+            $this->processStreamWithZeroHeader($stream);
+        } else {
+            $this->processStreamWithComputedHeader($stream);
+        }
+    }
+
+    protected function processStreamWithZeroHeader(StreamInterface $stream): void
+    {
+        $this->bits |= self::BIT_ZERO_HEADER;
+        $this->addFileHeader();
+        $this->readStream($stream, self::COMPUTE | self::SEND);
+        $this->addFileFooter();
+    }
+
+    protected function readStream(StreamInterface $stream, ?int $options = null): void
+    {
+        $this->deflateInit();
+        $total = 0;
+        $size = $this->opt->getSize();
+        while (!$stream->eof() && ($size === 0 || $total < $size)) {
+            $data = $stream->read(self::CHUNKED_READ_BLOCK_SIZE);
+            $total += strlen($data);
+            if ($size > 0 && $total > $size) {
+                $data = substr($data, 0 , strlen($data)-($total - $size));
+            }
+            $this->deflateData($stream, $data, $options);
+            if ($options & self::SEND) {
+                $this->zip->send($data);
+            }
+        }
+        $this->deflateFinish($options);
+    }
+
+    protected function deflateInit(): void
+    {
+        $this->hash = hash_init(self::HASH_ALGORITHM);
+        if ($this->method->equals(Method::DEFLATE())) {
+            $this->deflate = deflate_init(
+                ZLIB_ENCODING_RAW,
+                ['level' => $this->opt->getDeflateLevel()]
+            );
+        }
+    }
+
+    protected function deflateData(StreamInterface $stream, string &$data, ?int $options = null): void
+    {
+        if ($options & self::COMPUTE) {
+            $this->len = $this->len->add(Bigint::init(strlen($data)));
+            hash_update($this->hash, $data);
+        }
+        if ($this->deflate) {
+            $data = deflate_add(
+                $this->deflate,
+                $data,
+                $stream->eof()
+                    ? ZLIB_FINISH
+                    : ZLIB_NO_FLUSH
+            );
+        }
+        if ($options & self::COMPUTE) {
+            $this->zlen = $this->zlen->add(Bigint::init(strlen($data)));
+        }
+    }
+
+    protected function deflateFinish(?int $options = null): void
+    {
+        if ($options & self::COMPUTE) {
+            $this->crc = hexdec(hash_final($this->hash));
+        }
+    }
+
+    protected function processStreamWithComputedHeader(StreamInterface $stream): void
+    {
+        $this->readStream($stream, self::COMPUTE);
+        $stream->rewind();
+
+        // incremental compression with deflate_add
+        // makes this second read unnecessary
+        // but it is only available from PHP 7.0
+        if (!$this->deflate && $stream instanceof DeflateStream && $this->method->equals(Method::DEFLATE())) {
+            $stream->addDeflateFilter($this->opt);
+            $this->zlen = new Bigint();
+            while (!$stream->eof()) {
+                $data = $stream->read(self::CHUNKED_READ_BLOCK_SIZE);
+                $this->zlen = $this->zlen->add(Bigint::init(strlen($data)));
+            }
+            $stream->rewind();
+        }
+
+        $this->addFileHeader();
+        $this->readStream($stream, self::SEND);
+        $this->addFileFooter();
+    }
+
+    /**
+     * Send CDR record for specified file.
+     *
+     * @return string
+     */
+    public function getCdrFile(): string
+    {
+        $name = static::filterFilename($this->name);
+
+        // get attributes
+        $comment = $this->opt->getComment();
+
+        // get dos timestamp
+        $time = static::dosTime($this->opt->getTime()->getTimestamp());
+
+        $footer = $this->buildZip64ExtraBlock();
+
+        $fields = [
+            ['V', ZipStream::CDR_FILE_SIGNATURE],   // Central file header signature
+            ['v', ZipStream::ZIP_VERSION_MADE_BY],  // Made by version
+            ['v', $this->version->getValue()],      // Extract by version
+            ['v', $this->bits],                     // General purpose bit flags - data descriptor flag set
+            ['v', $this->method->getValue()],       // Compression method
+            ['V', $time],                           // Timestamp (DOS Format)
+            ['V', $this->crc],                      // CRC32
+            ['V', $this->zlen->getLowFF()],         // Compressed Data Length
+            ['V', $this->len->getLowFF()],          // Original Data Length
+            ['v', strlen($name)],                   // Length of filename
+            ['v', strlen($footer)],                 // Extra data len (see above)
+            ['v', strlen($comment)],                // Length of comment
+            ['v', 0],                               // Disk number
+            ['v', 0],                               // Internal File Attributes
+            ['V', 32],                              // External File Attributes
+            ['V', $this->ofs->getLowFF()]           // Relative offset of local header
+        ];
+
+        // pack fields, then append name and comment
+        $header = ZipStream::packFields($fields);
+
+        return $header . $name . $footer . $comment;
+    }
+
+    /**
+     * @return Bigint
+     */
+    public function getTotalLength(): Bigint
+    {
+        return $this->totalLength;
+    }
+}
diff --git a/lib/zipstream/src/Option/Archive.php b/lib/zipstream/src/Option/Archive.php
new file mode 100644 (file)
index 0000000..b6b95cc
--- /dev/null
@@ -0,0 +1,261 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream\Option;
+
+final class Archive
+{
+    const DEFAULT_DEFLATE_LEVEL = 6;
+    /**
+     * @var string
+     */
+    private $comment = '';
+    /**
+     * Size, in bytes, of the largest file to try
+     * and load into memory (used by
+     * addFileFromPath()).  Large files may also
+     * be compressed differently; see the
+     * 'largeFileMethod' option. Default is ~20 Mb.
+     *
+     * @var int
+     */
+    private $largeFileSize = 20 * 1024 * 1024;
+    /**
+     * How to handle large files.  Legal values are
+     * Method::STORE() (the default), or
+     * Method::DEFLATE(). STORE sends the file
+     * raw and is significantly
+     * faster, while DEFLATE compresses the file
+     * and is much, much slower. Note that DEFLATE
+     * must compress the file twice and is extremely slow.
+     *
+     * @var Method
+     */
+    private $largeFileMethod;
+    /**
+     * Boolean indicating whether or not to send
+     * the HTTP headers for this file.
+     *
+     * @var bool
+     */
+    private $sendHttpHeaders = false;
+    /**
+     * The method called to send headers
+     *
+     * @var Callable
+     */
+    private $httpHeaderCallback = 'header';
+    /**
+     * Enable Zip64 extension, supporting very large
+     * archives (any size > 4 GB or file count > 64k)
+     *
+     * @var bool
+     */
+    private $enableZip64 = true;
+    /**
+     * Enable streaming files with single read where
+     * general purpose bit 3 indicates local file header
+     * contain zero values in crc and size fields,
+     * these appear only after file contents
+     * in data descriptor block.
+     *
+     * @var bool
+     */
+    private $zeroHeader = false;
+    /**
+     * Enable reading file stat for determining file size.
+     * When a 32-bit system reads file size that is
+     * over 2 GB, invalid value appears in file size
+     * due to integer overflow. Should be disabled on
+     * 32-bit systems with method addFileFromPath
+     * if any file may exceed 2 GB. In this case file
+     * will be read in blocks and correct size will be
+     * determined from content.
+     *
+     * @var bool
+     */
+    private $statFiles = true;
+    /**
+     * Enable flush after every write to output stream.
+     * @var bool
+     */
+    private $flushOutput = false;
+    /**
+     * HTTP Content-Disposition.  Defaults to
+     * 'attachment', where
+     * FILENAME is the specified filename.
+     *
+     * Note that this does nothing if you are
+     * not sending HTTP headers.
+     *
+     * @var string
+     */
+    private $contentDisposition = 'attachment';
+    /**
+     * Note that this does nothing if you are
+     * not sending HTTP headers.
+     *
+     * @var string
+     */
+    private $contentType = 'application/x-zip';
+    /**
+     * @var int
+     */
+    private $deflateLevel = 6;
+
+    /**
+     * @var resource
+     */
+    private $outputStream;
+
+    /**
+     * Options constructor.
+     */
+    public function __construct()
+    {
+        $this->largeFileMethod = Method::STORE();
+        $this->outputStream = fopen('php://output', 'wb');
+    }
+
+    public function getComment(): string
+    {
+        return $this->comment;
+    }
+
+    public function setComment(string $comment): void
+    {
+        $this->comment = $comment;
+    }
+
+    public function getLargeFileSize(): int
+    {
+        return $this->largeFileSize;
+    }
+
+    public function setLargeFileSize(int $largeFileSize): void
+    {
+        $this->largeFileSize = $largeFileSize;
+    }
+
+    public function getLargeFileMethod(): Method
+    {
+        return $this->largeFileMethod;
+    }
+
+    public function setLargeFileMethod(Method $largeFileMethod): void
+    {
+        $this->largeFileMethod = $largeFileMethod;
+    }
+
+    public function isSendHttpHeaders(): bool
+    {
+        return $this->sendHttpHeaders;
+    }
+
+    public function setSendHttpHeaders(bool $sendHttpHeaders): void
+    {
+        $this->sendHttpHeaders = $sendHttpHeaders;
+    }
+
+    public function getHttpHeaderCallback(): Callable
+    {
+        return $this->httpHeaderCallback;
+    }
+
+    public function setHttpHeaderCallback(Callable $httpHeaderCallback): void
+    {
+        $this->httpHeaderCallback = $httpHeaderCallback;
+    }
+
+    public function isEnableZip64(): bool
+    {
+        return $this->enableZip64;
+    }
+
+    public function setEnableZip64(bool $enableZip64): void
+    {
+        $this->enableZip64 = $enableZip64;
+    }
+
+    public function isZeroHeader(): bool
+    {
+        return $this->zeroHeader;
+    }
+
+    public function setZeroHeader(bool $zeroHeader): void
+    {
+        $this->zeroHeader = $zeroHeader;
+    }
+
+    public function isFlushOutput(): bool
+    {
+        return $this->flushOutput;
+    }
+
+    public function setFlushOutput(bool $flushOutput): void
+    {
+        $this->flushOutput = $flushOutput;
+    }
+
+    public function isStatFiles(): bool
+    {
+        return $this->statFiles;
+    }
+
+    public function setStatFiles(bool $statFiles): void
+    {
+        $this->statFiles = $statFiles;
+    }
+
+    public function getContentDisposition(): string
+    {
+        return $this->contentDisposition;
+    }
+
+    public function setContentDisposition(string $contentDisposition): void
+    {
+        $this->contentDisposition = $contentDisposition;
+    }
+
+    public function getContentType(): string
+    {
+        return $this->contentType;
+    }
+
+    public function setContentType(string $contentType): void
+    {
+        $this->contentType = $contentType;
+    }
+
+    /**
+     * @return resource
+     */
+    public function getOutputStream()
+    {
+        return $this->outputStream;
+    }
+
+    /**
+     * @param resource $outputStream
+     */
+    public function setOutputStream($outputStream): void
+    {
+        $this->outputStream = $outputStream;
+    }
+
+    /**
+     * @return int
+     */
+    public function getDeflateLevel(): int
+    {
+        return $this->deflateLevel;
+    }
+
+    /**
+     * @param int $deflateLevel
+     */
+    public function setDeflateLevel(int $deflateLevel): void
+    {
+        $this->deflateLevel = $deflateLevel;
+    }
+}
diff --git a/lib/zipstream/src/Option/File.php b/lib/zipstream/src/Option/File.php
new file mode 100644 (file)
index 0000000..7fd29ea
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+declare(strict_types=1);
+
+namespace ZipStream\Option;
+
+use DateTime;
+
+final class File
+{
+    /**
+     * @var string
+     */
+    private $comment = '';
+    /**
+     * @var Method
+     */
+    private $method;
+    /**
+     * @var int
+     */
+    private $deflateLevel;
+    /**
+     * @var DateTime
+     */
+    private $time;
+    /**
+     * @var int
+     */
+    private $size = 0;
+
+    public function defaultTo(Archive $archiveOptions): void
+    {
+        $this->deflateLevel = $this->deflateLevel ?: $archiveOptions->getDeflateLevel();
+        $this->time = $this->time ?: new DateTime();
+    }
+
+    /**
+     * @return string
+     */
+    public function getComment(): string
+    {
+        return $this->comment;
+    }
+
+    /**
+     * @param string $comment
+     */
+    public function setComment(string $comment): void
+    {
+        $this->comment = $comment;
+    }
+
+    /**
+     * @return Method
+     */
+    public function getMethod(): Method
+    {
+        return $this->method ?: Method::DEFLATE();
+    }
+
+    /**
+     * @param Method $method
+     */
+    public function setMethod(Method $method): void
+    {
+        $this->method = $method;
+    }
+
+    /**
+     * @return int
+     */
+    public function getDeflateLevel(): int
+    {
+        return $this->deflateLevel ?: Archive::DEFAULT_DEFLATE_LEVEL;
+    }
+
+    /**
+     * @param int $deflateLevel
+     */
+    public function setDeflateLevel(int $deflateLevel): void
+    {
+        $this->deflateLevel = $deflateLevel;
+    }
+
+    /**
+     * @return DateTime
+     */
+    public function getTime(): DateTime
+    {
+        return $this->time;
+    }
+
+    /**
+     * @param DateTime $time
+     */
+    public function setTime(DateTime $time): void
+    {
+        $this->time = $time;
+    }
+
+    /**
+     * @return int
+     */
+    public function getSize(): int
+    {
+        return $this->size;
+    }
+
+    /**
+     * @param int $size
+     */
+    public function setSize(int $size): void
+    {
+        $this->size = $size;
+    }
+}
diff --git a/lib/zipstream/src/Option/Method.php b/lib/zipstream/src/Option/Method.php
new file mode 100644 (file)
index 0000000..bbec84c
--- /dev/null
@@ -0,0