Merge branch 'MDL-69583-master' of git://github.com/ferranrecio/moodle
authorJun Pataleta <jun@moodle.com>
Thu, 15 Oct 2020 05:56:47 +0000 (13:56 +0800)
committerJun Pataleta <jun@moodle.com>
Thu, 15 Oct 2020 05:56:47 +0000 (13:56 +0800)
99 files changed:
admin/classes/local/externalpage/accesscallback.php [new file with mode: 0644]
admin/modules.php
admin/plugins.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/lib.php
admin/tool/mobile/logout.php [new file with mode: 0644]
admin/tool/usertours/amd/build/filter_cssselector.min.js [new file with mode: 0644]
admin/tool/usertours/amd/build/filter_cssselector.min.js.map [new file with mode: 0644]
admin/tool/usertours/amd/build/usertours.min.js
admin/tool/usertours/amd/build/usertours.min.js.map
admin/tool/usertours/amd/src/filter_cssselector.js [new file with mode: 0644]
admin/tool/usertours/amd/src/usertours.js
admin/tool/usertours/classes/external/tour.php
admin/tool/usertours/classes/helper.php
admin/tool/usertours/classes/local/clientside_filter/clientside_filter.php [new file with mode: 0644]
admin/tool/usertours/classes/local/clientside_filter/cssselector.php [new file with mode: 0644]
admin/tool/usertours/classes/manager.php
admin/tool/usertours/classes/tour.php
admin/tool/usertours/lang/en/tool_usertours.php
admin/tool/usertours/tests/behat/tour_filter.feature
admin/tool/usertours/tests/manager_test.php
admin/tool/usertours/version.php
backup/util/ui/renderer.php
cache/classes/administration_helper.php
cache/tests/administration_helper_test.php
calendar/export_execute.php
config-dist.php
files/classes/external/delete/draft.php [new file with mode: 0644]
files/tests/externallib_test.php
h5p/classes/core.php
h5p/classes/editor_ajax.php
h5p/classes/editor_framework.php
h5p/classes/framework.php
h5p/tests/editor_ajax_test.php
h5p/tests/framework_test.php
h5p/tests/generator/lib.php
h5p/tests/generator_test.php
h5p/tests/h5p_core_test.php
install/lang/ar/error.php
install/lang/ar/install.php
lang/en/error.php
lang/en/moodle.php
lib/classes/component.php
lib/classes/plugininfo/base.php
lib/classes/session/redis.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/form/templates/element-defaultcustom.mustache
lib/jabber/XMPP/BOSH.php
lib/jabber/XMPP/Exception.php
lib/jabber/XMPP/Log.php
lib/jabber/XMPP/README.txt [deleted file]
lib/jabber/XMPP/Roster.php
lib/jabber/XMPP/XMLObj.php
lib/jabber/XMPP/XMLStream.php
lib/jabber/XMPP/XMPP.php
lib/jabber/XMPP/XMPP_Old.php [deleted file]
lib/jabber/readme_moodle.txt
lib/setup.php
lib/setuplib.php
lib/tests/setuplib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/weblib.php
message/classes/api.php
message/externallib.php
message/output/jabber/message_output_jabber.php
message/output/popup/db/upgrade.php
mod/feedback/backup/moodle2/restore_feedback_stepslib.php
mod/feedback/tests/restore_date_test.php
mod/forum/amd/build/discussion_nested_v2.min.js
mod/forum/amd/build/discussion_nested_v2.min.js.map
mod/forum/amd/build/inpage_reply.min.js
mod/forum/amd/build/inpage_reply.min.js.map
mod/forum/amd/build/posts_list.min.js
mod/forum/amd/build/posts_list.min.js.map
mod/forum/amd/src/discussion_nested_v2.js
mod/forum/amd/src/inpage_reply.js
mod/forum/amd/src/posts_list.js
mod/forum/externallib.php
mod/forum/tests/externallib_test.php
mod/forum/upgrade.txt
mod/glossary/classes/external.php
mod/glossary/classes/external/delete_entry.php [new file with mode: 0644]
mod/glossary/classes/external/prepare_entry.php [new file with mode: 0644]
mod/glossary/classes/external/update_entry.php [new file with mode: 0644]
mod/glossary/db/services.php
mod/glossary/deleteentry.php
mod/glossary/edit.php
mod/glossary/lib.php
mod/glossary/tests/external/delete_entry.php [new file with mode: 0644]
mod/glossary/tests/external/prepare_entry.php [new file with mode: 0644]
mod/glossary/tests/external/update_entry.php [new file with mode: 0644]
mod/glossary/tests/external_test.php
mod/glossary/tests/lib_test.php
mod/glossary/upgrade.txt
mod/glossary/version.php
version.php

diff --git a/admin/classes/local/externalpage/accesscallback.php b/admin/classes/local/externalpage/accesscallback.php
new file mode 100644 (file)
index 0000000..05fb07f
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * External admin page class that allows a callback to be provided to determine whether page can be accessed
+ *
+ * @package     core_admin
+ * @copyright   2019 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_admin\local\externalpage;
+
+use admin_externalpage;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("{$CFG->libdir}/adminlib.php");
+
+/**
+ * Admin externalpage class
+ *
+ * @package     core_admin
+ * @copyright   2019 Marina Glancy
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class accesscallback extends admin_externalpage {
+
+    /** @var callable $accesscheckcallback */
+    protected $accesscheckcallback;
+
+    /**
+     * Class constructor
+     *
+     * @param string $name
+     * @param string $visiblename
+     * @param string $url
+     * @param callable $accesscheckcallback The callback method that will be executed to check whether user has access to
+     *     this page. The setting instance ($this) is passed as an argument to the callback. Should return boolean value
+     * @param bool $hidden
+     */
+    public function __construct(string $name, string $visiblename, string $url, callable $accesscheckcallback,
+            bool $hidden = false) {
+
+        $this->accesscheckcallback = $accesscheckcallback;
+
+        parent::__construct($name, $visiblename, $url, [], $hidden);
+    }
+
+    /**
+     * Determines if the current user has access to this external page based on access callback
+     *
+     * @return bool
+     */
+    public function check_access() {
+        return ($this->accesscheckcallback)($this);
+    }
+}
index 01f67a1..e76a8e1 100644 (file)
@@ -49,6 +49,7 @@
                 array($module->id));
         core_plugin_manager::reset_caches();
         admin_get_root(true, false);  // settings not required - only pages
+        redirect(new moodle_url('/admin/modules.php'));
     }
 
     if (!empty($show) and confirm_sesskey()) {
@@ -66,6 +67,7 @@
                 array($module->id));
         core_plugin_manager::reset_caches();
         admin_get_root(true, false);  // settings not required - only pages
+        redirect(new moodle_url('/admin/modules.php'));
     }
 
     echo $OUTPUT->header();
             $count = -1;
         }
         if ($count>0) {
-            $countlink = "<a href=\"{$CFG->wwwroot}/course/search.php?modulelist=$module->name" .
-                "&amp;sesskey=".sesskey()."\" title=\"$strshowmodulecourse\">$count</a>";
+            $countlink = $OUTPUT->action_link(new moodle_url('/course/search.php', ['modulelist' => $module->name]),
+                $count, null, ['title' => $strshowmodulecourse]);
         } else if ($count < 0) {
             $countlink = get_string('error');
         } else {
index c99ae52..e4867c1 100644 (file)
@@ -53,7 +53,6 @@ $pageurl = new moodle_url('/admin/plugins.php', $pageparams);
 $pluginman = core_plugin_manager::instance();
 
 if ($uninstall) {
-    require_sesskey();
 
     if (!$confirmed) {
         admin_externalpage_setup('pluginsoverview', '', $pageparams);
@@ -92,6 +91,7 @@ if ($uninstall) {
         exit();
 
     } else {
+        require_sesskey();
         $SESSION->pluginuninstallreturn = $pluginfo->get_return_url_after_uninstall($return);
         $progress = new progress_trace_buffer(new text_progress_trace(), false);
         $pluginman->uninstall_plugin($pluginfo->component, $progress);
index 6fafc5f..f3e6d7d 100644 (file)
@@ -83,6 +83,7 @@ $string['iosappid_desc'] = 'This setting may be left as default unless you have
 $string['loginintheapp'] = 'Via the app';
 $string['logininthebrowser'] = 'Via a browser window (for SSO plugins)';
 $string['loginintheembeddedbrowser'] = 'Via an embedded browser (for SSO plugins)';
+$string['logoutconfirmation'] = 'Are you sure you want to log out from the mobile app on your mobile devices? By logging out, you will then need to re-enter your username and password in the mobile app on all devices where you have the app installed.';
 $string['mainmenu'] = 'Main menu';
 $string['managefiletypes'] = 'Manage file types';
 $string['minimumversion'] = 'If an app version is specified (3.8.0 or higher), any users using an older app version will be prompted to upgrade their app before being allowed access to the site.';
index 567af8b..6b9dbf3 100644 (file)
@@ -134,15 +134,11 @@ function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree
         return;
     }
 
-    if (!$iscurrentuser) {
-        return;
-    }
-
     $newnodes = [];
     $mobilesettings = get_config('tool_mobile');
 
     // Check if we should display a QR code.
-    if (!empty($mobilesettings->qrcodetype)) {
+    if ($iscurrentuser && !empty($mobilesettings->qrcodetype)) {
         $mobileqr = null;
         $qrcodeforappstr = get_string('qrcodeformobileappaccess', 'tool_mobile');
 
@@ -182,6 +178,13 @@ function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree
         $mobilestrconnected = get_string('lastsiteaccess');
         if ($usertoken->lastaccess) {
             $mobilelastaccess = userdate($usertoken->lastaccess) . "&nbsp; (" . format_time(time() - $usertoken->lastaccess) . ")";
+            // Logout link.
+            $validtoken = empty($usertoken->validuntil) || time() < $usertoken->validuntil;
+            if ($iscurrentuser && $validtoken) {
+                $url = new moodle_url('/'.$CFG->admin.'/tool/mobile/logout.php', ['sesskey' => sesskey()]);
+                $logoutlink = html_writer::link($url, get_string('logout'));
+                $mobilelastaccess .= "&nbsp; ($logoutlink)";
+            }
         } else {
             // We should not reach this point.
             $mobilelastaccess = get_string("never");
diff --git a/admin/tool/mobile/logout.php b/admin/tool/mobile/logout.php
new file mode 100644 (file)
index 0000000..17a768a
--- /dev/null
@@ -0,0 +1,70 @@
+<?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/>.
+
+/**
+ * Log out a user from his external mobile devices (phones, tables, Moodle Desktop app, etc..)
+ *
+ * @package tool_mobile
+ * @copyright 2020 Juan Leyva <juan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(__DIR__ . '/../../../config.php');
+require_once($CFG->dirroot . '/admin/tool/mobile/lib.php');
+require_once($CFG->dirroot . '/webservice/lib.php');
+
+if (!$CFG->enablemobilewebservice) {
+    print_error('enablewsdescription', 'webservice');
+}
+
+require_login(null, false);
+
+// Require an active user: not guest, not suspended.
+core_user::require_active_user($USER);
+
+$redirecturl = new \moodle_url('/user/profile.php');
+
+if (optional_param('confirm', 0, PARAM_INT) && data_submitted()) {
+    require_sesskey();
+
+    // Get the mobile service token to be deleted.
+    $token = tool_mobile_get_token($USER->id);
+
+    if ($token) {
+        $webservicemanager = new webservice();
+        $webservicemanager->delete_user_ws_token($token->id);
+    }
+    redirect($redirecturl);
+}
+
+// Page settings.
+$title = get_string('logout');
+$context = context_system::instance();
+$PAGE->set_url(new \moodle_url('/'.$CFG->admin.'/tool/mobile/logout.php'));
+$PAGE->navbar->add($title);
+$PAGE->set_context($context);
+$PAGE->set_title($SITE->fullname. ': ' . $title);
+
+// Display the page.
+echo $OUTPUT->header();
+
+$message = get_string('logoutconfirmation', 'tool_mobile');
+$confirmurl = new \moodle_url('logout.php', ['confirm' => 1]);
+$yesbutton = new single_button($confirmurl, get_string('yes'), 'post');
+$nobutton = new single_button($redirecturl, get_string('no'));
+echo $OUTPUT->confirm($message, $yesbutton, $nobutton);
+
+echo $OUTPUT->footer();
diff --git a/admin/tool/usertours/amd/build/filter_cssselector.min.js b/admin/tool/usertours/amd/build/filter_cssselector.min.js
new file mode 100644 (file)
index 0000000..6dbb873
Binary files /dev/null and b/admin/tool/usertours/amd/build/filter_cssselector.min.js differ
diff --git a/admin/tool/usertours/amd/build/filter_cssselector.min.js.map b/admin/tool/usertours/amd/build/filter_cssselector.min.js.map
new file mode 100644 (file)
index 0000000..9ea27ca
Binary files /dev/null and b/admin/tool/usertours/amd/build/filter_cssselector.min.js.map differ
index 6b16a51..8ebe59f 100644 (file)
Binary files a/admin/tool/usertours/amd/build/usertours.min.js and b/admin/tool/usertours/amd/build/usertours.min.js differ
index 9f614ad..c5cca21 100644 (file)
Binary files a/admin/tool/usertours/amd/build/usertours.min.js.map and b/admin/tool/usertours/amd/build/usertours.min.js.map differ
diff --git a/admin/tool/usertours/amd/src/filter_cssselector.js b/admin/tool/usertours/amd/src/filter_cssselector.js
new file mode 100644 (file)
index 0000000..06e825c
--- /dev/null
@@ -0,0 +1,39 @@
+// 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/>.
+
+/**
+ * CSS selector client side filter.
+ *
+ * @module     tool_usertours/filter_cssselector
+ * @class      filter_cssselector
+ * @package    tool_usertours
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Checks whether the configured CSS selector exists on this page.
+ *
+ * @param {array} tourConfig  The tour configuration.
+ * @returns {boolean}
+ */
+export const filterMatches = function(tourConfig) {
+    let filterValues = tourConfig.filtervalues.cssselector;
+    if (filterValues[0]) {
+        return !!document.querySelector(filterValues[0]);
+    }
+    // If there is no CSS selector configured, this page matches.
+    return true;
+};
index 4bb1050..a79f7f2 100644 (file)
@@ -14,36 +14,62 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
 
         currentTour: null,
 
-        context: null,
-
         /**
          * Initialise the user tour for the current page.
          *
          * @method  init
-         * @param   {Number}    tourId      The ID of the tour to start.
-         * @param   {Bool}      startTour   Attempt to start the tour now.
-         * @param   {Number}    context     The context of the current page.
+         * @param   {Array}    tourDetails      The matching tours for this page.
+         * @param   {Array}    filters          The names of all client side filters.
          */
-        init: function(tourId, startTour, context) {
-            // Only one tour per page is allowed.
-            usertours.tourId = tourId;
+        init: function(tourDetails, filters) {
+            let requirements = [];
+            for (var req = 0; req < filters.length; req++) {
+                requirements[req] = 'tool_usertours/filter_' + filters[req];
+            }
+            require(requirements, function() {
+                // Run the client side filters to find the first matching tour.
+                let matchingTour = null;
+                for (let key in tourDetails) {
+                    let tour = tourDetails[key];
+                    for (let i = 0; i < filters.length; i++) {
+                        let filter = arguments[i];
+                        if (filter.filterMatches(tour)) {
+                            matchingTour = tour;
+                        } else {
+                            // If any filter doesn't match, move on to the next tour.
+                            matchingTour = null;
+                            break;
+                        }
+                    }
+                    // If all filters matched then use this tour.
+                    if (matchingTour) {
+                        break;
+                    }
+                }
 
-            usertours.context = context;
+                if (matchingTour === null) {
+                    return;
+                }
 
-            if (typeof startTour === 'undefined') {
-                startTour = true;
-            }
+                // Only one tour per page is allowed.
+                usertours.tourId = matchingTour.tourId;
 
-            if (startTour) {
-                // Fetch the tour configuration.
-                usertours.fetchTour(tourId);
-            }
+                let startTour = matchingTour.startTour;
+                if (typeof startTour === 'undefined') {
+                    startTour = true;
+                }
+
+                if (startTour) {
+                    // Fetch the tour configuration.
+                    usertours.fetchTour(usertours.tourId);
+                }
 
-            usertours.addResetLink();
-            // Watch for the reset link.
-            $('body').on('click', '[data-action="tool_usertours/resetpagetour"]', function(e) {
-                e.preventDefault();
-                usertours.resetTourState(usertours.tourId);
+                usertours.addResetLink();
+                // Watch for the reset link.
+                $('body').on('click', '[data-action="tool_usertours/resetpagetour"]', function(e) {
+                    e.preventDefault();
+                    usertours.resetTourState(usertours.tourId);
+                });
             });
         },
 
@@ -61,7 +87,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
                         methodname: 'tool_usertours_fetch_and_start_tour',
                         args: {
                             tourid:     tourId,
-                            context:    usertours.context,
+                            context:    M.cfg.contextid,
                             pageurl:    window.location.href,
                         }
                     }
@@ -186,7 +212,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
                         methodname: 'tool_usertours_step_shown',
                         args: {
                             tourid:     usertours.tourId,
-                            context:    usertours.context,
+                            context:    M.cfg.contextid,
                             pageurl:    window.location.href,
                             stepid:     stepConfig.stepid,
                             stepindex:  this.getCurrentStepNumber(),
@@ -209,7 +235,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
                         methodname: 'tool_usertours_complete_tour',
                         args: {
                             tourid:     usertours.tourId,
-                            context:    usertours.context,
+                            context:    M.cfg.contextid,
                             pageurl:    window.location.href,
                             stepid:     stepConfig.stepid,
                             stepindex:  this.getCurrentStepNumber(),
@@ -232,7 +258,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
                         methodname: 'tool_usertours_reset_tour',
                         args: {
                             tourid:     tourId,
-                            context:    usertours.context,
+                            context:    M.cfg.contextid,
                             pageurl:    window.location.href,
                         }
                     }
index c16c5a0..7ec9cb8 100644 (file)
@@ -131,8 +131,9 @@ class tour extends external_api {
 
         $result = [];
 
-        if ($tourinstance = \tool_usertours\manager::get_matching_tours(new \moodle_url($params['pageurl']))) {
-            if ($tour->get_id() === $tourinstance->get_id()) {
+        $matchingtours = \tool_usertours\manager::get_matching_tours(new \moodle_url($params['pageurl']));
+        foreach ($matchingtours as $match) {
+            if ($tour->get_id() === $match->get_id()) {
                 $result['startTour'] = $tour->get_id();
 
                 \tool_usertours\event\tour_reset::create([
@@ -142,7 +143,7 @@ class tour extends external_api {
                         'pageurl'   => $params['pageurl'],
                     ],
                 ])->trigger();
-
+                break;
             }
         }
 
index df04ed9..f1f8e4b 100644 (file)
@@ -24,6 +24,8 @@
 
 namespace tool_usertours;
 
+use tool_usertours\local\clientside_filter\clientside_filter;
+
 defined('MOODLE_INTERNAL') || die();
 
 /**
@@ -523,33 +525,57 @@ class helper {
         }
         self::$bootstrapped = true;
 
-        if ($tour = manager::get_current_tour()) {
+        $tours = manager::get_current_tours();
+
+        if ($tours) {
+            $filters = static::get_all_clientside_filters();
+
+            $tourdetails = array_map(function($tour) use ($filters) {
+                return [
+                        'tourId' => $tour->get_id(),
+                        'startTour' => $tour->should_show_for_user(),
+                        'filtervalues' => $tour->get_client_filter_values($filters),
+                ];
+            }, $tours);
+
+            $filternames = [];
+            foreach ($filters as $filter) {
+                    $filternames[] = $filter::get_filter_name();
+            }
+
             $PAGE->requires->js_call_amd('tool_usertours/usertours', 'init', [
-                    $tour->get_id(),
-                    $tour->should_show_for_user(),
-                    $PAGE->context->id,
-                ]);
+                    $tourdetails,
+                    $filternames,
+            ]);
         }
     }
 
     /**
-     * Add the reset link to the current page.
+     * Get a list of all possible filters.
+     *
+     * @return  array
      */
-    public static function bootstrap_reset() {
-        if (manager::get_current_tour()) {
-            echo \html_writer::link('', get_string('resettouronpage', 'tool_usertours'), [
-                    'data-action'   => 'tool_usertours/resetpagetour',
-                ]);
-        }
+    public static function get_all_filters() {
+        $filters = \core_component::get_component_classes_in_namespace('tool_usertours', 'local\filter');
+        $filters = array_keys($filters);
+
+        $filters = array_filter($filters, function($filterclass) {
+            $rc = new \ReflectionClass($filterclass);
+            return $rc->isInstantiable();
+        });
+
+        $filters = array_merge($filters, static::get_all_clientside_filters());
+
+        return $filters;
     }
 
     /**
-     * Get a list of all possible filters.
+     * Get a list of all clientside filters.
      *
      * @return  array
      */
-    public static function get_all_filters() {
-        $filters = \core_component::get_component_classes_in_namespace('tool_usertours', 'local\filter');
+    public static function get_all_clientside_filters() {
+        $filters = \core_component::get_component_classes_in_namespace('tool_usertours', 'local\clientside_filter');
         $filters = array_keys($filters);
 
         $filters = array_filter($filters, function($filterclass) {
diff --git a/admin/tool/usertours/classes/local/clientside_filter/clientside_filter.php b/admin/tool/usertours/classes/local/clientside_filter/clientside_filter.php
new file mode 100644 (file)
index 0000000..6fa403a
--- /dev/null
@@ -0,0 +1,55 @@
+<?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/>.
+
+/**
+ * Clientside filter base.
+ *
+ * @package    tool_usertours
+ * @copyright  2020 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_usertours\local\clientside_filter;
+
+defined('MOODLE_INTERNAL') || die();
+
+use stdClass;
+use tool_usertours\local\filter\base;
+use tool_usertours\tour;
+
+/**
+ * Clientside filter base.
+ *
+ * @copyright  2020 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class clientside_filter extends base {
+    /**
+     * Returns the filter values needed for client side filtering.
+     *
+     * @param   tour            $tour       The tour to find the filter values for
+     * @return  stdClass
+     */
+    public static function get_client_side_values(tour $tour): stdClass {
+        $data = (object) [];
+
+        if (is_a(static::class, clientside_filter::class, true)) {
+            $data->filterdata = $tour->get_filter_values(static::get_filter_name());
+        }
+
+        return $data;
+    }
+}
\ No newline at end of file
diff --git a/admin/tool/usertours/classes/local/clientside_filter/cssselector.php b/admin/tool/usertours/classes/local/clientside_filter/cssselector.php
new file mode 100644 (file)
index 0000000..e6d6c2d
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * Selector filter.
+ *
+ * @package    tool_usertours
+ * @copyright  2020 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_usertours\local\clientside_filter;
+
+use stdClass;
+use tool_usertours\tour;
+
+/**
+ * Course filter.
+ *
+ * @copyright  2020 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cssselector extends clientside_filter {
+    /**
+     * The name of the filter.
+     *
+     * @return  string
+     */
+    public static function get_filter_name() {
+        return 'cssselector';
+    }
+
+    /**
+     * Overrides the base add form element with a selector text box.
+     *
+     * @param \MoodleQuickForm $mform
+     */
+    public static function add_filter_to_form(\MoodleQuickForm &$mform) {
+        $filtername = self::get_filter_name();
+        $key = "filter_{$filtername}";
+
+        $mform->addElement('text', $key, get_string($key, 'tool_usertours'));
+        $mform->setType($key, PARAM_RAW);
+        $mform->addHelpButton($key, $key, 'tool_usertours');
+    }
+
+    /**
+     * Prepare the filter values for the form.
+     *
+     * @param   tour            $tour       The tour to prepare values from
+     * @param   stdClass        $data       The data value
+     * @return  stdClass
+     */
+    public static function prepare_filter_values_for_form(tour $tour, \stdClass $data) {
+        $filtername = static::get_filter_name();
+
+        $key = "filter_{$filtername}";
+        $values = $tour->get_filter_values($filtername);
+        if (empty($values)) {
+            $values = [""];
+        }
+        $data->$key = $values[0];
+
+        return $data;
+    }
+
+    /**
+     * Save the filter values from the form to the tour.
+     *
+     * @param   tour            $tour       The tour to save values to
+     * @param   stdClass        $data       The data submitted in the form
+     */
+    public static function save_filter_values_from_form(tour $tour, \stdClass $data) {
+        $filtername = static::get_filter_name();
+
+        $key = "filter_{$filtername}";
+
+        $newvalue = [$data->$key];
+        if (empty($data->$key)) {
+            $newvalue = [];
+        }
+
+        $tour->set_filter_values($filtername, $newvalue);
+    }
+
+    /**
+     * Returns the filter values needed for client side filtering.
+     *
+     * @param   tour            $tour       The tour to find the filter values for
+     * @return  stdClass
+     */
+    public static function get_client_side_values(tour $tour): stdClass {
+        $filtername = static::get_filter_name();
+        $filtervalues = $tour->get_filter_values($filtername);
+
+        // Filter values might not exist for tours that were created before this filter existed.
+        if (!$filtervalues) {
+            return new stdClass;
+        }
+
+        return (object) $filtervalues;
+    }
+}
index 6741568..437b416 100644 (file)
@@ -608,42 +608,44 @@ class manager {
     }
 
     /**
-     * Get the first tour matching the current page URL.
+     * Get all tours for the current page URL.
      *
-     * @param   bool        $reset      Forcibly update the current tour
-     * @return  tour
+     * @param   bool        $reset      Forcibly update the current tours
+     * @return  array
      */
-    public static function get_current_tour($reset = false) {
+    public static function get_current_tours($reset = false): array {
         global $PAGE;
 
-        static $tour = false;
+        static $tours = false;
 
-        if ($tour === false || $reset) {
-            $tour = self::get_matching_tours($PAGE->url);
+        if ($tours === false || $reset) {
+            $tours = self::get_matching_tours($PAGE->url);
         }
 
-        return $tour;
+        return $tours;
     }
 
     /**
-     * Get the first tour matching the specified URL.
+     * Get all tours matching the specified URL.
      *
      * @param   moodle_url  $pageurl        The URL to match.
-     * @return  tour
+     * @return  array
      */
-    public static function get_matching_tours(\moodle_url $pageurl) {
+    public static function get_matching_tours(\moodle_url $pageurl): array {
         global $PAGE;
 
         $tours = cache::get_matching_tourdata($pageurl);
 
+        $matches = [];
+        $filters = helper::get_all_filters();
         foreach ($tours as $record) {
             $tour = tour::load_from_record($record);
-            if ($tour->is_enabled() && $tour->matches_all_filters($PAGE->context)) {
-                return $tour;
+            if ($tour->is_enabled() && $tour->matches_all_filters($PAGE->context, $filters)) {
+                $matches[] = $tour;
             }
         }
 
-        return null;
+        return $matches;
     }
 
     /**
index 0765ee3..08e8279 100644 (file)
@@ -24,6 +24,8 @@
 
 namespace tool_usertours;
 
+use tool_usertours\local\clientside_filter\clientside_filter;
+
 defined('MOODLE_INTERNAL') || die();
 
 /**
@@ -769,11 +771,14 @@ class tour {
     /**
      * Check whether this tour matches all filters.
      *
-     * @param   context     $context    The context to check
+     * @param   \context     $context    The context to check.
+     * @param   array|null   $filters    Optional array of filters.
      * @return  bool
      */
-    public function matches_all_filters(\context $context) {
-        $filters = helper::get_all_filters();
+    public function matches_all_filters(\context $context, array $filters = null): bool {
+        if (!$filters) {
+            $filters = helper::get_all_filters();
+        }
 
         // All filters must match.
         // If any one filter fails to match, we return false.
@@ -785,4 +790,20 @@ class tour {
 
         return true;
     }
+
+    /**
+     * Gets all filter values for use in client side filters.
+     *
+     * @param   array     $filters    Array of clientside filters.
+     * @return  array
+     */
+    public function get_client_filter_values(array $filters): array {
+        $results = [];
+
+        foreach ($filters as $filter) {
+            $results[$filter::get_filter_name()] = $filter::get_client_side_values($this);
+        }
+
+        return $results;
+    }
 }
index 3d86e56..f142b46 100644 (file)
@@ -63,6 +63,8 @@ $string['filter_course'] = 'Courses';
 $string['filter_course_help'] = 'Show the tour on a page that is associated with the selected course.';
 $string['filter_courseformat'] = 'Course format';
 $string['filter_courseformat_help'] = 'Show the tour on a page that is associated with a course using the selected course format.';
+$string['filter_cssselector'] = 'CSS selector';
+$string['filter_cssselector_help'] = 'Only show the tour when the specified CSS selector is found on the page.';
 $string['filter_header'] = 'Tour filters';
 $string['filter_help'] = 'Select the conditions under which the tour will be shown. All of the filters must match for a tour to be shown to a user.';
 $string['filter_date_account_creation'] = 'User account creation date within';
index ac3a164..0fe72f4 100644 (file)
@@ -142,3 +142,88 @@ Feature: Apply tour filters to a tour
     When I am on "Course 2" course homepage
     And I wait until the page is ready
     Then I should not see "Welcome to your course tour."
+
+  @javascript
+  Scenario: Add tours with CSS selectors
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | student1 | Student   | 1        | student1@example.com |
+    Given the following "courses" exist:
+      | fullname | shortname | format | enablecompletion |
+      | Course 1 | C1        | topics | 1                |
+      | Course 2 | C2        | topics | 1                |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Wiki" to section "1" and I fill the form with:
+      | Wiki name       | Test wiki name        |
+      | Description     | Test wiki description |
+      | First page name | First page            |
+      | Wiki mode       | Collaborative wiki    |
+    And I am on "Course 2" course homepage
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name  | Test forum name                |
+      | Forum type  | Standard forum for general use |
+      | Description | Test forum description         |
+    And I add a new user tour with:
+      | Name               | Wiki tour                |
+      | Description        | A tour with both matches |
+      | Apply to URL match | /course/view.php%        |
+      | Tour is enabled    | 1                        |
+      | CSS selector       | .modtype_wiki            |
+    And I add steps to the "Wiki tour" tour:
+      | targettype                | Title   | Content                  |
+      | Display in middle of page | Welcome | Welcome to the Wiki tour |
+    And I add a new user tour with:
+      | Name               | Forum tour               |
+      | Description        | A tour with both matches |
+      | Apply to URL match | /course/view.php%        |
+      | Tour is enabled    | 1                        |
+      | CSS selector       | .modtype_forum           |
+    And I add steps to the "Forum tour" tour:
+      | targettype                | Title   | Content                   |
+      | Display in middle of page | Welcome | Welcome to the Forum tour |
+    And I am on "Course 1" course homepage
+    Then I should see "Welcome to the Wiki tour"
+    And I am on "Course 2" course homepage
+    Then I should see "Welcome to the Forum tour"
+
+  @javascript
+  Scenario: Check filtering respects the sort order
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | student1 | Student   | 1        | student1@example.com |
+    And I log in as "admin"
+    And I add a new user tour with:
+      | Name               | First tour     |
+      | Description        | The first tour |
+      | Apply to URL match | /my/%          |
+      | Tour is enabled    | 1              |
+      | CSS selector       | #page-my-index |
+    And I add steps to the "First tour" tour:
+      | targettype                | Title   | Content                   |
+      | Display in middle of page | Welcome | Welcome to the First tour |
+    And I add a new user tour with:
+      | Name               | Second tour     |
+      | Description        | The second tour |
+      | Apply to URL match | /my/%           |
+      | Tour is enabled    | 0               |
+      | CSS selector       | #page-my-index  |
+    And I add steps to the "Second tour" tour:
+      | targettype                | Title   | Content                    |
+      | Display in middle of page | Welcome | Welcome to the Second tour |
+    And I add a new user tour with:
+      | Name               | Third tour     |
+      | Description        | The third tour |
+      | Apply to URL match | /my/%           |
+      | Tour is enabled    | 1               |
+      | CSS selector       | #page-my-index  |
+    And I add steps to the "Third tour" tour:
+      | targettype                | Title   | Content                   |
+      | Display in middle of page | Welcome | Welcome to the Third tour |
+    And I am on homepage
+    Then I should see "Welcome to the First tour"
+    And I open the User tour settings page
+    And I click on "Move tour down" "link" in the "The first tour" "table_row"
+    And I click on "Move tour down" "link" in the "The first tour" "table_row"
+    And I am on homepage
+    Then I should see "Welcome to the Third tour"
index 913c3e1..d5e4893 100644 (file)
@@ -222,6 +222,13 @@ class tool_usertours_manager_testcase extends advanced_testcase {
                     'description'   => '',
                     'configdata'    => '',
                 ],
+            [
+                    'pathmatch'     => '/my/%',
+                    'enabled'       => true,
+                    'name'          => 'My tour enabled 2',
+                    'description'   => '',
+                    'configdata'    => '',
+                ],
             [
                     'pathmatch'     => '/my/%',
                     'enabled'       => false,
@@ -277,32 +284,32 @@ class tool_usertours_manager_testcase extends advanced_testcase {
                 'No matches found' => [
                         $alltours,
                         $CFG->wwwroot . '/some/invalid/value',
-                        null,
+                        [],
                     ],
                 'Never return a disabled tour' => [
                         $alltours,
                         $CFG->wwwroot . '/my/index.php',
-                        'My tour enabled',
+                        ['My tour enabled', 'My tour enabled 2'],
                     ],
                 'My not course' => [
                         $alltours,
                         $CFG->wwwroot . '/my/index.php',
-                        'My tour enabled',
+                        ['My tour enabled', 'My tour enabled 2'],
                     ],
                 'My with params' => [
                         $alltours,
                         $CFG->wwwroot . '/my/index.php?id=42',
-                        'My tour enabled',
+                        ['My tour enabled', 'My tour enabled 2'],
                     ],
                 'Course with params' => [
                         $alltours,
                         $CFG->wwwroot . '/course/?id=42',
-                        'course tour enabled',
+                        ['course tour enabled'],
                     ],
                 'Course with params and trailing content' => [
                         $alltours,
                         $CFG->wwwroot . '/course/?id=42&foo=bar',
-                        'course tour with additional params enabled',
+                        ['course tour with additional params enabled', 'course tour enabled'],
                     ],
             ];
     }
@@ -311,11 +318,11 @@ class tool_usertours_manager_testcase extends advanced_testcase {
      * Tests for the get_matching_tours function.
      *
      * @dataProvider get_matching_tours_provider
-     * @param   array   $alltours   The list of tours to insert
-     * @param   string  $url        The URL to test
-     * @param   string  $expected   The name of the expected matching tour
+     * @param   array   $alltours   The list of tours to insert.
+     * @param   string  $url        The URL to test.
+     * @param   array   $expected   List of names of the expected matching tours.
      */
-    public function test_get_matching_tours($alltours, $url, $expected) {
+    public function test_get_matching_tours(array $alltours, string $url, array $expected) {
         $this->resetAfterTest();
 
         foreach ($alltours as $tourconfig) {
@@ -323,12 +330,10 @@ class tool_usertours_manager_testcase extends advanced_testcase {
             $this->helper_create_step((object) ['tourid' => $tour->get_id()]);
         }
 
-        $match = \tool_usertours\manager::get_matching_tours(new moodle_url($url));
-        if ($expected === null) {
-            $this->assertNull($match);
-        } else {
-            $this->assertNotNull($match);
-            $this->assertEquals($expected, $match->get_name());
+        $matches = \tool_usertours\manager::get_matching_tours(new moodle_url($url));
+        $this->assertEquals(count($expected), count($matches));
+        for ($i = 0; $i < count($matches); $i++) {
+            $this->assertEquals($expected[$i], $matches[$i]->get_name());
         }
     }
 }
index ead833e..ed8e6ee 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2021052501;            // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2021052502;            // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2021052500;            // Requires this Moodle version.
 $plugin->component = 'tool_usertours';      // Full name of the plugin (used for diagnostics).
index 4a1f187..d404126 100644 (file)
@@ -126,8 +126,9 @@ class core_backup_renderer extends plugin_renderer_base {
 
         $html  = html_writer::start_tag('div', array('class' => 'backup-restore'));
 
-        $html .= html_writer::start_tag('div', array('class' => 'backup-section'));
-        $html .= $this->output->heading(get_string('backupdetails', 'backup'), 2, array('class' => 'header'));
+        $html .= html_writer::start_tag('div', ['class' => 'backup-section',
+            'role' => 'table', 'aria-labelledby' => 'backupdetailsheader']);
+        $html .= $this->output->heading(get_string('backupdetails', 'backup'), 2, 'header', 'backupdetailsheader');
         $html .= $this->backup_detail_pair(get_string('backuptype', 'backup'), get_string('backuptype'.$details->type, 'backup'));
         $html .= $this->backup_detail_pair(get_string('backupformat', 'backup'), get_string('backupformat'.$details->format, 'backup'));
         $html .= $this->backup_detail_pair(get_string('backupmode', 'backup'), get_string('backupmode'.$details->mode, 'backup'));
@@ -153,8 +154,9 @@ class core_backup_renderer extends plugin_renderer_base {
 
         $html .= html_writer::end_tag('div');
 
-        $html .= html_writer::start_tag('div', array('class' => 'backup-section settings-section'));
-        $html .= $this->output->heading(get_string('backupsettings', 'backup'), 2, array('class' => 'header'));
+        $html .= html_writer::start_tag('div', ['class' => 'backup-section settings-section',
+            'role' => 'table', 'aria-labelledby' => 'backupsettingsheader']);
+        $html .= $this->output->heading(get_string('backupsettings', 'backup'), 2, 'header', 'backupsettingsheader');
         foreach ($details->root_settings as $label => $value) {
             if ($label == 'filename' or $label == 'user_files') {
                 continue;
@@ -164,8 +166,9 @@ class core_backup_renderer extends plugin_renderer_base {
         $html .= html_writer::end_tag('div');
 
         if ($details->type === 'course') {
-            $html .= html_writer::start_tag('div', array('class' => 'backup-section'));
-            $html .= $this->output->heading(get_string('backupcoursedetails', 'backup'), 2, array('class' => 'header'));
+            $html .= html_writer::start_tag('div', ['class' => 'backup-section',
+                    'role' => 'table', 'aria-labelledby' => 'backupcoursedetailsheader']);
+            $html .= $this->output->heading(get_string('backupcoursedetails', 'backup'), 2, 'header', 'backupcoursedetailsheader');
             $html .= $this->backup_detail_pair(get_string('coursetitle', 'backup'), $details->course->title);
             $html .= $this->backup_detail_pair(get_string('courseid', 'backup'), $details->course->courseid);
 
@@ -200,7 +203,7 @@ class core_backup_renderer extends plugin_renderer_base {
                         $table->data = array();
                     }
                     $name = get_string('pluginname', $activity->modulename);
-                    $icon = new image_icon('icon', $name, $activity->modulename, array('class' => 'iconlarge icon-pre'));
+                    $icon = new image_icon('icon', '', $activity->modulename, ['class' => 'iconlarge icon-pre']);
                     $table->data[] = array(
                         $this->output->render($icon).$name,
                         $activity->title,
@@ -424,13 +427,25 @@ class core_backup_renderer extends plugin_renderer_base {
     protected function backup_detail_pair($label, $value) {
         static $count = 0;
         $count ++;
-        $html  = html_writer::start_tag('div', array('class' => 'detail-pair'));
-        $html .= html_writer::tag('label', $label, array('class' => 'detail-pair-label', 'for' => 'detail-pair-value-'.$count));
-        $html .= html_writer::tag('div', $value, array('class' => 'detail-pair-value pl-2', 'name' => 'detail-pair-value-'.$count));
+        $html  = html_writer::start_tag('div', ['class' => 'detail-pair', 'role' => 'row']);
+        $html .= html_writer::tag('div', $label, ['class' => 'detail-pair-label mb-2', 'role' => 'cell']);
+        $html .= html_writer::tag('div', $value, ['class' => 'detail-pair-value pl-2', 'role' => 'cell']);
         $html .= html_writer::end_tag('div');
         return $html;
     }
 
+    /**
+     * Creates a unique id string by appending an incremental number to the prefix.
+     *
+     * @param string $prefix To be used as the left part of the id string.
+     * @return string
+     */
+    protected function make_unique_id(string $prefix): string {
+        static $count = 0;
+
+        return $prefix . '-' . $count++;
+    }
+
     /**
      * Created a detailed pairing with an input
      *
@@ -448,9 +463,11 @@ class core_backup_renderer extends plugin_renderer_base {
         } else {
             $description = '';
         }
+        $id = $this->make_unique_id('detail-pair-value');
         return $this->backup_detail_pair(
-            $label,
-            html_writer::empty_tag('input', $attributes + array('name' => $name, 'type' => $type, 'value' => $value)) . $description
+            html_writer::label($label, $id),
+            html_writer::empty_tag('input', $attributes + ['id' => $id, 'name' => $name, 'type' => $type, 'value' => $value]) .
+                $description
         );
     }
 
@@ -718,8 +735,6 @@ class core_backup_renderer extends plugin_renderer_base {
      * @return string
      */
     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 mb-1'));
         $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
 
@@ -733,11 +748,18 @@ class core_backup_renderer extends plugin_renderer_base {
                 if (!$course->visible) {
                     $row->attributes['class'] .= ' dimmed';
                 }
-                $row->cells = array(
-                    html_writer::empty_tag('input', array('type' => 'radio', 'name' => 'targetid', 'value' => $course->id)),
-                    format_string($course->shortname, true, array('context' => context_course::instance($course->id))),
-                    format_string($course->fullname, true, array('context' => context_course::instance($course->id)))
-                );
+                $id = $this->make_unique_id('restore-course');
+                $row->cells = [
+                    html_writer::empty_tag('input', ['type' => 'radio', 'name' => 'targetid', 'value' => $course->id,
+                        'id' => $id]),
+                    html_writer::label(
+                        format_string($course->shortname, true, ['context' => context_course::instance($course->id)]),
+                        $id,
+                        true,
+                        ['class' => 'd-block']
+                    ),
+                    format_string($course->fullname, true, ['context' => context_course::instance($course->id)])
+                ];
                 $table->data[] = $row;
             }
             if ($component->has_more_results()) {
@@ -779,8 +801,6 @@ class core_backup_renderer extends plugin_renderer_base {
      * @return string
      */
     public function render_import_course_search(import_course_search $component) {
-        $url = $component->get_url();
-
         $output = html_writer::start_tag('div', array('class' => 'import-course-search'));
         if ($component->get_count() === 0) {
             $output .= $this->output->notification(get_string('nomatchingcourses', 'backup'));
@@ -790,6 +810,8 @@ class core_backup_renderer extends plugin_renderer_base {
                 'type' => 'text',
                 'name' => restore_course_search::$VAR_SEARCH,
                 'value' => $component->get_search(),
+                'aria-label' => get_string('searchcourses'),
+                'placeholder' => get_string('searchcourses'),
                 'class' => 'form-control'
             );
             $output .= html_writer::empty_tag('input', $attrs);
@@ -825,11 +847,18 @@ class core_backup_renderer extends plugin_renderer_base {
             if (!$course->visible) {
                 $row->attributes['class'] .= ' dimmed';
             }
-            $row->cells = array(
-                html_writer::empty_tag('input', array('type' => 'radio', 'name' => 'importid', 'value' => $course->id)),
-                format_string($course->shortname, true, array('context' => context_course::instance($course->id))),
-                format_string($course->fullname, true, array('context' => context_course::instance($course->id)))
-            );
+            $id = $this->make_unique_id('import-course');
+            $row->cells = [
+                html_writer::empty_tag('input', ['type' => 'radio', 'name' => 'importid', 'value' => $course->id,
+                    'id' => $id]),
+                html_writer::label(
+                    format_string($course->shortname, true, ['context' => context_course::instance($course->id)]),
+                    $id,
+                    true,
+                    ['class' => 'd-block']
+                ),
+                format_string($course->fullname, true, ['context' => context_course::instance($course->id)])
+            ];
             $table->data[] = $row;
         }
         if ($component->has_more_results()) {
@@ -848,6 +877,8 @@ class core_backup_renderer extends plugin_renderer_base {
             'type' => 'text',
             'name' => restore_course_search::$VAR_SEARCH,
             'value' => $component->get_search(),
+            'aria-label' => get_string('searchcourses'),
+            'placeholder' => get_string('searchcourses'),
             'class' => 'form-control');
         $output .= html_writer::empty_tag('input', $attrs);
         $attrs = array(
@@ -870,8 +901,6 @@ class core_backup_renderer extends plugin_renderer_base {
      * @return string
      */
     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 mb-1'));
         $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
 
@@ -887,12 +916,19 @@ class core_backup_renderer extends plugin_renderer_base {
                     $row->attributes['class'] .= ' dimmed';
                 }
                 $context = context_coursecat::instance($category->id);
-                $row->cells = array(
-                    html_writer::empty_tag('input', array('type' => 'radio', 'name' => 'targetid', 'value' => $category->id)),
-                    format_string($category->name, true, array('context' => context_coursecat::instance($category->id))),
+                $id = $this->make_unique_id('restore-category');
+                $row->cells = [
+                    html_writer::empty_tag('input', ['type' => 'radio', 'name' => 'targetid', 'value' => $category->id,
+                        'id' => $id]),
+                    html_writer::label(
+                        format_string($category->name, true, ['context' => context_coursecat::instance($category->id)]),
+                        $id,
+                        true,
+                        ['class' => 'd-block']
+                    ),
                     format_text(file_rewrite_pluginfile_urls($category->description, 'pluginfile.php', $context->id,
-                        'coursecat', 'description', null), $category->descriptionformat, array('overflowdiv' => true))
-                );
+                        'coursecat', 'description', null), $category->descriptionformat, ['overflowdiv' => true])
+                ];
                 $table->data[] = $row;
             }
             if ($component->has_more_results()) {
@@ -918,7 +954,7 @@ class core_backup_renderer extends plugin_renderer_base {
             'inform' => true,
             'extraclasses' => 'rcs-search mb-3 w-25',
             'inputname' => restore_category_search::$VAR_SEARCH,
-            'searchstring' => get_string('search'),
+            'searchstring' => get_string('searchcoursecategories'),
             'query' => $component->get_search(),
         ];
         $output .= $this->output->render_from_template('core/search_input', $data);
index 551e62c..620505c 100644 (file)
@@ -101,13 +101,31 @@ abstract class administration_helper extends cache_helper {
         ksort($default);
         $return = $return + $default;
 
-        foreach ($instance->get_definition_mappings() as $mapping) {
+        $mappings = $instance->get_definition_mappings();
+        foreach ($mappings as $mapping) {
             if (!array_key_exists($mapping['store'], $return)) {
                 continue;
             }
             $return[$mapping['store']]['mappings']++;
         }
 
+        // Now get all definitions, and if not mapped, increment the defaults for the mode.
+        $modemappings = $instance->get_mode_mappings();
+        foreach ($instance->get_definitions() as $definition) {
+            // Construct the definition name to search for.
+            $defname = $definition['component'] . '/' . $definition['area'];
+            // Skip if definition is already mapped.
+            if (array_search($defname, array_column($mappings, 'definition')) !== false) {
+                continue;
+            }
+
+            $mode = $definition['mode'];
+            // Get the store name of the default mapping from the mode.
+            $index = array_search($mode, array_column($modemappings, 'mode'));
+            $store = $modemappings[$index]['store'];
+            $return[$store]['mappings']++;
+        }
+
         return $return;
     }
 
index 865539f..146efe3 100644 (file)
@@ -92,7 +92,12 @@ class core_cache_administration_helper_testcase extends advanced_testcase {
         $this->assertEquals(0, $summary['default']);
         $this->assertEquals(1, $summary['isready']);
         $this->assertEquals(1, $summary['requirementsmet']);
-        $this->assertEquals(1, $summary['mappings']);
+
+        // Find the number of mappings to sessionstore.
+        $mappingcount = count(array_filter($config->get_definitions(), function($element) {
+            return $element['mode'] === cache_store::MODE_APPLICATION;
+        }));
+        $this->assertEquals($mappingcount, $summary['mappings']);
 
         $definitionsummaries = core_cache\administration_helper::get_definition_summaries();
         $this->assertInternalType('array', $definitionsummaries);
index 66c348e..5a53e7e 100644 (file)
@@ -18,7 +18,7 @@ if (empty($CFG->enablecalendarexport)) {
 $checkuserid = !empty($userid) && $user = $DB->get_record('user', array('id' => $userid), 'id,password');
 //allowing for fallback check of old url - MDL-27542
 $checkusername = !empty($username) && $user = $DB->get_record('user', array('username' => $username), 'id,password');
-if (!$checkuserid && !$checkusername) {
+if ((!$checkuserid && !$checkusername) || !$user) {
     //No such user
     die('Invalid authentication');
 }
index 912b92e..083f217 100644 (file)
@@ -476,6 +476,7 @@ $CFG->admin = 'admin';
 //     $CFG->tempdir = '/var/www/moodle/temp';        // Directory MUST BE SHARED by all cluster nodes.
 //     $CFG->cachedir = '/var/www/moodle/cache';      // Directory MUST BE SHARED by all cluster nodes, locking required.
 //     $CFG->localcachedir = '/var/local/cache';      // Intended for local node caching.
+//     $CFG->localrequestdir = '/tmp';                // Intended for local only temporary files. The defaults uses sys_get_temp_dir().
 //
 // It is possible to specify a different backup temp directory, use local fast filesystem
 // for normal web servers. Server clusters MUST use shared filesystem for backuptempdir!
diff --git a/files/classes/external/delete/draft.php b/files/classes/external/delete/draft.php
new file mode 100644 (file)
index 0000000..3ad88a8
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is the external method for deleting draft files.
+ *
+ * @package    core_files
+ * @since      Moodle 3.10
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_files\external\delete;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->libdir . '/filelib.php');
+
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use context_user;
+
+/**
+ * This is the external method for deleting draft files.
+ *
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class draft extends external_api {
+
+    /**
+     * Describes the parameters for execute.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.10
+     */
+    public static function execute_parameters() : external_function_parameters {
+        return new external_function_parameters (
+            [
+                'draftitemid' => new external_value(PARAM_INT, 'Item id of the draft file area'),
+                'files' => new external_multiple_structure(
+                    new external_single_structure(
+                        [
+                            'filepath'  => new external_value(PARAM_PATH, 'Path to the file or directory to delete.'),
+                            'filename'  => new external_value(PARAM_FILE, 'Name of the file to delete.'),
+                        ]
+                    ), 'Files or directories to be deleted.'
+                ),
+            ]
+        );
+    }
+
+    /**
+     * Delete the indicated files (or directories) from a user draft file area.
+     *
+     * @param  int    $draftitemid item id of the draft file area
+     * @param  array  $files       files to be deleted
+     * @return array of warnings and parent paths of the files deleted
+     * @since Moodle 3.10
+     */
+    public static function execute(int $draftitemid, array $files) : array {
+        global $CFG, $USER;
+        require_once($CFG->dirroot . '/repository/lib.php');
+
+        $params = self::validate_parameters(self::execute_parameters(), compact('draftitemid', 'files'));
+        [$draftitemid, $files] = array_values($params);
+
+        $usercontext = context_user::instance($USER->id);
+        self::validate_context($usercontext);
+
+        $files = array_map(function($file) {
+            return (object) $file;
+        }, $files);
+        $parentpaths = repository_delete_selected_files($usercontext, 'user', 'draft', $draftitemid, $files);
+
+        return [
+            'parentpaths' => array_keys($parentpaths),
+            'warnings' => [],
+        ];
+    }
+
+    /**
+     * Describes the execute return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.10
+     */
+    public static function execute_returns() : external_single_structure {
+        return new external_single_structure(
+            [
+                'parentpaths' => new external_multiple_structure(
+                    new external_value(PARAM_PATH, 'Path to parent directory of the deleted files.')
+                ),
+                'warnings' => new external_warnings(),
+            ]
+        );
+    }
+}
index b652e6d..91a9e13 100644 (file)
@@ -293,4 +293,57 @@ class core_files_externallib_testcase extends advanced_testcase {
 
         $this->assertEquals($testfilelisting, $testdata);
     }
+
+    /**
+     * Test delete draft files
+     */
+    public function test_delete_draft_files() {
+        global $USER;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Add files to user draft area.
+        $draftitemid = file_get_unused_draft_itemid();
+        $context = context_user::instance($USER->id);
+        $filerecordinline = array(
+            'contextid' => $context->id,
+            'component' => 'user',
+            'filearea'  => 'draft',
+            'itemid'    => $draftitemid,
+            'filepath'  => '/',
+            'filename'  => 'faketxt.txt',
+        );
+        $fs = get_file_storage();
+        $fs->create_file_from_string($filerecordinline, 'fake txt contents 1.');
+
+        // Now create a folder with a file inside.
+        $fs->create_directory($context->id, 'user', 'draft', $draftitemid, '/fakefolder/');
+        $filerecordinline['filepath'] = '/fakefolder/';
+        $filerecordinline['filename'] = 'fakeimage.png';
+        $fs->create_file_from_string($filerecordinline, 'img...');
+
+        // Check two files were created (one file and one directory).
+        $files = core_files_external::get_files($context->id, 'user', 'draft', $draftitemid, '/', '');
+        $files = external_api::clean_returnvalue(core_files_external::get_files_returns(), $files);
+        $this->assertCount(2, $files['files']);
+
+        // Check the folder has one file.
+        $files = core_files_external::get_files($context->id, 'user', 'draft', $draftitemid, '/fakefolder/', '');
+        $files = external_api::clean_returnvalue(core_files_external::get_files_returns(), $files);
+        $this->assertCount(1, $files['files']);
+
+        // Delete a file and a folder.
+        $filestodelete = [
+            ['filepath' => '/', 'filename' => 'faketxt.txt'],
+            ['filepath' => '/fakefolder/', 'filename' => ''],
+        ];
+        $paths = core_files\external\delete\draft::execute($draftitemid, $filestodelete);
+        $paths = external_api::clean_returnvalue(core_files\external\delete\draft::execute_returns(), $paths);
+
+        // Check everything was deleted.
+        $files = core_files_external::get_files($context->id, 'user', 'draft', $draftitemid, '/', '');
+        $files = external_api::clean_returnvalue(core_files_external::get_files_returns(), $files);
+        $this->assertCount(0, $files['files']);
+    }
 }
index c3a9a88..3d80611 100644 (file)
@@ -190,6 +190,13 @@ class core extends \H5PCore {
                 'minorVersion' => $type->version->minor,
                 'patchVersion' => $type->version->patch,
             ];
+            // Add example and tutorial to the library, to store this information too.
+            if (isset($type->example)) {
+                $library['example'] = $type->example;
+            }
+            if (isset($type->tutorial)) {
+                $library['tutorial'] = $type->tutorial;
+            }
 
             $shoulddownload = true;
             if ($framework->getLibraryId($type->id, $type->version->major, $type->version->minor)) {
@@ -221,6 +228,8 @@ class core extends \H5PCore {
      * @return int|null Returns the id of the content type library installed, null otherwise.
      */
     public function fetch_content_type(array $library): ?int {
+        global $DB;
+
         $factory = new factory();
 
         // Download the latest content type from the H5P official repository.
@@ -250,6 +259,18 @@ class core extends \H5PCore {
         $librarykey = static::libraryToString($library);
         $libraryid = $factory->get_storage()->h5pC->librariesJsonData[$librarykey]["libraryId"];
 
+        // Update example and tutorial (if any of them are defined in $library).
+        $params = ['id' => $libraryid];
+        if (array_key_exists('example', $library)) {
+            $params['example'] = $library['example'];
+        }
+        if (array_key_exists('tutorial', $library)) {
+            $params['tutorial'] = $library['tutorial'];
+        }
+        if (count($params) > 1) {
+            $DB->update_record('h5p_libraries', $params);
+        }
+
         return $libraryid;
     }
 
index 6f293d0..43e4248 100644 (file)
@@ -76,8 +76,23 @@ class editor_ajax implements H5PEditorAjaxInterface {
      * @return mixed|null Returns results from querying the database
      */
     public function getContentTypeCache($machinename = null) {
-        // This is to be implemented when the Hub client is used.
-        return [];
+        global $DB;
+
+        // Added some extra fields to the result because they are expected by functions calling this. They have been
+        // taken from method getCachedLibsMap() in h5peditor.class.php.
+        $sql = "SELECT l.id, l.machinename AS machine_name, l.majorversion AS major_version,
+                       l.minorversion AS minor_version, l.patchversion AS patch_version, l.coremajor AS h5p_major_version,
+                       l.coreminor AS h5p_minor_version, l.title, l.tutorial, l.example,
+                       '' AS summary, '' AS description, '' AS icon, 0 AS created_at, 0 AS updated_at, 0 AS is_recommended,
+                       0 AS popularity, '' AS screenshots, '' AS license, '' AS owner
+                  FROM {h5p_libraries} l";
+        $params = [];
+        if (!empty($machinename)) {
+            $sql .= ' WHERE l.machinename = :machine_name';
+            $params = ['machine_name' => $machinename];
+        }
+
+        return $DB->get_records_sql($sql, $params);
     }
 
     /**
index 8f97a36..587b834 100644 (file)
@@ -228,7 +228,7 @@ class editor_framework implements H5peditorStorage {
         if ($libraries !== null) {
             // Get details for the specified libraries.
             $librariesin = [];
-            $fields = 'title, runnable, metadatasettings';
+            $fields = 'title, runnable, metadatasettings, example, tutorial';
 
             foreach ($libraries as $library) {
                 $params = [
@@ -243,11 +243,13 @@ class editor_framework implements H5peditorStorage {
                     $library->title = $details->title;
                     $library->runnable = $details->runnable;
                     $library->metadataSettings = json_decode($details->metadatasettings);
+                    $library->example = $details->example;
+                    $library->tutorial = $details->tutorial;
                     $librariesin[] = $library;
                 }
             }
         } else {
-            $fields = 'id, machinename as name, title, majorversion, minorversion, metadatasettings';
+            $fields = 'id, machinename as name, title, majorversion, minorversion, metadatasettings, example, tutorial';
             $librariesin = api::get_contenttype_libraries($fields);
         }
 
index 25d0ad9..d4b0d6a 100644 (file)
@@ -115,7 +115,16 @@ class framework implements \H5PFrameworkInterface {
      * @param string $url
      */
     public function setLibraryTutorialUrl($libraryname, $url) {
-        // Tutorial url is currently not being used or stored in libraries.
+        global $DB;
+
+        $sql = 'UPDATE {h5p_libraries}
+                   SET tutorial = :tutorial
+                 WHERE machinename = :machinename';
+        $params = [
+            'tutorial' => $url,
+            'machinename' => $libraryname,
+        ];
+        $DB->execute($sql, $params);
     }
 
     /**
index c0b6734..13f46c9 100644 (file)
@@ -79,6 +79,35 @@ class editor_ajax_testcase extends \advanced_testcase {
         $this->assertEquals($expectedlibraries, array_keys($actuallibraries));
     }
 
+    /**
+     * Test that getContentTypeCache method retrieves the latest library versions that exists locally.
+     */
+    public function test_getContentTypeCache(): void {
+        $this->resetAfterTest();
+
+        $h5pgenerator = \testing_util::get_data_generator()->get_plugin_generator('core_h5p');
+
+        // Create several libraries records.
+        $lib1 = $h5pgenerator->create_library_record('Library1', 'Lib1', 1, 0, 1, '', null, 'http://tutorial.org',
+            'http://example.org');
+        $lib2 = $h5pgenerator->create_library_record('Library2', 'Lib2', 2, 0, 1, '', null, 'http://tutorial.org');
+        $lib3 = $h5pgenerator->create_library_record('Library3', 'Lib3', 3, 0);
+        $libs = [$lib1, $lib2, $lib3];
+
+        $libraries = $this->editorajax->getContentTypeCache();
+        $this->assertCount(3, $libraries);
+        foreach ($libs as $lib) {
+            $library = $libraries[$lib->id];
+            $this->assertEquals($library->id, $lib->id);
+            $this->assertEquals($library->machine_name, $lib->machinename);
+            $this->assertEquals($library->major_version, $lib->majorversion);
+            $this->assertEquals($library->tutorial, $lib->tutorial);
+            $this->assertEquals($library->example, $lib->example);
+            $this->assertEquals($library->is_recommended, 0);
+            $this->assertEquals($library->summary, '');
+        }
+    }
+
     /**
      * Test that the method getTranslations retrieves the translations of several libraries.
      *
index 1a2c18b..13883c1 100644 (file)
@@ -25,7 +25,7 @@
 
 namespace core_h5p;
 
-defined('MOODLE_INTERNAL') || die();
+use core_collator;
 
 /**
  *
@@ -174,6 +174,55 @@ class framework_testcase extends \advanced_testcase {
         $this->assertEmpty($data);
     }
 
+    /**
+     * Test the behaviour of setLibraryTutorialUrl().
+     */
+    public function test_setLibraryTutorialUrl() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
+
+        // Create several libraries records.
+        $lib1 = $generator->create_library_record('Library1', 'Lib1', 1, 0, 1, '', null, 'http://tutorial1.org',
+            'http://example.org');
+        $lib2 = $generator->create_library_record('Library2', 'Lib2', 2, 0, 1, '', null, 'http://tutorial2.org');
+        $lib3 = $generator->create_library_record('Library3', 'Lib3', 3, 0);
+
+        // Check only lib1 tutorial URL is updated.
+        $url = 'https://newtutorial.cat';
+        $this->framework->setLibraryTutorialUrl($lib1->machinename, $url);
+
+        $libraries = $DB->get_records('h5p_libraries');
+        $this->assertEquals($libraries[$lib1->id]->tutorial, $url);
+        $this->assertNotEquals($libraries[$lib2->id]->tutorial, $url);
+
+        // Check lib1 tutorial URL is set to null.
+        $this->framework->setLibraryTutorialUrl($lib1->machinename, null);
+
+        $libraries = $DB->get_records('h5p_libraries');
+        $this->assertCount(3, $libraries);
+        $this->assertNull($libraries[$lib1->id]->tutorial);
+
+        // Check no tutorial URL is set if library name doesn't exist.
+        $this->framework->setLibraryTutorialUrl('Unexisting library', $url);
+
+        $libraries = $DB->get_records('h5p_libraries');
+        $this->assertCount(3, $libraries);
+        $this->assertNull($libraries[$lib1->id]->tutorial);
+        $this->assertEquals($libraries[$lib2->id]->tutorial, 'http://tutorial2.org');
+        $this->assertNull($libraries[$lib3->id]->tutorial);
+
+        // Check tutorial is set as expected when it was null.
+        $this->framework->setLibraryTutorialUrl($lib3->machinename, $url);
+
+        $libraries = $DB->get_records('h5p_libraries');
+        $this->assertEquals($libraries[$lib3->id]->tutorial, $url);
+        $this->assertNull($libraries[$lib1->id]->tutorial);
+        $this->assertEquals($libraries[$lib2->id]->tutorial, 'http://tutorial2.org');
+    }
+
     /**
      * Test the behaviour of setErrorMessage().
      */
@@ -472,15 +521,19 @@ class framework_testcase extends \advanced_testcase {
         // The addons array should return 2 results (Library and Library1 addon).
         $this->assertCount(2, $addons);
 
+        // Ensure the addons array is consistently ordered before asserting their contents.
+        core_collator::asort_array_of_arrays_by_key($addons, 'machineName');
+        [$addonone, $addontwo] = array_values($addons);
+
         // Make sure the version 1.3 is the latest 'Library' addon version.
-        $this->assertEquals('Library', $addons[0]['machineName']);
-        $this->assertEquals(1, $addons[0]['majorVersion']);
-        $this->assertEquals(3, $addons[0]['minorVersion']);
+        $this->assertEquals('Library', $addonone['machineName']);
+        $this->assertEquals(1, $addonone['majorVersion']);
+        $this->assertEquals(3, $addonone['minorVersion']);
 
         // Make sure the version 1.2 is the latest 'Library1' addon version.
-        $this->assertEquals('Library1', $addons[1]['machineName']);
-        $this->assertEquals(1, $addons[1]['majorVersion']);
-        $this->assertEquals(2, $addons[1]['minorVersion']);
+        $this->assertEquals('Library1', $addontwo['machineName']);
+        $this->assertEquals(1, $addontwo['majorVersion']);
+        $this->assertEquals(2, $addontwo['minorVersion']);
     }
 
     /**
@@ -504,7 +557,6 @@ class framework_testcase extends \advanced_testcase {
         $this->assertEquals('1', $libraries['MainLibrary'][0]->major_version);
         $this->assertEquals('0', $libraries['MainLibrary'][0]->minor_version);
         $this->assertEquals('1', $libraries['MainLibrary'][0]->patch_version);
-        $this->assertEquals('MainLibrary', $libraries['MainLibrary'][0]->machine_name);
     }
 
     /**
index 72e8087..78c234b 100644 (file)
@@ -173,9 +173,10 @@ class core_h5p_generator extends \component_generator_base {
      */
     public function generate_h5p_data(bool $createlibraryfiles = false): stdClass {
         // Create libraries.
-        $mainlib = $libraries[] = $this->create_library_record('MainLibrary', 'Main Lib', 1, 0);
-        $lib1 = $libraries[] = $this->create_library_record('Library1', 'Lib1', 2, 0);
-        $lib2 = $libraries[] = $this->create_library_record('Library2', 'Lib2', 2, 1);
+        $mainlib = $libraries[] = $this->create_library_record('MainLibrary', 'Main Lib', 1, 0, 1, '', null,
+            'http://tutorial.org', 'http://example.org');
+        $lib1 = $libraries[] = $this->create_library_record('Library1', 'Lib1', 2, 0, 1, '', null, null,  'http://example.org');
+        $lib2 = $libraries[] = $this->create_library_record('Library2', 'Lib2', 2, 1, 1, '', null, 'http://tutorial.org');
         $lib3 = $libraries[] = $this->create_library_record('Library3', 'Lib3', 3, 2);
         $lib4 = $libraries[] = $this->create_library_record('Library4', 'Lib4', 1, 1);
         $lib5 = $libraries[] = $this->create_library_record('Library5', 'Lib5', 1, 3);
@@ -248,10 +249,13 @@ class core_h5p_generator extends \component_generator_base {
      * @param int $patchversion The library's patch version
      * @param string $semantics Json describing the content structure for the library
      * @param string $addto The plugin configuration data
+     * @param string $tutorial The tutorial URL
+     * @param string $examlpe The example URL
      * @return stdClass An object representing the added library record
      */
     public function create_library_record(string $machinename, string $title, int $majorversion = 1,
-            int $minorversion = 0, int $patchversion = 1, string $semantics = '', string $addto = null): stdClass {
+            int $minorversion = 0, int $patchversion = 1, string $semantics = '', string $addto = null,
+            string $tutorial = null, string $example = null): stdClass {
         global $DB;
 
         $content = array(
@@ -266,7 +270,9 @@ class core_h5p_generator extends \component_generator_base {
             'preloadedcss' => 'css/example.css',
             'droplibrarycss' => '',
             'semantics' => $semantics,
-            'addto' => $addto
+            'addto' => $addto,
+            'tutorial' => $tutorial,
+            'example' => $example
         );
 
         $libraryid = $DB->insert_record('h5p_libraries', $content);
index dc266f5..05be967 100644 (file)
@@ -227,7 +227,9 @@ class generator_testcase extends \advanced_testcase {
 
         $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
 
-        $data = $generator->create_library_record('Library', 'Lib', 1, 2, 3, 'Semantics example', '/regex11/');
+        $data = $generator->create_library_record(
+            'Library', 'Lib', 1, 2, 3, 'Semantics example', '/regex11/', 'http://tutorial.org/', 'http://example.org/'
+        );
         unset($data->id);
 
         $expected = (object) [
@@ -244,6 +246,8 @@ class generator_testcase extends \advanced_testcase {
             'droplibrarycss' => '',
             'semantics' => 'Semantics example',
             'addto' => '/regex11/',
+            'tutorial' => 'http://tutorial.org/',
+            'example' => 'http://example.org/',
             'coremajor' => null,
             'coreminor' => null,
             'metadatasettings' => null,
index 8d7f088..d441fdc 100644 (file)
@@ -68,8 +68,14 @@ class h5p_core_testcase extends \advanced_testcase {
 
         // Get info of latest content types versions.
         $contenttypes = $this->core->get_latest_content_types()->contentTypes;
-        // We are installing the first content type.
+        // We are installing the first content type with tutorial and example fields (or the first one if none has them).
         $librarydata = $contenttypes[0];
+        foreach ($contenttypes as $contentype) {
+            if (isset($contenttype->tutorial) && isset($contenttype->example)) {
+                $librarydata = $contenttype;
+                break;
+            }
+        }
 
         $library = [
                 'machineName' => $librarydata->id,
@@ -77,6 +83,13 @@ class h5p_core_testcase extends \advanced_testcase {
                 'minorVersion' => $librarydata->version->minor,
                 'patchVersion' => $librarydata->version->patch,
         ];
+        // Add example and tutorial to the library.
+        if (isset($librarydata->example)) {
+            $library['example'] = $librarydata->example;
+        }
+        if (isset($librarydata->tutorial)) {
+            $library['tutorial'] = $librarydata->tutorial;
+        }
 
         // Verify that the content type is not yet installed.
         $conditions['machinename'] = $library['machineName'];
@@ -92,6 +105,10 @@ class h5p_core_testcase extends \advanced_testcase {
         $this->assertEquals($librarydata->id, $typeinstalled->machinename);
         $this->assertEquals($librarydata->coreApiVersionNeeded->major, $typeinstalled->coremajor);
         $this->assertEquals($librarydata->coreApiVersionNeeded->minor, $typeinstalled->coreminor);
+        if (isset($librarydata->tutorial)) {
+            $this->assertEquals($librarydata->tutorial, $typeinstalled->tutorial);
+            $this->assertEquals($librarydata->example, $typeinstalled->example);
+        }
     }
 
     /**
index 22a7ea1..a06f575 100644 (file)
@@ -30,9 +30,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['cannotcreatedboninstall'] = '<p> لا يمكن إنشاء قاعدة البيانات. </p>
-<p> لا وجود لقاعدة البيانات المحددة والمستخدم المُعين ليس لديه إذن لإنشاء قاعدة البيانات. </ p>
-<p> المسؤول عن الموقع يجب أن يتحقق من إعدادات قاعدة بيانات. </p>';
+$string['cannotcreatedboninstall'] = '<p>لا يمكن إنشاء قاعدة البيانات.</p>
+<p>لا وجود لقاعدة البيانات المحددة والمستخدم المُعطى ليس لديه صلاحية إنشاء قاعدة البيانات.</p>
+<p>مسؤول الموقع يجب أن يتحقق من إعدادات قاعدة البيانات.</p>';
 $string['cannotcreatelangdir'] = 'لا يمكن إنشاء مجلد اللغة';
 $string['cannotcreatetempdir'] = 'لا يمكن إنشاء المجلد المؤقت';
 $string['cannotdownloadcomponents'] = 'تعذر تنزيل المُكونات';
index 1b80459..82d998d 100644 (file)
@@ -42,7 +42,7 @@ $string['databasename'] = 'اسم قاعدة البيانات';
 $string['databasetypehead'] = 'إختر مشغل قاعدة البيانات';
 $string['dataroot'] = 'مجلد البيانات';
 $string['datarootpermission'] = 'صلاحيات مجلدات البيانات';
-$string['dbprefix'] = 'مقدمة الجداول';
+$string['dbprefix'] = 'بادئة الجداول';
 $string['dirroot'] = 'مجلد مودل';
 $string['environmenthead'] = 'يتم فحص البيئة';
 $string['environmentsub2'] = 'كل إصدار من مودل يتطلب وجود إصدار معين من PHP على الأقل مع عدد من امتداداته الإجبارية.
index 7530ded..f8d5b9b 100644 (file)
@@ -390,6 +390,7 @@ $string['loginasnoenrol'] = 'You cannot use enrol or unenrol when in course "Log
 $string['loginasonecourse'] = 'You cannot enter this course.<br /> You have to terminate the "Login as" session before entering any other course.';
 $string['maxbytesfile'] = 'The file {$a->file} is too large. The maximum size you can upload is {$a->size}.';
 $string['maxareabytes'] = 'The file is larger than the space remaining in this area.';
+$string['messageundeliveredbynotificationsettings'] = 'The message could not be sent because personal messages between users (in Notification settings) has been disabled by a site administrator.';
 $string['messagingdisable'] = 'Messaging is disabled on this site';
 $string['mimetexisnotexist'] = 'Your system is not configured to run mimeTeX. You need to obtain the C source from <a href="https://www.forkosh.com/mimetex.zip">https://www.forkosh.com/mimetex.zip</a>, compile it and put the executable into your moodle/filter/tex/ directory.';
 $string['mimetexnotexecutable'] = 'Custom mimetex is not executable!';
index 41b2cee..3db9de3 100644 (file)
@@ -1819,6 +1819,7 @@ $string['searchagain'] = 'Search again';
 $string['searchactivities'] = 'Search for activities by name or description';
 $string['searchbyemail'] = 'Search by email address';
 $string['searchbyusername'] = 'Search by username';
+$string['searchcoursecategories'] = 'Search categories';
 $string['searchcourses'] = 'Search courses';
 $string['searchoptions'] = 'Search options';
 $string['searchresults'] = 'Search results';
index fa25ae4..e3366b6 100644 (file)
@@ -96,6 +96,7 @@ class core_component {
         'MoodleHQ\\RTLCSS' => 'lib/rtlcss',
         'ScssPhp\\ScssPhp' => 'lib/scssphp',
         'Box\\Spout' => 'lib/spout/src/Spout',
+        'BirknerAlex\\XMPPHP' => 'lib/jabber/XMPP',
         'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
         'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
         'IMSGlobal\LTI' => 'lib/ltiprovider/src',
index 75d980f..486baee 100644 (file)
@@ -621,7 +621,6 @@ abstract class base {
      */
     public final function get_default_uninstall_url($return = 'overview') {
         return new moodle_url('/admin/plugins.php', array(
-            'sesskey' => sesskey(),
             'uninstall' => $this->component,
             'confirm' => 0,
             'return' => $return,
index 40639c2..61ee51e 100644 (file)
@@ -56,6 +56,9 @@ class redis extends handler {
     protected $lockretry = 100;
     /** @var int $serializer The serializer to use */
     protected $serializer = \Redis::SERIALIZER_PHP;
+    /** @var string $lasthash hash of the session data content */
+    protected $lasthash = null;
+
     /**
      * @var int $lockexpire how long to wait in seconds before expiring the lock automatically
      * so that other requests may continue execution, ignored if PECL redis is below version 2.2.0.
@@ -237,6 +240,7 @@ class redis extends handler {
      * @return bool true on success.  false on unable to unlock sessions.
      */
     public function handler_close() {
+        $this->lasthash = null;
         try {
             foreach ($this->locks as $id => $expirytime) {
                 if ($expirytime > $this->time()) {
@@ -269,6 +273,7 @@ class redis extends handler {
                 if ($this->requires_write_lock()) {
                     $this->unlock_session($id);
                 }
+                $this->lasthash = sha1('');
                 return '';
             }
             $this->connection->expire($id, $this->timeout);
@@ -276,6 +281,7 @@ class redis extends handler {
             error_log('Failed talking to redis: '.$e->getMessage());
             throw $e;
         }
+        $this->lasthash = sha1(base64_encode($sessiondata));
         return $sessiondata;
     }
 
@@ -287,6 +293,14 @@ class redis extends handler {
      * @return bool true on write success, false on failure
      */
     public function handler_write($id, $data) {
+
+        $hash = sha1(base64_encode($data));
+
+        // If the content has not changed don't bother writing.
+        if ($hash === $this->lasthash) {
+            return true;
+        }
+
         if (is_null($this->connection)) {
             // The session has already been closed, don't attempt another write.
             error_log('Tried to write session: '.$id.' before open or after close.');
@@ -313,6 +327,7 @@ class redis extends handler {
      * @return bool true if the session was deleted, false otherwise.
      */
     public function handler_destroy($id) {
+        $this->lasthash = null;
         try {
             $this->connection->del($id);
             $this->unlock_session($id);
index bcc3e18..4781461 100644 (file)
         <FIELD NAME="coremajor" TYPE="int" LENGTH="4" NOTNULL="false" SEQUENCE="false" COMMENT="H5P core API major version required"/>
         <FIELD NAME="coreminor" TYPE="int" LENGTH="4" NOTNULL="false" SEQUENCE="false" COMMENT="H5P core API minor version required"/>
         <FIELD NAME="metadatasettings" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Library metadata settings"/>
+        <FIELD NAME="tutorial" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Tutorial URL"/>
+        <FIELD NAME="example" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Example URL"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index c589221..9f21e40 100644 (file)
@@ -814,6 +814,13 @@ $functions = array(
         'type'        => 'write',
         'classpath'   => 'files/externallib.php',
     ),
+    'core_files_delete_draft_files' => array(
+        'classname' => 'core_files\external\delete\draft',
+        'methodname' => 'execute',
+        'description' => 'Delete the indicated files (or directories) from a user draft file area.',
+        'type'        => 'write',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_form_get_filetypes_browser_data' => array(
         'classname' => 'core_form\external',
         'methodname' => 'get_filetypes_browser_data',
index f7665df..412f94b 100644 (file)
@@ -2762,5 +2762,25 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2021052500.20);
     }
 
+    if ($oldversion < 2021052500.24) {
+        // Define fields tutorial and example to be added to h5p_libraries.
+        $table = new xmldb_table('h5p_libraries');
+
+        // Add tutorial field.
+        $field = new xmldb_field('tutorial', XMLDB_TYPE_TEXT, null, null, null, null, null, 'metadatasettings');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Add example field.
+        $field = new xmldb_field('example', XMLDB_TYPE_TEXT, null, null, null, null, null, 'tutorial');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2021052500.24);
+    }
+
     return true;
 }
index 3c6fbda..af33ffc 100644 (file)
         }
     }
 }}
-{{< core_form/element-template }}
+{{< core_form/element-group }}
     {{$element}}
-        <span class="fdefaultcustom">
-        {{#element.elements}}
-            {{{separator}}}
-            {{{html}}}
-        {{/element.elements}}
-        </span>
+        <fieldset class="w-100 m-0 p-0 border-0" id="{{element.id}}">
+            <legend class="sr-only">{{label}}</legend>
+            <div class="d-flex flex-wrap align-items-center">
+                <span class="fdefaultcustom">
+                    {{#element.elements}}
+                        {{{separator}}}
+                        {{{html}}}
+                    {{/element.elements}}
+                </span>
+            </div>
+        </fieldset>
     {{/element}}
-{{/ core_form/element-template }}
+{{/ core_form/element-group }}
index befaf60..51b7077 100644 (file)
 <?php
-/**
- * XMPPHP: The PHP XMPP Library
- * Copyright (C) 2008  Nathanael C. Fritz
- * This file is part of SleekXMPP.
- * 
- * XMPPHP 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 2 of the License, or
- * (at your option) any later version.
- * 
- * XMPPHP 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 XMPPHP; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
- *
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
- * @copyright  2008 Nathanael C. Fritz
- */
 
-/** XMPPHP_XMLStream */
-require_once dirname(__FILE__) . "/XMPP.php";
+namespace BirknerAlex\XMPPHP;
+
+    /**
+     * XMPPHP: The PHP XMPP Library
+     * Copyright (C) 2008  Nathanael C. Fritz
+     * This file is part of SleekXMPP.
+     *
+     * XMPPHP 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 2 of the License, or
+     * (at your option) any later version.
+     *
+     * XMPPHP 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 XMPPHP; if not, write to the Free Software
+     * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+     *
+     * @category   xmpphp
+     * @package    XMPPHP
+     * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+     * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+     * @author     Michael Garvin <JID: gar@netflint.net>
+     * @author     Alexander Birkner (https://github.com/BirknerAlex)
+     * @copyright  2008 Nathanael C. Fritz
+     */
 
 /**
  * XMPPHP Main Class
- * 
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
+ *
+ * @category   xmpphp
+ * @package    XMPPHP
+ * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author     Michael Garvin <JID: gar@netflint.net>
  * @copyright  2008 Nathanael C. Fritz
- * @version    $Id$
+ * @version    $Id$
  */
-class XMPPHP_BOSH extends XMPPHP_XMPP {
-
-               protected $rid;
-               protected $sid;
-               protected $http_server;
-               protected $http_buffer = Array();
-               protected $session = false;
+class BOSH extends XMPP
+{
+    protected $rid;
+    protected $sid;
+    protected $http_server;
+    protected $http_buffer = Array();
+    protected $session = false;
 
-               public function connect($server, $wait='1', $session=false) {
-                       $this->http_server = $server;
-                       $this->use_encryption = false;
-                       $this->session = $session;
+    public function connect($server, $wait = '1', $session = false)
+    {
+        $this->http_server = $server;
+        $this->use_encryption = false;
+        $this->session = $session;
 
-                       $this->rid = 3001;
-                       $this->sid = null;
-                       if($session)
-                       {
-                               $this->loadSession();
-                       }
-                       if(!$this->sid) {
-                               $body = $this->__buildBody();
-                               $body->addAttribute('hold','1');
-                               $body->addAttribute('to', $this->host);
-                               $body->addAttribute('route', "xmpp:{$this->host}:{$this->port}");
-                               $body->addAttribute('secure','true');
-                               $body->addAttribute('xmpp:version','1.6', 'urn:xmpp:xbosh');
-                               $body->addAttribute('wait', strval($wait));
-                               $body->addAttribute('ack','1');
-                               $body->addAttribute('xmlns:xmpp','urn:xmpp:xbosh');
-                               $buff = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>";
-                               xml_parse($this->parser, $buff, false);
-                               $response = $this->__sendBody($body);
-                               $rxml = new SimpleXMLElement($response);
-                               $this->sid = $rxml['sid'];
+        $this->rid = 3001;
+        $this->sid = null;
+        if ($session) {
+            $this->loadSession();
+        }
+        if (!$this->sid) {
+            $body = $this->__buildBody();
+            $body->addAttribute('hold', '1');
+            $body->addAttribute('to', $this->host);
+            $body->addAttribute('route', "xmpp:{$this->host}:{$this->port}");
+            $body->addAttribute('secure', 'true');
+            $body->addAttribute('xmpp:version', '1.6', 'urn:xmpp:xbosh');
+            $body->addAttribute('wait', strval($wait));
+            $body->addAttribute('ack', '1');
+            $body->addAttribute('xmlns:xmpp', 'urn:xmpp:xbosh');
+            $buff = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>";
+            xml_parse($this->parser, $buff, false);
+            $response = $this->__sendBody($body);
+            $rxml = new \SimpleXMLElement($response);
+            $this->sid = $rxml['sid'];
 
-                       } else {
-                               $buff = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>";
-                               xml_parse($this->parser, $buff, false);
-                       }
-               }
+        } else {
+            $buff = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>";
+            xml_parse($this->parser, $buff, false);
+        }
+    }
 
-               public function __sendBody($body=null, $recv=true) {
-                       if(!$body) {
-                               $body = $this->__buildBody();
-                       }
-                       $ch = curl_init($this->http_server);
-                       curl_setopt($ch, CURLOPT_HEADER, 0);
-                       curl_setopt($ch, CURLOPT_POST, 1);
-                       curl_setopt($ch, CURLOPT_POSTFIELDS, $body->asXML());
-                       curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
-                       $header = array('Accept-Encoding: gzip, deflate','Content-Type: text/xml; charset=utf-8');
-                       curl_setopt($ch, CURLOPT_HTTPHEADER, $header );
-                       curl_setopt($ch, CURLOPT_VERBOSE, 0);
-                       $output = '';
-                       if($recv) {
-                               curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
-                               $output = curl_exec($ch);
-                               $this->http_buffer[] = $output;
-                       }
-                       curl_close($ch);
-                       return $output;
-               }
+    public function __sendBody($body = null, $recv = true)
+    {
+        if (!$body) {
+            $body = $this->__buildBody();
+        }
+        $ch = curl_init($this->http_server);
+        curl_setopt($ch, CURLOPT_HEADER, 0);
+        curl_setopt($ch, CURLOPT_POST, 1);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, $body->asXML());
+        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+        $header = array('Accept-Encoding: gzip, deflate', 'Content-Type: text/xml; charset=utf-8');
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
+        curl_setopt($ch, CURLOPT_VERBOSE, 0);
+        $output = '';
+        if ($recv) {
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+            $output = curl_exec($ch);
+            $this->http_buffer[] = $output;
+        }
+        curl_close($ch);
+        return $output;
+    }
 
-               public function __buildBody($sub=null) {
-                       $xml = new SimpleXMLElement("<body xmlns='http://jabber.org/protocol/httpbind' xmlns:xmpp='urn:xmpp:xbosh' />");
-                       $xml->addAttribute('content', 'text/xml; charset=utf-8');
-                       $xml->addAttribute('rid', $this->rid);
-                       $this->rid += 1;
-                       if($this->sid) $xml->addAttribute('sid', $this->sid);
-                       #if($this->sid) $xml->addAttribute('xmlns', 'http://jabber.org/protocol/httpbind');
-                       $xml->addAttribute('xml:lang', 'en');
-                       if($sub) { // ok, so simplexml is lame
-                               $p = dom_import_simplexml($xml);
-                               $c = dom_import_simplexml($sub);
-                               $cn = $p->ownerDocument->importNode($c, true);
-                               $p->appendChild($cn);
-                               $xml = simplexml_import_dom($p);
-                       }
-                       return $xml;
-               }
+    public function __buildBody($sub = null)
+    {
+        $xml = new \SimpleXMLElement("<body xmlns='http://jabber.org/protocol/httpbind' xmlns:xmpp='urn:xmpp:xbosh' />");
+        $xml->addAttribute('content', 'text/xml; charset=utf-8');
+        $xml->addAttribute('rid', $this->rid);
+        $this->rid += 1;
+        if ($this->sid) $xml->addAttribute('sid', $this->sid);
+        #if($this->sid) $xml->addAttribute('xmlns', 'http://jabber.org/protocol/httpbind');
+        $xml->addAttribute('xml:lang', 'en');
+        if ($sub) { // ok, so simplexml is lame
+            $p = dom_import_simplexml($xml);
+            $c = dom_import_simplexml($sub);
+            $cn = $p->ownerDocument->importNode($c, true);
+            $p->appendChild($cn);
+            $xml = simplexml_import_dom($p);
+        }
+        return $xml;
+    }
 
-               public function __process() {
-                       if($this->http_buffer) {
-                               $this->__parseBuffer();
-                       } else {
-                               $this->__sendBody();
-                               $this->__parseBuffer();
-                       }
-               }
+    public function __process()
+    {
+        if ($this->http_buffer) {
+            $this->__parseBuffer();
+        } else {
+            $this->__sendBody();
+            $this->__parseBuffer();
+        }
+    }
 
-               public function __parseBuffer() {
-                       while ($this->http_buffer) {
-                               $idx = key($this->http_buffer);
-                               $buffer = $this->http_buffer[$idx];
-                               unset($this->http_buffer[$idx]);
-                               if($buffer) {
-                                       $xml = new SimpleXMLElement($buffer);
-                                       $children = $xml->xpath('child::node()');
-                                       foreach ($children as $child) {
-                                               $buff = $child->asXML();
-                                               $this->log->log("RECV: $buff",  XMPPHP_Log::LEVEL_VERBOSE);
-                                               xml_parse($this->parser, $buff, false);
-                                       }
-                               }
-                       }
-               }
+    public function __parseBuffer()
+    {
+        while ($this->http_buffer) {
+            $idx = key($this->http_buffer);
+            $buffer = $this->http_buffer[$idx];
+            unset($this->http_buffer[$idx]);
+            if ($buffer) {
+                $xml = new \SimpleXMLElement($buffer);
+                $children = $xml->xpath('child::node()');
+                foreach ($children as $child) {
+                    $buff = $child->asXML();
+                    $this->log->log("RECV: $buff", Log::LEVEL_VERBOSE);
+                    xml_parse($this->parser, $buff, false);
+                }
+            }
+        }
+    }
 
-               public function send($msg) {
-                       $this->log->log("SEND: $msg",  XMPPHP_Log::LEVEL_VERBOSE);
-                       $msg = new SimpleXMLElement($msg);
-                       #$msg->addAttribute('xmlns', 'jabber:client');
-                       $this->__sendBody($this->__buildBody($msg), true);
-                       #$this->__parseBuffer();
-               }
+    public function send($msg)
+    {
+        $this->log->log("SEND: $msg", Log::LEVEL_VERBOSE);
+        $msg = new \SimpleXMLElement($msg);
+        #$msg->addAttribute('xmlns', 'jabber:client');
+        $this->__sendBody($this->__buildBody($msg), true);
+        #$this->__parseBuffer();
+    }
 
-               public function reset() {
-                       $this->xml_depth = 0;
-                       unset($this->xmlobj);
-                       $this->xmlobj = array();
-                       $this->setupParser();
-                       #$this->send($this->stream_start);
-                       $body = $this->__buildBody();
-                       $body->addAttribute('to', $this->host);
-                       $body->addAttribute('xmpp:restart', 'true', 'urn:xmpp:xbosh');
-                       $buff = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>";
-                       $response = $this->__sendBody($body);
-                       $this->been_reset = true;
-                       xml_parse($this->parser, $buff, false);
-               }
+    public function reset()
+    {
+        $this->xml_depth = 0;
+        unset($this->xmlobj);
+        $this->xmlobj = array();
+        $this->setupParser();
+        #$this->send($this->stream_start);
+        $body = $this->__buildBody();
+        $body->addAttribute('to', $this->host);
+        $body->addAttribute('xmpp:restart', 'true', 'urn:xmpp:xbosh');
+        $buff = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>";
+        $response = $this->__sendBody($body);
+        $this->been_reset = true;
+        xml_parse($this->parser, $buff, false);
+    }
 
-               public function loadSession() {
-                       if(isset($_SESSION['XMPPHP_BOSH_RID'])) $this->rid = $_SESSION['XMPPHP_BOSH_RID'];
-                       if(isset($_SESSION['XMPPHP_BOSH_SID'])) $this->sid = $_SESSION['XMPPHP_BOSH_SID'];
-                       if(isset($_SESSION['XMPPHP_BOSH_authed'])) $this->authed = $_SESSION['XMPPHP_BOSH_authed'];
-                       if(isset($_SESSION['XMPPHP_BOSH_jid'])) $this->jid = $_SESSION['XMPPHP_BOSH_jid'];
-                       if(isset($_SESSION['XMPPHP_BOSH_fulljid'])) $this->fulljid = $_SESSION['XMPPHP_BOSH_fulljid'];
-               }
+    public function loadSession()
+    {
+        if (isset($_SESSION['XMPPHP_BOSH_RID'])) $this->rid = $_SESSION['XMPPHP_BOSH_RID'];
+        if (isset($_SESSION['XMPPHP_BOSH_SID'])) $this->sid = $_SESSION['XMPPHP_BOSH_SID'];
+        if (isset($_SESSION['XMPPHP_BOSH_authed'])) $this->authed = $_SESSION['XMPPHP_BOSH_authed'];
+        if (isset($_SESSION['XMPPHP_BOSH_jid'])) $this->jid = $_SESSION['XMPPHP_BOSH_jid'];
+        if (isset($_SESSION['XMPPHP_BOSH_fulljid'])) $this->fulljid = $_SESSION['XMPPHP_BOSH_fulljid'];
+    }
 
-               public function saveSession() {
-                       $_SESSION['XMPPHP_BOSH_RID'] = (string) $this->rid;
-                       $_SESSION['XMPPHP_BOSH_SID'] = (string) $this->sid;
-                       $_SESSION['XMPPHP_BOSH_authed'] = (boolean) $this->authed;
-                       $_SESSION['XMPPHP_BOSH_jid'] = (string) $this->jid;
-                       $_SESSION['XMPPHP_BOSH_fulljid'] = (string) $this->fulljid;
-               }
-}
+    public function saveSession()
+    {
+        $_SESSION['XMPPHP_BOSH_RID'] = (string)$this->rid;
+        $_SESSION['XMPPHP_BOSH_SID'] = (string)$this->sid;
+        $_SESSION['XMPPHP_BOSH_authed'] = (boolean)$this->authed;
+        $_SESSION['XMPPHP_BOSH_jid'] = (string)$this->jid;
+        $_SESSION['XMPPHP_BOSH_fulljid'] = (string)$this->fulljid;
+    }
+}
\ No newline at end of file
index da59bc7..c8129ab 100644 (file)
@@ -1,41 +1,47 @@
 <?php
+
+namespace BirknerAlex\XMPPHP;
+
+use \Exception as ObjectException;
+
 /**
  * XMPPHP: The PHP XMPP Library
  * Copyright (C) 2008  Nathanael C. Fritz
  * This file is part of SleekXMPP.
- * 
+ *
  * XMPPHP 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 2 of the License, or
  * (at your option) any later version.
- * 
+ *
  * XMPPHP 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 XMPPHP; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  *
- * @category   xmpphp 
+ * @category   xmpphp
  * @package    XMPPHP
  * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
  * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
+ * @author     Michael Garvin <JID: gar@netflint.net>
+ * @author     Alexander Birkner (https://github.com/BirknerAlex)
  * @copyright  2008 Nathanael C. Fritz
  */
 
 /**
- * XMPPHP Exception
+ * XMPPHP Main Class
  *
- * @category   xmpphp 
+ * @category   xmpphp
  * @package    XMPPHP
  * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
  * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
+ * @author     Michael Garvin <JID: gar@netflint.net>
  * @copyright  2008 Nathanael C. Fritz
  * @version    $Id$
  */
-class XMPPHP_Exception extends Exception {
+class Exception extends ObjectException {
 }
index a9bce3d..0ceaeac 100644 (file)
@@ -1,42 +1,47 @@
 <?php
-/**
- * XMPPHP: The PHP XMPP Library
- * Copyright (C) 2008  Nathanael C. Fritz
- * This file is part of SleekXMPP.
- * 
- * XMPPHP 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 2 of the License, or
- * (at your option) any later version.
- * 
- * XMPPHP 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 XMPPHP; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
- *
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
- * @copyright  2008 Nathanael C. Fritz
- */
+
+namespace BirknerAlex\XMPPHP;
+
+       /**
+        * XMPPHP: The PHP XMPP Library
+        * Copyright (C) 2008  Nathanael C. Fritz
+        * This file is part of SleekXMPP.
+        *
+        * XMPPHP 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 2 of the License, or
+        * (at your option) any later version.
+        *
+        * XMPPHP 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 XMPPHP; if not, write to the Free Software
+        * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+        *
+        * @category   xmpphp
+        * @package    XMPPHP
+        * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+        * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+        * @author     Michael Garvin <JID: gar@netflint.net>
+        * @author     Alexander Birkner (https://github.com/BirknerAlex)
+        * @copyright  2008 Nathanael C. Fritz
+        */
 
 /**
- * XMPPHP Log
- * 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
+ * XMPPHP Main Class
+ *
+ * @category   xmpphp
+ * @package    XMPPHP
+ * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author     Michael Garvin <JID: gar@netflint.net>
  * @copyright  2008 Nathanael C. Fritz
- * @version    $Id$
+ * @version    $Id$
  */
-class XMPPHP_Log {
+class Log {
        
        const LEVEL_ERROR   = 0;
        const LEVEL_WARNING = 1;
diff --git a/lib/jabber/XMPP/README.txt b/lib/jabber/XMPP/README.txt
deleted file mode 100644 (file)
index 0c2f53d..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-About
-================================================================================
-XMPPHP is an elegant PHP library for XMPP (aka Jabber, Google Talk, etc).
-
-Homepage: http://code.google.com/p/xmpphp
-Author: Nathan Fritz, jabber id: fritzy [at] netflint.net
-Co-Author: Stephan Wentz, jabber id: stephan [at] jabber.wentz.it
-
-If you have any questions (no matter how dumb), please send me an IM.  I enjoy
-helping people with my code.
-
-
-Requirements
-================================================================================
-* PHP 5.x
-* SSL Support Compiled
-
-History
-================================================================================
-Carlo Zottmann handed me maintenance of Class.Jabber.PHP years and years ago
-(2003?).  While I did fix some bugs, I never did much with it.  I promised many
-people that it would return as a PHP5 rewrite.  That day has finally come.
-
-This code is based on my experience with Class.Jabber.PHP, but more closely
-related to my Python library, SleekXMPP (http://code.google.com/p/sleekxmpp).
-
-Documentation
-================================================================================
-For now, look at the examples.  In the near future, I'll have better
-documentation on the website.
-
-TODO
-================================================================================
-* Documentation
-* MUC Support
-
-License Exception
-===============================================================================
-Please contact Nathan Fritz for library exceptions if you would like to
-distribute XMPPHP with a non-GPL compatible license.
-
-Also, if you would like to distribute XMPPHP as part of a commercial package,
-I sell commercial licenses.
index 69457b2..99ec8c1 100644 (file)
@@ -1,43 +1,46 @@
 <?php
+
+namespace BirknerAlex\XMPPHP;
+
 /**
  * XMPPHP: The PHP XMPP Library
  * Copyright (C) 2008  Nathanael C. Fritz
  * This file is part of SleekXMPP.
- * 
+ *
  * XMPPHP 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 2 of the License, or
  * (at your option) any later version.
- * 
+ *
  * XMPPHP 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 XMPPHP; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  *
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
+ * @category   xmpphp
+ * @package    XMPPHP
+ * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author     Michael Garvin <JID: gar@netflint.net>
+ * @author     Alexander Birkner (https://github.com/BirknerAlex)
  * @copyright  2008 Nathanael C. Fritz
  */
 
 /**
- * XMPPHP Roster Object
- * 
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
+ * XMPPHP Main Class
+ *
+ * @category   xmpphp
+ * @package    XMPPHP
+ * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author     Michael Garvin <JID: gar@netflint.net>
  * @copyright  2008 Nathanael C. Fritz
- * @version    $Id$
+ * @version    $Id$
  */
-
 class Roster {
        /**
         * Roster array, handles contacts and presence.  Indexed by jid.
@@ -118,15 +121,17 @@ class Roster {
         * @param string $status
        */
        public function setPresence($presence, $priority, $show, $status) {
-               list($jid, $resource) = explode("/", $presence);
+               $presence = explode('/', $presence, 2);
+               $jid = $presence[0];
+               $resource = isset($presence[1]) ? $presence[1] : '';
                if ($show != 'unavailable') {
                        if (!$this->isContact($jid)) {
                                $this->addContact($jid, 'not-in-roster');
                        }
-                       $resource = $resource ? $resource : '';
                        $this->roster_array[$jid]['presence'][$resource] = array('priority' => $priority, 'show' => $show, 'status' => $status);
                } else { //Nuke unavailable resources to save memory
                        unset($this->roster_array[$jid]['resource'][$resource]);
+                       unset($this->roster_array[$jid]['presence'][$resource]);
                }
        }
 
@@ -137,7 +142,7 @@ class Roster {
         * @param string $jid
         */
        public function getPresence($jid) {
-               $split = explode("/", $jid);
+               $split = explode('/', $jid, 2);
                $jid = $split[0];
                if($this->isContact($jid)) {
                        $current = array('resource' => '', 'active' => '', 'priority' => -129, 'show' => '', 'status' => ''); //Priorities can only be -128 = 127
index 0d3e219..0bb314d 100644 (file)
@@ -1,43 +1,47 @@
-<?php 
-/**
- * XMPPHP: The PHP XMPP Library
- * Copyright (C) 2008  Nathanael C. Fritz
- * This file is part of SleekXMPP.
- * 
- * XMPPHP 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 2 of the License, or
- * (at your option) any later version.
- * 
- * XMPPHP 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 XMPPHP; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
- *
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
- * @copyright  2008 Nathanael C. Fritz
- */
+<?php
+
+namespace BirknerAlex\XMPPHP;
+
+       /**
+        * XMPPHP: The PHP XMPP Library
+        * Copyright (C) 2008  Nathanael C. Fritz
+        * This file is part of SleekXMPP.
+        *
+        * XMPPHP 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 2 of the License, or
+        * (at your option) any later version.
+        *
+        * XMPPHP 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 XMPPHP; if not, write to the Free Software
+        * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+        *
+        * @category   xmpphp
+        * @package    XMPPHP
+        * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+        * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+        * @author     Michael Garvin <JID: gar@netflint.net>
+        * @author     Alexander Birkner (https://github.com/BirknerAlex)
+        * @copyright  2008 Nathanael C. Fritz
+        */
 
 /**
- * XMPPHP XML Object
- * 
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
+ * XMPPHP Main Class
+ *
+ * @category   xmpphp
+ * @package    XMPPHP
+ * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author     Michael Garvin <JID: gar@netflint.net>
  * @copyright  2008 Nathanael C. Fritz
- * @version    $Id$
+ * @version    $Id$
  */
-class XMPPHP_XMLObj {
+class XMLObj {
        /**
         * Tag name
         *
index 41c7a78..badc805 100644 (file)
@@ -1,52 +1,47 @@
 <?php
-/**
- * XMPPHP: The PHP XMPP Library
- * Copyright (C) 2008  Nathanael C. Fritz
- * This file is part of SleekXMPP.
- * 
- * XMPPHP 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 2 of the License, or
- * (at your option) any later version.
- * 
- * XMPPHP 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 XMPPHP; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
- *
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
- * @copyright  2008 Nathanael C. Fritz
- */
 
-/** XMPPHP_Exception */
-require_once dirname(__FILE__) . '/Exception.php';
+namespace BirknerAlex\XMPPHP;
 
-/** XMPPHP_XMLObj */
-require_once dirname(__FILE__) . '/XMLObj.php';
-
-/** XMPPHP_Log */
-require_once dirname(__FILE__) . '/Log.php';
+       /**
+        * XMPPHP: The PHP XMPP Library
+        * Copyright (C) 2008  Nathanael C. Fritz
+        * This file is part of SleekXMPP.
+        *
+        * XMPPHP 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 2 of the License, or
+        * (at your option) any later version.
+        *
+        * XMPPHP 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 XMPPHP; if not, write to the Free Software
+        * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+        *
+        * @category   xmpphp
+        * @package    XMPPHP
+        * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+        * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+        * @author     Michael Garvin <JID: gar@netflint.net>
+        * @author     Alexander Birkner (https://github.com/BirknerAlex)
+        * @copyright  2008 Nathanael C. Fritz
+        */
 
 /**
- * XMPPHP XML Stream
- * 
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
+ * XMPPHP Main Class
+ *
+ * @category   xmpphp
+ * @package    XMPPHP
+ * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author     Michael Garvin <JID: gar@netflint.net>
  * @copyright  2008 Nathanael C. Fritz
- * @version    $Id$
+ * @version    $Id$
  */
-class XMPPHP_XMLStream {
+class XMLStream {
        /**
         * @var resource
         */
@@ -82,7 +77,7 @@ class XMPPHP_XMLStream {
        /**
         * @var boolean
         */
-       protected $disconnected = false;
+       protected $disconnected = true;
        /**
         * @var boolean
         */
@@ -124,13 +119,13 @@ class XMPPHP_XMLStream {
         */
        protected $default_ns;
        /**
-        * @var string
+        * @var string[]
         */
-       protected $until = [];
+       protected $until = array();
        /**
-        * @var string
+        * @var int[]
         */
-       protected $until_count = [];
+       protected $until_count = array();
        /**
         * @var array
         */
@@ -140,7 +135,7 @@ class XMPPHP_XMLStream {
         */
        protected $until_payload = array();
        /**
-        * @var XMPPHP_Log
+        * @var Log
         */
        protected $log;
        /**
@@ -183,7 +178,7 @@ class XMPPHP_XMLStream {
                $this->host = $host;
                $this->port = $port;
                $this->setupParser();
-               $this->log = new XMPPHP_Log($printlog, $loglevel);
+               $this->log = new Log($printlog, $loglevel);
        }
 
        /**
@@ -199,7 +194,7 @@ class XMPPHP_XMLStream {
        /**
         * Return the log instance
         *
-        * @return XMPPHP_Log
+        * @return Log
         */
        public function getLog() {
                return $this->log;
@@ -263,7 +258,7 @@ class XMPPHP_XMLStream {
                        $ns_tags = array($xpath);
                }
                foreach($ns_tags as $ns_tag) {
-                       list($l, $r) = explode("}", $ns_tag);
+                       list($l, $r) = explode('}', $ns_tag);
                        if ($r != null) {
                                $xpart = array(substr($l, 1), $r);
                        } else {
@@ -288,9 +283,12 @@ class XMPPHP_XMLStream {
        /**
         * Connect to XMPP Host
         *
-        * @param integer $timeout
+        * @param integer $timeout    Timeout in seconds
         * @param boolean $persistent
-        * @param boolean $sendinit
+        * @param boolean $sendinit   Send XMPP starting sequence after connect
+        *                            automatically
+        *
+        * @throws Exception When the connection fails
         */
        public function connect($timeout = 30, $persistent = false, $sendinit = true) {
                $this->sent_disconnect = false;
@@ -310,10 +308,10 @@ class XMPPHP_XMLStream {
                        try {
                                $this->socket = @stream_socket_client("$conntype://{$this->host}:{$this->port}", $errno, $errstr, $timeout, $conflag);
                        } catch (Exception $e) {
-                               throw new XMPPHP_Exception($e->getMessage());
+                               throw new Exception($e->getMessage());
                        }
                        if(!$this->socket) {
-                               $this->log->log("Could not connect.",  XMPPHP_Log::LEVEL_ERROR);
+                               $this->log->log("Could not connect.",  Log::LEVEL_ERROR);
                                $this->disconnected = true;
                                # Take it easy for a few seconds
                                sleep(min($timeout, 5));
@@ -324,16 +322,20 @@ class XMPPHP_XMLStream {
                        stream_set_blocking($this->socket, 1);
                        if($sendinit) $this->send($this->stream_start);
                } else {
-                       throw new XMPPHP_Exception("Could not connect before timeout.");
+                       throw new Exception("Could not connect before timeout.");
                }
        }
 
        /**
         * Reconnect XMPP Host
+        *
+        * @throws Exception When the connection fails
+        * @uses   $reconnectTimeout
+        * @see    setReconnectTimeout()
         */
        public function doReconnect() {
                if(!$this->is_server) {
-                       $this->log->log("Reconnecting ($this->reconnectTimeout)...",  XMPPHP_Log::LEVEL_WARNING);
+                       $this->log->log("Reconnecting ($this->reconnectTimeout)...",  Log::LEVEL_WARNING);
                        $this->connect($this->reconnectTimeout, false, false);
                        $this->reset();
                        $this->event('reconnect');
@@ -348,7 +350,7 @@ class XMPPHP_XMLStream {
         * Disconnect from XMPP Host
         */
        public function disconnect() {
-               $this->log->log("Disconnecting...",  XMPPHP_Log::LEVEL_VERBOSE);
+               $this->log->log("Disconnecting...",  Log::LEVEL_VERBOSE);
                if(false == (bool) $this->socket) {
                        return;
                }
@@ -368,15 +370,58 @@ class XMPPHP_XMLStream {
                return $this->disconnected;
        }
 
+       /**
+        * Checks if the given string is closed with the same tag as it is
+        * opened. We try to be as fast as possible here.
+        *
+        * @param string $buff Read buffer of __process()
+        *
+        * @return boolean true if the buffer seems to be complete
+        */
+       protected function bufferComplete($buff)
+       {
+               if (substr($buff, -1) != '>') {
+                       return false;
+               }
+               //we always have a space since the namespace needs to be
+               //declared. could be a tab, though
+               $start = substr(
+                       $buff, 1,
+                       min(strpos($buff, '>', 2), strpos($buff, ' ', 2)) - 1
+               );
+               $stop  = substr($buff, -strlen($start) - 3);
+
+               if ($start == '?xml') {
+                       //starting with an xml tag. this means a stream is being
+                       // opened, which is not much of data, so no fear it's
+                       // not complete
+                       return true;
+               }
+               if (substr($stop, -2) == '/>') {
+                       //one tag, i.e. <success />
+                       return true;
+               }
+               if ('</' . $start . '>' == $stop) {
+                       return true;
+               }
+
+               return false;
+       }
+
        /**
         * Core reading tool
-        * 0 -> only read if data is immediately ready
-        * NULL -> wait forever and ever
-        * integer -> process for this amount of time 
+        *
+        * @param mixed   $maximum Limit when to return
+        *                         - 0: only read if data is immediately ready
+        *                         - NULL: wait forever and ever
+        *                         - integer: process for this amount of microseconds
+        * @param boolean $return_when_received Immediately return when data have been
+        *                                      received
+        *
+        * @return boolean True when all goes well, false when something fails
         */
-       
-       private function __process($maximum=5) {
-               
+       private function __process($maximum = 5, $return_when_received = false)
+       {
                $remaining = $maximum;
                
                do {
@@ -396,7 +441,7 @@ class XMPPHP_XMLStream {
                        }
                        $updated = @stream_select($read, $write, $except, $secs, $usecs);
                        if ($updated === false) {
-                               $this->log->log("Error on stream_select()",  XMPPHP_Log::LEVEL_VERBOSE);                                
+                               $this->log->log("Error on stream_select()",  Log::LEVEL_VERBOSE);
                                if ($this->reconnect) {
                                        $this->doReconnect();
                                } else {
@@ -405,19 +450,34 @@ class XMPPHP_XMLStream {
                                        return false;
                                }
                        } else if ($updated > 0) {
-                               # XXX: Is this big enough?
-                               $buff = @fread($this->socket, 4096);
-                               if(!$buff) { 
-                                       if($this->reconnect) {
-                                               $this->doReconnect();
-                                       } else {
-                                               fclose($this->socket);
-                                               $this->socket = NULL;
-                                               return false;
+                               $buff = '';
+                               do {
+                                       if ($buff != '') {
+                                               //disable blocking for now because fread() will
+                                               // block until the 4k are full if we already
+                                               // read a part of the packet
+                                               stream_set_blocking($this->socket, 0);
                                        }
-                               }
-                               $this->log->log("RECV: $buff",  XMPPHP_Log::LEVEL_VERBOSE);
+                                       $part = fread($this->socket, 4096);
+                                       stream_set_blocking($this->socket, 1);
+
+                                       if (!$part && feof($this->socket)) {
+                                               if($this->reconnect) {
+                                                       $this->doReconnect();
+                                               } else {
+                                                       fclose($this->socket);
+                                                       $this->socket = NULL;
+                                                       return false;
+                                               }
+                                       }
+                                       $this->log->log("RECV: $part",  Log::LEVEL_VERBOSE);
+                                       $buff .= $part;
+                               } while (!$this->bufferComplete($buff));
+
                                xml_parse($this->parser, $buff, false);
+                               if ($return_when_received) {
+                                       return true;
+                               }
                        } else {
                                # $updated == 0 means no changes during timeout.
                        }
@@ -440,8 +500,11 @@ class XMPPHP_XMLStream {
        /**
         * Process until a timeout occurs
         *
-        * @param integer $timeout
+        * @param integer $timeout Time in seconds
+        *
         * @return string
+        *
+        * @see __process()
         */
        public function processTime($timeout=NULL) {
                if (is_null($timeout)) {
@@ -454,23 +517,43 @@ class XMPPHP_XMLStream {
        /**
         * Process until a specified event or a timeout occurs
         *
-        * @param string|array $event
-        * @param integer $timeout
-        * @return string
+        * @param string|array $event   Event name or array of event names
+        * @param integer      $timeout Timeout in seconds
+        *
+        * @return array Payload
         */
-       public function processUntil($event, $timeout=-1) {
+       public function processUntil($event, $timeout = -1)
+       {
+               if ($this->disconnected) {
+                       throw new Exception('You need to connect first');
+               }
+
                $start = time();
-               if(!is_array($event)) $event = array($event);
+               if (!is_array($event)) {
+                       $event = array($event);
+               }
+
                $this->until[] = $event;
                end($this->until);
                $event_key = key($this->until);
                reset($this->until);
+
                $this->until_count[$event_key] = 0;
                $updated = '';
-               while(!$this->disconnected and $this->until_count[$event_key] < 1 and (time() - $start < $timeout or $timeout == -1)) {
-                       $this->__process();
+               while (!$this->disconnected
+                       && $this->until_count[$event_key] < 1
+                       && ($timeout == -1 || time() - $start < $timeout)
+               ) {
+                       $maximum = $timeout == -1
+                               ? NULL
+                               : ($timeout - (time() - $start)) * 1000000;
+                       $ret = $this->__process($maximum, true);
+                       if (!$ret) {
+                               break;
+                       }
                }
-               if(array_key_exists($event_key, $this->until_payload)) {
+
+               if (array_key_exists($event_key, $this->until_payload)) {
                        $payload = $this->until_payload[$event_key];
                        unset($this->until_payload[$event_key]);
                        unset($this->until_count[$event_key]);
@@ -478,6 +561,7 @@ class XMPPHP_XMLStream {
                } else {
                        $payload = array();
                }
+
                return $payload;
        }
 
@@ -522,7 +606,7 @@ class XMPPHP_XMLStream {
                        $ns = $this->ns_map[$name[0]];
                        $name = $name[1];
                }
-               $obj = new XMPPHP_XMLObj($name, $ns, $attr);
+               $obj = new XMLObj($name, $ns, $attr);
                if($this->xml_depth > 1) {
                        $this->xmlobj[$this->xml_depth - 1]->subs[] = $obj;
                }
@@ -538,7 +622,7 @@ class XMPPHP_XMLStream {
         * @param string   $name
         */
        public function endXML($parser, $name) {
-               #$this->log->log("Ending $name",  XMPPHP_Log::LEVEL_DEBUG);
+               #$this->log->log("Ending $name",  Log::LEVEL_DEBUG);
                #print "$name\n";
                if($this->been_reset) {
                        $this->been_reset = false;
@@ -563,7 +647,7 @@ class XMPPHP_XMLStream {
                                                }
                                                if ($searchxml !== null) {
                                                        if($handler[2] === null) $handler[2] = $this;
-                                                       $this->log->log("Calling {$handler[1]}",  XMPPHP_Log::LEVEL_DEBUG);
+                                                       $this->log->log("Calling {$handler[1]}",  Log::LEVEL_DEBUG);
                                                        $handler[2]->{$handler[1]}($this->xmlobj[2]);
                                                }
                                        }
@@ -577,7 +661,7 @@ class XMPPHP_XMLStream {
                                }
                                if($searchxml !== null and $searchxml->name == $handler[0] and ($searchxml->ns == $handler[1] or (!$handler[1] and $searchxml->ns == $this->default_ns))) {
                                        if($handler[3] === null) $handler[3] = $this;
-                                       $this->log->log("Calling {$handler[2]}",  XMPPHP_Log::LEVEL_DEBUG);
+                                       $this->log->log("Calling {$handler[2]}",  Log::LEVEL_DEBUG);
                                        $handler[3]->{$handler[2]}($this->xmlobj[2]);
                                }
                        }
@@ -592,7 +676,7 @@ class XMPPHP_XMLStream {
                        }
                        if(is_array($this->xmlobj)) {
                                $this->xmlobj = array_slice($this->xmlobj, 0, 1);
-                               if(isset($this->xmlobj[0]) && $this->xmlobj[0] instanceof XMPPHP_XMLObj) {
+                               if(isset($this->xmlobj[0]) && $this->xmlobj[0] instanceof XMLObj) {
                                        $this->xmlobj[0]->subs = null;
                                }
                        }
@@ -634,7 +718,7 @@ class XMPPHP_XMLStream {
         * @param string $payload
         */
        public function event($name, $payload = null) {
-               $this->log->log("EVENT: $name",  XMPPHP_Log::LEVEL_DEBUG);
+               $this->log->log("EVENT: $name",  Log::LEVEL_DEBUG);
                foreach($this->eventhandlers as $handler) {
                        if($name == $handler[0]) {
                                if($handler[2] === null) {
@@ -643,6 +727,7 @@ class XMPPHP_XMLStream {
                                $handler[2]->{$handler[1]}($payload);
                        }
                }
+
                foreach($this->until as $key => $until) {
                        if(is_array($until)) {
                                if(in_array($name, $until)) {
@@ -670,7 +755,7 @@ class XMPPHP_XMLStream {
                                return false;
                        }
                }
-               $this->log->log("RECV: $buff",  XMPPHP_Log::LEVEL_VERBOSE);
+               $this->log->log("RECV: $buff",  Log::LEVEL_VERBOSE);
                xml_parse($this->parser, $buff, false);
        }
 
@@ -705,20 +790,20 @@ class XMPPHP_XMLStream {
                        # TODO: retry send here
                        return false;
                } elseif ($select > 0) {
-                       $this->log->log("Socket is ready; send it.", XMPPHP_Log::LEVEL_VERBOSE);
+                       $this->log->log("Socket is ready; send it.", Log::LEVEL_VERBOSE);
                } else {
-                       $this->log->log("Socket is not ready; break.", XMPPHP_Log::LEVEL_ERROR);
+                       $this->log->log("Socket is not ready; break.", Log::LEVEL_ERROR);
                        return false;
                }
                
                $sentbytes = @fwrite($this->socket, $msg);
-               $this->log->log("SENT: " . mb_substr($msg, 0, $sentbytes, '8bit'), XMPPHP_Log::LEVEL_VERBOSE);
+               $this->log->log("SENT: " . mb_substr($msg, 0, $sentbytes, '8bit'), Log::LEVEL_VERBOSE);
                if($sentbytes === FALSE) {
-                       $this->log->log("ERROR sending message; reconnecting.", XMPPHP_Log::LEVEL_ERROR);
+                       $this->log->log("ERROR sending message; reconnecting.", Log::LEVEL_ERROR);
                        $this->doReconnect();
                        return false;
                }
-               $this->log->log("Successfully sent $sentbytes bytes.", XMPPHP_Log::LEVEL_VERBOSE);
+               $this->log->log("Successfully sent $sentbytes bytes.", Log::LEVEL_VERBOSE);
                return $sentbytes;
        }
 
index c0f8963..9714529 100644 (file)
@@ -1,47 +1,47 @@
 <?php
-/**
- * XMPPHP: The PHP XMPP Library
- * Copyright (C) 2008  Nathanael C. Fritz
- * This file is part of SleekXMPP.
- * 
- * XMPPHP 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 2 of the License, or
- * (at your option) any later version.
- * 
- * XMPPHP 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 XMPPHP; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
- *
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
- * @copyright  2008 Nathanael C. Fritz
- */
 
-/** XMPPHP_XMLStream */
-require_once dirname(__FILE__) . "/XMLStream.php";
-require_once dirname(__FILE__) . "/Roster.php";
+namespace BirknerAlex\XMPPHP;
+
+       /**
+        * XMPPHP: The PHP XMPP Library
+        * Copyright (C) 2008  Nathanael C. Fritz
+        * This file is part of SleekXMPP.
+        *
+        * XMPPHP 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 2 of the License, or
+        * (at your option) any later version.
+        *
+        * XMPPHP 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 XMPPHP; if not, write to the Free Software
+        * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+        *
+        * @category   xmpphp
+        * @package    XMPPHP
+        * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+        * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+        * @author     Michael Garvin <JID: gar@netflint.net>
+        * @author     Alexander Birkner (https://github.com/BirknerAlex)
+        * @copyright  2008 Nathanael C. Fritz
+        */
 
 /**
  * XMPPHP Main Class
- * 
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
+ *
+ * @category   xmpphp
+ * @package    XMPPHP
+ * @author     Nathanael C. Fritz <JID: fritzy@netflint.net>
+ * @author     Stephan Wentz <JID: stephan@jabber.wentz.it>
+ * @author     Michael Garvin <JID: gar@netflint.net>
  * @copyright  2008 Nathanael C. Fritz
- * @version    $Id$
+ * @version    $Id$
  */
-class XMPPHP_XMPP extends XMPPHP_XMLStream {
+class XMPP extends XMLStream {
        /**
         * @var string
         */
@@ -117,6 +117,7 @@ class XMPPHP_XMPP extends XMPPHP_XMLStream {
                $this->password = $password;
                $this->resource = $resource;
                if(!$server) $server = $host;
+               $this->server = $server;
                $this->basejid = $this->user . '@' . $this->host;
 
                $this->roster = new Roster();
@@ -162,11 +163,14 @@ class XMPPHP_XMPP extends XMPPHP_XMLStream {
         * @param string $subject
         */
        public function message($to, $body, $type = 'chat', $subject = null, $payload = null) {
-           if(is_null($type))
-           {
+               if ($this->disconnected) {
+                       throw new Exception('You need to connect first');
+               }
+
+           if(empty($type)) {
                $type = 'chat';
            }
-           
+
                $to       = htmlspecialchars($to);
                $body   = htmlspecialchars($body);
                $subject = htmlspecialchars($subject);
@@ -187,7 +191,11 @@ class XMPPHP_XMPP extends XMPPHP_XMLStream {
         * @param string $show
         * @param string $to
         */
-       public function presence($status = null, $show = 'available', $to = null, $type='available', $priority=0) {
+       public function presence($status = null, $show = 'available', $to = null, $type='available', $priority=null) {
+               if ($this->disconnected) {
+                       throw new Exception('You need to connect first');
+               }
+
                if($type == 'available') $type = '';
                $to      = htmlspecialchars($to);
                $status = htmlspecialchars($status);
@@ -196,13 +204,13 @@ class XMPPHP_XMPP extends XMPPHP_XMLStream {
                $out = "<presence";
                if($to) $out .= " to=\"$to\"";
                if($type) $out .= " type='$type'";
-               if($show == 'available' and !$status) {
+               if($show == 'available' and !$status and $priority !== null) {
                        $out .= "/>";
                } else {
                        $out .= ">";
                        if($show != 'available') $out .= "<show>$show</show>";
                        if($status) $out .= "<status>$status</status>";
-                       if($priority) $out .= "<priority>$priority</priority>";
+                       if($priority !== null) $out .= "<priority>$priority</priority>";
                        $out .= "</presence>";
                }
                
@@ -229,10 +237,11 @@ class XMPPHP_XMPP extends XMPPHP_XMLStream {
                } else {
                        $payload['type'] = 'chat';
                }
+               $body = $xml->sub('body');
                $payload['from'] = $xml->attrs['from'];
-               $payload['body'] = $xml->sub('body')->data;
+               $payload['body'] = is_object($body) ? $body->data : FALSE; // $xml->sub('body')->data;
                $payload['xml'] = $xml;
-               $this->log->log("Message: {$xml->sub('body')->data}", XMPPHP_Log::LEVEL_DEBUG);
+               $this->log->log("Message: {$payload['body']}", Log::LEVEL_DEBUG);
                $this->event('message', $payload);
        }
 
@@ -251,7 +260,7 @@ class XMPPHP_XMPP extends XMPPHP_XMLStream {
                if($this->track_presence) {
                        $this->roster->setPresence($payload['from'], $payload['priority'], $payload['show'], $payload['status']);
                }
-               $this->log->log("Presence: {$payload['from']} [{$payload['show']}] {$payload['status']}",  XMPPHP_Log::LEVEL_DEBUG);
+               $this->log->log("Presence: {$payload['from']} [{$payload['show']}] {$payload['status']}",  Log::LEVEL_DEBUG);
                if(array_key_exists('type', $xml->attrs) and $xml->attrs['type'] == 'subscribe') {
                        if($this->auto_subscribe) {
                                $this->send("<presence type='subscribed' to='{$xml->attrs['from']}' from='{$this->fulljid}' />");
@@ -304,10 +313,10 @@ class XMPPHP_XMPP extends XMPPHP_XMLStream {
         * @param string $xml
         */
        protected function sasl_failure_handler($xml) {
-               $this->log->log("Auth failed!",  XMPPHP_Log::LEVEL_ERROR);
+               $this->log->log("Auth failed!",  Log::LEVEL_ERROR);
                $this->disconnect();
                
-               throw new XMPPHP_Exception('Auth failed!');
+               throw new Exception('Auth failed!');
        }
 
        /**
diff --git a/lib/jabber/XMPP/XMPP_Old.php b/lib/jabber/XMPP/XMPP_Old.php
deleted file mode 100644 (file)
index 43f56b1..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-/**
- * XMPPHP: The PHP XMPP Library
- * Copyright (C) 2008  Nathanael C. Fritz
- * This file is part of SleekXMPP.
- * 
- * XMPPHP 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 2 of the License, or
- * (at your option) any later version.
- * 
- * XMPPHP 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 XMPPHP; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
- *
- * @category   xmpphp 
- * @package    XMPPHP
- * @author      Nathanael C. Fritz <JID: fritzy@netflint.net>
- * @author      Stephan Wentz <JID: stephan@jabber.wentz.it>
- * @author      Michael Garvin <JID: gar@netflint.net>
- * @copyright  2008 Nathanael C. Fritz
- */
-
-/** XMPPHP_XMPP 
- *
- * This file is unnecessary unless you need to connect to older, non-XMPP-compliant servers like Dreamhost's.
- * In this case, use instead of XMPPHP_XMPP, otherwise feel free to delete it.
- * The old Jabber protocol wasn't standardized, so use at your own risk.
- *
- */
-require_once "XMPP.php";
-
-       class XMPPHP_XMPPOld extends XMPPHP_XMPP {
-               /**
-                *
-                * @var string
-                */
-               protected $session_id;
-
-               public function __construct($host, $port, $user, $password, $resource, $server = null, $printlog = false, $loglevel = null) {
-                       parent::__construct($host, $port, $user, $password, $resource, $server, $printlog, $loglevel);
-                       if(!$server) $server = $host;
-                       $this->stream_start = '<stream:stream to="' . $server . '" xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">';
-                       $this->fulljid = "{$user}@{$server}/{$resource}";
-               }
-       
-               /**
-                * Override XMLStream's startXML
-                *
-                * @param parser $parser
-                * @param string $name
-                * @param array $attr
-                */
-               public function startXML($parser, $name, $attr) {
-                       if($this->xml_depth == 0) {
-                               $this->session_id = $attr['ID'];
-                               $this->authenticate();
-                       }
-                       parent::startXML($parser, $name, $attr);
-               }
-
-               /**
-                * Send Authenticate Info Request
-                *
-                */
-               public function authenticate() {
-                       $id = $this->getId();
-                       $this->addidhandler($id, 'authfieldshandler');
-                       $this->send("<iq type='get' id='$id'><query xmlns='jabber:iq:auth'><username>{$this->user}</username></query></iq>");
-               }
-
-               /**
-                * Retrieve auth fields and send auth attempt
-                *
-                * @param XMLObj $xml
-                */
-               public function authFieldsHandler($xml) {
-                       $id = $this->getId();
-                       $this->addidhandler($id, 'oldAuthResultHandler');
-                       if($xml->sub('query')->hasSub('digest')) {
-                               $hash = sha1($this->session_id . $this->password);
-                               print "{$this->session_id} {$this->password}\n";
-                               $out = "<iq type='set' id='$id'><query xmlns='jabber:iq:auth'><username>{$this->user}</username><digest>{$hash}</digest><resource>{$this->resource}</resource></query></iq>";
-                       } else {
-                               $out = "<iq type='set' id='$id'><query xmlns='jabber:iq:auth'><username>{$this->user}</username><password>{$this->password}</password><resource>{$this->resource}</resource></query></iq>";
-                       }
-                       $this->send($out);
-
-               }
-               
-               /**
-                * Determine authenticated or failure
-                *
-                * @param XMLObj $xml
-                */
-               public function oldAuthResultHandler($xml) {
-                       if($xml->attrs['type'] != 'result') {
-                               $this->log->log("Auth failed!",  XMPPHP_Log::LEVEL_ERROR);
-                               $this->disconnect();
-                               throw new XMPPHP_Exception('Auth failed!');
-                       } else {
-                               $this->log->log("Session started");
-                               $this->event('session_start');
-                       }
-               }
-       }
-
-
-?>
index 38e0bbd..fd91dc6 100644 (file)
@@ -1,5 +1,6 @@
-Description of XMPPHP (aka jabber) version 0.1rc2-r77 library import into Moodle
+Description of XMPPHP (aka jabber) version 2.2 library import into Moodle
 
-MDL-20876 - replaced deprecated split() with explode()
-MDL-52335 - PHP7 variable syntax changes
-MDL-64223 - PHP7.1 compatibility changes
+To upgrade:
+1. Go to https://github.com/BirknerAlex/XMPPHP/releases
+2. Download the latest archive file for the release.
+3. Extract the contents of src/BirknerAlex/XMPPHP to lib/jabber/XMPP
\ No newline at end of file
index cd31d05..1326f13 100644 (file)
@@ -40,6 +40,7 @@
  *  - $CFG->tempdir  - Path to moodle's temp file directory on server's filesystem.
  *  - $CFG->cachedir - Path to moodle's cache directory on server's filesystem (shared by cluster nodes).
  *  - $CFG->localcachedir - Path to moodle's local cache directory (not shared by cluster nodes).
+ *  - $CFG->localrequestdir - Path to moodle's local temp request directory (not shared by cluster nodes).
  *
  * @global object $CFG
  * @name $CFG
@@ -208,6 +209,11 @@ if (!isset($CFG->localcachedir)) {
     $CFG->localcachedir = "$CFG->dataroot/localcache";
 }
 
+// Allow overriding of localrequestdir.
+if (!isset($CFG->localrequestdir)) {
+    $CFG->localrequestdir = sys_get_temp_dir() . '/requestdir';
+}
+
 // Location of all languages except core English pack.
 if (!isset($CFG->langotherroot)) {
     $CFG->langotherroot = $CFG->dataroot.'/lang';
index 2133479..b28c58a 100644 (file)
@@ -1635,14 +1635,11 @@ function get_request_storage_directory($exceptiononerror = true, bool $forcecrea
     $createnewdirectory = $forcecreate || !$writabledirectoryexists;
 
     if ($createnewdirectory) {
-        if ($CFG->localcachedir !== "$CFG->dataroot/localcache") {
-            check_dir_exists($CFG->localcachedir, true, true);
-            protect_directory($CFG->localcachedir);
-        } else {
-            protect_directory($CFG->dataroot);
-        }
+        $basedir = "{$CFG->localrequestdir}/{$CFG->siteidentifier}";
+        make_writable_directory($basedir);
+        protect_directory($basedir);
 
-        if ($dir = make_unique_writable_directory($CFG->localcachedir, $exceptiononerror)) {
+        if ($dir = make_unique_writable_directory($basedir, $exceptiononerror)) {
             // Register a shutdown handler to remove the directory.
             \core_shutdown_manager::register_function('remove_dir', [$dir]);
         }
index bed8fc7..469f6ca 100644 (file)
@@ -204,6 +204,8 @@ class core_setuplib_testcase extends advanced_testcase {
     }
 
     public function test_get_request_storage_directory() {
+        $this->resetAfterTest(true);
+
         // Making a call to get_request_storage_directory should always give the same result.
         $firstdir = get_request_storage_directory();
         $seconddir = get_request_storage_directory();
@@ -230,6 +232,11 @@ class core_setuplib_testcase extends advanced_testcase {
         $fourthdir = get_request_storage_directory();
         $this->assertTrue(is_dir($fourthdir));
         $this->assertNotEquals($thirddir, $fourthdir);
+
+        $now = $this->setCurrentTimeStart();
+        set_config('localcachedirpurged', $now - 2);
+        purge_all_caches();
+        $this->assertTrue(is_dir($fourthdir));
     }
 
 
index 2f2733e..356cc82 100644 (file)
@@ -53,7 +53,7 @@
     <location>jabber</location>
     <name>XMPPHP</name>
     <license>GPL</license>
-    <version>0.1rc2-r77</version>
+    <version>2.2</version>
     <licenseversion>2.0+</licenseversion>
   </library>
   <library>
index c5fe41f..2d2536e 100644 (file)
@@ -52,6 +52,10 @@ information provided here is intended especially for developers.
   renamed to `is_listed()` and `get_not_listed()` respectively.
 * Method `mustache_helper_collection::strip_blacklisted_helpers()` has been deprecated and renamed to
   `strip_disallowed_helpers()`.
+* A new admin externalpage type `\core_admin\local\externalpage\accesscallback` for use in plugin settings is available that allows
+  a callback to be provided to determine whether page can be accessed.
+* New setting $CFG->localtempdir overrides which defaults to sys_get_temp_dir()
+* Function redirect() now emits a line of backtrace into the X-Redirect-By header when debugging is on
 
 === 3.9 ===
 * Following function has been deprecated, please use \core\task\manager::run_from_cli().
index 29ef0e7..4251846 100644 (file)
@@ -2954,9 +2954,17 @@ function redirect($url, $message='', $delay=null, $messagetype = \core\output\no
     \core\session\manager::write_close();
 
     if ($delay == 0 && !$debugdisableredirect && !headers_sent()) {
+
         // This helps when debugging redirect issues like loops and it is not clear
-        // which layer in the stack sent the redirect header.
-        @header('X-Redirect-By: Moodle');
+        // which layer in the stack sent the redirect header. If debugging is on
+        // then the file and line is also shown.
+        $redirectby = 'Moodle';
+        if (debugging('', DEBUG_DEVELOPER)) {
+            $origin = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
+            $redirectby .= ' /' . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
+        }
+        @header("X-Redirect-By: $redirectby");
+
         // 302 might not work for POST requests, 303 is ignored by obsolete clients.
         @header($_SERVER['SERVER_PROTOCOL'] . ' 303 See Other');
         @header('Location: '.$url);
index 4d758b1..dd6bea6 100644 (file)
@@ -1699,6 +1699,10 @@ class api {
 
         $messageid = message_send($eventdata);
 
+        if (!$messageid) {
+            throw new \moodle_exception('messageundeliveredbynotificationsettings', 'moodle');
+        }
+
         $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
                 timecreated, fullmessagetrust');
         $message = (object) [
index 32504c6..e809173 100644 (file)
@@ -215,6 +215,9 @@ class core_message_external extends external_api {
                 //          We should have thrown exceptions as these errors prevent results to be returned.
                 // See http://docs.moodle.org/dev/Errors_handling_in_web_services#When_to_send_a_warning_on_the_server_side .
                 $resultmsg['msgid'] = -1;
+                if (!isset($errormessage)) { // Nobody has set a message error or thrown an exception, let's set it.
+                    $errormessage = get_string('messageundeliveredbynotificationsettings', 'error');
+                }
                 $resultmsg['errormessage'] = $errormessage;
             }
 
index 7556cd7..f2c2a6b 100644 (file)
@@ -23,8 +23,6 @@
  */
 
 require_once($CFG->dirroot.'/message/output/lib.php');
-require_once($CFG->libdir.'/jabber/XMPP/XMPP.php');
-
 /**
  * The jabber message processor
  *
@@ -76,7 +74,7 @@ class message_output_jabber extends message_output {
 
         $jabbermessage .= "\n(".get_string('noreply','message').')';
 
-        $conn = new XMPPHP_XMPP($CFG->jabberhost,$CFG->jabberport,$CFG->jabberusername,$CFG->jabberpassword,'moodle',$CFG->jabberserver);
+        $conn = new \BirknerAlex\XMPPHP\XMPP($CFG->jabberhost,$CFG->jabberport,$CFG->jabberusername,$CFG->jabberpassword,'moodle',$CFG->jabberserver);
 
         // No need to track the presence during the sending message process.
         $conn->track_presence = false;
@@ -88,7 +86,7 @@ class message_output_jabber extends message_output {
             $conn->presence();
             $conn->message($jabberaddress, $jabbermessage);
             $conn->disconnect();
-        } catch(XMPPHP_Exception $e) {
+        } catch(\BirknerAlex\XMPPHP\Exception $e) {
             debugging($e->getMessage());
             return false;
         }
index 9152c48..69daf6f 100644 (file)
@@ -46,7 +46,22 @@ function xmldb_message_popup_upgrade($oldversion) {
 
     if ($oldversion < 2020020600) {
         // Clean up orphaned popup notification records.
-        $DB->delete_records_select('message_popup_notifications', 'notificationid NOT IN (SELECT id FROM {notifications})');
+        $fromsql = "FROM {message_popup_notifications} mpn
+               LEFT JOIN {notifications} n
+                      ON mpn.notificationid = n.id
+                   WHERE n.id IS NULL";
+        $total = $DB->count_records_sql("SELECT COUNT(mpn.id) " . $fromsql);
+        $i = 0;
+        $pbar = new progress_bar('deletepopupnotification', 500, true);
+        do {
+            if ($popupnotifications = $DB->get_records_sql("SELECT mpn.id " . $fromsql, null, 0, 1000)) {
+                list($insql, $inparams) = $DB->get_in_or_equal(array_keys($popupnotifications));
+                $DB->delete_records_select('message_popup_notifications', "id $insql", $inparams);
+                // Update progress.
+                $i += count($inparams);
+                $pbar->update($i, $total, "Cleaning up orphaned popup notification records - $i/$total.");
+            }
+        } while ($popupnotifications);
 
         // Reportbuilder savepoint reached.
         upgrade_plugin_savepoint(true, 2020020600, 'message', 'popup');
index 1de792d..6419450 100644 (file)
@@ -71,9 +71,6 @@ class restore_feedback_activity_structure_step extends restore_activity_structur
         $oldid = $data->id;
         $data->feedback = $this->get_new_parentid('feedback');
 
-        //dependitem
-        $data->dependitem = $this->get_mappingid('feedback_item', $data->dependitem);
-
         $newitemid = $DB->insert_record('feedback_item', $data);
         $this->set_mapping('feedback_item', $oldid, $newitemid, true); // Can have files
     }
@@ -117,9 +114,19 @@ class restore_feedback_activity_structure_step extends restore_activity_structur
     }
 
     protected function after_execute() {
+        global $DB;
         // Add feedback related files, no need to match by itemname (just internally handled context)
         $this->add_related_files('mod_feedback', 'intro', null);
         $this->add_related_files('mod_feedback', 'page_after_submit', null);
         $this->add_related_files('mod_feedback', 'item', 'feedback_item');
+
+        // Once all items are restored we can set their dependency.
+        if ($records = $DB->get_records('feedback_item', array('feedback' => $this->task->get_activityid()))) {
+            foreach ($records as $record) {
+                // Get new id for dependitem if present. This will also reset dependitem if not found.
+                $record->dependitem = $this->get_mappingid('feedback_item', $record->dependitem);
+                $DB->update_record('feedback_item', $record);
+            }
+        }
     }
 }
index 9642bfa..38aeb7d 100644 (file)
@@ -61,4 +61,37 @@ class mod_feedback_restore_date_testcase extends restore_date_testcase {
         $this->assertFieldsRolledForward($feedback, $newfeedback, $props);
         $this->assertEquals($response->timemodified, $newresponse->timemodified);
     }
+
+    /**
+     * Test that dependency for items is restored correctly.
+     */
+    public function test_restore_item_dependency() {
+        global $DB;
+        // Create a course and a feedback activity.
+        $course = $this->getDataGenerator()->create_course();
+        $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course));
+        $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
+
+        // Create a couple of items which depend on each other.
+        $item1 = $feedbackgenerator->create_item_numeric($feedback);
+        $item2 = $feedbackgenerator->create_item_numeric($feedback, array('dependitem' => $item1->id));
+        $DB->set_field('feedback_item', 'dependitem', $item2->id, ['id' => $item1->id]);
+
+        // Create one more item with fake/broken dependitem.
+        $item3 = $feedbackgenerator->create_item_numeric($feedback, array('dependitem' => 123456));
+
+        // Backup and restore the course.
+        $restoredcourseid = $this->backup_and_restore($course);
+        $restoredfeedback = $DB->get_record('feedback', ['course' => $restoredcourseid]);
+
+        // Restored item1 and item2 are expected to be dependent the same way as the original ones.
+        $restoreditem1 = $DB->get_record('feedback_item', ['feedback' => $restoredfeedback->id, 'name' => $item1->name]);
+        $restoreditem2 = $DB->get_record('feedback_item', ['feedback' => $restoredfeedback->id, 'name' => $item2->name]);
+        $this->assertEquals($restoreditem2->id, $restoreditem1->dependitem);
+        $this->assertEquals($restoreditem1->id, $restoreditem2->dependitem);
+
+        // Restored item3 is expected to have an empty dependitem.
+        $restoreditem3 = $DB->get_record('feedback_item', ['feedback' => $restoredfeedback->id, 'name' => $item3->name]);
+        $this->assertEquals(0, $restoreditem3->dependitem);
+    }
 }
index ebadb62..645ee6e 100644 (file)
Binary files a/mod/forum/amd/build/discussion_nested_v2.min.js and b/mod/forum/amd/build/discussion_nested_v2.min.js differ
index 3cab6b2..a370455 100644 (file)
Binary files a/mod/forum/amd/build/discussion_nested_v2.min.js.map and b/mod/forum/amd/build/discussion_nested_v2.min.js.map differ
index 7a3bc75..997e1c3 100644 (file)
Binary files a/mod/forum/amd/build/inpage_reply.min.js and b/mod/forum/amd/build/inpage_reply.min.js differ
index 8172888..8dd41f9 100644 (file)
Binary files a/mod/forum/amd/build/inpage_reply.min.js.map and b/mod/forum/amd/build/inpage_reply.min.js.map differ
index 12d10bb..b8bb0af 100644 (file)
Binary files a/mod/forum/amd/build/posts_list.min.js and b/mod/forum/amd/build/posts_list.min.js differ
index 0423c5e..a9833ff 100644 (file)
Binary files a/mod/forum/amd/build/posts_list.min.js.map and b/mod/forum/amd/build/posts_list.min.js.map differ
index 6c0e89c..1b8f3d2 100644 (file)
@@ -245,6 +245,21 @@ const buildShowInPageReplyFormFunction = (additionalTemplateContext) => {
             } catch (e) {
                 Notification.exception(e);
             }
+
+            // Load formchangechecker module.
+            import('core/yui')
+                .then(Y => {
+                    return new Promise(resolve => {
+                        Y.use('moodle-core-formchangechecker', Y => {
+                            resolve(Y);
+                        });
+                    });
+                })
+                .then(Y => {
+                    M.core_formchangechecker.init({formid: Y.one(postContainer[0].querySelector('form')).generateID()});
+                    return Y;
+                })
+                .catch();
         }
 
         inPageReplyCreateButton.fadeOut(ANIMATION_DURATION, () => {
index 0f2d1c9..7a3e524 100644 (file)
@@ -169,6 +169,12 @@ define([
                         submitButton.trigger(EVENTS.POST_CREATED, newid);
                         hideSubmitButtonLoadingIcon(submitButton);
                         allButtons.prop('disabled', false);
+
+                        // Tell formchangechecker we submitted the form.
+                        if (typeof M.core_formchangechecker !== 'undefined') {
+                            M.core_formchangechecker.reset_form_dirty_state();
+                        }
+
                         return currentRoot.find(Selectors.post.inpageReplyContent).hide();
                     })
                     .then(function() {
index ca92955..fe88af3 100644 (file)
@@ -30,6 +30,7 @@ define([
         'core/templates',
         'core/notification',
         'core/pending',
+        'core/yui',
         'mod_forum/selectors',
         'mod_forum/inpage_reply',
     ], function(
@@ -37,6 +38,7 @@ define([
         Templates,
         Notification,
         Pending,
+        Y,
         Selectors,
         InPageReply
     ) {
@@ -76,6 +78,13 @@ define([
                         return currentRoot.find(Selectors.post.inpageReplyContent)
                             .slideToggle(300, pending.resolve).find('textarea').focus();
                     })
+                    .then(function() {
+                        // Load formchangechecker module.
+                        Y.use('moodle-core-formchangechecker', () => {
+                            M.core_formchangechecker.init({formid: `inpage-reply-${context.postid}`});
+                        });
+                        return;
+                    })
                     .fail(Notification.exception);
             } else {
                 var form = currentRoot.find(Selectors.post.inpageReplyContent);
index 94b10b9..2956639 100644 (file)
@@ -2245,7 +2245,7 @@ class mod_forum_external extends external_api {
             $parentposts = [];
             if ($parentids) {
                 $parentposts = $postbuilder->build(
-                    $user,
+                    $USER,
                     [$forum],
                     [$discussion],
                     $postvault->get_from_ids(array_values($parentids))
@@ -2261,7 +2261,7 @@ class mod_forum_external extends external_api {
                 'timecreated' => $firstpost->get_time_created(),
                 'authorfullname' => $discussionauthor->get_full_name(),
                 'posts' => [
-                    'userposts' => $postbuilder->build($user, [$forum], [$discussion], $posts),
+                    'userposts' => $postbuilder->build($USER, [$forum], [$discussion], $posts),
                     'parentposts' => $parentposts,
                 ],
             ];
index 0ce852f..2430f27 100644 (file)
@@ -2611,6 +2611,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
      * Test get forum posts by user id.
      */
     public function test_mod_forum_get_discussion_posts_by_userid() {
+        global $DB;
         $this->resetAfterTest(true);
 
         $urlfactory = mod_forum\local\container::get_url_factory();
@@ -2722,9 +2723,20 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
 
         // Following line enrol and assign default role id to the user.
         // So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
-        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher');
         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
-
+        // Changed display period for the discussions in past.
+        $time = time();
+        $discussion = new \stdClass();
+        $discussion->id = $discussion1->id;
+        $discussion->timestart = $time - 200;
+        $discussion->timeend = $time - 100;
+        $DB->update_record('forum_discussions', $discussion);
+        $discussion = new \stdClass();
+        $discussion->id = $discussion2->id;
+        $discussion->timestart = $time - 200;
+        $discussion->timeend = $time - 100;
+        $DB->update_record('forum_discussions', $discussion);
         // Create what we expect to be returned when querying the discussion.
         $expectedposts = array(
             'discussions' => array(),
@@ -2773,34 +2785,36 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                             'view' => true,
                             'edit' => true,
                             'delete' => true,
-                            'split' => false,
+                            'split' => true,
                             'reply' => true,
                             'export' => false,
                             'controlreadstatus' => false,
-                            'canreplyprivately' => false,
+                            'canreplyprivately' => true,
                             'selfenrol' => false
                         ],
                         'urls' => [
                             'view' => $urlfactory->get_view_post_url_from_post_id(
-                                    $discussion1reply1->discussion, $discussion1reply1->id)->out(false),
+                                $discussion1reply1->discussion, $discussion1reply1->id)->out(false),
                             'viewisolated' => $isolatedurluser->out(false),
                             'viewparent' => $urlfactory->get_view_post_url_from_post_id(
-                                    $discussion1reply1->discussion, $discussion1reply1->parent)->out(false),
+                                $discussion1reply1->discussion, $discussion1reply1->parent)->out(false),
                             'edit' => (new moodle_url('/mod/forum/post.php', [
-                                    'edit' => $discussion1reply1->id
+                                'edit' => $discussion1reply1->id
                             ]))->out(false),
                             'delete' => (new moodle_url('/mod/forum/post.php', [
-                                    'delete' => $discussion1reply1->id
+                                'delete' => $discussion1reply1->id
+                            ]))->out(false),
+                            'split' => (new moodle_url('/mod/forum/post.php', [
+                                'prune' => $discussion1reply1->id
                             ]))->out(false),
-                            'split' => null,
                             'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
-                                    'reply' => $discussion1reply1->id
+                                'reply' => $discussion1reply1->id
                             ]))->out(false),
                             'export' => null,
                             'markasread' => null,
                             'markasunread' => null,
                             'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id(
-                                    $discussion1reply1->discussion)->out(false),
+                                $discussion1reply1->discussion)->out(false),
                         ],
                     ]
                 ],
@@ -2833,13 +2847,13 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                         'charcount' => null,
                         'capabilities' => [
                             'view' => true,
-                            'edit' => false,
-                            'delete' => false,
+                            'edit' => true,
+                            'delete' => true,
                             'split' => false,
                             'reply' => true,
                             'export' => false,
                             'controlreadstatus' => false,
-                            'canreplyprivately' => false,
+                            'canreplyprivately' => true,
                             'selfenrol' => false
                         ],
                         'urls' => [
@@ -2847,8 +2861,12 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                                 $discussion1firstpostobject->discussion, $discussion1firstpostobject->id)->out(false),
                             'viewisolated' => $isolatedurlparent->out(false),
                             'viewparent' => null,
-                            'edit' => null,
-                            'delete' => null,
+                            'edit' => (new moodle_url('/mod/forum/post.php', [
+                                'edit' => $discussion1firstpostobject->id
+                            ]))->out(false),
+                            'delete' => (new moodle_url('/mod/forum/post.php', [
+                                'delete' => $discussion1firstpostobject->id
+                            ]))->out(false),
                             'split' => null,
                             'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
                                 'reply' => $discussion1firstpostobject->id
@@ -2906,11 +2924,11 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                             'view' => true,
                             'edit' => true,
                             'delete' => true,
-                            'split' => false,
+                            'split' => true,
                             'reply' => true,
                             'export' => false,
                             'controlreadstatus' => false,
-                            'canreplyprivately' => false,
+                            'canreplyprivately' => true,
                             'selfenrol' => false
                         ],
                         'urls' => [
@@ -2925,7 +2943,9 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                             'delete' => (new moodle_url('/mod/forum/post.php', [
                                 'delete' => $discussion2reply1->id
                             ]))->out(false),
-                            'split' => null,
+                            'split' => (new moodle_url('/mod/forum/post.php', [
+                                'prune' => $discussion2reply1->id
+                            ]))->out(false),
                             'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
                                 'reply' => $discussion2reply1->id
                             ]))->out(false),
@@ -2966,13 +2986,13 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                         'charcount' => null,
                         'capabilities' => [
                             'view' => true,
-                            'edit' => false,
-                            'delete' => false,
+                            'edit' => true,
+                            'delete' => true,
                             'split' => false,
                             'reply' => true,
                             'export' => false,
                             'controlreadstatus' => false,
-                            'canreplyprivately' => false,
+                            'canreplyprivately' => true,
                             'selfenrol' => false
                         ],
                         'urls' => [
@@ -2980,8 +3000,12 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                                 $discussion2firstpostobject->discussion, $discussion2firstpostobject->id)->out(false),
                             'viewisolated' => $isolatedurlparent->out(false),
                             'viewparent' => null,
-                            'edit' => null,
-                            'delete' => null,
+                            'edit' => (new moodle_url('/mod/forum/post.php', [
+                                'edit' => $discussion2firstpostobject->id
+                            ]))->out(false),
+                            'delete' => (new moodle_url('/mod/forum/post.php', [
+                                'delete' => $discussion2firstpostobject->id
+                            ]))->out(false),
                             'split' => null,
                             'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
                                 'reply' => $discussion2firstpostobject->id
index dc259c0..154c1f1 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in /mod/forum/*,
 information provided here is intended especially for developers.
 
+=== 3.10 ===
+
+* Changes in external function mod_forum_external::get_discussion_posts_by_userid
+  Now returns the posts of a given user checking the current user capabilities ($USER, the user who is requesting the posts).
+  Previously, it returned the posts checking the capabilities of the user that created the posts.
+
 === 3.8 ===
 
 * The following functions have been finally deprecated and can not be used anymore:
index a06d115..98acf59 100644 (file)
@@ -162,7 +162,7 @@ class mod_glossary_external extends external_api {
      * @param  int $id The glossary ID.
      * @return array Contains glossary, context, course and cm.
      */
-    protected static function validate_glossary($id) {
+    public static function validate_glossary($id) {
         global $DB;
         $glossary = $DB->get_record('glossary', array('id' => $id), '*', MUST_EXIST);
         list($course, $cm) = get_course_and_cm_from_instance($glossary, 'glossary');
@@ -1397,7 +1397,7 @@ class mod_glossary_external extends external_api {
 
         // Get and validate the glossary.
         $entry = $DB->get_record('glossary_entries', array('id' => $id), '*', MUST_EXIST);
-        list($glossary, $context) = self::validate_glossary($entry->glossaryid);
+        list($glossary, $context, $course, $cm) = self::validate_glossary($entry->glossaryid);
 
         if (empty($entry->approved) && $entry->userid != $USER->id && !has_capability('mod/glossary:approve', $context)) {
             throw new invalid_parameter_exception('invalidentry');
@@ -1406,10 +1406,17 @@ class mod_glossary_external extends external_api {
         $entry = glossary_get_entry_by_id($id);
         self::fill_entry_details($entry, $context);
 
+        // Permissions (for entry edition).
+        $permissions = [
+            'candelete' => mod_glossary_can_delete_entry($entry, $glossary, $context),
+            'canupdate' => mod_glossary_can_update_entry($entry, $glossary, $context, $cm),
+        ];
+
         return array(
             'entry' => $entry,
             'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry',
                 array($entry)),
+            'permissions' => $permissions,
             'warnings' => $warnings
         );
     }
@@ -1424,6 +1431,13 @@ class mod_glossary_external extends external_api {
         return new external_single_structure(array(
             'entry' => self::get_entry_return_structure(),
             'ratinginfo' => \core_rating\external\util::external_ratings_structure(),
+            'permissions' => new external_single_structure(
+                [
+                    'candelete' => new external_value(PARAM_BOOL, 'Whether the user can delete the entry.'),
+                    'canupdate' => new external_value(PARAM_BOOL, 'Whether the user can update the entry.'),
+                ],
+                'User permissions for the managing the entry.', VALUE_OPTIONAL
+            ),
             'warnings' => new external_warnings()
         ));
     }
diff --git a/mod/glossary/classes/external/delete_entry.php b/mod/glossary/classes/external/delete_entry.php
new file mode 100644 (file)
index 0000000..7551c43
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is the external method for deleting a content.
+ *
+ * @package    mod_glossary
+ * @since      Moodle 3.10
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_glossary\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->dirroot . '/mod/glossary/lib.php');
+
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+
+/**
+ * This is the external method for deleting a content.
+ *
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class delete_entry extends external_api {
+    /**
+     * Parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function execute_parameters(): external_function_parameters {
+        return new external_function_parameters([
+            'entryid' => new external_value(PARAM_INT, 'Glossary entry id to delete'),
+        ]);
+    }
+
+    /**
+     * Delete the indicated entry from the glossary.
+     *
+     * @param  int $entryid The entry to delete
+     * @return array with result and warnings
+     * @throws moodle_exception
+     */
+    public static function execute(int $entryid): array {
+        global $DB;
+
+        $params = self::validate_parameters(self::execute_parameters(), compact('entryid'));
+        $id = $params['entryid'];
+
+        // Get and validate the glossary.
+        $entry = $DB->get_record('glossary_entries', ['id' => $id], '*', MUST_EXIST);
+        list($glossary, $context, $course, $cm) = \mod_glossary_external::validate_glossary($entry->glossaryid);
+
+        // Check and delete.
+        mod_glossary_can_delete_entry($entry, $glossary, $context, false);
+        mod_glossary_delete_entry($entry, $glossary, $cm, $context, $course);
+
+        return [
+            'result' => true,
+            'warnings' => [],
+        ];
+    }
+
+    /**
+     * Return.
+     *
+     * @return external_single_structure
+     */
+    public static function execute_returns(): external_single_structure {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'The processing result'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+}
diff --git a/mod/glossary/classes/external/prepare_entry.php b/mod/glossary/classes/external/prepare_entry.php
new file mode 100644 (file)
index 0000000..77134f4
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is the external method for preparing a entry for edition.
+ *
+ * @package    mod_glossary
+ * @since      Moodle 3.10
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_glossary\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->dirroot . '/mod/glossary/lib.php');
+
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+
+/**
+ * This is the external method for preparing a entry for edition.
+ *
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class prepare_entry extends external_api {
+    /**
+     * Parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function execute_parameters(): external_function_parameters {
+        return new external_function_parameters([
+            'entryid' => new external_value(PARAM_INT, 'Glossary entry id to update'),
+        ]);
+    }
+
+    /**
+     * Prepare for update the indicated entry from the glossary.
+     *
+     * @param  int $entryid The entry to update
+     * @return array with result and warnings
+     * @throws moodle_exception
+     */
+    public static function execute(int $entryid): array {
+        global $DB;
+
+        $params = self::validate_parameters(self::execute_parameters(), compact('entryid'));
+        $id = $params['entryid'];
+
+        // Get and validate the glossary.
+        $entry = $DB->get_record('glossary_entries', ['id' => $id], '*', MUST_EXIST);
+        list($glossary, $context, $course, $cm) = \mod_glossary_external::validate_glossary($entry->glossaryid);
+
+        // Check permissions.
+        mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false);
+
+        list($definitionoptions, $attachmentoptions) = glossary_get_editor_and_attachment_options($course, $context, $entry);
+
+        $entry->aliases = '';
+        $entry->categories = [];
+        $entry = mod_glossary_prepare_entry_for_edition($entry);
+        $entry = file_prepare_standard_editor($entry, 'definition', $definitionoptions, $context, 'mod_glossary', 'entry',
+            $entry->id);
+        $entry = file_prepare_standard_filemanager($entry, 'attachment', $attachmentoptions, $context, 'mod_glossary', 'attachment',
+            $entry->id);
+
+        // Just get a structure compatible with external API.
+        array_walk($definitionoptions, function(&$item, $key) use (&$definitionoptions) {
+            if (!is_scalar($item)) {
+                unset($definitionoptions[$key]);
+                return;
+            }
+            $item = ['name' => $key, 'value' => $item];
+        });
+
+        array_walk($attachmentoptions, function(&$item, $key) use (&$attachmentoptions) {
+            if (!is_scalar($item)) {
+                unset($attachmentoptions[$key]);
+                return;
+            }
+            $item = ['name' => $key, 'value' => $item];
+        });
+
+        return [
+            'inlineattachmentsid' => $entry->definition_editor['itemid'],
+            'attachmentsid' => $entry->attachment_filemanager,
+            'areas' => [
+                [
+                    'area' => 'definition',
+                    'options' => $definitionoptions,
+                ],
+                [
+                    'area' => 'attachment',
+                    'options' => $attachmentoptions,
+                ],
+            ],
+            'aliases' => explode("\n", trim($entry->aliases)),
+            'categories' => $entry->categories,
+        ];
+    }
+
+    /**
+     * Return.
+     *
+     * @return external_single_structure
+     */
+    public static function execute_returns(): external_single_structure {
+        return new external_single_structure([
+            'inlineattachmentsid' => new external_value(PARAM_INT, 'Draft item id for the text editor.'),
+            'attachmentsid' => new external_value(PARAM_INT, 'Draft item id for the file manager.'),
+            'areas' => new external_multiple_structure(
+                new external_single_structure(
+                    [
+                        'area' => new external_value(PARAM_ALPHA, 'File area name.'),
+                        'options' => new external_multiple_structure(
+                            new external_single_structure(
+                                [
+                                    'name' => new external_value(PARAM_RAW, 'Name of option.'),
+                                    'value' => new external_value(PARAM_RAW, 'Value of option.'),
+                                ]
+                            ), 'Draft file area options.'
+                        )
+                    ]
+                ), 'File areas including options'
+            ),
+            'aliases' => new external_multiple_structure(new external_value(PARAM_RAW, 'Alias name.')),
+            'categories' => new external_multiple_structure(new external_value(PARAM_INT, 'Category id')),
+            'warnings' => new external_warnings(),
+        ]);
+    }
+}
diff --git a/mod/glossary/classes/external/update_entry.php b/mod/glossary/classes/external/update_entry.php
new file mode 100644 (file)
index 0000000..695fbb7
--- /dev/null
@@ -0,0 +1,176 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is the external method for updating a glossary entry.
+ *
+ * @package    mod_glossary
+ * @since      Moodle 3.10
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_glossary\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->dirroot . '/mod/glossary/lib.php');
+
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_format_value;
+use external_warnings;
+use core_text;
+use moodle_exception;
+
+/**
+ * This is the external method for updating a glossary entry.
+ *
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class update_entry extends external_api {
+    /**
+     * Parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function execute_parameters(): external_function_parameters {
+        return new external_function_parameters([
+            'entryid' => new external_value(PARAM_INT, 'Glossary entry id to update'),
+            'concept' => new external_value(PARAM_TEXT, 'Glossary concept'),
+            'definition' => new external_value(PARAM_RAW, 'Glossary concept definition'),
+            'definitionformat' => new external_format_value('definition'),
+            'options' => new external_multiple_structure (
+                new external_single_structure(
+                    [
+                        'name' => new external_value(PARAM_ALPHANUM,
+                            'The allowed keys (value format) are:
+                            inlineattachmentsid (int); the draft file area id for inline attachments
+                            attachmentsid (int); the draft file area id for attachments
+                            categories (comma separated int); comma separated category ids
+                            aliases (comma separated str); comma separated aliases
+                            usedynalink (bool); whether the entry should be automatically linked.
+                            casesensitive (bool); whether the entry is case sensitive.
+                            fullmatch (bool); whether to match whole words only.'),
+                        'value' => new external_value(PARAM_RAW, 'the value of the option (validated inside the function)')
+                    ]
+                ), 'Optional settings', VALUE_DEFAULT, []
+            )
+        ]);
+    }
+
+    /**
+     * Update the indicated glossary entry.
+     *
+     * @param  int $entryid The entry to update
+     * @param string $concept    the glossary concept
+     * @param string $definition the concept definition
+     * @param int $definitionformat the concept definition format
+     * @param array  $options    additional settings
+     * @return array with result and warnings
+     * @throws moodle_exception
+     */
+    public static function execute(int $entryid, string $concept, string $definition, int $definitionformat,
+            array $options = []): array {
+
+        global $DB;
+
+        $params = self::validate_parameters(self::execute_parameters(), compact('entryid', 'concept', 'definition',
+            'definitionformat', 'options'));
+        $id = $params['entryid'];
+
+        // Get and validate the glossary entry.
+        $entry = $DB->get_record('glossary_entries', ['id' => $id], '*', MUST_EXIST);
+        list($glossary, $context, $course, $cm) = \mod_glossary_external::validate_glossary($entry->glossaryid);
+
+        // Check if the user can update the entry.
+        mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false);
+
+        // Check for duplicates if the concept changes.
+        if (!$glossary->allowduplicatedentries &&
+                core_text::strtolower($entry->concept) != core_text::strtolower(trim($params['concept']))) {
+
+            if (glossary_concept_exists($glossary, $params['concept'])) {
+                throw new moodle_exception('errconceptalreadyexists', 'glossary');
+            }
+        }
+
+        // Prepare the entry object.
+        $entry->aliases = '';
+        $entry = mod_glossary_prepare_entry_for_edition($entry);
+        $entry->concept = $params['concept'];
+        $entry->definition_editor = [
+            'text' => $params['definition'],
+            'format' => $params['definitionformat'],
+        ];
+        // Options.
+        foreach ($params['options'] as $option) {
+            $name = trim($option['name']);
+            switch ($name) {
+                case 'inlineattachmentsid':
+                    $entry->definition_editor['itemid'] = clean_param($option['value'], PARAM_INT);
+                    break;
+                case 'attachmentsid':
+                    $entry->attachment_filemanager = clean_param($option['value'], PARAM_INT);
+                    break;
+                case 'categories':
+                    $entry->categories = clean_param($option['value'], PARAM_SEQUENCE);
+                    $entry->categories = explode(',', $entry->categories);
+                    break;
+                case 'aliases':
+                    $entry->aliases = clean_param($option['value'], PARAM_NOTAGS);
+                    // Convert to the expected format.
+                    $entry->aliases = str_replace(",", "\n", $entry->aliases);
+                    break;
+                case 'usedynalink':
+                case 'casesensitive':
+                case 'fullmatch':
+                    // Only allow if linking is enabled.
+                    if ($glossary->usedynalink) {
+                        $entry->{$name} = clean_param($option['value'], PARAM_BOOL);
+                    }
+                    break;
+                default:
+                    throw new moodle_exception('errorinvalidparam', 'webservice', '', $name);
+            }
+        }
+
+        $entry = glossary_edit_entry($entry, $course, $cm, $glossary, $context);
+
+        return [
+            'result' => true,
+            'warnings' => [],
+        ];
+    }
+
+    /**
+     * Return.
+     *
+     * @return external_single_structure
+     */
+    public static function execute_returns(): external_single_structure {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'The update result'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+}
index 942434f..1584e01 100644 (file)
@@ -162,4 +162,30 @@ $functions = array(
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
+    'mod_glossary_delete_entry' => [
+        'classname'     => 'mod_glossary\external\delete_entry',
+        'methodname'    => 'execute',
+        'classpath'     => '',
+        'description'   => 'Delete the given entry from the glossary.',
+        'type'          => 'write',
+        'services'      => [MOODLE_OFFICIAL_MOBILE_SERVICE]
+    ],
+
+    'mod_glossary_update_entry' => [
+        'classname'     => 'mod_glossary\external\update_entry',
+        'methodname'    => 'execute',
+        'classpath'     => '',
+        'description'   => 'Updates the given glossary entry.',
+        'type'          => 'write',
+        'services'      => [MOODLE_OFFICIAL_MOBILE_SERVICE]
+    ],
+
+    'mod_glossary_prepare_entry_for_edition' => [
+        'classname'     => 'mod_glossary\external\prepare_entry',
+        'methodname'    => 'execute',
+        'classpath'     => '',
+        'description'   => 'Prepares the given entry for edition returning draft item areas and file areas information.',
+        'type'          => 'read',
+        'services'      => [MOODLE_OFFICIAL_MOBILE_SERVICE]
+    ],
 );
index 624130d..ea174b7 100644 (file)
@@ -46,107 +46,23 @@ if ($cm->instance != $entry->glossaryid) {
 
 require_login($course, false, $cm);
 $context = context_module::instance($cm->id);
-$manageentries = has_capability('mod/glossary:manageentries', $context);
 
 if (! $glossary = $DB->get_record("glossary", array("id"=>$cm->instance))) {
     print_error('invalidid', 'glossary');
 }
 
-
-$strareyousuredelete = get_string("areyousuredelete","glossary");
-
-if (($entry->userid != $USER->id) and !$manageentries) { // guest id is never matched, no need for special check here
-    print_error('nopermissiontodelentry');
-}
-$ineditperiod = ((time() - $entry->timecreated <  $CFG->maxeditingtime) || $glossary->editalways);
-if (!$ineditperiod and !$manageentries) {
-    print_error('errdeltimeexpired', 'glossary');
-}
+// Throws an exception if the user cannot delete the entry.
+mod_glossary_can_delete_entry($entry, $glossary, $context, false);
 
 /// If data submitted, then process and store.
 
 if ($confirm and confirm_sesskey()) { // the operation was confirmed.
-    // if it is an imported entry, just delete the relation
-
-    $origentry = fullclone($entry);
-    if ($entry->sourceglossaryid) {
-        if (!$newcm = get_coursemodule_from_instance('glossary', $entry->sourceglossaryid)) {
-            print_error('invalidcoursemodule');
-        }
-        $newcontext = context_module::instance($newcm->id);
-
-        $entry->glossaryid       = $entry->sourceglossaryid;
-        $entry->sourceglossaryid = 0;
-        $DB->update_record('glossary_entries', $entry);
-
-        // move attachments too
-        $fs = get_file_storage();
-
-        if ($oldfiles = $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id)) {
-            foreach ($oldfiles as $oldfile) {
-                $file_record = new stdClass();
-                $file_record->contextid = $newcontext->id;
-                $fs->create_file_from_storedfile($file_record, $oldfile);
-            }
-            $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id);
-            $entry->attachment = '1';
-        } else {
-            $entry->attachment = '0';
-        }
-        $DB->update_record('glossary_entries', $entry);
-
-    } else {
-        $fs = get_file_storage();
-        $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id);
-        $DB->delete_records("comments", array('itemid'=>$entry->id, 'commentarea'=>'glossary_entry', 'contextid'=>$context->id));
-        $DB->delete_records("glossary_alias", array("entryid"=>$entry->id));
-        $DB->delete_records("glossary_entries", array("id"=>$entry->id));
-
-        // Update completion state
-        $completion = new completion_info($course);
-        if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && $glossary->completionentries) {
-            $completion->update_state($cm, COMPLETION_INCOMPLETE, $entry->userid);
-        }
-
-        //delete glossary entry ratings
-        require_once($CFG->dirroot.'/rating/lib.php');
-        $delopt = new stdClass;
-        $delopt->contextid = $context->id;
-        $delopt->component = 'mod_glossary';
-        $delopt->ratingarea = 'entry';
-        $delopt->itemid = $entry->id;
-        $rm = new rating_manager();
-        $rm->delete_ratings($delopt);
-    }
-
-    // Delete cached RSS feeds.
-    if (!empty($CFG->enablerssfeeds)) {
-        require_once($CFG->dirroot.'/mod/glossary/rsslib.php');
-        glossary_rss_delete_file($glossary);
-    }
-
-    core_tag_tag::remove_all_item_tags('mod_glossary', 'glossary_entries', $origentry->id);
-
-    $event = \mod_glossary\event\entry_deleted::create(array(
-        'context' => $context,
-        'objectid' => $origentry->id,
-        'other' => array(
-            'mode' => $prevmode,
-            'hook' => $hook,
-            'concept' => $origentry->concept
-        )
-    ));
-    $event->add_record_snapshot('glossary_entries', $origentry);
-    $event->trigger();
-
-    // Reset caches.
-    if ($entry->usedynalink and $entry->approved) {
-        \mod_glossary\local\concept_cache::reset_glossary($glossary);
-    }
 
+    mod_glossary_delete_entry($entry, $glossary, $cm, $context, $course, $hook, $prevmode);
     redirect("view.php?id=$cm->id&amp;mode=$prevmode&amp;hook=$hook");
 
 } else {        // the operation has not been confirmed yet so ask the user to do so
+    $strareyousuredelete = get_string("areyousuredelete", "glossary");
     $PAGE->navbar->add(get_string('delete'));
     $PAGE->set_title($glossary->name);
     $PAGE->set_heading($course->fullname);
index 1e5461f..15d06e5 100644 (file)
@@ -38,23 +38,10 @@ if ($id) { // if entry is specified
         print_error('invalidentry');
     }
 
-    $ineditperiod = ((time() - $entry->timecreated <  $CFG->maxeditingtime) || $glossary->editalways);
-    if (!has_capability('mod/glossary:manageentries', $context) and !($entry->userid == $USER->id and ($ineditperiod and has_capability('mod/glossary:write', $context)))) {
-        if ($USER->id != $entry->userid) {
-            print_error('errcannoteditothers', 'glossary', "view.php?id=$cm->id&amp;mode=entry&amp;hook=$id");
-        } elseif (!$ineditperiod) {
-            print_error('erredittimeexpired', 'glossary', "view.php?id=$cm->id&amp;mode=entry&amp;hook=$id");
-        }
-    }
-
-    //prepare extra data
-    if ($aliases = $DB->get_records_menu("glossary_alias", array("entryid"=>$id), '', 'id, alias')) {
-        $entry->aliases = implode("\n", $aliases) . "\n";
-    }
-    if ($categoriesarr = $DB->get_records_menu("glossary_entries_categories", array('entryid'=>$id), '', 'id, categoryid')) {
-        // TODO: this fetches cats from both main and secondary glossary :-(
-        $entry->categories = array_values($categoriesarr);
-    }
+    // Check if the user can update the entry (trigger exception if he can't).
+    mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false);
+    // Prepare extra data.
+    $entry = mod_glossary_prepare_entry_for_edition($entry);
 
 } else { // new entry
     require_capability('mod/glossary:write', $context);
index 596660c..afae7fe 100644 (file)
@@ -4318,3 +4318,197 @@ function mod_glossary_get_completion_active_rule_descriptions($cm) {
     }
     return $descriptions;
 }
+
+/**
+ * Checks if the current user can delete the given glossary entry.
+ *
+ * @since Moodle 3.10
+ * @param stdClass $entry the entry database object
+ * @param stdClass $glossary the glossary database object
+ * @param stdClass $context the glossary context
+ * @param bool $return Whether to return a boolean value or stop the execution (exception)
+ * @return bool if the user can delete the entry
+ * @throws moodle_exception
+ */
+function mod_glossary_can_delete_entry($entry, $glossary, $context, $return = true) {
+    global $USER, $CFG;
+
+    $manageentries = has_capability('mod/glossary:manageentries', $context);
+
+    if ($manageentries) {   // Users with the capability will always be able to delete entries.
+        return true;
+    }
+
+    if ($entry->userid != $USER->id) { // Guest id is never matched, no need for special check here.
+        if ($return) {
+            return false;
+        }
+        throw new moodle_exception('nopermissiontodelentry');
+    }
+
+    $ineditperiod = ((time() - $entry->timecreated < $CFG->maxeditingtime) || $glossary->editalways);
+
+    if (!$ineditperiod) {
+        if ($return) {
+            return false;
+        }
+        throw new moodle_exception('errdeltimeexpired', 'glossary');
+    }
+
+    return true;
+}
+
+/**
+ * Deletes the given entry, this function does not perform capabilities/permission checks.
+ *
+ * @since Moodle 3.10
+ * @param stdClass $entry the entry database object
+ * @param stdClass $glossary the glossary database object
+ * @param stdClass $cm the glossary course moduule object
+ * @param stdClass $context the glossary context
+ * @param stdClass $course the glossary course
+ * @param string $hook the hook, usually type of filtering, value
+ * @param string $prevmode the previsualisation mode
+ * @throws moodle_exception
+ */
+function mod_glossary_delete_entry($entry, $glossary, $cm, $context, $course, $hook = '', $prevmode = '') {
+    global $CFG, $DB;
+
+    $origentry = fullclone($entry);
+
+    // If it is an imported entry, just delete the relation.
+    if ($entry->sourceglossaryid) {
+        if (!$newcm = get_coursemodule_from_instance('glossary', $entry->sourceglossaryid)) {
+            print_error('invalidcoursemodule');
+        }
+        $newcontext = context_module::instance($newcm->id);
+
+        $entry->glossaryid       = $entry->sourceglossaryid;
+        $entry->sourceglossaryid = 0;
+        $DB->update_record('glossary_entries', $entry);
+
+        // Move attachments too.
+        $fs = get_file_storage();
+
+        if ($oldfiles = $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id)) {
+            foreach ($oldfiles as $oldfile) {
+                $filerecord = new stdClass();
+                $filerecord->contextid = $newcontext->id;
+                $fs->create_file_from_storedfile($filerecord, $oldfile);
+            }
+            $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id);
+            $entry->attachment = '1';
+        } else {
+            $entry->attachment = '0';
+        }
+        $DB->update_record('glossary_entries', $entry);
+
+    } else {
+        $fs = get_file_storage();
+        $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id);
+        $DB->delete_records("comments",
+            ['itemid' => $entry->id, 'commentarea' => 'glossary_entry', 'contextid' => $context->id]);
+        $DB->delete_records("glossary_alias", ["entryid" => $entry->id]);
+        $DB->delete_records("glossary_entries", ["id" => $entry->id]);
+
+        // Update completion state.
+        $completion = new completion_info($course);
+        if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && $glossary->completionentries) {
+            $completion->update_state($cm, COMPLETION_INCOMPLETE, $entry->userid);
+        }
+
+        // Delete glossary entry ratings.
+        require_once($CFG->dirroot.'/rating/lib.php');
+        $delopt = new stdClass;
+        $delopt->contextid = $context->id;
+        $delopt->component = 'mod_glossary';
+        $delopt->ratingarea = 'entry';
+        $delopt->itemid = $entry->id;
+        $rm = new rating_manager();
+        $rm->delete_ratings($delopt);
+    }
+
+    // Delete cached RSS feeds.
+    if (!empty($CFG->enablerssfeeds)) {
+        require_once($CFG->dirroot . '/mod/glossary/rsslib.php');
+        glossary_rss_delete_file($glossary);
+    }
+
+    core_tag_tag::remove_all_item_tags('mod_glossary', 'glossary_entries', $origentry->id);
+
+    $event = \mod_glossary\event\entry_deleted::create(
+        [
+            'context' => $context,
+            'objectid' => $origentry->id,
+            'other' => [
+                'mode' => $prevmode,
+                'hook' => $hook,
+                'concept' => $origentry->concept
+            ]
+        ]
+    );
+    $event->add_record_snapshot('glossary_entries', $origentry);
+    $event->trigger();
+
+    // Reset caches.
+    if ($entry->usedynalink and $entry->approved) {
+        \mod_glossary\local\concept_cache::reset_glossary($glossary);
+    }
+}
+
+/**
+ * Checks if the current user can update the given glossary entry.
+ *
+ * @since Moodle 3.10
+ * @param stdClass $entry the entry database object
+ * @param stdClass $glossary the glossary database object
+ * @param stdClass $context the glossary context
+ * @param object $cm the course module object (cm record or cm_info instance)
+ * @param bool $return Whether to return a boolean value or stop the execution (exception)
+ * @return bool if the user can update the entry
+ * @throws moodle_exception
+ */
+function mod_glossary_can_update_entry(stdClass $entry, stdClass $glossary, stdClass $context, object $cm,
+        bool $return = true): bool {
+
+    global $USER, $CFG;
+
+    $ineditperiod = ((time() - $entry->timecreated < $CFG->maxeditingtime) || $glossary->editalways);
+    if (!has_capability('mod/glossary:manageentries', $context) and
+            !($entry->userid == $USER->id and ($ineditperiod and has_capability('mod/glossary:write', $context)))) {
+
+        if ($USER->id != $entry->userid) {
+            if ($return) {
+                return false;
+            }
+            throw new moodle_exception('errcannoteditothers', 'glossary', "view.php?id=$cm->id&amp;mode=entry&amp;hook=$entry->id");
+        } else if (!$ineditperiod) {
+            if ($return) {
+                return false;
+            }
+            throw new moodle_exception('erredittimeexpired', 'glossary', "view.php?id=$cm->id&amp;mode=entry&amp;hook=$entry->id");
+        }
+    }
+
+    return true;
+}
+
+/**
+ * Prepares an entry for editing, adding aliases and category information.
+ *
+ * @param  stdClass $entry the entry being edited
+ * @return stdClass the entry with the additional data
+ */
+function mod_glossary_prepare_entry_for_edition(stdClass $entry): stdClass {
+    global $DB;
+
+    if ($aliases = $DB->get_records_menu("glossary_alias", ["entryid" => $entry->id], '', 'id, alias')) {
+        $entry->aliases = implode("\n", $aliases) . "\n";
+    }
+    if ($categoriesarr = $DB->get_records_menu("glossary_entries_categories", ['entryid' => $entry->id], '', 'id, categoryid')) {
+        // TODO: this fetches cats from both main and secondary glossary :-(
+        $entry->categories = array_values($categoriesarr);
+    }
+
+    return $entry;
+}
diff --git a/mod/glossary/tests/external/delete_entry.php b/mod/glossary/tests/external/delete_entry.php
new file mode 100644 (file)
index 0000000..df56e07
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * External function test for delete_entry.
+ *
+ * @package    mod_glossary
+ * @category   external
+ * @since      Moodle 3.10
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_glossary\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+use external_api;
+use externallib_advanced_testcase;
+
+/**
+ * External function test for delete_entry.
+ *
+ * @package    mod_glossary
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class delete_entry_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test the behaviour of delete_entry().
+     */
+    public function test_delete_entry() {
+        global $DB;
+        $this->resetAfterTest();
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $anotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+
+        $this->setUser($student);
+        $entry = $gg->create_content($glossary);
+
+        // Test entry creator can delete.
+        $result = delete_entry::execute($entry->id);
+        $result = external_api::clean_returnvalue(delete_entry::execute_returns(), $result);
+        $this->assertTrue($result['result']);
+        $this->assertEquals(0, $DB->count_records('glossary_entries', ['id' => $entry->id]));
+
+        // Test admin can delete.
+        $this->setAdminUser();
+        $entry = $gg->create_content($glossary);
+        $result = delete_entry::execute($entry->id);
+        $result = external_api::clean_returnvalue(delete_entry::execute_returns(), $result);
+        $this->assertTrue($result['result']);
+        $this->assertEquals(0, $DB->count_records('glossary_entries', ['id' => $entry->id]));
+
+        $entry = $gg->create_content($glossary);
+        // Test a different student is not able to delete.
+        $this->setUser($anotherstudent);
+        $this->expectExceptionMessage(get_string('nopermissiontodelentry', 'error'));
+        delete_entry::execute($entry->id);
+    }
+}
diff --git a/mod/glossary/tests/external/prepare_entry.php b/mod/glossary/tests/external/prepare_entry.php
new file mode 100644 (file)
index 0000000..b5870a5
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * External function test for prepare_entry.
+ *
+ * @package    mod_glossary
+ * @category   external
+ * @since      Moodle 3.10
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_glossary\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+use external_api;
+use externallib_advanced_testcase;
+use mod_glossary_external;
+use context_module;
+use context_user;
+use external_util;
+
+/**
+ * External function test for prepare_entry.
+ *
+ * @package    mod_glossary
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class prepare_entry_testcase extends externallib_advanced_testcase {
+
+    /**
+     * test_prepare_entry
+     */
+    public function test_prepare_entry() {
+        global $USER;
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course();
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+
+        $this->setAdminUser();
+        $aliases = ['alias1', 'alias2'];
+        $entry = $gg->create_content(
+            $glossary,
+            ['approved' => 1, 'userid' => $USER->id],
+            $aliases
+        );
+
+        $cat1 = $gg->create_category($glossary, [], [$entry]);
+        $gg->create_category($glossary);
+
+        $return = prepare_entry::execute($entry->id);
+        $return = external_api::clean_returnvalue(prepare_entry::execute_returns(), $return);
+
+        $this->assertNotEmpty($return['inlineattachmentsid']);
+        $this->assertNotEmpty($return['attachmentsid']);
+        $this->assertEquals($aliases, $return['aliases']);
+        $this->assertEquals([$cat1->id], $return['categories']);
+        $this->assertCount(2, $return['areas']);
+        $this->assertNotEmpty($return['areas'][0]['options']);
+        $this->assertNotEmpty($return['areas'][1]['options']);
+    }
+}
diff --git a/mod/glossary/tests/external/update_entry.php b/mod/glossary/tests/external/update_entry.php
new file mode 100644 (file)
index 0000000..ed6ffc5
--- /dev/null
@@ -0,0 +1,297 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * External function test for update_entry.
+ *
+ * @package    mod_glossary
+ * @category   external
+ * @since      Moodle 3.10
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_glossary\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+use external_api;
+use externallib_advanced_testcase;
+use mod_glossary_external;
+use context_module;
+use context_user;
+use external_util;
+
+/**
+ * External function test for update_entry.
+ *
+ * @package    mod_glossary
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class update_entry_testcase extends externallib_advanced_testcase {
+
+    /**
+     * test_update_entry_without_optional_settings
+     */
+    public function test_update_entry_without_optional_settings() {
+        global $CFG, $DB;
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course();
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+
+        $this->setAdminUser();
+        $concept = 'A concept';
+        $definition = '<p>A definition</p>';
+        $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML);
+        $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return);
+        $entryid = $return['entryid'];
+
+        // Updates the entry.
+        $concept .= ' Updated!';
+        $definition .= ' <p>Updated!</p>';
+        $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML);
+        $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return);
+
+        // Get entry from DB.
+        $entry = $DB->get_record('glossary_entries', ['id' => $entryid]);
+
+        $this->assertEquals($concept, $entry->concept);
+        $this->assertEquals($definition, $entry->definition);
+        $this->assertEquals($CFG->glossary_linkentries, $entry->usedynalink);
+        $this->assertEquals($CFG->glossary_casesensitive, $entry->casesensitive);
+        $this->assertEquals($CFG->glossary_fullmatch, $entry->fullmatch);
+        $this->assertEmpty($DB->get_records('glossary_alias', ['entryid' => $entryid]));
+        $this->assertEmpty($DB->get_records('glossary_entries_categories', ['entryid' => $entryid]));
+    }
+
+    /**
+     * test_update_entry_duplicated
+     */
+    public function test_update_entry_duplicated() {
+        global $CFG, $DB;
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course();
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id, 'allowduplicatedentries' => 1]);
+
+        // Create three entries.
+        $this->setAdminUser();
+        $concept = 'A concept';
+        $definition = '<p>A definition</p>';
+        mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML);
+
+        $concept = 'B concept';
+        $definition = '<p>B definition</p>';
+        mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML);
+
+        $concept = 'Another concept';
+        $definition = '<p>Another definition</p>';
+        $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML);
+        $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return);
+        $entryid = $return['entryid'];
+
+        // Updates the entry using an existing entry name when duplicateds are allowed.
+        $concept = 'A concept';
+        update_entry::execute($entryid, $concept, $definition, FORMAT_HTML);
+
+        // Updates the entry using an existing entry name when duplicateds are NOT allowed.
+        $DB->set_field('glossary', 'allowduplicatedentries', 0, ['id' => $glossary->id]);
+        $concept = 'B concept';
+        $this->expectExceptionMessage(get_string('errconceptalreadyexists', 'glossary'));
+        update_entry::execute($entryid, $concept, $definition, FORMAT_HTML);
+    }
+
+    /**
+     * test_update_entry_with_aliases
+     */
+    public function test_update_entry_with_aliases() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course();
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+
+        $this->setAdminUser();
+        $concept = 'A concept';
+        $definition = 'A definition';
+        $paramaliases = 'abc, def, gez';
+        $options = [
+            [
+                'name' => 'aliases',
+                'value' => $paramaliases,
+            ]
+        ];
+        $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options);
+        $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return);
+        $entryid = $return['entryid'];
+
+        // Updates the entry.
+        $newaliases = 'abz, xyz';
+        $options[0]['value'] = $newaliases;
+        $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML, $options);
+        $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return);
+
+        $aliases = $DB->get_records('glossary_alias', ['entryid' => $entryid]);
+        $this->assertCount(2, $aliases);
+        foreach ($aliases as $alias) {
+            $this->assertContains($alias->alias, $newaliases);
+        }
+    }
+
+    /**
+     * test_update_entry_in_categories
+     */
+    public function test_update_entry_in_categories() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course();
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $cat1 = $gg->create_category($glossary);
+        $cat2 = $gg->create_category($glossary);
+        $cat3 = $gg->create_category($glossary);
+
+        $this->setAdminUser();
+        $concept = 'A concept';
+        $definition = 'A definition';
+        $paramcategories = "$cat1->id, $cat2->id";
+        $options = [
+            [
+                'name' => 'categories',
+                'value' => $paramcategories,
+            ]
+        ];
+        $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options);
+        $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return);
+        $entryid = $return['entryid'];
+
+        // Updates the entry.
+        $newcategories = "$cat1->id, $cat3->id";
+        $options[0]['value'] = $newcategories;
+        $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML, $options);
+        $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return);
+
+        $categories = $DB->get_records('glossary_entries_categories', ['entryid' => $entryid]);
+        $this->assertCount(2, $categories);
+        foreach ($categories as $category) {
+            $this->assertContains($category->categoryid, $newcategories);
+        }
+    }
+
+    /**
+     * test_update_entry_with_attachments
+     */
+    public function test_update_entry_with_attachments() {
+        global $DB, $USER;
+        $this->resetAfterTest(true);
+
+        $course = $this->getDataGenerator()->create_course();
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+        $context = context_module::instance($glossary->cmid);
+
+        $this->setAdminUser();
+        $concept = 'A concept';
+        $definition = 'A definition';
+
+        // Draft files.
+        $draftidinlineattach = file_get_unused_draft_itemid();
+        $draftidattach = file_get_unused_draft_itemid();
+        $usercontext = context_user::instance($USER->id);
+        $filerecordinline = [
+            'contextid' => $usercontext->id,
+            'component' => 'user',
+            'filearea'  => 'draft',
+            'itemid'    => $draftidinlineattach,
+            'filepath'  => '/',
+            'filename'  => 'shouldbeanimage.png',
+        ];
+        $fs = get_file_storage();
+
+        // Create a file in a draft area for regular attachments.
+        $filerecordattach = $filerecordinline;
+        $attachfilename = 'attachment.txt';
+        $filerecordattach['filename'] = $attachfilename;
+        $filerecordattach['itemid'] = $draftidattach;
+        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
+        $fs->create_file_from_string($filerecordattach, 'simple text attachment');
+
+        $options = [
+            [
+                'name' => 'inlineattachmentsid',
+                'value' => $draftidinlineattach,
+            ],
+            [
+                'name' => 'attachmentsid',
+                'value' => $draftidattach,
+            ]
+        ];
+        $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options);
+        $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return);
+        $entryid = $return['entryid'];
+        $entry = $DB->get_record('glossary_entries', ['id' => $entryid]);
+
+        list($definitionoptions, $attachmentoptions) = glossary_get_editor_and_attachment_options($course, $context, $entry);
+
+        $entry = file_prepare_standard_editor($entry, 'definition', $definitionoptions, $context, 'mod_glossary', 'entry',
+            $entry->id);
+        $entry = file_prepare_standard_filemanager($entry, 'attachment', $attachmentoptions, $context, 'mod_glossary', 'attachment',
+            $entry->id);
+
+        $inlineattachmentsid = $entry->definition_editor['itemid'];
+        $attachmentsid = $entry->attachment_filemanager;
+
+        // Change the file areas.
+
+        // Delete one inline editor file.
+        $selectedfile = (object)[
+            'filename' => $filerecordinline['filename'],
+            'filepath' => $filerecordinline['filepath'],
+        ];
+        $return = repository_delete_selected_files($usercontext, 'user', 'draft', $inlineattachmentsid, [$selectedfile]);
+
+        // Add more files.
+        $filerecordinline['filename'] = 'newvideo.mp4';
+        $filerecordinline['itemid'] = $inlineattachmentsid;
+
+        $filerecordattach['filename'] = 'newattach.txt';
+        $filerecordattach['itemid'] = $attachmentsid;
+
+        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
+        $fs->create_file_from_string($filerecordattach, 'simple text attachment');
+
+        // Updates the entry.
+        $options[0]['value'] = $inlineattachmentsid;
+        $options[1]['value'] = $attachmentsid;
+        $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML, $options);
+        $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return);
+
+        $editorfiles = external_util::get_area_files($context->id, 'mod_glossary', 'entry', $entryid);
+        $attachmentfiles = external_util::get_area_files($context->id, 'mod_glossary', 'attachment', $entryid);
+
+        $this->assertCount(1, $editorfiles);
+        $this->assertCount(2, $attachmentfiles);
+
+        $this->assertEquals('newvideo.mp4', $editorfiles[0]['filename']);
+        $this->assertEquals('attachment.txt', $attachmentfiles[0]['filename']);
+        $this->assertEquals('newattach.txt', $attachmentfiles[1]['filename']);
+    }
+}
index 18c3a41..4f6a849 100644 (file)
@@ -1077,11 +1077,14 @@ class mod_glossary_external_testcase extends externallib_advanced_testcase {
         $c1 = $this->getDataGenerator()->create_course();
         $c2 = $this->getDataGenerator()->create_course();
         $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id));
-        $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'visible' => 0));
+        $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c2->id, 'visible' => 0));
         $u1 = $this->getDataGenerator()->create_user();
         $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
         $ctx = context_module::instance($g1->cmid);
         $this->getDataGenerator()->enrol_user($u1->id, $c1->id);
+        $this->getDataGenerator()->enrol_user($u2->id, $c1->id);
+        $this->getDataGenerator()->enrol_user($u3->id, $c1->id);
 
         $e1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'tags' => array('Cats', 'Dogs')));
         // Add a fake inline image to the entry.
@@ -1108,10 +1111,12 @@ class mod_glossary_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('Cats', $return['entry']['tags'][0]['rawname']);
         $this->assertEquals('Dogs', $return['entry']['tags'][1]['rawname']);
         $this->assertEquals($filename, $return['entry']['definitioninlinefiles'][0]['filename']);
+        $this->assertTrue($return['permissions']['candelete']);
 
         $return = mod_glossary_external::get_entry_by_id($e2->id);
         $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return);
         $this->assertEquals($e2->id, $return['entry']['id']);
+        $this->assertTrue($return['permissions']['candelete']);
 
         try {
             $return = mod_glossary_external::get_entry_by_id($e3->id);
@@ -1127,11 +1132,19 @@ class mod_glossary_external_testcase extends externallib_advanced_testcase {
             // All good.
         }
 
-        // An admin can be other's entries to be approved.
+        // An admin can see other's entries to be approved.
         $this->setAdminUser();
         $return = mod_glossary_external::get_entry_by_id($e3->id);
         $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return);
         $this->assertEquals($e3->id, $return['entry']['id']);
+        $this->assertTrue($return['permissions']['candelete']);
+
+        // Students can see other students approved entries but they will not be able to delete them.
+        $this->setUser($u3);
+        $return = mod_glossary_external::get_entry_by_id($e1->id);
+        $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return);
+        $this->assertEquals($e1->id, $return['entry']['id']);
+        $this->assertFalse($return['permissions']['candelete']);
     }
 
     public function test_add_entry_without_optional_settings() {
index 58de9ee..24b0811 100644 (file)
@@ -503,4 +503,267 @@ class mod_glossary_lib_testcase extends advanced_testcase {
         $search = glossary_get_entries_search($concept, $course->id);
         $this->assertCount(0, $search);
     }
+
+    public function test_mod_glossary_can_delete_entry_users() {
+        $this->resetAfterTest();
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $anotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $this->setUser($student);
+        $entry = $gg->create_content($glossary);
+        $context = context_module::instance($glossary->cmid);
+
+        // Test student can delete.
+        $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test teacher can delete.
+        $this->setUser($teacher);
+        $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test admin can delete.
+        $this->setAdminUser();
+        $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test a different student is not able to delete.
+        $this->setUser($anotherstudent);
+        $this->assertFalse(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test exception.
+        $this->expectExceptionMessage(get_string('nopermissiontodelentry', 'error'));
+        mod_glossary_can_delete_entry($entry, $glossary, $context, false);
+    }
+
+    public function test_mod_glossary_can_delete_entry_edit_period() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id, 'editalways' => 1]);
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $this->setUser($student);
+        $entry = $gg->create_content($glossary);
+        $context = context_module::instance($glossary->cmid);
+
+        // Test student can always delete when edit always is set to 1.
+        $entry->timecreated = time() - 2 * $CFG->maxeditingtime;
+        $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test student cannot delete old entries when edit always is set to 0.
+        $glossary->editalways = 0;
+        $this->assertFalse(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Test student can delete recent entries when edit always is set to 0.
+        $entry->timecreated = time();
+        $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context));
+
+        // Check exception.
+        $entry->timecreated = time() - 2 * $CFG->maxeditingtime;
+        $this->expectExceptionMessage(get_string('errdeltimeexpired', 'glossary'));
+        mod_glossary_can_delete_entry($entry, $glossary, $context, false);
+    }
+
+    public function test_mod_glossary_delete_entry() {
+        global $DB, $CFG;
+        $this->resetAfterTest();
+        require_once($CFG->dirroot . '/rating/lib.php');
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        $record = new stdClass();
+        $record->course = $course->id;
+        $record->assessed = RATING_AGGREGATE_AVERAGE;
+        $scale = $this->getDataGenerator()->create_scale(['scale' => 'A,B,C,D']);
+        $record->scale = "-$scale->id";
+        $glossary = $this->getDataGenerator()->create_module('glossary', $record);
+        $context = context_module::instance($glossary->cmid);
+        $cm = get_coursemodule_from_instance('glossary', $glossary->id);
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $this->setUser($student1);
+
+        // Create entry with tags and rating.
+        $entry = $gg->create_content(
+            $glossary,
+            ['approved' => 1, 'userid' => $student1->id, 'tags' => ['Cats', 'Dogs']],
+            ['alias1', 'alias2']
+        );
+
+        // Rate the entry as user2.
+        $rating1 = new stdClass();
+        $rating1->contextid = $context->id;
+        $rating1->component = 'mod_glossary';
+        $rating1->ratingarea = 'entry';
+        $rating1->itemid = $entry->id;
+        $rating1->rating = 1; // 1 is A.
+        $rating1->scaleid = "-$scale->id";
+        $rating1->userid = $student2->id;
+        $rating1->timecreated = time();
+        $rating1->timemodified = time();
+        $rating1->id = $DB->insert_record('rating', $rating1);
+
+        $sink = $this->redirectEvents();
+        mod_glossary_delete_entry(fullclone($entry), $glossary, $cm, $context, $course);
+        $events = $sink->get_events();
+        $event = array_pop($events);
+
+        // Check events.
+        $this->assertEquals('\mod_glossary\event\entry_deleted', $event->eventname);
+        $this->assertEquals($entry->id, $event->objectid);
+        $sink->close();
+
+        // No entry, no alias, no ratings, no tags.
+        $this->assertEquals(0, $DB->count_records('glossary_entries', ['id' => $entry->id]));
+        $this->assertEquals(0, $DB->count_records('glossary_alias', ['entryid' => $entry->id]));
+        $this->assertEquals(0, $DB->count_records('rating', ['component' => 'mod_glossary', 'itemid' => $entry->id]));
+        $this->assertEmpty(core_tag_tag::get_by_name(0, 'Cats'));
+    }
+
+    public function test_mod_glossary_delete_entry_imported() {
+        global $DB;
+        $this->resetAfterTest();
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        $glossary1 = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+        $glossary2 = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]);
+
+        $context = context_module::instance($glossary2->cmid);
+        $cm = get_coursemodule_from_instance('glossary', $glossary2->id);
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $this->setUser($student);
+
+        $entry1 = $gg->create_content($glossary1);
+        $entry2 = $gg->create_content(
+            $glossary2,
+            ['approved' => 1, 'userid' => $student->id, 'sourceglossaryid' => $glossary1->id, 'tags' => ['Cats', 'Dogs']]
+        );
+
+        $sink = $this->redirectEvents();
+        mod_glossary_delete_entry(fullclone($entry2), $glossary2, $cm, $context, $course);
+        $events = $sink->get_events();
+        $event = array_pop($events);
+
+        // Check events.
+        $this->assertEquals('\mod_glossary\event\entry_deleted', $event->eventname);
+        $this->assertEquals($entry2->id, $event->objectid);
+        $sink->close();
+
+        // Check source.
+        $this->assertEquals(0, $DB->get_field('glossary_entries', 'sourceglossaryid', ['id' => $entry2->id]));
+        $this->assertEquals($glossary1->id, $DB->get_field('glossary_entries', 'glossaryid', ['id' => $entry2->id]));
+
+        // Tags.
+        $this->assertEmpty(core_tag_tag::get_by_name(0, 'Cats'));
+    }
+
+    public function test_mod_glossary_can_update_entry_users() {
+        $this->resetAfterTest();
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $anotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id));
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $this->setUser($student);
+        $entry = $gg->create_content($glossary);
+        $context = context_module::instance($glossary->cmid);
+        $cm = get_coursemodule_from_instance('glossary', $glossary->id);
+
+        // Test student can update.
+        $this->assertTrue(mod_glossary_can_update_entry($entry, $glossary, $context, $cm));
+
+        // Test teacher can update.
+        $this->setUser($teacher);
+        $this->assertTrue(mod_glossary_can_update_entry($entry, $glossary, $context, $cm));
+
+        // Test admin can update.
+        $this->setAdminUser();
+        $this->assertTrue(mod_glossary_can_update_entry($entry, $glossary, $context, $cm));
+
+        // Test a different student is not able to update.
+        $this->setUser($anotherstudent);
+        $this->assertFalse(mod_glossary_can_update_entry($entry, $glossary, $context, $cm));
+
+        // Test exception.
+        $this->expectExceptionMessage(get_string('errcannoteditothers', 'glossary'));
+        mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false);
+    }
+
+    public function test_mod_glossary_can_update_entry_edit_period() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        // Create required data.
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id, 'editalways' => 1));
+
+        $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
+        $this->setUser($student);
+        $entry = $gg->create_content($glossary);
+        $context = context_module::instance($glossary->cmid);
+        $cm = get_coursemodule_from_instance('glossary', $glossary->id);
+
+        // Test student can always update when edit always is set to 1.
+        $entry->timecreated = time() - 2 * $CFG->maxeditingtime;
+        $this->assertTrue(mod_glossary_can_update_entry($entry, $glossary, $context, $cm));
+
+        // Test student cannot update old entries when edit always is set to 0.
+        $glossary->editalways = 0;
+        $this->assertFalse(mod_glossary_can_update_entry($entry, $glossary, $context, $cm));
+
+        // Test student can update recent entries when edit always is set to 0.
+        $entry->timecreated = time();
+        $this->assertTrue(mod_glossary_can_update_entry($entry, $glossary, $context, $cm));
+
+        // Check exception.
+        $entry->timecreated = time() - 2 * $CFG->maxeditingtime;
+        $this->expectExceptionMessage(get_string('erredittimeexpired', 'glossary'));
+        mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false);
+    }
+
+