@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,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scaffoldTest = scaffoldTest;
|
|
4
|
+
function scaffoldTest(opts) {
|
|
5
|
+
const name = opts.addon_name.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
6
|
+
const Name = opts.class_name || name.split('_').map(capitalize).join('');
|
|
7
|
+
const classType = opts.class_type || 'model';
|
|
8
|
+
const files = {};
|
|
9
|
+
if (opts.options?.test_framework === 'codeception') {
|
|
10
|
+
files[`tests/unit/${name}/${Name}Test.php`] = generateCodeceptionTest(name, Name, classType, opts.methods, opts.options);
|
|
11
|
+
files[`tests/unit.suite.yml`] = generateCodeceptionSuite(name);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
files[`tests/phpunit/${Name}Test.php`] = generatePhpUnitTest(name, Name, classType, opts.methods, opts.options);
|
|
15
|
+
files[`phpunit.xml`] = generatePhpUnitConfig(name);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
addon_name: name,
|
|
19
|
+
class_name: Name,
|
|
20
|
+
class_type: classType,
|
|
21
|
+
test_framework: opts.options?.test_framework || 'phpunit',
|
|
22
|
+
files,
|
|
23
|
+
methods_count: opts.methods.length,
|
|
24
|
+
structure_notes: [
|
|
25
|
+
`Тесты: ${Object.keys(files).join(', ')}`,
|
|
26
|
+
`Запуск: ./vendor/bin/phpunit или ./codecept run unit`,
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function capitalize(str) {
|
|
31
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
32
|
+
}
|
|
33
|
+
function generatePhpUnitTest(name, Name, classType, methods, options) {
|
|
34
|
+
const mockDb = options?.mock_db ?? true;
|
|
35
|
+
const mockCache = options?.mock_cache ?? true;
|
|
36
|
+
let code = `<?php
|
|
37
|
+
|
|
38
|
+
use PHPUnit\\Framework\\TestCase;
|
|
39
|
+
|
|
40
|
+
class ${Name}Test extends TestCase {
|
|
41
|
+
|
|
42
|
+
`;
|
|
43
|
+
if (mockDb) {
|
|
44
|
+
code += ` protected $db;
|
|
45
|
+
|
|
46
|
+
protected function setUp(): void {
|
|
47
|
+
parent::setUp();
|
|
48
|
+
$this->db = $this->createMock(\\icms\\db\\DatabaseConnection::class);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
protected function tearDown(): void {
|
|
52
|
+
unset($this->db);
|
|
53
|
+
parent::tearDown();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
if (mockCache) {
|
|
59
|
+
code += ` protected $cache;
|
|
60
|
+
|
|
61
|
+
protected function setUp(): void {
|
|
62
|
+
parent::setUp();
|
|
63
|
+
$this->cache = $this->createMock(\\icms\\cache\\Cache::class);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
for (const method of methods) {
|
|
69
|
+
code += generateMethodTest(name, Name, classType, method);
|
|
70
|
+
}
|
|
71
|
+
code += `}
|
|
72
|
+
`;
|
|
73
|
+
return code;
|
|
74
|
+
}
|
|
75
|
+
function generateMethodTest(name, Name, classType, method) {
|
|
76
|
+
const methodName = method.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
77
|
+
let code = `
|
|
78
|
+
/**
|
|
79
|
+
* @covers ${Name}::${method}
|
|
80
|
+
*/
|
|
81
|
+
public function test${capitalize(methodName)}() {
|
|
82
|
+
`;
|
|
83
|
+
switch (classType) {
|
|
84
|
+
case 'model':
|
|
85
|
+
code += ` $model = new model${Name}();
|
|
86
|
+
$this->assertInstanceOf(\\cmsModel::class, $model);
|
|
87
|
+
`;
|
|
88
|
+
break;
|
|
89
|
+
case 'controller':
|
|
90
|
+
code += ` $controller = $this->createMock(\\cmsController::class);
|
|
91
|
+
$this->assertInstanceOf(\\cmsController::class, $controller);
|
|
92
|
+
`;
|
|
93
|
+
break;
|
|
94
|
+
case 'component':
|
|
95
|
+
code += ` $component = new ${Name}($this->request);
|
|
96
|
+
$this->assertTrue(method_exists($component, '${method}'));
|
|
97
|
+
`;
|
|
98
|
+
break;
|
|
99
|
+
case 'widget':
|
|
100
|
+
code += ` $widget = new ${Name}();
|
|
101
|
+
$this->assertInstanceOf(\\cmsWidget::class, $widget);
|
|
102
|
+
`;
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
code += ` // Test for ${classType}::${method}
|
|
106
|
+
$this->assertTrue(true);
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
code += ` }
|
|
110
|
+
|
|
111
|
+
`;
|
|
112
|
+
return code;
|
|
113
|
+
}
|
|
114
|
+
function generatePhpUnitConfig(name) {
|
|
115
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
116
|
+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
117
|
+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
|
118
|
+
bootstrap="vendor/autoload.php"
|
|
119
|
+
colors="true"
|
|
120
|
+
cacheDirectory=".phpunit.cache"
|
|
121
|
+
>
|
|
122
|
+
<testsuites>
|
|
123
|
+
<testsuite name="${name}">
|
|
124
|
+
<directory>tests/phpunit</directory>
|
|
125
|
+
</testsuite>
|
|
126
|
+
</testsuites>
|
|
127
|
+
<coverage>
|
|
128
|
+
<include>
|
|
129
|
+
<directory suffix=".php">system/controllers/${name}</directory>
|
|
130
|
+
</include>
|
|
131
|
+
</coverage>
|
|
132
|
+
</phpunit>
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
function generateCodeceptionTest(name, Name, classType, methods, options) {
|
|
136
|
+
const mockDb = options?.mock_db ?? true;
|
|
137
|
+
let code = `<?php
|
|
138
|
+
|
|
139
|
+
namespace tests\\unit\\${name};
|
|
140
|
+
|
|
141
|
+
use Codeception\\Test\\Unit;
|
|
142
|
+
|
|
143
|
+
class ${Name}Test extends Unit {
|
|
144
|
+
|
|
145
|
+
`;
|
|
146
|
+
if (mockDb) {
|
|
147
|
+
code += ` /** @var \\UnitTester */
|
|
148
|
+
protected $tester;
|
|
149
|
+
|
|
150
|
+
protected function _before() {
|
|
151
|
+
parent::_before();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected function _after() {
|
|
155
|
+
unset($this->tester);
|
|
156
|
+
parent::_after();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
for (const method of methods) {
|
|
162
|
+
code += `
|
|
163
|
+
/**
|
|
164
|
+
* @covers ${Name}::${method}
|
|
165
|
+
*/
|
|
166
|
+
public function test${capitalize(method)}($I) {
|
|
167
|
+
$I->assertTrue(true);
|
|
168
|
+
}
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
code += `
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
return code;
|
|
175
|
+
}
|
|
176
|
+
function generateCodeceptionSuite(_name) {
|
|
177
|
+
return `actor: UnitTester
|
|
178
|
+
modules:
|
|
179
|
+
enabled:
|
|
180
|
+
- Asserts
|
|
181
|
+
- \\Helper\\Unit
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Webhook scaffolding tool for InstantCMS
|
|
4
|
+
* Generates webhook handlers, queue processing, and security features
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.webhookToolSchema = void 0;
|
|
8
|
+
exports.scaffoldWebhook = scaffoldWebhook;
|
|
9
|
+
const scaffold_1 = require("../types/scaffold");
|
|
10
|
+
/**
|
|
11
|
+
* Generates webhook handler class
|
|
12
|
+
*/
|
|
13
|
+
function generateWebhookHandler(name, Name, events, options) {
|
|
14
|
+
const eventHandlers = events
|
|
15
|
+
.map(event => {
|
|
16
|
+
const eventName = event.replace('.', '_');
|
|
17
|
+
return ` '${event}' => 'handle${eventName}',`;
|
|
18
|
+
})
|
|
19
|
+
.join('\n');
|
|
20
|
+
const eventMethods = events
|
|
21
|
+
.map(event => {
|
|
22
|
+
const eventName = event.replace('.', '_');
|
|
23
|
+
return ` private function handle${eventName}($data) {
|
|
24
|
+
return ['processed' => true, 'event' => '${event}'];
|
|
25
|
+
}`;
|
|
26
|
+
})
|
|
27
|
+
.join('\n\n');
|
|
28
|
+
return `<?php
|
|
29
|
+
// InstantCMS 2. ${name}/webhooks.php
|
|
30
|
+
|
|
31
|
+
class ${Name}Webhook {
|
|
32
|
+
private $config = [];
|
|
33
|
+
private $logger = null;
|
|
34
|
+
|
|
35
|
+
public function __construct() {
|
|
36
|
+
$config_path = cmsConfig::get('root_path') . '/system/config/webhooks/${name}.php';
|
|
37
|
+
if (file_exists($config_path)) {
|
|
38
|
+
$this->config = include $config_path;
|
|
39
|
+
}
|
|
40
|
+
$this->logger = ${Name}WebhookLogger::getInstance();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public function handle($event, $data) {
|
|
44
|
+
if (!in_array($event, $this->config['events'])) {
|
|
45
|
+
return ['success' => false, 'error' => 'Event not configured'];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
$this->logger->log('webhook_received', [
|
|
49
|
+
'event' => $event,
|
|
50
|
+
'data' => $data,
|
|
51
|
+
'timestamp' => date('Y-m-d H:i:s'),
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
$handlers = [
|
|
55
|
+
${eventHandlers}
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
if (!isset($handlers[$event])) {
|
|
59
|
+
return ['success' => false, 'error' => 'No handler for event'];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
$result = $this->{$handlers[$event]}($data);
|
|
64
|
+
|
|
65
|
+
$this->logger->log('webhook_handled', [
|
|
66
|
+
'event' => $event,
|
|
67
|
+
'result' => $result,
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
return ['success' => true, 'result' => $result];
|
|
71
|
+
} catch (Exception $e) {
|
|
72
|
+
$this->logger->log('webhook_error', [
|
|
73
|
+
'event' => $event,
|
|
74
|
+
'error' => $e->getMessage(),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
if (${options.use_retry}) {
|
|
78
|
+
$this->scheduleRetry($event, $data, $e->getMessage());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return ['success' => false, 'error' => $e->getMessage()];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
${eventMethods}
|
|
86
|
+
|
|
87
|
+
private function scheduleRetry($event, $data, $error) {
|
|
88
|
+
$queue = new ${Name}WebhookQueue();
|
|
89
|
+
$queue->add([
|
|
90
|
+
'event' => $event,
|
|
91
|
+
'data' => $data,
|
|
92
|
+
'error' => $error,
|
|
93
|
+
'attempt' => 0,
|
|
94
|
+
'max_attempts' => ${options.retry_count},
|
|
95
|
+
'scheduled_at' => date('Y-m-d H:i:s'),
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public function processQueue() {
|
|
100
|
+
$queue = new ${Name}WebhookQueue();
|
|
101
|
+
$pending = $queue->getPending();
|
|
102
|
+
|
|
103
|
+
foreach ($pending as $item) {
|
|
104
|
+
$this->processQueueItem($item);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return ['processed' => count($pending)];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private function processQueueItem($item) {
|
|
111
|
+
$result = $this->handle($item['event'], $item['data']);
|
|
112
|
+
|
|
113
|
+
if ($result['success']) {
|
|
114
|
+
$item['status'] = 'completed';
|
|
115
|
+
$item['completed_at'] = date('Y-m-d H:i:s');
|
|
116
|
+
} else {
|
|
117
|
+
$item['attempt']++;
|
|
118
|
+
if ($item['attempt'] >= $item['max_attempts']) {
|
|
119
|
+
$item['status'] = 'failed';
|
|
120
|
+
$item['failed_at'] = date('Y-m-d H:i:s');
|
|
121
|
+
} else {
|
|
122
|
+
$item['scheduled_at'] = date('Y-m-d H:i:s', strtotime('+5 minutes'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
$queue = new ${Name}WebhookQueue();
|
|
127
|
+
$queue->update($item);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public static function receive($request) {
|
|
131
|
+
$webhook = new self();
|
|
132
|
+
|
|
133
|
+
$event = $request->get('event', '');
|
|
134
|
+
$data = $request->getAll();
|
|
135
|
+
|
|
136
|
+
unset($data['event']);
|
|
137
|
+
|
|
138
|
+
return $webhook->handle($event, $data);
|
|
139
|
+
}
|
|
140
|
+
}`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Generates webhook configuration file
|
|
144
|
+
*/
|
|
145
|
+
function generateWebhookConfig(name, _Name, events, options) {
|
|
146
|
+
return `<?php
|
|
147
|
+
// InstantCMS 2. system/config/webhooks/${name}.php
|
|
148
|
+
|
|
149
|
+
return [
|
|
150
|
+
'addon' => '${name}',
|
|
151
|
+
'events' => ['${events.join("', '")}'],
|
|
152
|
+
'endpoints' => [
|
|
153
|
+
[
|
|
154
|
+
'url' => '/webhooks/${name}',
|
|
155
|
+
'method' => 'POST',
|
|
156
|
+
'active' => true,
|
|
157
|
+
],
|
|
158
|
+
],
|
|
159
|
+
'options' => [
|
|
160
|
+
'signature' => ${options.use_signature},
|
|
161
|
+
'retry' => ${options.use_retry},
|
|
162
|
+
'retry_count' => ${options.retry_count},
|
|
163
|
+
'async' => ${options.async_execution},
|
|
164
|
+
'secret' => '${name}_webhook_secret_' . md5(cmsConfig::get('secret')),
|
|
165
|
+
],
|
|
166
|
+
'handlers' => [
|
|
167
|
+
${events.map(e => ` '${e}' => true,`).join('\n')}
|
|
168
|
+
],
|
|
169
|
+
];`;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Generates webhook hook handlers
|
|
173
|
+
*/
|
|
174
|
+
function generateWebhookHooks(name, Name, events, _options) {
|
|
175
|
+
const eventHooks = events
|
|
176
|
+
.map(event => {
|
|
177
|
+
return ` public function on${Name}${event.replace('.', '')}() {
|
|
178
|
+
$webhook = new ${Name}Webhook();
|
|
179
|
+
return $webhook->handle('${event}', func_get_args());
|
|
180
|
+
}`;
|
|
181
|
+
})
|
|
182
|
+
.join('\n\n');
|
|
183
|
+
return `<?php
|
|
184
|
+
// InstantCMS 2. system/hooks/${name}/webhook.hooks.php
|
|
185
|
+
|
|
186
|
+
class on${Name}WebhookHook {
|
|
187
|
+
${eventHooks}
|
|
188
|
+
|
|
189
|
+
public function onCronRun() {
|
|
190
|
+
$webhook = new ${Name}Webhook();
|
|
191
|
+
return $webhook->processQueue();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public function onAfterSave($item) {
|
|
195
|
+
$webhook = new ${Name}Webhook();
|
|
196
|
+
$webhook->handle('item.created', $item);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public function onAfterUpdate($item) {
|
|
201
|
+
$webhook = new ${Name}Webhook();
|
|
202
|
+
$webhook->handle('item.updated', $item);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public function onAfterDelete($item) {
|
|
207
|
+
$webhook = new ${Name}Webhook();
|
|
208
|
+
$webhook->handle('item.deleted', $item);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}`;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Generates webhook queue class
|
|
215
|
+
*/
|
|
216
|
+
function generateWebhookQueue(name, Name, _events, _options) {
|
|
217
|
+
return `<?php
|
|
218
|
+
// InstantCMS 2. ${name}/webhook.queue.php
|
|
219
|
+
|
|
220
|
+
class ${Name}WebhookQueue {
|
|
221
|
+
private $table = '${name}_webhook_queue';
|
|
222
|
+
private $db = null;
|
|
223
|
+
|
|
224
|
+
public function __construct() {
|
|
225
|
+
$this->db = cmsDatabase::getInstance();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public function add($item) {
|
|
229
|
+
$item['created_at'] = date('Y-m-d H:i:s');
|
|
230
|
+
$item['status'] = 'pending';
|
|
231
|
+
|
|
232
|
+
return $this->db->insert($this->table, $item);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public function getPending($limit = 100) {
|
|
236
|
+
return $this->db->get('${name}_webhook_queue', function ($item) {
|
|
237
|
+
return $item;
|
|
238
|
+
}, [
|
|
239
|
+
'status' => 'pending',
|
|
240
|
+
'scheduled_at <=' => date('Y-m-d H:i:s'),
|
|
241
|
+
], 'priority ASC, created_at ASC', 1, $limit);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public function getFailed($limit = 100) {
|
|
245
|
+
return $this->db->get('${name}_webhook_queue', function ($item) {
|
|
246
|
+
return $item;
|
|
247
|
+
}, [
|
|
248
|
+
'status' => 'failed',
|
|
249
|
+
], 'created_at DESC', 1, $limit);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public function update($item) {
|
|
253
|
+
return $this->db->update($this->table, $item['id'], $item);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public function delete($id) {
|
|
257
|
+
return $this->db->delete($this->table, $id);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public function getStats() {
|
|
261
|
+
return [
|
|
262
|
+
'pending' => $this->db->getCount($this->table, ['status' => 'pending']),
|
|
263
|
+
'completed' => $this->db->getCount($this->table, ['status' => 'completed']),
|
|
264
|
+
'failed' => $this->db->getCount($this->table, ['status' => 'failed']),
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
public function retry($id) {
|
|
269
|
+
$item = $this->db->getItem($this->table, $id);
|
|
270
|
+
if (!$item) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
$item['status'] = 'pending';
|
|
275
|
+
$item['attempt'] = 0;
|
|
276
|
+
$item['scheduled_at'] = date('Y-m-d H:i:s');
|
|
277
|
+
|
|
278
|
+
return $this->update($item);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
public function purge($days = 30) {
|
|
282
|
+
$before = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
|
283
|
+
return $this->db->query("DELETE FROM {$this->table} WHERE status IN ('completed', 'failed') AND created_at < '{$before}'");
|
|
284
|
+
}
|
|
285
|
+
}`;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Generates webhook security class
|
|
289
|
+
*/
|
|
290
|
+
function generateWebhookSecurity(name, Name, _options) {
|
|
291
|
+
return `<?php
|
|
292
|
+
// InstantCMS 2. ${name}/webhook.security.php
|
|
293
|
+
|
|
294
|
+
class ${Name}WebhookSecurity {
|
|
295
|
+
private $secret = '';
|
|
296
|
+
|
|
297
|
+
public function __construct() {
|
|
298
|
+
$config = include cmsConfig::get('root_path') . '/system/config/webhooks/${name}.php';
|
|
299
|
+
$this->secret = $config['options']['secret'] ?? '';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
public function verifySignature($payload, $signature, $timestamp = null) {
|
|
303
|
+
if (empty($this->secret)) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if ($timestamp !== null) {
|
|
308
|
+
$tolerance = 300;
|
|
309
|
+
if (abs(time() - $timestamp) > $tolerance) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
$expected = $this->generateSignature($payload, $timestamp);
|
|
315
|
+
|
|
316
|
+
return hash_equals($expected, $signature);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
public function generateSignature($payload, $timestamp = null) {
|
|
320
|
+
$timestamp = $timestamp ?? time();
|
|
321
|
+
|
|
322
|
+
if (is_array($payload)) {
|
|
323
|
+
$payload = json_encode($payload);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
$signed_payload = $timestamp . '.' . $payload;
|
|
327
|
+
|
|
328
|
+
return hash_hmac('sha256', $signed_payload, $this->secret);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
public function generateHeaders($payload) {
|
|
332
|
+
$timestamp = time();
|
|
333
|
+
$signature = $this->generateSignature($payload, $timestamp);
|
|
334
|
+
|
|
335
|
+
return [
|
|
336
|
+
'X-Webhook-Signature' => $signature,
|
|
337
|
+
'X-Webhook-Timestamp' => $timestamp,
|
|
338
|
+
'X-Webhook-Event' => '${name}',
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
public function verifyRequest($request) {
|
|
343
|
+
$signature = $request->get('HTTP_X_WEBHOOK_SIGNATURE', '');
|
|
344
|
+
$timestamp = $request->get('HTTP_X_WEBHOOK_TIMESTAMP', 0);
|
|
345
|
+
$payload = $request->getRawData();
|
|
346
|
+
|
|
347
|
+
if (empty($payload)) {
|
|
348
|
+
return ['valid' => false, 'error' => 'Empty payload'];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!$this->verifySignature($payload, $signature, $timestamp)) {
|
|
352
|
+
return ['valid' => false, 'error' => 'Invalid signature'];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return ['valid' => true];
|
|
356
|
+
}
|
|
357
|
+
}`;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Generates a complete webhook system for an InstantCMS addon
|
|
361
|
+
*
|
|
362
|
+
* @param opts - Configuration options for the webhook system
|
|
363
|
+
* @returns Object containing generated files and metadata
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```typescript
|
|
367
|
+
* const result = scaffoldWebhook({
|
|
368
|
+
* addon_name: 'shop',
|
|
369
|
+
* events: ['order.created', 'order.paid', 'order.cancelled'],
|
|
370
|
+
* options: { use_signature: true, use_retry: true }
|
|
371
|
+
* });
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
function scaffoldWebhook(opts) {
|
|
375
|
+
const { lowercase, UpperCamelCase } = (0, scaffold_1.normalizeAddonName)(opts.addon_name);
|
|
376
|
+
const files = {};
|
|
377
|
+
const events = opts.events || ['item.created', 'item.updated', 'item.deleted'];
|
|
378
|
+
const options = {
|
|
379
|
+
use_signature: opts.options?.use_signature ?? true,
|
|
380
|
+
use_retry: opts.options?.use_retry ?? true,
|
|
381
|
+
retry_count: opts.options?.retry_count ?? 3,
|
|
382
|
+
async_execution: opts.options?.async_execution ?? false,
|
|
383
|
+
};
|
|
384
|
+
files[`${lowercase}/webhooks.php`] = generateWebhookHandler(lowercase, UpperCamelCase, events, options);
|
|
385
|
+
files[`${lowercase}/webhook.config.php`] = generateWebhookConfig(lowercase, UpperCamelCase, events, options);
|
|
386
|
+
files[`system/hooks/${lowercase}/webhook.hooks.php`] = generateWebhookHooks(lowercase, UpperCamelCase, events, options);
|
|
387
|
+
if (options.use_retry) {
|
|
388
|
+
files[`${lowercase}/webhook.queue.php`] = generateWebhookQueue(lowercase, UpperCamelCase, events, options);
|
|
389
|
+
}
|
|
390
|
+
if (options.use_signature) {
|
|
391
|
+
files[`${lowercase}/webhook.security.php`] = generateWebhookSecurity(lowercase, UpperCamelCase, options);
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
addon_name: lowercase,
|
|
395
|
+
files,
|
|
396
|
+
events_count: events.length,
|
|
397
|
+
options,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
exports.webhookToolSchema = {
|
|
401
|
+
name: 'scaffold_webhook',
|
|
402
|
+
description: 'Генерация системы веб-хуков для InstantCMS',
|
|
403
|
+
inputSchema: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
properties: {
|
|
406
|
+
addon_name: { type: 'string', description: 'Имя дополнения' },
|
|
407
|
+
events: { type: 'array', items: { type: 'string' }, description: 'События для обработки' },
|
|
408
|
+
options: {
|
|
409
|
+
type: 'object',
|
|
410
|
+
properties: {
|
|
411
|
+
use_signature: { type: 'boolean', description: 'Проверка подписи' },
|
|
412
|
+
use_retry: { type: 'boolean', description: 'Повтор при ошибках' },
|
|
413
|
+
retry_count: { type: 'number', description: 'Количество попыток' },
|
|
414
|
+
async_execution: { type: 'boolean', description: 'Асинхронное выполнение' },
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
required: ['addon_name', 'events'],
|
|
419
|
+
},
|
|
420
|
+
inputExamples: [
|
|
421
|
+
{
|
|
422
|
+
addon_name: 'shop',
|
|
423
|
+
events: ['order.created', 'order.paid', 'order.cancelled'],
|
|
424
|
+
options: { use_signature: true, use_retry: true },
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
};
|