Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 9 May 2022 (12 months).
  • Bug fixes for security issues in 3.11.x will end 14 November 2022 (18 months).
  • 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 35 and 311] [Versions 36 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

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