@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,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
|
+
}
|