first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,81 @@
<?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\exporter;
use coding_exception;
use csv_export_writer;
/**
* CSV entries exporter for mod_data.
*
* @package mod_data
* @copyright 2023 ISB Bayern
* @author Philipp Memmel
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class csv_entries_exporter extends entries_exporter {
/** @var string[] Possible delimiter names. Only used internally to check if a valid delimiter name
* has been specified.
*/
private const POSSIBLE_DELIMITER_NAMES = ['comma', 'tab', 'semicolon', 'colon', 'cfg'];
/**
* @var string name of the delimiter to use for the csv export. Possible values:
* 'comma', 'tab', 'semicolon', 'colon' or 'cfg'.
*/
private string $delimitername = 'comma';
/**
* Returns the csv data exported by the csv_export_writer for further handling.
*
* @see \mod_data\local\exporter\entries_exporter::get_data_file_content()
*/
public function get_data_file_content(): string {
global $CFG;
require_once($CFG->libdir . '/csvlib.class.php');
return csv_export_writer::print_array($this->exportdata, $this->delimitername, '"', true);
}
/**
* Returns the file extension of this entries exporter.
*
* @see \mod_data\local\exporter\entries_exporter::get_export_data_file_extension()
*/
public function get_export_data_file_extension(): string {
return 'csv';
}
/**
* Setter for the delimiter name which should be used in this csv_entries_exporter object.
*
* Calling this setter is optional, the delimiter name defaults to 'comma'.
*
* @param string $delimitername one of 'comma', 'tab', 'semicolon', 'colon' or 'cfg'
* @return void
* @throws coding_exception if a wrong delimiter name has been specified
*/
public function set_delimiter_name(string $delimitername): void {
if (!in_array($delimitername, self::POSSIBLE_DELIMITER_NAMES)) {
throw new coding_exception('Wrong delimiter type',
'Please choose on of the following delimiters: '
. '\"comma\", \"tab\", \"semicolon\", \"colon\", \"cfg\"');
}
$this->delimitername = $delimitername;
}
}
@@ -0,0 +1,277 @@
<?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\exporter;
use file_serving_exception;
use moodle_exception;
use zip_archive;
/**
* Exporter class for exporting data and - if needed - files as well in a zip archive.
*
* @package mod_data
* @copyright 2023 ISB Bayern
* @author Philipp Memmel
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class entries_exporter {
/** @var int Tracks the currently edited row of the export data file. */
private int $currentrow;
/**
* @var array The data structure containing the data for exporting. It's a 2-dimensional array of
* rows and columns.
*/
protected array $exportdata;
/** @var string Name of the export file name without extension. */
protected string $exportfilename;
/** @var zip_archive The zip archive object we store all the files in, if we need to export files as well. */
private zip_archive $ziparchive;
/** @var bool Tracks the state if the zip archive already has been closed. */
private bool $isziparchiveclosed;
/** @var string full path of the zip archive. */
private string $zipfilepath;
/** @var array Array to store all filenames in the zip archive for export. */
private array $filenamesinzip;
/**
* Creates an entries_exporter object.
*
* This object can be used to export data to different formats including files. If files are added,
* everything will be bundled up in a zip archive.
*/
public function __construct() {
$this->currentrow = 0;
$this->exportdata = [];
$this->exportfilename = 'Exportfile';
$this->filenamesinzip = [];
$this->isziparchiveclosed = true;
}
/**
* Adds a row (array of strings) to the export data.
*
* @param array $row the row to add, $row has to be a plain array of strings
* @return void
*/
public function add_row(array $row): void {
$this->exportdata[] = $row;
$this->currentrow++;
}
/**
* Adds a data string (so the content for a "cell") to the current row.
*
* @param string $cellcontent the content to add to the current row
* @return void
*/
public function add_to_current_row(string $cellcontent): void {
$this->exportdata[$this->currentrow][] = $cellcontent;
}
/**
* Signal the entries_exporter to finish the current row and jump to the next row.
*
* @return void
*/
public function next_row(): void {
$this->currentrow++;
}
/**
* Sets the name of the export file.
*
* Only use the basename without path and without extension here.
*
* @param string $exportfilename name of the file without path and extension
* @return void
*/
public function set_export_file_name(string $exportfilename): void {
$this->exportfilename = $exportfilename;
}
/**
* The entries_exporter will prepare a data file from the rows and columns being added.
* Overwrite this method to generate the data file as string.
*
* @return string the data file as a string
*/
abstract protected function get_data_file_content(): string;
/**
* Overwrite the method to return the file extension your data file will have, for example
* <code>return 'csv';</code> for a csv file entries_exporter.
*
* @return string the file extension of the data file your entries_exporter is using
*/
abstract protected function get_export_data_file_extension(): string;
/**
* Returns the count of currently stored records (rows excluding header row).
*
* @return int the count of records/rows
*/
public function get_records_count(): int {
// The attribute $this->exportdata also contains a header. If only one row is present, this
// usually is the header, so record count should be 0.
if (count($this->exportdata) <= 1) {
return 0;
}
return count($this->exportdata) - 1;
}
/**
* Use this method to add a file which should be exported to the entries_exporter.
*
* @param string $filename the name of the file which should be added
* @param string $filecontent the content of the file as a string
* @param string $zipsubdir the subdirectory in the zip archive. Defaults to 'files/'.
* @return void
* @throws moodle_exception if there is an error adding the file to the zip archive
*/
public function add_file_from_string(string $filename, string $filecontent, string $zipsubdir = 'files/'): void {
if (empty($this->filenamesinzip)) {
// No files added yet, so we need to create a zip archive.
$this->create_zip_archive();
}
if (!str_ends_with($zipsubdir, '/')) {
$zipsubdir .= '/';
}
$zipfilename = $zipsubdir . $filename;
$this->filenamesinzip[] = $zipfilename;
$this->ziparchive->add_file_from_string($zipfilename, $filecontent);
}
/**
* Sends the generated export file.
*
* Care: By default this function finishes the current PHP request and directly serves the file to the user as download.
*
* @param bool $sendtouser true if the file should be sent directly to the user, if false the file content will be returned
* as string
* @return string|null file content as string if $sendtouser is true
* @throws moodle_exception if there is an issue adding the data file
* @throws file_serving_exception if the file could not be served properly
*/
public function send_file(bool $sendtouser = true): null|string {
if (empty($this->filenamesinzip)) {
if ($sendtouser) {
send_file($this->get_data_file_content(),
$this->exportfilename . '.' . $this->get_export_data_file_extension(),
null, 0, true, true);
return null;
} else {
return $this->get_data_file_content();
}
}
$this->add_file_from_string($this->exportfilename . '.' . $this->get_export_data_file_extension(),
$this->get_data_file_content(), '/');
$this->finish_zip_archive();
if ($this->isziparchiveclosed) {
if ($sendtouser) {
send_file($this->zipfilepath, $this->exportfilename . '.zip', null, 0, false, true);
return null;
} else {
return file_get_contents($this->zipfilepath);
}
} else {
throw new file_serving_exception('Could not serve zip file, it could not be closed properly.');
}
}
/**
* Checks if a file with the given name has already been added to the file export bundle.
*
* Care: Filenames are compared to all files in the specified zip subdirectory which
* defaults to 'files/'.
*
* @param string $filename the filename containing the zip path of the file to check
* @param string $zipsubdir The subdirectory in which the filename should be looked for,
* defaults to 'files/'
* @return bool true if file with the given name already exists, false otherwise
*/
public function file_exists(string $filename, string $zipsubdir = 'files/'): bool {
if (!str_ends_with($zipsubdir, '/')) {
$zipsubdir .= '/';
}
if (empty($filename)) {
return false;
}
return in_array($zipsubdir . $filename, $this->filenamesinzip, true);
}
/**
* Creates a unique filename based on the given filename.
*
* This method adds "_1", "_2", ... to the given file name until the newly generated filename
* is not equal to any of the already saved ones in the export file bundle.
*
* @param string $filename the filename based on which a unique filename should be generated
* @return string the unique filename
*/
public function create_unique_filename(string $filename): string {
if (!$this->file_exists($filename)) {
return $filename;
}
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$filenamewithoutextension = empty($extension)
? $filename
: substr($filename, 0,strlen($filename) - strlen($extension) - 1);
$filenamewithoutextension = $filenamewithoutextension . '_1';
$i = 1;
$filename = empty($extension) ? $filenamewithoutextension : $filenamewithoutextension . '.' . $extension;
while ($this->file_exists($filename)) {
// In case we have already a file ending with '_XX' where XX is an ascending number, we have to
// remove '_XX' first before adding '_YY' again where YY is the successor of XX.
$filenamewithoutextension = preg_replace('/_' . $i . '$/', '_' . ($i + 1), $filenamewithoutextension);
$filename = empty($extension) ? $filenamewithoutextension : $filenamewithoutextension . '.' . $extension;
$i++;
}
return $filename;
}
/**
* Prepares the zip archive.
*
* @return void
*/
private function create_zip_archive(): void {
$tmpdir = make_request_directory();
$this->zipfilepath = $tmpdir . '/' . $this->exportfilename . '.zip';
$this->ziparchive = new zip_archive();
$this->isziparchiveclosed = !$this->ziparchive->open($this->zipfilepath);
}
/**
* Closes the zip archive.
*
* @return void
*/
private function finish_zip_archive(): void {
if (!$this->isziparchiveclosed) {
$this->isziparchiveclosed = $this->ziparchive->close();
}
}
}
@@ -0,0 +1,65 @@
<?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\exporter;
use MoodleODSWorkbook;
use MoodleODSWriter;
/**
* ODS entries exporter for mod_data.
*
* @package mod_data
* @copyright 2023 ISB Bayern
* @author Philipp Memmel
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class ods_entries_exporter extends entries_exporter {
/**
* Returns the file extension of this entries exporter.
*
* @see \mod_data\local\exporter\entries_exporter::get_export_data_file_extension()
*/
public function get_export_data_file_extension(): string {
return 'ods';
}
/**
* Returns the ods data exported by the ODS library for further handling.
*
* @see \mod_data\local\exporter\entries_exporter::get_data_file_content()
*/
public function get_data_file_content(): string {
global $CFG;
require_once("$CFG->libdir/odslib.class.php");
$filearg = '-';
$workbook = new MoodleODSWorkbook($filearg);
$worksheet = [];
$worksheet[0] = $workbook->add_worksheet('');
$rowno = 0;
foreach ($this->exportdata as $row) {
$colno = 0;
foreach ($row as $col) {
$worksheet[0]->write($rowno, $colno, $col);
$colno++;
}
$rowno++;
}
$writer = new MoodleODSWriter($worksheet);
return $writer->get_file_content();
}
}
+141
View File
@@ -0,0 +1,141 @@
<?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\exporter;
use context;
use context_system;
/**
* Utility class for exporting data from a mod_data instance.
*
* @package mod_data
* @copyright 2023 ISB Bayern
* @author Philipp Memmel
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class utils {
/**
* Exports the data of the mod_data instance to an entries_exporter object which then can export it to a file format.
*
* @param int $dataid
* @param array $fields
* @param array $selectedfields
* @param entries_exporter $exporter the entries_exporter object used
* @param int $currentgroup group ID of the current group. This is used for
* exporting data while maintaining group divisions.
* @param context|null $context the context in which the operation is performed (for capability checks)
* @param bool $userdetails whether to include the details of the record author
* @param bool $time whether to include time created/modified
* @param bool $approval whether to include approval status
* @param bool $tags whether to include tags
* @param bool $includefiles whether files should be exported as well
* @return void
*/
public static function data_exportdata(int $dataid, array $fields, array $selectedfields, entries_exporter $exporter,
int $currentgroup = 0, context $context = null, bool $userdetails = false, bool $time = false, bool $approval = false,
bool $tags = false, bool $includefiles = true): void {
global $DB;
if (is_null($context)) {
$context = context_system::instance();
}
// Exporting user data needs special permission.
$userdetails = $userdetails && has_capability('mod/data:exportuserinfo', $context);
// Populate the header in first row of export.
$header = [];
foreach ($fields as $key => $field) {
if (!in_array($field->field->id, $selectedfields)) {
// Ignore values we aren't exporting.
unset($fields[$key]);
} else {
$header[] = $field->field->name;
}
}
if ($tags) {
$header[] = get_string('tags', 'data');
}
if ($userdetails) {
$header[] = get_string('user');
$header[] = get_string('username');
$header[] = get_string('email');
}
if ($time) {
$header[] = get_string('timeadded', 'data');
$header[] = get_string('timemodified', 'data');
}
if ($approval) {
$header[] = get_string('approved', 'data');
}
$exporter->add_row($header);
$datarecords = $DB->get_records('data_records', array('dataid' => $dataid));
ksort($datarecords);
$line = 1;
foreach ($datarecords as $record) {
// Get content indexed by fieldid.
if ($currentgroup) {
$select = 'SELECT c.fieldid, c.content, c.content1, c.content2, c.content3, c.content4 FROM {data_content} c, '
. '{data_records} r WHERE c.recordid = ? AND r.id = c.recordid AND r.groupid = ?';
$where = array($record->id, $currentgroup);
} else {
$select = 'SELECT fieldid, content, content1, content2, content3, content4 FROM {data_content} WHERE recordid = ?';
$where = array($record->id);
}
if ($content = $DB->get_records_sql($select, $where)) {
foreach ($fields as $field) {
$contents = '';
if (isset($content[$field->field->id])) {
$contents = $field->export_text_value($content[$field->field->id]);
if (!empty($contents) && $field->file_export_supported() && $includefiles
&& !is_null($field->export_file_value($record))) {
// For exporting overwrite the content of the column with a unique
// filename, even it is not exactly the name of the file in the
// mod_data instance content. But it's more important to match the name
// of the exported file.
$contents = $exporter->create_unique_filename($contents);
$exporter->add_file_from_string($contents, $field->export_file_value($record));
}
}
// Just be double sure.
$contents = !empty($contents) ? $contents : '';
$exporter->add_to_current_row($contents);
}
if ($tags) {
$itemtags = \core_tag_tag::get_item_tags_array('mod_data', 'data_records', $record->id);
$exporter->add_to_current_row(implode(', ', $itemtags));
}
if ($userdetails) { // Add user details to the export data.
$userdata = get_complete_user_data('id', $record->userid);
$exporter->add_to_current_row(fullname($userdata));
$exporter->add_to_current_row($userdata->username);
$exporter->add_to_current_row($userdata->email);
}
if ($time) { // Add time added / modified.
$exporter->add_to_current_row(userdate($record->timecreated));
$exporter->add_to_current_row(userdate($record->timemodified));
}
if ($approval) { // Add approval status.
$exporter->add_to_current_row((int) $record->approved);
}
}
$exporter->next_row();
}
}
}
@@ -0,0 +1,209 @@
<?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 context_module;
use core_php_time_limit;
use core_tag_tag;
use core_user;
use csv_import_reader;
use moodle_exception;
use stdClass;
/**
* CSV entries_importer class for importing data and - if needed - files as well from a zip archive.
*
* @package mod_data
* @copyright 2023 ISB Bayern
* @author Philipp Memmel
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class csv_entries_importer extends entries_importer {
/** @var array Log entries for successfully added records. */
private array $addedrecordsmessages = [];
/**
* Declares the entries_importer to use a csv file as data file.
*
* @see entries_importer::get_import_data_file_extension()
*/
public function get_import_data_file_extension(): string {
return 'csv';
}
/**
* Import records for a data instance from csv data.
*
* @param stdClass $cm Course module of the data instance.
* @param stdClass $data The data instance.
* @param string $encoding The encoding of csv data.
* @param string $fielddelimiter The delimiter of the csv data.
*
* @throws moodle_exception
*/
public function import_csv(stdClass $cm, stdClass $data, string $encoding, string $fielddelimiter): void {
global $CFG, $DB;
// Large files are likely to take their time and memory. Let PHP know
// that we'll take longer, and that the process should be recycled soon
// to free up memory.
core_php_time_limit::raise();
raise_memory_limit(MEMORY_HUGE);
$iid = csv_import_reader::get_new_iid('moddata');
$cir = new csv_import_reader($iid, 'moddata');
$context = context_module::instance($cm->id);
$readcount = $cir->load_csv_content($this->get_data_file_content(), $encoding, $fielddelimiter);
if (empty($readcount)) {
throw new \moodle_exception('csvfailed', 'data', "{$CFG->wwwroot}/mod/data/edit.php?d={$data->id}");
} else {
if (!$fieldnames = $cir->get_columns()) {
throw new \moodle_exception('cannotreadtmpfile', 'error');
}
// Check the fieldnames are valid.
$rawfields = $DB->get_records('data_fields', ['dataid' => $data->id], '', 'name, id, type');
$fields = [];
$errorfield = '';
$usernamestring = get_string('username');
$safetoskipfields = [get_string('user'), get_string('email'),
get_string('timeadded', 'data'), get_string('timemodified', 'data'),
get_string('approved', 'data'), get_string('tags', 'data')];
$userfieldid = null;
foreach ($fieldnames as $id => $name) {
if (!isset($rawfields[$name])) {
if ($name == $usernamestring) {
$userfieldid = $id;
} else if (!in_array($name, $safetoskipfields)) {
$errorfield .= "'$name' ";
}
} else {
// If this is the second time, a field with this name comes up, it must be a field not provided by the user...
// like the username.
if (isset($fields[$name])) {
if ($name == $usernamestring) {
$userfieldid = $id;
}
unset($fieldnames[$id]); // To ensure the user provided content fields remain in the array once flipped.
} else {
$field = $rawfields[$name];
$field->type = clean_param($field->type, PARAM_ALPHA);
$filepath = "$CFG->dirroot/mod/data/field/$field->type/field.class.php";
if (!file_exists($filepath)) {
$errorfield .= "'$name' ";
continue;
}
require_once($filepath);
$classname = 'data_field_' . $field->type;
$fields[$name] = new $classname($field, $data, $cm);
}
}
}
if (!empty($errorfield)) {
throw new \moodle_exception('fieldnotmatched', 'data',
"{$CFG->wwwroot}/mod/data/edit.php?d={$data->id}", $errorfield);
}
$fieldnames = array_flip($fieldnames);
$cir->init();
while ($record = $cir->next()) {
$authorid = null;
if ($userfieldid) {
if (!($author = core_user::get_user_by_username($record[$userfieldid], 'id'))) {
$authorid = null;
} else {
$authorid = $author->id;
}
}
// Determine presence of "approved" field within the record to import.
$approved = true;
if (array_key_exists(get_string('approved', 'data'), $fieldnames)) {
$approvedindex = $fieldnames[get_string('approved', 'data')];
$approved = !empty($record[$approvedindex]);
}
if ($recordid = data_add_record($data, 0, $authorid, $approved)) { // Add instance to data_record.
foreach ($fields as $field) {
$fieldid = $fieldnames[$field->field->name];
if (isset($record[$fieldid])) {
$value = $record[$fieldid];
} else {
$value = '';
}
if (method_exists($field, 'update_content_import')) {
$field->update_content_import($recordid, $value, 'field_' . $field->field->id);
} else {
$content = new stdClass();
$content->fieldid = $field->field->id;
$content->content = $value;
$content->recordid = $recordid;
if ($field->file_import_supported() && $this->importfiletype === 'zip') {
$filecontent = $this->get_file_content_from_zip($content->content);
if (!$filecontent) {
// No corresponding file in zip archive, so no record for this field being added at all.
continue;
}
$contentid = $DB->insert_record('data_content', $content);
$field->import_file_value($contentid, $filecontent, $content->content);
} else {
$DB->insert_record('data_content', $content);
}
}
}
if (core_tag_tag::is_enabled('mod_data', 'data_records') &&
isset($fieldnames[get_string('tags', 'data')])) {
$columnindex = $fieldnames[get_string('tags', 'data')];
$rawtags = $record[$columnindex];
$tags = explode(',', $rawtags);
foreach ($tags as $tag) {
$tag = trim($tag);
if (empty($tag)) {
continue;
}
core_tag_tag::add_item_tag('mod_data', 'data_records', $recordid, $context, $tag);
}
}
$this->addedrecordsmessages[] = get_string('added', 'moodle',
count($this->addedrecordsmessages) + 1)
. ". " . get_string('entry', 'data')
. " (ID $recordid)\n";
}
}
$cir->close();
$cir->cleanup(true);
}
}
/**
* Getter for the array of messages for added records.
*
* For each successfully added record the array contains a log message.
*
* @return array Array of message strings: For each added record one message string
*/
public function get_added_records_messages(): array {
return $this->addedrecordsmessages;
}
}
@@ -0,0 +1,144 @@
<?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 coding_exception;
use core_php_time_limit;
use file_packer;
/**
* Importer class for importing data and - if needed - files as well from a zip archive.
*
* @package mod_data
* @copyright 2023 ISB Bayern
* @author Philipp Memmel
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class entries_importer {
/** @var string The import file path of the file which data should be imported from. */
protected string $importfilepath;
/** @var string The original name of the import file name including extension of the file which data should be imported from. */
protected string $importfilename;
/** @var string $importfiletype The file type of the import file. */
protected string $importfiletype;
/** @var file_packer Zip file packer to extract files from a zip archive. */
private file_packer $packer;
/** @var bool Tracks state if zip archive has been extracted already. */
private bool $zipfileextracted;
/** @var string Temporary directory where zip archive is being extracted to. */
private string $extracteddir;
/**
* Creates an entries_importer object.
*
* This object can be used to import data from data files (like csv) and zip archives both including a data file and files to be
* stored in the course module context.
*
* @param string $importfilepath the complete path of the import file including filename
* @param string $importfilename the import file name as uploaded by the user
* @throws coding_exception if a wrong file type is being used
*/
public function __construct(string $importfilepath, string $importfilename) {
$this->importfilepath = $importfilepath;
$this->importfilename = $importfilename;
$this->importfiletype = pathinfo($importfilename, PATHINFO_EXTENSION);
$this->zipfileextracted = false;
if ($this->importfiletype !== $this->get_import_data_file_extension() && $this->importfiletype !== 'zip') {
throw new coding_exception('Only "zip" or "' . $this->get_import_data_file_extension() . '" files are '
. 'allowed.');
}
}
/**
* Return the file extension of the import data file which is being used, for example 'csv' for a csv entries_importer.
*
* @return string the file extension of the export data file
*/
abstract public function get_import_data_file_extension(): string;
/**
* Returns the file content of the data file.
*
* Returns the content of the file directly if the entries_importer's file is a data file itself.
* If the entries_importer's file is a zip archive, the content of the first found data file in the
* zip archive's root will be returned.
*
* @return false|string the data file content as string; false, if file cannot be found/read
*/
public function get_data_file_content(): false|string {
if ($this->importfiletype !== 'zip') {
// We have no zip archive, so the file itself must be the data file.
return file_get_contents($this->importfilepath);
}
// So we have a zip archive and need to find the right data file in the root of the zip archive.
$this->extract_zip();
$datafilenames = array_filter($this->packer->list_files($this->importfilepath),
fn($file) => pathinfo($file->pathname, PATHINFO_EXTENSION) === $this->get_import_data_file_extension()
&& !str_contains($file->pathname, '/'));
if (empty($datafilenames) || count($datafilenames) > 1) {
return false;
}
return file_get_contents($this->extracteddir . reset($datafilenames)->pathname);
}
/**
* Returns the file content from a file which has been stored in the zip archive.
*
* @param string $filename
* @param string $zipsubdir
* @return false|string the file content as string, false if the file could not be found/read
*/
public function get_file_content_from_zip(string $filename, string $zipsubdir = 'files/'): false|string {
if (empty($filename)) {
// Nothing to return.
return false;
}
// Just to be sure extract if not extracted yet.
$this->extract_zip();
if (!str_ends_with($zipsubdir, '/')) {
$zipsubdir .= '/';
}
$filepathinextractedzip = $this->extracteddir . $zipsubdir . $filename;
return file_exists($filepathinextractedzip) ? file_get_contents($filepathinextractedzip) : false;
}
/**
* Extracts (if not already done and if we have a zip file to deal with) the zip file to a temporary directory.
*
* @return void
*/
private function extract_zip(): void {
if ($this->zipfileextracted || $this->importfiletype !== 'zip') {
return;
}
$this->packer = get_file_packer();
core_php_time_limit::raise(180);
$this->extracteddir = make_request_directory();
if (!str_ends_with($this->extracteddir, '/')) {
$this->extracteddir .= '/';
}
$this->packer->extract_to_pathname($this->importfilepath, $this->extracteddir);
$this->zipfileextracted = true;
}
}
@@ -0,0 +1,79 @@
<?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 mod_data\manager;
/**
* Data preset importer for existing presets
* @package mod_data
* @copyright 2022 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class preset_existing_importer extends preset_importer {
/** @var int user id. */
protected $userid;
/** @var string fullname of the preset. */
private $fullname;
/**
* Constructor
*
* @param manager $manager
* @param string $fullname
*/
public function __construct(manager $manager, string $fullname) {
global $USER;
list($userid, $shortname) = explode('/', $fullname, 2);
$context = $manager->get_context();
if ($userid &&
($userid != $USER->id) &&
!has_capability('mod/data:manageuserpresets', $context) &&
!has_capability('mod/data:viewalluserpresets', $context)
) {
throw new \coding_exception('Invalid preset provided');
}
$this->userid = intval($userid);
$this->fullname = $fullname;
$cm = $manager->get_coursemodule();
$course = $cm->get_course();
$filepath = data_preset_path($course, $userid, $shortname);
parent::__construct($manager, $filepath);
}
/**
* Returns user ID
*
* @return int userid
*/
public function get_userid(): int {
return $this->userid;
}
/**
* 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' => 'fullname', 'value' => $this->get_userid().'/'.$this->get_directory()];
}
}
@@ -0,0 +1,506 @@
<?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);
}
}
@@ -0,0 +1,55 @@
<?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 mod_data\manager;
/**
* Data preset importer for uploaded presets
*
* @package mod_data
* @copyright 2022 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class preset_upload_importer extends preset_importer {
/**
* Constructor
*
* @param manager $manager
* @param string $filepath
*/
public function __construct(manager $manager, string $filepath) {
if (is_file($filepath)) {
$fp = get_file_packer();
if ($fp->extract_to_pathname($filepath, $filepath.'_extracted')) {
fulldelete($filepath);
}
$filepath .= '_extracted';
}
parent::__construct($manager, $filepath);
}
/**
* Clean uploaded files up
*
* @return bool Wether the preset has been successfully cleaned up.
*/
public function cleanup(): bool {
return fulldelete($this->directory);
}
}