Initial commit
This commit is contained in:
55
mcp-server/utils/cmsTargetSafety.js
Normal file
55
mcp-server/utils/cmsTargetSafety.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const SAFE_INTERNAL_HOSTS = new Set(["web", "acai-web", "localhost", "127.0.0.1"]);
|
||||
|
||||
function parseUrl(url, fieldName, context) {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch {
|
||||
throw new Error(`[${context}] Invalid ${fieldName}: ${url || "<empty>"}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertSafeCmsTarget(target, context = "cms") {
|
||||
const publicUrl = typeof target === "string" ? target : (target?.web_url || "");
|
||||
const apiUrl = typeof target === "string" ? target : (target?.api_web_url || "");
|
||||
|
||||
if (!apiUrl) {
|
||||
throw new Error(
|
||||
`[${context}] ACAI_API_WEB_URL is required. Refusing to use ACAI_WEB_URL directly for CMS requests.`
|
||||
);
|
||||
}
|
||||
|
||||
const parsedApiUrl = parseUrl(apiUrl, "ACAI_API_WEB_URL", context);
|
||||
if (!SAFE_INTERNAL_HOSTS.has(parsedApiUrl.hostname)) {
|
||||
throw new Error(
|
||||
`[${context}] Unsafe ACAI_API_WEB_URL host "${parsedApiUrl.hostname}". ` +
|
||||
`Only approved local hosts are allowed: ${Array.from(SAFE_INTERNAL_HOSTS).join(", ")}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!["http:", "https:"].includes(parsedApiUrl.protocol)) {
|
||||
throw new Error(
|
||||
`[${context}] Unsafe ACAI_API_WEB_URL protocol "${parsedApiUrl.protocol}".`
|
||||
);
|
||||
}
|
||||
|
||||
if (publicUrl) {
|
||||
const parsedPublicUrl = parseUrl(publicUrl, "ACAI_WEB_URL", context);
|
||||
const publicIsSafeInternal = SAFE_INTERNAL_HOSTS.has(parsedPublicUrl.hostname);
|
||||
if (!publicIsSafeInternal && parsedPublicUrl.host === parsedApiUrl.host) {
|
||||
throw new Error(
|
||||
`[${context}] ACAI_API_WEB_URL resolves to the same public host as ACAI_WEB_URL (${parsedApiUrl.host}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
publicUrl,
|
||||
apiUrl,
|
||||
forgeHost: typeof target === "string" ? null : (target?.forge_host || null),
|
||||
};
|
||||
}
|
||||
|
||||
export function isSafeInternalHost(hostname) {
|
||||
return SAFE_INTERNAL_HOSTS.has(hostname);
|
||||
}
|
||||
|
||||
195
mcp-server/utils/fieldHelpers.js
Normal file
195
mcp-server/utils/fieldHelpers.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Field and schema manipulation helpers
|
||||
*/
|
||||
|
||||
export const LIST_OPTION_ALIAS_KEYS = ["options", "optionsList", "optionList", "choices", "values", "items"];
|
||||
|
||||
export const optionEntryToLine = (entry) => {
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof entry === "string") {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) return null;
|
||||
if (trimmed.includes("|")) return trimmed.replace(/\r/g, "");
|
||||
return `${trimmed}|${trimmed}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(entry)) {
|
||||
const [valueRaw, labelRaw] = entry;
|
||||
const value = valueRaw ?? labelRaw;
|
||||
const label = labelRaw ?? valueRaw;
|
||||
if (value == null && label == null) return null;
|
||||
const valueStr = `${value ?? ""}`.trim();
|
||||
const labelStr = `${label ?? valueStr}`.trim();
|
||||
if (!valueStr) return null;
|
||||
return `${valueStr}|${labelStr || valueStr}`;
|
||||
}
|
||||
|
||||
if (typeof entry === "object") {
|
||||
const value =
|
||||
entry.value ??
|
||||
entry.id ??
|
||||
entry.key ??
|
||||
entry.slug ??
|
||||
entry.code ??
|
||||
entry.name ??
|
||||
entry.label ??
|
||||
entry.text;
|
||||
const label = entry.label ?? entry.text ?? entry.name ?? entry.title ?? value;
|
||||
if (value == null && label == null) return null;
|
||||
const valueStr = `${value ?? ""}`.trim();
|
||||
const labelStr = `${label ?? valueStr}`.trim();
|
||||
if (!valueStr) return null;
|
||||
return `${valueStr}|${labelStr || valueStr}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const buildOptionsTextFromInput = (input) => {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
return input.map(optionEntryToLine).filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
if (typeof input === "object") {
|
||||
return Object.entries(input)
|
||||
.map(([value, label]) => optionEntryToLine({ value, label }))
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
if (typeof input === "string") {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (trimmed.includes("|")) {
|
||||
return trimmed
|
||||
.replace(/\r/g, "")
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
const hasNewLines = /\r|\n/.test(trimmed);
|
||||
const separator = hasNewLines ? /\r?\n/ : /,/;
|
||||
return trimmed
|
||||
.split(separator)
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean)
|
||||
.map((token) => `${token}|${token}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const normalizeListFieldDefinition = (field = {}) => {
|
||||
if (!field || field.type !== "list") {
|
||||
return field;
|
||||
}
|
||||
|
||||
if (!field.listType) {
|
||||
field.listType = "pulldown";
|
||||
}
|
||||
|
||||
if (!field.optionsType) {
|
||||
field.optionsType = "text";
|
||||
}
|
||||
|
||||
if (field.optionsType === "text") {
|
||||
let source = field.optionsText;
|
||||
|
||||
if (Array.isArray(source) || (source && typeof source === "object" && !Array.isArray(source))) {
|
||||
field.optionsText = buildOptionsTextFromInput(source);
|
||||
} else {
|
||||
let aliasValue;
|
||||
for (const aliasKey of LIST_OPTION_ALIAS_KEYS) {
|
||||
if (field[aliasKey] != null) {
|
||||
aliasValue = field[aliasKey];
|
||||
delete field[aliasKey];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (aliasValue != null) {
|
||||
field.optionsText = buildOptionsTextFromInput(aliasValue);
|
||||
} else if (typeof field.optionsText === "string") {
|
||||
field.optionsText = buildOptionsTextFromInput(field.optionsText);
|
||||
} else {
|
||||
field.optionsText = field.optionsText ?? "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ensure plain text payload for non-text option sources.
|
||||
if (field.optionsText && typeof field.optionsText !== "string") {
|
||||
field.optionsText = "";
|
||||
}
|
||||
for (const aliasKey of LIST_OPTION_ALIAS_KEYS) {
|
||||
if (field[aliasKey] != null) {
|
||||
delete field[aliasKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
export const normalizeSchemaForSave = (schema = {}) => {
|
||||
if (!schema || typeof schema !== "object") {
|
||||
return schema;
|
||||
}
|
||||
|
||||
if (schema.schema && typeof schema.schema === "object") {
|
||||
const normalized = {};
|
||||
for (const [fieldName, fieldDefinition] of Object.entries(schema.schema)) {
|
||||
if (!fieldDefinition || typeof fieldDefinition !== "object") {
|
||||
normalized[fieldName] = fieldDefinition;
|
||||
continue;
|
||||
}
|
||||
const clonedDefinition = { ...fieldDefinition };
|
||||
if (clonedDefinition.type === "list") {
|
||||
normalized[fieldName] = normalizeListFieldDefinition(clonedDefinition);
|
||||
} else {
|
||||
normalized[fieldName] = clonedDefinition;
|
||||
}
|
||||
}
|
||||
schema.schema = normalized;
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
export const mergeTableSchemas = (currentTable, incomingSchema = {}) => {
|
||||
const merged = {
|
||||
...currentTable,
|
||||
schema: { ...(currentTable?.schema || {}) },
|
||||
schemaInfo: { ...(currentTable?.schemaInfo || {}) },
|
||||
};
|
||||
|
||||
if (!incomingSchema || typeof incomingSchema !== "object") {
|
||||
return merged;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(incomingSchema)) {
|
||||
if (key === "schema" && value && typeof value === "object") {
|
||||
merged.schema = { ...(currentTable?.schema || {}), ...value };
|
||||
} else if (key === "schemaInfo" && value && typeof value === "object") {
|
||||
merged.schemaInfo = { ...(currentTable?.schemaInfo || {}), ...value };
|
||||
} else {
|
||||
merged[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
};
|
||||
|
||||
|
||||
60
mcp-server/utils/helpers.js
Normal file
60
mcp-server/utils/helpers.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Utility functions shared across the MCP server
|
||||
*/
|
||||
|
||||
export const encodeBase64 = (value) => Buffer.from(value, "utf-8").toString("base64");
|
||||
|
||||
export const cleanDomainValue = (domain) => {
|
||||
if (!domain) {
|
||||
return null;
|
||||
}
|
||||
return domain.replace(/^https?:\/\//i, "").replace(/\/$/, "").toLowerCase();
|
||||
};
|
||||
|
||||
export const mapDomainsForOutput = (domains = []) =>
|
||||
domains.map((item) => ({
|
||||
num: item.num,
|
||||
domain: item.domain,
|
||||
label: item.domain,
|
||||
isMulti: item.is_multi_child === "1" || item.multiSite === "1",
|
||||
}));
|
||||
|
||||
export const extractTokenHash = (payload) => {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return null;
|
||||
}
|
||||
if (payload.tokenHash) {
|
||||
return payload.tokenHash;
|
||||
}
|
||||
if (payload.token_hash) {
|
||||
return payload.token_hash;
|
||||
}
|
||||
if (payload.data && typeof payload.data === "object") {
|
||||
return extractTokenHash(payload.data);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const pickDomain = (domains = [], domainInput, domainNumInput) => {
|
||||
if (!domains.length) {
|
||||
return null;
|
||||
}
|
||||
if (domainNumInput != null) {
|
||||
const domainNum = String(domainNumInput).trim();
|
||||
const domainByNum = domains.find((entry) => String(entry.num).trim() === domainNum);
|
||||
if (domainByNum) {
|
||||
return domainByNum;
|
||||
}
|
||||
}
|
||||
|
||||
if (!domainInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedDomain = cleanDomainValue(domainInput);
|
||||
return domains.find((entry) => cleanDomainValue(entry.domain) === normalizedDomain);
|
||||
};
|
||||
|
||||
export const getSessionKey = (username, password) => `${username}::${password}`;
|
||||
|
||||
|
||||
4
mcp-server/utils/index.js
Normal file
4
mcp-server/utils/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './helpers.js';
|
||||
export * from './fieldHelpers.js';
|
||||
|
||||
|
||||
436
mcp-server/utils/mixins/builderdata.js
Normal file
436
mcp-server/utils/mixins/builderdata.js
Normal file
@@ -0,0 +1,436 @@
|
||||
// Helper function for base64 encoding (works in both browser and Node.js)
|
||||
const btoa = typeof window !== 'undefined' ? btoa : (str) => Buffer.from(str).toString('base64');
|
||||
|
||||
export const builderData = {
|
||||
"data-field-type" : {
|
||||
type:"ATTRIBUTE",
|
||||
description:`Determina que el elemento es editable desde el Builder para los clientes. Se puede añadir un elemento multi que da la posibilidad de añadir distintos bloques a los clientes. Dentro de cada multi se podrán poner campos de edición
|
||||
<br><br>Nota : Los componentes de estos ejemplos pueden variar dependiendo del analizador léxico utilizado.`,
|
||||
example : `<div data-field-type="textfield" data-field-label="Label" >Elemento editable</div>
|
||||
|
||||
<div data-field-type="headfield" data-field-label="Titulo" >Elemento editable</div>
|
||||
|
||||
<div data-field-type="link" data-field-label="Enlace">Elemento editable</div>
|
||||
|
||||
<div data-field-type="textbox" data-field-label="Texto Largo">Elemento editable</div>
|
||||
|
||||
<div data-field-type="wysiwyg" data-field-label="Texto Largo enriquecido">Elemento editable</div>
|
||||
|
||||
<img data-field-type="upload" data-field-rand="true" data-field-label="Image" data-field-info1="titulo" data-field-width="ancho maximo en pixeles (opcional. Ej 1400)"\>
|
||||
|
||||
<div data-field-type="uploadBackground" data-field-label="Imagen de fondo">Elemento editable</div>
|
||||
|
||||
<div data-field-type="list" data-field-label="pruebas" data-list-options="opcion1,opcion2,|option3,4|opcion 4" ></div>
|
||||
<div data-field-type="list" data-field-label="pruebas" data-list-table="nombredeTabla" data-list-value="campoValor" data-list-label="campoLabel">
|
||||
{{record.name}}
|
||||
</div>
|
||||
|
||||
<div data-field-type="list" data-field-label="Apartados" data-list-query="select num,name from cms_otros_contenidos" data-list-multi>
|
||||
<div v-for="contenido in otros_contenidos" v-where="num=$record">{{contenido.name}} </div>
|
||||
</div>
|
||||
|
||||
Si se desea que sea multi añadir el atributo data-list-multi
|
||||
|
||||
<div data-field-type="list" data-list-multi></div>
|
||||
|
||||
<div data-field-type="uploadMulti" data-field-label="Imagenes" data-field-info1="titulo" >
|
||||
<img src="{{uploadMulti.urlPath | imagec(700)}}"\>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
NOTA : El Multi debe tener un nodo padre ( sin hermanos ).
|
||||
<li data-field-type="multiv2" data-field-label="Records">
|
||||
<div data-field-type="textfield" data-field-label="Label" >Elemento editable</div>
|
||||
<div data-field-type="textbox" data-field-label="Texto Largo">Elemento editable</div>
|
||||
<img data-field-type="upload" data-field-info1="titulo" data-field-label="Image" data-field-width="ancho maximo en pixeles (opcional. Ej 1400)"\>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
`,
|
||||
shortcode : ``,
|
||||
directLinks : {
|
||||
TextField : `<p c-if="label" data-field-type="textfield" data-field-label="Label" >Elemento editable</p>`,
|
||||
HeadField : `<p c-if="titulo" data-field-type="headfield" data-field-label="Titulo" >Elemento editable</p>`,
|
||||
'List (Options)' : `<div data-field-type="list" data-field-label="pruebas" data-list-options="opcion1,opcion2,|option3,4|opcion 4" ></div>`,
|
||||
'List (Table)' : `<div data-field-type="list" data-field-label="pruebas" data-list-table="nombredeTabla" data-list-value="campoValor" data-list-label="campoLabel">
|
||||
{{record.name}}
|
||||
</div>`,
|
||||
Link : `<a c-if="enlace_anchor" data-field-type="link" data-field-label="Enlace">Elemento editable</a>`,
|
||||
TextBox : `<div c-if="textolargo" data-field-type="textbox" data-field-label="Texto Largo">Elemento editable</div>`,
|
||||
Wysiwyg : `<div c-if="textolargoenriquecido" class="wysiwyg" data-field-type="wysiwyg" data-field-label="Texto Largo enriquecido">Elemento editable</div>`,
|
||||
Upload : `<div c-if="imagen.0.urlPath" class="p-1/6 relative"><img class="absolute top-0 left-0 w-full h-full object-cover object-center lazyload" data-field-type="upload" data-field-label="Imagen" data-lazy="true" data-field-info1="titulo" data-field-width="1400" alt=""\></div>`,
|
||||
UploadBackground : `<div c-if="imagendefondo.0.urlPath" class="bg-cover bg-center bg-no-repeat" data-field-type="uploadBackground" data-field-info1="titulo" data-field-label="Imagen de fondo">Elemento editable</div>`,
|
||||
UploadMulti : `<div c-if="imagenes" data-field-type="uploadMulti" data-field-label="Imagenes" data-field-info1="titulo" >
|
||||
<img src="{{uploadMulti.urlPath | imagec(700)}}"\>
|
||||
</div>`,
|
||||
Multi : `<ul>
|
||||
<li data-field-type="multiv2" data-field-label="Records">
|
||||
<div data-field-type="textfield" data-field-label="Label">Elemento editable</div>
|
||||
<div data-field-type="textbox" data-field-label="Texto Largo">Elemento editable</div>
|
||||
<div class="p-1/6 relative"><img class="absolute top-0 left-0 w-full h-full object-cover object-center lazyload" data-field-type="upload" data-field-label="Imagen" data-lazy="true" data-field-info1="titulo" data-field-width="1400" alt=""\></div>
|
||||
</li>
|
||||
</ul>`,
|
||||
},
|
||||
shortcuts: {
|
||||
'C-Form' : `<c-form tableName="" sendToClient="string campo que se usará como cliente" sendTo="string correos separados por coma" honeypot="true" captcha="true" mailRecord="['correos','SOLICITUD']" class="">
|
||||
</c-form>`,
|
||||
Hook: `<hook endpoint="/hooks/nombre_del_hook/" result="almacen_del_resultado" :variable="variable" :variable_string="'mi string'"></hook>`,
|
||||
Dump: `<pre style="display:none">{{dump(thisrecord)}}</pre>`,
|
||||
TextoGeneral: `{{'' | translate}}`
|
||||
},
|
||||
replace : (el,prefixVar) => {
|
||||
// ACAI ANALYZER
|
||||
let attr = el.getAttribute("data-field-type");
|
||||
if (!attr) return el.outerHTML;
|
||||
el.removeAttribute("data-field-type");
|
||||
|
||||
|
||||
let label = el.getAttribute("data-field-label");
|
||||
if (!label) {
|
||||
label = "Untitled " + new Date().getTime();
|
||||
}else{
|
||||
el.removeAttribute("data-field-label");
|
||||
}
|
||||
|
||||
let width = el.getAttribute("data-field-width");
|
||||
if (!width){
|
||||
width = 1600;
|
||||
}else{
|
||||
el.removeAttribute("data-field-width");
|
||||
}
|
||||
|
||||
let infos = [];
|
||||
for (let i=1;i<5;i++){
|
||||
var info = el.getAttribute("data-field-info"+i);
|
||||
if (info){
|
||||
infos.push(info);
|
||||
el.removeAttribute("data-field-info"+i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const field = prefixVar ? `${prefixVar}["${appParser.cleanString(label)}"]` : `$${appParser.cleanString(label)}`;
|
||||
const field_anchor = prefixVar ? `${prefixVar}["${appParser.cleanString(label)}_anchor"]` : `$${appParser.cleanString(label)}_anchor`;
|
||||
const field_tag = prefixVar ? `${prefixVar}["${appParser.cleanString(label)}_tag"]` : `$${appParser.cleanString(label)}_tag`;
|
||||
|
||||
let rand = el.getAttribute("data-field-rand");
|
||||
|
||||
if (!rand){
|
||||
rand = ``;
|
||||
}else{
|
||||
rand = `<? shuffle(${field});?>`;
|
||||
}
|
||||
|
||||
switch(attr){
|
||||
case "multiv2":
|
||||
let php1 = '|*' + btoa(`<? foreach(${field} as $index => $record){ ?>`) + '*|';
|
||||
let php2 = '|*' + btoa(`<? } ?>`) + '*|';
|
||||
let string = `${php1}${appParser.parseComponents(el.outerHTML,`$record`)}${php2}`;
|
||||
el.outerHTML = string;
|
||||
break;
|
||||
case "link":
|
||||
if (el.tagName!='A'){
|
||||
let php1 = '|*' + btoa(`<? echo ${field}; ?>`) + '*|';
|
||||
let php2 = '|*' + btoa(`<? echo ${field_anchor}; ?>`) + '*|';
|
||||
el.innerHTML = el.hasChildNodes() && Array.from(el.childNodes).filter(node => node.nodeType !== 3).length ? `<a href='${php1}'>${appParser.parseComponents(el.innerHTML,prefixVar)}</a>` : `<a href='${php1}'>${php2}</a>`;
|
||||
}else{
|
||||
el.setAttribute('href','|*' + btoa(`<? echo @${field}; ?>`) + '*|');
|
||||
el.innerHTML = el.hasChildNodes() && Array.from(el.childNodes).filter(node => node.nodeType !== 3).length ? appParser.parseComponents(el.innerHTML,prefixVar) : '|*' + btoa(`<? echo @${field_anchor};?>`) + '*|';
|
||||
}
|
||||
break;
|
||||
case "uploadMulti":
|
||||
let php1up = '|*' + btoa(`${rand}<? foreach(${field} as $index => $uploadMulti){ ?>`) + '*|';
|
||||
let php2up = '|*' + btoa(`<? } ?>`) + '*|';
|
||||
let resultVariables = appParser.parseVariables2(el.outerHTML);
|
||||
let preStringVars = resultVariables[0];
|
||||
let stringVars = resultVariables[1];
|
||||
let stringup = `${php1up}${preStringVars}${stringVars}${php2up}`;
|
||||
el.outerHTML = stringup;
|
||||
break;
|
||||
case "list":
|
||||
const isTable = el.hasAttribute("data-list-table");
|
||||
|
||||
const tableSelect = el.getAttribute("data-list-table");
|
||||
const valueSelect = el.getAttribute("data-list-value");
|
||||
const labelSelect = el.getAttribute("data-list-label");
|
||||
const querySelect = el.getAttribute("data-list-query");
|
||||
|
||||
el.removeAttribute("data-list-table");
|
||||
el.removeAttribute("data-list-options");
|
||||
el.removeAttribute("data-list-value");
|
||||
el.removeAttribute("data-list-label");
|
||||
el.removeAttribute("data-list-multi");
|
||||
el.removeAttribute("data-list-query");
|
||||
|
||||
let php1li = '|*' + btoa(`<? ${field} = array_filter(explode("\t",${field}));if (isset($record)) $auxRecord = $record; foreach(${field} as $index => $record){ ?>`) + '*|';
|
||||
if (isTable) php1li += '|*' + btoa(`<? $schema = loadSchema("${tableSelect}"); if (@$schema) {$record = @dame_registros("${tableSelect}","${valueSelect}='$record'","num desc",1)[0];}else{ global $TABLE_PREFIX; $record = @mysql_query_fetch_all_assoc("SELECT * FROM ".$TABLE_PREFIX."${tableSelect} WHERE ${valueSelect}='$record' LIMIT 1")[0]; } if (!$record) continue;?>`) + '*|';
|
||||
|
||||
let php2li = '|*' + btoa(`<? }
|
||||
if (isset($auxRecord)) $record = $auxRecord;?>`) + '*|';
|
||||
let stringli = `${php1li}${appParser.parseComponents(el.outerHTML,`$record`)}${php2li}`;
|
||||
el.outerHTML = stringli;
|
||||
|
||||
|
||||
break;
|
||||
case "upload":
|
||||
if (el.hasAttribute("src")){
|
||||
el.setAttribute('src','|*' + btoa(`${rand}<? echo CustomCode::imagec(${width},${field}[0]['urlPath']);?>`) + '*|');
|
||||
}else{
|
||||
// let classString = el.getAttribute("class");
|
||||
// let styleString = el.getAttribute("style");
|
||||
// let altString = el.getAttribute("alt");
|
||||
var srcAttr = "src";
|
||||
var output = "";
|
||||
const attrs = el.attributes;
|
||||
for(var i = attrs.length - 1; i >= 0; i--) {
|
||||
if (attrs[i].name.toLowerCase() == "src") continue;
|
||||
if (attrs[i].name.toLowerCase() == "data-lazy") { srcAttr="data-src"; continue;}
|
||||
output += `${attrs[i].name}="${attrs[i].value}" `;
|
||||
}
|
||||
console.log({output:output});
|
||||
let php = '|*' + btoa(`${rand}<? echo CustomCode::imagec(${width},${field}[0]['urlPath']); ?>`) + '*|';
|
||||
el.outerHTML = `<img ${srcAttr}='${php}' ${output}>`;
|
||||
|
||||
}
|
||||
break;
|
||||
case "uploadBackground":
|
||||
let php3 = '|*' + btoa(`${rand}<? echo CustomCode::imagec(${width},${field}[0]['urlPath']); ?>`) + '*|';
|
||||
el.setAttribute('style',`background-image:url('${php3}')`);
|
||||
break;
|
||||
case "headfield":
|
||||
let outputh = "";
|
||||
|
||||
const attrs = el.attributes;
|
||||
for(var i = attrs.length - 1; i >= 0; i--) {
|
||||
outputh += `${attrs[i].name}="${attrs[i].value}" `;
|
||||
}
|
||||
|
||||
let phph1 = '|*' + btoa(`<? echo ${field}; ?>`) + '*|';
|
||||
let phph2 = '|*' + btoa(`<? echo '<'.${field_tag}.' ${outputh}>'; ?>`) + '*|';
|
||||
let phph3 = '|*' + btoa(`<? echo '</'.${field_tag}.'>'; ?>`) + '*|';
|
||||
|
||||
el.outerHTML = `<span>${phph2} ${phph1} ${phph3}</span>`;
|
||||
|
||||
break;
|
||||
default:
|
||||
//el.classList.add(`wed_${field.replace(/\$/g,"")}:${attr}`);
|
||||
el.innerHTML = '|*' + btoa(`<? echo @${field} ? nl2br(${field}) : '${el.innerHTML}';?>`) + '*|';
|
||||
}
|
||||
return el;
|
||||
|
||||
},
|
||||
replace2 : (el,prefixVar) => {
|
||||
// TWIG ANALYZER
|
||||
let attr = el.getAttribute("data-field-type");
|
||||
if (!attr) return el.outerHTML;
|
||||
el.removeAttribute("data-field-type");
|
||||
|
||||
|
||||
let label = el.getAttribute("data-field-label");
|
||||
if (!label) {
|
||||
label = "Untitled " + new Date().getTime();
|
||||
}else{
|
||||
el.removeAttribute("data-field-label");
|
||||
}
|
||||
|
||||
let width = el.getAttribute("data-field-width");
|
||||
if (!width){
|
||||
width = 1600;
|
||||
}else{
|
||||
el.removeAttribute("data-field-width");
|
||||
}
|
||||
|
||||
let infos = [];
|
||||
for (let i=1;i<5;i++){
|
||||
var info = el.getAttribute("data-field-info"+i);
|
||||
if (info){
|
||||
infos.push(info);
|
||||
el.removeAttribute("data-field-info"+i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const field = prefixVar ? `${prefixVar}.${appParser.cleanString(label)}` : `${appParser.cleanString(label)}`;
|
||||
const field_anchor = prefixVar ? `${prefixVar}.${appParser.cleanString(label)}_anchor` : `${appParser.cleanString(label)}_anchor`;
|
||||
const field_tag = prefixVar ? `${prefixVar}.${appParser.cleanString(label)}_tag` : `${appParser.cleanString(label)}_tag`;
|
||||
|
||||
let rand = el.getAttribute("data-field-rand");
|
||||
|
||||
if (!rand){
|
||||
rand = ``;
|
||||
}else{
|
||||
rand = `{% ${field} = ${field} | shuffle %}\n`;
|
||||
}
|
||||
|
||||
switch(attr){
|
||||
case "multiv2":
|
||||
let php1 = `\n{% for record in ${field} %} {% set index = loop.index0 %}\n`;
|
||||
let php2 = `\n{% endfor %}\n`;
|
||||
|
||||
let string = `${php1}${appParser.parseComponents(el.outerHTML,`record`,2)}${php2}`;
|
||||
el.outerHTML = string;
|
||||
break;
|
||||
case "link":
|
||||
if (el.tagName!='A'){
|
||||
let php1 = `{{ ${field} }}`;
|
||||
let php2 = `{{ ${field_anchor} }}`;
|
||||
el.innerHTML = el.hasChildNodes() && Array.from(el.childNodes).filter(node => node.nodeType !== 3).length ? `<a href='${php1}'>${appParser.parseComponents(el.innerHTML,prefixVar)}</a>` : `<a href='${php1}'>${php2}</a>`;
|
||||
}else{
|
||||
el.setAttribute('href',`{{ ${field} }}`);
|
||||
el.innerHTML = el.hasChildNodes() && Array.from(el.childNodes).filter(node => node.nodeType !== 3).length ? appParser.parseComponents(el.innerHTML,prefixVar) : `{{ ${field_anchor} }}`;
|
||||
}
|
||||
break;
|
||||
case "uploadMulti":
|
||||
let php1up = `\n ${rand} \n {% for uploadMulti in ${field} %} \n {% set index = loop.index0 %} \n`;
|
||||
let php2up = `\n {% endfor %} \n `;
|
||||
|
||||
let stringup = `${php1up}${el.outerHTML}${php2up}`;
|
||||
el.outerHTML = stringup;
|
||||
break;
|
||||
case "list":
|
||||
const isTable = el.hasAttribute("data-list-table");
|
||||
|
||||
const tableSelect = el.getAttribute("data-list-table");
|
||||
const valueSelect = el.getAttribute("data-list-value");
|
||||
const labelSelect = el.getAttribute("data-list-label");
|
||||
const querySelect = el.getAttribute("data-list-query");
|
||||
|
||||
el.removeAttribute("data-list-table");
|
||||
el.removeAttribute("data-list-options");
|
||||
el.removeAttribute("data-list-value");
|
||||
el.removeAttribute("data-list-label");
|
||||
el.removeAttribute("data-list-multi");
|
||||
el.removeAttribute("data-list-query");
|
||||
|
||||
|
||||
let php1li = `\n
|
||||
{% set list_values = ${field} | trim | split("\t") %} \n
|
||||
`;
|
||||
|
||||
php1li += `\n
|
||||
{% if record %} \n
|
||||
{% set auxRecord = record %} \n
|
||||
{% endif %} \n
|
||||
`;
|
||||
|
||||
php1li += `\n
|
||||
|
||||
{% for record in list_values %} \n
|
||||
{% set index = loop.index0 %} \n
|
||||
`;
|
||||
if (isTable) {
|
||||
php1li += `\n
|
||||
{% if '${tableSelect}' | loadSchema %} \n
|
||||
{% set record = '${tableSelect}' | get([{'column':'${valueSelect}','operator':'=','value':record}]) %} \n
|
||||
{% set record = record.0 %} \n
|
||||
{% else %} \n
|
||||
{% set record = 'cms_${tableSelect}' | get([{'column':'${valueSelect}','operator':'=','value':record}],'num desc',1,{'ignoreSchema':true}) %}
|
||||
{% set record = record.0 %}
|
||||
{% endif %}
|
||||
`;
|
||||
}
|
||||
php2li = `\n
|
||||
{% endfor %}\n
|
||||
{% if auxRecord %} {% set record = auxRecord %} {% endif %}
|
||||
`;
|
||||
|
||||
/*let php1li = '|*' + btoa(`<? ${field} = array_filter(exp lode("\t",${field}));if (isset($record)) $auxRecord = $record; foreach(${field} as $index => $record){ ?>`) + '*|';
|
||||
if (isTable) php1li += '|*' + btoa(`<? $schema = loadSchema("${tableSelect}"); if (@$schema) {$record = @dame_registros("${tableSelect}","${valueSelect}='$record'","num desc",1)[0];}else{ global $TABLE_PREFIX; $record = @mysql_query_fetch_all_assoc("SELECT * FROM ".$TABLE_PREFIX."${tableSelect} WHERE ${valueSelect}='$record' LIMIT 1")[0]; } if (!$record) continue;?>`) + '*|';
|
||||
|
||||
let php2li = '|*' + btoa(`<? }
|
||||
if (isset($auxRecord)) $record = $auxRecord;?>`) + '*|';*/
|
||||
let stringli = `${php1li}${appParser.parseComponents(el.outerHTML,`record`,2)}${php2li}`;
|
||||
el.outerHTML = stringli;
|
||||
|
||||
|
||||
break;
|
||||
case "upload":
|
||||
|
||||
if (el.hasAttribute("src")){
|
||||
el.setAttribute('src',`${rand}{{ ${field}.0.urlPath | imagec(${width}) }}`);
|
||||
}else{
|
||||
// let classString = el.getAttribute("class");
|
||||
// let styleString = el.getAttribute("style");
|
||||
// let altString = el.getAttribute("alt");
|
||||
var srcAttr = "src";
|
||||
var output = "";
|
||||
const attrs = el.attributes;
|
||||
for(var i = attrs.length - 1; i >= 0; i--) {
|
||||
if (attrs[i].name.toLowerCase() == "src") continue;
|
||||
if (attrs[i].name.toLowerCase() == "data-lazy") { srcAttr="data-src"; continue;}
|
||||
output += `${attrs[i].name}="${attrs[i].value}" `;
|
||||
}
|
||||
console.log({output:output});
|
||||
let php = `${rand}{{ ${field}.0.urlPath | imagec(${width}) }}`;
|
||||
el.outerHTML = `<img ${srcAttr}='${php}' ${output}>`;
|
||||
}
|
||||
break;
|
||||
case "uploadBackground":
|
||||
let php3 = `${rand}{{ ${field}.0.urlPath | imagec(${width}) }}`;
|
||||
el.setAttribute('style',`background-image:url('${php3}')`);
|
||||
break;
|
||||
case "textbox":
|
||||
//el.classList.add(`wed_${field.replace(/\$/g,"")}:${attr}`);
|
||||
var filter = "nl2br";
|
||||
var expre = new RegExp("<(\\S*?)[^>]*>.*?</\\1>|<.*?/>");
|
||||
|
||||
el.innerHTML = `
|
||||
{% if ${field} %} \n
|
||||
{% if ${field} | isHTML %}
|
||||
{{ ${field} | raw }} \n
|
||||
{% else %}
|
||||
{{ ${field} | nl2br }} \n
|
||||
{% endif %}
|
||||
{% else %} \n
|
||||
{{ "${el.innerHTML.replace(/\x22/g, '\\\x22')}" | nl2br }} \n
|
||||
{% endif %}
|
||||
`;
|
||||
break;
|
||||
case "headfield":
|
||||
let outputh = "";
|
||||
|
||||
const attrs = el.attributes;
|
||||
for(var i = attrs.length - 1; i >= 0; i--) {
|
||||
outputh += `${attrs[i].name}="${attrs[i].value}" `;
|
||||
}
|
||||
let phph1 = `{{ ${field} }}`;
|
||||
let phph2 = `{{ '<' ~ (${field_tag} ? ${field_tag} : 'P') ~ '${outputh}>' }}`;
|
||||
let phph3 = `{{ '< /' ~ (${field_tag} ? ${field_tag} : 'P') ~ '>' }}`;
|
||||
// ESTA CODIFICADO EN BASE64 PORQUE ACAI LO CONVIERTE A COMENTARIO POR EL ANALIZADOR LEXICO DE ACAI QUE YA NO DEBE DE ESTAR
|
||||
let php4 = ``;
|
||||
let phph4 = ``;
|
||||
|
||||
if (prefixVar){
|
||||
// {% set record = record|merge({'nombre_tag':record.nombre_tag ?: 'P'}) %}
|
||||
phph4 = `
|
||||
{% set ${prefixVar} = ${prefixVar}|merge({'${appParser.cleanString(label)}_tag': ${field_tag} ?: 'P'}) %}
|
||||
{{ ('<' ~ ${field_tag} ~ ' ${outputh}>' ~ (${field} ? ${field} : '${el.innerHTML.replace(/\x22/g, '\\\x22')}') ~ ('PC8=' | base64_decode) ~ ${field_tag} ~ '>') | raw }}
|
||||
`;
|
||||
}else{
|
||||
phph4 = `
|
||||
{% set ${field_tag} = ${field_tag} ?: 'P' %}
|
||||
{{ ('<' ~ ${field_tag} ~ ' ${outputh}>' ~ (${field} ? ${field} : '${el.innerHTML.replace(/\x22/g, '\\\x22')}') ~ ('PC8=' | base64_decode) ~ ${field_tag} ~ '>') | raw }}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
el.outerHTML = `${phph4}`;
|
||||
|
||||
break;
|
||||
default:
|
||||
//el.classList.add(`wed_${field.replace(/\$/g,"")}:${attr}`);
|
||||
el.innerHTML = `
|
||||
{% if ${field} %} \n
|
||||
{{ ${field} | raw }} \n
|
||||
{% else %} \n
|
||||
{{ "${el.innerHTML.replace(/\x22/g, '\\\x22')}" | raw }} \n
|
||||
{% endif %}
|
||||
`;
|
||||
}
|
||||
return el;
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
33
mcp-server/utils/moduleParser.js
Normal file
33
mcp-server/utils/moduleParser.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { parseComponents as remoteParseComponents, generateBuilderVars as remoteGenerateBuilderVars } from "./remoteParser.js";
|
||||
|
||||
export class ModuleParser {
|
||||
|
||||
/**
|
||||
* Parse components (c-if, c-for, c-else, c-hidden) and module tags
|
||||
* Converts Acai syntax to Twig syntax
|
||||
* Uses the remote appParser to ensure consistency with frontend
|
||||
* @param {string} html - The HTML content to parse
|
||||
* @param {string[]} moduleIds - Optional list of module IDs to recognize as tags
|
||||
* @param {string} prefixVar - Optional prefix for variables (used internally)
|
||||
* @param {boolean} skipBuilderData - Optional flag to skip extractBuilderData (to avoid recursion)
|
||||
* @returns {Promise<string>} - The parsed HTML with Twig syntax
|
||||
*/
|
||||
static async parseComponents(html, moduleIds = [], listTables = [], prefixVar = "", skipBuilderData = false) {
|
||||
if (!html) return html;
|
||||
|
||||
// Use the remote appParser (same as frontend)
|
||||
return await remoteParseComponents(html, moduleIds, listTables, prefixVar, skipBuilderData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate builder variables from code
|
||||
* Uses the remote appParser to ensure consistency with frontend
|
||||
* @param {string} code - The HTML code to analyze
|
||||
* @param {object} previousSchema - Optional previous schema for field name mapping
|
||||
* @returns {Promise<{codeParsed: string, codeVars: object}>} - Parsed code and variables
|
||||
*/
|
||||
static async generateBuilderVars(code, previousSchema = null) {
|
||||
// Use the remote appParser (same as frontend)
|
||||
return await remoteGenerateBuilderVars(code, previousSchema);
|
||||
}
|
||||
}
|
||||
155
mcp-server/utils/remoteParser.js
Normal file
155
mcp-server/utils/remoteParser.js
Normal file
@@ -0,0 +1,155 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import axios from 'axios';
|
||||
import vm from 'vm';
|
||||
|
||||
// Cache para los scripts y appParser
|
||||
let appParserCache = null;
|
||||
let windowCache = null;
|
||||
let scriptsCache = null;
|
||||
|
||||
/**
|
||||
* Descarga y ejecuta los scripts remotos necesarios para appParser
|
||||
* Igual que en el frontend (src/main.js)
|
||||
*/
|
||||
async function loadRemoteParser() {
|
||||
if (appParserCache) {
|
||||
return { appParser: appParserCache, window: windowCache };
|
||||
}
|
||||
|
||||
const scripts = [
|
||||
"https://cms.cocosolution.com/lib/plugins/builder_saas/js/lexer.js",
|
||||
"https://cms.cocosolution.com/lib/plugins/builder_saas/js/mixins/vuecomponents.js",
|
||||
"https://cms.cocosolution.com/lib/plugins/builder_saas/js/mixins/builderdata.js",
|
||||
"https://cms.cocosolution.com/lib/plugins/builder_saas/js/mixins/filters.js",
|
||||
"https://cms.cocosolution.com/lib/plugins/builder_saas/js/parseDocument.js",
|
||||
];
|
||||
|
||||
// Crear un contexto jsdom
|
||||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
||||
runScripts: "dangerously",
|
||||
resources: "usable"
|
||||
});
|
||||
|
||||
const window = dom.window;
|
||||
const document = window.document;
|
||||
|
||||
// Mock de objetos necesarios que pueden no estar en jsdom
|
||||
if (!window.btoa) {
|
||||
window.btoa = (str) => Buffer.from(str).toString('base64');
|
||||
}
|
||||
if (!window.atob) {
|
||||
window.atob = (str) => Buffer.from(str, 'base64').toString();
|
||||
}
|
||||
|
||||
// Asegurar que DOMParser esté disponible (jsdom lo tiene en window)
|
||||
// Pero también lo necesitamos como variable global para los scripts
|
||||
const DOMParser = window.DOMParser;
|
||||
|
||||
// Mock de window.bus (puede que no sea necesario para el parseo)
|
||||
if (!window.bus) {
|
||||
window.bus = {
|
||||
$emit: () => {},
|
||||
$on: () => {},
|
||||
$off: () => {}
|
||||
};
|
||||
}
|
||||
|
||||
// Crear contexto VM con todas las referencias necesarias
|
||||
// Los scripts remotos esperan que window, document, DOMParser, etc. estén disponibles globalmente
|
||||
const context = vm.createContext({
|
||||
window: window,
|
||||
document: document,
|
||||
DOMParser: DOMParser, // Añadir DOMParser como variable global
|
||||
console: console,
|
||||
Buffer: Buffer,
|
||||
setTimeout: setTimeout,
|
||||
setInterval: setInterval,
|
||||
clearTimeout: clearTimeout,
|
||||
clearInterval: clearInterval,
|
||||
// Añadir todas las propiedades globales necesarias
|
||||
...global,
|
||||
// Asegurar que las referencias estén disponibles también como variables globales
|
||||
global: global,
|
||||
process: process
|
||||
});
|
||||
|
||||
// Descargar y ejecutar cada script
|
||||
for (const scriptUrl of scripts) {
|
||||
try {
|
||||
console.log(`Descargando script: ${scriptUrl}`);
|
||||
const response = await axios.get(scriptUrl, {
|
||||
timeout: 10000 // 10 segundos de timeout
|
||||
});
|
||||
const scriptContent = response.data;
|
||||
|
||||
// Ejecutar el script en el contexto VM
|
||||
// Los scripts pueden usar 'window', 'document', etc. directamente
|
||||
vm.runInContext(scriptContent, context);
|
||||
} catch (error) {
|
||||
console.error(`Error cargando script ${scriptUrl}:`, error.message);
|
||||
// Si falla un script crítico, lanzar error
|
||||
if (scriptUrl.includes('parseDocument.js')) {
|
||||
throw new Error(`Error crítico cargando parseDocument.js: ${error.message}`);
|
||||
}
|
||||
// Continuar con los demás scripts para los no críticos
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar que appParser esté disponible
|
||||
if (!window.appParser) {
|
||||
throw new Error('appParser no se cargó correctamente desde los scripts remotos');
|
||||
}
|
||||
|
||||
appParserCache = window.appParser;
|
||||
windowCache = window;
|
||||
scriptsCache = scripts;
|
||||
|
||||
return { appParser: appParserCache, window: windowCache };
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene appParser, cargándolo si es necesario
|
||||
*/
|
||||
export async function getAppParser() {
|
||||
const { appParser } = await loadRemoteParser();
|
||||
return appParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper para parseComponents usando appParser remoto
|
||||
* Usa tipo 2 (Twig) explícitamente
|
||||
* Firma real: parseComponents(code, prefixVar, type = 0)
|
||||
*/
|
||||
export async function parseComponents(html, moduleIds = [], listTables = [], prefixVar = "", skipBuilderData = false) {
|
||||
const { appParser, window } = await loadRemoteParser();
|
||||
|
||||
// Setear las variables globales en el window real donde se cargó appParser
|
||||
window.allModules = moduleIds;
|
||||
window.tables = listTables;
|
||||
|
||||
// La firma real es: parseComponents(code, prefixVar, type = 0)
|
||||
// Pasamos 2 (número) para Twig explícitamente
|
||||
return appParser.parseComponents(html, prefixVar, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper para generateBuilderVars usando appParser remoto
|
||||
* Usa tipo 2 (Twig) explícitamente, igual que en Api.js
|
||||
* Nota: El código remoto falla si previousSchema es null, así que pasamos {} en su lugar
|
||||
*/
|
||||
export async function generateBuilderVars(code, previousSchema = null) {
|
||||
const appParser = await getAppParser();
|
||||
// Pasar 2 para Twig (igual que en Api.js: parseInt(type) donde type="2")
|
||||
// El código remoto falla si previousSchema es null, así que usamos {} en su lugar
|
||||
const safePreviousSchema = previousSchema || {};
|
||||
return appParser.generateBuilderVars(code, 2, safePreviousSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia la caché (útil para forzar recarga)
|
||||
*/
|
||||
export function clearCache() {
|
||||
appParserCache = null;
|
||||
scriptsCache = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user