create-prisma-php-app 4.3.0-beta → 4.3.0
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 +543 -290
- package/dist/index.js +1 -1
- package/dist/prisma-php.js +1 -1
- package/dist/public/assets/images/prisma-php.svg +146 -146
- 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 +301 -0
- package/dist/src/Lib/Auth/Auth.php +80 -57
- package/dist/src/app/index.php +2 -2
- package/dist/src/app/layout.php +1 -1
- package/dist/src/app/not-found.php +2 -2
- package/dist/tsconfig.json +1 -1
- package/dist/vite.config.ts +25 -3
- package/package.json +4 -4
package/dist/bootstrap.php
CHANGED
|
@@ -24,9 +24,9 @@ use PP\MainLayout;
|
|
|
24
24
|
use PP\PHPX\TemplateCompiler;
|
|
25
25
|
use PP\CacheHandler;
|
|
26
26
|
use PP\ErrorHandler;
|
|
27
|
-
use
|
|
28
|
-
use
|
|
29
|
-
use PP\
|
|
27
|
+
use PP\Attributes\Exposed;
|
|
28
|
+
use PP\Streaming\SSE;
|
|
29
|
+
use PP\Security\RateLimiter;
|
|
30
30
|
|
|
31
31
|
final class Bootstrap extends RuntimeException
|
|
32
32
|
{
|
|
@@ -40,8 +40,6 @@ final class Bootstrap extends RuntimeException
|
|
|
40
40
|
public static bool $isContentVariableIncluded = false;
|
|
41
41
|
public static bool $secondRequestC69CD = false;
|
|
42
42
|
public static array $requestFilesData = [];
|
|
43
|
-
public static array $partialSelectors = [];
|
|
44
|
-
public static bool $isPartialRequest = false;
|
|
45
43
|
|
|
46
44
|
private string $context;
|
|
47
45
|
|
|
@@ -78,7 +76,7 @@ final class Bootstrap extends RuntimeException
|
|
|
78
76
|
'samesite' => 'Lax',
|
|
79
77
|
]);
|
|
80
78
|
|
|
81
|
-
self::
|
|
79
|
+
self::setCsrfCookie();
|
|
82
80
|
|
|
83
81
|
self::$secondRequestC69CD = Request::$data['secondRequestC69CD'] ?? false;
|
|
84
82
|
|
|
@@ -106,24 +104,20 @@ final class Bootstrap extends RuntimeException
|
|
|
106
104
|
self::authenticateUserToken();
|
|
107
105
|
|
|
108
106
|
self::$requestFilePath = APP_PATH . Request::$pathname;
|
|
109
|
-
self::$parentLayoutPath = APP_PATH . '/layout.php';
|
|
110
107
|
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
if (!empty(self::$layoutsToInclude)) {
|
|
109
|
+
self::$parentLayoutPath = self::$layoutsToInclude[0];
|
|
110
|
+
self::$isParentLayout = true;
|
|
111
|
+
} else {
|
|
112
|
+
self::$parentLayoutPath = APP_PATH . '/layout.php';
|
|
113
|
+
self::$isParentLayout = false;
|
|
114
|
+
}
|
|
113
115
|
|
|
114
116
|
self::$isContentVariableIncluded = self::containsChildren(self::$parentLayoutPath);
|
|
115
117
|
if (!self::$isContentVariableIncluded) {
|
|
116
118
|
self::$isContentIncluded = true;
|
|
117
119
|
}
|
|
118
120
|
|
|
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
121
|
self::$requestFilesData = PrismaPHPSettings::$includeFiles;
|
|
128
122
|
|
|
129
123
|
ErrorHandler::checkFatalError();
|
|
@@ -131,66 +125,62 @@ final class Bootstrap extends RuntimeException
|
|
|
131
125
|
|
|
132
126
|
private static function isLocalStoreCallback(): void
|
|
133
127
|
{
|
|
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()]);
|
|
128
|
+
if (empty($_SERVER['HTTP_X_PP_FUNCTION'])) {
|
|
129
|
+
return;
|
|
144
130
|
}
|
|
145
131
|
|
|
146
|
-
|
|
147
|
-
$callbackName = self::decryptCallback($data['callback'], $aesKey);
|
|
148
|
-
} catch (RuntimeException $e) {
|
|
149
|
-
self::jsonExit(['success' => false, 'error' => $e->getMessage()]);
|
|
150
|
-
}
|
|
132
|
+
$callbackName = $_SERVER['HTTP_X_PP_FUNCTION'];
|
|
151
133
|
|
|
152
134
|
if ($callbackName === PrismaPHPSettings::$localStoreKey) {
|
|
135
|
+
self::validateCsrfToken();
|
|
153
136
|
self::jsonExit(['success' => true, 'response' => 'localStorage updated']);
|
|
154
137
|
}
|
|
155
138
|
}
|
|
156
139
|
|
|
157
|
-
private static function
|
|
140
|
+
private static function setCsrfCookie(): void
|
|
158
141
|
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
142
|
+
if (!isset($_COOKIE['prisma_php_csrf'])) {
|
|
143
|
+
$secret = $_ENV['FUNCTION_CALL_SECRET'] ?? 'pp_default_insecure_secret';
|
|
144
|
+
$nonce = bin2hex(random_bytes(16));
|
|
145
|
+
$signature = hash_hmac('sha256', $nonce, $secret);
|
|
146
|
+
$token = $nonce . '.' . $signature;
|
|
147
|
+
|
|
148
|
+
setcookie('prisma_php_csrf', $token, [
|
|
149
|
+
'expires' => time() + 3600,
|
|
150
|
+
'path' => '/',
|
|
151
|
+
'secure' => true,
|
|
152
|
+
'httponly' => false,
|
|
153
|
+
'samesite' => 'Lax',
|
|
154
|
+
]);
|
|
155
|
+
$_COOKIE['prisma_php_csrf'] = $token;
|
|
162
156
|
}
|
|
157
|
+
}
|
|
163
158
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
159
|
+
private static function validateCsrfToken(): void
|
|
160
|
+
{
|
|
161
|
+
$headerToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
|
162
|
+
$cookieToken = $_COOKIE['prisma_php_csrf'] ?? '';
|
|
163
|
+
$secret = $_ENV['FUNCTION_CALL_SECRET'] ?? '';
|
|
164
|
+
|
|
165
|
+
if (empty($headerToken) || empty($cookieToken)) {
|
|
166
|
+
self::jsonExit(['success' => false, 'error' => 'CSRF token missing']);
|
|
173
167
|
}
|
|
174
168
|
|
|
175
|
-
$
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
'exp' => time() + 3600,
|
|
179
|
-
'iat' => time(),
|
|
180
|
-
];
|
|
181
|
-
$jwt = JWT::encode($payload, $hmacSecret, 'HS256');
|
|
169
|
+
if (!hash_equals($cookieToken, $headerToken)) {
|
|
170
|
+
self::jsonExit(['success' => false, 'error' => 'CSRF token mismatch']);
|
|
171
|
+
}
|
|
182
172
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
]
|
|
193
|
-
|
|
173
|
+
$parts = explode('.', $cookieToken);
|
|
174
|
+
if (count($parts) !== 2) {
|
|
175
|
+
self::jsonExit(['success' => false, 'error' => 'Invalid CSRF token format']);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
[$nonce, $signature] = $parts;
|
|
179
|
+
$expectedSignature = hash_hmac('sha256', $nonce, $secret);
|
|
180
|
+
|
|
181
|
+
if (!hash_equals($expectedSignature, $signature)) {
|
|
182
|
+
self::jsonExit(['success' => false, 'error' => 'Invalid CSRF token signature']);
|
|
183
|
+
}
|
|
194
184
|
}
|
|
195
185
|
|
|
196
186
|
private static function fileExistsCached(string $path): bool
|
|
@@ -270,72 +260,245 @@ final class Bootstrap extends RuntimeException
|
|
|
270
260
|
}
|
|
271
261
|
}
|
|
272
262
|
|
|
263
|
+
$layoutsToInclude = self::collectLayouts($pathname, $groupFolder, $dynamicRoute ?? null);
|
|
264
|
+
} else {
|
|
265
|
+
$contentData = self::getFilePrecedence();
|
|
266
|
+
$includePath = $baseDir . $contentData['file'];
|
|
267
|
+
$layoutsToInclude = self::collectRootLayouts($contentData['file']);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return [
|
|
271
|
+
'path' => $includePath,
|
|
272
|
+
'layouts' => $layoutsToInclude,
|
|
273
|
+
'pathname' => $pathname,
|
|
274
|
+
'uri' => $requestUri
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private static function collectLayouts(string $pathname, ?string $groupFolder, ?string $dynamicRoute): array
|
|
279
|
+
{
|
|
280
|
+
$layoutsToInclude = [];
|
|
281
|
+
$baseDir = APP_PATH;
|
|
282
|
+
|
|
283
|
+
$rootLayout = $baseDir . '/layout.php';
|
|
284
|
+
if (self::fileExistsCached($rootLayout)) {
|
|
285
|
+
$layoutsToInclude[] = $rootLayout;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
$groupName = null;
|
|
289
|
+
$groupParentPath = '';
|
|
290
|
+
$pathAfterGroup = '';
|
|
291
|
+
|
|
292
|
+
if ($groupFolder) {
|
|
293
|
+
$normalizedGroupFolder = str_replace('\\', '/', $groupFolder);
|
|
294
|
+
|
|
295
|
+
if (preg_match('#^\.?/src/app/(.+)/\(([^)]+)\)/(.+)$#', $normalizedGroupFolder, $matches)) {
|
|
296
|
+
$groupParentPath = $matches[1];
|
|
297
|
+
$groupName = $matches[2];
|
|
298
|
+
$pathAfterGroup = dirname($matches[3]);
|
|
299
|
+
if ($pathAfterGroup === '.') {
|
|
300
|
+
$pathAfterGroup = '';
|
|
301
|
+
}
|
|
302
|
+
} elseif (preg_match('#^\.?/src/app/\(([^)]+)\)/(.+)$#', $normalizedGroupFolder, $matches)) {
|
|
303
|
+
$groupName = $matches[1];
|
|
304
|
+
$pathAfterGroup = dirname($matches[2]);
|
|
305
|
+
if ($pathAfterGroup === '.') {
|
|
306
|
+
$pathAfterGroup = '';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if ($groupName && $groupParentPath) {
|
|
273
312
|
$currentPath = $baseDir;
|
|
274
|
-
$
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
$
|
|
313
|
+
foreach (explode('/', $groupParentPath) as $segment) {
|
|
314
|
+
if (empty($segment)) continue;
|
|
315
|
+
|
|
316
|
+
$currentPath .= '/' . $segment;
|
|
317
|
+
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
318
|
+
|
|
319
|
+
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
320
|
+
$layoutsToInclude[] = $potentialLayoutPath;
|
|
321
|
+
}
|
|
278
322
|
}
|
|
279
323
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
324
|
+
$groupLayoutPath = $baseDir . '/' . $groupParentPath . "/($groupName)/layout.php";
|
|
325
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
326
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!empty($pathAfterGroup)) {
|
|
330
|
+
$currentPath = $baseDir . '/' . $groupParentPath . "/($groupName)";
|
|
331
|
+
foreach (explode('/', $pathAfterGroup) as $segment) {
|
|
332
|
+
if (empty($segment)) continue;
|
|
333
|
+
|
|
334
|
+
$currentPath .= '/' . $segment;
|
|
335
|
+
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
336
|
+
|
|
337
|
+
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
338
|
+
$layoutsToInclude[] = $potentialLayoutPath;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} elseif ($groupName && !$groupParentPath) {
|
|
343
|
+
$groupLayoutPath = $baseDir . "/($groupName)/layout.php";
|
|
344
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
345
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!empty($pathAfterGroup)) {
|
|
349
|
+
$currentPath = $baseDir . "/($groupName)";
|
|
350
|
+
foreach (explode('/', $pathAfterGroup) as $segment) {
|
|
351
|
+
if (empty($segment)) continue;
|
|
352
|
+
|
|
353
|
+
$currentPath .= '/' . $segment;
|
|
354
|
+
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
355
|
+
|
|
356
|
+
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
357
|
+
$layoutsToInclude[] = $potentialLayoutPath;
|
|
358
|
+
}
|
|
283
359
|
}
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
$currentPath = $baseDir;
|
|
363
|
+
foreach (explode('/', $pathname) as $segment) {
|
|
364
|
+
if (empty($segment)) continue;
|
|
284
365
|
|
|
285
366
|
$currentPath .= '/' . $segment;
|
|
286
367
|
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
368
|
+
|
|
369
|
+
if ($potentialLayoutPath === $rootLayout) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
287
373
|
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
288
374
|
$layoutsToInclude[] = $potentialLayoutPath;
|
|
289
375
|
}
|
|
290
376
|
}
|
|
377
|
+
}
|
|
291
378
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
379
|
+
if (isset($dynamicRoute) && !empty($dynamicRoute)) {
|
|
380
|
+
$currentDynamicPath = $baseDir;
|
|
381
|
+
foreach (explode('/', $dynamicRoute) as $segment) {
|
|
382
|
+
if (empty($segment) || $segment === 'src' || $segment === 'app') {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
$currentDynamicPath .= '/' . $segment;
|
|
387
|
+
$potentialDynamicRoute = $currentDynamicPath . '/layout.php';
|
|
388
|
+
if (self::fileExistsCached($potentialDynamicRoute) && !in_array($potentialDynamicRoute, $layoutsToInclude, true)) {
|
|
389
|
+
$layoutsToInclude[] = $potentialDynamicRoute;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (empty($layoutsToInclude)) {
|
|
395
|
+
$layoutsToInclude = self::findFirstGroupLayout();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return $layoutsToInclude;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private static function collectRootLayouts(?string $matchedContentFile = null): array
|
|
402
|
+
{
|
|
403
|
+
$layoutsToInclude = [];
|
|
404
|
+
$baseDir = APP_PATH;
|
|
405
|
+
$rootLayout = $baseDir . '/layout.php';
|
|
406
|
+
|
|
407
|
+
if (self::fileExistsCached($rootLayout)) {
|
|
408
|
+
$layoutsToInclude[] = $rootLayout;
|
|
409
|
+
} else {
|
|
410
|
+
$layoutsToInclude = self::findFirstGroupLayout($matchedContentFile);
|
|
411
|
+
|
|
412
|
+
if (empty($layoutsToInclude)) {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return $layoutsToInclude;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private static function findFirstGroupLayout(?string $matchedContentFile = null): array
|
|
421
|
+
{
|
|
422
|
+
$baseDir = APP_PATH;
|
|
423
|
+
$layoutsToInclude = [];
|
|
424
|
+
|
|
425
|
+
if (is_dir($baseDir)) {
|
|
426
|
+
$items = scandir($baseDir);
|
|
427
|
+
|
|
428
|
+
if ($matchedContentFile) {
|
|
429
|
+
foreach ($items as $item) {
|
|
430
|
+
if ($item === '.' || $item === '..') {
|
|
299
431
|
continue;
|
|
300
432
|
}
|
|
301
433
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
434
|
+
if (preg_match('/^\([^)]+\)$/', $item)) {
|
|
435
|
+
if (strpos($matchedContentFile, "/$item/") === 0) {
|
|
436
|
+
$groupLayoutPath = $baseDir . '/' . $item . '/layout.php';
|
|
437
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
438
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
439
|
+
return $layoutsToInclude;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
306
442
|
}
|
|
307
443
|
}
|
|
308
444
|
}
|
|
309
445
|
|
|
310
|
-
|
|
311
|
-
$
|
|
446
|
+
foreach ($items as $item) {
|
|
447
|
+
if ($item === '.' || $item === '..') {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (preg_match('/^\([^)]+\)$/', $item)) {
|
|
452
|
+
$groupLayoutPath = $baseDir . '/' . $item . '/layout.php';
|
|
453
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
454
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
312
458
|
}
|
|
313
|
-
} else {
|
|
314
|
-
$includePath = $baseDir . self::getFilePrecedence();
|
|
315
459
|
}
|
|
316
460
|
|
|
317
|
-
return
|
|
318
|
-
'path' => $includePath,
|
|
319
|
-
'layouts' => $layoutsToInclude,
|
|
320
|
-
'pathname' => $pathname,
|
|
321
|
-
'uri' => $requestUri
|
|
322
|
-
];
|
|
461
|
+
return $layoutsToInclude;
|
|
323
462
|
}
|
|
324
463
|
|
|
325
|
-
private static function getFilePrecedence():
|
|
464
|
+
private static function getFilePrecedence(): array
|
|
326
465
|
{
|
|
466
|
+
$baseDir = APP_PATH;
|
|
467
|
+
$result = ['file' => null];
|
|
468
|
+
|
|
327
469
|
foreach (PrismaPHPSettings::$routeFiles as $route) {
|
|
328
470
|
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
329
471
|
continue;
|
|
330
472
|
}
|
|
331
473
|
if (preg_match('/^\.\/src\/app\/route\.php$/', $route)) {
|
|
332
|
-
return '/route.php';
|
|
474
|
+
return ['file' => '/route.php'];
|
|
333
475
|
}
|
|
334
476
|
if (preg_match('/^\.\/src\/app\/index\.php$/', $route)) {
|
|
335
|
-
return '/index.php';
|
|
477
|
+
return ['file' => '/index.php'];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (is_dir($baseDir)) {
|
|
482
|
+
$items = scandir($baseDir);
|
|
483
|
+
foreach ($items as $item) {
|
|
484
|
+
if ($item === '.' || $item === '..') {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (preg_match('/^\([^)]+\)$/', $item)) {
|
|
489
|
+
$groupDir = $baseDir . '/' . $item;
|
|
490
|
+
|
|
491
|
+
if (file_exists($groupDir . '/route.php')) {
|
|
492
|
+
return ['file' => '/' . $item . '/route.php'];
|
|
493
|
+
}
|
|
494
|
+
if (file_exists($groupDir . '/index.php')) {
|
|
495
|
+
return ['file' => '/' . $item . '/index.php'];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
336
498
|
}
|
|
337
499
|
}
|
|
338
|
-
|
|
500
|
+
|
|
501
|
+
return $result;
|
|
339
502
|
}
|
|
340
503
|
|
|
341
504
|
private static function uriExtractor(string $scriptUrl): string
|
|
@@ -424,17 +587,22 @@ final class Bootstrap extends RuntimeException
|
|
|
424
587
|
}
|
|
425
588
|
}
|
|
426
589
|
} elseif (strpos($normalizedRoute, '[...') !== false) {
|
|
427
|
-
|
|
590
|
+
$cleanedRoute = preg_replace('/\(.+\)/', '', $normalizedRoute);
|
|
591
|
+
$cleanedRoute = preg_replace('/\/+/', '/', $cleanedRoute);
|
|
592
|
+
$staticPart = preg_replace('/\[\.\.\..*?\].*/', '', $cleanedRoute);
|
|
593
|
+
$staticSegments = array_filter(explode('/', $staticPart));
|
|
594
|
+
$minRequiredSegments = count($staticSegments);
|
|
595
|
+
|
|
596
|
+
if (count($pathnameSegments) < $minRequiredSegments) {
|
|
428
597
|
continue;
|
|
429
598
|
}
|
|
430
599
|
|
|
431
|
-
$cleanedNormalizedRoute =
|
|
432
|
-
$
|
|
433
|
-
$dynamicSegmentRoute = preg_replace('/\[\.\.\..*?\].*/', '', $cleanedNormalizedRoute);
|
|
600
|
+
$cleanedNormalizedRoute = $cleanedRoute;
|
|
601
|
+
$dynamicSegmentRoute = $staticPart;
|
|
434
602
|
|
|
435
603
|
if (strpos("/src/app/$normalizedPathname", $dynamicSegmentRoute) === 0) {
|
|
436
604
|
$trimmedPathname = str_replace($dynamicSegmentRoute, '', "/src/app/$normalizedPathname");
|
|
437
|
-
$pathnameParts = explode('/', trim($trimmedPathname, '/'));
|
|
605
|
+
$pathnameParts = $trimmedPathname === '' ? [] : explode('/', trim($trimmedPathname, '/'));
|
|
438
606
|
|
|
439
607
|
if (preg_match('/\[\.\.\.(.*?)\]/', $normalizedRoute, $matches)) {
|
|
440
608
|
$dynamicParam = $matches[1];
|
|
@@ -451,18 +619,14 @@ final class Bootstrap extends RuntimeException
|
|
|
451
619
|
|
|
452
620
|
if (strpos($normalizedRoute, 'index.php') !== false) {
|
|
453
621
|
$segmentMatch = "[...$dynamicParam]";
|
|
454
|
-
$index = array_search($segmentMatch, $filteredRouteSegments);
|
|
455
|
-
|
|
456
|
-
if ($index !== false && isset($pathnameSegments[$index])) {
|
|
457
|
-
$dynamicRoutePathname = str_replace($segmentMatch, implode('/', $pathnameParts), $cleanedNormalizedRoute);
|
|
458
|
-
$dynamicRoutePathnameDirname = rtrim(dirname($dynamicRoutePathname), '/');
|
|
459
622
|
|
|
460
|
-
|
|
623
|
+
$dynamicRoutePathname = str_replace($segmentMatch, implode('/', $pathnameParts), $cleanedNormalizedRoute);
|
|
624
|
+
$dynamicRoutePathnameDirname = rtrim(dirname($dynamicRoutePathname), '/');
|
|
625
|
+
$expectedPathname = rtrim("/src/app/$normalizedPathname", '/');
|
|
461
626
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
627
|
+
if ($expectedPathname === $dynamicRoutePathnameDirname) {
|
|
628
|
+
$pathnameMatch = $normalizedRoute;
|
|
629
|
+
break;
|
|
466
630
|
}
|
|
467
631
|
}
|
|
468
632
|
}
|
|
@@ -483,6 +647,7 @@ final class Bootstrap extends RuntimeException
|
|
|
483
647
|
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
484
648
|
continue;
|
|
485
649
|
}
|
|
650
|
+
|
|
486
651
|
$normalizedRoute = trim(str_replace('\\', '/', $route), '.');
|
|
487
652
|
$cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
|
|
488
653
|
|
|
@@ -494,22 +659,26 @@ final class Bootstrap extends RuntimeException
|
|
|
494
659
|
}
|
|
495
660
|
}
|
|
496
661
|
|
|
497
|
-
|
|
498
|
-
|
|
662
|
+
if (!$bestMatch) {
|
|
663
|
+
foreach (PrismaPHPSettings::$routeFiles as $route) {
|
|
664
|
+
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
499
667
|
|
|
500
|
-
|
|
501
|
-
{
|
|
502
|
-
$lastSlashPos = strrpos($pathname, '/');
|
|
503
|
-
if ($lastSlashPos === false) {
|
|
504
|
-
return "";
|
|
505
|
-
}
|
|
668
|
+
$normalizedRoute = trim(str_replace('\\', '/', $route), '.');
|
|
506
669
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
670
|
+
if (preg_match('/\/\(([^)]+)\)\//', $normalizedRoute, $matches)) {
|
|
671
|
+
$cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
|
|
672
|
+
|
|
673
|
+
if ($cleanedRoute === $routeFile || $cleanedRoute === $indexFile) {
|
|
674
|
+
$bestMatch = $normalizedRoute;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
510
679
|
}
|
|
511
680
|
|
|
512
|
-
return
|
|
681
|
+
return $bestMatch;
|
|
513
682
|
}
|
|
514
683
|
|
|
515
684
|
private static function singleDynamicRoute($pathnameSegments, $routeSegments)
|
|
@@ -578,7 +747,7 @@ final class Bootstrap extends RuntimeException
|
|
|
578
747
|
}
|
|
579
748
|
}
|
|
580
749
|
|
|
581
|
-
public static function
|
|
750
|
+
public static function containsChildren($filePath): bool
|
|
582
751
|
{
|
|
583
752
|
if (!self::fileExistsCached($filePath)) {
|
|
584
753
|
return false;
|
|
@@ -589,118 +758,76 @@ final class Bootstrap extends RuntimeException
|
|
|
589
758
|
return false;
|
|
590
759
|
}
|
|
591
760
|
|
|
592
|
-
$pattern = '/\<\?=\s*MainLayout::\$
|
|
761
|
+
$pattern = '/\<\?=\s*MainLayout::\$children\s*;?\s*\?>|echo\s*MainLayout::\$children\s*;?/';
|
|
593
762
|
return (bool) preg_match($pattern, $fileContent);
|
|
594
763
|
}
|
|
595
764
|
|
|
596
|
-
private static function
|
|
765
|
+
private static function convertToArrayObject($data)
|
|
597
766
|
{
|
|
598
|
-
if (!
|
|
599
|
-
return
|
|
767
|
+
if (!is_array($data)) {
|
|
768
|
+
return $data;
|
|
600
769
|
}
|
|
601
770
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return false;
|
|
771
|
+
if (empty($data)) {
|
|
772
|
+
return $data;
|
|
605
773
|
}
|
|
606
774
|
|
|
607
|
-
$
|
|
608
|
-
return (bool) preg_match($pattern, $fileContent);
|
|
609
|
-
}
|
|
775
|
+
$isAssoc = array_keys($data) !== range(0, count($data) - 1);
|
|
610
776
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
777
|
+
if ($isAssoc) {
|
|
778
|
+
$obj = new stdClass();
|
|
779
|
+
foreach ($data as $key => $value) {
|
|
780
|
+
$obj->$key = self::convertToArrayObject($value);
|
|
781
|
+
}
|
|
782
|
+
return $obj;
|
|
783
|
+
} else {
|
|
784
|
+
return array_map([self::class, 'convertToArrayObject'], $data);
|
|
785
|
+
}
|
|
614
786
|
}
|
|
615
787
|
|
|
616
788
|
public static function wireCallback(): void
|
|
617
789
|
{
|
|
618
|
-
$
|
|
790
|
+
$callbackName = $_SERVER['HTTP_X_PP_FUNCTION'] ?? null;
|
|
619
791
|
|
|
620
|
-
if (empty($
|
|
621
|
-
self::jsonExit(['success' => false, 'error' => 'Callback not provided', 'response' => null]);
|
|
792
|
+
if (empty($callbackName)) {
|
|
793
|
+
self::jsonExit(['success' => false, 'error' => 'Callback header not provided', 'response' => null]);
|
|
622
794
|
}
|
|
623
795
|
|
|
624
|
-
|
|
625
|
-
$aesKey = self::getAesKeyFromJwt();
|
|
626
|
-
} catch (RuntimeException $e) {
|
|
627
|
-
self::jsonExit(['success' => false, 'error' => $e->getMessage()]);
|
|
628
|
-
}
|
|
796
|
+
self::validateCsrfToken();
|
|
629
797
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
} catch (RuntimeException $e) {
|
|
633
|
-
self::jsonExit(['success' => false, 'error' => $e->getMessage()]);
|
|
798
|
+
if (!preg_match('/^[a-zA-Z0-9_:\->]+$/', $callbackName)) {
|
|
799
|
+
self::jsonExit(['success' => false, 'error' => 'Invalid callback format']);
|
|
634
800
|
}
|
|
635
801
|
|
|
802
|
+
$data = self::getRequestData();
|
|
636
803
|
$args = self::convertToArrayObject($data);
|
|
637
|
-
|
|
804
|
+
|
|
805
|
+
$out = str_contains($callbackName, '->') || str_contains($callbackName, '::')
|
|
638
806
|
? self::dispatchMethod($callbackName, $args)
|
|
639
807
|
: self::dispatchFunction($callbackName, $args);
|
|
640
808
|
|
|
641
|
-
if ($out
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
exit;
|
|
645
|
-
}
|
|
646
|
-
|
|
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');
|
|
809
|
+
if ($out instanceof SSE) {
|
|
810
|
+
$out->send();
|
|
811
|
+
exit;
|
|
654
812
|
}
|
|
655
813
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
throw new RuntimeException('Invalid session key');
|
|
814
|
+
if ($out instanceof Generator) {
|
|
815
|
+
(new SSE($out))->send();
|
|
816
|
+
exit;
|
|
660
817
|
}
|
|
661
818
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
throw new RuntimeException('Bad key length');
|
|
819
|
+
if ($out !== null) {
|
|
820
|
+
self::jsonExit($out);
|
|
665
821
|
}
|
|
666
|
-
|
|
667
|
-
return $aesKey;
|
|
822
|
+
exit;
|
|
668
823
|
}
|
|
669
824
|
|
|
670
|
-
private static function jsonExit(
|
|
825
|
+
private static function jsonExit(mixed $payload): void
|
|
671
826
|
{
|
|
672
827
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
|
673
828
|
exit;
|
|
674
829
|
}
|
|
675
830
|
|
|
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
831
|
private static function getRequestData(): array
|
|
705
832
|
{
|
|
706
833
|
if (!empty($_FILES)) {
|
|
@@ -728,16 +855,132 @@ final class Bootstrap extends RuntimeException
|
|
|
728
855
|
return (json_last_error() === JSON_ERROR_NONE) ? $json : $_POST;
|
|
729
856
|
}
|
|
730
857
|
|
|
858
|
+
private static function validateAccess(Exposed $attribute): bool
|
|
859
|
+
{
|
|
860
|
+
if ($attribute->requiresAuth || !empty($attribute->allowedRoles)) {
|
|
861
|
+
$auth = Auth::getInstance();
|
|
862
|
+
|
|
863
|
+
if (!$auth->isAuthenticated()) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (!empty($attribute->allowedRoles)) {
|
|
868
|
+
$payload = $auth->getPayload();
|
|
869
|
+
$currentRole = null;
|
|
870
|
+
|
|
871
|
+
if (is_scalar($payload)) {
|
|
872
|
+
$currentRole = $payload;
|
|
873
|
+
} else {
|
|
874
|
+
$roleKey = !empty(Auth::ROLE_NAME) ? Auth::ROLE_NAME : 'role';
|
|
875
|
+
|
|
876
|
+
if (is_object($payload)) {
|
|
877
|
+
$currentRole = $payload->$roleKey ?? null;
|
|
878
|
+
} elseif (is_array($payload)) {
|
|
879
|
+
$currentRole = $payload[$roleKey] ?? null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if ($currentRole === null || !in_array($currentRole, $attribute->allowedRoles)) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
private static function isFunctionAllowed(string $fn): bool
|
|
893
|
+
{
|
|
894
|
+
try {
|
|
895
|
+
$ref = new ReflectionFunction($fn);
|
|
896
|
+
$attrs = $ref->getAttributes(Exposed::class);
|
|
897
|
+
|
|
898
|
+
if (empty($attrs)) {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return self::validateAccess($attrs[0]->newInstance());
|
|
903
|
+
} catch (Throwable) {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private static function isMethodAllowed(string $class, string $method): bool
|
|
909
|
+
{
|
|
910
|
+
try {
|
|
911
|
+
$ref = new ReflectionMethod($class, $method);
|
|
912
|
+
$attrs = $ref->getAttributes(Exposed::class);
|
|
913
|
+
|
|
914
|
+
if (empty($attrs)) {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return self::validateAccess($attrs[0]->newInstance());
|
|
919
|
+
} catch (Throwable) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private static function getExposedAttribute(string $classOrFn, ?string $method = null): ?Exposed
|
|
925
|
+
{
|
|
926
|
+
try {
|
|
927
|
+
if ($method) {
|
|
928
|
+
$ref = new ReflectionMethod($classOrFn, $method);
|
|
929
|
+
} else {
|
|
930
|
+
$ref = new ReflectionFunction($classOrFn);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
$attrs = $ref->getAttributes(Exposed::class);
|
|
934
|
+
return !empty($attrs) ? $attrs[0]->newInstance() : null;
|
|
935
|
+
} catch (Throwable) {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private static function enforceRateLimit(Exposed $attribute, string $identifier): void
|
|
941
|
+
{
|
|
942
|
+
$limits = $attribute->limits;
|
|
943
|
+
|
|
944
|
+
if (empty($limits)) {
|
|
945
|
+
if ($attribute->requiresAuth) {
|
|
946
|
+
$limits = $_ENV['RATE_LIMIT_AUTH'] ?? '10/minute';
|
|
947
|
+
} else {
|
|
948
|
+
$limits = $_ENV['RATE_LIMIT_RPC'] ?? '60/minute';
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if ($limits) {
|
|
953
|
+
RateLimiter::verify($identifier, $limits);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
731
957
|
private static function dispatchFunction(string $fn, mixed $args)
|
|
732
958
|
{
|
|
959
|
+
$attribute = self::getExposedAttribute($fn);
|
|
960
|
+
if (!$attribute) {
|
|
961
|
+
return ['success' => false, 'error' => 'Function not callable from client'];
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (!self::validateAccess($attribute)) {
|
|
965
|
+
return ['success' => false, 'error' => 'Permission denied'];
|
|
966
|
+
}
|
|
967
|
+
|
|
733
968
|
if (function_exists($fn) && is_callable($fn)) {
|
|
734
969
|
try {
|
|
970
|
+
self::enforceRateLimit($attribute, "fn:$fn");
|
|
971
|
+
|
|
735
972
|
$res = call_user_func($fn, $args);
|
|
736
|
-
|
|
737
|
-
|
|
973
|
+
|
|
974
|
+
if ($res instanceof Generator || $res instanceof SSE) {
|
|
975
|
+
return $res;
|
|
738
976
|
}
|
|
977
|
+
|
|
739
978
|
return $res;
|
|
740
979
|
} catch (Throwable $e) {
|
|
980
|
+
if ($e->getMessage() === 'Rate limit exceeded. Try again later.') {
|
|
981
|
+
return ['success' => false, 'error' => $e->getMessage()];
|
|
982
|
+
}
|
|
983
|
+
|
|
741
984
|
if (isset($_ENV['SHOW_ERRORS']) && $_ENV['SHOW_ERRORS'] === 'false') {
|
|
742
985
|
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
743
986
|
} else {
|
|
@@ -750,6 +993,17 @@ final class Bootstrap extends RuntimeException
|
|
|
750
993
|
|
|
751
994
|
private static function dispatchMethod(string $call, mixed $args)
|
|
752
995
|
{
|
|
996
|
+
if (!self::isMethodAllowed(
|
|
997
|
+
strpos($call, '->') !== false
|
|
998
|
+
? explode('->', $call, 2)[0]
|
|
999
|
+
: explode('::', $call, 2)[0],
|
|
1000
|
+
strpos($call, '->') !== false
|
|
1001
|
+
? explode('->', $call, 2)[1]
|
|
1002
|
+
: explode('::', $call, 2)[1]
|
|
1003
|
+
)) {
|
|
1004
|
+
return ['success' => false, 'error' => 'Method not callable from client'];
|
|
1005
|
+
}
|
|
1006
|
+
|
|
753
1007
|
if (strpos($call, '->') !== false) {
|
|
754
1008
|
list($requested, $method) = explode('->', $call, 2);
|
|
755
1009
|
$isStatic = false;
|
|
@@ -766,47 +1020,48 @@ final class Bootstrap extends RuntimeException
|
|
|
766
1020
|
}
|
|
767
1021
|
}
|
|
768
1022
|
|
|
769
|
-
if (
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1023
|
+
if (!class_exists($class)) {
|
|
1024
|
+
return ['success' => false, 'error' => "Class '$requested' not found"];
|
|
1025
|
+
}
|
|
1026
|
+
$attribute = self::getExposedAttribute($class, $method);
|
|
1027
|
+
|
|
1028
|
+
if (!$attribute) {
|
|
1029
|
+
return ['success' => false, 'error' => 'Method not callable from client'];
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (!self::validateAccess($attribute)) {
|
|
1033
|
+
return ['success' => false, 'error' => 'Permission denied'];
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
try {
|
|
1037
|
+
self::enforceRateLimit($attribute, "method:$class::$method");
|
|
1038
|
+
|
|
1039
|
+
$res = null;
|
|
1040
|
+
if (!$isStatic) {
|
|
1041
|
+
$instance = new $class();
|
|
1042
|
+
if (!is_callable([$instance, $method])) throw new Exception("Method not callable");
|
|
778
1043
|
$res = call_user_func([$instance, $method], $args);
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1044
|
+
} else {
|
|
1045
|
+
if (!is_callable([$class, $method])) throw new Exception("Static method invalid");
|
|
1046
|
+
$res = call_user_func([$class, $method], $args);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if ($res instanceof Generator || $res instanceof SSE) {
|
|
782
1050
|
return $res;
|
|
783
|
-
} catch (Throwable $e) {
|
|
784
|
-
if (isset($_ENV['SHOW_ERRORS']) && $_ENV['SHOW_ERRORS'] === 'false') {
|
|
785
|
-
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
786
|
-
} else {
|
|
787
|
-
return ['success' => false, 'error' => "Instance call error: {$e->getMessage()}"];
|
|
788
|
-
}
|
|
789
1051
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1052
|
+
|
|
1053
|
+
return $res;
|
|
1054
|
+
} catch (Throwable $e) {
|
|
1055
|
+
if ($e->getMessage() === 'Rate limit exceeded. Try again later.') {
|
|
1056
|
+
return ['success' => false, 'error' => $e->getMessage()];
|
|
793
1057
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
return $res;
|
|
800
|
-
} catch (Throwable $e) {
|
|
801
|
-
if (isset($_ENV['SHOW_ERRORS']) && $_ENV['SHOW_ERRORS'] === 'false') {
|
|
802
|
-
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
803
|
-
} else {
|
|
804
|
-
return ['success' => false, 'error' => "Static call error: {$e->getMessage()}"];
|
|
805
|
-
}
|
|
1058
|
+
|
|
1059
|
+
if (isset($_ENV['SHOW_ERRORS']) && $_ENV['SHOW_ERRORS'] === 'false') {
|
|
1060
|
+
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
1061
|
+
} else {
|
|
1062
|
+
return ['success' => false, 'error' => "Call error: {$e->getMessage()}"];
|
|
806
1063
|
}
|
|
807
1064
|
}
|
|
808
|
-
|
|
809
|
-
return ['success' => false, 'error' => 'Invalid callback'];
|
|
810
1065
|
}
|
|
811
1066
|
|
|
812
1067
|
private static function resolveClassImport(string $simpleClassKey): ?array
|
|
@@ -949,6 +1204,22 @@ final class Bootstrap extends RuntimeException
|
|
|
949
1204
|
|
|
950
1205
|
return Request::$isAjax || Request::$isXFileRequest || Request::$fileToInclude === 'route.php';
|
|
951
1206
|
}
|
|
1207
|
+
|
|
1208
|
+
public static function applyRootLayoutId(string $html): string
|
|
1209
|
+
{
|
|
1210
|
+
$rootLayoutPath = self::$layoutsToInclude[0] ?? self::$parentLayoutPath;
|
|
1211
|
+
$rootLayoutId = !empty($rootLayoutPath) ? md5($rootLayoutPath) : 'default-root';
|
|
1212
|
+
|
|
1213
|
+
header('X-PP-Root-Layout: ' . $rootLayoutId);
|
|
1214
|
+
|
|
1215
|
+
$rootLayoutMeta = '<meta name="pp-root-layout" content="' . $rootLayoutId . '">';
|
|
1216
|
+
|
|
1217
|
+
if (strpos($html, '<head>') !== false) {
|
|
1218
|
+
return preg_replace('/<head>/', "<head>\n $rootLayoutMeta", $html, 1);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return $rootLayoutMeta . $html;
|
|
1222
|
+
}
|
|
952
1223
|
}
|
|
953
1224
|
|
|
954
1225
|
Bootstrap::run();
|
|
@@ -989,40 +1260,32 @@ try {
|
|
|
989
1260
|
}
|
|
990
1261
|
|
|
991
1262
|
if (!empty(Bootstrap::$contentToInclude) && !empty(Request::$fileToInclude)) {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
996
|
-
}
|
|
1263
|
+
ob_start();
|
|
1264
|
+
require_once Bootstrap::$contentToInclude;
|
|
1265
|
+
MainLayout::$children = ob_get_clean();
|
|
997
1266
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1267
|
+
if (count(Bootstrap::$layoutsToInclude) > 1) {
|
|
1268
|
+
$nestedLayouts = array_slice(Bootstrap::$layoutsToInclude, 1);
|
|
1002
1269
|
|
|
1003
|
-
|
|
1004
|
-
Bootstrap
|
|
1005
|
-
|
|
1270
|
+
foreach (array_reverse($nestedLayouts) as $layoutPath) {
|
|
1271
|
+
if (!Bootstrap::containsChildren($layoutPath)) {
|
|
1272
|
+
Bootstrap::$isChildContentIncluded = true;
|
|
1273
|
+
}
|
|
1006
1274
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1275
|
+
ob_start();
|
|
1276
|
+
require_once $layoutPath;
|
|
1277
|
+
MainLayout::$children = ob_get_clean();
|
|
1278
|
+
}
|
|
1010
1279
|
}
|
|
1011
1280
|
} else {
|
|
1012
1281
|
ob_start();
|
|
1013
1282
|
require_once APP_PATH . '/not-found.php';
|
|
1014
|
-
MainLayout::$
|
|
1283
|
+
MainLayout::$children = ob_get_clean();
|
|
1015
1284
|
|
|
1016
1285
|
http_response_code(404);
|
|
1017
1286
|
CacheHandler::$isCacheable = false;
|
|
1018
1287
|
}
|
|
1019
1288
|
|
|
1020
|
-
if (Bootstrap::$isParentLayout && !empty(Bootstrap::$contentToInclude)) {
|
|
1021
|
-
ob_start();
|
|
1022
|
-
require_once Bootstrap::$contentToInclude;
|
|
1023
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
1289
|
if (!Bootstrap::$isContentIncluded && !Bootstrap::$isChildContentIncluded) {
|
|
1027
1290
|
if (!Bootstrap::$secondRequestC69CD) {
|
|
1028
1291
|
Bootstrap::createUpdateRequestData();
|
|
@@ -1034,7 +1297,7 @@ try {
|
|
|
1034
1297
|
if (file_exists($file)) {
|
|
1035
1298
|
ob_start();
|
|
1036
1299
|
require_once $file;
|
|
1037
|
-
MainLayout::$
|
|
1300
|
+
MainLayout::$children .= ob_get_clean();
|
|
1038
1301
|
}
|
|
1039
1302
|
}
|
|
1040
1303
|
}
|
|
@@ -1046,57 +1309,47 @@ try {
|
|
|
1046
1309
|
}
|
|
1047
1310
|
|
|
1048
1311
|
if ((!Request::$isWire && !Bootstrap::$secondRequestC69CD) && isset(Bootstrap::$requestFilesData[Request::$decodedUri])) {
|
|
1049
|
-
|
|
1050
|
-
CacheHandler
|
|
1312
|
+
$shouldCache = CacheHandler::$isCacheable === true
|
|
1313
|
+
|| (CacheHandler::$isCacheable === null && $_ENV['CACHE_ENABLED'] === 'true');
|
|
1314
|
+
|
|
1315
|
+
if ($shouldCache) {
|
|
1316
|
+
CacheHandler::serveCache(Request::$decodedUri, intval($_ENV['CACHE_TTL'] ?? 600));
|
|
1051
1317
|
}
|
|
1052
1318
|
}
|
|
1053
1319
|
|
|
1054
|
-
MainLayout::$children
|
|
1320
|
+
MainLayout::$children .= Bootstrap::getLoadingsFiles();
|
|
1055
1321
|
|
|
1056
1322
|
ob_start();
|
|
1057
|
-
|
|
1323
|
+
if (file_exists(Bootstrap::$parentLayoutPath)) {
|
|
1324
|
+
require_once Bootstrap::$parentLayoutPath;
|
|
1325
|
+
} else {
|
|
1326
|
+
echo MainLayout::$children;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1058
1329
|
MainLayout::$html = ob_get_clean();
|
|
1059
1330
|
MainLayout::$html = TemplateCompiler::compile(MainLayout::$html);
|
|
1060
1331
|
MainLayout::$html = TemplateCompiler::injectDynamicContent(MainLayout::$html);
|
|
1332
|
+
MainLayout::$html = Bootstrap::applyRootLayoutId(MainLayout::$html);
|
|
1333
|
+
|
|
1061
1334
|
MainLayout::$html = "<!DOCTYPE html>\n" . MainLayout::$html;
|
|
1062
1335
|
|
|
1063
1336
|
if (
|
|
1064
|
-
http_response_code() === 200
|
|
1337
|
+
http_response_code() === 200
|
|
1338
|
+
&& isset(Bootstrap::$requestFilesData[Request::$decodedUri]['fileName'])
|
|
1339
|
+
&& $shouldCache
|
|
1340
|
+
&& (!Request::$isWire && !Bootstrap::$secondRequestC69CD)
|
|
1065
1341
|
) {
|
|
1066
1342
|
CacheHandler::saveCache(Request::$decodedUri, MainLayout::$html);
|
|
1067
1343
|
}
|
|
1068
1344
|
|
|
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
1345
|
echo MainLayout::$html;
|
|
1088
1346
|
} else {
|
|
1089
1347
|
$layoutPath = Bootstrap::$isContentIncluded
|
|
1090
1348
|
? Bootstrap::$parentLayoutPath
|
|
1091
1349
|
: (Bootstrap::$layoutsToInclude[0] ?? '');
|
|
1092
1350
|
|
|
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
|
-
}
|
|
1351
|
+
$message = "The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?>\n<strong>$layoutPath</strong>";
|
|
1352
|
+
$htmlMessage = "<div class='error'>The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?><br><strong>$layoutPath</strong></div>";
|
|
1100
1353
|
|
|
1101
1354
|
$errorDetails = Bootstrap::isAjaxOrXFileRequestOrRouteFile() ? $message : $htmlMessage;
|
|
1102
1355
|
|