@maxisoft/instantcms-mcp 1.2.3
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/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/__tests__/artifact-tool.test.js +196 -0
- package/dist/__tests__/db-tool.test.js +152 -0
- package/dist/__tests__/define-tool.test.js +106 -0
- package/dist/__tests__/find-tool.test.js +185 -0
- package/dist/__tests__/get-component-api.test.js +144 -0
- package/dist/__tests__/hardening.test.js +73 -0
- package/dist/__tests__/hooks-tool.test.js +173 -0
- package/dist/__tests__/mcp-integration-extra.test.js +166 -0
- package/dist/__tests__/mcp-integration.test.js +53 -0
- package/dist/__tests__/pagination.test.js +114 -0
- package/dist/__tests__/performance-baseline.test.js +56 -0
- package/dist/__tests__/project-patch.test.js +144 -0
- package/dist/__tests__/project-source.test.js +44 -0
- package/dist/__tests__/project-workflows-extra.test.js +165 -0
- package/dist/__tests__/project-workflows.test.js +51 -0
- package/dist/__tests__/scaffold-addon-roundtrip.test.js +287 -0
- package/dist/__tests__/serialization.test.js +170 -0
- package/dist/__tests__/source-knowledge-parsers.test.js +42 -0
- package/dist/__tests__/template-development.test.js +78 -0
- package/dist/__tests__/template-productivity.test.js +87 -0
- package/dist/__tests__/tools.test.js +2143 -0
- package/dist/data/components.js +2818 -0
- package/dist/data/controllers-map.js +7571 -0
- package/dist/data/core-api.js +9356 -0
- package/dist/data/database-schema.js +4555 -0
- package/dist/data/events-map.js +1339 -0
- package/dist/data/fields-map.js +3692 -0
- package/dist/data/hooks.js +2257 -0
- package/dist/data/js-api.js +123 -0
- package/dist/data/libs-api.js +264 -0
- package/dist/data/routes-map.js +203 -0
- package/dist/data/schemas-validation.js +255 -0
- package/dist/data/schemas.js +1404 -0
- package/dist/data/traits-map.js +1004 -0
- package/dist/data/version-profiles.js +41 -0
- package/dist/data/widgets-map.js +131 -0
- package/dist/data/wysiwyg-map.js +486 -0
- package/dist/generated/components-source.js +14748 -0
- package/dist/generated/hooks-source.js +3399 -0
- package/dist/generated/knowledge-meta.js +101 -0
- package/dist/index.js +13 -0
- package/dist/registry/database-tools.js +112 -0
- package/dist/registry/extension-tools.js +473 -0
- package/dist/registry/generator-tools.js +506 -0
- package/dist/registry/knowledge-tools.js +441 -0
- package/dist/registry/language-tools.js +99 -0
- package/dist/registry/meta-tools.js +152 -0
- package/dist/registry/project-tools.js +48 -0
- package/dist/registry/resources.js +98 -0
- package/dist/registry/source-tools.js +211 -0
- package/dist/registry/template-development-tools.js +79 -0
- package/dist/server.js +32 -0
- package/dist/tools/addon-tool.js +1437 -0
- package/dist/tools/admin-partial-tool.js +498 -0
- package/dist/tools/api-tool.js +294 -0
- package/dist/tools/artifact-tool.js +95 -0
- package/dist/tools/cache-tool.js +360 -0
- package/dist/tools/component-tool.js +415 -0
- package/dist/tools/controllers-tool.js +125 -0
- package/dist/tools/cron-tool.js +197 -0
- package/dist/tools/crud-tool.js +659 -0
- package/dist/tools/db-tool.js +198 -0
- package/dist/tools/email-tool.js +222 -0
- package/dist/tools/external-api-tool.js +596 -0
- package/dist/tools/filter-tool.js +341 -0
- package/dist/tools/form-tool.js +275 -0
- package/dist/tools/grid-tool.js +189 -0
- package/dist/tools/hooks-tool.js +104 -0
- package/dist/tools/import-export-tool.js +548 -0
- package/dist/tools/lang-tool.js +251 -0
- package/dist/tools/layout-override-tool.js +125 -0
- package/dist/tools/layout-tool.js +548 -0
- package/dist/tools/maria-tool.js +127 -0
- package/dist/tools/mariadb.js +201 -0
- package/dist/tools/migration-tool.js +329 -0
- package/dist/tools/oauth-tool.js +520 -0
- package/dist/tools/parser/components-parser.js +76 -0
- package/dist/tools/parser/controllers-parser.js +313 -0
- package/dist/tools/parser/core-parser.js +294 -0
- package/dist/tools/parser/coverage-generator.js +424 -0
- package/dist/tools/parser/events-parser.js +127 -0
- package/dist/tools/parser/fields-parser.js +382 -0
- package/dist/tools/parser/hooks-parser.js +106 -0
- package/dist/tools/parser/sql-parser.js +197 -0
- package/dist/tools/parser/traits-parser.js +161 -0
- package/dist/tools/parser/widgets-parser.js +150 -0
- package/dist/tools/permission-tool.js +348 -0
- package/dist/tools/project-patch-tool.js +73 -0
- package/dist/tools/project-source-tool.js +188 -0
- package/dist/tools/project-workflow-tool.js +186 -0
- package/dist/tools/requirement-tool.js +212 -0
- package/dist/tools/scaffold-tool.js +815 -0
- package/dist/tools/seo-tool.js +412 -0
- package/dist/tools/source-tool.js +155 -0
- package/dist/tools/template-development-tool.js +271 -0
- package/dist/tools/template-overrides-tool.js +294 -0
- package/dist/tools/template-productivity-tool.js +319 -0
- package/dist/tools/template-tool.js +665 -0
- package/dist/tools/test-tool.js +183 -0
- package/dist/tools/webhook-tool.js +427 -0
- package/dist/tools/widget-tool.js +331 -0
- package/dist/tools/wysiwyg-tool.js +137 -0
- package/dist/types/scaffold.js +68 -0
- package/dist/utils/define-tool.js +37 -0
- package/dist/utils/find-tool.js +97 -0
- package/dist/utils/mcp-result.js +18 -0
- package/dist/utils/pagination.js +27 -0
- package/dist/utils/serialization.js +27 -0
- package/package.json +94 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scaffoldApi = scaffoldApi;
|
|
4
|
+
function scaffoldApi(opts) {
|
|
5
|
+
const name = opts.addon_name.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
6
|
+
const Name = name.split('_').map(capitalize).join('');
|
|
7
|
+
const version = opts.version || 'v1';
|
|
8
|
+
const basePath = opts.options?.base_path || `/api/${version}/${name}`;
|
|
9
|
+
const files = {};
|
|
10
|
+
const ctrl = `package/system/controllers/${name}`;
|
|
11
|
+
files[`${ctrl}/api/${version}/index.php`] = generateApiController(name, Name, version, opts.endpoints, basePath);
|
|
12
|
+
files[`${ctrl}/manifest.xml`] = generateManifest(name, Name, version);
|
|
13
|
+
if (opts.options?.use_swagger) {
|
|
14
|
+
files[`package/docs/openapi.json`] = generateOpenApi(name, version, opts.endpoints, basePath);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
addon_name: name,
|
|
18
|
+
api_version: version,
|
|
19
|
+
base_path: basePath,
|
|
20
|
+
endpoints_count: opts.endpoints.length,
|
|
21
|
+
files,
|
|
22
|
+
endpoints: opts.endpoints.map(e => ({
|
|
23
|
+
method: e.method,
|
|
24
|
+
path: `${basePath}${e.path}`,
|
|
25
|
+
name: e.name,
|
|
26
|
+
auth_required: e.auth_required ?? true,
|
|
27
|
+
})),
|
|
28
|
+
structure_notes: [
|
|
29
|
+
`API контроллер: ${ctrl}/api/${version}/index.php`,
|
|
30
|
+
`Маршрут: ${basePath}/*`,
|
|
31
|
+
`Аутентификация: через API ключ или OAuth токен`,
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function capitalize(str) {
|
|
36
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
37
|
+
}
|
|
38
|
+
function generateApiController(name, Name, version, endpoints, basePath) {
|
|
39
|
+
let code = `<?php
|
|
40
|
+
/**
|
|
41
|
+
* API Controller ${name} v${version}
|
|
42
|
+
* Base path: ${basePath}
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
class api${Name}${capitalize(version)} extends cmsBackend {
|
|
46
|
+
|
|
47
|
+
protected $token;
|
|
48
|
+
protected $user_id;
|
|
49
|
+
|
|
50
|
+
public function __construct(cmsRequest $request) {
|
|
51
|
+
parent::__construct($request);
|
|
52
|
+
$this->token = $this->request->get('token', '');
|
|
53
|
+
$this->checkAuth();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected function checkAuth() {
|
|
57
|
+
if (!$this->token) {
|
|
58
|
+
$this->errorResponse('API token required', 401);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
$user = $this->model->getUserByToken($this->token);
|
|
63
|
+
if (!$user) {
|
|
64
|
+
$this->errorResponse('Invalid API token', 401);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
$this->user_id = $user['id'];
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected function errorResponse($message, $code = 400) {
|
|
73
|
+
http_response_code($code);
|
|
74
|
+
echo json_encode(['error' => true, 'message' => $message]);
|
|
75
|
+
exit;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected function successResponse($data, $code = 200) {
|
|
79
|
+
http_response_code($code);
|
|
80
|
+
echo json_encode(['success' => true, 'data' => $data]);
|
|
81
|
+
exit;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
`;
|
|
85
|
+
for (const endpoint of endpoints) {
|
|
86
|
+
code += generateEndpoint(name, Name, endpoint);
|
|
87
|
+
}
|
|
88
|
+
code += `}
|
|
89
|
+
`;
|
|
90
|
+
return code;
|
|
91
|
+
}
|
|
92
|
+
function generateEndpoint(name, _Name, endpoint) {
|
|
93
|
+
const methodUpper = endpoint.method.toUpperCase();
|
|
94
|
+
let paramsValidation = '';
|
|
95
|
+
let pathParams = '';
|
|
96
|
+
if (endpoint.params) {
|
|
97
|
+
for (const param of endpoint.params) {
|
|
98
|
+
if (param.type === 'path') {
|
|
99
|
+
pathParams += `, $${param.name}`;
|
|
100
|
+
paramsValidation += `
|
|
101
|
+
$${param.name} = $this->request->get('${param.name}', ${param.required ? "''" : 'null'});
|
|
102
|
+
if (${param.required ? `empty($${param.name})` : 'false'}) {
|
|
103
|
+
$this->errorResponse('Missing required parameter: ${param.name}', 400);
|
|
104
|
+
}`;
|
|
105
|
+
}
|
|
106
|
+
else if (param.type === 'query') {
|
|
107
|
+
paramsValidation += `
|
|
108
|
+
$${param.name} = $this->request->get('${param.name}', null);`;
|
|
109
|
+
}
|
|
110
|
+
else if (param.type === 'body') {
|
|
111
|
+
paramsValidation += `
|
|
112
|
+
$${param.name} = $this->request->get('${param.name}', null);`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const authCheck = endpoint.auth_required !== false
|
|
117
|
+
? `
|
|
118
|
+
if (!$this->user_id) {
|
|
119
|
+
$this->errorResponse('Unauthorized', 401);
|
|
120
|
+
}`
|
|
121
|
+
: '';
|
|
122
|
+
const endpointDoc = endpoint.description
|
|
123
|
+
? `/**
|
|
124
|
+
* ${endpoint.description}
|
|
125
|
+
*/
|
|
126
|
+
`
|
|
127
|
+
: '';
|
|
128
|
+
let code = `${endpointDoc} public function run(${pathParams}) {${authCheck}${paramsValidation}
|
|
129
|
+
`;
|
|
130
|
+
switch (methodUpper) {
|
|
131
|
+
case 'GET':
|
|
132
|
+
code += generateGetHandler(name, endpoint);
|
|
133
|
+
break;
|
|
134
|
+
case 'POST':
|
|
135
|
+
code += generatePostHandler(name, endpoint);
|
|
136
|
+
break;
|
|
137
|
+
case 'PUT':
|
|
138
|
+
case 'PATCH':
|
|
139
|
+
code += generatePutPatchHandler(name, endpoint);
|
|
140
|
+
break;
|
|
141
|
+
case 'DELETE':
|
|
142
|
+
code += generateDeleteHandler(name, endpoint);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
code += ` }
|
|
146
|
+
|
|
147
|
+
`;
|
|
148
|
+
return code;
|
|
149
|
+
}
|
|
150
|
+
function generateGetHandler(name, endpoint) {
|
|
151
|
+
const isList = endpoint.path.includes('/list') || endpoint.name.includes('list');
|
|
152
|
+
const isSingle = endpoint.name.includes('_by_') || endpoint.path.match(/\/[a-z_]+\/\{.*\}/);
|
|
153
|
+
if (isList) {
|
|
154
|
+
return `
|
|
155
|
+
$page = $this->request->get('page', 1);
|
|
156
|
+
$perpage = $this->request->get('perpage', 20);
|
|
157
|
+
|
|
158
|
+
$items = $this->model->get${capitalize(name)}List($this->user_id, [
|
|
159
|
+
'page' => $page,
|
|
160
|
+
'perpage' => $perpage,
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
$this->successResponse($items);
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
else if (isSingle) {
|
|
167
|
+
const idVar = endpoint.params?.find(p => p.type === 'path')?.name || 'id';
|
|
168
|
+
return `
|
|
169
|
+
$${idVar} = (int) $${idVar};
|
|
170
|
+
|
|
171
|
+
$item = $this->model->get${capitalize(name)}ById($${idVar}, $this->user_id);
|
|
172
|
+
|
|
173
|
+
if (!$item) {
|
|
174
|
+
$this->errorResponse('Item not found', 404);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
$this->successResponse($item);
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
return `
|
|
182
|
+
$this->successResponse(['status' => 'ok']);
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function generatePostHandler(name, _endpoint) {
|
|
187
|
+
return `
|
|
188
|
+
$data = $this->request->getAll();
|
|
189
|
+
|
|
190
|
+
$id = $this->model->add${capitalize(name)}($data, $this->user_id);
|
|
191
|
+
|
|
192
|
+
if (!$id) {
|
|
193
|
+
$this->errorResponse('Failed to create item', 500);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
$this->successResponse(['id' => $id], 201);
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
function generatePutPatchHandler(name, endpoint) {
|
|
200
|
+
const idVar = endpoint.params?.find(p => p.type === 'path')?.name || 'id';
|
|
201
|
+
return `
|
|
202
|
+
$${idVar} = (int) $${idVar};
|
|
203
|
+
$data = $this->request->getAll();
|
|
204
|
+
|
|
205
|
+
$item = $this->model->get${capitalize(name)}ById($${idVar}, $this->user_id);
|
|
206
|
+
|
|
207
|
+
if (!$item) {
|
|
208
|
+
$this->errorResponse('Item not found', 404);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
$updated = $this->model->update${capitalize(name)}($${idVar}, $data);
|
|
212
|
+
|
|
213
|
+
if (!$updated) {
|
|
214
|
+
$this->errorResponse('Failed to update item', 500);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
$this->successResponse(['id' => $${idVar}]);
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
function generateDeleteHandler(name, endpoint) {
|
|
221
|
+
const idVar = endpoint.params?.find(p => p.type === 'path')?.name || 'id';
|
|
222
|
+
return `
|
|
223
|
+
$${idVar} = (int) $${idVar};
|
|
224
|
+
|
|
225
|
+
$deleted = $this->model->delete${capitalize(name)}($${idVar}, $this->user_id);
|
|
226
|
+
|
|
227
|
+
if (!$deleted) {
|
|
228
|
+
$this->errorResponse('Failed to delete item', 500);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
$this->successResponse(['deleted' => true]);
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
function generateManifest(name, Name, version) {
|
|
235
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
236
|
+
<addon>
|
|
237
|
+
<name>${name}_api</name>
|
|
238
|
+
<title>${Name} API ${version}</title>
|
|
239
|
+
<version>1.0.0</version>
|
|
240
|
+
<files>
|
|
241
|
+
<file>controllers/${name}/api/${version}/index.php</file>
|
|
242
|
+
</files>
|
|
243
|
+
<routes>
|
|
244
|
+
<route path="/api/${version}/${name}/">
|
|
245
|
+
<handler>${name}/api/${version}/index.php</handler>
|
|
246
|
+
</route>
|
|
247
|
+
</routes>
|
|
248
|
+
</addon>`;
|
|
249
|
+
}
|
|
250
|
+
function generateOpenApi(name, version, endpoints, basePath) {
|
|
251
|
+
const paths = {};
|
|
252
|
+
for (const endpoint of endpoints) {
|
|
253
|
+
const path = `${basePath}${endpoint.path}`;
|
|
254
|
+
paths[path] = {
|
|
255
|
+
[endpoint.method.toLowerCase()]: {
|
|
256
|
+
summary: endpoint.description || endpoint.name,
|
|
257
|
+
tags: [name],
|
|
258
|
+
security: endpoint.auth_required !== false ? [{ ApiToken: [] }] : [],
|
|
259
|
+
parameters: endpoint.params
|
|
260
|
+
?.filter(p => p.type === 'query')
|
|
261
|
+
.map(p => ({
|
|
262
|
+
name: p.name,
|
|
263
|
+
in: 'query',
|
|
264
|
+
required: p.required,
|
|
265
|
+
description: p.description,
|
|
266
|
+
})),
|
|
267
|
+
responses: {
|
|
268
|
+
'200': { description: 'Success' },
|
|
269
|
+
'400': { description: 'Bad Request' },
|
|
270
|
+
'401': { description: 'Unauthorized' },
|
|
271
|
+
'404': { description: 'Not Found' },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const openApiSpec = {
|
|
277
|
+
openapi: '3.0.0',
|
|
278
|
+
info: {
|
|
279
|
+
title: `${name} API ${version}`,
|
|
280
|
+
version: version,
|
|
281
|
+
},
|
|
282
|
+
paths,
|
|
283
|
+
components: {
|
|
284
|
+
securitySchemes: {
|
|
285
|
+
ApiToken: {
|
|
286
|
+
type: 'apiKey',
|
|
287
|
+
in: 'query',
|
|
288
|
+
name: 'token',
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
return JSON.stringify(openApiSpec, null, 2);
|
|
294
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateGeneratedArtifacts = validateGeneratedArtifacts;
|
|
4
|
+
exports.buildAddonArchive = buildAddonArchive;
|
|
5
|
+
exports.inspectAddonArchive = inspectAddonArchive;
|
|
6
|
+
const node_buffer_1 = require("node:buffer");
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const ini_1 = require("ini");
|
|
9
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
10
|
+
const yaml_1 = require("yaml");
|
|
11
|
+
const fflate_1 = require("fflate");
|
|
12
|
+
function validateGeneratedArtifacts(files) {
|
|
13
|
+
const diagnostics = [];
|
|
14
|
+
for (const [path, content] of Object.entries(files)) {
|
|
15
|
+
try {
|
|
16
|
+
if (path.endsWith('.xml')) {
|
|
17
|
+
const result = fast_xml_parser_1.XMLValidator.validate(content);
|
|
18
|
+
if (result !== true)
|
|
19
|
+
throw new Error(result.err.msg);
|
|
20
|
+
new fast_xml_parser_1.XMLParser().parse(content);
|
|
21
|
+
}
|
|
22
|
+
else if (path.endsWith('.ini')) {
|
|
23
|
+
(0, ini_1.parse)(content);
|
|
24
|
+
}
|
|
25
|
+
else if (/\.ya?ml$/i.test(path)) {
|
|
26
|
+
(0, yaml_1.parse)(content);
|
|
27
|
+
}
|
|
28
|
+
else if (path.endsWith('.php')) {
|
|
29
|
+
validatePhpShape(content);
|
|
30
|
+
const lint = (0, node_child_process_1.spawnSync)('php', ['-l'], { input: content, encoding: 'utf8', timeout: 5_000 });
|
|
31
|
+
if (lint.error && lint.error.code === 'ENOENT') {
|
|
32
|
+
if (!diagnostics.some(item => item.code === 'PHP_LINTER_UNAVAILABLE')) {
|
|
33
|
+
diagnostics.push({ code: 'PHP_LINTER_UNAVAILABLE', severity: 'warning', path, message: 'php executable не найден; выполнена только структурная проверка' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (lint.status !== 0) {
|
|
37
|
+
throw new Error((lint.stderr || lint.stdout || 'php -l failed').trim());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
diagnostics.push({
|
|
43
|
+
code: 'INVALID_ARTIFACT_SYNTAX',
|
|
44
|
+
severity: 'error',
|
|
45
|
+
path,
|
|
46
|
+
message: error instanceof Error ? error.message : String(error),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
is_valid: !diagnostics.some(item => item.severity === 'error'),
|
|
52
|
+
files_checked: Object.keys(files).length,
|
|
53
|
+
diagnostics,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function buildAddonArchive(files) {
|
|
57
|
+
assertSafePaths(Object.keys(files));
|
|
58
|
+
const entries = Object.fromEntries(Object.entries(files).map(([path, content]) => [normalizePackagePath(path), (0, fflate_1.strToU8)(content)]));
|
|
59
|
+
const bytes = (0, fflate_1.zipSync)(entries, { level: 6 });
|
|
60
|
+
return {
|
|
61
|
+
encoding: 'base64',
|
|
62
|
+
archive: node_buffer_1.Buffer.from(bytes).toString('base64'),
|
|
63
|
+
bytes: bytes.length,
|
|
64
|
+
files_count: Object.keys(entries).length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function inspectAddonArchive(base64) {
|
|
68
|
+
const entries = (0, fflate_1.unzipSync)(node_buffer_1.Buffer.from(base64, 'base64'));
|
|
69
|
+
const paths = Object.keys(entries);
|
|
70
|
+
assertSafePaths(paths);
|
|
71
|
+
const files = Object.fromEntries(paths.map(path => [path, (0, fflate_1.strFromU8)(entries[path])]));
|
|
72
|
+
return { paths, ...validateGeneratedArtifacts(files) };
|
|
73
|
+
}
|
|
74
|
+
function normalizePackagePath(path) {
|
|
75
|
+
return path
|
|
76
|
+
.replace(/^\[pkg\]\s*/, '')
|
|
77
|
+
.replace(/\\/g, '/')
|
|
78
|
+
.replace(/^\/+/, '');
|
|
79
|
+
}
|
|
80
|
+
function assertSafePaths(paths) {
|
|
81
|
+
for (const path of paths) {
|
|
82
|
+
const normalized = normalizePackagePath(path);
|
|
83
|
+
if (!normalized || normalized.split('/').includes('..'))
|
|
84
|
+
throw new Error(`Небезопасный путь архива: ${path}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function validatePhpShape(content) {
|
|
88
|
+
if (!content.trimStart().startsWith('<?php') && !content.trimStart().startsWith('<!DOCTYPE')) {
|
|
89
|
+
throw new Error('PHP-файл должен начинаться с <?php');
|
|
90
|
+
}
|
|
91
|
+
const opens = (content.match(/[({\[]/g) ?? []).length;
|
|
92
|
+
const closes = (content.match(/[)}\]]/g) ?? []).length;
|
|
93
|
+
if (opens !== closes)
|
|
94
|
+
throw new Error('Несбалансированные скобки в PHP-файле');
|
|
95
|
+
}
|