/** * Acai CMS HTTP Client * * Centralizado helper para todas las llamadas HTTP a Acai CMS. * Proporciona métodos consistentes para interactuar con: * - Admin panel (admin.php) * - Viewer functions API * - SaaS API * - File upload endpoints * * Ventajas: * - Consistencia en headers, manejo de errores, logging * - Reduce duplicación de código * - Facilita mantenimiento y debugging * - Centraliza URLs y configuración */ import axios from 'axios'; import { getSessionCredentials } from '../../auth/index.js'; import { CMS_URL } from '../../config/index.js'; import { assertSafeCmsTarget } from '../../utils/cmsTargetSafety.js'; /** * AcaiHttpClient - Helper para solicitudes HTTP a Acai CMS */ export class AcaiHttpClient { static resolveCmsTarget(target) { const { publicUrl, apiUrl, forgeHost } = assertSafeCmsTarget(target, "AcaiHttpClient"); const headers = {}; if (forgeHost) { headers.Host = forgeHost; } return { publicUrl, apiUrl, headers, }; } static buildViewerUrl(target, query = "") { const { apiUrl } = AcaiHttpClient.resolveCmsTarget(target); const baseUrl = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl; return `${baseUrl}/cms/lib/viewer_functions.php${query ? `?${query}` : ""}`; } static buildViewerHeaders(target, extraHeaders = {}) { const { headers } = AcaiHttpClient.resolveCmsTarget(target); return { "Content-Type": "application/json", ...headers, ...extraHeaders, }; } static async postViewerAction(target, actionWs, payload, token, tokenHash, extraHeaders = {}, timeout = 30000) { const viewerUrl = AcaiHttpClient.buildViewerUrl(target, `action_ws=${actionWs}`); const body = { ...payload, token, tokenHash, }; return axios.post(viewerUrl, body, { headers: AcaiHttpClient.buildViewerHeaders(target, extraHeaders), timeout, }); } /** * POST a admin.php con URLSearchParams * @param {string} website - Website/domain (no usado, se usa CMS_URL del config) * @param {URLSearchParams} params - Parámetros del formulario * @param {string} token - Token Acai * @returns {Promise} Respuesta del servidor */ static async postAdminForm(website, params, token) { const cmsUrl = `${CMS_URL}/admin.php`; try { console.error(`[AcaiHttpClient] postAdminForm - START: ${cmsUrl}`); const response = await axios.post(cmsUrl, params, { headers: { "X-Acai-Token": token, "Content-Type": "application/x-www-form-urlencoded" }, timeout: 30000 }); console.error(`[AcaiHttpClient] postAdminForm - SUCCESS: ${cmsUrl} (${response.status})`); return response; } catch (error) { console.error(`[AcaiHttpClient] postAdminForm - ERROR: ${cmsUrl} - ${error.message}`); if (error.response) { console.error(`[AcaiHttpClient] Response status: ${error.response.status}`); console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data); } else if (error.code) { console.error(`[AcaiHttpClient] Error code: ${error.code}`); } throw error; } } /** * POST a admin.php con FormData (para uploads) * @param {string} website - Website/domain (no usado, se usa CMS_URL del config) * @param {FormData} formData - Datos del formulario * @param {string} token - Token Acai * @returns {Promise} Respuesta del servidor */ static async postAdminFormData(website, formData, token) { const cmsUrl = `${CMS_URL}/admin.php`; try { console.error(`[AcaiHttpClient] postAdminFormData - START: ${cmsUrl}`); const response = await axios.post(cmsUrl, formData, { headers: { ...formData.getHeaders(), "X-Acai-Token": token }, timeout: 30000 }); console.error(`[AcaiHttpClient] postAdminFormData - SUCCESS: ${cmsUrl} (${response.status})`); return response; } catch (error) { console.error(`[AcaiHttpClient] postAdminFormData - ERROR: ${cmsUrl} - ${error.message}`); if (error.response) { console.error(`[AcaiHttpClient] Response status: ${error.response.status}`); console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data); } else if (error.code) { console.error(`[AcaiHttpClient] Error code: ${error.code}`); } throw error; } } /** * GET a admin.php con query parameters * @param {string} website - Website/domain (no usado, se usa CMS_URL del config) * @param {URLSearchParams | string} params - Parámetros de query * @param {string} token - Token Acai * @returns {Promise} Respuesta del servidor */ static async getAdminQuery(website, params, token) { const cmsUrl = `${CMS_URL}/admin.php`; const queryString = params instanceof URLSearchParams ? params.toString() : params; try { console.error(`[AcaiHttpClient] getAdminQuery - START: ${cmsUrl}?${queryString.substring(0, 100)}`); const response = await axios.get( `${cmsUrl}?${queryString}`, { headers: { "X-Acai-Token": token, "X-Requested-With": "XMLHttpRequest" }, timeout: 30000 } ); console.error(`[AcaiHttpClient] getAdminQuery - SUCCESS: ${cmsUrl} (${response.status})`); return response; } catch (error) { console.error(`[AcaiHttpClient] getAdminQuery - ERROR: ${cmsUrl} - ${error.message}`); if (error.response) { console.error(`[AcaiHttpClient] Response status: ${error.response.status}`); console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data); } else if (error.code) { console.error(`[AcaiHttpClient] Error code: ${error.code}`); } throw error; } } /** * POST a viewer_functions.php vía getApiClient * Requiere llamar desde dentro de withAuth para tener acceso a getApiClient * @param {Object} client - cliente de axios (getApiClient) * @param {Object} payload - Payload con action_ws y otros parámetros * @returns {Promise} Respuesta del servidor */ static async postViewerFunctions(client, payload) { return client.post("/cms/lib/viewer_functions.php", payload); } /** * POST a viewer_functions.php para saveLexicalData (secciones, contenido) * @param {string} web_url - URL base del sitio (ej: http://localhost:PORT) * @param {Object} credentials - {token, tokenHash} * @param {Object} data - Datos a guardar * @returns {Promise} Respuesta del servidor */ static async saveLexicalData(target, credentials, data) { const viewerUrl = AcaiHttpClient.buildViewerUrl(target); const payload = { action_ws: 'saveLexicalData', token: credentials.token, tokenHash: credentials.tokenHash, rawDataSended: true, ...data }; return axios.post(viewerUrl, payload, { headers: AcaiHttpClient.buildViewerHeaders(target) }); } /** * POST para generar módulo desde HTML * @param {Object} moduleData - Datos del módulo * @param {string} token - Token Acai * @returns {Promise} Respuesta del servidor */ static async generateModuleFromString(moduleData, token) { const cmsUrl = 'https://acai.cms.cocosolution.com/admin.php?menu=apartados&action=edit&generateModuleFromString=1'; return axios.post(cmsUrl, moduleData, { headers: { "Content-Type": "application/json", "X-Acai-Token": token } }); } /** * POST a viewer_functions para CMS API (insert, update, delete, get) * @param {string} web_url - URL base del sitio (ej: http://localhost:PORT) * @param {string} action - 'insert', 'update', 'delete', 'get' * @param {Object} payload - Datos de la operación * @param {string} token - Token Acai * @returns {Promise} Respuesta del servidor */ static async postCmsApi(target, action, payload, token, tokenHash) { const viewerUrl = AcaiHttpClient.buildViewerUrl(target, `action_ws=cmsApi&subaction=${action}`); try { console.error(`[AcaiHttpClient] postCmsApi - START: ${action} on ${viewerUrl}`); console.error(`[AcaiHttpClient] Payload:`, JSON.stringify(payload).substring(0, 500)); console.error(`[AcaiHttpClient] Token: ${token ? '****' + token.slice(-4) : 'No token provided'}`); payload["token"] = token; payload["tokenHash"] = tokenHash; const response = await axios.post(viewerUrl, payload, { headers: AcaiHttpClient.buildViewerHeaders(target, { "X-Acai-Token": token }), timeout: 30000 }); console.error(`[AcaiHttpClient] postCmsApi - SUCCESS: ${action} (${response.status})`); return response; } catch (error) { console.error(`[AcaiHttpClient] postCmsApi - ERROR: ${action} on ${viewerUrl} - ${error.message}`); if (error.response) { console.error(`[AcaiHttpClient] Response status: ${error.response.status}`); console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data); } else if (error.code) { console.error(`[AcaiHttpClient] Error code: ${error.code}`); } throw error; } } /** * POST a viewer_functions para checkModuleCode * @param {string} web_url - URL base del sitio (ej: http://localhost:PORT) * @param {string} token - Token Acai * @param {Object} data - {moduleName, vars} * @returns {Promise} Respuesta del servidor */ static async checkModuleCode(target, token, data) { const viewerUrl = AcaiHttpClient.buildViewerUrl(target, "action_ws=checkModuleCode"); try { data["token"] = token; console.error(`[AcaiHttpClient] checkModuleCode - START: ${viewerUrl}`); const response = await axios.post(viewerUrl, data, { headers: AcaiHttpClient.buildViewerHeaders(target), timeout: 30000 }); console.error(`[AcaiHttpClient] checkModuleCode - SUCCESS: ${viewerUrl} (${response.status})`); return response; } catch (error) { console.error(`[AcaiHttpClient] checkModuleCode - ERROR: ${viewerUrl}`, error.message); throw error; } } /** * POST a viewer_functions para addModuleToRecord * @param {string} web_url - URL base del sitio (ej: http://localhost:PORT) * @param {string} token - Token Acai * @param {Object} data - {moduleName, vars} * @returns {Promise} Respuesta del servidor */ static async addModuleToRecord(target, token, tokenHash, data) { const viewerUrl = AcaiHttpClient.buildViewerUrl(target, "action_ws=addModuleToRecord"); try { data["token"] = token; data["tokenHash"] = tokenHash; console.error(`[AcaiHttpClient] addModuleToRecord - START: ${viewerUrl}`); const response = await axios.post(viewerUrl, data, { headers: AcaiHttpClient.buildViewerHeaders(target), timeout: 30000 }); console.error(`[AcaiHttpClient] addModuleToRecord - SUCCESS: ${viewerUrl} (${response.status})`); return response; } catch (error) { console.error(`[AcaiHttpClient] addModuleToRecord - ERROR: ${viewerUrl}`, error.message); throw error; } } /** * POST a mcp_respond.php para setModuleConfigVars * @param {string} web_url - URL base del sitio (ej: http://localhost:PORT) * @param {string} token - Token Acai * @param {string} tokenHash - Token hash Acai * @param {Object} data - {tableName, recordNum, sectionId, vars} * @returns {Promise} Respuesta del servidor */ static async setModuleConfigVars(target, token, tokenHash, data) { const url = AcaiHttpClient.buildViewerUrl(target, "action_ws=setModuleConfigVars"); try { data["token"] = token; data["tokenHash"] = tokenHash; console.error(`[AcaiHttpClient] setModuleConfigVars - START: ${url}`); const response = await axios.post(url, data, { headers: AcaiHttpClient.buildViewerHeaders(target), timeout: 30000 }); console.error(`[AcaiHttpClient] setModuleConfigVars - SUCCESS: ${url} (${response.status})`); return response; } catch (error) { console.error(`[AcaiHttpClient] setModuleConfigVars - ERROR: ${url}`, error.message); throw error; } } /** * POST a viewer_functions para getModuleConfigVars * @param {string} web_url - URL base del sitio (ej: http://localhost:PORT) * @param {string} token - Token Acai * @param {string} tokenHash - Token hash Acai * @param {Object} data - {tableName, recordNum, sectionId} * @returns {Promise} Respuesta del servidor */ static async getModuleConfigVars(target, token, tokenHash, data) { const url = AcaiHttpClient.buildViewerUrl(target, "action_ws=getModuleConfigVars"); try { data["token"] = token; data["tokenHash"] = tokenHash; console.error(`[AcaiHttpClient] getModuleConfigVars - START: ${url}`); const response = await axios.post(url, data, { headers: AcaiHttpClient.buildViewerHeaders(target), timeout: 30000 }); console.error(`[AcaiHttpClient] getModuleConfigVars - SUCCESS: ${url} (${response.status})`); return response; } catch (error) { console.error(`[AcaiHttpClient] getModuleConfigVars - ERROR: ${url}`, error.message); throw error; } } /** * POST para subir imagen a campo de registro * @param {string} website - Website/domain (no usado, se usa CMS_URL del config) * @param {string} tableName - Nombre de la tabla * @param {string} recordId - ID del registro * @param {string} fieldName - Nombre del campo * @param {FormData} formData - Datos del archivo * @param {string} token - Token Acai * @returns {Promise} Respuesta del servidor */ static async uploadRecordImage(website, tableName, recordId, fieldName, formData, token) { const uploadUrl = `${CMS_URL}/lib/menus/modals/plupload/multiupload/upload.php?menu=${tableName}&fieldName=${fieldName}&num=${recordId}&preSaveTempId=`; return axios.post(uploadUrl, formData, { headers: { ...formData.getHeaders(), "X-Acai-Token": token } }); } /** * POST a SaaS API para guardar esquema * @param {Object} payload - {action, type, schema, dir, id, ...} * @param {string} token - Token autenticación * @returns {Promise} Respuesta del servidor */ static async saasPostRequest(payload, token) { const SAAS_URL = 'https://ws.cocosolution.com/api/schemas/'; try { console.error(`[AcaiHttpClient] saasPostRequest - START: ${SAAS_URL} (action: ${payload.action})`); const response = await axios.post(SAAS_URL, payload, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 30000 }); console.error(`[AcaiHttpClient] saasPostRequest - SUCCESS: ${SAAS_URL} (${response.status})`); return response; } catch (error) { console.error(`[AcaiHttpClient] saasPostRequest - ERROR: ${SAAS_URL} - ${error.message}`); if (error.response) { console.error(`[AcaiHttpClient] Response status: ${error.response.status}`); console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data); } else if (error.code) { console.error(`[AcaiHttpClient] Error code: ${error.code}`); } throw error; } } /** * PUT a SaaS API para actualizar esquema * @param {Object} payload - {action, type, schema, dir, id} * @param {string} token - Token autenticación * @returns {Promise} Respuesta del servidor */ static async saasPutRequest(payload, token) { const SAAS_URL = 'https://ws.cocosolution.com/api/schemas/'; try { console.error(`[AcaiHttpClient] saasPutRequest - START: ${SAAS_URL} (action: ${payload.action})`); const response = await axios.put(SAAS_URL, payload, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 30000 }); console.error(`[AcaiHttpClient] saasPutRequest - SUCCESS: ${SAAS_URL} (${response.status})`); return response; } catch (error) { console.error(`[AcaiHttpClient] saasPutRequest - ERROR: ${SAAS_URL} - ${error.message}`); if (error.response) { console.error(`[AcaiHttpClient] Response status: ${error.response.status}`); console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data); } else if (error.code) { console.error(`[AcaiHttpClient] Error code: ${error.code}`); } throw error; } } } /** * Helper para construir parámetros comunes de formulario */ export class FormParamsBuilder { static buildRecordSaveParams(tableName, recordId, fields, enlace) { const params = new URLSearchParams(); params.append('menu', tableName); params.append('_defaultAction', 'save'); params.append('num', recordId ? String(recordId) : ''); params.append('type', ''); params.append('preSaveTempId', Date.now().toString()); params.append('action=save', 'Guardar'); // Agregar todos los campos for (const [fieldName, value] of Object.entries(fields)) { if (fieldName === 'enlace') continue; if (value !== null && value !== undefined) { const strValue = String(value); params.append(fieldName, strValue); // Detectar y descomponer fechas const dateRegex = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/; const match = strValue.match(dateRegex); if (match) { params.append(`${fieldName}:year`, match[1]); params.append(`${fieldName}:mon`, match[2]); params.append(`${fieldName}:day`, match[3]); params.append(`${fieldName}:hour24`, match[4]); params.append(`${fieldName}:min`, match[5]); } } } params.append('enlace', enlace); return params; } static buildDeleteRecordsParams(tableName, recordIds) { const params = new URLSearchParams(); params.append('menu', tableName); params.append('_defaultAction', 'list'); params.append('page', '1'); params.append('_advancedAction', 'eraseRecords'); params.append('_advancedActionSubmit', 'Ejecutar'); recordIds.forEach(id => { params.append('selectedRecords[]', String(id)); }); return params; } } /** * Helper para construir URLs de query */ export class QueryParamsBuilder { static buildListRecordsQuery(tableName, page, keyword) { const params = new URLSearchParams({ menu: tableName, json: "1" }); if (page) params.append("page", String(page)); if (keyword) params.append("keyword", keyword); return params; } static buildListUploadsQuery(tableName, recordId, fieldName) { return new URLSearchParams({ menu: tableName, action: 'uploadList', fieldName: fieldName, num: recordId, preSaveTempId: '', json: '1' }); } static buildDeleteUploadQuery(tableName, recordId, fieldName, uploadId) { return new URLSearchParams({ menu: tableName, action: 'uploadErase', fieldName: fieldName, uploadNum: uploadId, num: recordId, preSaveTempId: '' }); } } export default AcaiHttpClient;