Search moodle.org's
Developer Documentation

  • 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 37 and 311] [Versions 38 and 311] [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  }