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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body