create-prisma-php-app 5.1.0-alpha.3 → 5.1.0-alpha.30
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/README.md +23 -2
- package/dist/.github/copilot-instructions.md +80 -33
- package/dist/AGENTS.md +59 -25
- package/dist/bootstrap.php +207 -187
- package/dist/index.js +2 -2
- package/dist/phpunit.xml +25 -0
- package/dist/postcss.config.js +4 -2
- package/dist/public/.htaccess +1 -1
- package/dist/public/js/pp-reactive-v2.min.js +1 -0
- package/dist/settings/bs-config.ts +44 -1
- package/dist/settings/run-postcss.ts +205 -0
- package/dist/settings/run-tests.ts +35 -0
- package/dist/src/Lib/Auth/Auth.php +12 -25
- package/dist/src/Lib/MCP/mcp-server.php +2 -3
- package/dist/src/Lib/Websocket/ConnectionManager.php +500 -47
- package/dist/src/Lib/Websocket/Socket.php +170 -0
- package/dist/src/Lib/Websocket/SocketPool.php +50 -0
- package/dist/src/Lib/Websocket/SocketRegistry.php +88 -0
- package/dist/src/Lib/Websocket/sockets.php +50 -0
- package/dist/src/Lib/Websocket/websocket-server.php +10 -3
- package/dist/src/app/globals.css +3 -1
- package/dist/src/app/layout.php +1 -1
- package/dist/tests/AuthTest.php +59 -0
- package/dist/tests/ConnectionManagerTest.php +277 -0
- package/dist/tests/CsrfTest.php +119 -0
- package/dist/tests/DeferComponentRootsTest.php +147 -0
- package/dist/tests/FeaturesTest.php +41 -0
- package/dist/tests/README.md +119 -0
- package/dist/tests/RpcWireContractTest.php +101 -0
- package/dist/tests/SocketPoolTest.php +69 -0
- package/dist/tests/SocketRegistryTest.php +77 -0
- package/dist/tests/SocketTest.php +124 -0
- package/dist/tests/SocketsRegistrationTest.php +40 -0
- package/dist/tests/Support/FakeConnection.php +62 -0
- package/dist/tests/Support/Features.php +38 -0
- package/dist/tests/Support/RequiresFeature.php +26 -0
- package/dist/tests/bootstrap.php +44 -0
- package/dist/ts/main.ts +5 -8
- package/dist/ts/tailwind-merge.ts +13 -0
- package/package.json +4 -4
- package/dist/public/js/pp-reactive-v2.js +0 -1
package/dist/bootstrap.php
CHANGED
|
@@ -31,8 +31,21 @@ use PP\Attributes\Exposed;
|
|
|
31
31
|
use PP\Attributes\ExposedRegistry;
|
|
32
32
|
use PP\Streaming\SSE;
|
|
33
33
|
use PP\Security\RateLimiter;
|
|
34
|
+
use PP\Security\Csrf;
|
|
34
35
|
use PP\Env;
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* An RPC failure meant for the wire: message plus HTTP status, serialized as
|
|
39
|
+
* `{"error": "..."}` — the shape the PulsePoint runtime reads.
|
|
40
|
+
*/
|
|
41
|
+
final class RpcError extends RuntimeException
|
|
42
|
+
{
|
|
43
|
+
public function __construct(string $message, public readonly int $status)
|
|
44
|
+
{
|
|
45
|
+
parent::__construct($message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
final class Bootstrap extends RuntimeException
|
|
37
50
|
{
|
|
38
51
|
public static string $contentToInclude = '';
|
|
@@ -43,7 +56,6 @@ final class Bootstrap extends RuntimeException
|
|
|
43
56
|
public static bool $isContentIncluded = false;
|
|
44
57
|
public static bool $isChildContentIncluded = false;
|
|
45
58
|
public static bool $isContentVariableIncluded = false;
|
|
46
|
-
public static bool $secondRequestC69CD = false;
|
|
47
59
|
public static array $requestFilesData = [];
|
|
48
60
|
|
|
49
61
|
private string $context;
|
|
@@ -94,9 +106,7 @@ final class Bootstrap extends RuntimeException
|
|
|
94
106
|
MainLayout::init();
|
|
95
107
|
ErrorHandler::registerHandlers();
|
|
96
108
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
self::$secondRequestC69CD = Request::$data['secondRequestC69CD'] ?? false;
|
|
109
|
+
Csrf::ensureCookie();
|
|
100
110
|
|
|
101
111
|
$contentInfo = self::determineContentToInclude();
|
|
102
112
|
self::$contentToInclude = $contentInfo['path'] ?? '';
|
|
@@ -137,73 +147,94 @@ final class Bootstrap extends RuntimeException
|
|
|
137
147
|
ErrorHandler::checkFatalError();
|
|
138
148
|
}
|
|
139
149
|
|
|
140
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Anti-CSRF origin check for RPC calls. NOT authentication: a browser
|
|
152
|
+
* cannot forge the Origin header, so this blocks cross-site script-driven
|
|
153
|
+
* calls; raw clients are handled by the CSRF token and auth gates.
|
|
154
|
+
*
|
|
155
|
+
* @return string|null An error message, or null when the origin is fine.
|
|
156
|
+
*/
|
|
157
|
+
private static function validateRpcOrigin(): ?string
|
|
141
158
|
{
|
|
142
|
-
$
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
throw new RuntimeException('FUNCTION_CALL_SECRET is required for CSRF protection.');
|
|
159
|
+
$origin = rtrim(trim($_SERVER['HTTP_ORIGIN'] ?? ''), '/');
|
|
160
|
+
if ($origin === '') {
|
|
161
|
+
return null;
|
|
146
162
|
}
|
|
147
|
-
$shouldRegenerate = true;
|
|
148
|
-
|
|
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
163
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
164
|
+
if (Env::string('APP_ENV', 'production') !== 'production') {
|
|
165
|
+
if (preg_match('#^http://(localhost|127\.0\.0\.1)(:\d+)?$#i', $origin)) {
|
|
166
|
+
return null;
|
|
158
167
|
}
|
|
159
168
|
}
|
|
160
169
|
|
|
161
|
-
|
|
162
|
-
$nonce = bin2hex(random_bytes(16));
|
|
163
|
-
$signature = hash_hmac('sha256', $nonce, $secret);
|
|
164
|
-
$token = $nonce . '.' . $signature;
|
|
170
|
+
$allowedOrigins = [rtrim(Request::$protocol . Request::$domainName, '/')];
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
'httponly' => false,
|
|
171
|
-
'samesite' => 'Lax',
|
|
172
|
-
]);
|
|
172
|
+
$baseUrl = rtrim(Env::string('APP_BASE_URL', ''), '/');
|
|
173
|
+
if ($baseUrl !== '') {
|
|
174
|
+
$allowedOrigins[] = $baseUrl;
|
|
175
|
+
}
|
|
173
176
|
|
|
174
|
-
|
|
177
|
+
foreach (self::parseOriginList(Env::string('CORS_ALLOWED_ORIGINS', '')) as $allowed) {
|
|
178
|
+
$allowedOrigins[] = $allowed;
|
|
175
179
|
}
|
|
180
|
+
|
|
181
|
+
return in_array($origin, $allowedOrigins, true) ? null : 'Invalid origin';
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Parse an origin list env value: CSV or a JSON array, the same formats
|
|
186
|
+
* CorsMiddleware accepts.
|
|
187
|
+
*
|
|
188
|
+
* @return string[]
|
|
189
|
+
*/
|
|
190
|
+
private static function parseOriginList(string $raw): array
|
|
179
191
|
{
|
|
180
|
-
$
|
|
181
|
-
$
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if ($secret === '') {
|
|
185
|
-
self::jsonExit(['success' => false, 'error' => 'CSRF secret is not configured']);
|
|
192
|
+
$raw = trim($raw);
|
|
193
|
+
if ($raw === '' || $raw === '[]') {
|
|
194
|
+
return [];
|
|
186
195
|
}
|
|
187
196
|
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
$values = null;
|
|
198
|
+
if ($raw[0] === '[') {
|
|
199
|
+
$decoded = json_decode($raw, true);
|
|
200
|
+
if (is_array($decoded)) {
|
|
201
|
+
$values = array_map('strval', $decoded);
|
|
202
|
+
}
|
|
190
203
|
}
|
|
191
204
|
|
|
192
|
-
|
|
193
|
-
self::jsonExit(['success' => false, 'error' => 'CSRF token mismatch']);
|
|
194
|
-
}
|
|
205
|
+
$values ??= explode(',', $raw);
|
|
195
206
|
|
|
196
|
-
$
|
|
197
|
-
|
|
198
|
-
|
|
207
|
+
$origins = [];
|
|
208
|
+
foreach ($values as $value) {
|
|
209
|
+
$value = rtrim(trim($value), '/');
|
|
210
|
+
if ($value !== '') {
|
|
211
|
+
$origins[] = $value;
|
|
212
|
+
}
|
|
199
213
|
}
|
|
200
214
|
|
|
201
|
-
|
|
202
|
-
|
|
215
|
+
return $origins;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* RPC bodies are JSON or multipart uploads; any other content type with a
|
|
220
|
+
* body is refused with 415, matching the runtime's error handling.
|
|
221
|
+
*/
|
|
222
|
+
private static function isRpcContentTypeAllowed(): bool
|
|
223
|
+
{
|
|
224
|
+
$contentType = strtolower(Request::$contentType);
|
|
225
|
+
if (
|
|
226
|
+
str_starts_with($contentType, 'application/json')
|
|
227
|
+
|| str_starts_with($contentType, 'multipart/form-data')
|
|
228
|
+
) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
203
231
|
|
|
204
|
-
|
|
205
|
-
|
|
232
|
+
$contentLength = $_SERVER['CONTENT_LENGTH'] ?? $_SERVER['HTTP_CONTENT_LENGTH'] ?? null;
|
|
233
|
+
if ($contentLength !== null) {
|
|
234
|
+
return (int) $contentLength <= 0;
|
|
206
235
|
}
|
|
236
|
+
|
|
237
|
+
return strtolower((string) ($_SERVER['HTTP_TRANSFER_ENCODING'] ?? '')) === '';
|
|
207
238
|
}
|
|
208
239
|
|
|
209
240
|
private static function fileExistsCached(string $path): bool
|
|
@@ -950,21 +981,35 @@ final class Bootstrap extends RuntimeException
|
|
|
950
981
|
$callbackName = $_SERVER['HTTP_X_PP_FUNCTION'] ?? null;
|
|
951
982
|
|
|
952
983
|
if (empty($callbackName)) {
|
|
953
|
-
self::jsonExit(['
|
|
984
|
+
self::jsonExit(['error' => 'Missing function name'], 400);
|
|
954
985
|
}
|
|
955
986
|
|
|
956
|
-
self::validateCsrfToken();
|
|
957
|
-
|
|
958
987
|
if (!preg_match('/^[a-zA-Z0-9_:\->]+$/', $callbackName)) {
|
|
959
|
-
self::jsonExit(['
|
|
988
|
+
self::jsonExit(['error' => 'Invalid function name'], 400);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (($error = self::validateRpcOrigin()) !== null) {
|
|
992
|
+
self::jsonExit(['error' => $error], 403);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (!self::isRpcContentTypeAllowed()) {
|
|
996
|
+
self::jsonExit(['error' => 'Invalid content type'], 415);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (($error = Csrf::validateHeaderToken($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '')) !== null) {
|
|
1000
|
+
self::jsonExit(['error' => $error], 403);
|
|
960
1001
|
}
|
|
961
1002
|
|
|
962
1003
|
$data = self::getRequestData();
|
|
963
1004
|
$args = self::convertToArrayObject($data);
|
|
964
1005
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1006
|
+
try {
|
|
1007
|
+
$out = str_contains($callbackName, '->') || str_contains($callbackName, '::')
|
|
1008
|
+
? self::dispatchMethod($callbackName, $args)
|
|
1009
|
+
: self::dispatchFunction($callbackName, $args);
|
|
1010
|
+
} catch (RpcError $e) {
|
|
1011
|
+
self::jsonExit(['error' => $e->getMessage()], $e->status);
|
|
1012
|
+
}
|
|
968
1013
|
|
|
969
1014
|
if ($out instanceof SSE) {
|
|
970
1015
|
$out->send();
|
|
@@ -976,14 +1021,18 @@ final class Bootstrap extends RuntimeException
|
|
|
976
1021
|
exit;
|
|
977
1022
|
}
|
|
978
1023
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
exit;
|
|
1024
|
+
// Always emit valid JSON — the runtime parses every non-stream RPC
|
|
1025
|
+
// response, so a null result must arrive as the JSON literal `null`.
|
|
1026
|
+
self::jsonExit($out);
|
|
983
1027
|
}
|
|
984
1028
|
|
|
985
|
-
private static function jsonExit(mixed $payload): void
|
|
1029
|
+
private static function jsonExit(mixed $payload, int $status = 200): void
|
|
986
1030
|
{
|
|
1031
|
+
if (!headers_sent()) {
|
|
1032
|
+
http_response_code($status);
|
|
1033
|
+
header('Content-Type: application/json; charset=UTF-8');
|
|
1034
|
+
}
|
|
1035
|
+
|
|
987
1036
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
|
988
1037
|
exit;
|
|
989
1038
|
}
|
|
@@ -991,7 +1040,18 @@ final class Bootstrap extends RuntimeException
|
|
|
991
1040
|
private static function getRequestData(): array
|
|
992
1041
|
{
|
|
993
1042
|
if (!empty($_FILES)) {
|
|
994
|
-
|
|
1043
|
+
// Non-file values in an RPC multipart body are JSON-encoded by
|
|
1044
|
+
// the runtime (objects/arrays) or stringified scalars; decode
|
|
1045
|
+
// what parses so `{count: 2}` arrives as a number, not "2".
|
|
1046
|
+
$data = [];
|
|
1047
|
+
foreach ($_POST as $key => $value) {
|
|
1048
|
+
if (is_string($value)) {
|
|
1049
|
+
$decoded = json_decode($value, true);
|
|
1050
|
+
$data[$key] = json_last_error() === JSON_ERROR_NONE ? $decoded : $value;
|
|
1051
|
+
} else {
|
|
1052
|
+
$data[$key] = $value;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
995
1055
|
foreach ($_FILES as $key => $file) {
|
|
996
1056
|
$data[$key] = is_array($file['name'])
|
|
997
1057
|
? array_map(
|
|
@@ -1015,69 +1075,41 @@ final class Bootstrap extends RuntimeException
|
|
|
1015
1075
|
return (json_last_error() === JSON_ERROR_NONE) ? $json : $_POST;
|
|
1016
1076
|
}
|
|
1017
1077
|
|
|
1018
|
-
|
|
1078
|
+
/**
|
|
1079
|
+
* Authorize the caller for one exposed function, mirroring the reference
|
|
1080
|
+
* server: 401 when auth is required and missing, 403 on a role mismatch.
|
|
1081
|
+
*/
|
|
1082
|
+
private static function authorizeAccess(Exposed $attribute): void
|
|
1019
1083
|
{
|
|
1020
|
-
if (
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
if (!$auth->isAuthenticated()) {
|
|
1024
|
-
return false;
|
|
1025
|
-
}
|
|
1084
|
+
if (!$attribute->requiresAuth && empty($attribute->allowedRoles)) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1026
1087
|
|
|
1027
|
-
|
|
1028
|
-
$payload = $auth->getPayload();
|
|
1029
|
-
$currentRole = null;
|
|
1088
|
+
$auth = Auth::getInstance();
|
|
1030
1089
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
} else {
|
|
1034
|
-
$roleKey = !empty(Auth::ROLE_NAME) ? Auth::ROLE_NAME : 'role';
|
|
1035
|
-
|
|
1036
|
-
if (is_object($payload)) {
|
|
1037
|
-
$currentRole = $payload->$roleKey ?? null;
|
|
1038
|
-
} elseif (is_array($payload)) {
|
|
1039
|
-
$currentRole = $payload[$roleKey] ?? null;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
if ($currentRole === null || !in_array($currentRole, $attribute->allowedRoles)) {
|
|
1044
|
-
return false;
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1090
|
+
if (!$auth->isAuthenticated()) {
|
|
1091
|
+
throw new RpcError('Authentication required', 401);
|
|
1047
1092
|
}
|
|
1048
1093
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1094
|
+
if (!empty($attribute->allowedRoles)) {
|
|
1095
|
+
$payload = $auth->getPayload();
|
|
1096
|
+
$currentRole = null;
|
|
1051
1097
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
$attrs = $ref->getAttributes(Exposed::class);
|
|
1098
|
+
if (is_scalar($payload)) {
|
|
1099
|
+
$currentRole = $payload;
|
|
1100
|
+
} else {
|
|
1101
|
+
$roleKey = !empty(Auth::ROLE_NAME) ? Auth::ROLE_NAME : 'role';
|
|
1057
1102
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1103
|
+
if (is_object($payload)) {
|
|
1104
|
+
$currentRole = $payload->$roleKey ?? null;
|
|
1105
|
+
} elseif (is_array($payload)) {
|
|
1106
|
+
$currentRole = $payload[$roleKey] ?? null;
|
|
1107
|
+
}
|
|
1060
1108
|
}
|
|
1061
1109
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
return false;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
private static function isMethodAllowed(string $class, string $method): bool
|
|
1069
|
-
{
|
|
1070
|
-
try {
|
|
1071
|
-
$ref = new ReflectionMethod($class, $method);
|
|
1072
|
-
$attrs = $ref->getAttributes(Exposed::class);
|
|
1073
|
-
|
|
1074
|
-
if (empty($attrs)) {
|
|
1075
|
-
return false;
|
|
1110
|
+
if ($currentRole === null || !in_array($currentRole, $attribute->allowedRoles)) {
|
|
1111
|
+
throw new RpcError('Permission denied', 403);
|
|
1076
1112
|
}
|
|
1077
|
-
|
|
1078
|
-
return self::validateAccess($attrs[0]->newInstance());
|
|
1079
|
-
} catch (Throwable) {
|
|
1080
|
-
return false;
|
|
1081
1113
|
}
|
|
1082
1114
|
}
|
|
1083
1115
|
|
|
@@ -1110,7 +1142,33 @@ final class Bootstrap extends RuntimeException
|
|
|
1110
1142
|
}
|
|
1111
1143
|
|
|
1112
1144
|
if ($limits) {
|
|
1113
|
-
|
|
1145
|
+
try {
|
|
1146
|
+
RateLimiter::verify($identifier, $limits);
|
|
1147
|
+
} catch (Throwable $e) {
|
|
1148
|
+
throw new RpcError('Rate limit exceeded. Try again later.', 429);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Run one exposed callable and translate its failures onto the wire:
|
|
1155
|
+
* `InvalidArgumentException` is a message meant for the caller (400, the
|
|
1156
|
+
* validation convention), anything else is a 500.
|
|
1157
|
+
*/
|
|
1158
|
+
private static function invokeExposed(callable $callable, mixed $args): mixed
|
|
1159
|
+
{
|
|
1160
|
+
try {
|
|
1161
|
+
return call_user_func($callable, $args);
|
|
1162
|
+
} catch (RpcError $e) {
|
|
1163
|
+
throw $e;
|
|
1164
|
+
} catch (InvalidArgumentException $e) {
|
|
1165
|
+
throw new RpcError($e->getMessage(), 400);
|
|
1166
|
+
} catch (Throwable $e) {
|
|
1167
|
+
if (Env::string('SHOW_ERRORS', 'false') === 'false') {
|
|
1168
|
+
throw new RpcError('Internal server error', 500);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
throw new RpcError("Function error: {$e->getMessage()}", 500);
|
|
1114
1172
|
}
|
|
1115
1173
|
}
|
|
1116
1174
|
|
|
@@ -1123,39 +1181,21 @@ final class Bootstrap extends RuntimeException
|
|
|
1123
1181
|
}
|
|
1124
1182
|
}
|
|
1125
1183
|
|
|
1126
|
-
if (!
|
|
1127
|
-
|
|
1184
|
+
if (!function_exists($fn) || !is_callable($fn)) {
|
|
1185
|
+
throw new RpcError('Function not found', 404);
|
|
1128
1186
|
}
|
|
1129
1187
|
|
|
1130
1188
|
$attribute = self::getExposedAttribute($fn);
|
|
1131
|
-
if (
|
|
1132
|
-
|
|
1189
|
+
if ($attribute === null) {
|
|
1190
|
+
// Existing but not #[Exposed]: indistinguishable from absent, on
|
|
1191
|
+
// purpose — the wire must not reveal which functions exist.
|
|
1192
|
+
throw new RpcError('Function not found', 404);
|
|
1133
1193
|
}
|
|
1134
1194
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
self::enforceRateLimit($attribute, "fn:$fn");
|
|
1138
|
-
|
|
1139
|
-
$res = call_user_func($fn, $args);
|
|
1140
|
-
|
|
1141
|
-
if ($res instanceof Generator || $res instanceof SSE) {
|
|
1142
|
-
return $res;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
return $res;
|
|
1146
|
-
} catch (Throwable $e) {
|
|
1147
|
-
if ($e->getMessage() === 'Rate limit exceeded. Try again later.') {
|
|
1148
|
-
return ['success' => false, 'error' => $e->getMessage()];
|
|
1149
|
-
}
|
|
1195
|
+
self::authorizeAccess($attribute);
|
|
1196
|
+
self::enforceRateLimit($attribute, "fn:$fn");
|
|
1150
1197
|
|
|
1151
|
-
|
|
1152
|
-
return ['success' => false, 'error' => 'An error occurred. Please try again later.'];
|
|
1153
|
-
} else {
|
|
1154
|
-
return ['success' => false, 'error' => "Function error: {$e->getMessage()}"];
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
return ['success' => false, 'error' => 'Invalid callback'];
|
|
1198
|
+
return self::invokeExposed($fn, $args);
|
|
1159
1199
|
}
|
|
1160
1200
|
|
|
1161
1201
|
private static function dispatchMethod(string $call, mixed $args)
|
|
@@ -1178,51 +1218,31 @@ final class Bootstrap extends RuntimeException
|
|
|
1178
1218
|
}
|
|
1179
1219
|
|
|
1180
1220
|
if (!class_exists($class)) {
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
if (!self::isMethodAllowed($class, $method)) {
|
|
1185
|
-
return ['success' => false, 'error' => 'Method not callable from client'];
|
|
1221
|
+
throw new RpcError('Function not found', 404);
|
|
1186
1222
|
}
|
|
1187
1223
|
|
|
1188
1224
|
$attribute = self::getExposedAttribute($class, $method);
|
|
1189
|
-
if (
|
|
1190
|
-
|
|
1225
|
+
if ($attribute === null) {
|
|
1226
|
+
throw new RpcError('Function not found', 404);
|
|
1191
1227
|
}
|
|
1192
1228
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
}
|
|
1229
|
+
self::authorizeAccess($attribute);
|
|
1230
|
+
self::enforceRateLimit($attribute, "method:$class::$method");
|
|
1196
1231
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
$res = null;
|
|
1201
|
-
if (!$isStatic) {
|
|
1202
|
-
$instance = new $class();
|
|
1203
|
-
if (!is_callable([$instance, $method])) throw new Exception("Method not callable");
|
|
1204
|
-
$res = call_user_func([$instance, $method], $args);
|
|
1205
|
-
} else {
|
|
1206
|
-
if (!is_callable([$class, $method])) throw new Exception("Static method invalid");
|
|
1207
|
-
$res = call_user_func([$class, $method], $args);
|
|
1232
|
+
if ($isStatic) {
|
|
1233
|
+
if (!is_callable([$class, $method])) {
|
|
1234
|
+
throw new RpcError('Function not found', 404);
|
|
1208
1235
|
}
|
|
1209
1236
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
return $res;
|
|
1215
|
-
} catch (Throwable $e) {
|
|
1216
|
-
if ($e->getMessage() === 'Rate limit exceeded. Try again later.') {
|
|
1217
|
-
return ['success' => false, 'error' => $e->getMessage()];
|
|
1218
|
-
}
|
|
1237
|
+
return self::invokeExposed([$class, $method], $args);
|
|
1238
|
+
}
|
|
1219
1239
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
return ['success' => false, 'error' => "Call error: {$e->getMessage()}"];
|
|
1224
|
-
}
|
|
1240
|
+
$instance = new $class();
|
|
1241
|
+
if (!is_callable([$instance, $method])) {
|
|
1242
|
+
throw new RpcError('Function not found', 404);
|
|
1225
1243
|
}
|
|
1244
|
+
|
|
1245
|
+
return self::invokeExposed([$instance, $method], $args);
|
|
1226
1246
|
}
|
|
1227
1247
|
|
|
1228
1248
|
private static function resolveClassImport(string $simpleClassKey): ?array
|
|
@@ -1333,7 +1353,7 @@ final class Bootstrap extends RuntimeException
|
|
|
1333
1353
|
array_merge($currentData[$currentUrl]['includedFiles'], $srcAppFiles)
|
|
1334
1354
|
));
|
|
1335
1355
|
|
|
1336
|
-
if (!Request::$
|
|
1356
|
+
if (!Request::$isRpc) {
|
|
1337
1357
|
$currentData[$currentUrl]['isCacheable'] = CacheHandler::$isCacheable;
|
|
1338
1358
|
}
|
|
1339
1359
|
} else {
|
|
@@ -1500,7 +1520,7 @@ try {
|
|
|
1500
1520
|
}
|
|
1501
1521
|
|
|
1502
1522
|
if (!Bootstrap::$isContentIncluded && !Bootstrap::$isChildContentIncluded) {
|
|
1503
|
-
if (Request::$
|
|
1523
|
+
if (Request::$isRpc) {
|
|
1504
1524
|
if (isset(Bootstrap::$requestFilesData[Request::$decodedUri])) {
|
|
1505
1525
|
foreach (Bootstrap::$requestFilesData[Request::$decodedUri]['includedFiles'] as $file) {
|
|
1506
1526
|
if (file_exists($file)) {
|
|
@@ -1512,14 +1532,14 @@ try {
|
|
|
1512
1532
|
}
|
|
1513
1533
|
}
|
|
1514
1534
|
|
|
1515
|
-
if (Request::$
|
|
1535
|
+
if (Request::$isRpc) {
|
|
1516
1536
|
while (ob_get_level() > 0) {
|
|
1517
1537
|
ob_end_clean();
|
|
1518
1538
|
}
|
|
1519
1539
|
Bootstrap::wireCallback();
|
|
1520
1540
|
}
|
|
1521
1541
|
|
|
1522
|
-
if (
|
|
1542
|
+
if (!Request::$isRpc && isset(Bootstrap::$requestFilesData[Request::$decodedUri])) {
|
|
1523
1543
|
$cacheEnabled = (Env::string('CACHE_ENABLED', 'false') === 'true');
|
|
1524
1544
|
|
|
1525
1545
|
$shouldCache = CacheHandler::$isCacheable === true
|
|
@@ -1552,22 +1572,22 @@ try {
|
|
|
1552
1572
|
MainLayout::$html = TemplateCompiler::compile(MainLayout::$html);
|
|
1553
1573
|
MainLayout::$html = TemplateCompiler::injectDynamicContent(MainLayout::$html);
|
|
1554
1574
|
MainLayout::$html = Bootstrap::applyRootLayoutId(MainLayout::$html);
|
|
1575
|
+
MainLayout::$html = TemplateCompiler::deferComponentRoots(MainLayout::$html);
|
|
1555
1576
|
|
|
1556
1577
|
MainLayout::$html = "<!DOCTYPE html>\n" . MainLayout::$html;
|
|
1557
1578
|
|
|
1558
|
-
|
|
1559
|
-
Bootstrap::createUpdateRequestData();
|
|
1560
|
-
}
|
|
1579
|
+
Bootstrap::createUpdateRequestData();
|
|
1561
1580
|
|
|
1562
1581
|
if (
|
|
1563
1582
|
http_response_code() === 200
|
|
1564
1583
|
&& isset(Bootstrap::$requestFilesData[Request::$decodedUri]['fileName'])
|
|
1565
1584
|
&& $shouldCache
|
|
1566
|
-
&&
|
|
1585
|
+
&& !Request::$isRpc
|
|
1567
1586
|
) {
|
|
1568
1587
|
CacheHandler::saveCache(Request::$decodedUri, MainLayout::$html);
|
|
1569
1588
|
}
|
|
1570
1589
|
|
|
1590
|
+
header('Content-Type: text/html; charset=UTF-8');
|
|
1571
1591
|
echo MainLayout::$html;
|
|
1572
1592
|
} else {
|
|
1573
1593
|
$layoutPath = Bootstrap::$isContentIncluded
|