create-prisma-php-app 4.2.0-beta → 4.2.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 +362 -251
- 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['pp_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('pp_csrf', $token, [
|
|
147
|
+
'expires' => time() + 3600,
|
|
148
|
+
'path' => '/',
|
|
149
|
+
'secure' => true,
|
|
150
|
+
'httponly' => false,
|
|
151
|
+
'samesite' => 'Lax',
|
|
152
|
+
]);
|
|
153
|
+
$_COOKIE['pp_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['pp_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,60 +258,191 @@ final class Bootstrap extends RuntimeException
|
|
|
270
258
|
}
|
|
271
259
|
}
|
|
272
260
|
|
|
273
|
-
$
|
|
274
|
-
|
|
275
|
-
$
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
261
|
+
$layoutsToInclude = self::collectLayouts($pathname, $groupFolder, $dynamicRoute ?? null);
|
|
262
|
+
} else {
|
|
263
|
+
$includePath = $baseDir . self::getFilePrecedence();
|
|
264
|
+
$layoutsToInclude = self::collectRootLayouts();
|
|
265
|
+
}
|
|
279
266
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
267
|
+
return [
|
|
268
|
+
'path' => $includePath,
|
|
269
|
+
'layouts' => $layoutsToInclude,
|
|
270
|
+
'pathname' => $pathname,
|
|
271
|
+
'uri' => $requestUri
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private static function collectLayouts(string $pathname, ?string $groupFolder, ?string $dynamicRoute): array
|
|
276
|
+
{
|
|
277
|
+
$layoutsToInclude = [];
|
|
278
|
+
$baseDir = APP_PATH;
|
|
279
|
+
|
|
280
|
+
$rootLayout = $baseDir . '/layout.php';
|
|
281
|
+
if (self::fileExistsCached($rootLayout)) {
|
|
282
|
+
$layoutsToInclude[] = $rootLayout;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
$groupName = null;
|
|
286
|
+
$groupParentPath = '';
|
|
287
|
+
$pathAfterGroup = '';
|
|
288
|
+
|
|
289
|
+
if ($groupFolder) {
|
|
290
|
+
$normalizedGroupFolder = str_replace('\\', '/', $groupFolder);
|
|
291
|
+
|
|
292
|
+
if (preg_match('#^\.?/src/app/(.+)/\(([^)]+)\)/(.+)$#', $normalizedGroupFolder, $matches)) {
|
|
293
|
+
$groupParentPath = $matches[1];
|
|
294
|
+
$groupName = $matches[2];
|
|
295
|
+
$pathAfterGroup = dirname($matches[3]);
|
|
296
|
+
if ($pathAfterGroup === '.') {
|
|
297
|
+
$pathAfterGroup = '';
|
|
283
298
|
}
|
|
299
|
+
} elseif (preg_match('#^\.?/src/app/\(([^)]+)\)/(.+)$#', $normalizedGroupFolder, $matches)) {
|
|
300
|
+
$groupName = $matches[1];
|
|
301
|
+
$pathAfterGroup = dirname($matches[2]);
|
|
302
|
+
if ($pathAfterGroup === '.') {
|
|
303
|
+
$pathAfterGroup = '';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if ($groupName && $groupParentPath) {
|
|
309
|
+
$currentPath = $baseDir;
|
|
310
|
+
foreach (explode('/', $groupParentPath) as $segment) {
|
|
311
|
+
if (empty($segment)) continue;
|
|
284
312
|
|
|
285
313
|
$currentPath .= '/' . $segment;
|
|
286
314
|
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
315
|
+
|
|
287
316
|
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
288
317
|
$layoutsToInclude[] = $potentialLayoutPath;
|
|
289
318
|
}
|
|
290
319
|
}
|
|
291
320
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
321
|
+
$groupLayoutPath = $baseDir . '/' . $groupParentPath . "/($groupName)/layout.php";
|
|
322
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
323
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!empty($pathAfterGroup)) {
|
|
327
|
+
$currentPath = $baseDir . '/' . $groupParentPath . "/($groupName)";
|
|
328
|
+
foreach (explode('/', $pathAfterGroup) 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;
|
|
300
336
|
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} elseif ($groupName && !$groupParentPath) {
|
|
340
|
+
$groupLayoutPath = $baseDir . "/($groupName)/layout.php";
|
|
341
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
342
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!empty($pathAfterGroup)) {
|
|
346
|
+
$currentPath = $baseDir . "/($groupName)";
|
|
347
|
+
foreach (explode('/', $pathAfterGroup) as $segment) {
|
|
348
|
+
if (empty($segment)) continue;
|
|
301
349
|
|
|
302
|
-
$
|
|
303
|
-
$
|
|
304
|
-
|
|
305
|
-
|
|
350
|
+
$currentPath .= '/' . $segment;
|
|
351
|
+
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
352
|
+
|
|
353
|
+
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
354
|
+
$layoutsToInclude[] = $potentialLayoutPath;
|
|
306
355
|
}
|
|
307
356
|
}
|
|
308
357
|
}
|
|
358
|
+
} else {
|
|
359
|
+
$currentPath = $baseDir;
|
|
360
|
+
foreach (explode('/', $pathname) as $segment) {
|
|
361
|
+
if (empty($segment)) continue;
|
|
309
362
|
|
|
310
|
-
|
|
311
|
-
$
|
|
363
|
+
$currentPath .= '/' . $segment;
|
|
364
|
+
$potentialLayoutPath = $currentPath . '/layout.php';
|
|
365
|
+
|
|
366
|
+
if ($potentialLayoutPath === $rootLayout) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (self::fileExistsCached($potentialLayoutPath) && !in_array($potentialLayoutPath, $layoutsToInclude, true)) {
|
|
371
|
+
$layoutsToInclude[] = $potentialLayoutPath;
|
|
372
|
+
}
|
|
312
373
|
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (isset($dynamicRoute) && !empty($dynamicRoute)) {
|
|
377
|
+
$currentDynamicPath = $baseDir;
|
|
378
|
+
foreach (explode('/', $dynamicRoute) as $segment) {
|
|
379
|
+
if (empty($segment) || $segment === 'src' || $segment === 'app') {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
$currentDynamicPath .= '/' . $segment;
|
|
384
|
+
$potentialDynamicRoute = $currentDynamicPath . '/layout.php';
|
|
385
|
+
if (self::fileExistsCached($potentialDynamicRoute) && !in_array($potentialDynamicRoute, $layoutsToInclude, true)) {
|
|
386
|
+
$layoutsToInclude[] = $potentialDynamicRoute;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (empty($layoutsToInclude)) {
|
|
392
|
+
$layoutsToInclude = self::findFirstGroupLayout();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return $layoutsToInclude;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private static function collectRootLayouts(): array
|
|
399
|
+
{
|
|
400
|
+
$layoutsToInclude = [];
|
|
401
|
+
$baseDir = APP_PATH;
|
|
402
|
+
$rootLayout = $baseDir . '/layout.php';
|
|
403
|
+
|
|
404
|
+
if (self::fileExistsCached($rootLayout)) {
|
|
405
|
+
$layoutsToInclude[] = $rootLayout;
|
|
313
406
|
} else {
|
|
314
|
-
$
|
|
407
|
+
$layoutsToInclude = self::findFirstGroupLayout();
|
|
408
|
+
|
|
409
|
+
if (empty($layoutsToInclude)) {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
315
412
|
}
|
|
316
413
|
|
|
317
|
-
return
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
414
|
+
return $layoutsToInclude;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private static function findFirstGroupLayout(): array
|
|
418
|
+
{
|
|
419
|
+
$baseDir = APP_PATH;
|
|
420
|
+
$layoutsToInclude = [];
|
|
421
|
+
|
|
422
|
+
if (is_dir($baseDir)) {
|
|
423
|
+
$items = scandir($baseDir);
|
|
424
|
+
foreach ($items as $item) {
|
|
425
|
+
if ($item === '.' || $item === '..') {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (preg_match('/^\([^)]+\)$/', $item)) {
|
|
430
|
+
$groupLayoutPath = $baseDir . '/' . $item . '/layout.php';
|
|
431
|
+
if (self::fileExistsCached($groupLayoutPath)) {
|
|
432
|
+
$layoutsToInclude[] = $groupLayoutPath;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return $layoutsToInclude;
|
|
323
440
|
}
|
|
324
441
|
|
|
325
442
|
private static function getFilePrecedence(): ?string
|
|
326
443
|
{
|
|
444
|
+
$baseDir = APP_PATH;
|
|
445
|
+
|
|
327
446
|
foreach (PrismaPHPSettings::$routeFiles as $route) {
|
|
328
447
|
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
329
448
|
continue;
|
|
@@ -335,6 +454,27 @@ final class Bootstrap extends RuntimeException
|
|
|
335
454
|
return '/index.php';
|
|
336
455
|
}
|
|
337
456
|
}
|
|
457
|
+
|
|
458
|
+
if (is_dir($baseDir)) {
|
|
459
|
+
$items = scandir($baseDir);
|
|
460
|
+
foreach ($items as $item) {
|
|
461
|
+
if ($item === '.' || $item === '..') {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (preg_match('/^\([^)]+\)$/', $item)) {
|
|
466
|
+
$groupDir = $baseDir . '/' . $item;
|
|
467
|
+
|
|
468
|
+
if (file_exists($groupDir . '/route.php')) {
|
|
469
|
+
return '/' . $item . '/route.php';
|
|
470
|
+
}
|
|
471
|
+
if (file_exists($groupDir . '/index.php')) {
|
|
472
|
+
return '/' . $item . '/index.php';
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
338
478
|
return null;
|
|
339
479
|
}
|
|
340
480
|
|
|
@@ -424,17 +564,22 @@ final class Bootstrap extends RuntimeException
|
|
|
424
564
|
}
|
|
425
565
|
}
|
|
426
566
|
} elseif (strpos($normalizedRoute, '[...') !== false) {
|
|
427
|
-
|
|
567
|
+
$cleanedRoute = preg_replace('/\(.+\)/', '', $normalizedRoute);
|
|
568
|
+
$cleanedRoute = preg_replace('/\/+/', '/', $cleanedRoute);
|
|
569
|
+
$staticPart = preg_replace('/\[\.\.\..*?\].*/', '', $cleanedRoute);
|
|
570
|
+
$staticSegments = array_filter(explode('/', $staticPart));
|
|
571
|
+
$minRequiredSegments = count($staticSegments);
|
|
572
|
+
|
|
573
|
+
if (count($pathnameSegments) < $minRequiredSegments) {
|
|
428
574
|
continue;
|
|
429
575
|
}
|
|
430
576
|
|
|
431
|
-
$cleanedNormalizedRoute =
|
|
432
|
-
$
|
|
433
|
-
$dynamicSegmentRoute = preg_replace('/\[\.\.\..*?\].*/', '', $cleanedNormalizedRoute);
|
|
577
|
+
$cleanedNormalizedRoute = $cleanedRoute;
|
|
578
|
+
$dynamicSegmentRoute = $staticPart;
|
|
434
579
|
|
|
435
580
|
if (strpos("/src/app/$normalizedPathname", $dynamicSegmentRoute) === 0) {
|
|
436
581
|
$trimmedPathname = str_replace($dynamicSegmentRoute, '', "/src/app/$normalizedPathname");
|
|
437
|
-
$pathnameParts = explode('/', trim($trimmedPathname, '/'));
|
|
582
|
+
$pathnameParts = $trimmedPathname === '' ? [] : explode('/', trim($trimmedPathname, '/'));
|
|
438
583
|
|
|
439
584
|
if (preg_match('/\[\.\.\.(.*?)\]/', $normalizedRoute, $matches)) {
|
|
440
585
|
$dynamicParam = $matches[1];
|
|
@@ -451,18 +596,14 @@ final class Bootstrap extends RuntimeException
|
|
|
451
596
|
|
|
452
597
|
if (strpos($normalizedRoute, 'index.php') !== false) {
|
|
453
598
|
$segmentMatch = "[...$dynamicParam]";
|
|
454
|
-
$index = array_search($segmentMatch, $filteredRouteSegments);
|
|
455
599
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
600
|
+
$dynamicRoutePathname = str_replace($segmentMatch, implode('/', $pathnameParts), $cleanedNormalizedRoute);
|
|
601
|
+
$dynamicRoutePathnameDirname = rtrim(dirname($dynamicRoutePathname), '/');
|
|
602
|
+
$expectedPathname = rtrim("/src/app/$normalizedPathname", '/');
|
|
459
603
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
$pathnameMatch = $normalizedRoute;
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
604
|
+
if ($expectedPathname === $dynamicRoutePathnameDirname) {
|
|
605
|
+
$pathnameMatch = $normalizedRoute;
|
|
606
|
+
break;
|
|
466
607
|
}
|
|
467
608
|
}
|
|
468
609
|
}
|
|
@@ -483,6 +624,7 @@ final class Bootstrap extends RuntimeException
|
|
|
483
624
|
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
484
625
|
continue;
|
|
485
626
|
}
|
|
627
|
+
|
|
486
628
|
$normalizedRoute = trim(str_replace('\\', '/', $route), '.');
|
|
487
629
|
$cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
|
|
488
630
|
|
|
@@ -494,22 +636,26 @@ final class Bootstrap extends RuntimeException
|
|
|
494
636
|
}
|
|
495
637
|
}
|
|
496
638
|
|
|
497
|
-
|
|
498
|
-
|
|
639
|
+
if (!$bestMatch) {
|
|
640
|
+
foreach (PrismaPHPSettings::$routeFiles as $route) {
|
|
641
|
+
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
499
644
|
|
|
500
|
-
|
|
501
|
-
{
|
|
502
|
-
$lastSlashPos = strrpos($pathname, '/');
|
|
503
|
-
if ($lastSlashPos === false) {
|
|
504
|
-
return "";
|
|
505
|
-
}
|
|
645
|
+
$normalizedRoute = trim(str_replace('\\', '/', $route), '.');
|
|
506
646
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
647
|
+
if (preg_match('/\/\(([^)]+)\)\//', $normalizedRoute, $matches)) {
|
|
648
|
+
$cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
|
|
649
|
+
|
|
650
|
+
if ($cleanedRoute === $routeFile || $cleanedRoute === $indexFile) {
|
|
651
|
+
$bestMatch = $normalizedRoute;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
510
656
|
}
|
|
511
657
|
|
|
512
|
-
return
|
|
658
|
+
return $bestMatch;
|
|
513
659
|
}
|
|
514
660
|
|
|
515
661
|
private static function singleDynamicRoute($pathnameSegments, $routeSegments)
|
|
@@ -578,7 +724,7 @@ final class Bootstrap extends RuntimeException
|
|
|
578
724
|
}
|
|
579
725
|
}
|
|
580
726
|
|
|
581
|
-
public static function
|
|
727
|
+
public static function containsChildren($filePath): bool
|
|
582
728
|
{
|
|
583
729
|
if (!self::fileExistsCached($filePath)) {
|
|
584
730
|
return false;
|
|
@@ -589,52 +735,51 @@ final class Bootstrap extends RuntimeException
|
|
|
589
735
|
return false;
|
|
590
736
|
}
|
|
591
737
|
|
|
592
|
-
$pattern = '/\<\?=\s*MainLayout::\$
|
|
738
|
+
$pattern = '/\<\?=\s*MainLayout::\$children\s*;?\s*\?>|echo\s*MainLayout::\$children\s*;?/';
|
|
593
739
|
return (bool) preg_match($pattern, $fileContent);
|
|
594
740
|
}
|
|
595
741
|
|
|
596
|
-
private static function
|
|
742
|
+
private static function convertToArrayObject($data)
|
|
597
743
|
{
|
|
598
|
-
if (!
|
|
599
|
-
return
|
|
744
|
+
if (!is_array($data)) {
|
|
745
|
+
return $data;
|
|
600
746
|
}
|
|
601
747
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return false;
|
|
748
|
+
if (empty($data)) {
|
|
749
|
+
return $data;
|
|
605
750
|
}
|
|
606
751
|
|
|
607
|
-
$
|
|
608
|
-
return (bool) preg_match($pattern, $fileContent);
|
|
609
|
-
}
|
|
752
|
+
$isAssoc = array_keys($data) !== range(0, count($data) - 1);
|
|
610
753
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
754
|
+
if ($isAssoc) {
|
|
755
|
+
$obj = new stdClass();
|
|
756
|
+
foreach ($data as $key => $value) {
|
|
757
|
+
$obj->$key = self::convertToArrayObject($value);
|
|
758
|
+
}
|
|
759
|
+
return $obj;
|
|
760
|
+
} else {
|
|
761
|
+
return array_map([self::class, 'convertToArrayObject'], $data);
|
|
762
|
+
}
|
|
614
763
|
}
|
|
615
764
|
|
|
616
765
|
public static function wireCallback(): void
|
|
617
766
|
{
|
|
618
|
-
$
|
|
767
|
+
$callbackName = $_SERVER['HTTP_X_PP_FUNCTION'] ?? null;
|
|
619
768
|
|
|
620
|
-
if (empty($
|
|
621
|
-
self::jsonExit(['success' => false, 'error' => 'Callback not provided', 'response' => null]);
|
|
769
|
+
if (empty($callbackName)) {
|
|
770
|
+
self::jsonExit(['success' => false, 'error' => 'Callback header not provided', 'response' => null]);
|
|
622
771
|
}
|
|
623
772
|
|
|
624
|
-
|
|
625
|
-
$aesKey = self::getAesKeyFromJwt();
|
|
626
|
-
} catch (RuntimeException $e) {
|
|
627
|
-
self::jsonExit(['success' => false, 'error' => $e->getMessage()]);
|
|
628
|
-
}
|
|
773
|
+
self::validateCsrfToken();
|
|
629
774
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
} catch (RuntimeException $e) {
|
|
633
|
-
self::jsonExit(['success' => false, 'error' => $e->getMessage()]);
|
|
775
|
+
if (!preg_match('/^[a-zA-Z0-9_:\->]+$/', $callbackName)) {
|
|
776
|
+
self::jsonExit(['success' => false, 'error' => 'Invalid callback format']);
|
|
634
777
|
}
|
|
635
778
|
|
|
779
|
+
$data = self::getRequestData();
|
|
636
780
|
$args = self::convertToArrayObject($data);
|
|
637
|
-
|
|
781
|
+
|
|
782
|
+
$out = str_contains($callbackName, '->') || str_contains($callbackName, '::')
|
|
638
783
|
? self::dispatchMethod($callbackName, $args)
|
|
639
784
|
: self::dispatchFunction($callbackName, $args);
|
|
640
785
|
|
|
@@ -644,63 +789,12 @@ final class Bootstrap extends RuntimeException
|
|
|
644
789
|
exit;
|
|
645
790
|
}
|
|
646
791
|
|
|
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
792
|
private static function jsonExit(array $payload): void
|
|
671
793
|
{
|
|
672
794
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
|
673
795
|
exit;
|
|
674
796
|
}
|
|
675
797
|
|
|
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
798
|
private static function getRequestData(): array
|
|
705
799
|
{
|
|
706
800
|
if (!empty($_FILES)) {
|
|
@@ -728,8 +822,34 @@ final class Bootstrap extends RuntimeException
|
|
|
728
822
|
return (json_last_error() === JSON_ERROR_NONE) ? $json : $_POST;
|
|
729
823
|
}
|
|
730
824
|
|
|
825
|
+
private static function isFunctionAllowed(string $fn): bool
|
|
826
|
+
{
|
|
827
|
+
try {
|
|
828
|
+
$ref = new ReflectionFunction($fn);
|
|
829
|
+
$attrs = $ref->getAttributes(Exposed::class);
|
|
830
|
+
return !empty($attrs);
|
|
831
|
+
} catch (Throwable) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
private static function isMethodAllowed(string $class, string $method): bool
|
|
837
|
+
{
|
|
838
|
+
try {
|
|
839
|
+
$ref = new ReflectionMethod($class, $method);
|
|
840
|
+
$attrs = $ref->getAttributes(Exposed::class);
|
|
841
|
+
return !empty($attrs);
|
|
842
|
+
} catch (Throwable) {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
731
847
|
private static function dispatchFunction(string $fn, mixed $args)
|
|
732
848
|
{
|
|
849
|
+
if (!self::isFunctionAllowed($fn)) {
|
|
850
|
+
return ['success' => false, 'error' => 'Function not callable from client'];
|
|
851
|
+
}
|
|
852
|
+
|
|
733
853
|
if (function_exists($fn) && is_callable($fn)) {
|
|
734
854
|
try {
|
|
735
855
|
$res = call_user_func($fn, $args);
|
|
@@ -750,6 +870,17 @@ final class Bootstrap extends RuntimeException
|
|
|
750
870
|
|
|
751
871
|
private static function dispatchMethod(string $call, mixed $args)
|
|
752
872
|
{
|
|
873
|
+
if (!self::isMethodAllowed(
|
|
874
|
+
strpos($call, '->') !== false
|
|
875
|
+
? explode('->', $call, 2)[0]
|
|
876
|
+
: explode('::', $call, 2)[0],
|
|
877
|
+
strpos($call, '->') !== false
|
|
878
|
+
? explode('->', $call, 2)[1]
|
|
879
|
+
: explode('::', $call, 2)[1]
|
|
880
|
+
)) {
|
|
881
|
+
return ['success' => false, 'error' => 'Method not callable from client'];
|
|
882
|
+
}
|
|
883
|
+
|
|
753
884
|
if (strpos($call, '->') !== false) {
|
|
754
885
|
list($requested, $method) = explode('->', $call, 2);
|
|
755
886
|
$isStatic = false;
|
|
@@ -989,40 +1120,32 @@ try {
|
|
|
989
1120
|
}
|
|
990
1121
|
|
|
991
1122
|
if (!empty(Bootstrap::$contentToInclude) && !empty(Request::$fileToInclude)) {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
996
|
-
}
|
|
1123
|
+
ob_start();
|
|
1124
|
+
require_once Bootstrap::$contentToInclude;
|
|
1125
|
+
MainLayout::$children = ob_get_clean();
|
|
997
1126
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1127
|
+
if (count(Bootstrap::$layoutsToInclude) > 1) {
|
|
1128
|
+
$nestedLayouts = array_slice(Bootstrap::$layoutsToInclude, 1);
|
|
1002
1129
|
|
|
1003
|
-
|
|
1004
|
-
Bootstrap
|
|
1005
|
-
|
|
1130
|
+
foreach (array_reverse($nestedLayouts) as $layoutPath) {
|
|
1131
|
+
if (!Bootstrap::containsChildren($layoutPath)) {
|
|
1132
|
+
Bootstrap::$isChildContentIncluded = true;
|
|
1133
|
+
}
|
|
1006
1134
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1135
|
+
ob_start();
|
|
1136
|
+
require_once $layoutPath;
|
|
1137
|
+
MainLayout::$children = ob_get_clean();
|
|
1138
|
+
}
|
|
1010
1139
|
}
|
|
1011
1140
|
} else {
|
|
1012
1141
|
ob_start();
|
|
1013
1142
|
require_once APP_PATH . '/not-found.php';
|
|
1014
|
-
MainLayout::$
|
|
1143
|
+
MainLayout::$children = ob_get_clean();
|
|
1015
1144
|
|
|
1016
1145
|
http_response_code(404);
|
|
1017
1146
|
CacheHandler::$isCacheable = false;
|
|
1018
1147
|
}
|
|
1019
1148
|
|
|
1020
|
-
if (Bootstrap::$isParentLayout && !empty(Bootstrap::$contentToInclude)) {
|
|
1021
|
-
ob_start();
|
|
1022
|
-
require_once Bootstrap::$contentToInclude;
|
|
1023
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
1149
|
if (!Bootstrap::$isContentIncluded && !Bootstrap::$isChildContentIncluded) {
|
|
1027
1150
|
if (!Bootstrap::$secondRequestC69CD) {
|
|
1028
1151
|
Bootstrap::createUpdateRequestData();
|
|
@@ -1034,7 +1157,7 @@ try {
|
|
|
1034
1157
|
if (file_exists($file)) {
|
|
1035
1158
|
ob_start();
|
|
1036
1159
|
require_once $file;
|
|
1037
|
-
MainLayout::$
|
|
1160
|
+
MainLayout::$children .= ob_get_clean();
|
|
1038
1161
|
}
|
|
1039
1162
|
}
|
|
1040
1163
|
}
|
|
@@ -1046,57 +1169,45 @@ try {
|
|
|
1046
1169
|
}
|
|
1047
1170
|
|
|
1048
1171
|
if ((!Request::$isWire && !Bootstrap::$secondRequestC69CD) && isset(Bootstrap::$requestFilesData[Request::$decodedUri])) {
|
|
1049
|
-
|
|
1050
|
-
CacheHandler
|
|
1172
|
+
$shouldCache = CacheHandler::$isCacheable === true
|
|
1173
|
+
|| (CacheHandler::$isCacheable === null && $_ENV['CACHE_ENABLED'] === 'true');
|
|
1174
|
+
|
|
1175
|
+
if ($shouldCache) {
|
|
1176
|
+
CacheHandler::serveCache(Request::$decodedUri, intval($_ENV['CACHE_TTL'] ?? 600));
|
|
1051
1177
|
}
|
|
1052
1178
|
}
|
|
1053
1179
|
|
|
1054
|
-
MainLayout::$children
|
|
1180
|
+
MainLayout::$children .= Bootstrap::getLoadingsFiles();
|
|
1055
1181
|
|
|
1056
1182
|
ob_start();
|
|
1057
|
-
|
|
1183
|
+
if (file_exists(Bootstrap::$parentLayoutPath)) {
|
|
1184
|
+
require_once Bootstrap::$parentLayoutPath;
|
|
1185
|
+
} else {
|
|
1186
|
+
echo MainLayout::$children;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1058
1189
|
MainLayout::$html = ob_get_clean();
|
|
1059
1190
|
MainLayout::$html = TemplateCompiler::compile(MainLayout::$html);
|
|
1060
1191
|
MainLayout::$html = TemplateCompiler::injectDynamicContent(MainLayout::$html);
|
|
1061
1192
|
MainLayout::$html = "<!DOCTYPE html>\n" . MainLayout::$html;
|
|
1062
1193
|
|
|
1063
1194
|
if (
|
|
1064
|
-
http_response_code() === 200
|
|
1195
|
+
http_response_code() === 200
|
|
1196
|
+
&& isset(Bootstrap::$requestFilesData[Request::$decodedUri]['fileName'])
|
|
1197
|
+
&& $shouldCache
|
|
1198
|
+
&& (!Request::$isWire && !Bootstrap::$secondRequestC69CD)
|
|
1065
1199
|
) {
|
|
1066
1200
|
CacheHandler::saveCache(Request::$decodedUri, MainLayout::$html);
|
|
1067
1201
|
}
|
|
1068
1202
|
|
|
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
1203
|
echo MainLayout::$html;
|
|
1088
1204
|
} else {
|
|
1089
1205
|
$layoutPath = Bootstrap::$isContentIncluded
|
|
1090
1206
|
? Bootstrap::$parentLayoutPath
|
|
1091
1207
|
: (Bootstrap::$layoutsToInclude[0] ?? '');
|
|
1092
1208
|
|
|
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
|
-
}
|
|
1209
|
+
$message = "The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?>\n<strong>$layoutPath</strong>";
|
|
1210
|
+
$htmlMessage = "<div class='error'>The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?><br><strong>$layoutPath</strong></div>";
|
|
1100
1211
|
|
|
1101
1212
|
$errorDetails = Bootstrap::isAjaxOrXFileRequestOrRouteFile() ? $message : $htmlMessage;
|
|
1102
1213
|
|