@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,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.generateTraitsMap = generateTraitsMap;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
function parseTraitFile(filePath, namespace) {
|
|
40
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
+
const nameMatch = filePath.match(/([^\/]+)\.php$/);
|
|
42
|
+
if (!nameMatch)
|
|
43
|
+
return null;
|
|
44
|
+
const classMatch = content.match(/trait\s+(\w+)/);
|
|
45
|
+
if (!classMatch)
|
|
46
|
+
return null;
|
|
47
|
+
const traitName = classMatch[1];
|
|
48
|
+
let description;
|
|
49
|
+
const docMatch = content.match(/\*\*[\s\S]*?@/);
|
|
50
|
+
if (docMatch) {
|
|
51
|
+
const descMatch = docMatch[0].match(/@([\s\S]+?)@/);
|
|
52
|
+
if (descMatch) {
|
|
53
|
+
description = descMatch[1]
|
|
54
|
+
.trim()
|
|
55
|
+
.replace(/\n\s*\*/g, '\n')
|
|
56
|
+
.trim();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const methods = [];
|
|
60
|
+
const methodRegex = /(public|protected|private)\s+function\s+(\w+)\s*\(([^)]*)\)/g;
|
|
61
|
+
let methodMatch;
|
|
62
|
+
while ((methodMatch = methodRegex.exec(content)) !== null) {
|
|
63
|
+
const paramsStr = methodMatch[3].trim();
|
|
64
|
+
const params = paramsStr ? paramsStr.split(',').map(p => p.trim()) : [];
|
|
65
|
+
methods.push({
|
|
66
|
+
name: methodMatch[2],
|
|
67
|
+
visibility: methodMatch[1],
|
|
68
|
+
params,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
name: traitName,
|
|
73
|
+
namespace,
|
|
74
|
+
filePath: filePath.replace(process.cwd(), ''),
|
|
75
|
+
methods,
|
|
76
|
+
description,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function scanTraitsDir(dirPath, namespace, traits) {
|
|
80
|
+
if (!fs.existsSync(dirPath))
|
|
81
|
+
return;
|
|
82
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
const newNamespace = namespace ? `${namespace}\\${entry.name}` : entry.name;
|
|
87
|
+
scanTraitsDir(entryPath, newNamespace, traits);
|
|
88
|
+
}
|
|
89
|
+
else if (entry.name.endsWith('.php')) {
|
|
90
|
+
const trait = parseTraitFile(entryPath, namespace);
|
|
91
|
+
if (trait) {
|
|
92
|
+
traits.push(trait);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function generateTraitsMap(sourceDir, outputPath) {
|
|
98
|
+
const traitsDir = path.join(sourceDir, 'system', 'traits');
|
|
99
|
+
const traits = [];
|
|
100
|
+
scanTraitsDir(traitsDir, 'icms', traits);
|
|
101
|
+
const byNamespace = {};
|
|
102
|
+
for (const trait of traits) {
|
|
103
|
+
if (!byNamespace[trait.namespace]) {
|
|
104
|
+
byNamespace[trait.namespace] = [];
|
|
105
|
+
}
|
|
106
|
+
byNamespace[trait.namespace].push(trait);
|
|
107
|
+
}
|
|
108
|
+
const data = {
|
|
109
|
+
traits,
|
|
110
|
+
byNamespace,
|
|
111
|
+
methodCount: traits.reduce((sum, t) => sum + t.methods.length, 0),
|
|
112
|
+
generatedAt: process.env.KNOWLEDGE_GENERATED_AT || new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
const typescriptContent = `// AUTO-GENERATED from source/system/traits
|
|
115
|
+
// Do not edit manually - run 'npm run parse:traits' to regenerate
|
|
116
|
+
|
|
117
|
+
export interface TraitMethod {
|
|
118
|
+
name: string;
|
|
119
|
+
visibility: 'public' | 'protected' | 'private';
|
|
120
|
+
params: string[];
|
|
121
|
+
description?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface TraitInfo {
|
|
125
|
+
name: string;
|
|
126
|
+
namespace: string;
|
|
127
|
+
filePath: string;
|
|
128
|
+
methods: TraitMethod[];
|
|
129
|
+
description?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface TraitsMap {
|
|
133
|
+
traits: TraitInfo[];
|
|
134
|
+
byNamespace: Record<string, TraitInfo[]>;
|
|
135
|
+
methodCount: number;
|
|
136
|
+
generatedAt: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const traitsMap: TraitsMap = ${JSON.stringify(data, null, 2)};
|
|
140
|
+
|
|
141
|
+
export function getTrait(name: string): TraitInfo | undefined {
|
|
142
|
+
return traitsMap.traits.find(t => t.name === name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getTraitMethods(name: string): TraitMethod[] {
|
|
146
|
+
const trait = getTrait(name);
|
|
147
|
+
return trait?.methods || [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getTraitsByNamespace(namespace: string): TraitInfo[] {
|
|
151
|
+
return traitsMap.byNamespace[namespace] || [];
|
|
152
|
+
}
|
|
153
|
+
`;
|
|
154
|
+
fs.writeFileSync(outputPath, typescriptContent);
|
|
155
|
+
console.log(`Generated ${data.traits.length} traits with ${data.methodCount} methods to ${outputPath}`);
|
|
156
|
+
}
|
|
157
|
+
if (require.main === module) {
|
|
158
|
+
const sourcePath = path.resolve(process.env.INSTANTCMS_SOURCE || 'source');
|
|
159
|
+
const outputPath = path.resolve('src/data/traits-map.ts');
|
|
160
|
+
generateTraitsMap(sourcePath, outputPath);
|
|
161
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.generateWidgetsMap = generateWidgetsMap;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
function scanWidgetsDir(dirPath, widgets) {
|
|
40
|
+
if (!fs.existsSync(dirPath))
|
|
41
|
+
return;
|
|
42
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (!entry.isDirectory())
|
|
45
|
+
continue;
|
|
46
|
+
if (entry.name.startsWith('.'))
|
|
47
|
+
continue;
|
|
48
|
+
const widgetPath = path.join(dirPath, entry.name);
|
|
49
|
+
const widgetFile = path.join(widgetPath, 'widget.php');
|
|
50
|
+
const optionsFormFile = path.join(widgetPath, 'options.form.php');
|
|
51
|
+
if (fs.existsSync(widgetFile)) {
|
|
52
|
+
const content = fs.readFileSync(widgetFile, 'utf-8');
|
|
53
|
+
const classMatch = content.match(/class\s+(widget\w+)\s+extends\s+(\w+)/);
|
|
54
|
+
if (classMatch) {
|
|
55
|
+
const className = classMatch[1];
|
|
56
|
+
let controller;
|
|
57
|
+
const controllerMatch = className.match(/^widget([A-Z]\w+)$/);
|
|
58
|
+
if (controllerMatch) {
|
|
59
|
+
controller = controllerMatch[1].toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
let description;
|
|
62
|
+
const docMatch = content.match(/\*\s*([^\n]+)/);
|
|
63
|
+
if (docMatch) {
|
|
64
|
+
description = docMatch[1].trim();
|
|
65
|
+
}
|
|
66
|
+
widgets.push({
|
|
67
|
+
name: entry.name,
|
|
68
|
+
className,
|
|
69
|
+
controller,
|
|
70
|
+
filePath: widgetFile.replace(process.cwd(), ''),
|
|
71
|
+
hasOptionsForm: fs.existsSync(optionsFormFile),
|
|
72
|
+
optionsFormPath: fs.existsSync(optionsFormFile)
|
|
73
|
+
? optionsFormFile.replace(process.cwd(), '')
|
|
74
|
+
: undefined,
|
|
75
|
+
description,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function generateWidgetsMap(sourceDir, outputPath) {
|
|
82
|
+
const widgetsDir = path.join(sourceDir, 'system', 'widgets');
|
|
83
|
+
const widgets = [];
|
|
84
|
+
scanWidgetsDir(widgetsDir, widgets);
|
|
85
|
+
const byName = {};
|
|
86
|
+
const byController = {};
|
|
87
|
+
for (const widget of widgets) {
|
|
88
|
+
byName[widget.name] = widget;
|
|
89
|
+
const ctrl = widget.controller || '_system';
|
|
90
|
+
if (!byController[ctrl]) {
|
|
91
|
+
byController[ctrl] = [];
|
|
92
|
+
}
|
|
93
|
+
byController[ctrl].push(widget);
|
|
94
|
+
}
|
|
95
|
+
const data = {
|
|
96
|
+
widgets,
|
|
97
|
+
byName,
|
|
98
|
+
byController,
|
|
99
|
+
widgetCount: widgets.length,
|
|
100
|
+
generatedAt: process.env.KNOWLEDGE_GENERATED_AT || new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
const typescriptContent = `// AUTO-GENERATED from source/system/widgets
|
|
103
|
+
// Do not edit manually - run 'npm run parse:widgets' to regenerate
|
|
104
|
+
|
|
105
|
+
export interface WidgetOption {
|
|
106
|
+
name: string;
|
|
107
|
+
type: string;
|
|
108
|
+
description?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface WidgetInfo {
|
|
112
|
+
name: string;
|
|
113
|
+
className: string;
|
|
114
|
+
controller?: string;
|
|
115
|
+
filePath: string;
|
|
116
|
+
hasOptionsForm: boolean;
|
|
117
|
+
optionsFormPath?: string;
|
|
118
|
+
description?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface WidgetsMap {
|
|
122
|
+
widgets: WidgetInfo[];
|
|
123
|
+
byName: Record<string, WidgetInfo>;
|
|
124
|
+
byController: Record<string, WidgetInfo[]>;
|
|
125
|
+
widgetCount: number;
|
|
126
|
+
generatedAt: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const widgetsMap: WidgetsMap = ${JSON.stringify(data, null, 2)};
|
|
130
|
+
|
|
131
|
+
export function getWidget(name: string): WidgetInfo | undefined {
|
|
132
|
+
return widgetsMap.byName[name];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getWidgetsByController(controller: string): WidgetInfo[] {
|
|
136
|
+
return widgetsMap.byController[controller] || [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getSystemWidgets(): WidgetInfo[] {
|
|
140
|
+
return widgetsMap.byController['_system'] || [];
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
fs.writeFileSync(outputPath, typescriptContent);
|
|
144
|
+
console.log(`Generated ${data.widgetCount} widgets to ${outputPath}`);
|
|
145
|
+
}
|
|
146
|
+
if (require.main === module) {
|
|
147
|
+
const sourcePath = path.resolve(process.env.INSTANTCMS_SOURCE || 'source');
|
|
148
|
+
const outputPath = path.resolve('src/data/widgets-map.ts');
|
|
149
|
+
generateWidgetsMap(sourcePath, outputPath);
|
|
150
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Permission system scaffolding tool for InstantCMS
|
|
4
|
+
* Generates permission checker, hooks, and admin backend
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.permissionSchema = exports.PermissionTypeEnum = void 0;
|
|
8
|
+
exports.scaffoldPermission = scaffoldPermission;
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const scaffold_1 = require("../types/scaffold");
|
|
11
|
+
/**
|
|
12
|
+
* Available permission types in InstantCMS
|
|
13
|
+
*/
|
|
14
|
+
exports.PermissionTypeEnum = zod_1.z.enum([
|
|
15
|
+
'view',
|
|
16
|
+
'add',
|
|
17
|
+
'edit',
|
|
18
|
+
'delete',
|
|
19
|
+
'publish',
|
|
20
|
+
'moderate',
|
|
21
|
+
'admin',
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Built-in permission rules for InstantCMS content types
|
|
25
|
+
*/
|
|
26
|
+
const PERMISSION_RULES = [
|
|
27
|
+
{ type: 'view', description: 'Просмотр контента', defaultRoles: ['all'] },
|
|
28
|
+
{ type: 'add', description: 'Добавление контента', defaultRoles: ['auth'] },
|
|
29
|
+
{ type: 'edit', description: 'Редактирование своего', defaultRoles: ['auth'] },
|
|
30
|
+
{ type: 'delete', description: 'Удаление своего', defaultRoles: ['auth'] },
|
|
31
|
+
{ type: 'publish', description: 'Публикация', defaultRoles: ['moderator', 'admin'] },
|
|
32
|
+
{ type: 'moderate', description: 'Модерация', defaultRoles: ['moderator', 'admin'] },
|
|
33
|
+
{ type: 'admin', description: 'Администрирование', defaultRoles: ['admin'] },
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Zod schema for permission tool input validation
|
|
37
|
+
*/
|
|
38
|
+
exports.permissionSchema = zod_1.z.object({
|
|
39
|
+
/** System name (lowercase, snake_case) */
|
|
40
|
+
name: zod_1.z
|
|
41
|
+
.string()
|
|
42
|
+
.min(1)
|
|
43
|
+
.max(50)
|
|
44
|
+
.regex(/^[a-z][a-z0-9_]*$/, 'Только lowercase буквы, цифры и подчёркивание'),
|
|
45
|
+
/** Display title */
|
|
46
|
+
title: zod_1.z.string().min(1).max(100),
|
|
47
|
+
/** Optional description */
|
|
48
|
+
description: zod_1.z.string().optional(),
|
|
49
|
+
/** Controller name */
|
|
50
|
+
controller: zod_1.z.string().optional(),
|
|
51
|
+
/** List of permissions to generate */
|
|
52
|
+
permissions: zod_1.z
|
|
53
|
+
.array(zod_1.z.enum(['view', 'add', 'edit', 'delete', 'publish', 'moderate', 'admin']))
|
|
54
|
+
.optional(),
|
|
55
|
+
/** Permission category */
|
|
56
|
+
category: zod_1.z.string().optional(),
|
|
57
|
+
/** Additional options */
|
|
58
|
+
options: zod_1.z
|
|
59
|
+
.object({
|
|
60
|
+
/** Enable category-based permissions */
|
|
61
|
+
withCategories: zod_1.z.boolean().optional().default(false),
|
|
62
|
+
/** Check item ownership for edit/delete */
|
|
63
|
+
withOwnership: zod_1.z.boolean().optional().default(true),
|
|
64
|
+
/** Generate role-based permissions */
|
|
65
|
+
withRoles: zod_1.z.boolean().optional().default(true),
|
|
66
|
+
})
|
|
67
|
+
.optional(),
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* Generates the manifest.json file
|
|
71
|
+
*/
|
|
72
|
+
function generateManifest(name, title, description, controller, permissions, category, options) {
|
|
73
|
+
return `<?php
|
|
74
|
+
// InstantCMS 2. manifest.json
|
|
75
|
+
return [
|
|
76
|
+
'type' => 'content_type',
|
|
77
|
+
'name' => '${name}',
|
|
78
|
+
'title' => '${title}',
|
|
79
|
+
'description' => '${description}',
|
|
80
|
+
'controller' => '${controller}',
|
|
81
|
+
'permissions' => ['${permissions.join("', '")}'],
|
|
82
|
+
'category' => '${category}',
|
|
83
|
+
'version' => '1.0.0',
|
|
84
|
+
'dependencies' => [],
|
|
85
|
+
'options' => [
|
|
86
|
+
'categories' => ${options?.withCategories ?? false},
|
|
87
|
+
'ownership' => ${options?.withOwnership ?? true},
|
|
88
|
+
'roles' => ${options?.withRoles ?? true},
|
|
89
|
+
],
|
|
90
|
+
];`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Generates the permission configuration file
|
|
94
|
+
*/
|
|
95
|
+
function generatePermissionConfig(name, title, description, controller, permissions, category) {
|
|
96
|
+
const permConfigs = PERMISSION_RULES.filter(r => permissions.includes(r.type))
|
|
97
|
+
.map(r => ` '${r.type}' => [
|
|
98
|
+
'title' => '${r.description}',
|
|
99
|
+
'default' => ['${r.defaultRoles.join("', '")}'],
|
|
100
|
+
]`)
|
|
101
|
+
.join(',\n');
|
|
102
|
+
return `<?php
|
|
103
|
+
// InstantCMS 2. system/config/permissions/${name}.php
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
'name' => '${name}',
|
|
107
|
+
'title' => '${title}',
|
|
108
|
+
'description' => '${description}',
|
|
109
|
+
'controller' => '${controller}',
|
|
110
|
+
'category' => '${category}',
|
|
111
|
+
'rules' => [
|
|
112
|
+
${permConfigs}
|
|
113
|
+
],
|
|
114
|
+
];`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Generates the permissions.php checker class
|
|
118
|
+
*/
|
|
119
|
+
function generatePermissionChecker(name, nameCapital, permissions, options) {
|
|
120
|
+
const permStrings = permissions.map(p => `'${p}'`).join(', ');
|
|
121
|
+
return `<?php
|
|
122
|
+
// InstantCMS 2. ${name}/permissions.php
|
|
123
|
+
|
|
124
|
+
class ${nameCapital}Permissions {
|
|
125
|
+
private static $instance = null;
|
|
126
|
+
private $config = [];
|
|
127
|
+
|
|
128
|
+
private function __construct() {
|
|
129
|
+
$config_path = cmsConfig::get('root_path') . '/system/config/permissions/${name}.php';
|
|
130
|
+
if (file_exists($config_path)) {
|
|
131
|
+
$this->config = include $config_path;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public static function getInstance() {
|
|
136
|
+
if (self::$instance === null) {
|
|
137
|
+
self::$instance = new self();
|
|
138
|
+
}
|
|
139
|
+
return self::$instance;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public function can($permission, $user_id = null, $item = null) {
|
|
143
|
+
$user_id = $user_id ?: cmsUser::getInstance()->id;
|
|
144
|
+
|
|
145
|
+
if (!$user_id) {
|
|
146
|
+
return in_array($permission, ['view']);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
$user = cmsModel::getInstance('users')->getUser($user_id);
|
|
150
|
+
if (!$user) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (${options?.withOwnership ?? true} && $item) {
|
|
155
|
+
if ($item['user_id'] == $user_id && in_array($permission, ['edit', 'delete'])) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
$rules = $this->config['rules'][$permission]['default'] ?? [];
|
|
161
|
+
|
|
162
|
+
if ($user['is_admin'] || in_array('admin', $rules)) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return in_array($this->getRoleById($user['role_id']), $rules);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private function getRoleById($role_id) {
|
|
170
|
+
$roles = [
|
|
171
|
+
1 => 'admin',
|
|
172
|
+
2 => 'moderator',
|
|
173
|
+
3 => 'auth',
|
|
174
|
+
4 => 'all',
|
|
175
|
+
];
|
|
176
|
+
return $roles[$role_id] ?? 'all';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public function getAvailablePermissions() {
|
|
180
|
+
return [${permStrings}];
|
|
181
|
+
}
|
|
182
|
+
}`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Generates hook handlers for permission events
|
|
186
|
+
*/
|
|
187
|
+
function generateHookHandlers(name, nameCapital) {
|
|
188
|
+
return `<?php
|
|
189
|
+
// InstantCMS 2. hooks/${name}/permissions.hooks.php
|
|
190
|
+
|
|
191
|
+
class on${nameCapital}PermissionsHook {
|
|
192
|
+
public function onUserAfterLogin($user) {
|
|
193
|
+
$permissions = ${nameCapital}Permissions::getInstance();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public function onContentBeforeSave($item) {
|
|
197
|
+
$permissions = ${nameCapital}Permissions::getInstance();
|
|
198
|
+
|
|
199
|
+
if (!$permissions->can('edit', null, $item)) {
|
|
200
|
+
cmsUser::addSessionMessage('У вас нет прав на редактирование', 'error');
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return $item;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public function onContentBeforeDelete($item) {
|
|
208
|
+
$permissions = ${nameCapital}Permissions::getInstance();
|
|
209
|
+
|
|
210
|
+
if (!$permissions->can('delete', null, $item)) {
|
|
211
|
+
cmsUser::addSessionMessage('У вас нет прав на удаление', 'error');
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return $item;
|
|
216
|
+
}
|
|
217
|
+
}`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Generates admin backend for permissions management
|
|
221
|
+
*/
|
|
222
|
+
function generateAdminBackend(name, nameCapital, permissions) {
|
|
223
|
+
const permStrings = permissions.map(p => `'${p}'`).join(', ');
|
|
224
|
+
return `<?php
|
|
225
|
+
// InstantCMS 2. ${name}/backend/permissions.php
|
|
226
|
+
|
|
227
|
+
class backend${nameCapital}Permissions extends cmsBackend {
|
|
228
|
+
|
|
229
|
+
protected $useOptions = false;
|
|
230
|
+
|
|
231
|
+
public function __construct($request) {
|
|
232
|
+
parent::__construct($request);
|
|
233
|
+
$this->controlName = '${nameCapital}Permissions';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public function actionIndex() {
|
|
237
|
+
$this->renderTemplate('permissions', [
|
|
238
|
+
'permissions' => ${nameCapital}Permissions::getInstance()->getAvailablePermissions(),
|
|
239
|
+
'roles' => $this->model->getRoles(),
|
|
240
|
+
]);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public function actionSave() {
|
|
244
|
+
$data = $this->request->getAll();
|
|
245
|
+
|
|
246
|
+
$config_path = cmsConfig::get('root_path') . '/system/config/permissions/${name}.php';
|
|
247
|
+
|
|
248
|
+
$config = [
|
|
249
|
+
'name' => '${name}',
|
|
250
|
+
'title' => '${nameCapital}',
|
|
251
|
+
'rules' => [],
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
foreach ([${permStrings}] as $perm) {
|
|
255
|
+
$config['rules'][$perm] = [
|
|
256
|
+
'title' => $data['titles'][$perm] ?? '',
|
|
257
|
+
'default' => $data['defaults'][$perm] ?? [],
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
file_put_contents($config_path, '<?php\\nreturn ' . var_export($config, true) . ';');
|
|
262
|
+
|
|
263
|
+
cmsUser::addSessionMessage('Права сохранены', 'success');
|
|
264
|
+
$this->redirectToAction('index');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public function actionCheck() {
|
|
268
|
+
$permission = $this->request->get('permission', '');
|
|
269
|
+
$user_id = $this->request->get('user_id', 0);
|
|
270
|
+
|
|
271
|
+
$permissions = ${nameCapital}Permissions::getInstance();
|
|
272
|
+
$result = $permissions->can($permission, $user_id);
|
|
273
|
+
|
|
274
|
+
$this->halt(200, json_encode(['allowed' => $result]));
|
|
275
|
+
}
|
|
276
|
+
}`;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Generates roles configuration file
|
|
280
|
+
*/
|
|
281
|
+
function generateRoles(name, permissions) {
|
|
282
|
+
return `<?php
|
|
283
|
+
// InstantCMS 2. ${name}/roles.php
|
|
284
|
+
|
|
285
|
+
return [
|
|
286
|
+
'admin' => [
|
|
287
|
+
'title' => 'Администратор',
|
|
288
|
+
'permissions' => ['${permissions.join("', '")}'],
|
|
289
|
+
],
|
|
290
|
+
'moderator' => [
|
|
291
|
+
'title' => 'Модератор',
|
|
292
|
+
'permissions' => ['${permissions.filter((p) => p !== 'admin').join("', '")}'],
|
|
293
|
+
],
|
|
294
|
+
'auth' => [
|
|
295
|
+
'title' => 'Авторизованный',
|
|
296
|
+
'permissions' => ['view', 'add'],
|
|
297
|
+
],
|
|
298
|
+
'all' => [
|
|
299
|
+
'title' => 'Гость',
|
|
300
|
+
'permissions' => ['view'],
|
|
301
|
+
],
|
|
302
|
+
];`;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Generates a complete permission system for an InstantCMS addon
|
|
306
|
+
*
|
|
307
|
+
* @param input - Configuration for the permission system
|
|
308
|
+
* @returns Object containing generated files and metadata
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```typescript
|
|
312
|
+
* const result = scaffoldPermission({
|
|
313
|
+
* name: 'blog_post',
|
|
314
|
+
* title: 'Записи блога',
|
|
315
|
+
* permissions: ['view', 'add', 'edit', 'delete'],
|
|
316
|
+
* options: { withOwnership: true, withRoles: true }
|
|
317
|
+
* });
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
function scaffoldPermission(input) {
|
|
321
|
+
const { lowercase, UpperCamelCase } = (0, scaffold_1.normalizeAddonName)(input.name);
|
|
322
|
+
const title = input.title;
|
|
323
|
+
const description = input.description || '';
|
|
324
|
+
const controller = input.controller || lowercase;
|
|
325
|
+
const permissions = input.permissions || ['view', 'add', 'edit', 'delete'];
|
|
326
|
+
const category = input.category || 'content';
|
|
327
|
+
const options = {
|
|
328
|
+
withCategories: input.options?.withCategories ?? false,
|
|
329
|
+
withOwnership: input.options?.withOwnership ?? true,
|
|
330
|
+
withRoles: input.options?.withRoles ?? true,
|
|
331
|
+
};
|
|
332
|
+
const files = {};
|
|
333
|
+
files[`${lowercase}/manifest.json`] = generateManifest(lowercase, title, description, controller, permissions, category, options);
|
|
334
|
+
files[`system/config/permissions/${lowercase}.php`] = generatePermissionConfig(lowercase, title, description, controller, permissions, category);
|
|
335
|
+
files[`${lowercase}/permissions.php`] = generatePermissionChecker(lowercase, UpperCamelCase, permissions, options);
|
|
336
|
+
files[`system/hooks/${lowercase}/permissions.hooks.php`] = generateHookHandlers(lowercase, UpperCamelCase);
|
|
337
|
+
files[`${lowercase}/backend/permissions.php`] = generateAdminBackend(lowercase, UpperCamelCase, permissions);
|
|
338
|
+
if (options.withRoles) {
|
|
339
|
+
files[`${lowercase}/roles.php`] = generateRoles(lowercase, permissions);
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
addon_name: lowercase,
|
|
343
|
+
title,
|
|
344
|
+
permissions_count: permissions.length,
|
|
345
|
+
options,
|
|
346
|
+
files,
|
|
347
|
+
};
|
|
348
|
+
}
|