create-prisma-php-app 4.2.2-beta → 4.2.2
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 +406 -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
|
|
139
|
+
{
|
|
140
|
+
if (!isset($_COOKIE['prisma_php_csrf'])) {
|
|
141
|
+
$secret = $_ENV['FUNCTION_CALL_SECRET'] ?? 'pp_default_insecure_secret';
|
|
142
|
+
$nonce = bin2hex(random_bytes(16));
|
|
143
|
+
$signature = hash_hmac('sha256', $nonce, $secret);
|
|
144
|
+
$token = $nonce . '.' . $signature;
|
|
145
|
+
|
|
146
|
+
setcookie('prisma_php_csrf', $token, [
|
|
147
|
+
'expires' => time() + 3600,
|
|
148
|
+
'path' => '/',
|
|
149
|
+
'secure' => true,
|
|
150
|
+
'httponly' => false,
|
|
151
|
+
'samesite' => 'Lax',
|
|
152
|
+
]);
|
|
153
|
+
$_COOKIE['prisma_php_csrf'] = $token;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private static function validateCsrfToken(): void
|
|
158
158
|
{
|
|
159
|
-
$
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
$headerToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
|
160
|
+
$cookieToken = $_COOKIE['prisma_php_csrf'] ?? '';
|
|
161
|
+
$secret = $_ENV['FUNCTION_CALL_SECRET'] ?? '';
|
|
162
|
+
|
|
163
|
+
if (empty($headerToken) || empty($cookieToken)) {
|
|
164
|
+
self::jsonExit(['success' => false, 'error' => 'CSRF token missing']);
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
$
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
$decoded = JWT::decode($existing, new Key($hmacSecret, 'HS256'));
|
|
168
|
-
if (isset($decoded->exp) && $decoded->exp > time() + 15) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
} catch (Throwable) {
|
|
172
|
-
}
|
|
167
|
+
if (!hash_equals($cookieToken, $headerToken)) {
|
|
168
|
+
self::jsonExit(['success' => false, 'error' => 'CSRF token mismatch']);
|
|
173
169
|
}
|
|
174
170
|
|
|
175
|
-
$
|
|
176
|
-
$
|
|
177
|
-
'
|
|
178
|
-
|
|
179
|
-
'iat' => time(),
|
|
180
|
-
];
|
|
181
|
-
$jwt = JWT::encode($payload, $hmacSecret, 'HS256');
|
|
171
|
+
$parts = explode('.', $cookieToken);
|
|
172
|
+
if (count($parts) !== 2) {
|
|
173
|
+
self::jsonExit(['success' => false, 'error' => 'Invalid CSRF token format']);
|
|
174
|
+
}
|
|
182
175
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
'secure' => true,
|
|
190
|
-
'httponly' => false,
|
|
191
|
-
'samesite' => 'Strict',
|
|
192
|
-
]
|
|
193
|
-
);
|
|
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
|
+
}
|
|
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
|
+
}
|
|
312
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,78 @@ final class Bootstrap extends RuntimeException
|
|
|
728
822
|
return (json_last_error() === JSON_ERROR_NONE) ? $json : $_POST;
|
|
729
823
|
}
|
|
730
824
|
|
|
825
|
+
private static function validateAccess(Exposed $attribute): bool
|
|
826
|
+
{
|
|
827
|
+
if ($attribute->requiresAuth || !empty($attribute->allowedRoles)) {
|
|
828
|
+
$auth = Auth::getInstance();
|
|
829
|
+
|
|
830
|
+
if (!$auth->isAuthenticated()) {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (!empty($attribute->allowedRoles)) {
|
|
835
|
+
$payload = $auth->getPayload();
|
|
836
|
+
$currentRole = null;
|
|
837
|
+
|
|
838
|
+
if (is_scalar($payload)) {
|
|
839
|
+
$currentRole = $payload;
|
|
840
|
+
} else {
|
|
841
|
+
$roleKey = !empty(Auth::ROLE_NAME) ? Auth::ROLE_NAME : 'role';
|
|
842
|
+
|
|
843
|
+
if (is_object($payload)) {
|
|
844
|
+
$currentRole = $payload->$roleKey ?? null;
|
|
845
|
+
} elseif (is_array($payload)) {
|
|
846
|
+
$currentRole = $payload[$roleKey] ?? null;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if ($currentRole === null || !in_array($currentRole, $attribute->allowedRoles)) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private static function isFunctionAllowed(string $fn): bool
|
|
860
|
+
{
|
|
861
|
+
try {
|
|
862
|
+
$ref = new ReflectionFunction($fn);
|
|
863
|
+
$attrs = $ref->getAttributes(Exposed::class);
|
|
864
|
+
|
|
865
|
+
if (empty($attrs)) {
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return self::validateAccess($attrs[0]->newInstance());
|
|
870
|
+
} catch (Throwable) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private static function isMethodAllowed(string $class, string $method): bool
|
|
876
|
+
{
|
|
877
|
+
try {
|
|
878
|
+
$ref = new ReflectionMethod($class, $method);
|
|
879
|
+
$attrs = $ref->getAttributes(Exposed::class);
|
|
880
|
+
|
|
881
|
+
if (empty($attrs)) {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return self::validateAccess($attrs[0]->newInstance());
|
|
886
|
+
} catch (Throwable) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
731
891
|
private static function dispatchFunction(string $fn, mixed $args)
|
|
732
892
|
{
|
|
893
|
+
if (!self::isFunctionAllowed($fn)) {
|
|
894
|
+
return ['success' => false, 'error' => 'Function not callable from client'];
|
|
895
|
+
}
|
|
896
|
+
|
|
733
897
|
if (function_exists($fn) && is_callable($fn)) {
|
|
734
898
|
try {
|
|
735
899
|
$res = call_user_func($fn, $args);
|
|
@@ -750,6 +914,17 @@ final class Bootstrap extends RuntimeException
|
|
|
750
914
|
|
|
751
915
|
private static function dispatchMethod(string $call, mixed $args)
|
|
752
916
|
{
|
|
917
|
+
if (!self::isMethodAllowed(
|
|
918
|
+
strpos($call, '->') !== false
|
|
919
|
+
? explode('->', $call, 2)[0]
|
|
920
|
+
: explode('::', $call, 2)[0],
|
|
921
|
+
strpos($call, '->') !== false
|
|
922
|
+
? explode('->', $call, 2)[1]
|
|
923
|
+
: explode('::', $call, 2)[1]
|
|
924
|
+
)) {
|
|
925
|
+
return ['success' => false, 'error' => 'Method not callable from client'];
|
|
926
|
+
}
|
|
927
|
+
|
|
753
928
|
if (strpos($call, '->') !== false) {
|
|
754
929
|
list($requested, $method) = explode('->', $call, 2);
|
|
755
930
|
$isStatic = false;
|
|
@@ -989,40 +1164,32 @@ try {
|
|
|
989
1164
|
}
|
|
990
1165
|
|
|
991
1166
|
if (!empty(Bootstrap::$contentToInclude) && !empty(Request::$fileToInclude)) {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
996
|
-
}
|
|
1167
|
+
ob_start();
|
|
1168
|
+
require_once Bootstrap::$contentToInclude;
|
|
1169
|
+
MainLayout::$children = ob_get_clean();
|
|
997
1170
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1171
|
+
if (count(Bootstrap::$layoutsToInclude) > 1) {
|
|
1172
|
+
$nestedLayouts = array_slice(Bootstrap::$layoutsToInclude, 1);
|
|
1002
1173
|
|
|
1003
|
-
|
|
1004
|
-
Bootstrap
|
|
1005
|
-
|
|
1174
|
+
foreach (array_reverse($nestedLayouts) as $layoutPath) {
|
|
1175
|
+
if (!Bootstrap::containsChildren($layoutPath)) {
|
|
1176
|
+
Bootstrap::$isChildContentIncluded = true;
|
|
1177
|
+
}
|
|
1006
1178
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1179
|
+
ob_start();
|
|
1180
|
+
require_once $layoutPath;
|
|
1181
|
+
MainLayout::$children = ob_get_clean();
|
|
1182
|
+
}
|
|
1010
1183
|
}
|
|
1011
1184
|
} else {
|
|
1012
1185
|
ob_start();
|
|
1013
1186
|
require_once APP_PATH . '/not-found.php';
|
|
1014
|
-
MainLayout::$
|
|
1187
|
+
MainLayout::$children = ob_get_clean();
|
|
1015
1188
|
|
|
1016
1189
|
http_response_code(404);
|
|
1017
1190
|
CacheHandler::$isCacheable = false;
|
|
1018
1191
|
}
|
|
1019
1192
|
|
|
1020
|
-
if (Bootstrap::$isParentLayout && !empty(Bootstrap::$contentToInclude)) {
|
|
1021
|
-
ob_start();
|
|
1022
|
-
require_once Bootstrap::$contentToInclude;
|
|
1023
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
1193
|
if (!Bootstrap::$isContentIncluded && !Bootstrap::$isChildContentIncluded) {
|
|
1027
1194
|
if (!Bootstrap::$secondRequestC69CD) {
|
|
1028
1195
|
Bootstrap::createUpdateRequestData();
|
|
@@ -1034,7 +1201,7 @@ try {
|
|
|
1034
1201
|
if (file_exists($file)) {
|
|
1035
1202
|
ob_start();
|
|
1036
1203
|
require_once $file;
|
|
1037
|
-
MainLayout::$
|
|
1204
|
+
MainLayout::$children .= ob_get_clean();
|
|
1038
1205
|
}
|
|
1039
1206
|
}
|
|
1040
1207
|
}
|
|
@@ -1046,57 +1213,45 @@ try {
|
|
|
1046
1213
|
}
|
|
1047
1214
|
|
|
1048
1215
|
if ((!Request::$isWire && !Bootstrap::$secondRequestC69CD) && isset(Bootstrap::$requestFilesData[Request::$decodedUri])) {
|
|
1049
|
-
|
|
1050
|
-
CacheHandler
|
|
1216
|
+
$shouldCache = CacheHandler::$isCacheable === true
|
|
1217
|
+
|| (CacheHandler::$isCacheable === null && $_ENV['CACHE_ENABLED'] === 'true');
|
|
1218
|
+
|
|
1219
|
+
if ($shouldCache) {
|
|
1220
|
+
CacheHandler::serveCache(Request::$decodedUri, intval($_ENV['CACHE_TTL'] ?? 600));
|
|
1051
1221
|
}
|
|
1052
1222
|
}
|
|
1053
1223
|
|
|
1054
|
-
MainLayout::$children
|
|
1224
|
+
MainLayout::$children .= Bootstrap::getLoadingsFiles();
|
|
1055
1225
|
|
|
1056
1226
|
ob_start();
|
|
1057
|
-
|
|
1227
|
+
if (file_exists(Bootstrap::$parentLayoutPath)) {
|
|
1228
|
+
require_once Bootstrap::$parentLayoutPath;
|
|
1229
|
+
} else {
|
|
1230
|
+
echo MainLayout::$children;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1058
1233
|
MainLayout::$html = ob_get_clean();
|
|
1059
1234
|
MainLayout::$html = TemplateCompiler::compile(MainLayout::$html);
|
|
1060
1235
|
MainLayout::$html = TemplateCompiler::injectDynamicContent(MainLayout::$html);
|
|
1061
1236
|
MainLayout::$html = "<!DOCTYPE html>\n" . MainLayout::$html;
|
|
1062
1237
|
|
|
1063
1238
|
if (
|
|
1064
|
-
http_response_code() === 200
|
|
1239
|
+
http_response_code() === 200
|
|
1240
|
+
&& isset(Bootstrap::$requestFilesData[Request::$decodedUri]['fileName'])
|
|
1241
|
+
&& $shouldCache
|
|
1242
|
+
&& (!Request::$isWire && !Bootstrap::$secondRequestC69CD)
|
|
1065
1243
|
) {
|
|
1066
1244
|
CacheHandler::saveCache(Request::$decodedUri, MainLayout::$html);
|
|
1067
1245
|
}
|
|
1068
1246
|
|
|
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
1247
|
echo MainLayout::$html;
|
|
1088
1248
|
} else {
|
|
1089
1249
|
$layoutPath = Bootstrap::$isContentIncluded
|
|
1090
1250
|
? Bootstrap::$parentLayoutPath
|
|
1091
1251
|
: (Bootstrap::$layoutsToInclude[0] ?? '');
|
|
1092
1252
|
|
|
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
|
-
}
|
|
1253
|
+
$message = "The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?>\n<strong>$layoutPath</strong>";
|
|
1254
|
+
$htmlMessage = "<div class='error'>The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?><br><strong>$layoutPath</strong></div>";
|
|
1100
1255
|
|
|
1101
1256
|
$errorDetails = Bootstrap::isAjaxOrXFileRequestOrRouteFile() ? $message : $htmlMessage;
|
|
1102
1257
|
|