@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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/dist/__tests__/artifact-tool.test.js +196 -0
  4. package/dist/__tests__/db-tool.test.js +152 -0
  5. package/dist/__tests__/define-tool.test.js +106 -0
  6. package/dist/__tests__/find-tool.test.js +185 -0
  7. package/dist/__tests__/get-component-api.test.js +144 -0
  8. package/dist/__tests__/hardening.test.js +73 -0
  9. package/dist/__tests__/hooks-tool.test.js +173 -0
  10. package/dist/__tests__/mcp-integration-extra.test.js +166 -0
  11. package/dist/__tests__/mcp-integration.test.js +53 -0
  12. package/dist/__tests__/pagination.test.js +114 -0
  13. package/dist/__tests__/performance-baseline.test.js +56 -0
  14. package/dist/__tests__/project-patch.test.js +144 -0
  15. package/dist/__tests__/project-source.test.js +44 -0
  16. package/dist/__tests__/project-workflows-extra.test.js +165 -0
  17. package/dist/__tests__/project-workflows.test.js +51 -0
  18. package/dist/__tests__/scaffold-addon-roundtrip.test.js +287 -0
  19. package/dist/__tests__/serialization.test.js +170 -0
  20. package/dist/__tests__/source-knowledge-parsers.test.js +42 -0
  21. package/dist/__tests__/template-development.test.js +78 -0
  22. package/dist/__tests__/template-productivity.test.js +87 -0
  23. package/dist/__tests__/tools.test.js +2143 -0
  24. package/dist/data/components.js +2818 -0
  25. package/dist/data/controllers-map.js +7571 -0
  26. package/dist/data/core-api.js +9356 -0
  27. package/dist/data/database-schema.js +4555 -0
  28. package/dist/data/events-map.js +1339 -0
  29. package/dist/data/fields-map.js +3692 -0
  30. package/dist/data/hooks.js +2257 -0
  31. package/dist/data/js-api.js +123 -0
  32. package/dist/data/libs-api.js +264 -0
  33. package/dist/data/routes-map.js +203 -0
  34. package/dist/data/schemas-validation.js +255 -0
  35. package/dist/data/schemas.js +1404 -0
  36. package/dist/data/traits-map.js +1004 -0
  37. package/dist/data/version-profiles.js +41 -0
  38. package/dist/data/widgets-map.js +131 -0
  39. package/dist/data/wysiwyg-map.js +486 -0
  40. package/dist/generated/components-source.js +14748 -0
  41. package/dist/generated/hooks-source.js +3399 -0
  42. package/dist/generated/knowledge-meta.js +101 -0
  43. package/dist/index.js +13 -0
  44. package/dist/registry/database-tools.js +112 -0
  45. package/dist/registry/extension-tools.js +473 -0
  46. package/dist/registry/generator-tools.js +506 -0
  47. package/dist/registry/knowledge-tools.js +441 -0
  48. package/dist/registry/language-tools.js +99 -0
  49. package/dist/registry/meta-tools.js +152 -0
  50. package/dist/registry/project-tools.js +48 -0
  51. package/dist/registry/resources.js +98 -0
  52. package/dist/registry/source-tools.js +211 -0
  53. package/dist/registry/template-development-tools.js +79 -0
  54. package/dist/server.js +32 -0
  55. package/dist/tools/addon-tool.js +1437 -0
  56. package/dist/tools/admin-partial-tool.js +498 -0
  57. package/dist/tools/api-tool.js +294 -0
  58. package/dist/tools/artifact-tool.js +95 -0
  59. package/dist/tools/cache-tool.js +360 -0
  60. package/dist/tools/component-tool.js +415 -0
  61. package/dist/tools/controllers-tool.js +125 -0
  62. package/dist/tools/cron-tool.js +197 -0
  63. package/dist/tools/crud-tool.js +659 -0
  64. package/dist/tools/db-tool.js +198 -0
  65. package/dist/tools/email-tool.js +222 -0
  66. package/dist/tools/external-api-tool.js +596 -0
  67. package/dist/tools/filter-tool.js +341 -0
  68. package/dist/tools/form-tool.js +275 -0
  69. package/dist/tools/grid-tool.js +189 -0
  70. package/dist/tools/hooks-tool.js +104 -0
  71. package/dist/tools/import-export-tool.js +548 -0
  72. package/dist/tools/lang-tool.js +251 -0
  73. package/dist/tools/layout-override-tool.js +125 -0
  74. package/dist/tools/layout-tool.js +548 -0
  75. package/dist/tools/maria-tool.js +127 -0
  76. package/dist/tools/mariadb.js +201 -0
  77. package/dist/tools/migration-tool.js +329 -0
  78. package/dist/tools/oauth-tool.js +520 -0
  79. package/dist/tools/parser/components-parser.js +76 -0
  80. package/dist/tools/parser/controllers-parser.js +313 -0
  81. package/dist/tools/parser/core-parser.js +294 -0
  82. package/dist/tools/parser/coverage-generator.js +424 -0
  83. package/dist/tools/parser/events-parser.js +127 -0
  84. package/dist/tools/parser/fields-parser.js +382 -0
  85. package/dist/tools/parser/hooks-parser.js +106 -0
  86. package/dist/tools/parser/sql-parser.js +197 -0
  87. package/dist/tools/parser/traits-parser.js +161 -0
  88. package/dist/tools/parser/widgets-parser.js +150 -0
  89. package/dist/tools/permission-tool.js +348 -0
  90. package/dist/tools/project-patch-tool.js +73 -0
  91. package/dist/tools/project-source-tool.js +188 -0
  92. package/dist/tools/project-workflow-tool.js +186 -0
  93. package/dist/tools/requirement-tool.js +212 -0
  94. package/dist/tools/scaffold-tool.js +815 -0
  95. package/dist/tools/seo-tool.js +412 -0
  96. package/dist/tools/source-tool.js +155 -0
  97. package/dist/tools/template-development-tool.js +271 -0
  98. package/dist/tools/template-overrides-tool.js +294 -0
  99. package/dist/tools/template-productivity-tool.js +319 -0
  100. package/dist/tools/template-tool.js +665 -0
  101. package/dist/tools/test-tool.js +183 -0
  102. package/dist/tools/webhook-tool.js +427 -0
  103. package/dist/tools/widget-tool.js +331 -0
  104. package/dist/tools/wysiwyg-tool.js +137 -0
  105. package/dist/types/scaffold.js +68 -0
  106. package/dist/utils/define-tool.js +37 -0
  107. package/dist/utils/find-tool.js +97 -0
  108. package/dist/utils/mcp-result.js +18 -0
  109. package/dist/utils/pagination.js +27 -0
  110. package/dist/utils/serialization.js +27 -0
  111. package/package.json +94 -0
@@ -0,0 +1,1437 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAddonStructure = getAddonStructure;
4
+ exports.getComponentApi = getComponentApi;
5
+ exports.listComponents = listComponents;
6
+ exports.validateAddon = validateAddon;
7
+ exports.getFieldTypes = getFieldTypes;
8
+ exports.getCodeExample = getCodeExample;
9
+ exports.scaffoldHook = scaffoldHook;
10
+ const schemas_js_1 = require("../data/schemas.js");
11
+ const components_js_1 = require("../data/components.js");
12
+ const hooks_js_1 = require("../data/hooks.js");
13
+ const pagination_js_1 = require("../utils/pagination.js");
14
+ function getAddonStructure(addonType = 'basic') {
15
+ const structure = schemas_js_1.addonStructures[addonType];
16
+ if (!structure) {
17
+ return {
18
+ error: `Тип дополнения "${addonType}" не найден`,
19
+ available_types: Object.keys(schemas_js_1.addonStructures),
20
+ };
21
+ }
22
+ return {
23
+ type: structure.type,
24
+ description: structure.description,
25
+ notes: structure.notes || [],
26
+ available_types: Object.keys(schemas_js_1.addonStructures).map(t => ({
27
+ key: t,
28
+ description: schemas_js_1.addonStructures[t].description,
29
+ })),
30
+ files: structure.files.map(f => ({
31
+ path: f.path,
32
+ required: f.required,
33
+ description: f.description,
34
+ template: f.template,
35
+ })),
36
+ directory_layout: schemas_js_1.controllerDirectoryLayout,
37
+ naming_conventions: schemas_js_1.classNamingConventions,
38
+ db_conventions: {
39
+ table_naming: 'Таблицы: {addon_name}_{entity}. Пример: catalog_items, catalog_categories',
40
+ prefix: 'Префикс из конфига: в install.php используйте $this->db->prefix, в SQL-запросах — {prefix}',
41
+ common_fields: {
42
+ id: 'INT UNSIGNED AUTO_INCREMENT PRIMARY KEY',
43
+ user_id: 'INT UNSIGNED — ID автора',
44
+ date_pub: 'DATETIME — дата публикации',
45
+ is_pub: 'TINYINT(1) — статус (1=опубликован, 0=скрыт)',
46
+ title: 'VARCHAR(255) — заголовок',
47
+ ordering: 'INT UNSIGNED — порядок сортировки (для drag&drop в гридах)',
48
+ },
49
+ },
50
+ url_structure: {
51
+ frontend: '/{addon_name}/{action}/{param}',
52
+ backend: '/admin/{addon_name}/{action}/{param}',
53
+ examples: [
54
+ '/{name}/ → action index (actions/index.php)',
55
+ "/{name}/view/123 → action view (actions/view.php), request->get('id')=123",
56
+ '/admin/{name}/ → backend action index (backend/actions/index.php)',
57
+ '/admin/{name}/items → backend action items (backend/actions/items.php)',
58
+ "/admin/{name}/items/add → external_action_prefix='items_' → backend/actions/items_add.php",
59
+ '/admin/{name}/items/edit/5 → backend/actions/items_add.php с params[0]=5',
60
+ ],
61
+ },
62
+ language_files: {
63
+ location: '/system/languages/{lang}/controllers/{name}/{name}.php',
64
+ note: 'Языковые файлы расположены ВНЕ папки контроллера!',
65
+ examples: [
66
+ '/system/languages/ru/controllers/catalog/catalog.php',
67
+ '/system/languages/en/controllers/catalog/catalog.php',
68
+ ],
69
+ },
70
+ template_files: {
71
+ frontend: '/templates/{theme}/controllers/{name}/{action}.tpl.php',
72
+ backend: '/templates/admincoreui/controllers/{name}/{action}.tpl.php',
73
+ widgets: '/templates/{theme}/controllers/{name}/{widget_name}.tpl.php',
74
+ },
75
+ };
76
+ }
77
+ function getComponentApi(componentName) {
78
+ const lower = componentName.toLowerCase();
79
+ const exact = components_js_1.components.filter(c => c.name.toLowerCase() === lower || c.class.toLowerCase() === lower);
80
+ const partial = components_js_1.components.filter(c => c.name.toLowerCase().includes(lower) || c.class.toLowerCase().includes(lower));
81
+ const rawMatches = exact.length > 0 ? exact : partial;
82
+ const matches = rawMatches.filter((component, index, all) => all.findIndex(item => item.name === component.name && item.class === component.class) ===
83
+ index);
84
+ if (matches.length === 0) {
85
+ return {
86
+ code: 'COMPONENT_NOT_FOUND',
87
+ error: `Компонент "${componentName}" не найден`,
88
+ available: components_js_1.components.map(c => ({
89
+ name: c.name,
90
+ class: c.class,
91
+ description: c.description.slice(0, 80),
92
+ })),
93
+ };
94
+ }
95
+ if (matches.length > 1) {
96
+ return {
97
+ code: 'AMBIGUOUS_COMPONENT',
98
+ error: `Запрос "${componentName}" соответствует нескольким компонентам`,
99
+ candidates: matches.map(c => ({ name: c.name, class: c.class })),
100
+ };
101
+ }
102
+ const [component] = matches;
103
+ return {
104
+ name: component.name,
105
+ class: component.class,
106
+ description: component.description,
107
+ access: component.access,
108
+ source: component.source,
109
+ methods: component.methods,
110
+ };
111
+ }
112
+ function listComponents(pageOptions = {}) {
113
+ const page = (0, pagination_js_1.paginate)(components_js_1.components, pageOptions);
114
+ return {
115
+ total: components_js_1.components.length,
116
+ page: page.page,
117
+ components: page.items.map(c => ({
118
+ name: c.name,
119
+ class: c.class,
120
+ description: c.description,
121
+ methods_count: c.methods.length,
122
+ access: c.access,
123
+ source: c.source,
124
+ })),
125
+ };
126
+ }
127
+ function validateAddon(structure) {
128
+ const normalized = {};
129
+ for (const [originalPath, content] of Object.entries(structure)) {
130
+ const path = originalPath.replace(/\\/g, '/').replace(/^\.\//, '');
131
+ normalized[path] = content;
132
+ const controllerMatch = path.match(/(?:^|\/)controllers\/[^/]+\/(.+)$/);
133
+ if (controllerMatch)
134
+ normalized[controllerMatch[1]] ??= content;
135
+ const languageMatch = path.match(/(?:^|\/)(languages\/.+)$/);
136
+ if (languageMatch)
137
+ normalized[languageMatch[1]] ??= content;
138
+ }
139
+ structure = normalized;
140
+ const errors = [];
141
+ const warnings = [];
142
+ const tips = [];
143
+ const files = Object.keys(structure);
144
+ const addonName = extractAddonName(structure);
145
+ // Обязательные файлы
146
+ const required = ['manifest.xml', 'install.php', 'uninstall.php', 'frontend.php'];
147
+ for (const req of required) {
148
+ if (!files.includes(req)) {
149
+ errors.push(`Отсутствует обязательный файл: ${req}`);
150
+ }
151
+ }
152
+ // Проверки manifest.xml
153
+ if (structure['manifest.xml']) {
154
+ const manifest = structure['manifest.xml'];
155
+ // Базовые теги
156
+ if (!manifest.includes('<name>')) {
157
+ errors.push('manifest.xml: отсутствует тег <name>');
158
+ }
159
+ if (!manifest.includes('<title>')) {
160
+ errors.push('manifest.xml: отсутствует тег <title>');
161
+ }
162
+ if (!manifest.includes('<version>')) {
163
+ warnings.push('manifest.xml: рекомендуется указать <version>');
164
+ }
165
+ if (!manifest.includes('<author>')) {
166
+ warnings.push('manifest.xml: рекомендуется указать <author>');
167
+ }
168
+ if (!manifest.includes('<description>')) {
169
+ warnings.push('manifest.xml: рекомендуется указать <description>');
170
+ }
171
+ // Проверка хуков в manifest
172
+ const manifestHooks = extractHooksFromManifest(manifest);
173
+ if (manifestHooks.length > 0) {
174
+ const validHooks = hooks_js_1.hooks.map(h => h.name);
175
+ for (const hookName of manifestHooks) {
176
+ if (!validHooks.includes(hookName)) {
177
+ warnings.push(`manifest.xml: хук "${hookName}" не найден в системе. Возможно опечатка?`);
178
+ }
179
+ }
180
+ }
181
+ // Проверка permissions
182
+ const permissions = extractPermissionsFromManifest(manifest);
183
+ if (permissions.length > 0) {
184
+ tips.push(`permissions: найдено ${permissions.length} прав (${permissions.join(', ')})`);
185
+ }
186
+ // Проверка зависимостей
187
+ if (manifest.includes('<dependencies>')) {
188
+ tips.push('manifest.xml: есть зависимости от других компонентов');
189
+ }
190
+ }
191
+ // Проверки frontend.php
192
+ if (structure['frontend.php']) {
193
+ const frontend = structure['frontend.php'];
194
+ if (!frontend.includes('cmsFrontend') && !frontend.includes('cmsController')) {
195
+ errors.push('frontend.php: класс должен наследовать cmsFrontend или cmsController');
196
+ }
197
+ if (!frontend.includes('actionIndex') && !frontend.includes('function action')) {
198
+ warnings.push('frontend.php: нет ни одного action-метода (actionIndex и т.д.)');
199
+ }
200
+ // Проверка неймспейса
201
+ if (!frontend.includes('namespace') && !frontend.includes('class ')) {
202
+ warnings.push('frontend.php: рекомендуется использовать неймспейсы');
203
+ }
204
+ }
205
+ // Проверки backend.php
206
+ if (structure['backend.php']) {
207
+ const backend = structure['backend.php'];
208
+ if (!backend.includes('cmsBackend')) {
209
+ errors.push('backend.php: класс должен наследовать cmsBackend');
210
+ }
211
+ }
212
+ // Проверки install.php
213
+ if (structure['install.php']) {
214
+ const install = structure['install.php'];
215
+ if (!install.includes('cmsInstaller')) {
216
+ errors.push('install.php: класс должен наследовать cmsInstaller');
217
+ }
218
+ if (!install.includes('public function install()')) {
219
+ errors.push('install.php: отсутствует метод install()');
220
+ }
221
+ // Проверка создания таблиц
222
+ if (!install.includes('createTable') && !install.includes('query')) {
223
+ warnings.push('install.php: не найдено создание таблиц (createTable или query)');
224
+ }
225
+ }
226
+ // Проверки uninstall.php
227
+ if (structure['uninstall.php']) {
228
+ const uninstall = structure['uninstall.php'];
229
+ if (!uninstall.includes('cmsInstaller')) {
230
+ errors.push('uninstall.php: класс должен наследовать cmsInstaller');
231
+ }
232
+ if (!uninstall.includes('public function uninstall()')) {
233
+ errors.push('uninstall.php: отсутствует метод uninstall()');
234
+ }
235
+ // Проверка удаления таблиц
236
+ if (!uninstall.includes('dropTable')) {
237
+ warnings.push('uninstall.php: не найдено удаление таблиц (dropTable)');
238
+ }
239
+ }
240
+ // Проверки хуков
241
+ const hookFiles = files.filter(f => f.startsWith('hooks/'));
242
+ for (const hookFile of hookFiles) {
243
+ const content = structure[hookFile];
244
+ if (content && !content.includes('extends cmsAction')) {
245
+ errors.push(`${hookFile}: класс хука должен наследовать cmsAction`);
246
+ }
247
+ if (content && !content.includes('public function run(')) {
248
+ errors.push(`${hookFile}: отсутствует метод run($data)`);
249
+ }
250
+ if (content && !content.includes('return $data')) {
251
+ warnings.push(`${hookFile}: метод run() должен возвращать $data`);
252
+ }
253
+ // Извлекаем имя хука из имени файла
254
+ const hookFileName = hookFile.replace('hooks/', '').replace('.php', '');
255
+ const hookMatch = hookFileName.match(/^on(\w+)(.+)$/i);
256
+ if (hookMatch && addonName) {
257
+ const expectedHook = hookMatch[2]
258
+ .replace(/([A-Z])/g, '_$1')
259
+ .toLowerCase()
260
+ .replace(/^_/, '');
261
+ const validHook = hooks_js_1.hooks.find(h => h.name === expectedHook);
262
+ if (validHook) {
263
+ tips.push(`${hookFile}: хук "${expectedHook}" найден в системе`);
264
+ }
265
+ }
266
+ }
267
+ // Проверки форм
268
+ const formFiles = files.filter(f => f.startsWith('forms/'));
269
+ for (const formFile of formFiles) {
270
+ const content = structure[formFile];
271
+ if (content && !content.includes('extends cmsForm')) {
272
+ errors.push(`${formFile}: класс формы должен наследовать cmsForm`);
273
+ }
274
+ if (content && !content.includes('public function init(')) {
275
+ errors.push(`${formFile}: отсутствует метод init() с полями формы`);
276
+ }
277
+ }
278
+ // Проверки модели
279
+ if (structure['model.php']) {
280
+ const model = structure['model.php'];
281
+ if (!model.includes('cmsModel')) {
282
+ warnings.push('model.php: рекомендуется наследовать cmsModel');
283
+ }
284
+ }
285
+ // Проверки языковых файлов
286
+ if (!files.some(f => f.startsWith('languages/'))) {
287
+ warnings.push('Нет языковых файлов в languages/. Рекомендуется добавить languages/ru/lang.php');
288
+ }
289
+ // Проверка версии PHP
290
+ if (structure['manifest.xml'] && structure['manifest.xml'].includes('<php_version>')) {
291
+ const phpMatch = structure['manifest.xml'].match(/<php_version>([^<]+)<\/php_version>/);
292
+ if (phpMatch) {
293
+ const minVersion = phpMatch[1];
294
+ if (compareVersions(minVersion, '7.2.0') < 0) {
295
+ errors.push(`manifest.xml: минимальная версия PHP ${minVersion} ниже поддерживаемой (7.2.0)`);
296
+ }
297
+ }
298
+ }
299
+ // Советы
300
+ if (!files.some(f => f.startsWith('widgets/'))) {
301
+ tips.push('Рассмотрите добавление виджета в widgets/ для размещения на страницах');
302
+ }
303
+ if (!structure['routes.php']) {
304
+ tips.push('Если нужны кастомные URL — добавьте routes.php');
305
+ }
306
+ if (!structure['model.php']) {
307
+ tips.push('Рекомендуется создать model.php для работы с базой данных');
308
+ }
309
+ if (files.some(f => f.endsWith('.php') && !f.includes('/'))) {
310
+ warnings.push('PHP файлы в корневой директории. Все файлы должны быть внутри system/controllers/{addon}/');
311
+ }
312
+ return {
313
+ is_valid: errors.length === 0,
314
+ files_checked: files.length,
315
+ addon_name: addonName || 'не определено',
316
+ errors,
317
+ warnings,
318
+ tips,
319
+ diagnostics: [
320
+ ...errors.map(message => ({ code: diagnosticCode(message), severity: 'error', message })),
321
+ ...warnings.map(message => ({ code: diagnosticCode(message), severity: 'warning', message })),
322
+ ...tips.map(message => ({ code: 'SUGGESTION', severity: 'tip', message })),
323
+ ],
324
+ summary: errors.length === 0
325
+ ? `✓ Дополнение прошло валидацию (${warnings.length} предупреждений, ${tips.length} советов)`
326
+ : `✗ Найдено ${errors.length} ошибок, ${warnings.length} предупреждений, ${tips.length} советов`,
327
+ checked_categories: {
328
+ required_files: required.every(f => files.includes(f)),
329
+ manifest_valid: structure['manifest.xml']
330
+ ? structure['manifest.xml'].includes('<name>') &&
331
+ structure['manifest.xml'].includes('<title>')
332
+ : false,
333
+ hooks_valid: hookFiles.length === 0 ||
334
+ hookFiles.every(f => structure[f]?.includes('extends cmsAction') &&
335
+ structure[f]?.includes('public function run')),
336
+ forms_valid: formFiles.length === 0 ||
337
+ formFiles.every(f => structure[f]?.includes('extends cmsForm') &&
338
+ structure[f]?.includes('public function init')),
339
+ },
340
+ };
341
+ }
342
+ function diagnosticCode(message) {
343
+ if (message.startsWith('Отсутствует обязательный файл:'))
344
+ return 'MISSING_REQUIRED_FILE';
345
+ if (message.includes('должен наследовать'))
346
+ return 'INVALID_BASE_CLASS';
347
+ if (message.includes('отсутствует метод'))
348
+ return 'MISSING_REQUIRED_METHOD';
349
+ if (message.startsWith('manifest.xml: отсутствует'))
350
+ return 'MANIFEST_MISSING_ELEMENT';
351
+ return 'VALIDATION_NOTICE';
352
+ }
353
+ function extractAddonName(structure) {
354
+ if (structure['manifest.xml']) {
355
+ const match = structure['manifest.xml'].match(/<name>([^<]+)<\/name>/);
356
+ if (match)
357
+ return match[1];
358
+ }
359
+ if (structure['frontend.php']) {
360
+ const match = structure['frontend.php'].match(/class\s+(\w+)\s+extends/);
361
+ if (match)
362
+ return match[1].replace('Controller', '').toLowerCase();
363
+ }
364
+ return null;
365
+ }
366
+ function extractHooksFromManifest(manifest) {
367
+ const hooks = [];
368
+ const hookRegex = /<hook[^>]*name=["']([^"']+)["'][^>]*>/gi;
369
+ let match;
370
+ while ((match = hookRegex.exec(manifest)) !== null) {
371
+ hooks.push(match[1]);
372
+ }
373
+ // Также ищем <hook event="...">
374
+ const eventRegex = /<hook[^>]*event=["']([^"']+)["'][^>]*>/gi;
375
+ while ((match = eventRegex.exec(manifest)) !== null) {
376
+ hooks.push(match[1]);
377
+ }
378
+ return hooks;
379
+ }
380
+ function extractPermissionsFromManifest(manifest) {
381
+ const perms = [];
382
+ const permRegex = /<item[^>]*name=["']([^"']+)["'][^>]*>/gi;
383
+ let match;
384
+ while ((match = permRegex.exec(manifest)) !== null) {
385
+ perms.push(match[1]);
386
+ }
387
+ return perms;
388
+ }
389
+ function compareVersions(a, b) {
390
+ const partsA = a.split('.').map(Number);
391
+ const partsB = b.split('.').map(Number);
392
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
393
+ const partA = partsA[i] || 0;
394
+ const partB = partsB[i] || 0;
395
+ if (partA > partB)
396
+ return 1;
397
+ if (partA < partB)
398
+ return -1;
399
+ }
400
+ return 0;
401
+ }
402
+ function getFieldTypes(fieldType) {
403
+ if (fieldType) {
404
+ const ft = schemas_js_1.fieldTypes[fieldType] || schemas_js_1.fieldTypes[`field${fieldType}`];
405
+ if (!ft) {
406
+ return {
407
+ error: `Тип поля "${fieldType}" не найден`,
408
+ available: Object.keys(schemas_js_1.fieldTypes),
409
+ };
410
+ }
411
+ return { name: fieldType, ...ft };
412
+ }
413
+ return {
414
+ total: Object.keys(schemas_js_1.fieldTypes).length,
415
+ field_types: Object.entries(schemas_js_1.fieldTypes).map(([name, info]) => ({
416
+ name,
417
+ description: info.description,
418
+ example: info.example,
419
+ })),
420
+ usage_note: 'Поля используются в классах форм (cmsForm). Экземпляры передаются в массив init()',
421
+ };
422
+ }
423
+ function getCodeExample(task) {
424
+ const lower = task.toLowerCase();
425
+ const examples = [
426
+ // СПИСКИ И ПАГИНАЦИЯ
427
+ {
428
+ keywords: ['список', 'items', 'list', 'страниц', 'paginat', 'index'],
429
+ title: 'Список элементов с пагинацией',
430
+ category: 'pagination',
431
+ explanation: 'Паттерн получения постраничного списка из БД',
432
+ code: `public function actionIndex() {
433
+ $page = $this->request->get('page', 1);
434
+ $perpage = $this->options['perpage'] ?? 10;
435
+
436
+ $this->model->filterEqual('is_pub', 1)->orderBy('date_pub', 'desc');
437
+
438
+ $total = $this->model->getCount('myaddon_items');
439
+ $items = $this->model->limitPage($page, $perpage)->get('myaddon_items');
440
+
441
+ if (!$items && $page > 1) { return cmsCore::error404(); }
442
+
443
+ return $this->cms_template->render('index', [
444
+ 'items' => $items,
445
+ 'total' => $total,
446
+ 'page' => $page,
447
+ 'perpage' => $perpage,
448
+ ]);
449
+ }`,
450
+ },
451
+ {
452
+ keywords: ['один', 'item', 'detail', 'просмотр', 'view'],
453
+ title: 'Просмотр одного элемента',
454
+ category: 'item',
455
+ explanation: 'Паттерн для страницы просмотра элемента',
456
+ code: `public function actionView($id) {
457
+
458
+ $item = $this->model
459
+ ->filterEqual('id', $id)
460
+ ->filterEqual('is_pub', 1)
461
+ ->getItem('myaddon_items');
462
+
463
+ if (!$item) { return cmsCore::error404(); }
464
+
465
+ // Инкримент просмотров
466
+ $this->model->increment('myaddon_items', $id, 'views');
467
+
468
+ return $this->cms_template->render('view', [
469
+ 'item' => $item
470
+ ]);
471
+ }`,
472
+ },
473
+ {
474
+ keywords: ['join', 'связь', 'related', 'users', 'user_id'],
475
+ title: 'Получение элементов с данными пользователя',
476
+ category: 'joins',
477
+ explanation: 'JOIN с таблицей пользователей для получения nickname',
478
+ code: `$items = $this->model
479
+ ->select('i.*')
480
+ ->select('u.nickname as user_nickname', 'u.avatar as user_avatar')
481
+ ->join('users u', 'u.id = i.user_id')
482
+ ->filterEqual('i.is_pub', 1)
483
+ ->orderBy('i.date_pub', 'desc')
484
+ ->limit(20)
485
+ ->get('myaddon_items i');
486
+
487
+ // В шаблоне: $item['user_nickname'], $item['user_avatar']`,
488
+ },
489
+ {
490
+ keywords: ['группировка', 'group', 'count', 'groupBy', 'статистика'],
491
+ title: 'Группировка и подсчёт',
492
+ category: 'aggregation',
493
+ explanation: 'GROUP BY для статистики',
494
+ code: `$stats = $this->model
495
+ ->select('category_id')
496
+ ->select('COUNT(*) as cnt')
497
+ ->groupBy('category_id')
498
+ ->orderBy('cnt', 'desc')
499
+ ->get('myaddon_items');
500
+
501
+ // Результат: [['category_id' => 1, 'cnt' => 15], ...]`,
502
+ },
503
+ // AJAX И JSON
504
+ {
505
+ keywords: ['ajax', 'json', 'fetch', 'запрос', 'api'],
506
+ title: 'Обработка AJAX запроса',
507
+ category: 'ajax',
508
+ explanation: 'Паттерн для AJAX action, возвращающего JSON',
509
+ code: `public function actionSave() {
510
+
511
+ if (!$this->request->isPost() || !$this->request->isAjax()) {
512
+ return $this->renderJSON(['error' => 'Bad request']);
513
+ }
514
+
515
+ $title = $this->request->post('title', '');
516
+
517
+ if (empty($title)) {
518
+ return $this->renderJSON(['error' => 'Заголовок обязателен']);
519
+ }
520
+
521
+ $id = $this->model->insert('myaddon_items', [
522
+ 'title' => $title,
523
+ 'user_id' => $this->cms_user->id,
524
+ 'date_pub' => date('Y-m-d H:i:s'),
525
+ 'is_pub' => 1
526
+ ]);
527
+
528
+ return $this->renderJSON([
529
+ 'success' => true,
530
+ 'id' => $id
531
+ ]);
532
+ }`,
533
+ },
534
+ {
535
+ keywords: ['удалить', 'delete', 'del'],
536
+ title: 'AJAX удаление элемента',
537
+ category: 'ajax',
538
+ explanation: 'Удаление с проверкой прав и AJAX ответом',
539
+ code: `public function actionDelete() {
540
+
541
+ if (!$this->request->isAjax()) {
542
+ return $this->renderJSON(['error' => 'Bad request']);
543
+ }
544
+
545
+ $id = $this->request->get('id', 0);
546
+
547
+ $item = $this->model->getItemByField('myaddon_items', 'id', $id);
548
+ if (!$item) {
549
+ return $this->renderJSON(['error' => 'Элемент не найден']);
550
+ }
551
+
552
+ // Проверка прав
553
+ if ($item['user_id'] !== $this->cms_user->id && !$this->cms_user->is_admin) {
554
+ return $this->renderJSON(['error' => 'Нет прав']);
555
+ }
556
+
557
+ $this->model->delete('myaddon_items', $id);
558
+
559
+ return $this->renderJSON(['success' => true]);
560
+ }`,
561
+ },
562
+ // ФОРМЫ
563
+ {
564
+ keywords: ['форм', 'form', 'валидац', 'add', 'create'],
565
+ title: 'Обработка формы добавления',
566
+ category: 'forms',
567
+ explanation: 'Паттерн для страницы с формой добавления',
568
+ code: `public function actionAdd() {
569
+
570
+ $form = $this->makeForm('form_item');
571
+
572
+ if ($this->request->isPost()) {
573
+
574
+ $item_data = $form->parse($this->request->data('post'));
575
+
576
+ if ($form->validate($item_data)) {
577
+
578
+ $item_data['user_id'] = $this->cms_user->id;
579
+ $item_data['date_pub'] = date('Y-m-d H:i:s');
580
+
581
+ $id = $this->model->insert('myaddon_items', $item_data);
582
+
583
+ return $this->redirect($this->href_to('view', $id));
584
+ }
585
+ }
586
+
587
+ $this->cms_template->setTitle('Добавить элемент');
588
+
589
+ return $this->cms_template->render('add', [
590
+ 'form' => $form->render()
591
+ ]);
592
+ }`,
593
+ },
594
+ {
595
+ keywords: ['edit', 'редактир', 'update', 'изменить'],
596
+ title: 'Форма редактирования',
597
+ category: 'forms',
598
+ explanation: 'Паттерн для редактирования существующего элемента',
599
+ code: `public function actionEdit($id) {
600
+
601
+ $item = $this->model->getItemByField('myaddon_items', 'id', $id);
602
+ if (!$item) { return cmsCore::error404(); }
603
+
604
+ // Проверка прав
605
+ if ($item['user_id'] !== $this->cms_user->id && !$this->cms_user->is_admin) {
606
+ return cmsCore::error403();
607
+ }
608
+
609
+ $form = $this->makeForm('form_item');
610
+
611
+ if ($this->request->isPost()) {
612
+
613
+ $item_data = $form->parse($this->request->data('post'));
614
+
615
+ if ($form->validate($item_data)) {
616
+ $this->model->update('myaddon_items', $id, $item_data);
617
+ return $this->redirect($this->href_to('view', $id));
618
+ }
619
+ }
620
+
621
+ return $this->cms_template->render('edit', [
622
+ 'form' => $form->render($item),
623
+ 'item' => $item
624
+ ]);
625
+ }`,
626
+ },
627
+ // КЭШИРОВАНИЕ
628
+ {
629
+ keywords: ['кэш', 'cache', 'кэшировани'],
630
+ title: 'Кэширование данных',
631
+ category: 'cache',
632
+ explanation: 'Паттерн get-or-set кэширования',
633
+ code: `public function getPopularItems(): array {
634
+
635
+ $cache = cmsCache::getInstance();
636
+ $key = 'myaddon.popular.list';
637
+
638
+ $items = $cache->get($key);
639
+
640
+ if ($items === false) {
641
+ $items = $this->model
642
+ ->filterEqual('is_pub', 1)
643
+ ->orderBy('views', 'desc')
644
+ ->limit(10)
645
+ ->get('myaddon_items');
646
+
647
+ $cache->set($key, $items, 3600);
648
+ }
649
+
650
+ return $items ?: [];
651
+ }`,
652
+ },
653
+ {
654
+ keywords: ['cache set', 'cache get', 'cache clean', 'cmscache'],
655
+ title: 'cmsCache API — set, get, clean',
656
+ category: 'cache',
657
+ explanation: 'Базовые операции с кэшем',
658
+ code: `$cache = cmsCache::getInstance();
659
+
660
+ // SET — записать в кэш (TTL в секундах)
661
+ $cache->set('my_key', $data, 3600); // 1 час
662
+ $cache->set('my_key', $data, 86400); // 1 день
663
+
664
+ // GET — получить из кэша (false если нет)
665
+ $data = $cache->get('my_key');
666
+ if ($data === false) {
667
+ // Ключ не найден или истёк
668
+ $data = $this->loadFromDb();
669
+ $cache->set('my_key', $data, 3600);
670
+ }
671
+
672
+ // CLEAN — очистить кэш по ключу
673
+ $cache->clean('my_key'); // один ключ
674
+ $cache->clean('myaddon.items'); // все ключи начинающиеся с 'myaddon.items'
675
+ $cache->clean(); // ОЧИСТИТЬ ВЕСЬ КЭШ (осторожно!)`,
676
+ },
677
+ {
678
+ keywords: ['cache pause', 'cache resume', 'отключить кэш'],
679
+ title: 'Временное отключение кэша',
680
+ category: 'cache',
681
+ explanation: 'pause/resume для массовых операций',
682
+ code: `// Отключить кэширование во время массовой операции
683
+ $cache = cmsCache::getInstance();
684
+ $cache->pause(); // Кэширование отключено
685
+
686
+ // ... массовая вставка/обновление ...
687
+ foreach ($items as $item) {
688
+ $this->model->insert('myaddon_items', $item);
689
+ }
690
+
691
+ $cache->resume(); // Восстановить кэширование
692
+
693
+ // После resume старые ключи остаются, но будут обновлены при следующем get/set`,
694
+ },
695
+ {
696
+ keywords: ['cache options', 'cache_path', 'cache_ttl'],
697
+ title: 'Настройки кэширования',
698
+ category: 'cache',
699
+ explanation: 'Конфигурация cache_method, cache_ttl',
700
+ code: `// system/config/autoload.php или в cmsConfig:
701
+ $cfg = [
702
+ 'cache_enabled' => true,
703
+ 'cache_method' => 'files', // files, redis, memcache, memcached
704
+ 'cache_ttl' => 3600, // TTL по умолчанию (1 час)
705
+ 'cache_path' => '/tmp/icms_cache/'
706
+ ];
707
+
708
+ // Проверка в коде:
709
+ if (cmsConfig::get('cache_enabled')) {
710
+ $cache = cmsCache::getInstance();
711
+ // ...
712
+ }`,
713
+ },
714
+ {
715
+ keywords: ['cache redis', 'memcache', 'memcached'],
716
+ title: 'Redis/Memcached кэш',
717
+ category: 'cache',
718
+ explanation: 'Использование Redis или Memcached',
719
+ code: `// В config/autoload.php:
720
+ $cfg = [
721
+ 'cache_enabled' => true,
722
+ 'cache_method' => 'redis', // redis, memcache, memcached
723
+ 'cache_ttl' => 7200,
724
+ 'cache_host' => '127.0.0.1',
725
+ 'cache_port' => 6379
726
+ ];
727
+
728
+ // API одинаковый для всех методов:
729
+ $cache = cmsCache::getInstance();
730
+ $cache->set('key', $data, 3600);
731
+ $data = $cache->get('key');
732
+ $cache->clean('key');`,
733
+ },
734
+ {
735
+ keywords: ['cache page', 'page cache', 'старт стоп'],
736
+ title: 'Кэширование страниц',
737
+ category: 'cache',
738
+ explanation: 'start/stop для полного кэширования страницы',
739
+ code: `// В bootstrap.php или начале action:
740
+ cmsCache::getInstance()->start();
741
+
742
+ // ... вся логика страницы ...
743
+
744
+ cmsCache::getInstance()->stop();
745
+
746
+ // Внимание: start/stop работают только если cache_method поддерживает это
747
+ // (не все cacher-ы имеют start/stop: Files — имеет, Redis — нет)`,
748
+ },
749
+ {
750
+ keywords: ['cache dependency', 'depends', 'зависимос'],
751
+ title: 'Инвалидация кэша при изменениях',
752
+ category: 'cache',
753
+ explanation: 'Чистка кэша при добавлении/редактировании/удалении',
754
+ code: `// При добавлении элемента:
755
+ public function actionAdd() {
756
+ // ... сохранение ...
757
+ $id = $this->model->insert('myaddon_items', $data);
758
+
759
+ // Чистим кэш списков
760
+ cmsCache::getInstance()->clean('myaddon.list');
761
+ cmsCache::getInstance()->clean('myaddon.popular');
762
+
763
+ return $this->redirect(...);
764
+ }
765
+
766
+ // При редактировании:
767
+ public function actionEdit($id) {
768
+ // ... обновление ...
769
+ $this->model->update('myaddon_items', $id, $data);
770
+
771
+ // Чистим кэш конкретного элемента и списков
772
+ cmsCache::getInstance()->clean('myaddon.item.' . $id);
773
+ cmsCache::getInstance()->clean('myaddon.list');
774
+
775
+ return $this->redirect(...);
776
+ }
777
+
778
+ // При удалении:
779
+ public function actionDelete($id) {
780
+ $this->model->delete('myaddon_items', $id);
781
+
782
+ cmsCache::getInstance()->clean('myaddon.item.' . $id);
783
+ cmsCache::getInstance()->clean('myaddon.list');
784
+ cmsCache::getInstance()->clean('myaddon.popular');
785
+ }`,
786
+ },
787
+ {
788
+ keywords: ['cache tag', 'key pattern', 'group'],
789
+ title: 'Именование ключей кэша',
790
+ category: 'cache',
791
+ explanation: 'Соглашение об именовании для групповой инвалидации',
792
+ code: `// Соглашение: используйте точки для группировки
793
+ // Формат: {addon}.{entity}.{specific}
794
+
795
+ // Группы:
796
+ myaddon.list — список всех элементов
797
+ myaddon.item.{id} — конкретный элемент
798
+ myaddon.popular — популярные элементы
799
+ myaddon.categories — категории
800
+ myaddon.user.{id} — данные пользователя
801
+
802
+ // Примеры использования:
803
+ $cache->set('myaddon.list', $items, 3600);
804
+ $cache->set('myaddon.item.' . $id, $item, 7200);
805
+ $cache->set('myaddon.popular', $popular, 1800);
806
+
807
+ // Чистка группы:
808
+ $cache->clean('myaddon'); // Чистит ВСЕ ключи myaddon.*`,
809
+ },
810
+ // ХУКИ И СОБЫТИЯ
811
+ {
812
+ keywords: ['хук', 'hook', 'событи', 'event'],
813
+ title: 'Создание и вызов хука',
814
+ category: 'hooks',
815
+ explanation: 'Как добавить свои хуки для расширяемости',
816
+ code: `// 1. Запуск хука:
817
+ $item = cmsEventsManager::hook('myaddon_before_item', $item);
818
+
819
+ // 2. В manifest.xml:
820
+ <hooks>
821
+ <hook event="myaddon_before_item" />
822
+ </hooks>
823
+
824
+ // 3. Реализация хука:
825
+ class onMyaddonBeforeItem extends cmsAction {
826
+ public function run($data) {
827
+ $item = $data['item'];
828
+ $item['custom_field'] = 'processed';
829
+ return $data;
830
+ }
831
+ }`,
832
+ },
833
+ // ПРАВА ДОСТУПА
834
+ {
835
+ keywords: ['прав', 'перм', 'permission', 'acl', 'доступ', 'is_admin'],
836
+ title: 'Проверка прав доступа',
837
+ category: 'permissions',
838
+ explanation: 'Работа с системой прав пользователей',
839
+ code: `public function actionSecure() {
840
+
841
+ if (!$this->cms_user->is_logged) {
842
+ return $this->redirect(href_to('auth', 'login'));
843
+ }
844
+
845
+ if (!$this->cms_user->checkAccess('myaddon', 'can_post')) {
846
+ return $this->cms_template->render('no_access');
847
+ }
848
+
849
+ if (!$this->cms_user->isInGroup('admins')) {
850
+ return cmsCore::error404();
851
+ }
852
+
853
+ return $this->cms_template->render('secure_page');
854
+ }`,
855
+ },
856
+ // ФАЙЛЫ И ИЗОБРАЖЕНИЯ
857
+ {
858
+ keywords: ['файл', 'upload', 'загруз', 'изображ', 'image', 'file'],
859
+ title: 'Загрузка файлов/изображений',
860
+ category: 'files',
861
+ explanation: 'Работа с загрузкой файлов через поле fieldImage',
862
+ code: `// В форме:
863
+ new fieldImage('photo', [
864
+ 'title' => 'Фото',
865
+ 'max_width' => 800,
866
+ 'max_height'=> 600,
867
+ 'max_size' => 2048
868
+ ]),
869
+
870
+ // В контроллере:
871
+ public function actionAdd() {
872
+ $form = $this->makeForm('form_item');
873
+ if ($this->request->isPost()) {
874
+ $item_data = $form->parse($this->request->data('post'));
875
+ // $item_data['photo'] содержит путь к файлу
876
+ }
877
+ }`,
878
+ },
879
+ // RSS
880
+ {
881
+ keywords: ['rss', 'feed', 'лент'],
882
+ title: 'RSS лента',
883
+ category: 'rss',
884
+ explanation: 'Генерация RSS ленты для контента',
885
+ code: `public function actionRss() {
886
+
887
+ $items = $this->model
888
+ ->filterEqual('is_pub', 1)
889
+ ->orderBy('date_pub', 'desc')
890
+ ->limit(20)
891
+ ->get('myaddon_items');
892
+
893
+ $feed = [
894
+ 'title' => cmsConfig::get('sitename') . ' - ' . $this->options['title'],
895
+ 'description' => $this->options['description'],
896
+ 'link' => cmsConfig::get('root_domain'),
897
+ 'language' => 'ru',
898
+ 'items' => []
899
+ ];
900
+
901
+ foreach ($items as $item) {
902
+ $feed['items'][] = [
903
+ 'title' => $item['title'],
904
+ 'link' => href_to($this->name, 'view', $item['id']),
905
+ 'description' => $item['description'],
906
+ 'pubDate' => $item['date_pub']
907
+ ];
908
+ }
909
+
910
+ return $this->cms_template->renderRSS($feed);
911
+ }`,
912
+ },
913
+ // SITEMAP
914
+ {
915
+ keywords: ['sitemap', 'site', 'map', 'поисковик', 'seo'],
916
+ title: 'Генерация sitemap',
917
+ category: 'sitemap',
918
+ explanation: 'Интеграция с модулем sitemap',
919
+ code: `// 1. В manifest.xml:
920
+ <hooks>
921
+ <hook event="sitemap_sources" />
922
+ <hook event="sitemap_urls" />
923
+ </hooks>
924
+
925
+ // 2. hooks/sitemap_sources.php:
926
+ class onMyaddonSitemapSources extends cmsAction {
927
+ public function run($data) {
928
+ $data['sources'][] = [
929
+ 'name' => $this->name,
930
+ 'title' => $this->options['title'],
931
+ 'urls' => href_to($this->name)
932
+ ];
933
+ return $data;
934
+ }
935
+ }
936
+
937
+ // 3. hooks/sitemap_urls.php:
938
+ class onMyaddonSitemapUrls extends cmsAction {
939
+ public function run($data) {
940
+ $items = $this->model
941
+ ->filterEqual('is_pub', 1)
942
+ ->orderBy('date_pub', 'desc')
943
+ ->limit(1000)
944
+ ->get('myaddon_items');
945
+
946
+ foreach ($items as $item) {
947
+ $data['urls'][] = [
948
+ 'loc' => href_to($this->name, 'view', $item['id']),
949
+ 'lastmod' => $item['date_pub'],
950
+ 'priority' => 0.7,
951
+ 'changefreq' => 'weekly'
952
+ ];
953
+ }
954
+ return $data;
955
+ }
956
+ }`,
957
+ },
958
+ // ПОИСК
959
+ {
960
+ keywords: ['search', 'поиск', 'fulltext', 'найти'],
961
+ title: 'Интеграция с поиском',
962
+ category: 'search',
963
+ explanation: 'Добавление контента в поисковый индекс',
964
+ code: `// 1. manifest.xml:
965
+ <hooks>
966
+ <hook event="search_sources" />
967
+ <hook event="search_item" />
968
+ <hook event="delete_item" />
969
+ </hooks>
970
+
971
+ // 2. hooks/search_sources.php:
972
+ class onMyaddonSearchSources extends cmsAction {
973
+ public function run($data) {
974
+ $data['sources'][] = [
975
+ 'name' => $this->name,
976
+ 'url' => href_to($this->name),
977
+ 'item_url' => href_to($this->name, 'view', '%id%')
978
+ ];
979
+ return $data;
980
+ }
981
+ }
982
+
983
+ // 3. hooks/search_item.php:
984
+ class onMyaddonSearchItem extends cmsAction {
985
+ public function run($data) {
986
+ if ($data['ctype_name'] !== $this->name) { return $data; }
987
+
988
+ $item = $this->model->getItemByField('myaddon_items', 'id', $data['item_id']);
989
+ if ($item) {
990
+ $data['title'] = $item['title'];
991
+ $data['content'] = $item['description'] . ' ' . $item['content'];
992
+ }
993
+ return $data;
994
+ }
995
+ }`,
996
+ },
997
+ // ТЕГИ
998
+ {
999
+ keywords: ['tag', 'тег', 'tags'],
1000
+ title: 'Работа с тегами',
1001
+ category: 'tags',
1002
+ explanation: 'Интеграция с системой тегов',
1003
+ code: `// 1. manifest.xml:
1004
+ <hooks>
1005
+ <hook event="tagsdelete" />
1006
+ </hooks>
1007
+
1008
+ // 2. При сохранении элемента - привязка тегов:
1009
+ $tags = $this->request->post('tags', []);
1010
+ cmsCore::getModel('tags')->bindTags($tags, $item_id, $this->name);
1011
+
1012
+ // 3. При удалении - очистка тегов:
1013
+ $this->model->deleteFiltered('tags_items', ['target_id' => $item_id]);`,
1014
+ },
1015
+ // КАТЕГОРИИ
1016
+ {
1017
+ keywords: ['categor', 'категор', 'parent'],
1018
+ title: 'Иерархия категорий',
1019
+ category: 'categories',
1020
+ explanation: 'Работа с вложенными категориями',
1021
+ code: `// Получить дерево категорий:
1022
+ $cats = $this->model->getCategoriesTree($ctype['name']);
1023
+
1024
+ // Формат результата:
1025
+ [
1026
+ ['id' => 1, 'title' => 'Parent', 'level' => 0, 'children' => [
1027
+ ['id' => 2, 'title' => 'Child', 'level' => 1, 'children' => []]
1028
+ ]]
1029
+ ]
1030
+
1031
+ // Получить категорию с родителями:
1032
+ $path = $this->model->getCategoryPath($category_id);
1033
+
1034
+ // ID всех потомков категории:
1035
+ $child_ids = $this->model->getCategoryChildren($category_id);`,
1036
+ },
1037
+ // РЕЙТИНГ
1038
+ {
1039
+ keywords: ['rating', 'рейтинг', 'голос'],
1040
+ title: 'Интеграция с рейтингом',
1041
+ category: 'rating',
1042
+ explanation: 'Добавление голосования к элементам',
1043
+ code: `// 1. manifest.xml:
1044
+ <hooks>
1045
+ <hook event="rating_vote" />
1046
+ </hooks>
1047
+
1048
+ // 2. hooks/rating_vote.php:
1049
+ class onMyaddonRatingVote extends cmsAction {
1050
+ public function run($data) {
1051
+ if ($data['target'] !== $this->name) { return $data; }
1052
+
1053
+ $this->model->increment('myaddon_items', $data['target_id'], 'rating_sum', $data['vote']);
1054
+ $this->model->increment('myaddon_items', $data['target_id'], 'rating_count', 1);
1055
+
1056
+ return $data;
1057
+ }
1058
+ }`,
1059
+ },
1060
+ // КОММЕНТАРИИ
1061
+ {
1062
+ keywords: ['comment', 'коммент'],
1063
+ title: 'Интеграция с комментариями',
1064
+ category: 'comments',
1065
+ explanation: 'Включение комментариев к элементам',
1066
+ code: `// 1. manifest.xml:
1067
+ <hooks>
1068
+ <hook event="comments_count" />
1069
+ </hooks>
1070
+
1071
+ // 2. В типе контента включить комментарии в админке
1072
+
1073
+ // 3. hooks/comments_count.php:
1074
+ class onMyaddonCommentsCount extends cmsAction {
1075
+ public function run($data) {
1076
+ if ($data['ctype_name'] !== $this->name) { return $data; }
1077
+
1078
+ $count = $this->model->getCommentsCount($data['item_id']);
1079
+ $data['comments_count'] = $count;
1080
+ return $data;
1081
+ }
1082
+ }`,
1083
+ },
1084
+ // УВЕДОМЛЕНИЯ
1085
+ {
1086
+ keywords: ['notice', 'уведомл', 'email', 'notification'],
1087
+ title: 'Отправка уведомлений',
1088
+ category: 'notifications',
1089
+ explanation: 'Отправка email и in-site уведомлений',
1090
+ code: `// Email уведомление:
1091
+ cmsCore::sendEmail([
1092
+ 'to' => $user_email,
1093
+ 'subject' => 'Новое уведомление',
1094
+ 'body' => "Привет, {$user_name}!\\n\\n{$message}"
1095
+ ], $this->cms_config);
1096
+
1097
+ // In-site уведомление:
1098
+ cmsCore::getController('messages')->addNotice($user_id, 'myaddon_notification', [
1099
+ 'content' => $message,
1100
+ 'options' => ['link' => href_to($this->name, 'view', $item_id)]
1101
+ ]);`,
1102
+ },
1103
+ // SEO
1104
+ {
1105
+ keywords: ['seo', 'meta', 'title', 'description', 'og'],
1106
+ title: 'SEO метатеги',
1107
+ category: 'seo',
1108
+ explanation: 'Настройка метатегов для страниц',
1109
+ code: `// В action:
1110
+ public function actionView($id) {
1111
+ $item = $this->model->getItemByField('myaddon_items', 'id', $id);
1112
+ if (!$item) { return cmsCore::error404(); }
1113
+
1114
+ $this->cms_template->setPageTitle($item['title']);
1115
+ $this->cms_template->setPageDescription($item['description']);
1116
+ $this->cms_template->addHeadOpenGraph([
1117
+ 'og:title' => $item['title'],
1118
+ 'og:description' => $item['description'],
1119
+ 'og:image' => $item['photo'] ? cmsConfig::get('uploads_url') . '/' . $item['photo'] : '',
1120
+ 'og:url' => href_to($this->name, 'view', $item['id'])
1121
+ ]);
1122
+
1123
+ return $this->cms_template->render('view', ['item' => $item]);
1124
+ }`,
1125
+ },
1126
+ // CRUD COMPLETE
1127
+ {
1128
+ keywords: ['crud', 'create', 'read', 'update', 'delete', 'полный'],
1129
+ title: 'Полный CRUD контроллер',
1130
+ category: 'crud',
1131
+ explanation: 'Шаблон минимального CRUD контроллера',
1132
+ code: `class myaddon extends cmsFrontend {
1133
+
1134
+ // Список
1135
+ public function actionIndex() {
1136
+ $page = $this->request->get('page', 1);
1137
+ $total = $this->model->getCount('myaddon_items');
1138
+ $items = $this->model->limitPage($page, 10)->get('myaddon_items');
1139
+ return $this->cms_template->render('index', ['items' => $items, 'total' => $total]);
1140
+ }
1141
+
1142
+ // Просмотр
1143
+ public function actionView($id) {
1144
+ $item = $this->model->getItemByField('myaddon_items', 'id', $id);
1145
+ if (!$item) { return cmsCore::error404(); }
1146
+ return $this->cms_template->render('view', ['item' => $item]);
1147
+ }
1148
+
1149
+ // Добавление
1150
+ public function actionAdd() {
1151
+ if ($this->request->isPost()) {
1152
+ $id = $this->model->insert('myaddon_items', $this->request->data('post'));
1153
+ return $this->redirect(href_to($this->name, 'view', $id));
1154
+ }
1155
+ return $this->cms_template->render('add');
1156
+ }
1157
+
1158
+ // Удаление
1159
+ public function actionDelete($id) {
1160
+ $this->model->delete('myaddon_items', $id);
1161
+ return $this->redirect(href_to($this->name));
1162
+ }
1163
+ }`,
1164
+ },
1165
+ // МИГРАЦИЯ БД
1166
+ {
1167
+ keywords: ['migration', 'install', 'uninstall', 'sql', 'таблиц'],
1168
+ title: 'Создание таблиц при установке',
1169
+ category: 'migration',
1170
+ explanation: 'Пример install.php с созданием таблицы',
1171
+ code: `class installerMyaddon extends cmsInstaller {
1172
+
1173
+ public function install() {
1174
+
1175
+ // Создание таблицы
1176
+ $this->createTable('myaddon_items', [
1177
+ 'id' => 'INT UNSIGNED AUTO_INCREMENT PRIMARY KEY',
1178
+ 'title' => 'VARCHAR(255) NOT NULL',
1179
+ 'description' => 'TEXT',
1180
+ 'user_id' => 'INT UNSIGNED DEFAULT NULL',
1181
+ 'date_pub' => 'DATETIME DEFAULT NULL',
1182
+ 'is_pub' => 'TINYINT(1) UNSIGNED DEFAULT 1',
1183
+ 'ordering' => 'INT UNSIGNED DEFAULT 0'
1184
+ ], 'Документы');
1185
+
1186
+ // Добавление индекса
1187
+ $this->db->query("CREATE INDEX idx_user ON {myaddon_items} (user_id)");
1188
+
1189
+ return true;
1190
+ }
1191
+
1192
+ public function uninstall() {
1193
+ $this->dropTable('myaddon_items');
1194
+ return true;
1195
+ }
1196
+ }`,
1197
+ },
1198
+ // SECURITY - XSS
1199
+ {
1200
+ keywords: ['xss', 'html', 'escape', 'безопасност', 'sanitize'],
1201
+ title: 'Защита от XSS',
1202
+ category: 'security',
1203
+ explanation: 'Фильтрация HTML и экранирование вывода',
1204
+ code: `// 1. В модели - валидация HTML:
1205
+ public function validateHtml($content) {
1206
+ // Разрешённые теги
1207
+ $allowed = '<p><a><strong><em><ul><ol><li><br>';
1208
+ return strip_tags($content, $allowed);
1209
+ }
1210
+
1211
+ // 2. В шаблоне - экранирование:
1212
+ <?php html($item['content']); ?>
1213
+ <?php htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8'); ?>
1214
+
1215
+ // 3. Не доверяйте пользовательскому вводу:
1216
+ $title = strip_tags($this->request->get('title', ''));
1217
+ $title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');`,
1218
+ },
1219
+ // SECURITY - CSRF
1220
+ {
1221
+ keywords: ['csrf', 'token', 'form', 'csrf_token'],
1222
+ title: 'Защита от CSRF',
1223
+ category: 'security',
1224
+ explanation: 'CSRF токен для форм и AJAX запросов',
1225
+ code: `// 1. В форме - добавление токена:
1226
+ <form method="post">
1227
+ <?php csrf_field(); ?>
1228
+ <input type="text" name="title">
1229
+ </form>
1230
+
1231
+ // 2. В AJAX - отправка токена:
1232
+ $.ajax({
1233
+ url: '/myaddon/save',
1234
+ type: 'POST',
1235
+ data: {
1236
+ title: 'Test',
1237
+ csrf_token: '<?php cmsUser::csrfToken(); ?>'
1238
+ }
1239
+ });
1240
+
1241
+ // 3. В контроллере - проверка:
1242
+ if ($this->request->isPost()) {
1243
+ if (!cmsForm::validateCSRFToken($this->request->get('csrf_token'))) {
1244
+ return $this->renderJSON(['error' => 'Invalid token']);
1245
+ }
1246
+ }`,
1247
+ },
1248
+ // SECURITY - SQL INJECTION
1249
+ {
1250
+ keywords: ['sql', 'injection', 'query', 'базы'],
1251
+ title: 'Защита от SQL Injection',
1252
+ category: 'security',
1253
+ explanation: 'Использование query builder вместо сырых запросов',
1254
+ code: `// ✅ ПРАВИЛЬНО - используйте model:
1255
+ $items = $this->model
1256
+ ->filterEqual('user_id', $user_id) // параметризованный запрос
1257
+ ->get('myaddon_items');
1258
+
1259
+ // ✅ JSON запросы через модель:
1260
+ $id = $this->request->get('id', 0);
1261
+ $item = $this->model->getItemByField('myaddon_items', 'id', (int)$id);
1262
+
1263
+ // ❌ НЕПРАВИЛЬНО - сырой запрос:
1264
+ $query = "SELECT * FROM cms_myaddon WHERE id = " . $_GET['id']; // УЯЗВИМОСТЬ!
1265
+
1266
+ // ✅ Если нужен сырой запрос - используйте параметры:
1267
+ $this->db->query("SELECT * FROM {myaddon_items} WHERE id = %d", $id);`,
1268
+ },
1269
+ // SECURITY - ACCESS CONTROL
1270
+ {
1271
+ keywords: ['access', 'rbac', 'роль', 'admin', 'moderator'],
1272
+ title: 'RBAC - Контроль доступа',
1273
+ category: 'security',
1274
+ explanation: 'Проверка прав по ролям и группам',
1275
+ code: `// 1. Проверка группы:
1276
+ if (!$this->cms_user->isInGroup('admins')) {
1277
+ return cmsCore::error403();
1278
+ }
1279
+
1280
+ // 2. Проверка прав компонента:
1281
+ if (!$this->cms_user->checkAccess('myaddon', 'view')) {
1282
+ return cmsCore::error403();
1283
+ }
1284
+
1285
+ // 3. Проверка владельца:
1286
+ if ($item['user_id'] !== $this->cms_user->id && !$this->cms_user->is_admin) {
1287
+ return cmsCore::error403();
1288
+ }
1289
+
1290
+ // 4. В manifest.xml - регистрация прав:
1291
+ <permissions>
1292
+ <item name="view" title="Просмотр" />
1293
+ <item name="add" title="Добавление" />
1294
+ <item name="edit" title="Редактирование" />
1295
+ <item name="delete" title="Удаление" />
1296
+ </permissions>`,
1297
+ },
1298
+ // SECURITY - FILE UPLOAD
1299
+ {
1300
+ keywords: ['upload', 'file', 'безопасн', 'extension'],
1301
+ title: 'Безопасная загрузка файлов',
1302
+ category: 'security',
1303
+ explanation: 'Проверка типа и размера файла',
1304
+ code: `// 1. Проверка расширения:
1305
+ $allowed = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'];
1306
+ $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
1307
+ if (!in_array($ext, $allowed)) {
1308
+ return ['error' => 'Недопустимый тип файла'];
1309
+ }
1310
+
1311
+ // 2. Проверка MIME типа:
1312
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
1313
+ $mime = finfo_file($finfo, $tmp_name);
1314
+ finfo_close($finfo);
1315
+ $allowed_mime = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
1316
+ if (!in_array($mime, $allowed_mime)) {
1317
+ return ['error' => 'Недопустимый тип файла'];
1318
+ }
1319
+
1320
+ // 3. Проверка размера:
1321
+ $max_size = 5 * 1024 * 1024; // 5MB
1322
+ if ($file_size > $max_size) {
1323
+ return ['error' => 'Файл слишком большой'];
1324
+ }
1325
+
1326
+ // 4. Генерация случайного имени:
1327
+ $new_name = md5(uniqid()) . '.' . $ext;`,
1328
+ },
1329
+ ];
1330
+ const matched = examples.filter(ex => ex.keywords.some(kw => lower.includes(kw)));
1331
+ if (matched.length === 0) {
1332
+ return {
1333
+ message: 'Точный пример не найден. Доступные темы:',
1334
+ available_topics: examples.map(ex => ex.title),
1335
+ categories: [...new Set(examples.map(ex => ex.category))],
1336
+ tip: "Попробуйте: 'список', 'ajax', 'форма', 'кэш', 'права', 'rss', 'sitemap', 'поиск', 'теги', 'рейтинг', 'уведомления', 'crud', 'миграция'",
1337
+ };
1338
+ }
1339
+ return {
1340
+ query: task,
1341
+ found: matched.length,
1342
+ examples: matched.map(ex => ({
1343
+ title: ex.title,
1344
+ category: ex.category,
1345
+ explanation: ex.explanation,
1346
+ code: ex.code,
1347
+ })),
1348
+ };
1349
+ }
1350
+ function scaffoldHook(params) {
1351
+ const { addon_name, hook_name, type = 'action' } = params;
1352
+ // Поиск хука в системе
1353
+ const systemHook = hooks_js_1.hooks.find(h => h.name === hook_name);
1354
+ // Генерация имени класса
1355
+ const className = buildHookClassName(addon_name, hook_name);
1356
+ // Путь к файлу
1357
+ const filePath = `hooks/${hook_name}.php`;
1358
+ // Генерация параметров для run метода
1359
+ let paramsCode = '';
1360
+ let paramsDoc = '';
1361
+ const returnCode = 'return $data;';
1362
+ if (systemHook) {
1363
+ const hookParams = systemHook.parameters;
1364
+ if (hookParams.length > 0 && hookParams[0].name !== '$data') {
1365
+ paramsDoc = hookParams.map(p => ` * @param ${p.type} ${p.name} ${p.description}`).join('\n');
1366
+ paramsCode = hookParams
1367
+ .map(p => {
1368
+ const paramName = p.name.startsWith('$') ? p.name.slice(1) : p.name;
1369
+ return ` ${paramName} = $data['${paramName}'] ?? null`;
1370
+ })
1371
+ .join(',\n');
1372
+ }
1373
+ }
1374
+ // Определение типа возврата
1375
+ const isFilter = systemHook?.type === 'filter' || type === 'filter';
1376
+ // Шаблон кода
1377
+ const code = `<?php
1378
+
1379
+ /**
1380
+ * ${systemHook?.description || `Хук: ${hook_name}`}
1381
+ *
1382
+ ${paramsDoc}
1383
+ * @return ${isFilter ? 'mixed (для filter)' : 'void (для action)'}
1384
+ */
1385
+ class ${className} extends cmsAction {
1386
+
1387
+ public function run(${paramsCode ? '\n' + paramsCode + '\n ' : ''}) {
1388
+
1389
+ ${isFilter
1390
+ ? ` // Фильтр: модифицируйте $data и верните
1391
+ // Пример:
1392
+ // $data['key'] = 'new_value';
1393
+ ${returnCode}`
1394
+ : ` // Action: выполните действия
1395
+ // Не забудьте вернуть $data для цепочки хуков
1396
+ ${returnCode}`}
1397
+ }
1398
+
1399
+ }`.trim();
1400
+ // manifest.xml запись
1401
+ const manifestEntry = `<hook controller="${addon_name}" name="${hook_name}" />`;
1402
+ // Языковой ключ
1403
+ const langKey = `${addon_name.toUpperCase()}_HOOK_${hook_name.toUpperCase().replace(/_/g, '_')}`;
1404
+ return {
1405
+ hook_name,
1406
+ hook_type: isFilter ? 'filter' : 'action',
1407
+ system_hook: systemHook
1408
+ ? {
1409
+ name: systemHook.name,
1410
+ category: systemHook.category,
1411
+ description: systemHook.description,
1412
+ parameters: systemHook.parameters,
1413
+ return_type: systemHook.return_type,
1414
+ }
1415
+ : null,
1416
+ addon_name,
1417
+ class_name: className,
1418
+ file_path: filePath,
1419
+ code,
1420
+ manifest_xml: manifestEntry,
1421
+ lang_key: langKey,
1422
+ usage: {
1423
+ step1: `Добавьте в manifest.xml:\n${manifestEntry}`,
1424
+ step2: `Создайте файл: ${filePath}`,
1425
+ step3: `Добавьте языковую константу в languages/ru/lang.php:\nLANG_${langKey} = 'Описание хука';`,
1426
+ step4: 'Перейдите в Панель управления → Дополнения и включите хук',
1427
+ },
1428
+ };
1429
+ }
1430
+ function buildHookClassName(addonName, hookName) {
1431
+ // Преобразуем snake_case в CamelCase для hookName
1432
+ const hookParts = hookName.split('_');
1433
+ const camelHook = hookParts
1434
+ .map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
1435
+ .join('');
1436
+ return `on${addonName.charAt(0).toUpperCase() + addonName.slice(1)}${camelHook.charAt(0).toUpperCase() + camelHook.slice(1)}`;
1437
+ }