Search moodle.org's
Developer Documentation

See Release Notes

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

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

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