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\exporter; 18 19 use file_serving_exception; 20 use moodle_exception; 21 use zip_archive; 22 23 /** 24 * Exporter class for exporting data and - if needed - files as well in a zip archive. 25 * 26 * @package mod_data 27 * @copyright 2023 ISB Bayern 28 * @author Philipp Memmel 29 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 */ 31 abstract class entries_exporter { 32 33 /** @var int Tracks the currently edited row of the export data file. */ 34 private int $currentrow; 35 36 /** 37 * @var array The data structure containing the data for exporting. It's a 2-dimensional array of 38 * rows and columns. 39 */ 40 protected array $exportdata; 41 42 /** @var string Name of the export file name without extension. */ 43 protected string $exportfilename; 44 45 /** @var zip_archive The zip archive object we store all the files in, if we need to export files as well. */ 46 private zip_archive $ziparchive; 47 48 /** @var bool Tracks the state if the zip archive already has been closed. */ 49 private bool $isziparchiveclosed; 50 51 /** @var string full path of the zip archive. */ 52 private string $zipfilepath; 53 54 /** @var array Array to store all filenames in the zip archive for export. */ 55 private array $filenamesinzip; 56 57 /** 58 * Creates an entries_exporter object. 59 * 60 * This object can be used to export data to different formats including files. If files are added, 61 * everything will be bundled up in a zip archive. 62 */ 63 public function __construct() { 64 $this->currentrow = 0; 65 $this->exportdata = []; 66 $this->exportfilename = 'Exportfile'; 67 $this->filenamesinzip = []; 68 $this->isziparchiveclosed = true; 69 } 70 71 /** 72 * Adds a row (array of strings) to the export data. 73 * 74 * @param array $row the row to add, $row has to be a plain array of strings 75 * @return void 76 */ 77 public function add_row(array $row): void { 78 $this->exportdata[] = $row; 79 $this->currentrow++; 80 } 81 82 /** 83 * Adds a data string (so the content for a "cell") to the current row. 84 * 85 * @param string $cellcontent the content to add to the current row 86 * @return void 87 */ 88 public function add_to_current_row(string $cellcontent): void { 89 $this->exportdata[$this->currentrow][] = $cellcontent; 90 } 91 92 /** 93 * Signal the entries_exporter to finish the current row and jump to the next row. 94 * 95 * @return void 96 */ 97 public function next_row(): void { 98 $this->currentrow++; 99 } 100 101 /** 102 * Sets the name of the export file. 103 * 104 * Only use the basename without path and without extension here. 105 * 106 * @param string $exportfilename name of the file without path and extension 107 * @return void 108 */ 109 public function set_export_file_name(string $exportfilename): void { 110 $this->exportfilename = $exportfilename; 111 } 112 113 /** 114 * The entries_exporter will prepare a data file from the rows and columns being added. 115 * Overwrite this method to generate the data file as string. 116 * 117 * @return string the data file as a string 118 */ 119 abstract protected function get_data_file_content(): string; 120 121 /** 122 * Overwrite the method to return the file extension your data file will have, for example 123 * <code>return 'csv';</code> for a csv file entries_exporter. 124 * 125 * @return string the file extension of the data file your entries_exporter is using 126 */ 127 abstract protected function get_export_data_file_extension(): string; 128 129 /** 130 * Returns the count of currently stored records (rows excluding header row). 131 * 132 * @return int the count of records/rows 133 */ 134 public function get_records_count(): int { 135 // The attribute $this->exportdata also contains a header. If only one row is present, this 136 // usually is the header, so record count should be 0. 137 if (count($this->exportdata) <= 1) { 138 return 0; 139 } 140 return count($this->exportdata) - 1; 141 } 142 143 /** 144 * Use this method to add a file which should be exported to the entries_exporter. 145 * 146 * @param string $filename the name of the file which should be added 147 * @param string $filecontent the content of the file as a string 148 * @param string $zipsubdir the subdirectory in the zip archive. Defaults to 'files/'. 149 * @return void 150 * @throws moodle_exception if there is an error adding the file to the zip archive 151 */ 152 public function add_file_from_string(string $filename, string $filecontent, string $zipsubdir = 'files/'): void { 153 if (empty($this->filenamesinzip)) { 154 // No files added yet, so we need to create a zip archive. 155 $this->create_zip_archive(); 156 } 157 if (!str_ends_with($zipsubdir, '/')) { 158 $zipsubdir .= '/'; 159 } 160 $zipfilename = $zipsubdir . $filename; 161 $this->filenamesinzip[] = $zipfilename; 162 $this->ziparchive->add_file_from_string($zipfilename, $filecontent); 163 } 164 165 /** 166 * Sends the generated export file. 167 * 168 * Care: By default this function finishes the current PHP request and directly serves the file to the user as download. 169 * 170 * @param bool $sendtouser true if the file should be sent directly to the user, if false the file content will be returned 171 * as string 172 * @return string|null file content as string if $sendtouser is true 173 * @throws moodle_exception if there is an issue adding the data file 174 * @throws file_serving_exception if the file could not be served properly 175 */ 176 public function send_file(bool $sendtouser = true): null|string { 177 if (empty($this->filenamesinzip)) { 178 if ($sendtouser) { 179 send_file($this->get_data_file_content(), 180 $this->exportfilename . '.' . $this->get_export_data_file_extension(), 181 null, 0, true, true); 182 return null; 183 } else { 184 return $this->get_data_file_content(); 185 } 186 } 187 $this->add_file_from_string($this->exportfilename . '.' . $this->get_export_data_file_extension(), 188 $this->get_data_file_content(), '/'); 189 $this->finish_zip_archive(); 190 191 if ($this->isziparchiveclosed) { 192 if ($sendtouser) { 193 send_file($this->zipfilepath, $this->exportfilename . '.zip', null, 0, false, true); 194 return null; 195 } else { 196 return file_get_contents($this->zipfilepath); 197 } 198 } else { 199 throw new file_serving_exception('Could not serve zip file, it could not be closed properly.'); 200 } 201 } 202 203 /** 204 * Checks if a file with the given name has already been added to the file export bundle. 205 * 206 * Care: Filenames are compared to all files in the specified zip subdirectory which 207 * defaults to 'files/'. 208 * 209 * @param string $filename the filename containing the zip path of the file to check 210 * @param string $zipsubdir The subdirectory in which the filename should be looked for, 211 * defaults to 'files/' 212 * @return bool true if file with the given name already exists, false otherwise 213 */ 214 public function file_exists(string $filename, string $zipsubdir = 'files/'): bool { 215 if (!str_ends_with($zipsubdir, '/')) { 216 $zipsubdir .= '/'; 217 } 218 if (empty($filename)) { 219 return false; 220 } 221 return in_array($zipsubdir . $filename, $this->filenamesinzip, true); 222 } 223 224 /** 225 * Creates a unique filename based on the given filename. 226 * 227 * This method adds "_1", "_2", ... to the given file name until the newly generated filename 228 * is not equal to any of the already saved ones in the export file bundle. 229 * 230 * @param string $filename the filename based on which a unique filename should be generated 231 * @return string the unique filename 232 */ 233 public function create_unique_filename(string $filename): string { 234 if (!$this->file_exists($filename)) { 235 return $filename; 236 } 237 238 $extension = pathinfo($filename, PATHINFO_EXTENSION); 239 $filenamewithoutextension = empty($extension) 240 ? $filename 241 : substr($filename, 0,strlen($filename) - strlen($extension) - 1); 242 $filenamewithoutextension = $filenamewithoutextension . '_1'; 243 $i = 1; 244 $filename = empty($extension) ? $filenamewithoutextension : $filenamewithoutextension . '.' . $extension; 245 while ($this->file_exists($filename)) { 246 // In case we have already a file ending with '_XX' where XX is an ascending number, we have to 247 // remove '_XX' first before adding '_YY' again where YY is the successor of XX. 248 $filenamewithoutextension = preg_replace('/_' . $i . '$/', '_' . ($i + 1), $filenamewithoutextension); 249 $filename = empty($extension) ? $filenamewithoutextension : $filenamewithoutextension . '.' . $extension; 250 $i++; 251 } 252 return $filename; 253 } 254 255 /** 256 * Prepares the zip archive. 257 * 258 * @return void 259 */ 260 private function create_zip_archive(): void { 261 $tmpdir = make_request_directory(); 262 $this->zipfilepath = $tmpdir . '/' . $this->exportfilename . '.zip'; 263 $this->ziparchive = new zip_archive(); 264 $this->isziparchiveclosed = !$this->ziparchive->open($this->zipfilepath); 265 } 266 267 /** 268 * Closes the zip archive. 269 * 270 * @return void 271 */ 272 private function finish_zip_archive(): void { 273 if (!$this->isziparchiveclosed) { 274 $this->isziparchiveclosed = $this->ziparchive->close(); 275 } 276 } 277 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body