--- /dev/null
+<?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);
+ }
+}
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()) {
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" .
- "&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 {
$pluginman = core_plugin_manager::instance();
if ($uninstall) {
- require_sesskey();
if (!$confirmed) {
admin_externalpage_setup('pluginsoverview', '', $pageparams);
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);
$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.';
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');
$mobilestrconnected = get_string('lastsiteaccess');
if ($usertoken->lastaccess) {
$mobilelastaccess = userdate($usertoken->lastaccess) . " (" . 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 .= " ($logoutlink)";
+ }
} else {
// We should not reach this point.
$mobilelastaccess = get_string("never");
--- /dev/null
+<?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();
--- /dev/null
+// 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;
+};
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);
+ });
});
},
methodname: 'tool_usertours_fetch_and_start_tour',
args: {
tourid: tourId,
- context: usertours.context,
+ context: M.cfg.contextid,
pageurl: window.location.href,
}
}
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(),
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(),
methodname: 'tool_usertours_reset_tour',
args: {
tourid: tourId,
- context: usertours.context,
+ context: M.cfg.contextid,
pageurl: window.location.href,
}
}
$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([
'pageurl' => $params['pageurl'],
],
])->trigger();
-
+ break;
}
}
namespace tool_usertours;
+use tool_usertours\local\clientside_filter\clientside_filter;
+
defined('MOODLE_INTERNAL') || die();
/**
}
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) {
--- /dev/null
+<?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
--- /dev/null
+<?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;
+ }
+}
}
/**
- * 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;
}
/**
namespace tool_usertours;
+use tool_usertours\local\clientside_filter\clientside_filter;
+
defined('MOODLE_INTERNAL') || die();
/**
/**
* 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.
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;
+ }
}
$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';
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"
'description' => '',
'configdata' => '',
],
+ [
+ 'pathmatch' => '/my/%',
+ 'enabled' => true,
+ 'name' => 'My tour enabled 2',
+ 'description' => '',
+ 'configdata' => '',
+ ],
[
'pathmatch' => '/my/%',
'enabled' => false,
'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'],
],
];
}
* 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) {
$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());
}
}
}
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).
$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'));
$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;
$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);
$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,
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
*
} 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
);
}
* @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'));
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()) {
* @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'));
'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);
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()) {
'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(
* @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'));
$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()) {
'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);
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;
}
$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);
$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');
}
// $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!
--- /dev/null
+<?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(),
+ ]
+ );
+ }
+}
$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']);
+ }
}
'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)) {
* @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.
$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;
}
* @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);
}
/**
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 = [
$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);
}
* @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);
}
/**
$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.
*
namespace core_h5p;
-defined('MOODLE_INTERNAL') || die();
+use core_collator;
/**
*
$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().
*/
// 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']);
}
/**
$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);
}
/**
*/
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);
* @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(
'preloadedcss' => 'css/example.css',
'droplibrarycss' => '',
'semantics' => $semantics,
- 'addto' => $addto
+ 'addto' => $addto,
+ 'tutorial' => $tutorial,
+ 'example' => $example
);
$libraryid = $DB->insert_record('h5p_libraries', $content);
$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) [
'droplibrarycss' => '',
'semantics' => 'Semantics example',
'addto' => '/regex11/',
+ 'tutorial' => 'http://tutorial.org/',
+ 'example' => 'http://example.org/',
'coremajor' => null,
'coreminor' => null,
'metadatasettings' => null,
// 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,
'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'];
$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);
+ }
}
/**
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'] = 'تعذر تنزيل المُكونات';
$string['databasetypehead'] = 'إختر مشغل قاعدة البيانات';
$string['dataroot'] = 'مجلد البيانات';
$string['datarootpermission'] = 'صلاحيات مجلدات البيانات';
-$string['dbprefix'] = 'مقدمة الجداول';
+$string['dbprefix'] = 'بادئة الجداول';
$string['dirroot'] = 'مجلد مودل';
$string['environmenthead'] = 'يتم فحص البيئة';
$string['environmentsub2'] = 'كل إصدار من مودل يتطلب وجود إصدار معين من PHP على الأقل مع عدد من امتداداته الإجبارية.
$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!';
$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';
'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',
*/
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,
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.
* @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()) {
if ($this->requires_write_lock()) {
$this->unlock_session($id);
}
+ $this->lasthash = sha1('');
return '';
}
$this->connection->expire($id, $this->timeout);
error_log('Failed talking to redis: '.$e->getMessage());
throw $e;
}
+ $this->lasthash = sha1(base64_encode($sessiondata));
return $sessiondata;
}
* @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.');
* @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);
<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"/>
'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',
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;
}
}
}
}}
-{{< 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 }}
<?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
<?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 {
}
<?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;
+++ /dev/null
-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.
<?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.
* @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]);
}
}
* @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
-<?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
*
<?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
*/
/**
* @var boolean
*/
- protected $disconnected = false;
+ protected $disconnected = true;
/**
* @var boolean
*/
*/
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
*/
*/
protected $until_payload = array();
/**
- * @var XMPPHP_Log
+ * @var Log
*/
protected $log;
/**
$this->host = $host;
$this->port = $port;
$this->setupParser();
- $this->log = new XMPPHP_Log($printlog, $loglevel);
+ $this->log = new Log($printlog, $loglevel);
}
/**
/**
* Return the log instance
*
- * @return XMPPHP_Log
+ * @return Log
*/
public function getLog() {
return $this->log;
$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 {
/**
* 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;
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));
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');
* 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;
}
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 {
}
$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 {
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.
}
/**
* 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)) {
/**
* 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]);
} else {
$payload = array();
}
+
return $payload;
}
$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;
}
* @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;
}
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]);
}
}
}
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]);
}
}
}
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;
}
}
* @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) {
$handler[2]->{$handler[1]}($payload);
}
}
+
foreach($this->until as $key => $until) {
if(is_array($until)) {
if(in_array($name, $until)) {
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);
}
# 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;
}
<?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
*/
$this->password = $password;
$this->resource = $resource;
if(!$server) $server = $host;
+ $this->server = $server;
$this->basejid = $this->user . '@' . $this->host;
$this->roster = new Roster();
* @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);
* @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);
$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>";
}
} 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);
}
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}' />");
* @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!');
}
/**
+++ /dev/null
-<?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');
- }
- }
- }
-
-
-?>
-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
* - $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
$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';
$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]);
}
}
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();
$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));
}
<location>jabber</location>
<name>XMPPHP</name>
<license>GPL</license>
- <version>0.1rc2-r77</version>
+ <version>2.2</version>
<licenseversion>2.0+</licenseversion>
</library>
<library>
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().
\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);
$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) [
// 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;
}
*/
require_once($CFG->dirroot.'/message/output/lib.php');
-require_once($CFG->libdir.'/jabber/XMPP/XMPP.php');
-
/**
* The jabber message processor
*
$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;
$conn->presence();
$conn->message($jabberaddress, $jabbermessage);
$conn->disconnect();
- } catch(XMPPHP_Exception $e) {
+ } catch(\BirknerAlex\XMPPHP\Exception $e) {
debugging($e->getMessage());
return false;
}
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');
$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
}
}
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);
+ }
+ }
}
}
$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);
+ }
}
} 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, () => {
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() {
'core/templates',
'core/notification',
'core/pending',
+ 'core/yui',
'mod_forum/selectors',
'mod_forum/inpage_reply',
], function(
Templates,
Notification,
Pending,
+ Y,
Selectors,
InPageReply
) {
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);
$parentposts = [];
if ($parentids) {
$parentposts = $postbuilder->build(
- $user,
+ $USER,
[$forum],
[$discussion],
$postvault->get_from_ids(array_values($parentids))
'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,
],
];
* 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();
// 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(),
'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),
],
]
],
'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' => [
$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
'view' => true,
'edit' => true,
'delete' => true,
- 'split' => false,
+ 'split' => true,
'reply' => true,
'export' => false,
'controlreadstatus' => false,
- 'canreplyprivately' => false,
+ 'canreplyprivately' => true,
'selfenrol' => false
],
'urls' => [
'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),
'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' => [
$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
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:
* @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');
// 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');
$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
);
}
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()
));
}
--- /dev/null
+<?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()
+ ]);
+ }
+}
--- /dev/null
+<?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(),
+ ]);
+ }
+}
--- /dev/null
+<?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()
+ ]);
+ }
+}
'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]
+ ],
);
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&mode=$prevmode&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);
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&mode=entry&hook=$id");
- } elseif (!$ineditperiod) {
- print_error('erredittimeexpired', 'glossary', "view.php?id=$cm->id&mode=entry&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);
}
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&mode=entry&hook=$entry->id");
+ } else if (!$ineditperiod) {
+ if ($return) {
+ return false;
+ }
+ throw new moodle_exception('erredittimeexpired', 'glossary', "view.php?id=$cm->id&mode=entry&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;
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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']);
+ }
+}
--- /dev/null
+<?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']);
+ }
+}
$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.
$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);
// 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() {
$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);
+ }
+
+