AutorÃa | Ultima modificación | Ver Log |
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_data\local\importer;
use core\notification;
use mod_data\manager;
use mod_data\preset;
use stdClass;
use html_writer;
/**
* Abstract class used for data preset importers
*
* @package mod_data
* @copyright 2022 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class preset_importer {
/** @var manager manager instance. */
private $manager;
/** @var string directory where to find the preset. */
protected $directory;
/** @var array fields to remove. */
public $fieldstoremove;
/** @var array fields to update. */
public $fieldstoupdate;
/** @var array fields to create. */
public $fieldstocreate;
/** @var array settings to be imported. */
public $settings;
/**
* Constructor
*
* @param manager $manager
* @param string $directory
*/
public function __construct(manager $manager, string $directory) {
$this->manager = $manager;
$this->directory = $directory;
// Read the preset and saved result.
$this->settings = $this->get_preset_settings();
}
/**
* Returns the name of the directory the preset is located in
*
* @return string
*/
public function get_directory(): string {
return basename($this->directory);
}
/**
* Retreive the contents of a file. That file may either be in a conventional directory of the Moodle file storage
*
* @param \file_storage|null $filestorage . Should be null if using a conventional directory
* @param \stored_file|null $fileobj the directory to look in. null if using a conventional directory
* @param string|null $dir the directory to look in. null if using the Moodle file storage
* @param string $filename the name of the file we want
* @return string|null the contents of the file or null if the file doesn't exist.
*/
public function get_file_contents(
?\file_storage &$filestorage,
?\stored_file &$fileobj,
?string $dir,
string $filename
): ?string {
if (empty($filestorage) || empty($fileobj)) {
if (substr($dir, -1) != '/') {
$dir .= '/';
}
if (file_exists($dir.$filename)) {
return file_get_contents($dir.$filename);
} else {
return null;
}
} else {
if ($filestorage->file_exists(
DATA_PRESET_CONTEXT,
DATA_PRESET_COMPONENT,
DATA_PRESET_FILEAREA,
0,
$fileobj->get_filepath(),
$filename)
) {
$file = $filestorage->get_file(
DATA_PRESET_CONTEXT,
DATA_PRESET_COMPONENT,
DATA_PRESET_FILEAREA,
0,
$fileobj->get_filepath(),
$filename
);
return $file->get_content();
} else {
return null;
}
}
}
/**
* Gets the preset settings
*
* @return stdClass Settings to be imported.
*/
public function get_preset_settings(): stdClass {
global $CFG;
require_once($CFG->libdir.'/xmlize.php');
$fs = null;
$fileobj = null;
if (!preset::is_directory_a_preset($this->directory)) {
// Maybe the user requested a preset stored in the Moodle file storage.
$fs = get_file_storage();
$files = $fs->get_area_files(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA);
// Preset name to find will be the final element of the directory.
$explodeddirectory = explode('/', $this->directory);
$presettofind = end($explodeddirectory);
// Now go through the available files available and see if we can find it.
foreach ($files as $file) {
if (($file->is_directory() && $file->get_filepath() == '/') || !$file->is_directory()) {
continue;
}
$presetname = trim($file->get_filepath(), '/');
if ($presetname == $presettofind) {
$this->directory = $presetname;
$fileobj = $file;
}
}
if (empty($fileobj)) {
throw new \moodle_exception('invalidpreset', 'data', '', $this->directory);
}
}
$allowedsettings = [
'intro',
'comments',
'requiredentries',
'requiredentriestoview',
'maxentries',
'rssarticles',
'approval',
'defaultsortdir',
'defaultsort'
];
$module = $this->manager->get_instance();
$result = new stdClass;
$result->settings = new stdClass;
$result->importfields = [];
$result->currentfields = $this->manager->get_field_records();
// Grab XML.
$presetxml = $this->get_file_contents($fs, $fileobj, $this->directory, 'preset.xml');
$parsedxml = xmlize($presetxml, 0);
// First, do settings. Put in user friendly array.
$settingsarray = $parsedxml['preset']['#']['settings'][0]['#'];
$result->settings = new StdClass();
foreach ($settingsarray as $setting => $value) {
if (!is_array($value) || !in_array($setting, $allowedsettings)) {
// Unsupported setting.
continue;
}
$result->settings->$setting = $value[0]['#'];
}
// Now work out fields to user friendly array.
if (
array_key_exists('preset', $parsedxml) &&
array_key_exists('#', $parsedxml['preset']) &&
array_key_exists('field', $parsedxml['preset']['#'])) {
$fieldsarray = $parsedxml['preset']['#']['field'];
foreach ($fieldsarray as $field) {
if (!is_array($field)) {
continue;
}
$fieldstoimport = new StdClass();
foreach ($field['#'] as $param => $value) {
if (!is_array($value)) {
continue;
}
$fieldstoimport->$param = $value[0]['#'];
}
$fieldstoimport->dataid = $module->id;
$fieldstoimport->type = clean_param($fieldstoimport->type, PARAM_ALPHA);
$result->importfields[] = $fieldstoimport;
}
}
// Calculate default mapping.
if (is_null($this->fieldstoremove) && is_null($this->fieldstocreate) && is_null($this->fieldstoupdate)) {
$this->set_affected_fields($result->importfields, $result->currentfields);
}
// Now add the HTML templates to the settings array so we can update d.
foreach (manager::TEMPLATES_LIST as $templatename => $templatefile) {
$result->settings->$templatename = $this->get_file_contents(
$fs,
$fileobj,
$this->directory,
$templatefile
);
}
$result->settings->instance = $module->id;
return $result;
}
/**
* Import the preset into the given database module
*
* @param bool $overwritesettings Whether to overwrite activity settings or not.
* @return bool Wether the importing has been successful.
*/
public function import(bool $overwritesettings): bool {
global $DB, $OUTPUT, $CFG;
$settings = $this->settings->settings;
$currentfields = $this->settings->currentfields;
$missingfieldtypes = [];
$module = $this->manager->get_instance();
foreach ($this->fieldstoupdate as $currentid => $updatable) {
if ($currentid != -1 && isset($currentfields[$currentid])) {
$fieldobject = data_get_field_from_id($currentfields[$currentid]->id, $module);
$toupdate = false;
foreach ($updatable as $param => $value) {
if ($param != "id" && $fieldobject->field->$param !== $value) {
$fieldobject->field->$param = $value;
}
}
unset($fieldobject->field->similarfield);
$fieldobject->update_field();
unset($fieldobject);
}
}
foreach ($this->fieldstocreate as $newfield) {
/* Make a new field */
$filepath = $CFG->dirroot."/mod/data/field/$newfield->type/field.class.php";
if (!file_exists($filepath)) {
$missingfieldtypes[] = $newfield->name;
continue;
}
include_once($filepath);
if (!isset($newfield->description)) {
$newfield->description = '';
}
$classname = 'data_field_' . $newfield->type;
$fieldclass = new $classname($newfield, $module);
$fieldclass->insert_field();
unset($fieldclass);
}
if (!empty($missingfieldtypes)) {
echo $OUTPUT->notification(get_string('missingfieldtypeimport', 'data') . html_writer::alist($missingfieldtypes));
}
// Get rid of all old unused data.
foreach ($currentfields as $cid => $currentfield) {
if (!array_key_exists($cid, $this->fieldstoupdate)) {
// Delete all information related to fields.
$todelete = data_get_field_from_id($currentfield->id, $module);
$todelete->delete_field();
}
}
// Handle special settings here.
if (!empty($settings->defaultsort)) {
if (is_numeric($settings->defaultsort)) {
// Old broken value.
$settings->defaultsort = 0;
} else {
$settings->defaultsort = (int)$DB->get_field(
'data_fields',
'id',
['dataid' => $module->id, 'name' => $settings->defaultsort]
);
}
} else {
$settings->defaultsort = 0;
}
// Do we want to overwrite all current database settings?
if ($overwritesettings) {
// All supported settings.
$overwrite = array_keys((array)$settings);
} else {
// Only templates and sorting.
$overwrite = ['singletemplate', 'listtemplate', 'listtemplateheader', 'listtemplatefooter',
'addtemplate', 'rsstemplate', 'rsstitletemplate', 'csstemplate', 'jstemplate',
'asearchtemplate', 'defaultsortdir', 'defaultsort'];
}
// Now overwrite current data settings.
foreach ($module as $prop => $unused) {
if (in_array($prop, $overwrite)) {
$module->$prop = $settings->$prop;
}
}
data_update_instance($module);
return $this->cleanup();
}
/**
* Returns information about the fields needs to be removed, updated or created.
*
* @param array $newfields Array of new fields to be applied.
* @param array $currentfields Array of current fields on database activity.
* @return void
*/
public function set_affected_fields(array $newfields = [], array $currentfields = []): void {
$fieldstoremove = [];
$fieldstocreate = [];
$preservedfields = [];
// Maps fields and makes new ones.
if (!empty($newfields)) {
// We require an injective mapping, and need to know what to protect.
foreach ($newfields as $newid => $newfield) {
$preservedfieldid = optional_param("field_$newid", -1, PARAM_INT);
if (array_key_exists($preservedfieldid, $preservedfields)) {
throw new \moodle_exception('notinjectivemap', 'data');
}
if ($preservedfieldid == -1) {
// Let's check if there is any field with same type and name that we could map to.
foreach ($currentfields as $currentid => $currentfield) {
if (($currentfield->type == $newfield->type) &&
($currentfield->name == $newfield->name) && !array_key_exists($currentid, $preservedfields)) {
// We found a possible default map.
$preservedfieldid = $currentid;
$preservedfields[$currentid] = $newfield;
}
}
}
if ($preservedfieldid == -1) {
// We need to create a new field.
$fieldstocreate[] = $newfield;
} else {
$preservedfields[$preservedfieldid] = $newfield;
}
}
}
foreach ($currentfields as $currentid => $currentfield) {
if (!array_key_exists($currentid, $preservedfields)) {
$fieldstoremove[] = $currentfield;
}
}
$this->fieldstocreate = $fieldstocreate;
$this->fieldstoremove = $fieldstoremove;
$this->fieldstoupdate = $preservedfields;
}
/**
* Any clean up routines should go here
*
* @return bool Wether the preset has been successfully cleaned up.
*/
public function cleanup(): bool {
return true;
}
/**
* Check if the importing process needs fields mapping.
*
* @return bool True if the current database needs to map the fields imported.
*/
public function needs_mapping(): bool {
if (!$this->manager->has_fields()) {
return false;
}
return (!empty($this->fieldstocreate) || !empty($this->fieldstoremove));
}
/**
* Returns the information we need to build the importer selector.
*
* @return array Value and name for the preset importer selector
*/
public function get_preset_selector(): array {
return ['name' => 'directory', 'value' => $this->get_directory()];
}
/**
* Helper function to finish up the import routine.
*
* Called from fields and presets pages.
*
* @param bool $overwritesettings Whether to overwrite activity settings or not.
* @param stdClass $instance database instance object
* @return void
*/
public function finish_import_process(bool $overwritesettings, stdClass $instance): void {
$result = $this->import($overwritesettings);
if ($result) {
notification::success(get_string('importsuccess', 'mod_data'));
} else {
notification::error(get_string('cannotapplypreset', 'mod_data'));
}
$backurl = new \moodle_url('/mod/data/field.php', ['d' => $instance->id]);
redirect($backurl);
}
/**
* Get the right importer instance from the provided parameters (POST or GET)
*
* @param manager $manager the current database manager
* @return preset_importer the relevant preset_importer instance
* @throws \moodle_exception when the file provided as parameter (POST or GET) does not exist
*/
public static function create_from_parameters(manager $manager): preset_importer {
$fullname = optional_param('fullname', '', PARAM_PATH); // Directory the preset is in.
if (!$fullname) {
$fullname = required_param('directory', PARAM_FILE);
}
return self::create_from_plugin_or_directory($manager, $fullname);
}
/**
* Get the right importer instance from the provided parameters (POST or GET)
*
* @param manager $manager the current database manager
* @param string $pluginordirectory The plugin name or directory to create the importer from.
* @return preset_importer the relevant preset_importer instance
*/
public static function create_from_plugin_or_directory(manager $manager, string $pluginordirectory): preset_importer {
global $CFG;
if (!$pluginordirectory) {
throw new \moodle_exception('emptypresetname', 'mod_data');
}
try {
$presetdir = $CFG->tempdir . '/forms/' . $pluginordirectory;
if (file_exists($presetdir) && is_dir($presetdir)) {
return new preset_upload_importer($manager, $presetdir);
} else {
return new preset_existing_importer($manager, $pluginordirectory);
}
} catch (\moodle_exception $e) {
throw new \moodle_exception('errorpresetnotfound', 'mod_data', '', $pluginordirectory);
}
}
/**
* Get the information needed to decide the modal
*
* @return array An array with all the information to decide the mapping
*/
public function get_mapping_information(): array {
return [
'needsmapping' => $this->needs_mapping(),
'presetname' => preset::get_name_from_plugin($this->get_directory()),
'fieldstocreate' => $this->get_field_names($this->fieldstocreate),
'fieldstoremove' => $this->get_field_names($this->fieldstoremove),
];
}
/**
* Returns a list of the fields
*
* @param array $fields Array of fields to get name from.
* @return string A string listing the names of the fields.
*/
public function get_field_names(array $fields): string {
$fieldnames = array_map(function($field) {
return $field->name;
}, $fields);
return implode(', ', $fieldnames);
}
}