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