create-prisma-php-app 4.2.4-beta → 4.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.php +447 -253
- package/dist/index.js +1 -1
- package/dist/prisma-php.js +1 -1
- package/dist/public/js/pp-reactive-v1.js +1 -1
- package/dist/settings/bs-config.ts +16 -3
- package/dist/settings/vite-plugins/generate-global-types.ts +246 -0
- package/dist/src/Lib/Auth/Auth.php +53 -50
- package/dist/src/app/index.php +2 -2
- package/dist/src/app/layout.php +1 -2
- package/dist/src/app/not-found.php +2 -2
- package/dist/tsconfig.json +1 -1
- package/dist/vite.config.ts +20 -6
- package/package.json +4 -4
package/dist/bootstrap.php
CHANGED
|
@@ -24,9 +24,7 @@ use PP\MainLayout;
|
|
|
24
24
|
use PP\PHPX\TemplateCompiler;
|
|
25
25
|
use PP\CacheHandler;
|
|
26
26
|
use PP\ErrorHandler;
|
|
27
|
-
use
|
|
28
|
-
use Firebase\JWT\Key;
|
|
29
|
-
use PP\PartialRenderer;
|
|
27
|
+
use PP\Attributes\Exposed;
|
|
30
28
|
|
|
31
29
|
final class Bootstrap extends RuntimeException
|
|
32
30
|
{
|
|
@@ -40,8 +38,6 @@ final class Bootstrap extends RuntimeException
|
|
|
40
38
|
public static bool $isContentVariableIncluded = false;
|
|
41
39
|
public static bool $secondRequestC69CD = false;
|
|
42
40
|
public static array $requestFilesData = [];
|
|
43
|
-
public static array $partialSelectors = [];
|
|
44
|
-
public static bool $isPartialRequest = false;
|
|
45
41
|
|
|
46
42
|
private string $context;
|
|
47
43
|
|
|
@@ -78,7 +74,7 @@ final class Bootstrap extends RuntimeException
|
|
|
78
74
|
'samesite' => 'Lax',
|
|
79
75
|
]);
|
|
80
76
|
|
|
81
|
-
self::
|
|
77
|
+
self::setCsrfCookie();
|
|
82
78
|
|
|
83
79
|
self::$secondRequestC69CD = Request::$data['secondRequestC69CD'] ?? false;
|
|
84
80
|
|
|
@@ -106,24 +102,20 @@ final class Bootstrap extends RuntimeException
|
|
|
106
102
|
self::authenticateUserToken();
|
|
107
103
|
|
|
108
104
|
self::$requestFilePath = APP_PATH . Request::$pathname;
|
|
109
|
-
self::$parentLayoutPath = APP_PATH . '/layout.php';
|
|
110
105
|
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
if (!empty(self::$layoutsToInclude)) {
|
|
107
|
+
self::$parentLayoutPath = self::$layoutsToInclude[0];
|
|
108
|
+
self::$isParentLayout = true;
|
|
109
|
+
} else {
|
|
110
|
+
self::$parentLayoutPath = APP_PATH . '/layout.php';
|
|
111
|
+
self::$isParentLayout = false;
|
|
112
|
+
}
|
|
113
113
|
|
|
114
114
|
self::$isContentVariableIncluded = self::containsChildren(self::$parentLayoutPath);
|
|
115
115
|
if (!self::$isContentVariableIncluded) {
|
|
116
116
|
self::$isContentIncluded = true;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
self::$isPartialRequest =
|
|
120
|
-
!empty(Request::$data['ppSync71163'])
|
|
121
|
-
&& !empty(Request::$data['selectors'])
|
|
122
|
-
&& self::$secondRequestC69CD;
|
|
123
|
-
|
|
124
|
-
if (self::$isPartialRequest) {
|
|
125
|
-
self::$partialSelectors = (array)Request::$data['selectors'];
|
|
126
|
-
}
|
|
127
119
|
self::$requestFilesData = PrismaPHPSettings::$includeFiles;
|
|
128
120
|
|
|
129
121
|
ErrorHandler::checkFatalError();
|
|
@@ -131,66 +123,62 @@ final class Bootstrap extends RuntimeException
|
|
|
131
123
|
|
|
132
124
|
private static function isLocalStoreCallback(): void
|
|
133
125
|
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (empty($data['callback'])) {
|
|
137
|
-
self::jsonExit(['success' => false, 'error' => 'Callback not provided', 'response' => null]);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
$aesKey = self::getAesKeyFromJwt();
|
|
142
|
-
} catch (RuntimeException $e) {
|
|
143
|
-
self::jsonExit(['success' => false, 'error' => $e->getMessage()]);
|
|
126
|
+
if (empty($_SERVER['HTTP_X_PP_FUNCTION'])) {
|
|
127
|
+
return;
|
|
144
128
|
}
|
|
145
129
|
|
|
146
|
-
|
|
147
|
-
$callbackName = self::decryptCallback($data['callback'], $aesKey);
|
|
148
|
-
} catch (RuntimeException $e) {
|
|
149
|
-
self::jsonExit(['success' => false, 'error' => $e->getMessage()]);
|
|
150
|
-
}
|
|
130
|
+
$callbackName = $_SERVER['HTTP_X_PP_FUNCTION'];
|
|
151
131
|
|
|
152
132
|
if ($callbackName === PrismaPHPSettings::$localStoreKey) {
|
|
133
|
+
self::validateCsrfToken();
|
|
153
134
|
self::jsonExit(['success' => true, 'response' => 'localStorage updated']);
|
|
154
135
|
}
|
|
155
136
|
}
|
|
156
137
|
|
|
157
|
-
private static function
|
|
138
|
+
private static function setCsrfCookie(): void
|
|
158
139
|
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
140
|
+
if (!isset($_COOKIE['prisma_php_csrf'])) {
|
|
141
|
+
$secret = $_ENV['FUNCTION_CALL_SECRET'] ?? 'pp_default_insecure_secret';
|
|
142
|
+
$nonce = bin2hex(random_bytes(16));
|
|
143
|
+
$signature = hash_hmac('sha256', $nonce, $secret);
|
|
144
|
+
$token = $nonce . '.' . $signature;
|
|
145
|
+
|
|
146
|
+
setcookie('prisma_php_csrf', $token, [
|
|
147
|
+
'expires' => time() + 3600,
|
|
148
|
+
'path' => '/',
|
|
149
|
+
'secure' => true,
|
|
150
|
+
'httponly' => false,
|
|
151
|
+
'samesite' => 'Lax',
|
|
152
|
+
]);
|
|
153
|
+
$_COOKIE['prisma_php_csrf'] = $token;
|
|
162
154
|
}
|
|
155
|
+
}
|
|
163
156
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
157
|
+
private static function validateCsrfToken(): void
|
|
158
|
+
{
|
|
159
|
+
$headerToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
|
160
|
+
$cookieToken = $_COOKIE['prisma_php_csrf'] ?? '';
|
|
161
|
+
$secret = $_ENV['FUNCTION_CALL_SECRET'] ?? '';
|
|
162
|
+
|
|
163
|
+
if (empty($headerToken) || empty($cookieToken)) {
|
|
164
|
+
self::jsonExit(['success' => false, 'error' => 'CSRF token missing']);
|
|
173
165
|
}
|
|
174
166
|
|
|
175
|
-
$
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
'exp' => time() + 3600,
|
|
179
|
-
'iat' => time(),
|
|
180
|
-
];
|
|
181
|
-
$jwt = JWT::encode($payload, $hmacSecret, 'HS256');
|
|
167
|
+
if (!hash_equals($cookieToken, $headerToken)) {
|
|
168
|
+
self::jsonExit(['success' => false, 'error' => 'CSRF token mismatch']);
|
|
169
|
+
}
|
|
182
170
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
]
|
|
193
|
-
|
|
171
|
+
$parts = explode('.', $cookieToken);
|
|
172
|
+
if (count($parts) !== 2) {
|
|
173
|
+
self::jsonExit(['success' => false, 'error' => 'Invalid CSRF token format']);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
[$nonce, $signature] = $parts;
|
|
177
|
+
$expectedSignature = hash_hmac('sha256', $nonce, $secret);
|
|
178
|
+
|
|
179
|
+
if (!hash_equals($expectedSignature, $signature)) {
|
|
180
|
+
self::jsonExit(['success' => false, 'error' => 'Invalid CSRF token signature']);
|
|
181
|
+
}
|
|
194
182
|
}
|
|
195
183
|
|
|
196
184
|
private static function fileExistsCached(string $path): bool
|
|
@@ -270,72 +258,245 @@ final class Bootstrap extends RuntimeException
|
|
|
270
258
|
}
|
|
271
259
|
}
|
|
272
260
|
|
|
261
|
+
$layoutsToInclude = self::collectLayouts($pathname, $groupFolder, $dynamicRoute ?? null);
|
|
262
|
+
} else {
|
|
263
|
+
$contentData = self::getFilePrecedence();
|
|
264
|
+
$includePath = $baseDir . $contentData['file'];
|
|
265
|
+
$layoutsToInclude = self::collectRootLayouts($contentData['file']);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return [
|
|
269
|
+
'path' => $includePath,
|
|
270
|
+
'layouts' => $layoutsToInclude,
|
|
271
|
+
'pathname' => $pathname,
|
|
272
|
+
'uri' => $requestUri
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private static function collectLayouts(string $pathname, ?string $groupFolder, ?string $dynamicRoute): array
|
|
277
|
+
{
|
|
278
|
+
$layoutsToInclude = [];
|
|
279
|
+
$baseDir = APP_PATH;
|
|
280
|
+
|
|
281
|
+
$rootLayout = $baseDir . '/layout.php';
|
|
282
|
+
if (self::fileExistsCached($rootLayout)) {
|
|
283
|
+
$layoutsToInclude[] = $rootLayout;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
$groupName = null;
|
|
287
|
+
$groupParentPath = '';
|
|
288
|
+
$pathAfterGroup = '';
|
|
289
|
+
|
|
290
|
+
if ($groupFolder) {
|
|
291
|
+
$normalizedGroupFolder = str_replace('\\', '/', $groupFolder);
|
|
292
|
+
|
|
293
|
+
if (preg_match('#^\.?/src/app/(.+)/\(([^)]+)\)/(.+)$#', $normalizedGroupFolder, $matches)) {
|
|
294
|
+
$groupParentPath = $matches[1];
|
|
295
|
+
$groupName = $matches[2];
|
|
296
|
+
$pathAfterGroup = dirname($matches[3]);
|
|
297
|
+
if ($pathAfterGroup === '.') {
|
|
298
|
+
$pathAfterGroup = '';
|
|
299
|
+
}
|
|
300
|
+
} elseif (preg_match('#^\.?/src/app/\(([^)]+)\)/(.+)$#', $normalizedGroupFolder, $matches)) {
|
|
301
|
+
$groupName = $matches[1];
|
|
302
|
+
$pathAfterGroup = dirname($matches[2]);
|
|
303
|
+
if ($pathAfterGroup === '.') {
|
|
304
|
+
$pathAfterGroup = '';
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if ($groupName && $groupParentPath) {
|
|
273
310
|
$currentPath = $baseDir;
|
|
274
|
-
$
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
$
|
|
311
|
+
foreach (explode('/', $groupParentPath) as $segment) {
|
|
312
|
+
if (empty($segment)) continue;
|
|
313
|
+
|
|
314
|
+
$currentPath .= '/' . $segment;
|
|
315
|
+
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
316
|
+
|
|
317
|
+
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
318
|
+
$layoutsToInclude[] = $potentialLayoutPath;
|
|
319
|
+
}
|
|
278
320
|
}
|
|
279
321
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
322
|
+
$groupLayoutPath = $baseDir . '/' . $groupParentPath . "/($groupName)/layout.php";
|
|
323
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
324
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!empty($pathAfterGroup)) {
|
|
328
|
+
$currentPath = $baseDir . '/' . $groupParentPath . "/($groupName)";
|
|
329
|
+
foreach (explode('/', $pathAfterGroup) as $segment) {
|
|
330
|
+
if (empty($segment)) continue;
|
|
331
|
+
|
|
332
|
+
$currentPath .= '/' . $segment;
|
|
333
|
+
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
334
|
+
|
|
335
|
+
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
336
|
+
$layoutsToInclude[] = $potentialLayoutPath;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} elseif ($groupName && !$groupParentPath) {
|
|
341
|
+
$groupLayoutPath = $baseDir . "/($groupName)/layout.php";
|
|
342
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
343
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!empty($pathAfterGroup)) {
|
|
347
|
+
$currentPath = $baseDir . "/($groupName)";
|
|
348
|
+
foreach (explode('/', $pathAfterGroup) as $segment) {
|
|
349
|
+
if (empty($segment)) continue;
|
|
350
|
+
|
|
351
|
+
$currentPath .= '/' . $segment;
|
|
352
|
+
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
353
|
+
|
|
354
|
+
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
355
|
+
$layoutsToInclude[] = $potentialLayoutPath;
|
|
356
|
+
}
|
|
283
357
|
}
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
$currentPath = $baseDir;
|
|
361
|
+
foreach (explode('/', $pathname) as $segment) {
|
|
362
|
+
if (empty($segment)) continue;
|
|
284
363
|
|
|
285
364
|
$currentPath .= '/' . $segment;
|
|
286
365
|
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
366
|
+
|
|
367
|
+
if ($potentialLayoutPath === $rootLayout) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
287
371
|
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
288
372
|
$layoutsToInclude[] = $potentialLayoutPath;
|
|
289
373
|
}
|
|
290
374
|
}
|
|
375
|
+
}
|
|
291
376
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
377
|
+
if (isset($dynamicRoute) && !empty($dynamicRoute)) {
|
|
378
|
+
$currentDynamicPath = $baseDir;
|
|
379
|
+
foreach (explode('/', $dynamicRoute) as $segment) {
|
|
380
|
+
if (empty($segment) || $segment === 'src' || $segment === 'app') {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
$currentDynamicPath .= '/' . $segment;
|
|
385
|
+
$potentialDynamicRoute = $currentDynamicPath . '/layout.php';
|
|
386
|
+
if (self::fileExistsCached($potentialDynamicRoute) && !in_array($potentialDynamicRoute, $layoutsToInclude, true)) {
|
|
387
|
+
$layoutsToInclude[] = $potentialDynamicRoute;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (empty($layoutsToInclude)) {
|
|
393
|
+
$layoutsToInclude = self::findFirstGroupLayout();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return $layoutsToInclude;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private static function collectRootLayouts(?string $matchedContentFile = null): array
|
|
400
|
+
{
|
|
401
|
+
$layoutsToInclude = [];
|
|
402
|
+
$baseDir = APP_PATH;
|
|
403
|
+
$rootLayout = $baseDir . '/layout.php';
|
|
404
|
+
|
|
405
|
+
if (self::fileExistsCached($rootLayout)) {
|
|
406
|
+
$layoutsToInclude[] = $rootLayout;
|
|
407
|
+
} else {
|
|
408
|
+
$layoutsToInclude = self::findFirstGroupLayout($matchedContentFile);
|
|
409
|
+
|
|
410
|
+
if (empty($layoutsToInclude)) {
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return $layoutsToInclude;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private static function findFirstGroupLayout(?string $matchedContentFile = null): array
|
|
419
|
+
{
|
|
420
|
+
$baseDir = APP_PATH;
|
|
421
|
+
$layoutsToInclude = [];
|
|
422
|
+
|
|
423
|
+
if (is_dir($baseDir)) {
|
|
424
|
+
$items = scandir($baseDir);
|
|
425
|
+
|
|
426
|
+
if ($matchedContentFile) {
|
|
427
|
+
foreach ($items as $item) {
|
|
428
|
+
if ($item === '.' || $item === '..') {
|
|
299
429
|
continue;
|
|
300
430
|
}
|
|
301
431
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
432
|
+
if (preg_match('/^\([^)]+\)$/', $item)) {
|
|
433
|
+
if (strpos($matchedContentFile, "/$item/") === 0) {
|
|
434
|
+
$groupLayoutPath = $baseDir . '/' . $item . '/layout.php';
|
|
435
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
436
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
437
|
+
return $layoutsToInclude;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
306
440
|
}
|
|
307
441
|
}
|
|
308
442
|
}
|
|
309
443
|
|
|
310
|
-
|
|
311
|
-
$
|
|
444
|
+
foreach ($items as $item) {
|
|
445
|
+
if ($item === '.' || $item === '..') {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (preg_match('/^\([^)]+\)$/', $item)) {
|
|
450
|
+
$groupLayoutPath = $baseDir . '/' . $item . '/layout.php';
|
|
451
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
452
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
312
456
|
}
|
|
313
|
-
} else {
|
|
314
|
-
$includePath = $baseDir . self::getFilePrecedence();
|
|
315
457
|
}
|
|
316
458
|
|
|
317
|
-
return
|
|
318
|
-
'path' => $includePath,
|
|
319
|
-
'layouts' => $layoutsToInclude,
|
|
320
|
-
'pathname' => $pathname,
|
|
321
|
-
'uri' => $requestUri
|
|
322
|
-
];
|
|
459
|
+
return $layoutsToInclude;
|
|
323
460
|
}
|
|
324
461
|
|
|
325
|
-
private static function getFilePrecedence():
|
|
462
|
+
private static function getFilePrecedence(): array
|
|
326
463
|
{
|
|
464
|
+
$baseDir = APP_PATH;
|
|
465
|
+
$result = ['file' => null];
|
|
466
|
+
|
|
327
467
|
foreach (PrismaPHPSettings::$routeFiles as $route) {
|
|
328
468
|
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
329
469
|
continue;
|
|
330
470
|
}
|
|
331
471
|
if (preg_match('/^\.\/src\/app\/route\.php$/', $route)) {
|
|
332
|
-
return '/route.php';
|
|
472
|
+
return ['file' => '/route.php'];
|
|
333
473
|
}
|
|
334
474
|
if (preg_match('/^\.\/src\/app\/index\.php$/', $route)) {
|
|
335
|
-
return '/index.php';
|
|
475
|
+
return ['file' => '/index.php'];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (is_dir($baseDir)) {
|
|
480
|
+
$items = scandir($baseDir);
|
|
481
|
+
foreach ($items as $item) {
|
|
482
|
+
if ($item === '.' || $item === '..') {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (preg_match('/^\([^)]+\)$/', $item)) {
|
|
487
|
+
$groupDir = $baseDir . '/' . $item;
|
|
488
|
+
|
|
489
|
+
if (file_exists($groupDir . '/route.php')) {
|
|
490
|
+
return ['file' => '/' . $item . '/route.php'];
|
|
491
|
+
}
|
|
492
|
+
if (file_exists($groupDir . '/index.php')) {
|
|
493
|
+
return ['file' => '/' . $item . '/index.php'];
|
|
494
|
+
}
|
|
495
|
+
}
|
|
336
496
|
}
|
|
337
497
|
}
|
|
338
|
-
|
|
498
|
+
|
|
499
|
+
return $result;
|
|
339
500
|
}
|
|
340
501
|
|
|
341
502
|
private static function uriExtractor(string $scriptUrl): string
|
|
@@ -424,17 +585,22 @@ final class Bootstrap extends RuntimeException
|
|
|
424
585
|
}
|
|
425
586
|
}
|
|
426
587
|
} elseif (strpos($normalizedRoute, '[...') !== false) {
|
|
427
|
-
|
|
588
|
+
$cleanedRoute = preg_replace('/\(.+\)/', '', $normalizedRoute);
|
|
589
|
+
$cleanedRoute = preg_replace('/\/+/', '/', $cleanedRoute);
|
|
590
|
+
$staticPart = preg_replace('/\[\.\.\..*?\].*/', '', $cleanedRoute);
|
|
591
|
+
$staticSegments = array_filter(explode('/', $staticPart));
|
|
592
|
+
$minRequiredSegments = count($staticSegments);
|
|
593
|
+
|
|
594
|
+
if (count($pathnameSegments) < $minRequiredSegments) {
|
|
428
595
|
continue;
|
|
429
596
|
}
|
|
430
597
|
|
|
431
|
-
$cleanedNormalizedRoute =
|
|
432
|
-
$
|
|
433
|
-
$dynamicSegmentRoute = preg_replace('/\[\.\.\..*?\].*/', '', $cleanedNormalizedRoute);
|
|
598
|
+
$cleanedNormalizedRoute = $cleanedRoute;
|
|
599
|
+
$dynamicSegmentRoute = $staticPart;
|
|
434
600
|
|
|
435
601
|
if (strpos("/src/app/$normalizedPathname", $dynamicSegmentRoute) === 0) {
|
|
436
602
|
$trimmedPathname = str_replace($dynamicSegmentRoute, '', "/src/app/$normalizedPathname");
|
|
437
|
-
$pathnameParts = explode('/', trim($trimmedPathname, '/'));
|
|
603
|
+
$pathnameParts = $trimmedPathname === '' ? [] : explode('/', trim($trimmedPathname, '/'));
|
|
438
604
|
|
|
439
605
|
if (preg_match('/\[\.\.\.(.*?)\]/', $normalizedRoute, $matches)) {
|
|
440
606
|
$dynamicParam = $matches[1];
|
|
@@ -451,18 +617,14 @@ final class Bootstrap extends RuntimeException
|
|
|
451
617
|
|
|
452
618
|
if (strpos($normalizedRoute, 'index.php') !== false) {
|
|
453
619
|
$segmentMatch = "[...$dynamicParam]";
|
|
454
|
-
$index = array_search($segmentMatch, $filteredRouteSegments);
|
|
455
620
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
621
|
+
$dynamicRoutePathname = str_replace($segmentMatch, implode('/', $pathnameParts), $cleanedNormalizedRoute);
|
|
622
|
+
$dynamicRoutePathnameDirname = rtrim(dirname($dynamicRoutePathname), '/');
|
|
623
|
+
$expectedPathname = rtrim("/src/app/$normalizedPathname", '/');
|
|
459
624
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
$pathnameMatch = $normalizedRoute;
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
625
|
+
if ($expectedPathname === $dynamicRoutePathnameDirname) {
|
|
626
|
+
$pathnameMatch = $normalizedRoute;
|
|
627
|
+
break;
|
|
466
628
|
}
|
|
467
629
|
}
|
|
468
630
|
}
|
|
@@ -483,6 +645,7 @@ final class Bootstrap extends RuntimeException
|
|
|
483
645
|
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
484
646
|
continue;
|
|
485
647
|
}
|
|
648
|
+
|
|
486
649
|
$normalizedRoute = trim(str_replace('\\', '/', $route), '.');
|
|
487
650
|
$cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
|
|
488
651
|
|
|
@@ -494,22 +657,26 @@ final class Bootstrap extends RuntimeException
|
|
|
494
657
|
}
|
|
495
658
|
}
|
|
496
659
|
|
|
497
|
-
|
|
498
|
-
|
|
660
|
+
if (!$bestMatch) {
|
|
661
|
+
foreach (PrismaPHPSettings::$routeFiles as $route) {
|
|
662
|
+
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
499
665
|
|
|
500
|
-
|
|
501
|
-
{
|
|
502
|
-
$lastSlashPos = strrpos($pathname, '/');
|
|
503
|
-
if ($lastSlashPos === false) {
|
|
504
|
-
return "";
|
|
505
|
-
}
|
|
666
|
+
$normalizedRoute = trim(str_replace('\\', '/', $route), '.');
|
|
506
667
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
668
|
+
if (preg_match('/\/\(([^)]+)\)\//', $normalizedRoute, $matches)) {
|
|
669
|
+
$cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
|
|
670
|
+
|
|
671
|
+
if ($cleanedRoute === $routeFile || $cleanedRoute === $indexFile) {
|
|
672
|
+
$bestMatch = $normalizedRoute;
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
510
677
|
}
|
|
511
678
|
|
|
512
|
-
return
|
|
679
|
+
return $bestMatch;
|
|
513
680
|
}
|
|
514
681
|
|
|
515
682
|
private static function singleDynamicRoute($pathnameSegments, $routeSegments)
|
|
@@ -578,7 +745,7 @@ final class Bootstrap extends RuntimeException
|
|
|
578
745
|
}
|
|
579
746
|
}
|
|
580
747
|
|
|
581
|
-
public static function
|
|
748
|
+
public static function containsChildren($filePath): bool
|
|
582
749
|
{
|
|
583
750
|
if (!self::fileExistsCached($filePath)) {
|
|
584
751
|
return false;
|
|
@@ -589,52 +756,51 @@ final class Bootstrap extends RuntimeException
|
|
|
589
756
|
return false;
|
|
590
757
|
}
|
|
591
758
|
|
|
592
|
-
$pattern = '/\<\?=\s*MainLayout::\$
|
|
759
|
+
$pattern = '/\<\?=\s*MainLayout::\$children\s*;?\s*\?>|echo\s*MainLayout::\$children\s*;?/';
|
|
593
760
|
return (bool) preg_match($pattern, $fileContent);
|
|
594
761
|
}
|
|
595
762
|
|
|
596
|
-
private static function
|
|
763
|
+
private static function convertToArrayObject($data)
|
|
597
764
|
{
|
|
598
|
-
if (!
|
|
599
|
-
return
|
|
765
|
+
if (!is_array($data)) {
|
|
766
|
+
return $data;
|
|
600
767
|
}
|
|
601
768
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return false;
|
|
769
|
+
if (empty($data)) {
|
|
770
|
+
return $data;
|
|
605
771
|
}
|
|
606
772
|
|
|
607
|
-
$
|
|
608
|
-
return (bool) preg_match($pattern, $fileContent);
|
|
609
|
-
}
|
|
773
|
+
$isAssoc = array_keys($data) !== range(0, count($data) - 1);
|
|
610
774
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
775
|
+
if ($isAssoc) {
|
|
776
|
+
$obj = new stdClass();
|
|
777
|
+
foreach ($data as $key => $value) {
|
|
778
|
+
$obj->$key = self::convertToArrayObject($value);
|
|
779
|
+
}
|
|
780
|
+
return $obj;
|
|
781
|
+
} else {
|
|
782
|
+
return array_map([self::class, 'convertToArrayObject'], $data);
|
|
783
|
+
}
|
|
614
784
|
}
|
|
615
785
|
|
|
616
786
|
public static function wireCallback(): void
|
|
617
787
|
{
|
|
618
|
-
$
|
|
788
|
+
$callbackName = $_SERVER['HTTP_X_PP_FUNCTION'] ?? null;
|
|
619
789
|
|
|
620
|
-
if (empty($
|
|
621
|
-
self::jsonExit(['success' => false, 'error' => 'Callback not provided', 'response' => null]);
|
|
790
|
+
if (empty($callbackName)) {
|
|
791
|
+
self::jsonExit(['success' => false, 'error' => 'Callback header not provided', 'response' => null]);
|
|
622
792
|
}
|
|
623
793
|
|
|
624
|
-
|
|
625
|
-
$aesKey = self::getAesKeyFromJwt();
|
|
626
|
-
} catch (RuntimeException $e) {
|
|
627
|
-
self::jsonExit(['success' => false, 'error' => $e->getMessage()]);
|
|
628
|
-
}
|
|
794
|
+
self::validateCsrfToken();
|
|
629
795
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
} catch (RuntimeException $e) {
|
|
633
|
-
self::jsonExit(['success' => false, 'error' => $e->getMessage()]);
|
|
796
|
+
if (!preg_match('/^[a-zA-Z0-9_:\->]+$/', $callbackName)) {
|
|
797
|
+
self::jsonExit(['success' => false, 'error' => 'Invalid callback format']);
|
|
634
798
|
}
|
|
635
799
|
|
|
800
|
+
$data = self::getRequestData();
|
|
636
801
|
$args = self::convertToArrayObject($data);
|
|
637
|
-
|
|
802
|
+
|
|
803
|
+
$out = str_contains($callbackName, '->') || str_contains($callbackName, '::')
|
|
638
804
|
? self::dispatchMethod($callbackName, $args)
|
|
639
805
|
: self::dispatchFunction($callbackName, $args);
|
|
640
806
|
|
|
@@ -644,63 +810,12 @@ final class Bootstrap extends RuntimeException
|
|
|
644
810
|
exit;
|
|
645
811
|
}
|
|
646
812
|
|
|
647
|
-
private static function getAesKeyFromJwt(): string
|
|
648
|
-
{
|
|
649
|
-
$token = $_COOKIE['pp_function_call_jwt'] ?? null;
|
|
650
|
-
$jwtSecret = $_ENV['FUNCTION_CALL_SECRET'] ?? null;
|
|
651
|
-
|
|
652
|
-
if (!$token || !$jwtSecret) {
|
|
653
|
-
throw new RuntimeException('Missing session key or secret');
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
try {
|
|
657
|
-
$decoded = JWT::decode($token, new Key($jwtSecret, 'HS256'));
|
|
658
|
-
} catch (Throwable) {
|
|
659
|
-
throw new RuntimeException('Invalid session key');
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
$aesKey = base64_decode($decoded->k, true);
|
|
663
|
-
if ($aesKey === false || strlen($aesKey) !== 32) {
|
|
664
|
-
throw new RuntimeException('Bad key length');
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
return $aesKey;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
813
|
private static function jsonExit(array $payload): void
|
|
671
814
|
{
|
|
672
815
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
|
673
816
|
exit;
|
|
674
817
|
}
|
|
675
818
|
|
|
676
|
-
private static function decryptCallback(string $encrypted, string $aesKey): string
|
|
677
|
-
{
|
|
678
|
-
$parts = explode(':', $encrypted, 2);
|
|
679
|
-
if (count($parts) !== 2) {
|
|
680
|
-
throw new RuntimeException('Malformed callback payload');
|
|
681
|
-
}
|
|
682
|
-
[$ivB64, $ctB64] = $parts;
|
|
683
|
-
|
|
684
|
-
$iv = base64_decode($ivB64, true);
|
|
685
|
-
$ct = base64_decode($ctB64, true);
|
|
686
|
-
|
|
687
|
-
if ($iv === false || strlen($iv) !== 16 || $ct === false) {
|
|
688
|
-
throw new RuntimeException('Invalid callback payload');
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
$plain = openssl_decrypt($ct, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
|
|
692
|
-
if ($plain === false) {
|
|
693
|
-
throw new RuntimeException('Decryption failed');
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
$callback = preg_replace('/[^a-zA-Z0-9_:\->]/', '', $plain);
|
|
697
|
-
if ($callback === '' || $callback[0] === '_') {
|
|
698
|
-
throw new RuntimeException('Invalid callback');
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
return $callback;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
819
|
private static function getRequestData(): array
|
|
705
820
|
{
|
|
706
821
|
if (!empty($_FILES)) {
|
|
@@ -728,8 +843,78 @@ final class Bootstrap extends RuntimeException
|
|
|
728
843
|
return (json_last_error() === JSON_ERROR_NONE) ? $json : $_POST;
|
|
729
844
|
}
|
|
730
845
|
|
|
846
|
+
private static function validateAccess(Exposed $attribute): bool
|
|
847
|
+
{
|
|
848
|
+
if ($attribute->requiresAuth || !empty($attribute->allowedRoles)) {
|
|
849
|
+
$auth = Auth::getInstance();
|
|
850
|
+
|
|
851
|
+
if (!$auth->isAuthenticated()) {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (!empty($attribute->allowedRoles)) {
|
|
856
|
+
$payload = $auth->getPayload();
|
|
857
|
+
$currentRole = null;
|
|
858
|
+
|
|
859
|
+
if (is_scalar($payload)) {
|
|
860
|
+
$currentRole = $payload;
|
|
861
|
+
} else {
|
|
862
|
+
$roleKey = !empty(Auth::ROLE_NAME) ? Auth::ROLE_NAME : 'role';
|
|
863
|
+
|
|
864
|
+
if (is_object($payload)) {
|
|
865
|
+
$currentRole = $payload->$roleKey ?? null;
|
|
866
|
+
} elseif (is_array($payload)) {
|
|
867
|
+
$currentRole = $payload[$roleKey] ?? null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if ($currentRole === null || !in_array($currentRole, $attribute->allowedRoles)) {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
private static function isFunctionAllowed(string $fn): bool
|
|
881
|
+
{
|
|
882
|
+
try {
|
|
883
|
+
$ref = new ReflectionFunction($fn);
|
|
884
|
+
$attrs = $ref->getAttributes(Exposed::class);
|
|
885
|
+
|
|
886
|
+
if (empty($attrs)) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return self::validateAccess($attrs[0]->newInstance());
|
|
891
|
+
} catch (Throwable) {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
private static function isMethodAllowed(string $class, string $method): bool
|
|
897
|
+
{
|
|
898
|
+
try {
|
|
899
|
+
$ref = new ReflectionMethod($class, $method);
|
|
900
|
+
$attrs = $ref->getAttributes(Exposed::class);
|
|
901
|
+
|
|
902
|
+
if (empty($attrs)) {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return self::validateAccess($attrs[0]->newInstance());
|
|
907
|
+
} catch (Throwable) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
731
912
|
private static function dispatchFunction(string $fn, mixed $args)
|
|
732
913
|
{
|
|
914
|
+
if (!self::isFunctionAllowed($fn)) {
|
|
915
|
+
return ['success' => false, 'error' => 'Function not callable from client'];
|
|
916
|
+
}
|
|
917
|
+
|
|
733
918
|
if (function_exists($fn) && is_callable($fn)) {
|
|
734
919
|
try {
|
|
735
920
|
$res = call_user_func($fn, $args);
|
|
@@ -750,6 +935,17 @@ final class Bootstrap extends RuntimeException
|
|
|
750
935
|
|
|
751
936
|
private static function dispatchMethod(string $call, mixed $args)
|
|
752
937
|
{
|
|
938
|
+
if (!self::isMethodAllowed(
|
|
939
|
+
strpos($call, '->') !== false
|
|
940
|
+
? explode('->', $call, 2)[0]
|
|
941
|
+
: explode('::', $call, 2)[0],
|
|
942
|
+
strpos($call, '->') !== false
|
|
943
|
+
? explode('->', $call, 2)[1]
|
|
944
|
+
: explode('::', $call, 2)[1]
|
|
945
|
+
)) {
|
|
946
|
+
return ['success' => false, 'error' => 'Method not callable from client'];
|
|
947
|
+
}
|
|
948
|
+
|
|
753
949
|
if (strpos($call, '->') !== false) {
|
|
754
950
|
list($requested, $method) = explode('->', $call, 2);
|
|
755
951
|
$isStatic = false;
|
|
@@ -949,6 +1145,22 @@ final class Bootstrap extends RuntimeException
|
|
|
949
1145
|
|
|
950
1146
|
return Request::$isAjax || Request::$isXFileRequest || Request::$fileToInclude === 'route.php';
|
|
951
1147
|
}
|
|
1148
|
+
|
|
1149
|
+
public static function applyRootLayoutId(string $html): string
|
|
1150
|
+
{
|
|
1151
|
+
$rootLayoutPath = self::$layoutsToInclude[0] ?? self::$parentLayoutPath;
|
|
1152
|
+
$rootLayoutId = !empty($rootLayoutPath) ? md5($rootLayoutPath) : 'default-root';
|
|
1153
|
+
|
|
1154
|
+
header('X-PP-Root-Layout: ' . $rootLayoutId);
|
|
1155
|
+
|
|
1156
|
+
$rootLayoutMeta = '<meta name="pp-root-layout" content="' . $rootLayoutId . '">';
|
|
1157
|
+
|
|
1158
|
+
if (strpos($html, '<head>') !== false) {
|
|
1159
|
+
return preg_replace('/<head>/', "<head>\n $rootLayoutMeta", $html, 1);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return $rootLayoutMeta . $html;
|
|
1163
|
+
}
|
|
952
1164
|
}
|
|
953
1165
|
|
|
954
1166
|
Bootstrap::run();
|
|
@@ -989,40 +1201,32 @@ try {
|
|
|
989
1201
|
}
|
|
990
1202
|
|
|
991
1203
|
if (!empty(Bootstrap::$contentToInclude) && !empty(Request::$fileToInclude)) {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
996
|
-
}
|
|
1204
|
+
ob_start();
|
|
1205
|
+
require_once Bootstrap::$contentToInclude;
|
|
1206
|
+
MainLayout::$children = ob_get_clean();
|
|
997
1207
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1208
|
+
if (count(Bootstrap::$layoutsToInclude) > 1) {
|
|
1209
|
+
$nestedLayouts = array_slice(Bootstrap::$layoutsToInclude, 1);
|
|
1002
1210
|
|
|
1003
|
-
|
|
1004
|
-
Bootstrap
|
|
1005
|
-
|
|
1211
|
+
foreach (array_reverse($nestedLayouts) as $layoutPath) {
|
|
1212
|
+
if (!Bootstrap::containsChildren($layoutPath)) {
|
|
1213
|
+
Bootstrap::$isChildContentIncluded = true;
|
|
1214
|
+
}
|
|
1006
1215
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1216
|
+
ob_start();
|
|
1217
|
+
require_once $layoutPath;
|
|
1218
|
+
MainLayout::$children = ob_get_clean();
|
|
1219
|
+
}
|
|
1010
1220
|
}
|
|
1011
1221
|
} else {
|
|
1012
1222
|
ob_start();
|
|
1013
1223
|
require_once APP_PATH . '/not-found.php';
|
|
1014
|
-
MainLayout::$
|
|
1224
|
+
MainLayout::$children = ob_get_clean();
|
|
1015
1225
|
|
|
1016
1226
|
http_response_code(404);
|
|
1017
1227
|
CacheHandler::$isCacheable = false;
|
|
1018
1228
|
}
|
|
1019
1229
|
|
|
1020
|
-
if (Bootstrap::$isParentLayout && !empty(Bootstrap::$contentToInclude)) {
|
|
1021
|
-
ob_start();
|
|
1022
|
-
require_once Bootstrap::$contentToInclude;
|
|
1023
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
1230
|
if (!Bootstrap::$isContentIncluded && !Bootstrap::$isChildContentIncluded) {
|
|
1027
1231
|
if (!Bootstrap::$secondRequestC69CD) {
|
|
1028
1232
|
Bootstrap::createUpdateRequestData();
|
|
@@ -1034,7 +1238,7 @@ try {
|
|
|
1034
1238
|
if (file_exists($file)) {
|
|
1035
1239
|
ob_start();
|
|
1036
1240
|
require_once $file;
|
|
1037
|
-
MainLayout::$
|
|
1241
|
+
MainLayout::$children .= ob_get_clean();
|
|
1038
1242
|
}
|
|
1039
1243
|
}
|
|
1040
1244
|
}
|
|
@@ -1046,57 +1250,47 @@ try {
|
|
|
1046
1250
|
}
|
|
1047
1251
|
|
|
1048
1252
|
if ((!Request::$isWire && !Bootstrap::$secondRequestC69CD) && isset(Bootstrap::$requestFilesData[Request::$decodedUri])) {
|
|
1049
|
-
|
|
1050
|
-
CacheHandler
|
|
1253
|
+
$shouldCache = CacheHandler::$isCacheable === true
|
|
1254
|
+
|| (CacheHandler::$isCacheable === null && $_ENV['CACHE_ENABLED'] === 'true');
|
|
1255
|
+
|
|
1256
|
+
if ($shouldCache) {
|
|
1257
|
+
CacheHandler::serveCache(Request::$decodedUri, intval($_ENV['CACHE_TTL'] ?? 600));
|
|
1051
1258
|
}
|
|
1052
1259
|
}
|
|
1053
1260
|
|
|
1054
|
-
MainLayout::$children
|
|
1261
|
+
MainLayout::$children .= Bootstrap::getLoadingsFiles();
|
|
1055
1262
|
|
|
1056
1263
|
ob_start();
|
|
1057
|
-
|
|
1264
|
+
if (file_exists(Bootstrap::$parentLayoutPath)) {
|
|
1265
|
+
require_once Bootstrap::$parentLayoutPath;
|
|
1266
|
+
} else {
|
|
1267
|
+
echo MainLayout::$children;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1058
1270
|
MainLayout::$html = ob_get_clean();
|
|
1059
1271
|
MainLayout::$html = TemplateCompiler::compile(MainLayout::$html);
|
|
1060
1272
|
MainLayout::$html = TemplateCompiler::injectDynamicContent(MainLayout::$html);
|
|
1273
|
+
MainLayout::$html = Bootstrap::applyRootLayoutId(MainLayout::$html);
|
|
1274
|
+
|
|
1061
1275
|
MainLayout::$html = "<!DOCTYPE html>\n" . MainLayout::$html;
|
|
1062
1276
|
|
|
1063
1277
|
if (
|
|
1064
|
-
http_response_code() === 200
|
|
1278
|
+
http_response_code() === 200
|
|
1279
|
+
&& isset(Bootstrap::$requestFilesData[Request::$decodedUri]['fileName'])
|
|
1280
|
+
&& $shouldCache
|
|
1281
|
+
&& (!Request::$isWire && !Bootstrap::$secondRequestC69CD)
|
|
1065
1282
|
) {
|
|
1066
1283
|
CacheHandler::saveCache(Request::$decodedUri, MainLayout::$html);
|
|
1067
1284
|
}
|
|
1068
1285
|
|
|
1069
|
-
if (Bootstrap::$isPartialRequest) {
|
|
1070
|
-
$parts = PartialRenderer::extract(
|
|
1071
|
-
MainLayout::$html,
|
|
1072
|
-
Bootstrap::$partialSelectors
|
|
1073
|
-
);
|
|
1074
|
-
|
|
1075
|
-
if (count($parts) === 1) {
|
|
1076
|
-
echo reset($parts);
|
|
1077
|
-
} else {
|
|
1078
|
-
header('Content-Type: application/json');
|
|
1079
|
-
echo json_encode(
|
|
1080
|
-
['success' => true, 'fragments' => $parts],
|
|
1081
|
-
JSON_UNESCAPED_UNICODE
|
|
1082
|
-
);
|
|
1083
|
-
}
|
|
1084
|
-
exit;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
1286
|
echo MainLayout::$html;
|
|
1088
1287
|
} else {
|
|
1089
1288
|
$layoutPath = Bootstrap::$isContentIncluded
|
|
1090
1289
|
? Bootstrap::$parentLayoutPath
|
|
1091
1290
|
: (Bootstrap::$layoutsToInclude[0] ?? '');
|
|
1092
1291
|
|
|
1093
|
-
$message = "The layout file does not contain <?php echo MainLayout::\$
|
|
1094
|
-
$htmlMessage = "<div class='error'>The layout file does not contain <?php echo MainLayout::\$
|
|
1095
|
-
|
|
1096
|
-
if (Bootstrap::$isContentIncluded) {
|
|
1097
|
-
$message = "The parent layout file does not contain <?php echo MainLayout::\$children; ?> Or <?= MainLayout::\$children ?><br><strong>$layoutPath</strong>";
|
|
1098
|
-
$htmlMessage = "<div class='error'>The parent layout file does not contain <?php echo MainLayout::\$children; ?> Or <?= MainLayout::\$children ?><br><strong>$layoutPath</strong></div>";
|
|
1099
|
-
}
|
|
1292
|
+
$message = "The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?>\n<strong>$layoutPath</strong>";
|
|
1293
|
+
$htmlMessage = "<div class='error'>The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?><br><strong>$layoutPath</strong></div>";
|
|
1100
1294
|
|
|
1101
1295
|
$errorDetails = Bootstrap::isAjaxOrXFileRequestOrRouteFile() ? $message : $htmlMessage;
|
|
1102
1296
|
|