ReportAsFile.php 10.7 KB
Newer Older
1
2
3
4
<?php

namespace IMATHUZH\Qfq\Core\Report;

5
use IMATHUZH\Qfq\Core\Database\Database;
6
use IMATHUZH\Qfq\Core\Exception\Thrower;
7
use IMATHUZH\Qfq\Core\Helper\HelperFile;
8
use IMATHUZH\Qfq\Core\Helper\OnString;
9
use IMATHUZH\Qfq\Core\Helper\Path;
10
use IMATHUZH\Qfq\Core\Helper\Sanitize;
11
use IMATHUZH\Qfq\Core\Store\Store;
12
13
14
15

class ReportAsFile
{
    /**
16
17
     * Finds the keyword file=<pathFileName> and returns pathFileName of report.
     * If the given path starts with an underscore and a system report with the given path exists, then the path of the system file is returned instead.
18
     * Returns null if the keyword is not present.
19
20
     *
     * @param string $bodyText
21
     * @return string|null pathFileName of report relative to CWD or null
22
     * @throws \UserFormException
23
     */
24
    public static function parseFileKeyword(string $bodyText) // : ?string
25
    {
26
        if (preg_match('/^\s*' . TOKEN_REPORT_FILE . '\s*=\s*([^\s]*)/m', $bodyText, $matches)) {
27
28
            $providedPathFileName = $matches[1];
            if(isset($providedPathFileName[0]) && $providedPathFileName[0] === '_') {
29

30
31
32
33
34
35
                $pathFileNameSystem = Path::absoluteExt(Path::EXT_TO_REPORT_SYSTEM, substr($providedPathFileName, 1) . REPORT_FILE_EXTENSION);
                if(HelperFile::isReadableException($pathFileNameSystem)) {
                    return $pathFileNameSystem;
                }
            }
            return Path::join(self::reportPath(), $providedPathFileName);
36
37
38
39
        } else {
            return null;
        }
    }
40

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    /**
     * Read report file from given pathFileName. Throw exception on failure.
     *
     * @param string $reportPathFileNameFull
     * @return string|null
     * @throws \UserReportException
     */
    public static function read_report_file(string $reportPathFileNameFull): string
    {
        $fileContents = file_get_contents($reportPathFileNameFull);
        if ($fileContents === false) {
            throw new \UserReportException(json_encode([
                ERROR_MESSAGE_TO_USER => "File read error.",
                ERROR_MESSAGE_TO_DEVELOPER => "Report file not found or no permission to read file: '$reportPathFileNameFull'"]),
                ERROR_FORM_NOT_FOUND);
        }
        return $fileContents;
    }

    /**
     * Create new file named after the tt_content header or the UID if not defined.
     * - Invalid characters are stripped from filename.
     * - If file exists, throw exception.
64
     * - The path of the file is derived from the typo3 page structure (use page alias if exists, else page name).
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
     * - The path is created if not exists.
     *
     * @param int $uid
     * @param Database $databaseT3
     * @return string pathFileName of report relative to CWD
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function create_file_from_ttContent(int $uid, Database $databaseT3): string
    {
        // Select tt_content
        $dbT3 = Store::getInstance()->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
        $sql = "SELECT `bodytext`, `header`, `pid` FROM `$dbT3`.`tt_content` WHERE `uid` = ?";
        $ttContent = $databaseT3->sql($sql, ROW_EXPECT_1,
            [$uid], "Typo3 content element not found. Uid: $uid");

        // read page path of tt-content element (use page alias if exists)
        $reportPath = '';
        $pid = $ttContent['pid'];
        while (intval($pid) !== 0) {
            $sql = "SELECT `pid`, `alias`, `title` FROM `$dbT3`.`pages` WHERE `uid` = ?";
            $page = $databaseT3->sql($sql, ROW_EXPECT_1,
                [$pid], "Typo3 page not found. Uid: $pid");
            $supDir = $page['alias'] !== '' ? $page['alias'] : $page['title'];
91
            $reportPath = HelperFile::joinPathFilename(Sanitize::safeFilename($supDir), $reportPath);
92
93
94
95
            $pid = $page['pid'];
            if (strlen($reportPath) > 4096) {
                // Throw exception in case of infinite loop.
                throw new \UserReportException(json_encode([
96
                    ERROR_MESSAGE_TO_USER => "Exporting report to file failed.",
97
98
99
100
                    ERROR_MESSAGE_TO_DEVELOPER => "Report path too long: '$reportPath'"]),
                    ERROR_FORM_NOT_FOUND);
            }
        }
101
        $reportPathFull = HelperFile::joinPathFilename(self::reportPath(), $reportPath);
102
103
104
105
106

        // create path in filesystem if not exists
        HelperFile::createPathRecursive($reportPathFull);

        // add filename
107
        $fileName = Sanitize::safeFilename($ttContent['header']);
108
        if (empty($fileName)) {
109
110
111
112
113
114
115
            $fileName = strval($uid);
        }
        $reportPathFileNameFull = HelperFile::joinPathFilename($reportPathFull, $fileName) . REPORT_FILE_EXTENSION;

        // if file exists, throw exception
        if (file_exists($reportPathFileNameFull)) {
            throw new \UserReportException(json_encode([
116
117
                ERROR_MESSAGE_TO_USER => "Exporting report to file failed.",
                ERROR_MESSAGE_TO_DEVELOPER => "File already exists: '$reportPathFileNameFull'"]),
118
119
120
121
122
123
124
125
                ERROR_FORM_NOT_FOUND);
        }

        // create file with content bodytext
        HelperFile::file_put_contents($reportPathFileNameFull, $ttContent['bodytext']);

        // replace tt-content bodytext with file=<path>
        $newBodytext = TOKEN_REPORT_FILE . "=" . HelperFile::joinPathFilename($reportPath, $fileName) . REPORT_FILE_EXTENSION;
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
        self::write_tt_content($uid, $newBodytext, $databaseT3);

        return $reportPathFileNameFull;
    }

    /**
     * Replace bodytext of given tt-content element with the given string.
     *
     * @param int $uid
     * @param string $newBodytext
     * @param Database $databaseT3
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function write_tt_content(int $uid, string $newBodytext, Database $databaseT3) {
        $dbT3 = Store::getInstance()->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
        $sql = "UPDATE `$dbT3`.`tt_content` SET `bodytext` = ?, `tstamp` = UNIX_TIMESTAMP(NOW()) WHERE `uid` = ?";
        $databaseT3->sql($sql, ROW_REGULAR, [$newBodytext, $uid]);

        // Clear cache
        // Need to truncate cf_cache_pages because it is used to restore page-specific cache
        $sql = "DELETE FROM `$dbT3`.`cf_cache_pages`";
        $databaseT3->sql($sql);
    }

    /**
     * Overwrites report file given by the 'file=<path>' keyword of the tt_content element with the given uid.
     *
     * @param int $uid
     * @param string $newContent
     * @param Database $databaseT3
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function write_file_uid(int $uid, string $newContent, Database $databaseT3) // : void
    {
        // Get the report file path from tt_content
        $dbT3 = Store::getInstance()->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
        $sql = "SELECT `bodytext` FROM `$dbT3`.`tt_content` WHERE `uid` = ?";
        $ttContent = $databaseT3->sql($sql, ROW_EXPECT_1,
            [$uid], "Typo3 content element not found. Uid: $uid");

172
173
        $absoluteReportFilePath = self::parseFileKeyword($ttContent['bodytext']);
        if ($absoluteReportFilePath === null) {
174
175
176
177
178
179
180
            throw new \UserReportException(json_encode([
                ERROR_MESSAGE_TO_USER => "No report file defined.",
                ERROR_MESSAGE_TO_DEVELOPER => "The keyword '" . TOKEN_REPORT_FILE . "' is not present in the typo3 content element with id $uid"]),
                ERROR_FORM_NOT_FOUND);
        }

        // Update report file
181
182
        self::backupReportFile($absoluteReportFilePath);
        HelperFile::file_put_contents($absoluteReportFilePath, $newContent);
183
184
185
186
187
188
    }

    /**
     * Create copy of given report file in _backup directory. If given file does not exist, do nothing.
     * New file name: <reportFileName>.YYYMMDDhhmmss.json
     *
189
     * @param string $absoluteReportFilePath
190
191
     * @throws \UserFormException
     */
192
    private static function backupReportFile(string $absoluteReportFilePath)
193
    {
194
        if (file_exists($absoluteReportFilePath))
195
        {
196
197
            if (!is_readable($absoluteReportFilePath)) {
                Thrower::userFormException('Error while trying to backup report file.', "Report file is not readable: $absoluteReportFilePath");
198
199
200
            }

            // copy file
201
202
            $absoluteBackupFilePath = self::newBackupPathFileName($absoluteReportFilePath);
            $success = copy($absoluteReportFilePath, $absoluteBackupFilePath);
203
            if ($success === false) {
204
                Thrower::userFormException('Error while trying to backup report file.', "Can't copy file $absoluteReportFilePath to $absoluteBackupFilePath");
205
206
207
208
209
210
211
212
            }
        }
    }

    /**
     * Return the path to a (non-existing) report backup file with name:
     * <reportFileName>.YYYMMDDhhmmss.<tag>.qfqr
     *
213
     * @param string $absoluteReportFilePath
214
215
216
     * @return string
     * @throws \UserFormException
     */
217
    private static function newBackupPathFileName(string $absoluteReportFilePath): string
218
219
    {
        // create backup path if not exists
220
221
222
        $absoluteBackupPath = Path::join(dirname($absoluteReportFilePath), Path::REPORT_FILE_TO_BACKUP);
        if (!is_dir($absoluteBackupPath)) {
            $success = mkdir($absoluteBackupPath, 0777, true);
223
            if ($success === false) {
224
                Thrower::userFormException('Error while trying to backup report file.', "Can't create backup path: $absoluteBackupPath");
225
226
227
            }
        }

228
        $absoluteBackupFilePath = Path::join($absoluteBackupPath, basename($absoluteReportFilePath, REPORT_FILE_EXTENSION) . REPORT_FILE_EXTENSION . '.' . date('Y-m-d_H-i-s'));
229
230
231

        // add index to filename if backup file with current timestamp already exists
        $index = 1;
232
233
        while (file_exists($absoluteBackupFilePath)) {
            $absoluteBackupFilePath = Path::join($absoluteBackupPath, basename($absoluteReportFilePath, REPORT_FILE_EXTENSION) . REPORT_FILE_EXTENSION . '.' . date('Y-m-d_H-i-s') . ".$index");
234
            $index ++;
235
            if ($index > 20) {
236
237
                Thrower::userFormException('Error while trying to backup report file.', 'Infinite loop.');
            }
238
        }
239

240
        return $absoluteBackupFilePath;
241
242
    }

243
244
245
246
247
248
249
    /**
     * Return the path of the report directory relative to CWD.
     * Create path if it doesn't exist.
     *
     * @return string
     * @throws \UserFormException
     */
250
    private static function reportPath(): string
251
    {
252
        $absoluteReportPath = Path::absoluteProject(Path::projectToReport());
253
254
        HelperFile::createPathRecursive($absoluteReportPath);
        return $absoluteReportPath;
255
    }
256
}