create-prisma-php-app 4.4.1-beta → 4.4.1
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 +573 -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,134 @@ 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
|
+
$attribute = self::getExposedAttribute($fn);
|
|
978
|
+
if (!$attribute) {
|
|
979
|
+
return ['success' => false, 'error' => 'Function not callable from client'];
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
$attribute = self::getExposedAttribute($fn);
|
|
983
|
+
if (!self::validateAccess($attribute)) {
|
|
984
|
+
return ['success' => false, 'error' => 'Permission denied'];
|
|
985
|
+
}
|
|
986
|
+
|
|
733
987
|
if (function_exists($fn) && is_callable($fn)) {
|
|
734
988
|
try {
|
|
989
|
+
self::enforceRateLimit($attribute, "fn:$fn");
|
|
990
|
+
|
|
735
991
|
$res = call_user_func($fn, $args);
|
|
736
|
-
|
|
737
|
-
|
|
992
|
+
|
|
993
|
+
if ($res instanceof Generator || $res instanceof SSE) {
|
|
994
|
+
return $res;
|
|
738
995
|
}
|
|
996
|
+
|
|
739
997
|
return $res;
|
|
740
998
|
} catch (Throwable $e) {
|
|
741
|
-
if (
|
|
999
|
+
if ($e->getMessage() === 'Rate limit exceeded. Try again later.') {
|
|
1000
|
+
return ['success' => false, 'error' => $e->getMessage()];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (Env::string('SHOW_ERRORS', 'false') === 'false') {
|
|
742
1004
|
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
743
1005
|
} else {
|
|
744
1006
|
return ['success' => false, 'error' => "Function error: {$e->getMessage()}"];
|
|
@@ -750,6 +1012,17 @@ final class Bootstrap extends RuntimeException
|
|
|
750
1012
|
|
|
751
1013
|
private static function dispatchMethod(string $call, mixed $args)
|
|
752
1014
|
{
|
|
1015
|
+
if (!self::isMethodAllowed(
|
|
1016
|
+
strpos($call, '->') !== false
|
|
1017
|
+
? explode('->', $call, 2)[0]
|
|
1018
|
+
: explode('::', $call, 2)[0],
|
|
1019
|
+
strpos($call, '->') !== false
|
|
1020
|
+
? explode('->', $call, 2)[1]
|
|
1021
|
+
: explode('::', $call, 2)[1]
|
|
1022
|
+
)) {
|
|
1023
|
+
return ['success' => false, 'error' => 'Method not callable from client'];
|
|
1024
|
+
}
|
|
1025
|
+
|
|
753
1026
|
if (strpos($call, '->') !== false) {
|
|
754
1027
|
list($requested, $method) = explode('->', $call, 2);
|
|
755
1028
|
$isStatic = false;
|
|
@@ -766,47 +1039,48 @@ final class Bootstrap extends RuntimeException
|
|
|
766
1039
|
}
|
|
767
1040
|
}
|
|
768
1041
|
|
|
769
|
-
if (
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1042
|
+
if (!class_exists($class)) {
|
|
1043
|
+
return ['success' => false, 'error' => "Class '$requested' not found"];
|
|
1044
|
+
}
|
|
1045
|
+
$attribute = self::getExposedAttribute($class, $method);
|
|
1046
|
+
|
|
1047
|
+
if (!$attribute) {
|
|
1048
|
+
return ['success' => false, 'error' => 'Method not callable from client'];
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (!self::validateAccess($attribute)) {
|
|
1052
|
+
return ['success' => false, 'error' => 'Permission denied'];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
try {
|
|
1056
|
+
self::enforceRateLimit($attribute, "method:$class::$method");
|
|
1057
|
+
|
|
1058
|
+
$res = null;
|
|
1059
|
+
if (!$isStatic) {
|
|
1060
|
+
$instance = new $class();
|
|
1061
|
+
if (!is_callable([$instance, $method])) throw new Exception("Method not callable");
|
|
778
1062
|
$res = call_user_func([$instance, $method], $args);
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1063
|
+
} else {
|
|
1064
|
+
if (!is_callable([$class, $method])) throw new Exception("Static method invalid");
|
|
1065
|
+
$res = call_user_func([$class, $method], $args);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if ($res instanceof Generator || $res instanceof SSE) {
|
|
782
1069
|
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
1070
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1071
|
+
|
|
1072
|
+
return $res;
|
|
1073
|
+
} catch (Throwable $e) {
|
|
1074
|
+
if ($e->getMessage() === 'Rate limit exceeded. Try again later.') {
|
|
1075
|
+
return ['success' => false, 'error' => $e->getMessage()];
|
|
793
1076
|
}
|
|
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
|
-
}
|
|
1077
|
+
|
|
1078
|
+
if (Env::string('SHOW_ERRORS', 'false') === 'false') {
|
|
1079
|
+
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
1080
|
+
} else {
|
|
1081
|
+
return ['success' => false, 'error' => "Call error: {$e->getMessage()}"];
|
|
806
1082
|
}
|
|
807
1083
|
}
|
|
808
|
-
|
|
809
|
-
return ['success' => false, 'error' => 'Invalid callback'];
|
|
810
1084
|
}
|
|
811
1085
|
|
|
812
1086
|
private static function resolveClassImport(string $simpleClassKey): ?array
|
|
@@ -949,6 +1223,22 @@ final class Bootstrap extends RuntimeException
|
|
|
949
1223
|
|
|
950
1224
|
return Request::$isAjax || Request::$isXFileRequest || Request::$fileToInclude === 'route.php';
|
|
951
1225
|
}
|
|
1226
|
+
|
|
1227
|
+
public static function applyRootLayoutId(string $html): string
|
|
1228
|
+
{
|
|
1229
|
+
$rootLayoutPath = self::$layoutsToInclude[0] ?? self::$parentLayoutPath;
|
|
1230
|
+
$rootLayoutId = !empty($rootLayoutPath) ? md5($rootLayoutPath) : 'default-root';
|
|
1231
|
+
|
|
1232
|
+
header('X-PP-Root-Layout: ' . $rootLayoutId);
|
|
1233
|
+
|
|
1234
|
+
$rootLayoutMeta = '<meta name="pp-root-layout" content="' . $rootLayoutId . '">';
|
|
1235
|
+
|
|
1236
|
+
if (strpos($html, '<head>') !== false) {
|
|
1237
|
+
return preg_replace('/<head>/', "<head>\n $rootLayoutMeta", $html, 1);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
return $rootLayoutMeta . $html;
|
|
1241
|
+
}
|
|
952
1242
|
}
|
|
953
1243
|
|
|
954
1244
|
Bootstrap::run();
|
|
@@ -989,40 +1279,32 @@ try {
|
|
|
989
1279
|
}
|
|
990
1280
|
|
|
991
1281
|
if (!empty(Bootstrap::$contentToInclude) && !empty(Request::$fileToInclude)) {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
996
|
-
}
|
|
1282
|
+
ob_start();
|
|
1283
|
+
require_once Bootstrap::$contentToInclude;
|
|
1284
|
+
MainLayout::$children = ob_get_clean();
|
|
997
1285
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1286
|
+
if (count(Bootstrap::$layoutsToInclude) > 1) {
|
|
1287
|
+
$nestedLayouts = array_slice(Bootstrap::$layoutsToInclude, 1);
|
|
1002
1288
|
|
|
1003
|
-
|
|
1004
|
-
Bootstrap
|
|
1005
|
-
|
|
1289
|
+
foreach (array_reverse($nestedLayouts) as $layoutPath) {
|
|
1290
|
+
if (!Bootstrap::containsChildren($layoutPath)) {
|
|
1291
|
+
Bootstrap::$isChildContentIncluded = true;
|
|
1292
|
+
}
|
|
1006
1293
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1294
|
+
ob_start();
|
|
1295
|
+
require_once $layoutPath;
|
|
1296
|
+
MainLayout::$children = ob_get_clean();
|
|
1297
|
+
}
|
|
1010
1298
|
}
|
|
1011
1299
|
} else {
|
|
1012
1300
|
ob_start();
|
|
1013
1301
|
require_once APP_PATH . '/not-found.php';
|
|
1014
|
-
MainLayout::$
|
|
1302
|
+
MainLayout::$children = ob_get_clean();
|
|
1015
1303
|
|
|
1016
1304
|
http_response_code(404);
|
|
1017
1305
|
CacheHandler::$isCacheable = false;
|
|
1018
1306
|
}
|
|
1019
1307
|
|
|
1020
|
-
if (Bootstrap::$isParentLayout && !empty(Bootstrap::$contentToInclude)) {
|
|
1021
|
-
ob_start();
|
|
1022
|
-
require_once Bootstrap::$contentToInclude;
|
|
1023
|
-
MainLayout::$childLayoutChildren = ob_get_clean();
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
1308
|
if (!Bootstrap::$isContentIncluded && !Bootstrap::$isChildContentIncluded) {
|
|
1027
1309
|
if (!Bootstrap::$secondRequestC69CD) {
|
|
1028
1310
|
Bootstrap::createUpdateRequestData();
|
|
@@ -1034,69 +1316,63 @@ try {
|
|
|
1034
1316
|
if (file_exists($file)) {
|
|
1035
1317
|
ob_start();
|
|
1036
1318
|
require_once $file;
|
|
1037
|
-
MainLayout::$
|
|
1319
|
+
MainLayout::$children .= ob_get_clean();
|
|
1038
1320
|
}
|
|
1039
1321
|
}
|
|
1040
1322
|
}
|
|
1041
1323
|
}
|
|
1042
1324
|
|
|
1043
1325
|
if (Request::$isWire && !Bootstrap::$secondRequestC69CD) {
|
|
1044
|
-
|
|
1326
|
+
while (ob_get_level() > 0) {
|
|
1327
|
+
ob_end_clean();
|
|
1328
|
+
}
|
|
1045
1329
|
Bootstrap::wireCallback();
|
|
1046
1330
|
}
|
|
1047
1331
|
|
|
1048
1332
|
if ((!Request::$isWire && !Bootstrap::$secondRequestC69CD) && isset(Bootstrap::$requestFilesData[Request::$decodedUri])) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1333
|
+
$cacheEnabled = (Env::string('CACHE_ENABLED', 'false') === 'true');
|
|
1334
|
+
|
|
1335
|
+
$shouldCache = CacheHandler::$isCacheable === true
|
|
1336
|
+
|| (CacheHandler::$isCacheable === null && $cacheEnabled);
|
|
1337
|
+
|
|
1338
|
+
if ($shouldCache) {
|
|
1339
|
+
CacheHandler::serveCache(Request::$decodedUri, intval(Env::string('CACHE_TTL', '600')));
|
|
1051
1340
|
}
|
|
1052
1341
|
}
|
|
1053
1342
|
|
|
1054
|
-
MainLayout::$children
|
|
1343
|
+
MainLayout::$children .= Bootstrap::getLoadingsFiles();
|
|
1055
1344
|
|
|
1056
1345
|
ob_start();
|
|
1057
|
-
|
|
1346
|
+
if (file_exists(Bootstrap::$parentLayoutPath)) {
|
|
1347
|
+
require_once Bootstrap::$parentLayoutPath;
|
|
1348
|
+
} else {
|
|
1349
|
+
echo MainLayout::$children;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1058
1352
|
MainLayout::$html = ob_get_clean();
|
|
1059
1353
|
MainLayout::$html = TemplateCompiler::compile(MainLayout::$html);
|
|
1060
1354
|
MainLayout::$html = TemplateCompiler::injectDynamicContent(MainLayout::$html);
|
|
1355
|
+
MainLayout::$html = Bootstrap::applyRootLayoutId(MainLayout::$html);
|
|
1356
|
+
|
|
1061
1357
|
MainLayout::$html = "<!DOCTYPE html>\n" . MainLayout::$html;
|
|
1062
1358
|
|
|
1063
1359
|
if (
|
|
1064
|
-
http_response_code() === 200
|
|
1360
|
+
http_response_code() === 200
|
|
1361
|
+
&& isset(Bootstrap::$requestFilesData[Request::$decodedUri]['fileName'])
|
|
1362
|
+
&& $shouldCache
|
|
1363
|
+
&& (!Request::$isWire && !Bootstrap::$secondRequestC69CD)
|
|
1065
1364
|
) {
|
|
1066
1365
|
CacheHandler::saveCache(Request::$decodedUri, MainLayout::$html);
|
|
1067
1366
|
}
|
|
1068
1367
|
|
|
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
1368
|
echo MainLayout::$html;
|
|
1088
1369
|
} else {
|
|
1089
1370
|
$layoutPath = Bootstrap::$isContentIncluded
|
|
1090
1371
|
? Bootstrap::$parentLayoutPath
|
|
1091
1372
|
: (Bootstrap::$layoutsToInclude[0] ?? '');
|
|
1092
1373
|
|
|
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
|
-
}
|
|
1374
|
+
$message = "The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?>\n<strong>$layoutPath</strong>";
|
|
1375
|
+
$htmlMessage = "<div class='error'>The layout file does not contain <?php echo MainLayout::\$children; ?> or <?= MainLayout::\$children ?><br><strong>$layoutPath</strong></div>";
|
|
1100
1376
|
|
|
1101
1377
|
$errorDetails = Bootstrap::isAjaxOrXFileRequestOrRouteFile() ? $message : $htmlMessage;
|
|
1102
1378
|
|