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


namespace IMATHUZH\Qfq\Core\Helper;

6
/*
7
8
9
10
11
 * Glossar:
 * - App: directory in which the index.php file is located. All urls should be relative to this.
 * - Ext: directory in which the QFQ extension is loacted. e.g. the folder Classes is in there.
 * - API: api folder of qfq extension
 *
Marc Egger's avatar
Marc Egger committed
12
 * Naming convention of path constants/functions/variables:
13
14
 * 1) name a path by its origin and its destination separated by 'to'. E.g. APP_TO_SYSTEM_LOG, $appToProject.
 * 2) if the destination is a file, append "File". E.g. APP_TO_SYSTEM_QFQ_LOG_FILE.
15
16
17
18
19
 * 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.
20
21
 */

22
use IMATHUZH\Qfq\Core\Exception\Thrower;
23
24
use IMATHUZH\Qfq\Core\Store\Config;

25
26
class Path
{
27
    // App
28
29
    // This is manually set in self::setMainPaths()
    private static $absoluteApp = null;
30
31

    // Extension
32
    const APP_TO_EXT = 'typo3conf/ext/qfq';
33

34
    // API
35
    const EXT_TO_API = 'Classes/Api';
36
    const API_TO_APP = '../../../../../'; // TODO: make relatvie to ext instead
37

38
39
40
41
    // Javascript
    const EXT_TO_JAVASCRIPT = 'Resources/Public/JavaScript';
    const JAVASCRIPT_TO_EXT = '../../../';

42
    // Icons
43
    const EXT_TO_GFX_INFO_FILE = 'Resources/Public/icons/note.gif';
44
    const EXT_TO_PATH_ICONS = 'Resources/Public/icons';
45

46
    // Annotate
47
    const EXT_TO_HIGHLIGHT_JSON_DIR = 'Resources/Public/Json';  // TODO: refactor: remove DIR at the end of the constant name
48
49
50
51
52
53
54

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

    // QFQ Project dir
    private static $appToProject = null;
    private const APP_TO_PROJECT_DEFAULT = '../';
55
    private const APP_TO_FILEADMIN = 'fileadmin';
Marc Egger's avatar
Marc Egger committed
56
    private const APP_TO_PROJECT_IN_PROTECTED = 'fileadmin/protected/qfqProject';
57
    const PROJECT_TO_FORM = 'form';
58
    const PROJECT_TO_CONF = 'conf';
59
    const FORM_TO_FORM_BACKUP = '.backup';
60
    const PROJECT_DIR_TO_REPORT = 'report'; // TODO: refactor: remove DIR from constant name
61
    const REPORT_FILE_TO_BACKUP = '.backup'; // The path from a directory containing a report file to the directory containing backups of that report file
62
63

    // Config
Marc Egger's avatar
Marc Egger committed
64
    const APP_TO_TYPO3_CONF = 'typo3conf';
65

66
    // Log files
67
    private static $absoluteLog = null;
68
69
70
71
72
73
    private static $overloadAbsoluteQfqLogFile = null;
    private static $overloadAbsoluteMailLogFile = null;
    private static $overloadAbsoluteSqlLogFile = null;
    private const LOG_TO_QFQ_LOG_FILE_DEFAULT = 'qfq.log';
    private const LOG_TO_MAIL_LOG_FILE_DEFAULT = 'mail.log';
    private const LOG_TO_SQL_LOG_FILE_DEFAULT = 'sql.log';
74
    private const PROJECT_TO_LOG_DEFAULT = 'log';
75
    private const APP_TO_LOG_IN_PROTECTED = 'fileadmin/protected/log';
76

77
    // Thumbnail
78
79
80
    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/';
81

82
    // Send Email
Marc Egger's avatar
Marc Egger committed
83
    const EXT_TO_SEND_EMAIL_FILE = 'Classes/External/sendEmail';
84

85
    /**
86
     * Manually set the paths which are not constant nor can be inferred by other paths.
87
     * This function must be called at the beginning of every entry point, to tell the Path class, where things are.
88
     *
89
     * @param string|null $absoluteApp
90
91
92
     * @throws \CodeException
     * @throws \UserFormException
     */
93
    public static function setMainPaths(string $absoluteApp = null)
94
    {
95
96
97
98
        if (is_null($absoluteApp)) {
            $absoluteApp = self::findAbsoluteApp();
        }
        self::setAbsoluteApp($absoluteApp);
99
100

        // Only executed on first call:
101
102
        self::findAppToProject();
        self::findAbsoluteLog();
103
    }
104

105
    /**
106
     * @param array $pathPartsToAppend
107
108
109
     * @return string
     * @throws \UserFormException
     */
110
    public static function absoluteApp(...$pathPartsToAppend): string
111
    {
112
113
        self::enforcePathIsSet(self::$absoluteApp);
        return self::join(self::$absoluteApp, $pathPartsToAppend);
114
115
116
    }

    /**
117
     * @param array $pathPartsToAppend
118
119
120
     * @return string
     * @throws \UserFormException
     */
121
    public static function absoluteExt(...$pathPartsToAppend): string
122
    {
123
        return self::absoluteApp(self::appToExt(), $pathPartsToAppend);
124
125
    }

126
    /**
127
     * @param array $pathPartsToAppend
128
129
130
     * @return string
     * @throws \UserFormException
     */
131
    public static function absoluteLog(...$pathPartsToAppend): string
132
    {
133
134
135
        if (is_null(self::$absoluteLog)) {
            self::findAbsoluteLog();
        }
136
137
        self::enforcePathIsSet(self::$absoluteLog);
        return self::join(self::$absoluteLog, $pathPartsToAppend);
138
139
140
141
142
143
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
172
173
174
175
    }

    /**
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteSqlLogFile(): string
    {
        if (is_null(self::$overloadAbsoluteSqlLogFile)) {
            return self::absoluteLog(self::LOG_TO_SQL_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteSqlLogFile;
    }

    /**
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteQfqLogFile(): string
    {
        if (is_null(self::$overloadAbsoluteQfqLogFile)) {
            return self::absoluteLog(self::LOG_TO_QFQ_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteQfqLogFile;
    }

    /**
     * @return string
     * @throws \UserFormException
     */
    public static function absoluteMailLogFile(): string
    {
        if (is_null(self::$overloadAbsoluteMailLogFile)) {
            return self::absoluteLog(self::LOG_TO_MAIL_LOG_FILE_DEFAULT);
        }
        return self::$overloadAbsoluteMailLogFile;
    }

176
    /**
177
     * @param array $pathPartsToAppend
178
179
180
     * @return string
     * @throws \UserFormException
     */
181
    public static function appToProject(...$pathPartsToAppend): string
182
    {
183
        self::enforcePathIsSet(self::$appToProject);
184
        return self::join(self::$appToProject, $pathPartsToAppend);
185
186
    }

187
    /**
188
     * @param array $pathPartsToAppend
189
190
191
     * @return string
     * @throws \UserFormException
     */
192
    public static function absoluteProject(...$pathPartsToAppend): string
193
    {
194
        return self::absoluteApp(self::appToProject($pathPartsToAppend));
195
196
    }

197
198
199
200
201
    /**
     * @param array $pathPartsToAppend
     * @return string
     * @throws \UserFormException
     */
202
    public static function absoluteConf(...$pathPartsToAppend): string
203
    {
204
        return self::absoluteProject(self::PROJECT_TO_CONF, $pathPartsToAppend);
205
206
    }

207
    /**
208
     * @param array $pathPartsToAppend
209
210
211
     * @return string
     * @throws \UserFormException
     */
212
    public static function appToExt(...$pathPartsToAppend): string
213
    {
214
        return self::join(self::APP_TO_EXT, $pathPartsToAppend);
215
216
    }

217
    /**
218
     * @param array $pathPartsToAppend
219
220
221
     * @return string
     * @throws \UserFormException
     */
222
    public static function extToApi(...$pathPartsToAppend): string
223
    {
224
        return self::join(self::EXT_TO_API, $pathPartsToAppend);
225
226
227
    }

    /**
228
     * @param array $pathPartsToAppend
229
230
231
     * @return string
     * @throws \UserFormException
     */
232
    public static function appToApi(...$pathPartsToAppend): string
233
    {
234
        return self::join(self::APP_TO_EXT, self::EXT_TO_API, $pathPartsToAppend);
235
236
    }

237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
    /**
     * @param string $newPath
     * @throws \UserFormException
     */
    public static function setAbsoluteSqlLogFile(string $newPath)
    {
        self::enforcePathIsAbsolute($newPath);
        self::$overloadAbsoluteSqlLogFile = $newPath;
    }

    /**
     * @param string $newPath
     * @throws \UserFormException
     */
    public static function setAbsoluteQfqLogFile(string $newPath)
    {
        self::enforcePathIsAbsolute($newPath);
        self::$overloadAbsoluteQfqLogFile = $newPath;
    }

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

267
268
269
    /**
     * Override the paths of sql.log, qfq.log, mail.log using the values from the config file or Typo3.
     *
270
     * @throws \CodeException
271
     * @throws \UserFormException
272
     * @throws \UserReportException
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
     */
    public static function overrideLogPathsFromConfig()
    {
        // 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
     */
    public static function joinIfNotAbsolute (string $path1, string $path2): string
    {
        if ($path2 !== '' && $path2[0] === '/') {
            return $path2;
        }
        return self::join($path1, $path2);
    }

311
    /**
312
313
     * Join the arguments together as a path. Array arguments are flattened!
     * Trailing path parts may not start with '/'.
314
     *
315
316
317
     * join(['this', 'is'], 'my/wonderful', [['path/of', 'nice'], 'things']) === 'this/is/my/wonderful/path/of/nice/things'
     *
     * @param array $pathPartsToAppend
318
319
     * @return string
     */
320
    public static function join(...$pathPartsToAppend): string
321
    {
322
        $parts = $pathPartsToAppend;
323
324
325
326

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

328
            // filter out empty string and null arguments
329
330
            if (is_null($part) || $part === '') {
                return;
331
332
            }

333
            // first part added without '/'
334
335
336
337
            if ($path === '') {
                $path .= $part;
            } else {

338
                // trailing path parts may not be absolute
339
340
341
342
343
344
345
346
347
348
                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;
            }
        });
349

350
        // remove multiple occurrences of '/'
351
        return preg_replace('/\/{2,}/','/', $path);
352
    }
353
354
355

    ///////////////////////////////////////////////////  Private  //////////////////////////////////////////////////////

356
    /**
357
358
     * @param string $newPath
     */
359
    private static function setAbsoluteApp(string $newPath)
360
    {
361
        self::$absoluteApp = $newPath;
362
363
364
365
366
367
368
369
370
371
372
373
374
375
    }

    /**
     * @param string $newPath
     */
    private static function setAppToProject(string $newPath)
    {
        self::$appToProject = $newPath;
    }

    /**
     * @param string $newPath
     * @return string
     */
376
    private static function setAbsoluteLog(string $newPath)
377
    {
378
        self::$absoluteLog = $newPath;
379
380
381
    }

    /**
382
383
384
385
386
     * Searches these places for log directory:
     *   1) project-directory/log
     *   2) fileadmin/protected/log
     * If not found create log dir in: project-directory/log
     *
387
388
     * @throws \UserFormException
     */
389
    private static function findAbsoluteLog()
390
    {
391
392
393
394
395
        if (!is_null(self::$absoluteLog)) {
            // only execute once
            return;
        }

Marc Egger's avatar
Marc Egger committed
396
        // search log dir qfqProject/log
397
398
399
        $absoluteLog = self::absoluteApp(self::appToProject(self::PROJECT_TO_LOG_DEFAULT));
        if (file_exists($absoluteLog)) {
            self::setAbsoluteLog($absoluteLog);
400

Marc Egger's avatar
Marc Egger committed
401
        // search log dir fileadmin/protected/log
402
403
        } elseif (file_exists(self::absoluteApp(self::APP_TO_LOG_IN_PROTECTED))) {
            self::setAbsoluteLog(self::absoluteApp(self::APP_TO_LOG_IN_PROTECTED));
404

Marc Egger's avatar
Marc Egger committed
405
        // create default log dir qfqProject/log
406
        } else {
407
408
            HelperFile::createPathRecursive($absoluteLog);
            self::setAbsoluteLog($absoluteLog);
409
410
411
        }
    }

412
413
414
415
416
417
    /**
     * Read the project location from qfq.project.path.php or create the file with default path.
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
418
    private static function findAppToProject()
419
    {
420
421
422
423
424
        if (!is_null(self::$appToProject)) {
            // only execute once
            return;
        }

425
        // does qfq.project.path.php exist? => read path
426
427
428
        $absoluteProjectPathFile = self::absoluteApp(PROJECT_PATH_PHP_FILE);
        if (HelperFile::isReadableException($absoluteProjectPathFile)) {
            self::setAppToProject(HelperFile::include($absoluteProjectPathFile));
429

430
        // does the deprecated config.qfq.php exist? => fileadmin/protected/qfqProject & migrate to qfq.json
431
432
        } 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
433
            self::setAppToProject(self::APP_TO_PROJECT_IN_PROTECTED);
434
            Config::migrateConfigPhpToJson();
435
            self::writeProjectPathPhp();
436

437
        // does fileadmin exist? => fileadmin/protected/qfqProject
438
439
        } elseif (file_exists(self::absoluteApp(self::APP_TO_FILEADMIN))) {
            HelperFile::createPathRecursive(self::absoluteApp(self::APP_TO_PROJECT_IN_PROTECTED));
440
            self::setAppToProject(self::APP_TO_PROJECT_IN_PROTECTED);
Marc Egger's avatar
Marc Egger committed
441
442
            self::writeProjectPathPhp();

443
        // else => folder above APP
444
445
446
447
448
        } else {
            self::setAppToProject(self::APP_TO_PROJECT_DEFAULT);
            self::writeProjectPathPhp();
        }
    }
Marc Egger's avatar
Marc Egger committed
449

450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
    /**
     * 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
     */
    private static function findAbsoluteApp()
    {
        $absoluteApp = self::realpath(self::join(__DIR__, '../../../../../../'));
        if (!file_exists(self::join($absoluteApp, self::APP_TO_TYPO3_CONF)))
        {
            Thrower::userFormException('App path seems to be wrong: Directory "typo3conf" not found in app path.'
                , "Current app path: $absoluteApp");
        }

        return $absoluteApp;
    }

Marc Egger's avatar
Marc Egger committed
468
469
470
471
472
473
474
475
476
477
478
479
480
    /**
     * Write the project path configuration file to the project directory.
     *
     * @throws \UserFormException
     */
    private static function writeProjectPathPhp()
    {
        $appToProject = self::appToProject();
        $fileContent = <<<EOF
<?php

/**
QFQ project path configuration
Marc Egger's avatar
Marc Egger committed
481
!! ATTENTION !!: The files in the project directory should NOT be served by your http server! 
Marc Egger's avatar
Marc Egger committed
482
483
484
485
486
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;
487
        HelperFile::file_put_contents(self::absoluteApp(PROJECT_PATH_PHP_FILE), $fileContent);
Marc Egger's avatar
Marc Egger committed
488
    }
489
490

    /**
491
492
     * Throw an exception if the given path is not set (i.e. === null).
     *
493
494
495
496
497
     * @param $path
     * @throws \UserFormException
     */
    private static function enforcePathIsSet($path) {
        if(is_null($path)) {
498
            Thrower::userFormException('Path accessed before set.', 'Make sure Path::setMainPaths(...) is called before this code is executed..');
499
500
        }
    }
501
502

    /**
503
504
     * Throw exception if path does not start with '/'
     *
505
506
507
     * @param $path
     * @throws \UserFormException
     */
508
509
510
511
512
513
514
515
    private static function enforcePathIsAbsolute(string $path) {
        if($path !== '' && $path[0] === '/') {
            return;
        }
        Thrower::userFormException('Path is not absolute', "Path does not start with '/' : $path");
    }

    /**
516
     * PHP System function: realpath() with QFQ exception.
517
     * The first argument must be a path to something that exists. Otherwise an exception is thrown!
518
     *
519
     * @param string $pathToSomethingThatExists This file/dir MUST exist! Otherwise exception!
520
     * @param array $pathPartsToAppend The appended path doesnt have to exist.
521
522
523
     * @return string
     * @throws \UserFormException
     */
524
    private static function realpath(string $pathToSomethingThatExists, ...$pathPartsToAppend): string
525
    {
526
        $absolutePath = realpath($pathToSomethingThatExists);
527
        if ($absolutePath === false) {
528
            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.");
529
        }
530
        return self::join($absolutePath, $pathPartsToAppend);
531
    }
532
}