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