Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   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  namespace mod_data\local\importer;
  18  
  19  use core\notification;
  20  use mod_data\manager;
  21  use mod_data\preset;
  22  use stdClass;
  23  use html_writer;
  24  
  25  /**
  26   * Abstract class used for data preset importers
  27   *
  28   * @package    mod_data
  29   * @copyright  2022 Amaia Anabitarte <amaia@moodle.com>
  30   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31   */
  32  abstract class preset_importer {
  33  
  34      /** @var manager manager instance. */
  35      private $manager;
  36  
  37      /** @var string directory where to find the preset. */
  38      protected $directory;
  39  
  40      /** @var array fields to remove. */
  41      public $fieldstoremove;
  42  
  43      /** @var array fields to update. */
  44      public $fieldstoupdate;
  45  
  46      /** @var array fields to create. */
  47      public $fieldstocreate;
  48  
  49      /** @var array settings to be imported. */
  50      public $settings;
  51  
  52      /**
  53       * Constructor
  54       *
  55       * @param manager $manager
  56       * @param string $directory
  57       */
  58      public function __construct(manager $manager, string $directory) {
  59          $this->manager = $manager;
  60          $this->directory = $directory;
  61  
  62          // Read the preset and saved result.
  63          $this->settings = $this->get_preset_settings();
  64      }
  65  
  66      /**
  67       * Returns the name of the directory the preset is located in
  68       *
  69       * @return string
  70       */
  71      public function get_directory(): string {
  72          return basename($this->directory);
  73      }
  74  
  75      /**
  76       * Retreive the contents of a file. That file may either be in a conventional directory of the Moodle file storage
  77       *
  78       * @param \file_storage|null $filestorage . Should be null if using a conventional directory
  79       * @param \stored_file|null $fileobj the directory to look in. null if using a conventional directory
  80       * @param string|null $dir the directory to look in. null if using the Moodle file storage
  81       * @param string $filename the name of the file we want
  82       * @return string|null the contents of the file or null if the file doesn't exist.
  83       */
  84      public function get_file_contents(
  85          ?\file_storage &$filestorage,
  86          ?\stored_file &$fileobj,
  87          ?string $dir,
  88          string $filename
  89      ): ?string {
  90          if (empty($filestorage) || empty($fileobj)) {
  91              if (substr($dir, -1) != '/') {
  92                  $dir .= '/';
  93              }
  94              if (file_exists($dir.$filename)) {
  95                  return file_get_contents($dir.$filename);
  96              } else {
  97                  return null;
  98              }
  99          } else {
 100              if ($filestorage->file_exists(
 101                  DATA_PRESET_CONTEXT,
 102                  DATA_PRESET_COMPONENT,
 103                  DATA_PRESET_FILEAREA,
 104                  0,
 105                  $fileobj->get_filepath(),
 106                  $filename)
 107              ) {
 108                  $file = $filestorage->get_file(
 109                      DATA_PRESET_CONTEXT,
 110                      DATA_PRESET_COMPONENT,
 111                      DATA_PRESET_FILEAREA,
 112                      0,
 113                      $fileobj->get_filepath(),
 114                      $filename
 115                  );
 116                  return $file->get_content();
 117              } else {
 118                  return null;
 119              }
 120          }
 121      }
 122  
 123      /**
 124       * Gets the preset settings
 125       *
 126       * @return stdClass Settings to be imported.
 127       */
 128      public function get_preset_settings(): stdClass {
 129          global $CFG;
 130          require_once($CFG->libdir.'/xmlize.php');
 131  
 132          $fs = null;
 133          $fileobj = null;
 134          if (!preset::is_directory_a_preset($this->directory)) {
 135              // Maybe the user requested a preset stored in the Moodle file storage.
 136  
 137              $fs = get_file_storage();
 138              $files = $fs->get_area_files(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA);
 139  
 140              // Preset name to find will be the final element of the directory.
 141              $explodeddirectory = explode('/', $this->directory);
 142              $presettofind = end($explodeddirectory);
 143  
 144              // Now go through the available files available and see if we can find it.
 145              foreach ($files as $file) {
 146                  if (($file->is_directory() && $file->get_filepath() == '/') || !$file->is_directory()) {
 147                      continue;
 148                  }
 149                  $presetname = trim($file->get_filepath(), '/');
 150                  if ($presetname == $presettofind) {
 151                      $this->directory = $presetname;
 152                      $fileobj = $file;
 153                  }
 154              }
 155  
 156              if (empty($fileobj)) {
 157                  throw new \moodle_exception('invalidpreset', 'data', '', $this->directory);
 158              }
 159          }
 160  
 161          $allowedsettings = [
 162              'intro',
 163              'comments',
 164              'requiredentries',
 165              'requiredentriestoview',
 166              'maxentries',
 167              'rssarticles',
 168              'approval',
 169              'defaultsortdir',
 170              'defaultsort'
 171          ];
 172  
 173          $module = $this->manager->get_instance();
 174          $result = new stdClass;
 175          $result->settings = new stdClass;
 176          $result->importfields = [];
 177          $result->currentfields = $this->manager->get_field_records();
 178  
 179          // Grab XML.
 180          $presetxml = $this->get_file_contents($fs, $fileobj, $this->directory, 'preset.xml');
 181          $parsedxml = xmlize($presetxml, 0);
 182  
 183          // First, do settings. Put in user friendly array.
 184          $settingsarray = $parsedxml['preset']['#']['settings'][0]['#'];
 185          $result->settings = new StdClass();
 186          foreach ($settingsarray as $setting => $value) {
 187              if (!is_array($value) || !in_array($setting, $allowedsettings)) {
 188                  // Unsupported setting.
 189                  continue;
 190              }
 191              $result->settings->$setting = $value[0]['#'];
 192          }
 193  
 194          // Now work out fields to user friendly array.
 195          if (
 196              array_key_exists('preset', $parsedxml) &&
 197              array_key_exists('#', $parsedxml['preset']) &&
 198              array_key_exists('field', $parsedxml['preset']['#'])) {
 199              $fieldsarray = $parsedxml['preset']['#']['field'];
 200              foreach ($fieldsarray as $field) {
 201                  if (!is_array($field)) {
 202                      continue;
 203                  }
 204                  $fieldstoimport = new StdClass();
 205                  foreach ($field['#'] as $param => $value) {
 206                      if (!is_array($value)) {
 207                          continue;
 208                      }
 209                      $fieldstoimport->$param = $value[0]['#'];
 210                  }
 211                  $fieldstoimport->dataid = $module->id;
 212                  $fieldstoimport->type = clean_param($fieldstoimport->type, PARAM_ALPHA);
 213                  $result->importfields[] = $fieldstoimport;
 214              }
 215          }
 216  
 217          // Calculate default mapping.
 218          if (is_null($this->fieldstoremove) && is_null($this->fieldstocreate) && is_null($this->fieldstoupdate)) {
 219              $this->set_affected_fields($result->importfields, $result->currentfields);
 220          }
 221  
 222          // Now add the HTML templates to the settings array so we can update d.
 223          foreach (manager::TEMPLATES_LIST as $templatename => $templatefile) {
 224              $result->settings->$templatename = $this->get_file_contents(
 225                  $fs,
 226                  $fileobj,
 227                  $this->directory,
 228                  $templatefile
 229              );
 230          }
 231  
 232          $result->settings->instance = $module->id;
 233          return $result;
 234      }
 235  
 236      /**
 237       * Import the preset into the given database module
 238       *
 239       * @param bool $overwritesettings Whether to overwrite activity settings or not.
 240       * @return bool Wether the importing has been successful.
 241       */
 242      public function import(bool $overwritesettings): bool {
 243          global $DB, $OUTPUT, $CFG;
 244  
 245          $settings = $this->settings->settings;
 246          $currentfields = $this->settings->currentfields;
 247          $missingfieldtypes = [];
 248          $module = $this->manager->get_instance();
 249  
 250          foreach ($this->fieldstoupdate as $currentid => $updatable) {
 251              if ($currentid != -1 && isset($currentfields[$currentid])) {
 252                  $fieldobject = data_get_field_from_id($currentfields[$currentid]->id, $module);
 253                  $toupdate = false;
 254                  foreach ($updatable as $param => $value) {
 255                      if ($param != "id" && $fieldobject->field->$param !== $value) {
 256                          $fieldobject->field->$param = $value;
 257                      }
 258                  }
 259                  unset($fieldobject->field->similarfield);
 260                  $fieldobject->update_field();
 261                  unset($fieldobject);
 262              }
 263          }
 264  
 265          foreach ($this->fieldstocreate as $newfield) {
 266              /* Make a new field */
 267              $filepath = $CFG->dirroot."/mod/data/field/$newfield->type/field.class.php";
 268              if (!file_exists($filepath)) {
 269                  $missingfieldtypes[] = $newfield->name;
 270                  continue;
 271              }
 272              include_once($filepath);
 273  
 274              if (!isset($newfield->description)) {
 275                  $newfield->description = '';
 276              }
 277              $classname = 'data_field_' . $newfield->type;
 278              $fieldclass = new $classname($newfield, $module);
 279              $fieldclass->insert_field();
 280              unset($fieldclass);
 281          }
 282          if (!empty($missingfieldtypes)) {
 283              echo $OUTPUT->notification(get_string('missingfieldtypeimport', 'data') . html_writer::alist($missingfieldtypes));
 284          }
 285  
 286          // Get rid of all old unused data.
 287          foreach ($currentfields as $cid => $currentfield) {
 288              if (!array_key_exists($cid, $this->fieldstoupdate)) {
 289  
 290                  // Delete all information related to fields.
 291                  $todelete = data_get_field_from_id($currentfield->id, $module);
 292                  $todelete->delete_field();
 293              }
 294          }
 295  
 296          // Handle special settings here.
 297          if (!empty($settings->defaultsort)) {
 298              if (is_numeric($settings->defaultsort)) {
 299                  // Old broken value.
 300                  $settings->defaultsort = 0;
 301              } else {
 302                  $settings->defaultsort = (int)$DB->get_field(
 303                      'data_fields',
 304                      'id',
 305                      ['dataid' => $module->id, 'name' => $settings->defaultsort]
 306                  );
 307              }
 308          } else {
 309              $settings->defaultsort = 0;
 310          }
 311  
 312          // Do we want to overwrite all current database settings?
 313          if ($overwritesettings) {
 314              // All supported settings.
 315              $overwrite = array_keys((array)$settings);
 316          } else {
 317              // Only templates and sorting.
 318              $overwrite = ['singletemplate', 'listtemplate', 'listtemplateheader', 'listtemplatefooter',
 319                  'addtemplate', 'rsstemplate', 'rsstitletemplate', 'csstemplate', 'jstemplate',
 320                  'asearchtemplate', 'defaultsortdir', 'defaultsort'];
 321          }
 322  
 323          // Now overwrite current data settings.
 324          foreach ($module as $prop => $unused) {
 325              if (in_array($prop, $overwrite)) {
 326                  $module->$prop = $settings->$prop;
 327              }
 328          }
 329  
 330          data_update_instance($module);
 331  
 332          return $this->cleanup();
 333      }
 334  
 335      /**
 336       * Returns information about the fields needs to be removed, updated or created.
 337       *
 338       * @param array $newfields Array of new fields to be applied.
 339       * @param array $currentfields Array of current fields on database activity.
 340       * @return void
 341       */
 342      public function set_affected_fields(array $newfields = [], array $currentfields = []): void {
 343          $fieldstoremove = [];
 344          $fieldstocreate = [];
 345          $preservedfields = [];
 346  
 347          // Maps fields and makes new ones.
 348          if (!empty($newfields)) {
 349              // We require an injective mapping, and need to know what to protect.
 350              foreach ($newfields as $newid => $newfield) {
 351                  $preservedfieldid = optional_param("field_$newid", -1, PARAM_INT);
 352  
 353                  if (array_key_exists($preservedfieldid, $preservedfields)) {
 354                      throw new \moodle_exception('notinjectivemap', 'data');
 355                  }
 356  
 357                  if ($preservedfieldid == -1) {
 358                      // Let's check if there is any field with same type and name that we could map to.
 359                      foreach ($currentfields as $currentid => $currentfield) {
 360                          if (($currentfield->type == $newfield->type) &&
 361                              ($currentfield->name == $newfield->name) && !array_key_exists($currentid, $preservedfields)) {
 362                              // We found a possible default map.
 363                              $preservedfieldid = $currentid;
 364                              $preservedfields[$currentid] = $newfield;
 365                          }
 366                      }
 367                  }
 368                  if ($preservedfieldid == -1) {
 369                      // We need to create a new field.
 370                      $fieldstocreate[] = $newfield;
 371                  } else {
 372                      $preservedfields[$preservedfieldid] = $newfield;
 373                  }
 374              }
 375          }
 376  
 377          foreach ($currentfields as $currentid => $currentfield) {
 378              if (!array_key_exists($currentid, $preservedfields)) {
 379                  $fieldstoremove[] = $currentfield;
 380              }
 381          }
 382  
 383          $this->fieldstocreate = $fieldstocreate;
 384          $this->fieldstoremove = $fieldstoremove;
 385          $this->fieldstoupdate = $preservedfields;
 386      }
 387  
 388      /**
 389       * Any clean up routines should go here
 390       *
 391       * @return bool Wether the preset has been successfully cleaned up.
 392       */
 393      public function cleanup(): bool {
 394          return true;
 395      }
 396  
 397      /**
 398       * Check if the importing process needs fields mapping.
 399       *
 400       * @return bool True if the current database needs to map the fields imported.
 401       */
 402      public function needs_mapping(): bool {
 403          if (!$this->manager->has_fields()) {
 404              return false;
 405          }
 406          return (!empty($this->fieldstocreate) || !empty($this->fieldstoremove));
 407      }
 408  
 409      /**
 410       * Returns the information we need to build the importer selector.
 411       *
 412       * @return array Value and name for the preset importer selector
 413       */
 414      public function get_preset_selector(): array {
 415          return ['name' => 'directory', 'value' => $this->get_directory()];
 416      }
 417  
 418      /**
 419       * Helper function to finish up the import routine.
 420       *
 421       * Called from fields and presets pages.
 422       *
 423       * @param bool $overwritesettings Whether to overwrite activity settings or not.
 424       * @param stdClass $instance database instance object
 425       * @return void
 426       */
 427      public function finish_import_process(bool $overwritesettings, stdClass $instance): void {
 428          $result = $this->import($overwritesettings);
 429          if ($result) {
 430              notification::success(get_string('importsuccess', 'mod_data'));
 431          } else {
 432              notification::error(get_string('cannotapplypreset', 'mod_data'));
 433          }
 434          $backurl = new \moodle_url('/mod/data/field.php', ['d' => $instance->id]);
 435          redirect($backurl);
 436      }
 437  
 438      /**
 439       * Get the right importer instance from the provided parameters (POST or GET)
 440       *
 441       * @param manager $manager the current database manager
 442       * @return preset_importer the relevant preset_importer instance
 443       * @throws \moodle_exception when the file provided as parameter (POST or GET) does not exist
 444       */
 445      public static function create_from_parameters(manager $manager): preset_importer {
 446  
 447          $fullname = optional_param('fullname', '', PARAM_PATH);    // Directory the preset is in.
 448          if (!$fullname) {
 449              $fullname = required_param('directory', PARAM_FILE);
 450          }
 451  
 452          return self::create_from_plugin_or_directory($manager, $fullname);
 453      }
 454  
 455      /**
 456       * Get the right importer instance from the provided parameters (POST or GET)
 457       *
 458       * @param manager $manager the current database manager
 459       * @param string $pluginordirectory The plugin name or directory to create the importer from.
 460       * @return preset_importer the relevant preset_importer instance
 461       */
 462      public static function create_from_plugin_or_directory(manager $manager, string $pluginordirectory): preset_importer {
 463          global $CFG;
 464  
 465          if (!$pluginordirectory) {
 466              throw new \moodle_exception('emptypresetname', 'mod_data');
 467          }
 468          try {
 469              $presetdir = $CFG->tempdir . '/forms/' . $pluginordirectory;
 470              if (file_exists($presetdir) && is_dir($presetdir)) {
 471                  return new preset_upload_importer($manager, $presetdir);
 472              } else {
 473                  return new preset_existing_importer($manager, $pluginordirectory);
 474              }
 475          } catch (\moodle_exception $e) {
 476              throw new \moodle_exception('errorpresetnotfound', 'mod_data', '', $pluginordirectory);
 477          }
 478      }
 479  
 480      /**
 481       * Get the information needed to decide the modal
 482       *
 483       * @return array An array with all the information to decide the mapping
 484       */
 485      public function get_mapping_information(): array {
 486          return [
 487              'needsmapping' => $this->needs_mapping(),
 488              'presetname' => preset::get_name_from_plugin($this->get_directory()),
 489              'fieldstocreate' => $this->get_field_names($this->fieldstocreate),
 490              'fieldstoremove' => $this->get_field_names($this->fieldstoremove),
 491          ];
 492      }
 493  
 494      /**
 495       * Returns a list of the fields
 496       *
 497       * @param array $fields Array of fields to get name from.
 498       * @return string   A string listing the names of the fields.
 499       */
 500      public function get_field_names(array $fields): string {
 501          $fieldnames = array_map(function($field) {
 502              return $field->name;
 503          }, $fields);
 504          return implode(', ', $fieldnames);
 505      }
 506  }