@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,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fc = __importStar(require("fast-check"));
|
|
37
|
+
const ini_1 = require("ini");
|
|
38
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
39
|
+
const scaffold_tool_js_1 = require("../tools/scaffold-tool.js");
|
|
40
|
+
const addon_tool_js_1 = require("../tools/addon-tool.js");
|
|
41
|
+
const xmlParser = new fast_xml_parser_1.XMLParser({ parseTagValue: false, trimValues: false });
|
|
42
|
+
const addonTypes = ['basic', 'with_admin', 'with_hooks', 'with_routes', 'with_widget'];
|
|
43
|
+
function extractFiles(result) {
|
|
44
|
+
return result.files;
|
|
45
|
+
}
|
|
46
|
+
function safePhrasing(s) {
|
|
47
|
+
return s
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.replace(/[^a-z0-9_]/g, '')
|
|
50
|
+
.slice(0, 40);
|
|
51
|
+
}
|
|
52
|
+
describe('scaffoldAddon round-trip', () => {
|
|
53
|
+
test.each(addonTypes)('тип %s: manifest.xml парсится через XMLParser', type => {
|
|
54
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({
|
|
55
|
+
name: `t_${type}_${Math.random().toString(36).slice(2, 6)}`,
|
|
56
|
+
title: `Title ${type}`,
|
|
57
|
+
type,
|
|
58
|
+
});
|
|
59
|
+
const files = extractFiles(result);
|
|
60
|
+
const xmlFile = Object.keys(files).find(p => p.endsWith('manifest.xml'));
|
|
61
|
+
expect(xmlFile).toBeDefined();
|
|
62
|
+
const parsed = xmlParser.parse(files[xmlFile]);
|
|
63
|
+
expect(parsed).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
test.each(addonTypes)('тип %s: manifest.ru.ini парсится через ini', type => {
|
|
66
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({
|
|
67
|
+
name: `ini_${type}_${Math.random().toString(36).slice(2, 6)}`,
|
|
68
|
+
title: `Title ${type}`,
|
|
69
|
+
type,
|
|
70
|
+
});
|
|
71
|
+
const files = extractFiles(result);
|
|
72
|
+
const iniFile = '[pkg] manifest.ru.ini';
|
|
73
|
+
expect(files[iniFile]).toBeDefined();
|
|
74
|
+
const parsed = (0, ini_1.parse)(files[iniFile]);
|
|
75
|
+
expect(parsed.info.title).toMatch(/Title/);
|
|
76
|
+
expect(parsed.version.major).toMatch(/^\d+$/);
|
|
77
|
+
});
|
|
78
|
+
test('все 5 типов проходят validateAddon без errors', () => {
|
|
79
|
+
for (const type of addonTypes) {
|
|
80
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({
|
|
81
|
+
name: `v_${type}_${safePhrasing(type)}`,
|
|
82
|
+
title: `Title for ${type}`,
|
|
83
|
+
type,
|
|
84
|
+
});
|
|
85
|
+
const files = extractFiles(result);
|
|
86
|
+
const validation = (0, addon_tool_js_1.validateAddon)(files);
|
|
87
|
+
expect(validation.errors).toEqual([]);
|
|
88
|
+
expect(validation.is_valid).toBe(true);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
test('PHP class names соответствуют файлам и регистру', () => {
|
|
92
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'names_test', title: 'Names Test', type: 'basic' });
|
|
93
|
+
const files = extractFiles(result);
|
|
94
|
+
// frontend.php определяет class names_test extends cmsFrontend.
|
|
95
|
+
expect(files['package/system/controllers/names_test/frontend.php']).toMatch(/class\s+names_test\s+extends\s+cmsFrontend/);
|
|
96
|
+
// model.php определяет class modelNamesTest extends cmsModel.
|
|
97
|
+
expect(files['package/system/controllers/names_test/model.php']).toMatch(/class\s+modelNamesTest\s+extends\s+cmsModel/);
|
|
98
|
+
});
|
|
99
|
+
test('manifest.xml содержит имя контроллера в теге <name>', () => {
|
|
100
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'ctlname', title: 'Ctl Name', type: 'basic' });
|
|
101
|
+
const files = extractFiles(result);
|
|
102
|
+
const xml = files['package/system/controllers/ctlname/manifest.xml'];
|
|
103
|
+
expect(xml).toContain('<name>ctlname</name>');
|
|
104
|
+
});
|
|
105
|
+
test('manifest.ru.ini содержит правильный тип контроллера', () => {
|
|
106
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'inichk', title: 'Ini Check', type: 'basic' });
|
|
107
|
+
const iniText = extractFiles(result)['[pkg] manifest.ru.ini'];
|
|
108
|
+
const parsed = (0, ini_1.parse)(iniText);
|
|
109
|
+
expect(parsed.install.type).toBe('controller');
|
|
110
|
+
expect(parsed.install.name).toBe('inichk');
|
|
111
|
+
});
|
|
112
|
+
test('injection-пейлоады в title не ломают generated PHP и сериализуются безопасно', () => {
|
|
113
|
+
const xss = '</title><script>alert(1)</script>';
|
|
114
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({
|
|
115
|
+
name: 'injtest',
|
|
116
|
+
title: xss,
|
|
117
|
+
type: 'basic',
|
|
118
|
+
});
|
|
119
|
+
const files = extractFiles(result);
|
|
120
|
+
// 1. manifest.xml экранирует спецсимволы XML.
|
|
121
|
+
const xml = files['package/system/controllers/injtest/manifest.xml'];
|
|
122
|
+
expect(xml).not.toContain('<script>alert(1)</script>');
|
|
123
|
+
expect(xml).toContain('<script>');
|
|
124
|
+
// 2. INI: строки квотятся — ini-парсер восстанавливает исходник (включая < и >).
|
|
125
|
+
const iniText = files['[pkg] manifest.ru.ini'];
|
|
126
|
+
const parsedIni = (0, ini_1.parse)(iniText);
|
|
127
|
+
expect(parsedIni.info.title).toBe(xss);
|
|
128
|
+
// В ini-файле строка double-quoted, ini-парсер корректно восстанавливает.
|
|
129
|
+
// 3. PHP LANG-файл: PHP-строка в одинарных кавычках содержит апострофы экранированными,
|
|
130
|
+
// а < > — как литералы (PHP их интерпретирует корректно внутри строки).
|
|
131
|
+
const php = files['package/system/languages/ru/controllers/injtest/injtest.php'];
|
|
132
|
+
expect(php).toContain("define('LANG_INJTEST_TITLE'");
|
|
133
|
+
// Исходная строка восстановима через unescape апострофов — но в этом случае
|
|
134
|
+
// апострофов в `xss` нет, поэтому просто проверим наличие строки как литерала.
|
|
135
|
+
expect(php).toContain(xss);
|
|
136
|
+
});
|
|
137
|
+
test('PHP LANG-файл корректно сериализует title с апострофами и backslash', () => {
|
|
138
|
+
const tricky = `Title with 'quotes' and \\backslash`;
|
|
139
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'qesc', title: tricky, type: 'basic' });
|
|
140
|
+
const files = extractFiles(result);
|
|
141
|
+
const php = files['package/system/languages/ru/controllers/qesc/qesc.php'];
|
|
142
|
+
// PHP-строки экранируют апострофы через \' и backslash через \\.
|
|
143
|
+
expect(php).toContain("\\'quotes\\'");
|
|
144
|
+
expect(php).toContain('\\\\backslash');
|
|
145
|
+
});
|
|
146
|
+
test('author URL должен быть URL; невалидный формат → throw', () => {
|
|
147
|
+
expect(() => (0, scaffold_tool_js_1.scaffoldAddon)({
|
|
148
|
+
name: 'authtest',
|
|
149
|
+
title: 'Auth',
|
|
150
|
+
type: 'basic',
|
|
151
|
+
author_url: 'not a url',
|
|
152
|
+
})).not.toThrow(); // валидация author_url делается на уровне Zod registry, не внутри функции
|
|
153
|
+
// Внутри scaffoldAddon author_url квотируется как строка, но финальный URL
|
|
154
|
+
// в manifest.ru.ini парсится через ini.
|
|
155
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({
|
|
156
|
+
name: 'authtest',
|
|
157
|
+
title: 'Auth',
|
|
158
|
+
type: 'basic',
|
|
159
|
+
author_url: 'https://example.com/path',
|
|
160
|
+
});
|
|
161
|
+
const files = extractFiles(result);
|
|
162
|
+
const iniText = files['[pkg] manifest.ru.ini'];
|
|
163
|
+
const parsed = (0, ini_1.parse)(iniText);
|
|
164
|
+
expect(parsed.author.url).toBe('https://example.com/path');
|
|
165
|
+
});
|
|
166
|
+
test('with_hooks: каждый запрошенный хук попадает в manifest.xml', () => {
|
|
167
|
+
const requestedHooks = ['user_registered', 'content_after_add_approve'];
|
|
168
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({
|
|
169
|
+
name: 'hkok',
|
|
170
|
+
title: 'Hooks',
|
|
171
|
+
type: 'with_hooks',
|
|
172
|
+
hooks: requestedHooks,
|
|
173
|
+
});
|
|
174
|
+
const files = extractFiles(result);
|
|
175
|
+
const xml = files['package/system/controllers/hkok/manifest.xml'];
|
|
176
|
+
expect(xml).toContain('user_registered');
|
|
177
|
+
expect(xml).toContain('content_after_add_approve');
|
|
178
|
+
// Также должны быть файлы хуков.
|
|
179
|
+
const hookFiles = Object.keys(files).filter(p => p.includes('/hooks/'));
|
|
180
|
+
expect(hookFiles.length).toBeGreaterThanOrEqual(requestedHooks.length);
|
|
181
|
+
});
|
|
182
|
+
test('with_admin: присутствует backend.php', () => {
|
|
183
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'admn', title: 'Admin', type: 'with_admin' });
|
|
184
|
+
const files = extractFiles(result);
|
|
185
|
+
expect(files['package/system/controllers/admn/backend.php']).toBeDefined();
|
|
186
|
+
});
|
|
187
|
+
test('with_widget: присутствует widget файлы', () => {
|
|
188
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'wdgt', title: 'Widget', type: 'with_widget' });
|
|
189
|
+
const files = extractFiles(result);
|
|
190
|
+
const widgetFile = Object.keys(files).find(p => p.includes('/widgets/'));
|
|
191
|
+
expect(widgetFile).toBeDefined();
|
|
192
|
+
});
|
|
193
|
+
test('with_routes: присутствует routes.php или эквивалент', () => {
|
|
194
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'rts', title: 'Routes', type: 'with_routes' });
|
|
195
|
+
const files = extractFiles(result);
|
|
196
|
+
const routesFile = Object.keys(files).find(p => p.endsWith('routes.php') || p.includes('/routes/'));
|
|
197
|
+
expect(routesFile).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
test('description передаётся в manifest.ru.ini', () => {
|
|
200
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({
|
|
201
|
+
name: 'desc',
|
|
202
|
+
title: 'Desc',
|
|
203
|
+
description: 'Custom description for the addon',
|
|
204
|
+
type: 'basic',
|
|
205
|
+
});
|
|
206
|
+
const files = extractFiles(result);
|
|
207
|
+
const iniText = files['[pkg] manifest.ru.ini'];
|
|
208
|
+
const parsed = (0, ini_1.parse)(iniText);
|
|
209
|
+
expect(parsed.info.description).toBe('Custom description for the addon');
|
|
210
|
+
});
|
|
211
|
+
test('version из строки X.Y.Z попадает в [version] секцию', () => {
|
|
212
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({
|
|
213
|
+
name: 'ver',
|
|
214
|
+
title: 'Ver',
|
|
215
|
+
version: '3.2.1',
|
|
216
|
+
type: 'basic',
|
|
217
|
+
});
|
|
218
|
+
const files = extractFiles(result);
|
|
219
|
+
const iniText = files['[pkg] manifest.ru.ini'];
|
|
220
|
+
const parsed = (0, ini_1.parse)(iniText);
|
|
221
|
+
expect(parsed.version.major).toBe('3');
|
|
222
|
+
expect(parsed.version.minor).toBe('2');
|
|
223
|
+
expect(parsed.version.build).toBe('1');
|
|
224
|
+
});
|
|
225
|
+
test('плохие name бросают понятную ошибку', () => {
|
|
226
|
+
const cases = [
|
|
227
|
+
'', // empty
|
|
228
|
+
'a', // 1 char — regex требует минимум 2
|
|
229
|
+
'1abc', // не начинается с буквы
|
|
230
|
+
'ABC', // uppercase не разрешён
|
|
231
|
+
'with-dash', // dash не разрешён
|
|
232
|
+
'a'.repeat(65), // > 64
|
|
233
|
+
'../bad', // path traversal
|
|
234
|
+
];
|
|
235
|
+
for (const name of cases) {
|
|
236
|
+
expect(() => (0, scaffold_tool_js_1.scaffoldAddon)({ name, title: 'Bad', type: 'basic' })).toThrow();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
test('плохие version бросают ошибку', () => {
|
|
240
|
+
const cases = ['latest', '1.0', '1.0.0-beta', 'v1.0.0', '1', '1.0.0.0'];
|
|
241
|
+
for (const version of cases) {
|
|
242
|
+
expect(() => (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'vrb', title: 'V', version, type: 'basic' })).toThrow();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
test('property-based: имя из regex и валидный title всегда дают файл-мап', () => {
|
|
246
|
+
// Не используем faker.random — нам нужно уложиться в regex.
|
|
247
|
+
fc.assert(fc.property(fc.stringMatching(/^[a-z][a-z0-9_]{1,63}$/), fc.string({ minLength: 1, maxLength: 100 }), (name, title) => {
|
|
248
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name, title, type: 'basic' });
|
|
249
|
+
const files = extractFiles(result);
|
|
250
|
+
// Базовые обязательные файлы.
|
|
251
|
+
expect(files['[pkg] manifest.ru.ini']).toBeDefined();
|
|
252
|
+
expect(files['[pkg] install.sql']).toBeDefined();
|
|
253
|
+
expect(files[`package/system/controllers/${name}/frontend.php`]).toBeDefined();
|
|
254
|
+
expect(files[`package/system/controllers/${name}/model.php`]).toBeDefined();
|
|
255
|
+
return true;
|
|
256
|
+
}), { numRuns: 30 });
|
|
257
|
+
});
|
|
258
|
+
test('property-based: title с управляющими символами и Unicode не ломает вывод', () => {
|
|
259
|
+
fc.assert(fc.property(fc.stringMatching(/^[a-z][a-z0-9_]{1,20}$/), fc.string({ minLength: 1, maxLength: 200 }), (name, title) => {
|
|
260
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name, title, type: 'basic' });
|
|
261
|
+
const files = extractFiles(result);
|
|
262
|
+
// Просто проверим, что manifest.ru.ini парсится.
|
|
263
|
+
const iniText = files['[pkg] manifest.ru.ini'];
|
|
264
|
+
const parsed = (0, ini_1.parse)(iniText);
|
|
265
|
+
// ini-парсер не должен потерять данные; обратный unescape через
|
|
266
|
+
// стандартное деквотирование.
|
|
267
|
+
expect(parsed.info.title).toBeDefined();
|
|
268
|
+
return true;
|
|
269
|
+
}), { numRuns: 30 });
|
|
270
|
+
});
|
|
271
|
+
test('install.sql CREATE TABLE содержит правильное имя таблицы', () => {
|
|
272
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'sql', title: 'SQL', type: 'basic' });
|
|
273
|
+
const files = extractFiles(result);
|
|
274
|
+
const sql = files['[pkg] install.sql'];
|
|
275
|
+
expect(sql).toContain('CREATE TABLE');
|
|
276
|
+
expect(sql).toContain('cms_sql_items');
|
|
277
|
+
});
|
|
278
|
+
test('property-based: каждый тип не падает на базовом вводе', () => {
|
|
279
|
+
fc.assert(fc.property(fc.stringMatching(/^[a-z][a-z0-9_]{1,30}$/), fc.constantFrom(...addonTypes), fc.string({ minLength: 1, maxLength: 50 }), (name, type, title) => {
|
|
280
|
+
const result = (0, scaffold_tool_js_1.scaffoldAddon)({ name, title, type });
|
|
281
|
+
const files = extractFiles(result);
|
|
282
|
+
expect(typeof files).toBe('object');
|
|
283
|
+
expect(Object.keys(files).length).toBeGreaterThan(0);
|
|
284
|
+
return true;
|
|
285
|
+
}), { numRuns: 30 });
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fc = __importStar(require("fast-check"));
|
|
37
|
+
const ini_1 = require("ini");
|
|
38
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
39
|
+
const yaml_1 = require("yaml");
|
|
40
|
+
const serialization_js_1 = require("../utils/serialization.js");
|
|
41
|
+
const xmlParser = new fast_xml_parser_1.XMLParser({ parseTagValue: false, trimValues: false });
|
|
42
|
+
/**
|
|
43
|
+
* Конвертирует значение, экранированное escapeXml, в экранированный текстовый узел,
|
|
44
|
+
* который XML-парсер может вернуть в виде исходной строки.
|
|
45
|
+
*/
|
|
46
|
+
function wrapXmlText(value) {
|
|
47
|
+
return `<r>${(0, serialization_js_1.escapeXml)(value)}</r>`;
|
|
48
|
+
}
|
|
49
|
+
describe('serialization property-based', () => {
|
|
50
|
+
test('escapeXml: round-trip через XML-парсер сохраняет произвольную строку', () => {
|
|
51
|
+
fc.assert(fc.property(fc.string({ maxLength: 500 }), raw => {
|
|
52
|
+
const parsed = xmlParser.parse(wrapXmlText(raw));
|
|
53
|
+
return parsed.r === raw;
|
|
54
|
+
}), { numRuns: 200 });
|
|
55
|
+
});
|
|
56
|
+
test('escapeXml: спецсимволы всегда экранированы', () => {
|
|
57
|
+
fc.assert(fc.property(fc.string({ maxLength: 200 }), raw => {
|
|
58
|
+
const escaped = (0, serialization_js_1.escapeXml)(raw);
|
|
59
|
+
return !escaped.includes('<') && !escaped.includes('>');
|
|
60
|
+
}), { numRuns: 100 });
|
|
61
|
+
});
|
|
62
|
+
test('escapeXml: кавычки не появляются как голые после экранирования', () => {
|
|
63
|
+
fc.assert(fc.property(fc.string({ maxLength: 200 }), raw => {
|
|
64
|
+
const escaped = (0, serialization_js_1.escapeXml)(raw);
|
|
65
|
+
const inner = escaped
|
|
66
|
+
.replace(/</g, '<')
|
|
67
|
+
.replace(/>/g, '>')
|
|
68
|
+
.replace(/"/g, '"')
|
|
69
|
+
.replace(/'/g, "'")
|
|
70
|
+
.replace(/&/g, '&');
|
|
71
|
+
return inner === raw;
|
|
72
|
+
}), { numRuns: 100 });
|
|
73
|
+
});
|
|
74
|
+
test('escapeXml: текущая реализация НЕ идемпотентна на & (известное ограничение)', () => {
|
|
75
|
+
// escapeXml экранирует амперсанд ПОСЛЕ, что приводит к двойному экранированию.
|
|
76
|
+
// Это документированное поведение — пользователи должны передавать сырой текст.
|
|
77
|
+
expect((0, serialization_js_1.escapeXml)('&')).toBe('&amp;');
|
|
78
|
+
expect((0, serialization_js_1.escapeXml)('a&b')).toBe('a&amp;b');
|
|
79
|
+
});
|
|
80
|
+
test('escapeXml: XSS- и XXE-пейлоады становятся безопасным текстом', () => {
|
|
81
|
+
const xss = '<script>alert(1)</script>';
|
|
82
|
+
const xxe = '<!ENTITY xxe SYSTEM "file:///etc/passwd">';
|
|
83
|
+
expect((0, serialization_js_1.escapeXml)(xss)).toContain('<script>');
|
|
84
|
+
expect((0, serialization_js_1.escapeXml)(xss)).not.toContain('<script>');
|
|
85
|
+
expect((0, serialization_js_1.escapeXml)(xxe)).toContain('<!ENTITY');
|
|
86
|
+
expect((0, serialization_js_1.escapeXml)(xxe)).not.toContain('<!');
|
|
87
|
+
});
|
|
88
|
+
test('quoteIni: round-trip через ini-парсер сохраняет значение', () => {
|
|
89
|
+
fc.assert(fc.property(fc.string({ maxLength: 200 }).filter(s => !s.includes('\0')), raw => {
|
|
90
|
+
const iniText = `[s]\nk=${(0, serialization_js_1.quoteIni)(raw)}\n`;
|
|
91
|
+
const parsed = (0, ini_1.parse)(iniText);
|
|
92
|
+
return parsed.s.k === raw;
|
|
93
|
+
}), { numRuns: 200 });
|
|
94
|
+
});
|
|
95
|
+
test('quoteIni: переводы строк заменяются на пробел', () => {
|
|
96
|
+
fc.assert(fc.property(fc.string({ minLength: 1, maxLength: 100 }), fc.array(fc.constantFrom('\n', '\r', '\r\n'), { maxLength: 5 }), (raw, breaks) => {
|
|
97
|
+
const withBreaks = breaks.join(raw);
|
|
98
|
+
const quoted = (0, serialization_js_1.quoteIni)(withBreaks);
|
|
99
|
+
// Внутри кавычек не должно быть сырых \n или \r.
|
|
100
|
+
const inner = quoted.slice(1, -1);
|
|
101
|
+
return !inner.includes('\n') && !inner.includes('\r');
|
|
102
|
+
}), { numRuns: 100 });
|
|
103
|
+
});
|
|
104
|
+
test('quoteIni: обратный слэш и кавычка экранируются — round-trip даёт исходник', () => {
|
|
105
|
+
fc.assert(fc.property(fc.string({ maxLength: 100 }).filter(s => !s.includes('\0')), raw => {
|
|
106
|
+
const quoted = (0, serialization_js_1.quoteIni)(raw);
|
|
107
|
+
const iniText = `[s]\nk=${quoted}\n`;
|
|
108
|
+
const parsed = (0, ini_1.parse)(iniText);
|
|
109
|
+
return parsed.s.k === raw;
|
|
110
|
+
}), { numRuns: 200 });
|
|
111
|
+
});
|
|
112
|
+
test('quotePhp: экранирует апострофы и обратные слэши', () => {
|
|
113
|
+
fc.assert(fc.property(fc.string({ maxLength: 200 }), raw => {
|
|
114
|
+
const quoted = (0, serialization_js_1.quotePhp)(raw);
|
|
115
|
+
// Должно начинаться с апострофа и заканчиваться апострофом.
|
|
116
|
+
if (!quoted.startsWith("'") || !quoted.endsWith("'"))
|
|
117
|
+
return false;
|
|
118
|
+
const inner = quoted.slice(1, -1);
|
|
119
|
+
// Внутри не должно быть неэкранированных апострофов.
|
|
120
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
121
|
+
if (inner[i] === "'" && (i === 0 || inner[i - 1] !== '\\')) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}), { numRuns: 200 });
|
|
127
|
+
});
|
|
128
|
+
test('quotePhp: SQL-injection-строки корректно проходят через функцию без потери', () => {
|
|
129
|
+
const payloads = [
|
|
130
|
+
"'; DROP TABLE users; --",
|
|
131
|
+
"admin'--",
|
|
132
|
+
"' OR '1'='1",
|
|
133
|
+
"\\'; DROP TABLE x; --",
|
|
134
|
+
];
|
|
135
|
+
for (const payload of payloads) {
|
|
136
|
+
const quoted = (0, serialization_js_1.quotePhp)(payload);
|
|
137
|
+
// Из PHP-строки можно реконструировать исходник через стандартный unescape.
|
|
138
|
+
const reconstructed = quoted.slice(1, -1).replace(/\\'/g, "'").replace(/\\\\/g, '\\');
|
|
139
|
+
expect(reconstructed).toBe(payload);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
test('quoteYaml: для строк результат парсится обратно в исходное значение', () => {
|
|
143
|
+
fc.assert(fc.property(fc.string({ maxLength: 200 }), raw => {
|
|
144
|
+
const quoted = (0, serialization_js_1.quoteYaml)(raw);
|
|
145
|
+
const parsed = (0, yaml_1.parse)(quoted);
|
|
146
|
+
return parsed === raw;
|
|
147
|
+
}), { numRuns: 200 });
|
|
148
|
+
});
|
|
149
|
+
test('quoteYaml: числа сериализуются как валидный YAML scalar (int или string-of-int)', () => {
|
|
150
|
+
// Текущая реализация сознательно использует JSON.stringify(String(value)),
|
|
151
|
+
// поэтому числа становятся строкой. Зафиксируем это документированное поведение,
|
|
152
|
+
// чтобы изменение подсветилось при рефакторинге.
|
|
153
|
+
fc.assert(fc.property(fc.integer(), raw => {
|
|
154
|
+
const quoted = (0, serialization_js_1.quoteYaml)(raw);
|
|
155
|
+
const parsed = (0, yaml_1.parse)(quoted);
|
|
156
|
+
// Парсер YAML для "0" даст 0 (int). Для 1.5 — может дать number или "1.5".
|
|
157
|
+
// Главное — значение должно быть распарсено без ошибки и соответствовать String(raw).
|
|
158
|
+
return String(parsed) === String(raw);
|
|
159
|
+
}), { numRuns: 50 });
|
|
160
|
+
});
|
|
161
|
+
test('quoteYaml: всегда возвращает валидный double-quoted YAML scalar', () => {
|
|
162
|
+
fc.assert(fc.property(fc.string({ maxLength: 200 }), raw => {
|
|
163
|
+
const quoted = (0, serialization_js_1.quoteYaml)(raw);
|
|
164
|
+
// JSON-строка является валидным YAML double-quoted scalar.
|
|
165
|
+
// При парсинге через yaml-драйвер значение должно быть raw.
|
|
166
|
+
const parsed = (0, yaml_1.parse)(quoted);
|
|
167
|
+
return parsed === raw;
|
|
168
|
+
}), { numRuns: 100 });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_fs_1 = require("node:fs");
|
|
4
|
+
const node_os_1 = require("node:os");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const hooks_parser_js_1 = require("../tools/parser/hooks-parser.js");
|
|
7
|
+
const components_parser_js_1 = require("../tools/parser/components-parser.js");
|
|
8
|
+
describe('source-backed knowledge parsers', () => {
|
|
9
|
+
const fixtureRoot = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'instantcms-knowledge-'));
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(fixtureRoot, 'system/core'), { recursive: true });
|
|
12
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(fixtureRoot, 'system/controllers/demo/actions'), { recursive: true });
|
|
13
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(fixtureRoot, 'system/controllers/demo/actions/index.php'), `<?php
|
|
14
|
+
$data = cmsEventsManager::hook('demo_before_save', [$item, $user]);
|
|
15
|
+
cmsEventsManager::hookAll('demo_after_save', $item);
|
|
16
|
+
$controller->runHook('demo_before_save', [$item]);`);
|
|
17
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(fixtureRoot, 'system/core/demo.php'), `<?php
|
|
18
|
+
/** Demo API */
|
|
19
|
+
class cmsDemo {
|
|
20
|
+
/** @return string */
|
|
21
|
+
public function hello(string $name) { return $name; }
|
|
22
|
+
protected function hidden() {}
|
|
23
|
+
}`);
|
|
24
|
+
});
|
|
25
|
+
afterAll(() => (0, node_fs_1.rmSync)(fixtureRoot, { recursive: true, force: true }));
|
|
26
|
+
test('discovers, merges and sorts literal hook calls', () => {
|
|
27
|
+
const hooks = (0, hooks_parser_js_1.discoverHooks)(fixtureRoot);
|
|
28
|
+
expect(hooks.map(hook => hook.name)).toEqual(['demo_after_save', 'demo_before_save']);
|
|
29
|
+
expect(hooks.find(hook => hook.name === 'demo_before_save')).toMatchObject({
|
|
30
|
+
inferredType: 'filter',
|
|
31
|
+
occurrences: 2,
|
|
32
|
+
parameters: ['$item', '$user'],
|
|
33
|
+
});
|
|
34
|
+
expect((0, hooks_parser_js_1.generateHooksSource)(fixtureRoot)).toContain('system/controllers/demo/actions/index.php');
|
|
35
|
+
});
|
|
36
|
+
test('exports public core methods and excludes protected methods', () => {
|
|
37
|
+
const generated = (0, components_parser_js_1.generateComponentsSource)(fixtureRoot);
|
|
38
|
+
expect(generated).toContain('cmsDemo');
|
|
39
|
+
expect(generated).toContain('hello');
|
|
40
|
+
expect(generated).not.toContain('hidden');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const template_development_tool_js_1 = require("../tools/template-development-tool.js");
|
|
4
|
+
describe('template development workflows', () => {
|
|
5
|
+
test('scaffolds a complete template and layout scheme', () => {
|
|
6
|
+
const result = (0, template_development_tool_js_1.scaffoldCompleteTemplate)({
|
|
7
|
+
name: 'studio_theme',
|
|
8
|
+
title: 'Studio Theme',
|
|
9
|
+
inherit: ['modern'],
|
|
10
|
+
});
|
|
11
|
+
expect(result.files['manifest.php']).toContain("'inherit' => ['modern']");
|
|
12
|
+
expect(result.files['manifest.php']).toContain("'has_options' => false");
|
|
13
|
+
expect(result.files['main.tpl.php']).toContain("linkJS('js/main.js')");
|
|
14
|
+
expect(result.files['widgets/wrapper.tpl.php']).toContain('html($widget->title)');
|
|
15
|
+
expect(result.layout_scheme?.summary.positions).toEqual(expect.arrayContaining(['header', 'content', 'right-top', 'footer']));
|
|
16
|
+
});
|
|
17
|
+
test('analyzes template structure, positions and overrides', () => {
|
|
18
|
+
const result = (0, template_development_tool_js_1.analyzeInstantCmsTemplate)({
|
|
19
|
+
'manifest.php': '<?php return [];',
|
|
20
|
+
'main.tpl.php': "<?= $this->widgets('header') ?><?= $this->body() ?>",
|
|
21
|
+
'controllers/content/view.tpl.php': "<?= html($item['title']) ?>",
|
|
22
|
+
});
|
|
23
|
+
expect(result.is_valid).toBe(true);
|
|
24
|
+
expect(result.widget_positions).toContain('header');
|
|
25
|
+
expect(result.overrides).toContain('controllers/content/view.tpl.php');
|
|
26
|
+
});
|
|
27
|
+
test('discovers and analyzes a theme inside a complete project map', () => {
|
|
28
|
+
const result = (0, template_development_tool_js_1.analyzeInstantCmsTemplate)({
|
|
29
|
+
'templates/studio_theme/manifest.php': '<?php return [];',
|
|
30
|
+
'templates/studio_theme/main.tpl.php': '<?= $this->body() ?>',
|
|
31
|
+
});
|
|
32
|
+
expect(result.theme).toBe('studio_theme');
|
|
33
|
+
expect(result.is_valid).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
test('requires an explicit selection when a project contains multiple themes', () => {
|
|
36
|
+
const result = (0, template_development_tool_js_1.analyzeInstantCmsTemplate)({
|
|
37
|
+
'templates/first/manifest.php': '<?php return [];',
|
|
38
|
+
'templates/second/manifest.php': '<?php return [];',
|
|
39
|
+
});
|
|
40
|
+
expect(result.is_valid).toBe(false);
|
|
41
|
+
expect(result.diagnostics.some(item => item.code === 'MULTIPLE_TEMPLATES_FOUND')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
test('creates frontend and backend overrides from an upstream path', () => {
|
|
44
|
+
const result = (0, template_development_tool_js_1.scaffoldTemplateOverride)({
|
|
45
|
+
theme: 'studio_theme',
|
|
46
|
+
source_path: 'templates/modern/controllers/content/backend/items.tpl.php',
|
|
47
|
+
source_content: '<?php echo $this->renderGrid($grid);',
|
|
48
|
+
});
|
|
49
|
+
expect(result.target_path).toBe('templates/studio_theme/controllers/content/backend/items.tpl.php');
|
|
50
|
+
expect(result.files['controllers/content/backend/items.tpl.php']).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
test('validates layout YAML and reports duplicate positions', () => {
|
|
53
|
+
const result = (0, template_development_tool_js_1.validateLayoutScheme)(`layout:\n rows:\n - cols:\n - position: sidebar\n - position: sidebar\n`);
|
|
54
|
+
expect(result.is_valid).toBe(true);
|
|
55
|
+
expect(result.diagnostics.some(item => item.code === 'DUPLICATE_LAYOUT_POSITION')).toBe(true);
|
|
56
|
+
expect((0, template_development_tool_js_1.validateLayoutScheme)('rows: []').is_valid).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
test('marks an override when its upstream source changed or disappeared', () => {
|
|
59
|
+
const result = (0, template_development_tool_js_1.checkTemplateOverrideCompatibility)({
|
|
60
|
+
'controllers/content/view.tpl.php': 'custom',
|
|
61
|
+
'controllers/users/index.tpl.php': 'custom users',
|
|
62
|
+
}, {
|
|
63
|
+
'templates/modern/controllers/content/view.tpl.php': 'old',
|
|
64
|
+
'templates/modern/controllers/users/index.tpl.php': 'old users',
|
|
65
|
+
}, { 'templates/modern/controllers/content/view.tpl.php': 'new' });
|
|
66
|
+
expect(result.requires_review).toBe(2);
|
|
67
|
+
expect(result.results).toEqual(expect.arrayContaining([
|
|
68
|
+
expect.objectContaining({
|
|
69
|
+
path: 'controllers/content/view.tpl.php',
|
|
70
|
+
status: 'upstream_changed',
|
|
71
|
+
}),
|
|
72
|
+
expect.objectContaining({
|
|
73
|
+
path: 'controllers/users/index.tpl.php',
|
|
74
|
+
status: 'missing_upstream',
|
|
75
|
+
}),
|
|
76
|
+
]));
|
|
77
|
+
});
|
|
78
|
+
});
|