Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
}