Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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