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 400 and 401] [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   * Tour manager.
  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  use tool_usertours\local\forms;
  30  use tool_usertours\local\table;
  31  use core\notification;
  32  
  33  /**
  34   * Tour manager.
  35   *
  36   * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class manager {
  40  
  41      /**
  42       * @var ACTION_LISTTOURS      The action to get the list of tours.
  43       */
  44      const ACTION_LISTTOURS = 'listtours';
  45  
  46      /**
  47       * @var ACTION_NEWTOUR        The action to create a new tour.
  48       */
  49      const ACTION_NEWTOUR = 'newtour';
  50  
  51      /**
  52       * @var ACTION_EDITTOUR       The action to edit the tour.
  53       */
  54      const ACTION_EDITTOUR = 'edittour';
  55  
  56      /**
  57       * @var ACTION_MOVETOUR The action to move a tour up or down.
  58       */
  59      const ACTION_MOVETOUR = 'movetour';
  60  
  61      /**
  62       * @var ACTION_EXPORTTOUR     The action to export the tour.
  63       */
  64      const ACTION_EXPORTTOUR = 'exporttour';
  65  
  66      /**
  67       * @var ACTION_IMPORTTOUR     The action to import the tour.
  68       */
  69      const ACTION_IMPORTTOUR = 'importtour';
  70  
  71      /**
  72       * @var ACTION_DELETETOUR     The action to delete the tour.
  73       */
  74      const ACTION_DELETETOUR = 'deletetour';
  75  
  76      /**
  77       * @var ACTION_VIEWTOUR       The action to view the tour.
  78       */
  79      const ACTION_VIEWTOUR = 'viewtour';
  80  
  81      /**
  82       * @var ACTION_DUPLICATETOUR     The action to duplicate the tour.
  83       */
  84      const ACTION_DUPLICATETOUR = 'duplicatetour';
  85  
  86      /**
  87       * @var ACTION_NEWSTEP The action to create a new step.
  88       */
  89      const ACTION_NEWSTEP = 'newstep';
  90  
  91      /**
  92       * @var ACTION_EDITSTEP The action to edit step configuration.
  93       */
  94      const ACTION_EDITSTEP = 'editstep';
  95  
  96      /**
  97       * @var ACTION_MOVESTEP The action to move a step up or down.
  98       */
  99      const ACTION_MOVESTEP = 'movestep';
 100  
 101      /**
 102       * @var ACTION_DELETESTEP The action to delete a step.
 103       */
 104      const ACTION_DELETESTEP = 'deletestep';
 105  
 106      /**
 107       * @var ACTION_VIEWSTEP The action to view a step.
 108       */
 109      const ACTION_VIEWSTEP = 'viewstep';
 110  
 111      /**
 112       * @var ACTION_HIDETOUR The action to hide a tour.
 113       */
 114      const ACTION_HIDETOUR = 'hidetour';
 115  
 116      /**
 117       * @var ACTION_SHOWTOUR The action to show a tour.
 118       */
 119      const ACTION_SHOWTOUR = 'showtour';
 120  
 121      /**
 122       * @var ACTION_RESETFORALL
 123       */
 124      const ACTION_RESETFORALL = 'resetforall';
 125  
 126      /**
 127       * @var CONFIG_SHIPPED_TOUR
 128       */
 129      const CONFIG_SHIPPED_TOUR = 'shipped_tour';
 130  
 131      /**
 132       * @var CONFIG_SHIPPED_FILENAME
 133       */
 134      const CONFIG_SHIPPED_FILENAME = 'shipped_filename';
 135  
 136      /**
 137       * @var CONFIG_SHIPPED_VERSION
 138       */
 139      const CONFIG_SHIPPED_VERSION = 'shipped_version';
 140  
 141      /**
 142       * Helper method to initialize admin page, setting appropriate extra URL parameters
 143       *
 144       * @param string $action
 145       */
 146      protected function setup_admin_externalpage(string $action): void {
 147          admin_externalpage_setup('tool_usertours/tours', '', array_filter([
 148              'action' => $action,
 149              'id' => optional_param('id', 0, PARAM_INT),
 150              'tourid' => optional_param('tourid', 0, PARAM_INT),
 151              'direction' => optional_param('direction', 0, PARAM_INT),
 152          ]));
 153      }
 154  
 155      /**
 156       * This is the entry point for this controller class.
 157       *
 158       * @param   string  $action     The action to perform.
 159       */
 160      public function execute($action) {
 161          global $PAGE;
 162          $this->setup_admin_externalpage($action);
 163          $PAGE->set_primary_active_tab('siteadminnode');
 164  
 165          // Add the main content.
 166          switch($action) {
 167              case self::ACTION_NEWTOUR:
 168              case self::ACTION_EDITTOUR:
 169                  $this->edit_tour(optional_param('id', null, PARAM_INT));
 170                  break;
 171  
 172              case self::ACTION_MOVETOUR:
 173                  $this->move_tour(required_param('id', PARAM_INT));
 174                  break;
 175  
 176              case self::ACTION_EXPORTTOUR:
 177                  $this->export_tour(required_param('id', PARAM_INT));
 178                  break;
 179  
 180              case self::ACTION_IMPORTTOUR:
 181                  $this->import_tour();
 182                  break;
 183  
 184              case self::ACTION_VIEWTOUR:
 185                  $this->view_tour(required_param('id', PARAM_INT));
 186                  break;
 187  
 188              case self::ACTION_DUPLICATETOUR:
 189                  $this->duplicate_tour(required_param('id', PARAM_INT));
 190                  break;
 191  
 192              case self::ACTION_HIDETOUR:
 193                  $this->hide_tour(required_param('id', PARAM_INT));
 194                  break;
 195  
 196              case self::ACTION_SHOWTOUR:
 197                  $this->show_tour(required_param('id', PARAM_INT));
 198                  break;
 199  
 200              case self::ACTION_DELETETOUR:
 201                  $this->delete_tour(required_param('id', PARAM_INT));
 202                  break;
 203  
 204              case self::ACTION_RESETFORALL:
 205                  $this->reset_tour_for_all(required_param('id', PARAM_INT));
 206                  break;
 207  
 208              case self::ACTION_NEWSTEP:
 209              case self::ACTION_EDITSTEP:
 210                  $this->edit_step(optional_param('id', null, PARAM_INT));
 211                  break;
 212  
 213              case self::ACTION_MOVESTEP:
 214                  $this->move_step(required_param('id', PARAM_INT));
 215                  break;
 216  
 217              case self::ACTION_DELETESTEP:
 218                  $this->delete_step(required_param('id', PARAM_INT));
 219                  break;
 220  
 221              case self::ACTION_LISTTOURS:
 222              default:
 223                  $this->print_tour_list();
 224                  break;
 225          }
 226      }
 227  
 228      /**
 229       * Print out the page header.
 230       *
 231       * @param   string  $title     The title to display.
 232       */
 233      protected function header($title = null) {
 234          global $OUTPUT;
 235  
 236          // Print the page heading.
 237          echo $OUTPUT->header();
 238  
 239          if ($title === null) {
 240              $title = get_string('tours', 'tool_usertours');
 241          }
 242  
 243          echo $OUTPUT->heading($title);
 244      }
 245  
 246      /**
 247       * Print out the page footer.
 248       *
 249       * @return void
 250       */
 251      protected function footer() {
 252          global $OUTPUT;
 253  
 254          echo $OUTPUT->footer();
 255      }
 256  
 257      /**
 258       * Print the the list of tours.
 259       */
 260      protected function print_tour_list() {
 261          global $PAGE, $OUTPUT;
 262  
 263          $this->header();
 264          echo \html_writer::span(get_string('tourlist_explanation', 'tool_usertours'));
 265          $table = new table\tour_list();
 266          $tours = helper::get_tours();
 267          foreach ($tours as $tour) {
 268              $table->add_data_keyed($table->format_row($tour));
 269          }
 270  
 271          $table->finish_output();
 272          $actions = [
 273              (object) [
 274                  'link'  => helper::get_edit_tour_link(),
 275                  'linkproperties' => [],
 276                  'img'   => 'b/tour-new',
 277                  'title' => get_string('newtour', 'tool_usertours'),
 278              ],
 279              (object) [
 280                  'link'  => helper::get_import_tour_link(),
 281                  'linkproperties' => [],
 282                  'img'   => 'b/tour-import',
 283                  'title' => get_string('importtour', 'tool_usertours'),
 284              ],
 285              (object) [
 286                  'link'  => new \moodle_url('https://archive.moodle.net/tours'),
 287                  'linkproperties' => [
 288                          'target' => '_blank',
 289                      ],
 290                  'img'   => 'b/tour-shared',
 291                  'title' => get_string('sharedtourslink', 'tool_usertours'),
 292              ],
 293          ];
 294  
 295          echo \html_writer::start_tag('div', [
 296                  'class' => 'tour-actions',
 297              ]);
 298  
 299          echo \html_writer::start_tag('ul');
 300          foreach ($actions as $config) {
 301              $action = \html_writer::start_tag('li');
 302              $linkproperties = $config->linkproperties;
 303              $linkproperties['href'] = $config->link;
 304              $action .= \html_writer::start_tag('a', $linkproperties);
 305              $action .= $OUTPUT->pix_icon($config->img, $config->title, 'tool_usertours');
 306              $action .= \html_writer::div($config->title);
 307              $action .= \html_writer::end_tag('a');
 308              $action .= \html_writer::end_tag('li');
 309              echo $action;
 310          }
 311          echo \html_writer::end_tag('ul');
 312          echo \html_writer::end_tag('div');
 313  
 314          // JS for Tour management.
 315          $PAGE->requires->js_call_amd('tool_usertours/managetours', 'setup');
 316          $this->footer();
 317      }
 318  
 319      /**
 320       * Return the edit tour link.
 321       *
 322       * @param   int         $id     The ID of the tour
 323       * @return string
 324       */
 325      protected function get_edit_tour_link($id = null) {
 326          $addlink = helper::get_edit_tour_link($id);
 327          return \html_writer::link($addlink, get_string('newtour', 'tool_usertours'));
 328      }
 329  
 330      /**
 331       * Print the edit tour link.
 332       *
 333       * @param   int         $id     The ID of the tour
 334       */
 335      protected function print_edit_tour_link($id = null) {
 336          echo $this->get_edit_tour_link($id);
 337      }
 338  
 339      /**
 340       * Get the import tour link.
 341       *
 342       * @return string
 343       */
 344      protected function get_import_tour_link() {
 345          $importlink = helper::get_import_tour_link();
 346          return \html_writer::link($importlink, get_string('importtour', 'tool_usertours'));
 347      }
 348  
 349      /**
 350       * Print the edit tour page.
 351       *
 352       * @param   int         $id     The ID of the tour
 353       */
 354      protected function edit_tour($id = null) {
 355          global $PAGE;
 356          if ($id) {
 357              $tour = tour::instance($id);
 358              $PAGE->navbar->add(helper::get_string_from_input($tour->get_name()), $tour->get_edit_link());
 359  
 360          } else {
 361              $tour = new tour();
 362              $PAGE->navbar->add(get_string('newtour', 'tool_usertours'), $tour->get_edit_link());
 363          }
 364  
 365          $form = new forms\edittour($tour);
 366  
 367          if ($form->is_cancelled()) {
 368              redirect(helper::get_list_tour_link());
 369          } else if ($data = $form->get_data()) {
 370              // Creating a new tour.
 371              $tour->set_name($data->name);
 372              $tour->set_description($data->description);
 373              $tour->set_pathmatch($data->pathmatch);
 374              $tour->set_enabled(!empty($data->enabled));
 375              $tour->set_endtourlabel($data->endtourlabel);
 376              $tour->set_display_step_numbers(!empty($data->displaystepnumbers));
 377  
 378              foreach (configuration::get_defaultable_keys() as $key) {
 379                  $tour->set_config($key, $data->$key);
 380              }
 381  
 382              // Save filter values.
 383              foreach (helper::get_all_filters() as $filterclass) {
 384                  $filterclass::save_filter_values_from_form($tour, $data);
 385              }
 386  
 387              $tour->persist();
 388  
 389              redirect(helper::get_list_tour_link());
 390          } else {
 391              if (empty($tour)) {
 392                  $this->header('newtour');
 393              } else {
 394                  if (!empty($tour->get_config(self::CONFIG_SHIPPED_TOUR))) {
 395                      notification::add(get_string('modifyshippedtourwarning', 'tool_usertours'), notification::WARNING);
 396                  }
 397  
 398                  $tourname = !empty($tour->get_name()) ? helper::get_string_from_input($tour->get_name()) : '';
 399                  $this->header($tourname);
 400                  $data = $tour->prepare_data_for_form();
 401  
 402                  // Prepare filter values for the form.
 403                  foreach (helper::get_all_filters() as $filterclass) {
 404                      $filterclass::prepare_filter_values_for_form($tour, $data);
 405                  }
 406                  $form->set_data($data);
 407              }
 408  
 409              $form->display();
 410              $this->footer();
 411          }
 412      }
 413  
 414      /**
 415       * Print the export tour page.
 416       *
 417       * @param   int         $id     The ID of the tour
 418       */
 419      protected function export_tour($id) {
 420          $tour = tour::instance($id);
 421  
 422          // Grab the full data record.
 423          $export = $tour->to_record();
 424  
 425          // Remove the id.
 426          unset($export->id);
 427  
 428          // Set the version.
 429          $export->version = get_config('tool_usertours', 'version');
 430  
 431          // Step export.
 432          $export->steps = [];
 433          foreach ($tour->get_steps() as $step) {
 434              $record = $step->to_record(true);
 435              unset($record->id);
 436              unset($record->tourid);
 437  
 438              $export->steps[] = $record;
 439          }
 440  
 441          $exportstring = json_encode($export);
 442  
 443          $filename = 'tour_export_' . $tour->get_id() . '_' . time() . '.json';
 444  
 445          // Force download.
 446          send_file($exportstring, $filename, 0, 0, true, true);
 447      }
 448  
 449      /**
 450       * Handle tour import.
 451       */
 452      protected function import_tour() {
 453          global $PAGE;
 454          $PAGE->navbar->add(get_string('importtour', 'tool_usertours'), helper::get_import_tour_link());
 455  
 456          $form = new forms\importtour();
 457  
 458          if ($form->is_cancelled()) {
 459              redirect(helper::get_list_tour_link());
 460          } else if ($form->get_data()) {
 461              // Importing a tour.
 462              $tourconfigraw = $form->get_file_content('tourconfig');
 463              $tour = self::import_tour_from_json($tourconfigraw);
 464  
 465              redirect($tour->get_view_link());
 466          } else {
 467              $this->header();
 468              $form->display();
 469              $this->footer();
 470          }
 471      }
 472  
 473      /**
 474       * Print the view tour page.
 475       *
 476       * @param   int         $tourid     The ID of the tour to display.
 477       */
 478      protected function view_tour($tourid) {
 479          global $PAGE;
 480          $tour = helper::get_tour($tourid);
 481          $tourname = helper::get_string_from_input($tour->get_name());
 482  
 483          $PAGE->navbar->add($tourname, $tour->get_view_link());
 484  
 485          $this->header($tourname);
 486          echo \html_writer::span(get_string('viewtour_info', 'tool_usertours', [
 487                  'tourname'  => $tourname,
 488                  'path'      => $tour->get_pathmatch(),
 489              ]));
 490          echo \html_writer::div(get_string('viewtour_edit', 'tool_usertours', [
 491                  'editlink'  => $tour->get_edit_link()->out(),
 492                  'resetlink' => $tour->get_reset_link()->out(),
 493              ]));
 494  
 495          $table = new table\step_list($tourid);
 496          foreach ($tour->get_steps() as $step) {
 497              $table->add_data_keyed($table->format_row($step));
 498          }
 499  
 500          $table->finish_output();
 501          $this->print_edit_step_link($tourid);
 502  
 503          // JS for Step management.
 504          $PAGE->requires->js_call_amd('tool_usertours/managesteps', 'setup');
 505  
 506          $this->footer();
 507      }
 508  
 509      /**
 510       * Duplicate an existing tour.
 511       *
 512       * @param   int         $tourid     The ID of the tour to duplicate.
 513       */
 514      protected function duplicate_tour($tourid) {
 515          $tour = helper::get_tour($tourid);
 516  
 517          $export = $tour->to_record();
 518          // Remove the id.
 519          unset($export->id);
 520  
 521          // Set the version.
 522          $export->version = get_config('tool_usertours', 'version');
 523  
 524          $export->name = get_string('duplicatetour_name', 'tool_usertours', $export->name);
 525  
 526          // Step export.
 527          $export->steps = [];
 528          foreach ($tour->get_steps() as $step) {
 529              $record = $step->to_record(true);
 530              unset($record->id);
 531              unset($record->tourid);
 532  
 533              $export->steps[] = $record;
 534          }
 535  
 536          $exportstring = json_encode($export);
 537          $newtour = self::import_tour_from_json($exportstring);
 538  
 539          redirect($newtour->get_view_link());
 540      }
 541  
 542      /**
 543       * Show the tour.
 544       *
 545       * @param   int         $tourid     The ID of the tour to display.
 546       */
 547      protected function show_tour($tourid) {
 548          $this->show_hide_tour($tourid, 1);
 549      }
 550  
 551      /**
 552       * Hide the tour.
 553       *
 554       * @param   int         $tourid     The ID of the tour to display.
 555       */
 556      protected function hide_tour($tourid) {
 557          $this->show_hide_tour($tourid, 0);
 558      }
 559  
 560      /**
 561       * Show or Hide the tour.
 562       *
 563       * @param   int         $tourid     The ID of the tour to display.
 564       * @param   int         $visibility The intended visibility.
 565       */
 566      protected function show_hide_tour($tourid, $visibility) {
 567          global $DB;
 568  
 569          require_sesskey();
 570  
 571          $tour = $DB->get_record('tool_usertours_tours', array('id' => $tourid));
 572          $tour->enabled = $visibility;
 573          $DB->update_record('tool_usertours_tours', $tour);
 574  
 575          redirect(helper::get_list_tour_link());
 576      }
 577  
 578      /**
 579       * Delete the tour.
 580       *
 581       * @param   int         $tourid     The ID of the tour to remove.
 582       */
 583      protected function delete_tour($tourid) {
 584          require_sesskey();
 585  
 586          $tour = tour::instance($tourid);
 587          $tour->remove();
 588  
 589          redirect(helper::get_list_tour_link());
 590      }
 591  
 592      /**
 593       * Reset the tour state for all users.
 594       *
 595       * @param   int         $tourid     The ID of the tour to remove.
 596       */
 597      protected function reset_tour_for_all($tourid) {
 598          require_sesskey();
 599  
 600          $tour = tour::instance($tourid);
 601          $tour->mark_major_change();
 602  
 603          redirect(helper::get_view_tour_link($tourid), get_string('tour_resetforall', 'tool_usertours'));
 604      }
 605  
 606      /**
 607       * Get all tours for the current page URL.
 608       *
 609       * @param   bool        $reset      Forcibly update the current tours
 610       * @return  array
 611       */
 612      public static function get_current_tours($reset = false): array {
 613          global $PAGE;
 614  
 615          static $tours = false;
 616  
 617          if ($tours === false || $reset) {
 618              $tours = self::get_matching_tours($PAGE->url);
 619          }
 620  
 621          return $tours;
 622      }
 623  
 624      /**
 625       * Get all tours matching the specified URL.
 626       *
 627       * @param   moodle_url  $pageurl        The URL to match.
 628       * @return  array
 629       */
 630      public static function get_matching_tours(\moodle_url $pageurl): array {
 631          global $PAGE;
 632  
 633          if (\core_user::awaiting_action()) {
 634              // User not fully ready to use the site. Don't show any tours, we need the user to get properly set up so
 635              // that all require_login() and other bits work as expected.
 636              return [];
 637          }
 638  
 639          $tours = cache::get_matching_tourdata($pageurl);
 640  
 641          $matches = [];
 642          if ($tours) {
 643              $filters = helper::get_all_filters();
 644              foreach ($tours as $record) {
 645                  $tour = tour::load_from_record($record);
 646                  if ($tour->is_enabled() && $tour->matches_all_filters($PAGE->context, $filters)) {
 647                      $matches[] = $tour;
 648                  }
 649              }
 650          }
 651  
 652          return $matches;
 653      }
 654  
 655      /**
 656       * Import the provided tour JSON.
 657       *
 658       * @param   string      $json           The tour configuration.
 659       * @return  tour
 660       */
 661      public static function import_tour_from_json($json) {
 662          $tourconfig = json_decode($json);
 663  
 664          // We do not use this yet - we may do in the future.
 665          unset($tourconfig->version);
 666  
 667          $steps = $tourconfig->steps;
 668          unset($tourconfig->steps);
 669  
 670          $tourconfig->id = null;
 671          $tourconfig->sortorder = null;
 672          $tour = tour::load_from_record($tourconfig, true);
 673          $tour->persist(true);
 674  
 675          // Ensure that steps are orderered by their sortorder.
 676          \core_collator::asort_objects_by_property($steps, 'sortorder', \core_collator::SORT_NUMERIC);
 677  
 678          foreach ($steps as $stepconfig) {
 679              $stepconfig->id = null;
 680              $stepconfig->tourid = $tour->get_id();
 681              $step = step::load_from_record($stepconfig, true, true);
 682              $step->persist(true);
 683          }
 684  
 685          return $tour;
 686      }
 687  
 688      /**
 689       * Helper to fetch the renderer.
 690       *
 691       * @return  renderer
 692       */
 693      protected function get_renderer() {
 694          global $PAGE;
 695          return $PAGE->get_renderer('tool_usertours');
 696      }
 697  
 698      /**
 699       * Print the edit step link.
 700       *
 701       * @param   int     $tourid     The ID of the tour.
 702       * @param   int     $stepid     The ID of the step.
 703       * @return  string
 704       */
 705      protected function print_edit_step_link($tourid, $stepid = null) {
 706          $addlink = helper::get_edit_step_link($tourid, $stepid);
 707          $attributes = [];
 708          if (empty($stepid)) {
 709              $attributes['class'] = 'createstep';
 710          }
 711          echo \html_writer::link($addlink, get_string('newstep', 'tool_usertours'), $attributes);
 712      }
 713  
 714      /**
 715       * Display the edit step form for the specified step.
 716       *
 717       * @param   int     $id     The step to edit.
 718       */
 719      protected function edit_step($id) {
 720          global $PAGE;
 721  
 722          if (isset($id)) {
 723              $step = step::instance($id);
 724          } else {
 725              $step = new step();
 726              $step->set_tourid(required_param('tourid', PARAM_INT));
 727          }
 728  
 729          $tour = $step->get_tour();
 730  
 731          if (!empty($tour->get_config(self::CONFIG_SHIPPED_TOUR))) {
 732              notification::add(get_string('modifyshippedtourwarning', 'tool_usertours'), notification::WARNING);
 733          }
 734  
 735          $PAGE->navbar->add(helper::get_string_from_input($tour->get_name()), $tour->get_view_link());
 736          if (isset($id)) {
 737              $PAGE->navbar->add(helper::get_string_from_input($step->get_title()), $step->get_edit_link());
 738          } else {
 739              $PAGE->navbar->add(get_string('newstep', 'tool_usertours'), $step->get_edit_link());
 740          }
 741  
 742          $form = new forms\editstep($step->get_edit_link(), $step);
 743          if ($form->is_cancelled()) {
 744              redirect($step->get_tour()->get_view_link());
 745          } else if ($data = $form->get_data()) {
 746              $step->handle_form_submission($form, $data);
 747              $step->get_tour()->reset_step_sortorder();
 748              redirect($step->get_tour()->get_view_link());
 749          } else {
 750              if (empty($id)) {
 751                  $this->header(get_string('newstep', 'tool_usertours'));
 752              } else {
 753                  $this->header(get_string('editstep', 'tool_usertours', helper::get_string_from_input($step->get_title())));
 754              }
 755              $form->set_data($step->prepare_data_for_form());
 756  
 757              $form->display();
 758              $this->footer();
 759          }
 760      }
 761  
 762      /**
 763       * Move a tour up or down and redirect once complete.
 764       *
 765       * @param   int     $id     The tour to move.
 766       */
 767      protected function move_tour($id) {
 768          require_sesskey();
 769  
 770          $direction = required_param('direction', PARAM_INT);
 771  
 772          $tour = tour::instance($id);
 773          self::_move_tour($tour, $direction);
 774  
 775          redirect(helper::get_list_tour_link());
 776      }
 777  
 778      /**
 779       * Move a tour up or down.
 780       *
 781       * @param   tour    $tour   The tour to move.
 782       *
 783       * @param   int     $direction
 784       */
 785      protected static function _move_tour(tour $tour, $direction) {
 786          // We can't move the first tour higher, nor the last tour any lower.
 787          if (($tour->is_first_tour() && $direction == helper::MOVE_UP) ||
 788                  ($tour->is_last_tour() && $direction == helper::MOVE_DOWN)) {
 789  
 790              return;
 791          }
 792  
 793          $currentsortorder   = $tour->get_sortorder();
 794          $targetsortorder    = $currentsortorder + $direction;
 795  
 796          $swapwith = helper::get_tour_from_sortorder($targetsortorder);
 797  
 798          // Set the sort order to something out of the way.
 799          $tour->set_sortorder(-1);
 800          $tour->persist();
 801  
 802          // Swap the two sort orders.
 803          $swapwith->set_sortorder($currentsortorder);
 804          $swapwith->persist();
 805  
 806          $tour->set_sortorder($targetsortorder);
 807          $tour->persist();
 808      }
 809  
 810      /**
 811       * Move a step up or down.
 812       *
 813       * @param   int     $id     The step to move.
 814       */
 815      protected function move_step($id) {
 816          require_sesskey();
 817  
 818          $direction = required_param('direction', PARAM_INT);
 819  
 820          $step = step::instance($id);
 821          $currentsortorder   = $step->get_sortorder();
 822          $targetsortorder    = $currentsortorder + $direction;
 823  
 824          $tour = $step->get_tour();
 825          $swapwith = helper::get_step_from_sortorder($tour->get_id(), $targetsortorder);
 826  
 827          // Set the sort order to something out of the way.
 828          $step->set_sortorder(-1);
 829          $step->persist();
 830  
 831          // Swap the two sort orders.
 832          $swapwith->set_sortorder($currentsortorder);
 833          $swapwith->persist();
 834  
 835          $step->set_sortorder($targetsortorder);
 836          $step->persist();
 837  
 838          // Reset the sort order.
 839          $tour->reset_step_sortorder();
 840          redirect($tour->get_view_link());
 841      }
 842  
 843      /**
 844       * Delete the step.
 845       *
 846       * @param   int         $stepid     The ID of the step to remove.
 847       */
 848      protected function delete_step($stepid) {
 849          require_sesskey();
 850  
 851          $step = step::instance($stepid);
 852          $tour = $step->get_tour();
 853  
 854          $step->remove();
 855          redirect($tour->get_view_link());
 856      }
 857  
 858      /**
 859       * Make sure all of the default tours that are shipped with Moodle are created
 860       * and up to date with the latest version.
 861       */
 862      public static function update_shipped_tours() {
 863          global $DB, $CFG;
 864  
 865          // A list of tours that are shipped with Moodle. They are in
 866          // the format filename => version. The version value needs to
 867          // be increased if the tour has been updated.
 868          $shippedtours = [
 869              '40_tour_navigation_dashboard.json' => 4,
 870              '40_tour_navigation_mycourse.json' => 5,
 871              '40_tour_navigation_course_teacher.json' => 3,
 872              '40_tour_navigation_course_student.json' => 3,
 873          ];
 874  
 875          // These are tours that we used to ship but don't ship any longer.
 876          // We do not remove them, but we do disable them.
 877          $unshippedtours = [
 878              // Formerly included in Moodle 3.2.0.
 879              'boost_administrator.json' => 1,
 880              'boost_course_view.json' => 1,
 881  
 882              // Formerly included in Moodle 3.6.0.
 883              '36_dashboard.json' => 3,
 884              '36_messaging.json' => 3,
 885  
 886              // Formerly included in Moodle 3.11.0.
 887              '311_activity_information_activity_page_student.json' => 2,
 888              '311_activity_information_activity_page_teacher.json' => 2,
 889              '311_activity_information_course_page_student.json' => 2,
 890              '311_activity_information_course_page_teacher.json' => 2,
 891          ];
 892  
 893          $existingtourrecords = $DB->get_recordset('tool_usertours_tours');
 894  
 895          // Get all of the existing shipped tours and check if they need to be
 896          // updated.
 897          foreach ($existingtourrecords as $tourrecord) {
 898              $tour = tour::load_from_record($tourrecord);
 899  
 900              if (!empty($tour->get_config(self::CONFIG_SHIPPED_TOUR))) {
 901                  $filename = $tour->get_config(self::CONFIG_SHIPPED_FILENAME);
 902                  $version = $tour->get_config(self::CONFIG_SHIPPED_VERSION);
 903  
 904                  // If we know about this tour (otherwise leave it as is).
 905                  if (isset($shippedtours[$filename])) {
 906                      // And the version in the DB is an older version.
 907                      if ($version < $shippedtours[$filename]) {
 908                          // Remove the old version because it's been updated
 909                          // and needs to be recreated.
 910                          $tour->remove();
 911                      } else {
 912                          // The tour has not been updated so we don't need to
 913                          // do anything with it.
 914                          unset($shippedtours[$filename]);
 915                      }
 916                  }
 917  
 918                  if (isset($unshippedtours[$filename])) {
 919                      if ($version <= $unshippedtours[$filename]) {
 920                          $tour = tour::instance($tour->get_id());
 921                          $tour->set_enabled(tour::DISABLED);
 922                          $tour->persist();
 923                      }
 924                  }
 925              }
 926          }
 927          $existingtourrecords->close();
 928  
 929          // Ensure we correct the sortorder in any existing tours, prior to adding latest shipped tours.
 930          helper::reset_tour_sortorder();
 931  
 932          foreach (array_reverse($shippedtours) as $filename => $version) {
 933              $filepath = $CFG->dirroot . "/{$CFG->admin}/tool/usertours/tours/" . $filename;
 934              $tourjson = file_get_contents($filepath);
 935              $tour = self::import_tour_from_json($tourjson);
 936  
 937              // Set some additional config data to record that this tour was
 938              // added as a shipped tour.
 939              $tour->set_config(self::CONFIG_SHIPPED_TOUR, true);
 940              $tour->set_config(self::CONFIG_SHIPPED_FILENAME, $filename);
 941              $tour->set_config(self::CONFIG_SHIPPED_VERSION, $version);
 942  
 943              // Bump new tours to the top of the list.
 944              while ($tour->get_sortorder() > 0) {
 945                  self::_move_tour($tour, helper::MOVE_UP);
 946              }
 947  
 948              if (defined('BEHAT_SITE_RUNNING') || (defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {
 949                  // Disable this tour if this is behat or phpunit.
 950                  $tour->set_enabled(false);
 951              }
 952  
 953              $tour->persist();
 954          }
 955      }
 956  }