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.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 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   * Step 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 context_system;
  28  use stdClass;
  29  
  30  defined('MOODLE_INTERNAL') || die();
  31  
  32  /**
  33   * Step class.
  34   *
  35   * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class step {
  39  
  40      /**
  41       * @var     int     $id         The id of the step.
  42       */
  43      protected $id;
  44  
  45      /**
  46       * @var     int     $tourid     The id of the tour that this step belongs to.
  47       */
  48      protected $tourid;
  49  
  50      /**
  51       * @var     tour    $tour       The tour class that this step belongs to.
  52       */
  53      protected $tour;
  54  
  55      /**
  56       * @var     string  $title      The title of the step.
  57       */
  58      protected $title;
  59  
  60      /**
  61       * @var     string  $content    The content of this step.
  62       */
  63      protected $content;
  64  
  65      /**
  66       * @var     int  $contentformat    The content format: FORMAT_MOODLE/FORMAT_HTML/FORMAT_PLAIN/FORMAT_MARKDOWN.
  67       */
  68      protected $contentformat;
  69  
  70      /**
  71       * @var     int     $targettype The type of target.
  72       */
  73      protected $targettype;
  74  
  75      /**
  76       * @var     string  $targetvalue    The value for this type of target.
  77       */
  78      protected $targetvalue;
  79  
  80      /**
  81       * @var     int     $sortorder  The sort order.
  82       */
  83      protected $sortorder;
  84  
  85      /**
  86       * @var     object  $config     The configuration as an object.
  87       */
  88      protected $config;
  89  
  90      /**
  91       * @var     bool    $dirty      Whether the step has been changed since it was loaded
  92       */
  93      protected $dirty = false;
  94  
  95      /**
  96       * @var bool $isimporting Whether the step is being imported or not.
  97       */
  98      protected $isimporting;
  99  
 100      /**
 101       * @var stdClass[] $files The list of attached files for this step.
 102       */
 103      protected $files = [];
 104  
 105      /**
 106       * Fetch the step instance.
 107       *
 108       * @param   int             $id         The id of the step to be retrieved.
 109       * @return  step
 110       */
 111      public static function instance($id) {
 112          $step = new step();
 113          return $step->fetch($id);
 114      }
 115  
 116      /**
 117       * Load the step instance.
 118       *
 119       * @param stdClass $record The step record to be loaded.
 120       * @param bool $clean Clean the values.
 121       * @param bool $isimporting Whether the step is being imported or not.
 122       * @return step
 123       */
 124      public static function load_from_record($record, $clean = false, bool $isimporting = false) {
 125          $step = new self();
 126          $step->set_importing($isimporting);
 127          return $step->reload_from_record($record, $clean);
 128      }
 129  
 130      /**
 131       * Fetch the step instance.
 132       *
 133       * @param   int             $id         The id of the step to be retrieved.
 134       * @return  step
 135       */
 136      protected function fetch($id) {
 137          global $DB;
 138  
 139          return $this->reload_from_record(
 140              $DB->get_record('tool_usertours_steps', array('id' => $id))
 141          );
 142      }
 143  
 144      /**
 145       * Refresh the current step from the datbase.
 146       *
 147       * @return  step
 148       */
 149      protected function reload() {
 150          return $this->fetch($this->id);
 151      }
 152  
 153      /**
 154       * Reload the current step from the supplied record.
 155       *
 156       * @param stdClass $record The step record to be loaded.
 157       * @param bool $clean Clean the values.
 158       * @return step
 159       */
 160      protected function reload_from_record($record, $clean = false) {
 161          $this->id           = $record->id;
 162          $this->tourid       = $record->tourid;
 163          if ($clean) {
 164              $this->title    = clean_param($record->title, PARAM_TEXT);
 165              $this->content  = clean_text($record->content);
 166          } else {
 167              $this->title    = $record->title;
 168              $this->content  = $record->content;
 169          }
 170          $this->contentformat = isset($record->contentformat) ? $record->contentformat : FORMAT_MOODLE;
 171          $this->targettype   = $record->targettype;
 172          $this->targetvalue  = $record->targetvalue;
 173          $this->sortorder    = $record->sortorder;
 174          $this->config       = json_decode($record->configdata);
 175          $this->dirty        = false;
 176  
 177          if ($this->isimporting && isset($record->files)) {
 178              // We are importing/exporting the step.
 179              $this->files = $record->files;
 180          }
 181  
 182          return $this;
 183      }
 184  
 185      /**
 186       * Set the import state for the step.
 187       *
 188       * @param bool $isimporting True if the step is imported, otherwise false.
 189       * @return void
 190       */
 191      protected function set_importing(bool $isimporting = false): void {
 192          $this->isimporting = $isimporting;
 193      }
 194  
 195      /**
 196       * Get the ID of the step.
 197       *
 198       * @return  int
 199       */
 200      public function get_id() {
 201          return $this->id;
 202      }
 203  
 204      /**
 205       * Get the Tour ID of the step.
 206       *
 207       * @return  int
 208       */
 209      public function get_tourid() {
 210          return $this->tourid;
 211      }
 212  
 213      /**
 214       * Get the Tour instance that this step belongs to.
 215       *
 216       * @return  tour
 217       */
 218      public function get_tour() {
 219          if ($this->tour === null) {
 220              $this->tour = tour::instance($this->tourid);
 221          }
 222          return $this->tour;
 223      }
 224  
 225      /**
 226       * Set the id of the tour.
 227       *
 228       * @param   int             $value      The id of the tour.
 229       * @return  self
 230       */
 231      public function set_tourid($value) {
 232          $this->tourid = $value;
 233          $this->tour = null;
 234          $this->dirty = true;
 235  
 236          return $this;
 237      }
 238  
 239      /**
 240       * Get the Title of the step.
 241       *
 242       * @return  string
 243       */
 244      public function get_title() {
 245          return $this->title;
 246      }
 247  
 248      /**
 249       * Set the title for this step.
 250       *
 251       * @param   string      $value      The new title to use.
 252       * @return  $this
 253       */
 254      public function set_title($value) {
 255          $this->title = clean_text($value);
 256          $this->dirty = true;
 257  
 258          return $this;
 259      }
 260  
 261      /**
 262       * Get the content format of the step.
 263       *
 264       * @return  int
 265       */
 266      public function get_contentformat(): int {
 267          return $this->contentformat;
 268      }
 269  
 270      /**
 271       * Get the body content of the step.
 272       *
 273       * @return  string
 274       */
 275      public function get_content() {
 276          return $this->content;
 277      }
 278  
 279      /**
 280       * Set the content value for this step.
 281       *
 282       * @param   string      $value      The new content to use.
 283       * @param   int         $format     The new format to use: FORMAT_MOODLE/FORMAT_HTML/FORMAT_PLAIN/FORMAT_MARKDOWN.
 284       * @return  $this
 285       */
 286      public function set_content($value, $format = FORMAT_HTML) {
 287          $this->content = clean_text($value);
 288          $this->contentformat = $format;
 289          $this->dirty = true;
 290  
 291          return $this;
 292      }
 293  
 294      /**
 295       * Get the content value for this step.
 296       *
 297       * @return  string
 298       */
 299      public function get_targettype() {
 300          return $this->targettype;
 301      }
 302  
 303      /**
 304       * Set the type of target for this step.
 305       *
 306       * @param   string      $value      The new target to use.
 307       * @return  $this
 308       */
 309      public function set_targettype($value) {
 310          $this->targettype = $value;
 311          $this->dirty = true;
 312  
 313          return $this;
 314      }
 315  
 316      /**
 317       * Get the target value for this step.
 318       *
 319       * @return  string
 320       */
 321      public function get_targetvalue() {
 322          return $this->targetvalue;
 323      }
 324  
 325      /**
 326       * Set the target value for this step.
 327       *
 328       * @param   string      $value      The new target value to use.
 329       * @return  $this
 330       */
 331      public function set_targetvalue($value) {
 332          $this->targetvalue = $value;
 333          $this->dirty = true;
 334  
 335          return $this;
 336      }
 337  
 338      /**
 339       * Get the target instance for this step.
 340       *
 341       * @return  target
 342       */
 343      public function get_target() {
 344          return target::get_target_instance($this);
 345      }
 346  
 347      /**
 348       * Get the current sortorder for this step.
 349       *
 350       * @return  int
 351       */
 352      public function get_sortorder() {
 353          return (int) $this->sortorder;
 354      }
 355  
 356      /**
 357       * Whether this step is the first step in the tour.
 358       *
 359       * @return  boolean
 360       */
 361      public function is_first_step() {
 362          return ($this->get_sortorder() === 0);
 363      }
 364  
 365      /**
 366       * Whether this step is the last step in the tour.
 367       *
 368       * @return  boolean
 369       */
 370      public function is_last_step() {
 371          $stepcount = $this->get_tour()->count_steps();
 372          return ($this->get_sortorder() === $stepcount - 1);
 373      }
 374  
 375      /**
 376       * Set the sortorder for this step.
 377       *
 378       * @param   int         $value      The new sortorder to use.
 379       * @return  $this
 380       */
 381      public function set_sortorder($value) {
 382          $this->sortorder = $value;
 383          $this->dirty = true;
 384  
 385          return $this;
 386      }
 387  
 388      /**
 389       * Get the link to move this step up in the sortorder.
 390       *
 391       * @return  moodle_url
 392       */
 393      public function get_moveup_link() {
 394          return helper::get_move_step_link($this->get_id(), helper::MOVE_UP);
 395      }
 396  
 397      /**
 398       * Get the link to move this step down in the sortorder.
 399       *
 400       * @return  moodle_url
 401       */
 402      public function get_movedown_link() {
 403          return helper::get_move_step_link($this->get_id(), helper::MOVE_DOWN);
 404      }
 405  
 406      /**
 407       * Get the value of the specified configuration item.
 408       *
 409       * If notvalue was found, and no default was specified, the default for the tour will be used.
 410       *
 411       * @param   string      $key        The configuration key to set.
 412       * @param   mixed       $default    The default value to use if a value was not found.
 413       * @return  mixed
 414       */
 415      public function get_config($key = null, $default = null) {
 416          if ($this->config === null) {
 417              $this->config = (object) array();
 418          }
 419  
 420          if ($key === null) {
 421              return $this->config;
 422          }
 423  
 424          if ($this->get_targettype() !== null) {
 425              $target = $this->get_target();
 426              if ($target->is_setting_forced($key)) {
 427                  return $target->get_forced_setting_value($key);
 428              }
 429          }
 430  
 431          if (property_exists($this->config, $key)) {
 432              return $this->config->$key;
 433          }
 434  
 435          if ($default !== null) {
 436              return $default;
 437          }
 438  
 439          return $this->get_tour()->get_config($key);
 440      }
 441  
 442      /**
 443       * Set the configuration item as specified.
 444       *
 445       * @param   string      $key        The configuration key to set.
 446       * @param   mixed       $value      The new value for the configuration item.
 447       * @return  $this
 448       */
 449      public function set_config($key, $value) {
 450          if ($this->config === null) {
 451              $this->config = (object) array();
 452          }
 453  
 454          if ($value === null) {
 455              unset($this->config->$key);
 456          } else {
 457              $this->config->$key = $value;
 458          }
 459          $this->dirty = true;
 460  
 461          return $this;
 462      }
 463  
 464      /**
 465       * Get the edit link for this step.
 466       *
 467       * @return  moodle_url
 468       */
 469      public function get_edit_link() {
 470          return helper::get_edit_step_link($this->tourid, $this->id);
 471      }
 472  
 473      /**
 474       * Get the delete link for this step.
 475       *
 476       * @return  moodle_url
 477       */
 478      public function get_delete_link() {
 479          return helper::get_delete_step_link($this->id);
 480      }
 481  
 482      /**
 483       * Embed attached file to the json file for step.
 484       *
 485       * @return array List of files.
 486       */
 487      protected function embed_files(): array {
 488          $systemcontext = context_system::instance();
 489          $fs = get_file_storage();
 490          $areafiles = $fs->get_area_files($systemcontext->id, 'tool_usertours', 'stepcontent', $this->id);
 491          $files = [];
 492          foreach ($areafiles as $file) {
 493              if ($file->is_directory()) {
 494                  continue;
 495              }
 496              $files[] = [
 497                  'name' => $file->get_filename(),
 498                  'path' => $file->get_filepath(),
 499                  'content' => base64_encode($file->get_content()),
 500                  'encode' => 'base64'
 501              ];
 502          }
 503  
 504          return $files;
 505      }
 506  
 507      /**
 508       * Get the embed files information and create store_file for this step.
 509       *
 510       * @return void
 511       */
 512      protected function extract_files() {
 513          $fs = get_file_storage();
 514          $systemcontext = context_system::instance();
 515          foreach ($this->files as $file) {
 516              $filename = $file->name;
 517              $filepath = $file->path;
 518              $filecontent = $file->content;
 519              $filerecord = [
 520                  'contextid' => $systemcontext->id,
 521                  'component' => 'tool_usertours',
 522                  'filearea' => 'stepcontent',
 523                  'itemid' => $this->get_id(),
 524                  'filepath' => $filepath,
 525                  'filename' => $filename,
 526              ];
 527              $fs->create_file_from_string($filerecord, base64_decode($filecontent));
 528          }
 529      }
 530  
 531      /**
 532       * Prepare this step for saving to the database.
 533       *
 534       * @param bool $isexporting Whether the step is being exported or not.
 535       * @return  object
 536       */
 537      public function to_record(bool $isexporting = false) {
 538          $record = [
 539              'id'            => $this->id,
 540              'tourid'        => $this->tourid,
 541              'title'         => $this->title,
 542              'content'       => $this->content,
 543              'contentformat' => $this->contentformat,
 544              'targettype'    => $this->targettype,
 545              'targetvalue'   => $this->targetvalue,
 546              'sortorder'     => $this->sortorder,
 547              'configdata'    => json_encode($this->config),
 548          ];
 549          if ($isexporting) {
 550              // We are exporting the step, adding files node to the json record.
 551              $record['files'] = $this->embed_files();
 552          }
 553          return (object) $record;
 554      }
 555  
 556      /**
 557       * Calculate the next sort-order value.
 558       *
 559       * @return  int
 560       */
 561      protected function calculate_sortorder() {
 562          $count = $this->get_tour()->count_steps();
 563          $this->sortorder = $count;
 564  
 565          return $this;
 566      }
 567  
 568      /**
 569       * Save the tour and it's configuration to the database.
 570       *
 571       * @param   boolean     $force      Whether to force writing to the database.
 572       * @return  $this
 573       */
 574      public function persist($force = false) {
 575          global $DB;
 576  
 577          if (!$this->dirty && !$force) {
 578              return $this;
 579          }
 580  
 581          if ($this->id) {
 582              $record = $this->to_record();
 583              $DB->update_record('tool_usertours_steps', $record);
 584          } else {
 585              $this->calculate_sortorder();
 586              $record = $this->to_record();
 587              unset($record->id);
 588              $this->id = $DB->insert_record('tool_usertours_steps', $record);
 589              $this->get_tour()->reset_step_sortorder();
 590          }
 591  
 592          $systemcontext = context_system::instance();
 593          if ($draftid = file_get_submitted_draft_itemid('content')) {
 594              // Take any files added to the stepcontent draft file area and
 595              // convert them into the proper event description file area. Also
 596              // parse the content text and replace the URLs to the draft files
 597              // with the @@PLUGIN_FILE@@ placeholder to be persisted in the DB.
 598              $this->content = file_save_draft_area_files(
 599                  $draftid,
 600                  $systemcontext->id,
 601                  'tool_usertours',
 602                  'stepcontent',
 603                  $this->id,
 604                  ['subdirs' => true],
 605                  $this->content
 606              );
 607              $DB->set_field('tool_usertours_steps', 'content', $this->content, ['id' => $this->id]);
 608          }
 609  
 610          if ($this->isimporting) {
 611              // We are importing the step, we need to create store_file from the json record.
 612              $this->extract_files();
 613          }
 614          $this->reload();
 615  
 616          // Notify of a change to the step configuration.
 617          // This must be done separately to tour change notifications.
 618          cache::notify_step_change($this->get_tourid());
 619  
 620          // Notify the cache that a tour has changed.
 621          // Tours are only stored in the cache if there are steps.
 622          // If there step count has changed for some reason, this will change the potential cache results.
 623          cache::notify_tour_change();
 624  
 625          return $this;
 626      }
 627  
 628      /**
 629       * Remove this step.
 630       */
 631      public function remove() {
 632          global $DB;
 633  
 634          if ($this->id === null) {
 635              return;
 636          }
 637  
 638          $DB->delete_records('tool_usertours_steps', array('id' => $this->id));
 639          $this->get_tour()->reset_step_sortorder();
 640  
 641          // Notify of a change to the step configuration.
 642          // This must be done separately to tour change notifications.
 643          cache::notify_step_change($this->get_id());
 644  
 645          // Notify the cache that a tour has changed.
 646          // Tours are only stored in the cache if there are steps.
 647          // If there step count has changed for some reason, this will change the potential cache results.
 648          cache::notify_tour_change();
 649      }
 650  
 651      /**
 652       * Get the list of possible placement options.
 653       *
 654       * @return  array
 655       */
 656      public function get_placement_options() {
 657          return configuration::get_placement_options(true);
 658      }
 659  
 660      /**
 661       * The list of possible configuration keys.
 662       *
 663       * @return  array
 664       */
 665      public static function get_config_keys() {
 666          return [
 667              'placement',
 668              'orphan',
 669              'backdrop',
 670              'reflex',
 671          ];
 672      }
 673  
 674      /**
 675       * Add the step configuration to the form.
 676       *
 677       * @param   MoodleQuickForm $mform      The form to add configuration to.
 678       * @return  $this
 679       */
 680      public function add_config_to_form(\MoodleQuickForm $mform) {
 681          $tour = $this->get_tour();
 682  
 683          $options = configuration::get_placement_options($tour->get_config('placement'));
 684          $mform->addElement('select', 'placement', get_string('placement', 'tool_usertours'), $options);
 685          $mform->addHelpButton('placement', 'placement', 'tool_usertours');
 686  
 687          $this->add_config_field_to_form($mform, 'orphan');
 688          $this->add_config_field_to_form($mform, 'backdrop');
 689          $this->add_config_field_to_form($mform, 'reflex');
 690  
 691          return $this;
 692      }
 693  
 694      /**
 695       * Add the specified step field configuration to the form.
 696       *
 697       * @param   MoodleQuickForm $mform      The form to add configuration to.
 698       * @param   string          $key        The key to add.
 699       * @return  $this
 700       */
 701      public function add_config_field_to_form(\MoodleQuickForm $mform, $key) {
 702          $tour = $this->get_tour();
 703  
 704          $default = (bool) $tour->get_config($key);
 705  
 706          $options = [
 707              true    => get_string('yes'),
 708              false   => get_string('no'),
 709          ];
 710  
 711          if (!isset($options[$default])) {
 712              $default = configuration::get_default_value($key);
 713          }
 714  
 715          $options = array_reverse($options, true);
 716          $options[configuration::TOURDEFAULT] = get_string('defaultvalue', 'tool_usertours', $options[$default]);
 717          $options = array_reverse($options, true);
 718  
 719          $mform->addElement('select', $key, get_string($key, 'tool_usertours'), $options);
 720          $mform->setDefault($key, configuration::TOURDEFAULT);
 721          $mform->addHelpButton($key, $key, 'tool_usertours');
 722  
 723          return $this;
 724      }
 725  
 726      /**
 727       * Prepare the configuration data for the moodle form.
 728       *
 729       * @return  object
 730       */
 731      public function prepare_data_for_form() {
 732          $data = $this->to_record();
 733          foreach (self::get_config_keys() as $key) {
 734              $data->$key = $this->get_config($key, configuration::get_step_default_value($key));
 735          }
 736  
 737          if ($this->get_targettype() !== null) {
 738              $this->get_target()->prepare_data_for_form($data);
 739          }
 740  
 741          // Prepare content for editing in a form 'editor' field type.
 742          $draftitemid = file_get_submitted_draft_itemid('tool_usertours');
 743          $systemcontext = context_system::instance();
 744          $data->content = [
 745              'format' => $data->contentformat,
 746              'itemid' => $draftitemid,
 747              'text' => file_prepare_draft_area(
 748                  $draftitemid,
 749                  $systemcontext->id,
 750                  'tool_usertours',
 751                  'stepcontent',
 752                  $this->id,
 753                  ['subdirs' => true],
 754                  $data->content
 755              ),
 756          ];
 757  
 758          return $data;
 759      }
 760  
 761      /**
 762       * Handle submission of the step editing form.
 763       *
 764       * @param   local\forms\editstep  $mform      The sumitted form.
 765       * @param   stdClass        $data       The submitted data.
 766       * @return  $this
 767       */
 768      public function handle_form_submission(local\forms\editstep &$mform, stdClass $data) {
 769          $this->set_title($data->title);
 770          $this->set_content($data->content['text'], $data->content['format']);
 771          $this->set_targettype($data->targettype);
 772  
 773          $this->set_targetvalue($this->get_target()->get_value_from_form($data));
 774  
 775          foreach (self::get_config_keys() as $key) {
 776              if (!$this->get_target()->is_setting_forced($key)) {
 777                  if (isset($data->$key)) {
 778                      $value = $data->$key;
 779                  } else {
 780                      $value = configuration::TOURDEFAULT;
 781                  }
 782                  if ($value === configuration::TOURDEFAULT) {
 783                      $this->set_config($key, null);
 784                  } else {
 785                      $this->set_config($key, $value);
 786                  }
 787              }
 788          }
 789  
 790          $this->persist();
 791  
 792          return $this;
 793      }
 794  
 795      /**
 796       * Attempt to fetch any matching langstring if the string is in the
 797       * format identifier,component.
 798       *
 799       * @deprecated since Moodle 4.0 MDL-72783. Please use helper::get_string_from_input() instead.
 800       * @param   string  $string
 801       * @return  string
 802       */
 803      public static function get_string_from_input($string) {
 804          debugging('Use of ' . __FUNCTION__ .
 805              '() have been deprecated, please update your code to use helper::get_string_from_input()', DEBUG_DEVELOPER);
 806          return helper::get_string_from_input($string);
 807      }
 808  
 809      /**
 810       * Attempt to replace PIXICON placeholder with the correct images for tour step content.
 811       *
 812       * @param string $content Tour content
 813       * @return string Processed tour content
 814       */
 815      public static function get_step_image_from_input(string $content): string {
 816          if (strpos($content, '@@PIXICON') === false) {
 817              return $content;
 818          }
 819  
 820          $content = preg_replace_callback('%@@PIXICON::(?P<identifier>([^::]*))::(?P<component>([^@@]*))@@%',
 821              function(array $matches) {
 822                  global $OUTPUT;
 823                  $component = $matches['component'];
 824                  if ($component == 'moodle') {
 825                      $component = 'core';
 826                  }
 827                  return \html_writer::img($OUTPUT->image_url($matches['identifier'], $component)->out(false), '',
 828                      ['class' => 'img-fluid']);
 829              },
 830              $content
 831          );
 832  
 833          return $content;
 834      }
 835  }