Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
<?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/>.

/**
 * Tour class.
 *
 * @package    tool_usertours
 * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace tool_usertours;

use tool_usertours\local\clientside_filter\clientside_filter;

defined('MOODLE_INTERNAL') || die();

/**
 * Tour class.
 *
 * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class tour {

    /**
     * The tour is currently disabled
     *
     * @var DISABLED
     */
    const DISABLED = 0;

    /**
     * The tour is currently disabled
     *
     * @var DISABLED
     */
    const ENABLED = 1;

    /**
     * The user preference value to indicate the time of completion of the tour for a user.
     *
     * @var TOUR_LAST_COMPLETED_BY_USER
     */
    const TOUR_LAST_COMPLETED_BY_USER   = 'tool_usertours_tour_completion_time_';

    /**
     * The user preference value to indicate the time that a user last requested to see the tour.
     *
     * @var TOUR_REQUESTED_BY_USER
     */
    const TOUR_REQUESTED_BY_USER        = 'tool_usertours_tour_reset_time_';

    /**
     * @var $id The tour ID.
     */
    protected $id;

    /**
     * @var $name The tour name.
     */
    protected $name;

    /**
     * @var $description The tour description.
     */
    protected $description;

    /**
     * @var $pathmatch The tour pathmatch.
     */
    protected $pathmatch;

    /**
     * @var $enabled The tour enabled state.
     */
    protected $enabled;

    /**
     * @var $endtourlabel The end tour label.
     */
    protected $endtourlabel;

    /**
     * @var $sortorder The sort order.
     */
    protected $sortorder;

    /**
     * @var $dirty Whether the current view of the tour has been modified.
     */
    protected $dirty = false;

    /**
     * @var $config The configuration object for the tour.
     */
    protected $config;

    /**
     * @var $filtervalues The filter configuration object for the tour.
     */
    protected $filtervalues;

    /**
     * @var $steps  The steps in this tour.
     */
    protected $steps = [];

    /**
     * @var bool $displaystepnumbers Display the step numbers in this tour.
     */
    protected $displaystepnumbers = true;

    /**
     * Create an instance of the specified tour.
     *
     * @param   int         $id         The ID of the tour to load.
     * @return  tour
     */
    public static function instance($id) {
        $tour = new self();
        return $tour->fetch($id);
    }

    /**
     * Create an instance of tour from its provided DB record.
     *
     * @param   stdClass    $record     The record of the tour to load.
     * @param   boolean     $clean      Clean the values.
     * @return  tour
     */
    public static function load_from_record($record, $clean = false) {
        $tour = new self();
        return $tour->reload_from_record($record, $clean);
    }

    /**
     * Fetch the specified tour into the current object.
     *
     * @param   int         $id         The ID of the tour to fetch.
     * @return  tour
     */
    protected function fetch($id) {
        global $DB;

        return $this->reload_from_record(
            $DB->get_record('tool_usertours_tours', array('id' => $id), '*', MUST_EXIST)
        );
    }

    /**
     * Reload the current tour from database.
     *
     * @return  tour
     */
    protected function reload() {
        return $this->fetch($this->id);
    }

    /**
     * Reload the tour into the current object.
     *
     * @param   stdClass    $record     The record to reload.
     * @param   boolean     $clean      Clean the values.
     * @return  tour
     */
    protected function reload_from_record($record, $clean = false) {
        $this->id           = $record->id;
        if (!property_exists($record, 'description')) {
            if (property_exists($record, 'comment')) {
                $record->description = $record->comment;
                unset($record->comment);
            }
        }
        if ($clean) {
            $this->name         = clean_param($record->name, PARAM_TEXT);
            $this->description  = clean_text($record->description);
        } else {
            $this->name         = $record->name;
            $this->description  = $record->description;
        }
        $this->pathmatch    = $record->pathmatch;
        $this->enabled      = $record->enabled;
        if (isset($record->sortorder)) {
            $this->sortorder = $record->sortorder;
        }
        $this->endtourlabel = $record->endtourlabel ?? null;
        $this->config       = json_decode($record->configdata);
        $this->dirty        = false;
        $this->steps        = [];
        $this->displaystepnumbers = !empty($record->displaystepnumbers);

        return $this;
    }

    /**
     * Fetch all steps in the tour.
     *
     * @return  step[]
     */
    public function get_steps() {
        if (empty($this->steps)) {
            $this->steps = helper::get_steps($this->id);
        }

        return $this->steps;
    }

    /**
     * Count the number of steps in the tour.
     *
     * @return  int
     */
    public function count_steps() {
        return count($this->get_steps());
    }

    /**
     * The ID of the tour.
     *
     * @return  int
     */
    public function get_id() {
        return $this->id;
    }

    /**
     * The name of the tour.
     *
     * @return  string
     */
    public function get_name() {
        return $this->name;
    }

    /**
     * Set the name of the tour to the specified value.
     *
     * @param   string      $value      The new name.
     * @return  $this
     */
    public function set_name($value) {
        $this->name = clean_param($value, PARAM_TEXT);
        $this->dirty = true;

        return $this;
    }

    /**
     * The description associated with the tour.
     *
     * @return  string
     */
    public function get_description() {
        return $this->description;
    }

    /**
     * Set the description of the tour to the specified value.
     *
     * @param   string      $value      The new description.
     * @return  $this
     */
    public function set_description($value) {
        $this->description = clean_text($value);
        $this->dirty = true;

        return $this;
    }

    /**
     * The path match for the tour.
     *
     * @return  string
     */
    public function get_pathmatch() {
        return $this->pathmatch;
    }

    /**
     * Set the patchmatch of the tour to the specified value.
     *
     * @param   string      $value      The new patchmatch.
     * @return  $this
     */
    public function set_pathmatch($value) {
        $this->pathmatch = $value;
        $this->dirty = true;

        return $this;
    }

    /**
     * The enabled state of the tour.
     *
     * @return  int
     */
    public function get_enabled() {
        return $this->enabled;
    }

    /**
     * Whether the tour is currently enabled.
     *
     * @return  boolean
     */
    public function is_enabled() {
        return ($this->enabled == self::ENABLED);
    }

    /**
     * Set the enabled state of the tour to the specified value.
     *
     * @param   boolean     $value      The new state.
     * @return  $this
     */
    public function set_enabled($value) {
        $this->enabled = $value;
        $this->dirty = true;

        return $this;
    }

    /**
     * The end tour label for the tour.
     *
     * @return string
     */
    public function get_endtourlabel(): string {
        if ($this->endtourlabel) {
            $label = helper::get_string_from_input($this->endtourlabel);
        } else if ($this->count_steps() == 1) {
            $label = get_string('endonesteptour', 'tool_usertours');
        } else {
            $label = get_string('endtour', 'tool_usertours');
        }

        return $label;
    }

    /**
     * Set the endtourlabel of the tour to the specified value.
     *
     * @param string $value
     * @return $this
     */
    public function set_endtourlabel(string $value): tour {
        $this->endtourlabel = $value;
        $this->dirty = true;

        return $this;
    }

    /**
     * The link to view this tour.
     *
< * @return moodle_url
> * @return \moodle_url
*/ public function get_view_link() { return helper::get_view_tour_link($this->id); } /** * The link to edit this tour. *
< * @return moodle_url
> * @return \moodle_url
*/ public function get_edit_link() { return helper::get_edit_tour_link($this->id); } /** * The link to reset the state of this tour for all users. * * @return moodle_url */ public function get_reset_link() { return helper::get_reset_tour_for_all_link($this->id); } /** * The link to export this tour. * * @return moodle_url */ public function get_export_link() { return helper::get_export_tour_link($this->id); } /** * The link to duplicate this tour. * * @return moodle_url */ public function get_duplicate_link() { return helper::get_duplicate_tour_link($this->id); } /** * The link to remove this tour. * * @return moodle_url */ public function get_delete_link() { return helper::get_delete_tour_link($this->id); } /** * Prepare this tour for saving to the database. * * @return object */ public function to_record() { return (object) array( 'id' => $this->id, 'name' => $this->name, 'description' => $this->description, 'pathmatch' => $this->pathmatch, 'enabled' => $this->enabled, 'sortorder' => $this->sortorder, 'endtourlabel' => $this->endtourlabel, 'configdata' => json_encode($this->config), 'displaystepnumbers' => $this->displaystepnumbers, ); } /** * Get the current sortorder for this tour. * * @return int */ public function get_sortorder() { return (int) $this->sortorder; } /** * Whether this tour is the first tour. * * @return boolean */ public function is_first_tour() { return ($this->get_sortorder() === 0); } /** * Whether this tour is the last tour. * * @param int $tourcount The pre-fetched count of tours * @return boolean */ public function is_last_tour($tourcount = null) { if ($tourcount === null) { $tourcount = helper::count_tours(); } return ($this->get_sortorder() === ($tourcount - 1)); } /** * Set the sortorder for this tour. * * @param int $value The new sortorder to use. * @return $this */ public function set_sortorder($value) { $this->sortorder = $value; $this->dirty = true; return $this; } /** * Calculate the next sort-order value. * * @return int */ protected function calculate_sortorder() { $this->sortorder = helper::count_tours(); return $this; } /** * Get the link to move this tour up in the sortorder. * * @return moodle_url */ public function get_moveup_link() { return helper::get_move_tour_link($this->get_id(), helper::MOVE_UP); } /** * Get the link to move this tour down in the sortorder. * * @return moodle_url */ public function get_movedown_link() { return helper::get_move_tour_link($this->get_id(), helper::MOVE_DOWN); } /** * Get the value of the specified configuration item. * * @param string $key The configuration key to set. * @param mixed $default The default value to use if a value was not found. * @return mixed */ public function get_config($key = null, $default = null) { if ($this->config === null) { $this->config = (object) array(); } if ($key === null) { return $this->config; } if (property_exists($this->config, $key)) { return $this->config->$key; } if ($default !== null) { return $default; } return configuration::get_default_value($key); } /** * Set the configuration item as specified. * * @param string $key The configuration key to set. * @param mixed $value The new value for the configuration item. * @return $this */ public function set_config($key, $value) { if ($this->config === null) { $this->config = (object) array(); } $this->config->$key = $value; $this->dirty = true; return $this; } /** * Save the tour and it's configuration to the database. * * @param boolean $force Whether to force writing to the database. * @return $this */ public function persist($force = false) { global $DB; if (!$this->dirty && !$force) { return $this; } if ($this->id) { $record = $this->to_record(); $DB->update_record('tool_usertours_tours', $record); } else { $this->calculate_sortorder(); $record = $this->to_record(); unset($record->id); $this->id = $DB->insert_record('tool_usertours_tours', $record); } $this->reload(); // Notify the cache that a tour has changed. cache::notify_tour_change(); return $this; } /** * Remove this step. */ public function remove() { global $DB; if ($this->id === null) { // Nothing to delete - this tour has not been persisted. return null; } // Delete all steps associated with this tour. // Note, although they are currently just DB records, there may be other components in the future. foreach ($this->get_steps() as $step) { $step->remove(); } // Remove the configuration for the tour. $DB->delete_records('tool_usertours_tours', array('id' => $this->id)); helper::reset_tour_sortorder(); $this->remove_user_preferences(); return null; } /** * Reset the sortorder for all steps in the tour. * * @return $this */ public function reset_step_sortorder() { global $DB; $steps = $DB->get_records('tool_usertours_steps', array('tourid' => $this->id), 'sortorder ASC', 'id'); $index = 0; foreach ($steps as $step) { $DB->set_field('tool_usertours_steps', 'sortorder', $index, array('id' => $step->id)); $index++; } // Notify of a change to the step configuration. // Note: Do not notify of a tour change here. This is only a step change for a tour. cache::notify_step_change($this->get_id()); return $this; } /** * Remove stored user preferences for the tour */ protected function remove_user_preferences(): void { global $DB; $DB->delete_records('user_preferences', ['name' => self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id()]); $DB->delete_records('user_preferences', ['name' => self::TOUR_REQUESTED_BY_USER . $this->get_id()]); } /** * Whether this tour should be displayed to the user. * * @return boolean */ public function should_show_for_user() { if (!$this->is_enabled()) { // The tour is disabled - it should not be shown. return false; } if ($tourcompletiondate = get_user_preferences(self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id(), null)) { if ($tourresetdate = get_user_preferences(self::TOUR_REQUESTED_BY_USER . $this->get_id(), null)) { if ($tourresetdate >= $tourcompletiondate) { return true; } } $lastmajorupdate = $this->get_config('majorupdatetime', time()); if ($tourcompletiondate > $lastmajorupdate) { // The user has completed the tour since the last major update. return false; } } return true; } /** * Get the key for this tour. * This is used in the session cookie to determine whether the user has seen this tour before. */ public function get_tour_key() { global $USER; $tourtime = $this->get_config('majorupdatetime', null); if ($tourtime === null) { // This tour has no majorupdate time. // Set one now to prevent repeated displays to the user. $this->set_config('majorupdatetime', time()); $this->persist(); $tourtime = $this->get_config('majorupdatetime', null); } if ($userresetdate = get_user_preferences(self::TOUR_REQUESTED_BY_USER . $this->get_id(), null)) { $tourtime = max($tourtime, $userresetdate); } return sprintf('tool_usertours_%d_%d_%s', $USER->id, $this->get_id(), $tourtime); } /** * Reset the requested by user date. * * @return $this */ public function request_user_reset() { set_user_preference(self::TOUR_REQUESTED_BY_USER . $this->get_id(), time()); return $this; } /** * Mark this tour as completed for this user. * * @return $this */ public function mark_user_completed() { set_user_preference(self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id(), time()); return $this; } /** * Update a tour giving it a new major update time. * This will ensure that it is displayed to all users, even those who have already seen it. * * @return $this */ public function mark_major_change() { // Clear old reset and completion notes. $this->remove_user_preferences(); $this->set_config('majorupdatetime', time()); $this->persist(); return $this; } /** * Add the step configuration to the form. * * @param MoodleQuickForm $mform The form to add configuration to. * @return $this */ public function add_config_to_form(\MoodleQuickForm &$mform) { $options = configuration::get_placement_options(); $mform->addElement('select', 'placement', get_string('placement', 'tool_usertours'), $options); $mform->addHelpButton('placement', 'placement', 'tool_usertours'); $this->add_config_field_to_form($mform, 'orphan'); $this->add_config_field_to_form($mform, 'backdrop'); $this->add_config_field_to_form($mform, 'reflex'); return $this; } /** * Add the specified step field configuration to the form. * * @param MoodleQuickForm $mform The form to add configuration to. * @param string $key The key to add. * @return $this */ protected function add_config_field_to_form(\MoodleQuickForm &$mform, $key) { $options = [ true => get_string('yes'), false => get_string('no'), ]; $mform->addElement('select', $key, get_string($key, 'tool_usertours'), $options); $mform->setDefault($key, configuration::get_default_value($key)); $mform->addHelpButton($key, $key, 'tool_usertours'); return $this; } /** * Prepare the configuration data for the moodle form. * * @return object */ public function prepare_data_for_form() { $data = $this->to_record(); foreach (configuration::get_defaultable_keys() as $key) { $data->$key = $this->get_config($key, configuration::get_default_value($key)); } return $data; } /** * Get the configured filter values. * * @param string $filter The filter to retrieve values for. * @return array */ public function get_filter_values($filter) { if ($allvalues = (array) $this->get_config('filtervalues')) { if (isset($allvalues[$filter])) { return $allvalues[$filter]; } } return []; } /** * Set the values for the specified filter. * * @param string $filter The filter to set. * @param array $values The values to set. * @return $this */ public function set_filter_values($filter, array $values = []) { $allvalues = (array) $this->get_config('filtervalues', []); $allvalues[$filter] = $values; return $this->set_config('filtervalues', $allvalues); } /** * Check whether this tour matches all filters. * * @param \context $context The context to check. * @param array|null $filters Optional array of filters. * @return bool */ 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. foreach ($filters as $filterclass) { if (!$filterclass::filter_matches($this, $context)) { 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; } /** * Set the value for the display step numbers setting. * * @param bool $value True for enable. * @return $this */ public function set_display_step_numbers(bool $value): tour { $this->displaystepnumbers = $value; $this->dirty = true; return $this; } /** * Get the value of the display step numbers setting. * * @return bool */ public function get_display_step_numbers(): bool { return $this->displaystepnumbers; } }