Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Tour class.
  19   *
  20   * @package    tool_usertours
  21   * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace tool_usertours;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Tour class.
  31   *
  32   * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class tour {
  36  
  37      /**
  38       * The tour is currently disabled
  39       *
  40       * @var DISABLED
  41       */
  42      const DISABLED = 0;
  43  
  44      /**
  45       * The tour is currently disabled
  46       *
  47       * @var DISABLED
  48       */
  49      const ENABLED = 1;
  50  
  51      /**
  52       * The user preference value to indicate the time of completion of the tour for a user.
  53       *
  54       * @var TOUR_LAST_COMPLETED_BY_USER
  55       */
  56      const TOUR_LAST_COMPLETED_BY_USER   = 'tool_usertours_tour_completion_time_';
  57  
  58      /**
  59       * The user preference value to indicate the time that a user last requested to see the tour.
  60       *
  61       * @var TOUR_REQUESTED_BY_USER
  62       */
  63      const TOUR_REQUESTED_BY_USER        = 'tool_usertours_tour_reset_time_';
  64  
  65      /**
  66       * @var $id The tour ID.
  67       */
  68      protected $id;
  69  
  70      /**
  71       * @var $name The tour name.
  72       */
  73      protected $name;
  74  
  75      /**
  76       * @var $description The tour description.
  77       */
  78      protected $description;
  79  
  80      /**
  81       * @var $pathmatch The tour pathmatch.
  82       */
  83      protected $pathmatch;
  84  
  85      /**
  86       * @var $enabled The tour enabled state.
  87       */
  88      protected $enabled;
  89  
  90      /**
  91       * @var $sortorder The sort order.
  92       */
  93      protected $sortorder;
  94  
  95      /**
  96       * @var $dirty Whether the current view of the tour has been modified.
  97       */
  98      protected $dirty = false;
  99  
 100      /**
 101       * @var $config The configuration object for the tour.
 102       */
 103      protected $config;
 104  
 105      /**
 106       * @var $filtervalues The filter configuration object for the tour.
 107       */
 108      protected $filtervalues;
 109  
 110      /**
 111       * @var $steps  The steps in this tour.
 112       */
 113      protected $steps = [];
 114  
 115      /**
 116       * Create an instance of the specified tour.
 117       *
 118       * @param   int         $id         The ID of the tour to load.
 119       * @return  tour
 120       */
 121      public static function instance($id) {
 122          $tour = new self();
 123          return $tour->fetch($id);
 124      }
 125  
 126      /**
 127       * Create an instance of tour from its provided DB record.
 128       *
 129       * @param   stdClass    $record     The record of the tour to load.
 130       * @param   boolean     $clean      Clean the values.
 131       * @return  tour
 132       */
 133      public static function load_from_record($record, $clean = false) {
 134          $tour = new self();
 135          return $tour->reload_from_record($record, $clean);
 136      }
 137  
 138      /**
 139       * Fetch the specified tour into the current object.
 140       *
 141       * @param   int         $id         The ID of the tour to fetch.
 142       * @return  tour
 143       */
 144      protected function fetch($id) {
 145          global $DB;
 146  
 147          return $this->reload_from_record(
 148              $DB->get_record('tool_usertours_tours', array('id' => $id), '*', MUST_EXIST)
 149          );
 150      }
 151  
 152      /**
 153       * Reload the current tour from database.
 154       *
 155       * @return  tour
 156       */
 157      protected function reload() {
 158          return $this->fetch($this->id);
 159      }
 160  
 161      /**
 162       * Reload the tour into the current object.
 163       *
 164       * @param   stdClass    $record     The record to reload.
 165       * @param   boolean     $clean      Clean the values.
 166       * @return  tour
 167       */
 168      protected function reload_from_record($record, $clean = false) {
 169          $this->id           = $record->id;
 170          if (!property_exists($record, 'description')) {
 171              if (property_exists($record, 'comment')) {
 172                  $record->description = $record->comment;
 173                  unset($record->comment);
 174              }
 175          }
 176          if ($clean) {
 177              $this->name         = clean_param($record->name, PARAM_TEXT);
 178              $this->description  = clean_text($record->description);
 179          } else {
 180              $this->name         = $record->name;
 181              $this->description  = $record->description;
 182          }
 183          $this->pathmatch    = $record->pathmatch;
 184          $this->enabled      = $record->enabled;
 185          if (isset($record->sortorder)) {
 186              $this->sortorder = $record->sortorder;
 187          }
 188          $this->config       = json_decode($record->configdata);
 189          $this->dirty        = false;
 190          $this->steps        = [];
 191  
 192          return $this;
 193      }
 194  
 195      /**
 196       * Fetch all steps in the tour.
 197       *
 198       * @return  stdClass[]
 199       */
 200      public function get_steps() {
 201          if (empty($this->steps)) {
 202              $this->steps = helper::get_steps($this->id);
 203          }
 204  
 205          return $this->steps;
 206      }
 207  
 208      /**
 209       * Count the number of steps in the tour.
 210       *
 211       * @return  int
 212       */
 213      public function count_steps() {
 214          return count($this->get_steps());
 215      }
 216  
 217      /**
 218       * The ID of the tour.
 219       *
 220       * @return  int
 221       */
 222      public function get_id() {
 223          return $this->id;
 224      }
 225  
 226      /**
 227       * The name of the tour.
 228       *
 229       * @return  string
 230       */
 231      public function get_name() {
 232          return $this->name;
 233      }
 234  
 235      /**
 236       * Set the name of the tour to the specified value.
 237       *
 238       * @param   string      $value      The new name.
 239       * @return  $this
 240       */
 241      public function set_name($value) {
 242          $this->name = clean_param($value, PARAM_TEXT);
 243          $this->dirty = true;
 244  
 245          return $this;
 246      }
 247  
 248      /**
 249       * The description associated with the tour.
 250       *
 251       * @return  string
 252       */
 253      public function get_description() {
 254          return $this->description;
 255      }
 256  
 257      /**
 258       * Set the description of the tour to the specified value.
 259       *
 260       * @param   string      $value      The new description.
 261       * @return  $this
 262       */
 263      public function set_description($value) {
 264          $this->description = clean_text($value);
 265          $this->dirty = true;
 266  
 267          return $this;
 268      }
 269  
 270      /**
 271       * The path match for the tour.
 272       *
 273       * @return  string
 274       */
 275      public function get_pathmatch() {
 276          return $this->pathmatch;
 277      }
 278  
 279      /**
 280       * Set the patchmatch of the tour to the specified value.
 281       *
 282       * @param   string      $value      The new patchmatch.
 283       * @return  $this
 284       */
 285      public function set_pathmatch($value) {
 286          $this->pathmatch = $value;
 287          $this->dirty = true;
 288  
 289          return $this;
 290      }
 291  
 292      /**
 293       * The enabled state of the tour.
 294       *
 295       * @return  int
 296       */
 297      public function get_enabled() {
 298          return $this->enabled;
 299      }
 300  
 301      /**
 302       * Whether the tour is currently enabled.
 303       *
 304       * @return  boolean
 305       */
 306      public function is_enabled() {
 307          return ($this->enabled == self::ENABLED);
 308      }
 309  
 310      /**
 311       * Set the enabled state of the tour to the specified value.
 312       *
 313       * @param   boolean     $value      The new state.
 314       * @return  $this
 315       */
 316      public function set_enabled($value) {
 317          $this->enabled = $value;
 318          $this->dirty = true;
 319  
 320          return $this;
 321      }
 322  
 323      /**
 324       * The link to view this tour.
 325       *
 326       * @return  moodle_url
 327       */
 328      public function get_view_link() {
 329          return helper::get_view_tour_link($this->id);
 330      }
 331  
 332      /**
 333       * The link to edit this tour.
 334       *
 335       * @return  moodle_url
 336       */
 337      public function get_edit_link() {
 338          return helper::get_edit_tour_link($this->id);
 339      }
 340  
 341      /**
 342       * The link to reset the state of this tour for all users.
 343       *
 344       * @return  moodle_url
 345       */
 346      public function get_reset_link() {
 347          return helper::get_reset_tour_for_all_link($this->id);
 348      }
 349  
 350      /**
 351       * The link to export this tour.
 352       *
 353       * @return  moodle_url
 354       */
 355      public function get_export_link() {
 356          return helper::get_export_tour_link($this->id);
 357      }
 358  
 359      /**
 360       * The link to duplicate this tour.
 361       *
 362       * @return  moodle_url
 363       */
 364      public function get_duplicate_link() {
 365          return helper::get_duplicate_tour_link($this->id);
 366      }
 367  
 368      /**
 369       * The link to remove this tour.
 370       *
 371       * @return  moodle_url
 372       */
 373      public function get_delete_link() {
 374          return helper::get_delete_tour_link($this->id);
 375      }
 376  
 377      /**
 378       * Prepare this tour for saving to the database.
 379       *
 380       * @return  object
 381       */
 382      public function to_record() {
 383          return (object) array(
 384              'id'            => $this->id,
 385              'name'          => $this->name,
 386              'description'   => $this->description,
 387              'pathmatch'     => $this->pathmatch,
 388              'enabled'       => $this->enabled,
 389              'sortorder'     => $this->sortorder,
 390              'configdata'    => json_encode($this->config),
 391          );
 392      }
 393  
 394      /**
 395       * Get the current sortorder for this tour.
 396       *
 397       * @return  int
 398       */
 399      public function get_sortorder() {
 400          return (int) $this->sortorder;
 401      }
 402  
 403      /**
 404       * Whether this tour is the first tour.
 405       *
 406       * @return  boolean
 407       */
 408      public function is_first_tour() {
 409          return ($this->get_sortorder() === 0);
 410      }
 411  
 412      /**
 413       * Whether this tour is the last tour.
 414       *
 415       * @param   int         $tourcount  The pre-fetched count of tours
 416       * @return  boolean
 417       */
 418      public function is_last_tour($tourcount = null) {
 419          if ($tourcount === null) {
 420              $tourcount = helper::count_tours();
 421          }
 422          return ($this->get_sortorder() === ($tourcount - 1));
 423      }
 424  
 425      /**
 426       * Set the sortorder for this tour.
 427       *
 428       * @param   int         $value      The new sortorder to use.
 429       * @return  $this
 430       */
 431      public function set_sortorder($value) {
 432          $this->sortorder = $value;
 433          $this->dirty = true;
 434  
 435          return $this;
 436      }
 437  
 438      /**
 439       * Calculate the next sort-order value.
 440       *
 441       * @return  int
 442       */
 443      protected function calculate_sortorder() {
 444          $this->sortorder = helper::count_tours();
 445  
 446          return $this;
 447      }
 448  
 449      /**
 450       * Get the link to move this tour up in the sortorder.
 451       *
 452       * @return  moodle_url
 453       */
 454      public function get_moveup_link() {
 455          return helper::get_move_tour_link($this->get_id(), helper::MOVE_UP);
 456      }
 457  
 458      /**
 459       * Get the link to move this tour down in the sortorder.
 460       *
 461       * @return  moodle_url
 462       */
 463      public function get_movedown_link() {
 464          return helper::get_move_tour_link($this->get_id(), helper::MOVE_DOWN);
 465      }
 466  
 467      /**
 468       * Get the value of the specified configuration item.
 469       *
 470       * @param   string      $key        The configuration key to set.
 471       * @param   mixed       $default    The default value to use if a value was not found.
 472       * @return  mixed
 473       */
 474      public function get_config($key = null, $default = null) {
 475          if ($this->config === null) {
 476              $this->config = (object) array();
 477          }
 478          if ($key === null) {
 479              return $this->config;
 480          }
 481  
 482          if (property_exists($this->config, $key)) {
 483              return $this->config->$key;
 484          }
 485  
 486          if ($default !== null) {
 487              return $default;
 488          }
 489  
 490          return configuration::get_default_value($key);
 491      }
 492  
 493      /**
 494       * Set the configuration item as specified.
 495       *
 496       * @param   string      $key        The configuration key to set.
 497       * @param   mixed       $value      The new value for the configuration item.
 498       * @return  $this
 499       */
 500      public function set_config($key, $value) {
 501          if ($this->config === null) {
 502              $this->config = (object) array();
 503          }
 504          $this->config->$key = $value;
 505          $this->dirty = true;
 506  
 507          return $this;
 508      }
 509  
 510      /**
 511       * Save the tour and it's configuration to the database.
 512       *
 513       * @param   boolean     $force      Whether to force writing to the database.
 514       * @return  $this
 515       */
 516      public function persist($force = false) {
 517          global $DB;
 518  
 519          if (!$this->dirty && !$force) {
 520              return $this;
 521          }
 522  
 523          if ($this->id) {
 524              $record = $this->to_record();
 525              $DB->update_record('tool_usertours_tours', $record);
 526          } else {
 527              $this->calculate_sortorder();
 528              $record = $this->to_record();
 529              unset($record->id);
 530              $this->id = $DB->insert_record('tool_usertours_tours', $record);
 531          }
 532  
 533          $this->reload();
 534  
 535          // Notify the cache that a tour has changed.
 536          cache::notify_tour_change();
 537  
 538          return $this;
 539      }
 540  
 541      /**
 542       * Remove this step.
 543       */
 544      public function remove() {
 545          global $DB;
 546  
 547          if ($this->id === null) {
 548              // Nothing to delete - this tour has not been persisted.
 549              return null;
 550          }
 551  
 552          // Delete all steps associated with this tour.
 553          // Note, although they are currently just DB records, there may be other components in the future.
 554          foreach ($this->get_steps() as $step) {
 555              $step->remove();
 556          }
 557  
 558          // Remove the configuration for the tour.
 559          $DB->delete_records('tool_usertours_tours', array('id' => $this->id));
 560          helper::reset_tour_sortorder();
 561  
 562          $this->remove_user_preferences();
 563  
 564          return null;
 565      }
 566  
 567      /**
 568       * Reset the sortorder for all steps in the tour.
 569       *
 570       * @return  $this
 571       */
 572      public function reset_step_sortorder() {
 573          global $DB;
 574          $steps = $DB->get_records('tool_usertours_steps', array('tourid' => $this->id), 'sortorder ASC', 'id');
 575  
 576          $index = 0;
 577          foreach ($steps as $step) {
 578              $DB->set_field('tool_usertours_steps', 'sortorder', $index, array('id' => $step->id));
 579              $index++;
 580          }
 581  
 582          // Notify of a change to the step configuration.
 583          // Note: Do not notify of a tour change here. This is only a step change for a tour.
 584          cache::notify_step_change($this->get_id());
 585  
 586          return $this;
 587      }
 588  
 589      /**
 590       * Remove stored user preferences for the tour
 591       */
 592      protected function remove_user_preferences(): void {
 593          global $DB;
 594  
 595          $DB->delete_records('user_preferences', ['name' => self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id()]);
 596          $DB->delete_records('user_preferences', ['name' => self::TOUR_REQUESTED_BY_USER . $this->get_id()]);
 597      }
 598  
 599      /**
 600       * Whether this tour should be displayed to the user.
 601       *
 602       * @return  boolean
 603       */
 604      public function should_show_for_user() {
 605          if (!$this->is_enabled()) {
 606              // The tour is disabled - it should not be shown.
 607              return false;
 608          }
 609  
 610          if ($tourcompletiondate = get_user_preferences(self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id(), null)) {
 611              if ($tourresetdate = get_user_preferences(self::TOUR_REQUESTED_BY_USER . $this->get_id(), null)) {
 612                  if ($tourresetdate >= $tourcompletiondate) {
 613                      return true;
 614                  }
 615              }
 616              $lastmajorupdate = $this->get_config('majorupdatetime', time());
 617              if ($tourcompletiondate > $lastmajorupdate) {
 618                  // The user has completed the tour since the last major update.
 619                  return false;
 620              }
 621          }
 622  
 623          return true;
 624      }
 625  
 626      /**
 627       * Get the key for this tour.
 628       * This is used in the session cookie to determine whether the user has seen this tour before.
 629       */
 630      public function get_tour_key() {
 631          global $USER;
 632  
 633          $tourtime = $this->get_config('majorupdatetime', null);
 634  
 635          if ($tourtime === null) {
 636              // This tour has no majorupdate time.
 637              // Set one now to prevent repeated displays to the user.
 638              $this->set_config('majorupdatetime', time());
 639              $this->persist();
 640              $tourtime = $this->get_config('majorupdatetime', null);
 641          }
 642  
 643          if ($userresetdate = get_user_preferences(self::TOUR_REQUESTED_BY_USER . $this->get_id(), null)) {
 644              $tourtime = max($tourtime, $userresetdate);
 645          }
 646  
 647          return sprintf('tool_usertours_%d_%d_%s', $USER->id, $this->get_id(), $tourtime);
 648      }
 649  
 650      /**
 651       * Reset the requested by user date.
 652       *
 653       * @return  $this
 654       */
 655      public function request_user_reset() {
 656          set_user_preference(self::TOUR_REQUESTED_BY_USER . $this->get_id(), time());
 657  
 658          return $this;
 659      }
 660  
 661      /**
 662       * Mark this tour as completed for this user.
 663       *
 664       * @return  $this
 665       */
 666      public function mark_user_completed() {
 667          set_user_preference(self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id(), time());
 668  
 669          return $this;
 670      }
 671  
 672      /**
 673       * Update a tour giving it a new major update time.
 674       * This will ensure that it is displayed to all users, even those who have already seen it.
 675       *
 676       * @return  $this
 677       */
 678      public function mark_major_change() {
 679          // Clear old reset and completion notes.
 680          $this->remove_user_preferences();
 681  
 682          $this->set_config('majorupdatetime', time());
 683          $this->persist();
 684  
 685          return $this;
 686      }
 687  
 688      /**
 689       * Add the step configuration to the form.
 690       *
 691       * @param   MoodleQuickForm $mform      The form to add configuration to.
 692       * @return  $this
 693       */
 694      public function add_config_to_form(\MoodleQuickForm &$mform) {
 695          $options = configuration::get_placement_options();
 696          $mform->addElement('select', 'placement', get_string('placement', 'tool_usertours'), $options);
 697          $mform->addHelpButton('placement', 'placement', 'tool_usertours');
 698  
 699          $this->add_config_field_to_form($mform, 'orphan');
 700          $this->add_config_field_to_form($mform, 'backdrop');
 701          $this->add_config_field_to_form($mform, 'reflex');
 702  
 703          return $this;
 704      }
 705  
 706      /**
 707       * Add the specified step field configuration to the form.
 708       *
 709       * @param   MoodleQuickForm $mform      The form to add configuration to.
 710       * @param   string          $key        The key to add.
 711       * @return  $this
 712       */
 713      protected function add_config_field_to_form(\MoodleQuickForm &$mform, $key) {
 714          $options = [
 715              true    => get_string('yes'),
 716              false   => get_string('no'),
 717          ];
 718          $mform->addElement('select', $key, get_string($key, 'tool_usertours'), $options);
 719          $mform->setDefault($key, configuration::get_default_value($key));
 720          $mform->addHelpButton($key, $key, 'tool_usertours');
 721  
 722          return $this;
 723      }
 724  
 725      /**
 726       * Prepare the configuration data for the moodle form.
 727       *
 728       * @return  object
 729       */
 730      public function prepare_data_for_form() {
 731          $data = $this->to_record();
 732          foreach (configuration::get_defaultable_keys() as $key) {
 733              $data->$key = $this->get_config($key, configuration::get_default_value($key));
 734          }
 735  
 736          return $data;
 737      }
 738  
 739      /**
 740       * Get the configured filter values.
 741       *
 742       * @param   string      $filter     The filter to retrieve values for.
 743       * @return  array
 744       */
 745      public function get_filter_values($filter) {
 746          if ($allvalues = (array) $this->get_config('filtervalues')) {
 747              if (isset($allvalues[$filter])) {
 748                  return $allvalues[$filter];
 749              }
 750          }
 751  
 752          return [];
 753      }
 754  
 755      /**
 756       * Set the values for the specified filter.
 757       *
 758       * @param   string      $filter     The filter to set.
 759       * @param   array       $values     The values to set.
 760       * @return  $this
 761       */
 762      public function set_filter_values($filter, array $values = []) {
 763          $allvalues = (array) $this->get_config('filtervalues', []);
 764          $allvalues[$filter] = $values;
 765  
 766          return $this->set_config('filtervalues', $allvalues);
 767      }
 768  
 769      /**
 770       * Check whether this tour matches all filters.
 771       *
 772       * @param   context     $context    The context to check
 773       * @return  bool
 774       */
 775      public function matches_all_filters(\context $context) {
 776          $filters = helper::get_all_filters();
 777  
 778          // All filters must match.
 779          // If any one filter fails to match, we return false.
 780          foreach ($filters as $filterclass) {
 781              if (!$filterclass::filter_matches($this, $context)) {
 782                  return false;
 783              }
 784          }
 785  
 786          return true;
 787      }
 788  }