Path.php 20.6 KB
Newer Older
1
2
3
4
5
<?php


namespace IMATHUZH\Qfq\Core\Helper;

6
/**
Marc Egger's avatar
Marc Egger committed
7
 * Glossary:
8
 * - App: directory in which the index.php file is located. All urls should be relative to this.
Marc Egger's avatar
Marc Egger committed
9
 * - Ext: directory in which the QFQ extension is located. e.g. the folder Classes is in there.
10
11
 * - API: api folder of qfq extension
 *
12
13
14
15
16
 * Conventions of Path class:
 * 1) naming conventions of of path constants/functions/variables:
 *   a) name a path by its origin and its destination separated by 'to'. E.g. APP_TO_SYSTEM_LOG, $appToProject.
 *   b) Or only by its destination and prefix "absolute" if the path is absolute e.g. absoluteApp().
 *   c) Or by its destination and prefix "url" if the path returns a fully qualified url using the base path e.g. urlExt().
17
 * 2) if the destination is a file, append "File". E.g. APP_TO_SYSTEM_QFQ_LOG_FILE.
18
19
20
21
22
 * 3) if a path has to be variable, create a setter and getter. E.g. self::setAbsoluteApp(), self::absoluteApp(), private static $absoluteApp.
 * 4) a path getter appends the given arguments to the requested path using self::join(..., func_get_args()). E.g. see absoluteApp().
 * 5) additional path getters may be defined which combine other getters. E.g. see absoluteProject().
 * 6) avoid manually defining new absolute paths. Define paths relative to App then create a getter which joins it with absoluteApp()
 * 7) avoid defining redundant paths in constants. E.g. create appToApi() by combining appToExt() and extToApi() instead of defining APP_TO_API.
23
24
 */

25
use IMATHUZH\Qfq\Core\Exception\Thrower;
26
27
use IMATHUZH\Qfq\Core\Store\Config;

28
class Path {
29
    // App
30
    private static $absoluteApp = null;
31
    private static $urlApp = null;
32
33

    // Extension
34
    const APP_TO_EXT = 'typo3conf/ext/qfq';
35

36
    // API
37
    const EXT_TO_API = 'Classes/Api';
38

39
40
    // Report
    const EXT_TO_REPORT_SYSTEM = 'Resources/Private/Report';
41
    const EXT_TO_FORM_SYSTEM = 'Resources/Private/Form';
42

43
44
45
46
    // Javascript
    const EXT_TO_JAVASCRIPT = 'Resources/Public/JavaScript';
    const JAVASCRIPT_TO_EXT = '../../../';

47
    // Icons
48
    const EXT_TO_GFX_INFO_FILE = 'Resources/Public/icons/note.gif';
49
    const EXT_TO_PATH_ICONS = 'Resources/Public/icons';
50

51
    // Annotate
52
    const EXT_TO_HIGHLIGHT_JSON = 'Resources/Public/Json';
53
54
55
56
57
58

    // Twig
    const EXT_TO_TWIG_TEMPLATES = 'Resources/Public/twig_templates';

    // QFQ Project dir
    private static $appToProject = null;
59
60
61
    private static $projectToForm = null;
    private static $projectToReport = null;
    private const APP_TO_PROJECT_DEFAULT = '../'; // Don't use directly, use appToProject()
62
    private const APP_TO_FILEADMIN = 'fileadmin';
63
    private const APP_TO_PROJECT_IN_PROTECTED = 'fileadmin/protected/qfqProject'; // Don't use directly, use appToProject()
64
    const PROJECT_TO_CONF = 'conf';
65
66
    const PROJECT_TO_FORM_DEFAULT = 'form'; // Don't use directly, use projectToForm()
    const PROJECT_TO_FORM_PHPUNIT = 'form_phpunit'; // Don't use directly, use projectToForm()
67
    const FORM_TO_FORM_BACKUP = '.backup';
68
69
    const PROJECT_TO_REPORT_DEFAULT = 'report'; // Don't use directly, use projectToReport()
    const PROJECT_TO_REPORT_PHPUNIT = 'report_phpunit'; // Don't use directly, use projectToReport()
70
    const REPORT_FILE_TO_BACKUP = '.backup'; // The path from a directory containing a report file to the directory containing backups of that report file
71
72

    // Config
Marc Egger's avatar
Marc Egger committed
73
    const APP_TO_TYPO3_CONF = 'typo3conf';
74

75
    // Log files
76
    private static $absoluteLog = null;
77
78
79
    private static $overloadAbsoluteQfqLogFile = null;
    private static $overloadAbsoluteMailLogFile = null;
    private static $overloadAbsoluteSqlLogFile = null;
80
81
82
83
84
    private const LOG_TO_QFQ_LOG_FILE_DEFAULT = 'qfq.log'; // Don't use directly, use absoluteQfqLogFile()
    private const LOG_TO_MAIL_LOG_FILE_DEFAULT = 'mail.log'; // Don't use directly, use absoluteMailLogFile()
    private const LOG_TO_SQL_LOG_FILE_DEFAULT = 'sql.log'; // Don't use directly, use absoluteSqlLogFile()
    private const PROJECT_TO_LOG_DEFAULT = 'log'; // Don't use directly, use absoluteLog()
    private const APP_TO_LOG_IN_PROTECTED = 'fileadmin/protected/log'; // Don't use directly, use absoluteLog()
85

86
    // Thumbnail
87
88
89
    const APP_TO_SYSTEM_THUMBNAIL_DIR_SECURE_DEFAULT = 'fileadmin/protected/qfqThumbnail';
    const APP_TO_SYSTEM_THUMBNAIL_DIR_PUBLIC_DEFAULT = 'typo3temp/qfqThumbnail';
    const APP_TO_THUMBNAIL_UNKNOWN_TYPE = 'typo3/sysext/frontend/Resources/Public/Icons/FileIcons/';
90

91
    // Send Email
Marc Egger's avatar
Marc Egger committed
92
    const EXT_TO_SEND_EMAIL_FILE = 'Classes/External/sendEmail';
93
94

    /**
95
     * @param array $pathPartsToAppend
96
97
98
     * @return string
     * @throws \UserFormException
     */
99
    public static function absoluteApp(...$pathPartsToAppend): string {
100
101
102
103
104
105
        if (is_null(self::$absoluteApp)) {
            self::findAbsoluteApp();
        }
        if (is_null(self::$absoluteApp)) {
            Thrower::userFormException('Path not set.', 'Absolute app path is null. This should not happen at this point.');
        }
106
        return self::join(self::$absoluteApp, $pathPartsToAppend);
107
108
109
    }

    /**
110
     * @param array $pathPartsToAppend
111
112
113
     * @return string
     * @throws \UserFormException
     */
114
    public static function absoluteExt(...$pathPartsToAppend): string {
115
        return self::absoluteApp(self::appToExt(), $pathPartsToAppend);
116
117
    }

118
    /**
119
     * @param array $pathPartsToAppend
120
121
122
     * @return string
     * @throws \UserFormException
     */
123
    public static function absoluteLog(...$pathPartsToAppend): string {
124
125
126
        if (is_null(self::$absoluteLog)) {
            self::findAbsoluteLog();
        }
127
128
129
        if (is_null(self::$absoluteLog)) {
            Thrower::userFormException('Path not set.', 'Absolute log path is null. This should not happen at this point.');
        }
130
        return self::join(self::$absoluteLog, $pathPartsToAppend);
131
132
133
134
135
136
    }

    /**
     * @return string
     * @throws \UserFormException
     */
137
    public static function absoluteSqlLogFile(): string {
138
139
140
141
142
143
144
145
146
147
        if (is_null(self::$overloadAbsoluteSqlLogFile)) {
            return self::absoluteLog(self::LOG_TO_SQL_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteSqlLogFile;
    }

    /**
     * @return string
     * @throws \UserFormException
     */
148
    public static function absoluteQfqLogFile(): string {
149
150
151
152
153
154
155
156
157
158
        if (is_null(self::$overloadAbsoluteQfqLogFile)) {
            return self::absoluteLog(self::LOG_TO_QFQ_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteQfqLogFile;
    }

    /**
     * @return string
     * @throws \UserFormException
     */
159
    public static function absoluteMailLogFile(): string {
160
161
162
163
164
165
        if (is_null(self::$overloadAbsoluteMailLogFile)) {
            return self::absoluteLog(self::LOG_TO_MAIL_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteMailLogFile;
    }

166
    /**
167
     * @param array $pathPartsToAppend
168
     * @return string
169
     * @throws \CodeException
170
171
     * @throws \UserFormException
     */
172
    public static function appToProject(...$pathPartsToAppend): string {
173
174
175
176
177
178
        if (is_null(self::$appToProject)) {
            self::findAppToProject();
        }
        if (is_null(self::$appToProject)) {
            Thrower::userFormException('Path not set.', 'Project path is null. This should not happen at this point.');
        }
179
        return self::join(self::$appToProject, $pathPartsToAppend);
180
181
    }

182
    /**
183
     * @param array $pathPartsToAppend
184
185
186
     * @return string
     * @throws \UserFormException
     */
187
    public static function absoluteProject(...$pathPartsToAppend): string {
188
        return self::absoluteApp(self::appToProject($pathPartsToAppend));
189
190
    }

191
192
193
194
195
    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
196
    public static function absoluteConf(...$pathPartsToAppend): string {
197
        return self::absoluteProject(self::PROJECT_TO_CONF, $pathPartsToAppend);
198
199
    }

200
    /**
201
     * @param array $pathPartsToAppend
202
203
204
     * @return string
     * @throws \UserFormException
     */
205
    public static function appToExt(...$pathPartsToAppend): string {
206
        return self::join(self::APP_TO_EXT, $pathPartsToAppend);
207
208
    }

209
210
211
    /**
     * @param mixed ...$pathPartsToAppend
     * @return string
212
     * @throws \CodeException
213
     * @throws \UserFormException
214
     * @throws \UserReportException
215
     */
216
    public static function urlApp(...$pathPartsToAppend): string {
217
218
219
220
        if (is_null(self::$urlApp)) {
            self::setUrlApp(Config::get(SYSTEM_BASE_URL));
        }

221
222
        // ensure base url is configured
        if (is_null(self::$urlApp) || self::$urlApp === '') {
223
224
225
226
227
            if($GLOBALS["_SERVER"]["SCRIPT_URI"] ==='') {
                Thrower::userFormException('Base url not configured.', 'Go to QFQ extension configuration in the Typo3 backend and fill in a value for config.baseUrl');
            }else{
                Thrower::userFormException('Base url has now been configured.', 'Please refresh page');
            }
228
229
        }
        return self::join(self::$urlApp, $pathPartsToAppend);
230
231
    }

232
    /**
233
     * @param array $pathPartsToAppend
234
235
236
     * @return string
     * @throws \UserFormException
     */
237
    public static function urlExt(...$pathPartsToAppend): string {
238
        return self::urlApp(self::appToExt($pathPartsToAppend));
239
240
241
    }

    /**
242
     * @param array $pathPartsToAppend
243
244
245
     * @return string
     * @throws \UserFormException
     */
246
    public static function appToApi(...$pathPartsToAppend): string {
247
        return self::join(self::APP_TO_EXT, self::EXT_TO_API, $pathPartsToAppend);
248
249
    }

250
251
252
253
254
    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
255
    public static function urlApi(...$pathPartsToAppend): string {
256
257
258
        return self::urlApp(self::appToApi($pathPartsToAppend));
    }

259
260
261
262
    /**
     * @param mixed ...$pathPartsToAppend
     * @return string
     */
263
    public static function projectToForm(...$pathPartsToAppend): string {
264
265
266
267
268
269
270
271
        $projectToForm = is_null(self::$projectToForm) ? self::PROJECT_TO_FORM_DEFAULT : self::$projectToForm;
        return self::join($projectToForm, $pathPartsToAppend);
    }

    /**
     * @param mixed ...$pathPartsToAppend
     * @return string
     */
272
    public static function projectToReport(...$pathPartsToAppend): string {
273
274
275
276
277
278
279
        $projectToReport = is_null(self::$projectToReport) ? self::PROJECT_TO_REPORT_DEFAULT : self::$projectToReport;
        return self::join($projectToReport, $pathPartsToAppend);
    }

    /**
     * @param string $newPath
     */
280
    public static function setProjectToForm(string $newPath) {
281
282
283
284
285
286
        self::$projectToForm = $newPath;
    }

    /**
     * @param string $newPath
     */
287
    public static function setProjectToReport(string $newPath) {
288
289
290
        self::$projectToReport = $newPath;
    }

291
292
293
294
    /**
     * @param string $newPath
     * @throws \UserFormException
     */
295
    public static function setAbsoluteSqlLogFile(string $newPath) {
296
297
298
299
300
301
302
303
        self::enforcePathIsAbsolute($newPath);
        self::$overloadAbsoluteSqlLogFile = $newPath;
    }

    /**
     * @param string $newPath
     * @throws \UserFormException
     */
304
    public static function setAbsoluteQfqLogFile(string $newPath) {
305
306
307
308
309
310
311
312
        self::enforcePathIsAbsolute($newPath);
        self::$overloadAbsoluteQfqLogFile = $newPath;
    }

    /**
     * @param string $newPath
     * @throws \UserFormException
     */
313
    public static function setAbsoluteMailLogFile(string $newPath) {
314
315
316
317
        self::enforcePathIsAbsolute($newPath);
        self::$overloadAbsoluteMailLogFile = $newPath;
    }

318
319
320
    /**
     * @param $urlApp
     */
321
    public static function setUrlApp($urlApp) {
322
323
324
        self::$urlApp = $urlApp;
    }

325
326
327
    /**
     * Override the paths of sql.log, qfq.log, mail.log using the values from the config file or Typo3.
     *
328
     * @throws \CodeException
329
     * @throws \UserFormException
330
     * @throws \UserReportException
331
     */
332
    public static function overrideLogPathsFromConfig() {
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
        // QFQ log
        $absoluteQfqLogFile = Config::get(SYSTEM_QFQ_LOG_PATHFILENAME);
        if (!empty($absoluteQfqLogFile)) {
            self::setAbsoluteQfqLogFile(self::joinIfNotAbsolute(self::absoluteApp(), $absoluteQfqLogFile));
        }

        // Mail log
        $absoluteMailLogFile = Config::get(SYSTEM_MAIL_LOG_PATHFILENAME);
        if (!empty($absoluteMailLogFile)) {
            self::setAbsoluteMailLogFile(self::joinIfNotAbsolute(self::absoluteApp(), $absoluteMailLogFile));
        }

        // SQL log
        $absoluteSqlLogFile = Config::get(SYSTEM_SQL_LOG_PATHFILENAME);
        if (!empty($absoluteSqlLogFile)) {
            self::setAbsoluteSqlLogFile(self::joinIfNotAbsolute(self::absoluteApp(), $absoluteSqlLogFile));
        }
    }

    /**
     * Return the second path if it is absolute. Otherwise concatenate and return the two paths.
     *
     * @param string $path1
     * @param string $path2
     * @return string
     * @throws \UserFormException
     */
360
    public static function joinIfNotAbsolute(string $path1, string $path2): string {
361
362
363
364
365
366
        if ($path2 !== '' && $path2[0] === '/') {
            return $path2;
        }
        return self::join($path1, $path2);
    }

367
    /**
368
369
     * Join the arguments together as a path. Array arguments are flattened!
     * Trailing path parts may not start with '/'.
370
     *
371
372
373
     * join(['this', 'is'], 'my/wonderful', [['path/of', 'nice'], 'things']) === 'this/is/my/wonderful/path/of/nice/things'
     *
     * @param array $pathPartsToAppend
374
375
     * @return string
     */
376
    public static function join(...$pathPartsToAppend): string {
377
        $parts = $pathPartsToAppend;
378
379
380
381

        // concatenate all parts (arrays are flattened)
        $path = '';
        array_walk_recursive($parts, function ($part) use (&$path) {
382

383
            // filter out empty string and null arguments
384
385
            if (is_null($part) || $part === '') {
                return;
386
387
            }

388
            // first part added without '/'
389
390
391
392
            if ($path === '') {
                $path .= $part;
            } else {

393
                // trailing path parts may not be absolute
394
395
396
397
398
399
400
401
402
403
                if ($part[0] === '/') {
                    throw new \UserFormException(json_encode([
                        ERROR_MESSAGE_TO_USER => 'Failed: Join path.',
                        ERROR_MESSAGE_TO_DEVELOPER => "Trailing path parts may not start with '/'. Trailing part: '$part'."]),
                        ERROR_PATH_INVALID);
                }

                $path .= '/' . $part;
            }
        });
404

405
        // remove multiple occurrences of '/' (but keep http://)
406
        if (preg_match('/^\w*:\/\//', $path, $match)) {
407
408
409
410
411
            $protocol = $match[0];
            $path = substr($path, strlen($protocol));
        } else {
            $protocol = '';
        }
412
        $path = preg_replace('/\/{2,}/', '/', $path);
413
414

        return $protocol . $path;
415
    }
416

417
    /**
418
419
     * @param string $newPath
     */
420
    public static function setAbsoluteApp(string $newPath) {
421
        self::$absoluteApp = $newPath;
422
423
424
425
426
    }

    /**
     * @param string $newPath
     */
427
    private static function setAppToProject(string $newPath) {
428
429
430
431
432
433
        self::$appToProject = $newPath;
    }

    /**
     * @param string $newPath
     */
434
    private static function setAbsoluteLog(string $newPath) {
435
        self::$absoluteLog = $newPath;
436
437
438
    }

    /**
439
440
441
442
443
     * Searches these places for log directory:
     *   1) project-directory/log
     *   2) fileadmin/protected/log
     * If not found create log dir in: project-directory/log
     *
444
445
     * @throws \UserFormException
     */
446
    private static function findAbsoluteLog() {
447

Marc Egger's avatar
Marc Egger committed
448
        // search log dir qfqProject/log
449
450
451
        $absoluteLog = self::absoluteApp(self::appToProject(self::PROJECT_TO_LOG_DEFAULT));
        if (file_exists($absoluteLog)) {
            self::setAbsoluteLog($absoluteLog);
452

453
            // search log dir fileadmin/protected/log
454
455
        } elseif (file_exists(self::absoluteApp(self::APP_TO_LOG_IN_PROTECTED))) {
            self::setAbsoluteLog(self::absoluteApp(self::APP_TO_LOG_IN_PROTECTED));
456

457
            // create default log dir qfqProject/log
458
        } else {
459
460
            HelperFile::createPathRecursive($absoluteLog);
            self::setAbsoluteLog($absoluteLog);
461
462
463
        }
    }

464
465
466
467
468
469
    /**
     * Read the project location from qfq.project.path.php or create the file with default path.
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
470
    private static function findAppToProject() {
471

472
        // does qfq.project.path.php exist? => read path
473
474
475
        $absoluteProjectPathFile = self::absoluteApp(PROJECT_PATH_PHP_FILE);
        if (HelperFile::isReadableException($absoluteProjectPathFile)) {
            self::setAppToProject(HelperFile::include($absoluteProjectPathFile));
476

477
            // does the deprecated config.qfq.php exist? => fileadmin/protected/qfqProject & migrate to qfq.json
478
479
        } elseif (HelperFile::isReadableException(self::absoluteApp(self::APP_TO_TYPO3_CONF, CONFIG_QFQ_PHP))) {
            HelperFile::createPathRecursive(self::absoluteApp(self::APP_TO_PROJECT_IN_PROTECTED));
Marc Egger's avatar
Marc Egger committed
480
            self::setAppToProject(self::APP_TO_PROJECT_IN_PROTECTED);
481
            Config::migrateConfigPhpToJson();
482
            self::writeProjectPathPhp();
483

484
            // does fileadmin exist? => fileadmin/protected/qfqProject
485
486
        } elseif (file_exists(self::absoluteApp(self::APP_TO_FILEADMIN))) {
            HelperFile::createPathRecursive(self::absoluteApp(self::APP_TO_PROJECT_IN_PROTECTED));
487
            self::setAppToProject(self::APP_TO_PROJECT_IN_PROTECTED);
Marc Egger's avatar
Marc Egger committed
488
489
            self::writeProjectPathPhp();

490
            // else => folder above APP
491
492
493
494
495
        } else {
            self::setAppToProject(self::APP_TO_PROJECT_DEFAULT);
            self::writeProjectPathPhp();
        }
    }
Marc Egger's avatar
Marc Egger committed
496

497
498
499
500
501
502
    /**
     * Find the absolute path of the App directory using the path of this file.
     * Fails if typo3conf is not found in that path.
     *
     * @throws \UserFormException
     */
503
    public static function findAbsoluteApp() {
504
        // look for typo3conf directory
505
        $absoluteApp = self::realpath(self::join(__DIR__, '../../../../../../'));
506
        if (!file_exists(self::join($absoluteApp, self::APP_TO_TYPO3_CONF))) {
507
508
509
            Thrower::userFormException('App path seems to be wrong: Directory "typo3conf" not found in app path.',
                " Current app path: $absoluteApp .",
                " In unit tests this can be manually set using Path::setAbsoluteApp() before the path is accessed.");
510
        }
511
        self::setAbsoluteApp($absoluteApp);
512
513
    }

Marc Egger's avatar
Marc Egger committed
514
515
516
517
518
    /**
     * Write the project path configuration file to the project directory.
     *
     * @throws \UserFormException
     */
519
    private static function writeProjectPathPhp() {
Marc Egger's avatar
Marc Egger committed
520
521
522
523
524
525
        $appToProject = self::appToProject();
        $fileContent = <<<EOF
<?php

/**
QFQ project path configuration
Marc Egger's avatar
Marc Egger committed
526
!! ATTENTION !!: The files in the project directory should NOT be served by your http server! 
Marc Egger's avatar
Marc Egger committed
527
528
529
530
531
Only exception: The app directory inside the project directory may be served.
*/

return '$appToProject'; // path relative to app directory (i.e. location of this file).
EOF;
532
        HelperFile::file_put_contents(self::absoluteApp(PROJECT_PATH_PHP_FILE), $fileContent);
Marc Egger's avatar
Marc Egger committed
533
    }
534

535
    /**
536
537
     * Throw exception if path does not start with '/'
     *
538
539
540
     * @param $path
     * @throws \UserFormException
     */
541
    private static function enforcePathIsAbsolute(string $path) {
542
        if ($path !== '' && $path[0] === '/') {
543
544
545
546
547
548
            return;
        }
        Thrower::userFormException('Path is not absolute', "Path does not start with '/' : $path");
    }

    /**
549
     * PHP System function: realpath() with QFQ exception.
550
     * The first argument must be a path to something that exists. Otherwise an exception is thrown!
551
     *
552
     * @param string $pathToSomethingThatExists This file/dir MUST exist! Otherwise exception!
553
     * @param array $pathPartsToAppend The appended path doesnt have to exist.
554
555
556
     * @return string
     * @throws \UserFormException
     */
557
    private static function realpath(string $pathToSomethingThatExists, ...$pathPartsToAppend): string {
558
        $absolutePath = realpath($pathToSomethingThatExists);
559
        if ($absolutePath === false) {
560
            Thrower::userFormException('Path not found.', "Either path does not exist or access not permitted. Make sure the whole path is executable by the current user. Path: $pathToSomethingThatExists.");
561
        }
562
        return self::join($absolutePath, $pathPartsToAppend);
563
    }
564
565
566
567
568
569
570
571

    /**
     * Returns true if the given path contains the double dot operator '..'.
     * File/directory names which contain '..' are not counted.
     *
     * @param string $path
     * @return bool
     */
572
573
    private static function containsDoubleDot(string $path) {
        return $path === '..' || OnString::strStartsWith($path, '../') || OnString::strEndsWith($path, '/..') || OnString::strContains($path, '/../');
574
    }
575
}