create-prisma-php-app 4.4.6-beta → 4.4.6
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 +560 -284
- 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
|
|
@@ -484,6 +666,7 @@ final class Bootstrap extends RuntimeException
|
|
|
484
666
|
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
485
667
|
continue;
|
|
486
668
|
}
|
|
669
|
+
|
|
487
670
|
$normalizedRoute = trim(str_replace('\\', '/', $route), '.');
|
|
488
671
|
$cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
|
|
489
672
|
|
|
@@ -495,22 +678,26 @@ final class Bootstrap extends RuntimeException
|
|
|
495
678
|
}
|
|
496
679
|
}
|
|
497
680
|
|
|
498
|
-
|
|
499
|
-
|
|
681
|
+
if (!$bestMatch) {
|
|
682
|
+
foreach (PrismaPHPSettings::$routeFiles as $route) {
|
|
683
|
+
if (pathinfo($route, PATHINFO_EXTENSION) !== 'php') {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
500
686
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
return "";
|
|
506
|
-
}
|
|
687
|
+
$normalizedRoute = trim(str_replace('\\', '/', $route), '.');
|
|
688
|
+
|
|
689
|
+
if (preg_match('/\/\(([^)]+)\)\//', $normalizedRoute, $matches)) {
|
|
690
|
+
$cleanedRoute = preg_replace('/\/\([^)]+\)/', '', $normalizedRoute);
|
|
507
691
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
692
|
+
if ($cleanedRoute === $routeFile || $cleanedRoute === $indexFile) {
|
|
693
|
+
$bestMatch = $normalizedRoute;
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
511
698
|
}
|
|
512
699
|
|
|
513
|
-
return
|
|
700
|
+
return $bestMatch;
|
|
514
701
|
}
|
|
515
702
|
|
|
516
703
|
private static function singleDynamicRoute($pathnameSegments, $routeSegments)
|
|
@@ -530,7 +717,7 @@ final class Bootstrap extends RuntimeException
|
|
|
530
717
|
|
|
531
718
|
private static function checkForDuplicateRoutes(): void
|
|
532
719
|
{
|
|
533
|
-
if (
|
|
720
|
+
if (Env::string('APP_ENV', 'production') === 'production') {
|
|
534
721
|
return;
|
|
535
722
|
}
|
|
536
723
|
|
|
@@ -579,7 +766,7 @@ final class Bootstrap extends RuntimeException
|
|
|
579
766
|
}
|
|
580
767
|
}
|
|
581
768
|
|
|
582
|
-
public static function
|
|
769
|
+
public static function containsChildren($filePath): bool
|
|
583
770
|
{
|
|
584
771
|
if (!self::fileExistsCached($filePath)) {
|
|
585
772
|
return false;
|
|
@@ -590,118 +777,76 @@ final class Bootstrap extends RuntimeException
|
|
|
590
777
|
return false;
|
|
591
778
|
}
|
|
592
779
|
|
|
593
|
-
$pattern = '/\<\?=\s*MainLayout::\$
|
|
780
|
+
$pattern = '/\<\?=\s*MainLayout::\$children\s*;?\s*\?>|echo\s*MainLayout::\$children\s*;?/';
|
|
594
781
|
return (bool) preg_match($pattern, $fileContent);
|
|
595
782
|
}
|
|
596
783
|
|
|
597
|
-
private static function
|
|
784
|
+
private static function convertToArrayObject($data)
|
|
598
785
|
{
|
|
599
|
-
if (!
|
|
600
|
-
return
|
|
786
|
+
if (!is_array($data)) {
|
|
787
|
+
return $data;
|
|
601
788
|
}
|
|
602
789
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
return false;
|
|
790
|
+
if (empty($data)) {
|
|
791
|
+
return $data;
|
|
606
792
|
}
|
|
607
793
|
|
|
608
|
-
$
|
|
609
|
-
return (bool) preg_match($pattern, $fileContent);
|
|
610
|
-
}
|
|
794
|
+
$isAssoc = array_keys($data) !== range(0, count($data) - 1);
|
|
611
795
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
+
}
|
|
615
805
|
}
|
|
616
806
|
|
|
617
807
|
public static function wireCallback(): void
|
|
618
808
|
{
|
|
619
|
-
$
|
|
809
|
+
$callbackName = $_SERVER['HTTP_X_PP_FUNCTION'] ?? null;
|
|
620
810
|
|
|
621
|
-
if (empty($
|
|
622
|
-
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]);
|
|
623
813
|
}
|
|
624
814
|
|
|
625
|
-
|
|
626
|
-
$aesKey = self::getAesKeyFromJwt();
|
|
627
|
-
} catch (RuntimeException $e) {
|
|
628
|
-
self::jsonExit(['success' => false, 'error' => $e->getMessage()]);
|
|
629
|
-
}
|
|
815
|
+
self::validateCsrfToken();
|
|
630
816
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
} catch (RuntimeException $e) {
|
|
634
|
-
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']);
|
|
635
819
|
}
|
|
636
820
|
|
|
821
|
+
$data = self::getRequestData();
|
|
637
822
|
$args = self::convertToArrayObject($data);
|
|
638
|
-
|
|
823
|
+
|
|
824
|
+
$out = str_contains($callbackName, '->') || str_contains($callbackName, '::')
|
|
639
825
|
? self::dispatchMethod($callbackName, $args)
|
|
640
826
|
: self::dispatchFunction($callbackName, $args);
|
|
641
827
|
|
|
642
|
-
if ($out
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
exit;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
private static function getAesKeyFromJwt(): string
|
|
649
|
-
{
|
|
650
|
-
$token = $_COOKIE['pp_function_call_jwt'] ?? null;
|
|
651
|
-
$jwtSecret = $_ENV['FUNCTION_CALL_SECRET'] ?? null;
|
|
652
|
-
|
|
653
|
-
if (!$token || !$jwtSecret) {
|
|
654
|
-
throw new RuntimeException('Missing session key or secret');
|
|
828
|
+
if ($out instanceof SSE) {
|
|
829
|
+
$out->send();
|
|
830
|
+
exit;
|
|
655
831
|
}
|
|
656
832
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
throw new RuntimeException('Invalid session key');
|
|
833
|
+
if ($out instanceof Generator) {
|
|
834
|
+
(new SSE($out))->send();
|
|
835
|
+
exit;
|
|
661
836
|
}
|
|
662
837
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
throw new RuntimeException('Bad key length');
|
|
838
|
+
if ($out !== null) {
|
|
839
|
+
self::jsonExit($out);
|
|
666
840
|
}
|
|
667
|
-
|
|
668
|
-
return $aesKey;
|
|
841
|
+
exit;
|
|
669
842
|
}
|
|
670
843
|
|
|
671
|
-
private static function jsonExit(
|
|
844
|
+
private static function jsonExit(mixed $payload): void
|
|
672
845
|
{
|
|
673
846
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
|
674
847
|
exit;
|
|
675
848
|
}
|
|
676
849
|
|
|
677
|
-
private static function decryptCallback(string $encrypted, string $aesKey): string
|
|
678
|
-
{
|
|
679
|
-
$parts = explode(':', $encrypted, 2);
|
|
680
|
-
if (count($parts) !== 2) {
|
|
681
|
-
throw new RuntimeException('Malformed callback payload');
|
|
682
|
-
}
|
|
683
|
-
[$ivB64, $ctB64] = $parts;
|
|
684
|
-
|
|
685
|
-
$iv = base64_decode($ivB64, true);
|
|
686
|
-
$ct = base64_decode($ctB64, true);
|
|
687
|
-
|
|
688
|
-
if ($iv === false || strlen($iv) !== 16 || $ct === false) {
|
|
689
|
-
throw new RuntimeException('Invalid callback payload');
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
$plain = openssl_decrypt($ct, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
|
|
693
|
-
if ($plain === false) {
|
|
694
|
-
throw new RuntimeException('Decryption failed');
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
$callback = preg_replace('/[^a-zA-Z0-9_:\->]/', '', $plain);
|
|
698
|
-
if ($callback === '' || $callback[0] === '_') {
|
|
699
|
-
throw new RuntimeException('Invalid callback');
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
return $callback;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
850
|
private static function getRequestData(): array
|
|
706
851
|
{
|
|
707
852
|
if (!empty($_FILES)) {
|
|
@@ -729,17 +874,140 @@ final class Bootstrap extends RuntimeException
|
|
|
729
874
|
return (json_last_error() === JSON_ERROR_NONE) ? $json : $_POST;
|
|
730
875
|
}
|
|
731
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
|
+
|
|
732
976
|
private static function dispatchFunction(string $fn, mixed $args)
|
|
733
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
|
+
|
|
734
994
|
if (function_exists($fn) && is_callable($fn)) {
|
|
735
995
|
try {
|
|
996
|
+
self::enforceRateLimit($attribute, "fn:$fn");
|
|
997
|
+
|
|
736
998
|
$res = call_user_func($fn, $args);
|
|
737
|
-
|
|
738
|
-
|
|
999
|
+
|
|
1000
|
+
if ($res instanceof Generator || $res instanceof SSE) {
|
|
1001
|
+
return $res;
|
|
739
1002
|
}
|
|
1003
|
+
|
|
740
1004
|
return $res;
|
|
741
1005
|
} catch (Throwable $e) {
|
|
742
|
-
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') {
|
|
743
1011
|
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
744
1012
|
} else {
|
|
745
1013
|
return ['success' => false, 'error' => "Function error: {$e->getMessage()}"];
|
|
@@ -752,14 +1020,15 @@ final class Bootstrap extends RuntimeException
|
|
|
752
1020
|
private static function dispatchMethod(string $call, mixed $args)
|
|
753
1021
|
{
|
|
754
1022
|
if (strpos($call, '->') !== false) {
|
|
755
|
-
|
|
1023
|
+
[$requested, $method] = explode('->', $call, 2);
|
|
756
1024
|
$isStatic = false;
|
|
757
1025
|
} else {
|
|
758
|
-
|
|
1026
|
+
[$requested, $method] = explode('::', $call, 2);
|
|
759
1027
|
$isStatic = true;
|
|
760
1028
|
}
|
|
761
1029
|
|
|
762
1030
|
$class = $requested;
|
|
1031
|
+
|
|
763
1032
|
if (!class_exists($class)) {
|
|
764
1033
|
if ($import = self::resolveClassImport($requested)) {
|
|
765
1034
|
require_once $import['file'];
|
|
@@ -767,47 +1036,52 @@ final class Bootstrap extends RuntimeException
|
|
|
767
1036
|
}
|
|
768
1037
|
}
|
|
769
1038
|
|
|
770
|
-
if (
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1039
|
+
if (!class_exists($class)) {
|
|
1040
|
+
return ['success' => false, 'error' => "Class '$requested' not found"];
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (!self::isMethodAllowed($class, $method)) {
|
|
1044
|
+
return ['success' => false, 'error' => 'Method not callable from client'];
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
$attribute = self::getExposedAttribute($class, $method);
|
|
1048
|
+
if (!$attribute) {
|
|
1049
|
+
return ['success' => false, 'error' => 'Method not callable from client'];
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (!self::validateAccess($attribute)) {
|
|
1053
|
+
return ['success' => false, 'error' => 'Permission denied'];
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
try {
|
|
1057
|
+
self::enforceRateLimit($attribute, "method:$class::$method");
|
|
1058
|
+
|
|
1059
|
+
$res = null;
|
|
1060
|
+
if (!$isStatic) {
|
|
1061
|
+
$instance = new $class();
|
|
1062
|
+
if (!is_callable([$instance, $method])) throw new Exception("Method not callable");
|
|
779
1063
|
$res = call_user_func([$instance, $method], $args);
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1064
|
+
} else {
|
|
1065
|
+
if (!is_callable([$class, $method])) throw new Exception("Static method invalid");
|
|
1066
|
+
$res = call_user_func([$class, $method], $args);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if ($res instanceof Generator || $res instanceof SSE) {
|
|
783
1070
|
return $res;
|
|
784
|
-
} catch (Throwable $e) {
|
|
785
|
-
if (isset($_ENV['SHOW_ERRORS']) && $_ENV['SHOW_ERRORS'] === 'false') {
|
|
786
|
-
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
787
|
-
} else {
|
|
788
|
-
return ['success' => false, 'error' => "Instance call error: {$e->getMessage()}"];
|
|
789
|
-
}
|
|
790
1071
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1072
|
+
|
|
1073
|
+
return $res;
|
|
1074
|
+
} catch (Throwable $e) {
|
|
1075
|
+
if ($e->getMessage() === 'Rate limit exceeded. Try again later.') {
|
|
1076
|
+
return ['success' => false, 'error' => $e->getMessage()];
|
|
794
1077
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
return $res;
|
|
801
|
-
} catch (Throwable $e) {
|
|
802
|
-
if (isset($_ENV['SHOW_ERRORS']) && $_ENV['SHOW_ERRORS'] === 'false') {
|
|
803
|
-
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
804
|
-
} else {
|
|
805
|
-
return ['success' => false, 'error' => "Static call error: {$e->getMessage()}"];
|
|
806
|
-
}
|
|
1078
|
+
|
|
1079
|
+
if (Env::string('SHOW_ERRORS', 'false') === 'false') {
|
|
1080
|
+
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
1081
|
+
} else {
|
|
1082
|
+
return ['success' => false, 'error' => "Call error: {$e->getMessage()}"];
|
|
807
1083
|
}
|
|
808
1084
|
}
|
|
809
|
-
|
|
810
|
-
return ['success' => false, 'error' => 'Invalid callback'];
|
|
811
1085
|
}
|
|
812
1086
|
|
|
813
1087
|
private static function resolveClassImport(string $simpleClassKey): ?array
|
|
@@ -950,6 +1224,22 @@ final class Bootstrap extends RuntimeException
|
|
|
950
1224
|
|
|
951
1225
|
return Request::$isAjax || Request::$isXFileRequest || Request::$fileToInclude === 'route.php';
|
|
952
1226
|
}
|
|
1227
|
+
|
|
1228
|
+
public static function applyRootLayoutId(string $html): string
|
|
1229
|
+
{
|
|
1230
|
+
$rootLayoutPath = self::$layoutsToInclude[0] ?? self::$parentLayoutPath;
|
|
1231
|
+
$rootLayoutId = !empty($rootLayoutPath) ? md5($rootLayoutPath) : 'default-root';
|
|
1232
|
+
|
|
1233
|
+
header('X-PP-Root-Layout: ' . $rootLayoutId);
|
|
1234
|
+
|
|
1235
|
+
$rootLayoutMeta = '<meta name="pp-root-layout" content="' . $rootLayoutId . '">';
|
|
1236
|
+
|
|
1237
|
+
if (strpos($html, '<head>') !== false) {
|
|
1238
|
+
return preg_replace('/<head>/', "<head>\n $rootLayoutMeta", $html, 1);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return $rootLayoutMeta . $html;
|
|
1242
|
+
}
|
|
953
1243
|
}
|
|
954
1244
|
|
|
955
1245
|
Bootstrap::run();
|
|
@@ -990,40 +1280,32 @@ try {
|
|
|
990
1280
|
}
|
|
991
1281
|
|
|
992
1282
|
if (!empty(Bootstrap::$contentToInclude) && !empty(Request::$fileToInclude)) {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
997
|
-
}
|
|
1283
|
+
ob_start();
|
|
1284
|
+
require_once Bootstrap::$contentToInclude;
|
|
1285
|
+
MainLayout::$children = ob_get_clean();
|
|
998
1286
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
continue;
|
|
1002
|
-
}
|
|
1287
|
+
if (count(Bootstrap::$layoutsToInclude) > 1) {
|
|
1288
|
+
$nestedLayouts = array_slice(Bootstrap::$layoutsToInclude, 1);
|
|
1003
1289
|
|
|
1004
|
-
|
|
1005
|
-
Bootstrap
|
|
1006
|
-
|
|
1290
|
+
foreach (array_reverse($nestedLayouts) as $layoutPath) {
|
|
1291
|
+
if (!Bootstrap::containsChildren($layoutPath)) {
|
|
1292
|
+
Bootstrap::$isChildContentIncluded = true;
|
|
1293
|
+
}
|
|
1007
1294
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1295
|
+
ob_start();
|
|
1296
|
+
require_once $layoutPath;
|
|
1297
|
+
MainLayout::$children = ob_get_clean();
|
|
1298
|
+
}
|
|
1011
1299
|
}
|
|
1012
1300
|
} else {
|
|
1013
1301
|
ob_start();
|
|
1014
1302
|
require_once APP_PATH . '/not-found.php';
|
|
1015
|
-
MainLayout::$
|
|
1303
|
+
MainLayout::$children = ob_get_clean();
|
|
1016
1304
|
|
|
1017
1305
|
http_response_code(404);
|
|
1018
1306
|
CacheHandler::$isCacheable = false;
|
|
1019
1307
|
}
|
|
1020
1308
|
|
|
1021
|
-
if (Bootstrap::$isParentLayout && !empty(Bootstrap::$contentToInclude)) {
|
|
1022
|
-
ob_start();
|
|
1023
|
-
require_once Bootstrap::$contentToInclude;
|
|
1024
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
1309
|
if (!Bootstrap::$isContentIncluded && !Bootstrap::$isChildContentIncluded) {
|
|
1028
1310
|
if (!Bootstrap::$secondRequestC69CD) {
|
|
1029
1311
|
Bootstrap::createUpdateRequestData();
|
|
@@ -1035,69 +1317,63 @@ try {
|
|
|
1035
1317
|
if (file_exists($file)) {
|
|
1036
1318
|
ob_start();
|
|
1037
1319
|
require_once $file;
|
|
1038
|
-
MainLayout::$
|
|
1320
|
+
MainLayout::$children .= ob_get_clean();
|
|
1039
1321
|
}
|
|
1040
1322
|
}
|
|
1041
1323
|
}
|
|
1042
1324
|
}
|
|
1043
1325
|
|
|
1044
1326
|
if (Request::$isWire && !Bootstrap::$secondRequestC69CD) {
|
|
1045
|
-
|
|
1327
|
+
while (ob_get_level() > 0) {
|
|
1328
|
+
ob_end_clean();
|
|
1329
|
+
}
|
|
1046
1330
|
Bootstrap::wireCallback();
|
|
1047
1331
|
}
|
|
1048
1332
|
|
|
1049
1333
|
if ((!Request::$isWire && !Bootstrap::$secondRequestC69CD) && isset(Bootstrap::$requestFilesData[Request::$decodedUri])) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1334
|
+
$cacheEnabled = (Env::string('CACHE_ENABLED', 'false') === 'true');
|
|
1335
|
+
|
|
1336
|
+
$shouldCache = CacheHandler::$isCacheable === true
|
|
1337
|
+
|| (CacheHandler::$isCacheable === null && $cacheEnabled);
|
|
1338
|
+
|
|
1339
|
+
if ($shouldCache) {
|
|
1340
|
+
CacheHandler::serveCache(Request::$decodedUri, intval(Env::string('CACHE_TTL', '600')));
|
|
1052
1341
|
}
|
|
1053
1342
|
}
|
|
1054
1343
|
|
|
1055
|
-
MainLayout::$children
|
|
1344
|
+
MainLayout::$children .= Bootstrap::getLoadingsFiles();
|
|
1056
1345
|
|
|
1057
1346
|
ob_start();
|
|
1058
|
-
|
|
1347
|
+
if (file_exists(Bootstrap::$parentLayoutPath)) {
|
|
1348
|
+
require_once Bootstrap::$parentLayoutPath;
|
|
1349
|
+
} else {
|
|
1350
|
+
echo MainLayout::$children;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1059
1353
|
MainLayout::$html = ob_get_clean();
|
|
1060
1354
|
MainLayout::$html = TemplateCompiler::compile(MainLayout::$html);
|
|
1061
1355
|
MainLayout::$html = TemplateCompiler::injectDynamicContent(MainLayout::$html);
|
|
1356
|
+
MainLayout::$html = Bootstrap::applyRootLayoutId(MainLayout::$html);
|
|
1357
|
+
|
|
1062
1358
|
MainLayout::$html = "<!DOCTYPE html>\n" . MainLayout::$html;
|
|
1063
1359
|
|
|
1064
1360
|
if (
|
|
1065
|
-
http_response_code() === 200
|
|
1361
|
+
http_response_code() === 200
|
|
1362
|
+
&& isset(Bootstrap::$requestFilesData[Request::$decodedUri]['fileName'])
|
|
1363
|
+
&& $shouldCache
|
|
1364
|
+
&& (!Request::$isWire && !Bootstrap::$secondRequestC69CD)
|
|
1066
1365
|
) {
|
|
1067
1366
|
CacheHandler::saveCache(Request::$decodedUri, MainLayout::$html);
|
|
1068
1367
|
}
|
|
1069
1368
|
|
|
1070
|
-
if (Bootstrap::$isPartialRequest) {
|
|
1071
|
-
$parts = PartialRenderer::extract(
|
|
1072
|
-
MainLayout::$html,
|
|
1073
|
-
Bootstrap::$partialSelectors
|
|
1074
|
-
);
|
|
1075
|
-
|
|
1076
|
-
if (count($parts) === 1) {
|
|
1077
|
-
echo reset($parts);
|
|
1078
|
-
} else {
|
|
1079
|
-
header('Content-Type: application/json');
|
|
1080
|
-
echo json_encode(
|
|
1081
|
-
['success' => true, 'fragments' => $parts],
|
|
1082
|
-
JSON_UNESCAPED_UNICODE
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1085
|
-
exit;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
1369
|
echo MainLayout::$html;
|
|
1089
1370
|
} else {
|
|
1090
1371
|
$layoutPath = Bootstrap::$isContentIncluded
|
|
1091
1372
|
? Bootstrap::$parentLayoutPath
|
|
1092
1373
|
: (Bootstrap::$layoutsToInclude[0] ?? '');
|
|
1093
1374
|
|
|
1094
|
-
$message = "The layout file does not contain <?php echo MainLayout::\$
|
|
1095
|
-
$htmlMessage = "<div class='error'>The layout file does not contain <?php echo MainLayout::\$
|
|
1096
|
-
|
|
1097
|
-
if (Bootstrap::$isContentIncluded) {
|
|
1098
|
-
$message = "The parent layout file does not contain <?php echo MainLayout::\$children; ?> Or <?= MainLayout::\$children ?><br><strong>$layoutPath</strong>";
|
|
1099
|
-
$htmlMessage = "<div class='error'>The parent layout file does not contain <?php echo MainLayout::\$children; ?> Or <?= MainLayout::\$children ?><br><strong>$layoutPath</strong></div>";
|
|
1100
|
-
}
|
|
1375
|
+
$message = "The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?>\n<strong>$layoutPath</strong>";
|
|
1376
|
+
$htmlMessage = "<div class='error'>The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?><br><strong>$layoutPath</strong></div>";
|
|
1101
1377
|
|
|
1102
1378
|
$errorDetails = Bootstrap::isAjaxOrXFileRequestOrRouteFile() ? $message : $htmlMessage;
|
|
1103
1379
|
|