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\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
}