Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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          $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 the first tour matching the current page URL.
 602       *
 603       * @param   bool        $reset      Forcibly update the current tour
 604       * @return  tour
 605       */
 606      public static function get_current_tour($reset = false) {
 607          global $PAGE;
 608  
 609          static $tour = false;
 610  
 611          if ($tour === false || $reset) {
 612              $tour = self::get_matching_tours($PAGE->url);
 613          }
 614  
 615          return $tour;
 616      }
 617  
 618      /**
 619       * Get the first tour matching the specified URL.
 620       *
 621       * @param   moodle_url  $pageurl        The URL to match.
 622       * @return  tour
 623       */
 624      public static function get_matching_tours(\moodle_url $pageurl) {
 625          global $PAGE;
 626  
 627          $tours = cache::get_matching_tourdata($pageurl);
 628  
 629          foreach ($tours as $record) {
 630              $tour = tour::load_from_record($record);
 631              if ($tour->is_enabled() && $tour->matches_all_filters($PAGE->context)) {
 632                  return $tour;
 633              }
 634          }
 635  
 636          return null;
 637      }
 638  
 639      /**
 640       * Import the provided tour JSON.
 641       *
 642       * @param   string      $json           The tour configuration.
 643       * @return  tour
 644       */
 645      public static function import_tour_from_json($json) {
 646          $tourconfig = json_decode($json);
 647  
 648          // We do not use this yet - we may do in the future.
 649          unset($tourconfig->version);
 650  
 651          $steps = $tourconfig->steps;
 652          unset($tourconfig->steps);
 653  
 654          $tourconfig->id = null;
 655          $tourconfig->sortorder = null;
 656          $tour = tour::load_from_record($tourconfig, true);
 657          $tour->persist(true);
 658  
 659          // Ensure that steps are orderered by their sortorder.
 660          \core_collator::asort_objects_by_property($steps, 'sortorder', \core_collator::SORT_NUMERIC);
 661  
 662          foreach ($steps as $stepconfig) {
 663              $stepconfig->id = null;
 664              $stepconfig->tourid = $tour->get_id();
 665              $step = step::load_from_record($stepconfig, true);
 666              $step->persist(true);
 667          }
 668  
 669          return $tour;
 670      }
 671  
 672      /**
 673       * Helper to fetch the renderer.
 674       *
 675       * @return  renderer
 676       */
 677      protected function get_renderer() {
 678          global $PAGE;
 679          return $PAGE->get_renderer('tool_usertours');
 680      }
 681  
 682      /**
 683       * Print the edit step link.
 684       *
 685       * @param   int     $tourid     The ID of the tour.
 686       * @param   int     $stepid     The ID of the step.
 687       * @return  string
 688       */
 689      protected function print_edit_step_link($tourid, $stepid = null) {
 690          $addlink = helper::get_edit_step_link($tourid, $stepid);
 691          $attributes = [];
 692          if (empty($stepid)) {
 693              $attributes['class'] = 'createstep';
 694          }
 695          echo \html_writer::link($addlink, get_string('newstep', 'tool_usertours'), $attributes);
 696      }
 697  
 698      /**
 699       * Display the edit step form for the specified step.
 700       *
 701       * @param   int     $id     The step to edit.
 702       */
 703      protected function edit_step($id) {
 704          global $PAGE;
 705  
 706          if (isset($id)) {
 707              $step = step::instance($id);
 708          } else {
 709              $step = new step();
 710              $step->set_tourid(required_param('tourid', PARAM_INT));
 711          }
 712  
 713          $tour = $step->get_tour();
 714  
 715          if (!empty($tour->get_config(self::CONFIG_SHIPPED_TOUR))) {
 716              notification::add(get_string('modifyshippedtourwarning', 'tool_usertours'), notification::WARNING);
 717          }
 718  
 719          $PAGE->navbar->add($tour->get_name(), $tour->get_view_link());
 720          if (isset($id)) {
 721              $PAGE->navbar->add($step->get_title(), $step->get_edit_link());
 722          } else {
 723              $PAGE->navbar->add(get_string('newstep', 'tool_usertours'), $step->get_edit_link());
 724          }
 725  
 726          $form = new forms\editstep($step->get_edit_link(), $step);
 727          if ($form->is_cancelled()) {
 728              redirect($step->get_tour()->get_view_link());
 729          } else if ($data = $form->get_data()) {
 730              $step->handle_form_submission($form, $data);
 731              $step->get_tour()->reset_step_sortorder();
 732              redirect($step->get_tour()->get_view_link());
 733          } else {
 734              if (empty($id)) {
 735                  $this->header(get_string('newstep', 'tool_usertours'));
 736              } else {
 737                  $this->header(get_string('editstep', 'tool_usertours', $step->get_title()));
 738              }
 739              $form->set_data($step->prepare_data_for_form());
 740  
 741              $form->display();
 742              $this->footer();
 743          }
 744      }
 745  
 746      /**
 747       * Move a tour up or down and redirect once complete.
 748       *
 749       * @param   int     $id     The tour to move.
 750       */
 751      protected function move_tour($id) {
 752          require_sesskey();
 753  
 754          $direction = required_param('direction', PARAM_INT);
 755  
 756          $tour = tour::instance($id);
 757          self::_move_tour($tour, $direction);
 758  
 759          redirect(helper::get_list_tour_link());
 760      }
 761  
 762      /**
 763       * Move a tour up or down.
 764       *
 765       * @param   tour    $tour   The tour to move.
 766       *
 767       * @param   int     $direction
 768       */
 769      protected static function _move_tour(tour $tour, $direction) {
 770          // We can't move the first tour higher, nor the last tour any lower.
 771          if (($tour->is_first_tour() && $direction == helper::MOVE_UP) ||
 772                  ($tour->is_last_tour() && $direction == helper::MOVE_DOWN)) {
 773  
 774              return;
 775          }
 776  
 777          $currentsortorder   = $tour->get_sortorder();
 778          $targetsortorder    = $currentsortorder + $direction;
 779  
 780          $swapwith = helper::get_tour_from_sortorder($targetsortorder);
 781  
 782          // Set the sort order to something out of the way.
 783          $tour->set_sortorder(-1);
 784          $tour->persist();
 785  
 786          // Swap the two sort orders.
 787          $swapwith->set_sortorder($currentsortorder);
 788          $swapwith->persist();
 789  
 790          $tour->set_sortorder($targetsortorder);
 791          $tour->persist();
 792      }
 793  
 794      /**
 795       * Move a step up or down.
 796       *
 797       * @param   int     $id     The step to move.
 798       */
 799      protected function move_step($id) {
 800          require_sesskey();
 801  
 802          $direction = required_param('direction', PARAM_INT);
 803  
 804          $step = step::instance($id);
 805          $currentsortorder   = $step->get_sortorder();
 806          $targetsortorder    = $currentsortorder + $direction;
 807  
 808          $tour = $step->get_tour();
 809          $swapwith = helper::get_step_from_sortorder($tour->get_id(), $targetsortorder);
 810  
 811          // Set the sort order to something out of the way.
 812          $step->set_sortorder(-1);
 813          $step->persist();
 814  
 815          // Swap the two sort orders.
 816          $swapwith->set_sortorder($currentsortorder);
 817          $swapwith->persist();
 818  
 819          $step->set_sortorder($targetsortorder);
 820          $step->persist();
 821  
 822          // Reset the sort order.
 823          $tour->reset_step_sortorder();
 824          redirect($tour->get_view_link());
 825      }
 826  
 827      /**
 828       * Delete the step.
 829       *
 830       * @param   int         $stepid     The ID of the step to remove.
 831       */
 832      protected function delete_step($stepid) {
 833          require_sesskey();
 834  
 835          $step = step::instance($stepid);
 836          $tour = $step->get_tour();
 837  
 838          $step->remove();
 839          redirect($tour->get_view_link());
 840      }
 841  
 842      /**
 843       * Make sure all of the default tours that are shipped with Moodle are created
 844       * and up to date with the latest version.
 845       */
 846      public static function update_shipped_tours() {
 847          global $DB, $CFG;
 848  
 849          // A list of tours that are shipped with Moodle. They are in
 850          // the format filename => version. The version value needs to
 851          // be increased if the tour has been updated.
 852          $shippedtours = [
 853          ];
 854  
 855          // These are tours that we used to ship but don't ship any longer.
 856          // We do not remove them, but we do disable them.
 857          $unshippedtours = [
 858              // Formerly included in Moodle 3.2.0.
 859              'boost_administrator.json' => 1,
 860              'boost_course_view.json' => 1,
 861  
 862              // Formerly included in Moodle 3.6.0.
 863              '36_dashboard.json' => 3,
 864              '36_messaging.json' => 3,
 865          ];
 866  
 867          $existingtourrecords = $DB->get_recordset('tool_usertours_tours');
 868  
 869          // Get all of the existing shipped tours and check if they need to be
 870          // updated.
 871          foreach ($existingtourrecords as $tourrecord) {
 872              $tour = tour::load_from_record($tourrecord);
 873  
 874              if (!empty($tour->get_config(self::CONFIG_SHIPPED_TOUR))) {
 875                  $filename = $tour->get_config(self::CONFIG_SHIPPED_FILENAME);
 876                  $version = $tour->get_config(self::CONFIG_SHIPPED_VERSION);
 877  
 878                  // If we know about this tour (otherwise leave it as is).
 879                  if (isset($shippedtours[$filename])) {
 880                      // And the version in the DB is an older version.
 881                      if ($version < $shippedtours[$filename]) {
 882                          // Remove the old version because it's been updated
 883                          // and needs to be recreated.
 884                          $tour->remove();
 885                      } else {
 886                          // The tour has not been updated so we don't need to
 887                          // do anything with it.
 888                          unset($shippedtours[$filename]);
 889                      }
 890                  }
 891  
 892                  if (isset($unshippedtours[$filename])) {
 893                      if ($version <= $unshippedtours[$filename]) {
 894                          $tour = tour::instance($tour->get_id());
 895                          $tour->set_enabled(tour::DISABLED);
 896                          $tour->persist();
 897                      }
 898                  }
 899              }
 900          }
 901          $existingtourrecords->close();
 902  
 903          // Ensure we correct the sortorder in any existing tours, prior to adding latest shipped tours.
 904          helper::reset_tour_sortorder();
 905  
 906          foreach (array_reverse($shippedtours) as $filename => $version) {
 907              $filepath = $CFG->dirroot . "/{$CFG->admin}/tool/usertours/tours/" . $filename;
 908              $tourjson = file_get_contents($filepath);
 909              $tour = self::import_tour_from_json($tourjson);
 910  
 911              // Set some additional config data to record that this tour was
 912              // added as a shipped tour.
 913              $tour->set_config(self::CONFIG_SHIPPED_TOUR, true);
 914              $tour->set_config(self::CONFIG_SHIPPED_FILENAME, $filename);
 915              $tour->set_config(self::CONFIG_SHIPPED_VERSION, $version);
 916  
 917              // Bump new tours to the top of the list.
 918              while ($tour->get_sortorder() > 0) {
 919                  self::_move_tour($tour, helper::MOVE_UP);
 920              }
 921  
 922              if (defined('BEHAT_SITE_RUNNING') || (defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {
 923                  // Disable this tour if this is behat or phpunit.
 924                  $tour->set_enabled(false);
 925              }
 926  
 927              $tour->persist();
 928          }
 929      }
 930  }