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