<?php
namespace App\Controller;
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Empresa;
use App\Entity\Usuario;
use App\Entity\ConexionBD;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Description of AdminController
*
* @author joseangelparra
*/
class AdminController extends AbstractController{
//put your code here
private $params;
public function __construct(ParameterBagInterface $params){
$this->session = new Session();
$this->params = $params;
}
#[Route('/', name: 'login')]
public function Login(AuthenticationUtils $authenticationUtils){
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUSername();
return $this->render('base.html.twig',array(
'error'=>$error,
'last_username'=>$lastUsername
));
}
public function changePWD(UserPasswordHasherInterface $encoder, EntityManagerInterface $em){
$user = $em->getRepository(Usuario::class)->find(10);
$encoded = $encoder->hashPassword($user,'docuManager2025');
$user->setPassword($encoded);
$em->persist($user);
$flush=$em->flush();
die();
}
public function checkUserExists(Request $request, EntityManagerInterface $em){
$email = $request->request->get("user");
$exists = $em->getRepository(Usuario::class)->findBy(array("email"=>$email));
if($exists){
return new JsonResponse(array("exists"=>true));
}else{
return new JsonResponse(array("exists"=>false));
}
}
private function azurePortForTenant(int $tenantId, int $base = 12000, int $max = 20999): int {
$port = $base + $tenantId;
if ($port > $max) {
$range = max(1, $max - $base);
$port = $base + ($tenantId % $range);
}
return $port;
}
#[Route('/list', name: 'list')]
public function List( EntityManagerInterface $entityManager){
if (!$this->getUser() || !is_object($this->getUser())) {
return $this->redirectToRoute('logout');
}
$empresas = $entityManager->getRepository(Empresa::class)->findAll();
return $this->render('listusers.html.twig',array(
'empresas' => $empresas,
));
}
public function addEmpresa(Request $req, EntityManagerInterface $em)
{
//dd(\shell_exec('whoami'));
if (!$this->getUser() || !is_object($this->getUser())) {
return $this->redirectToRoute('logout');
}
if ($req->request->get("submit") != "") {
$data = $req->request->all();
// Crear nombre de base de datos y credenciales aleatorios
$dbName = 'doc_' . bin2hex(random_bytes(3));
$dbUser = 'doc_' . bin2hex(random_bytes(2));
$dbPass = bin2hex(random_bytes(8));
$dbHost = 'localhost';
$dbPort = '3306';
// Credenciales de HestiaCP desde variables de entorno
$hestiaApiUrl = $_ENV['HESTIA_API_URL'];
$hestiaApiUser = $_ENV['HESTIA_API_USER'];
$hestiaApiPass = $_ENV['HESTIA_API_PASS'];
$hestiaOwner = $_ENV['HESTIA_OWNER'];
$accessKeyId = $_ENV['HESTIA_ACCESS_KEY_ID'] ?? '';
$secretKey = $_ENV['HESTIA_SECRET_KEY'] ?? '';
// Variables para el script de bash
$ocrBinary = $_ENV['OCR_BINARY'];
$filesPath = $_ENV['FILES_PATH'];
$owner = $hestiaOwner; // o el dueño del hosting
$postFields = http_build_query([
'user' => $hestiaApiUser,
'password' => $hestiaApiPass,
'returncode' => 'yes',
'cmd' => 'v-add-database',
'arg1' => $owner,
'arg2' => $dbName,
'arg3' => $dbUser,
'arg4' => $dbPass,
'arg5' => 'mysql'
]);
//dd($postFields);
$headers = [
'Authorization: Bearer ' . $accessKeyId . ':' . $secretKey
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $hestiaApiUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Solo si usas certificados autofirmados
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error || trim($response) !== '0') {
$this->addFlash('danger', 'Error al crear la base de datos en HestiaCP: ' . ($error ?: $response));
return $this->redirectToRoute('app_empresa_new');
}
//Añadir sql
$sqlFile = __DIR__ . '/../../db/db_base.sql'; // Ajusta la ruta si está en otro sitio
if (!file_exists($sqlFile)) {
$this->addFlash('danger', 'Archivo db_base.sql no encontrado.');
return $this->redirectToRoute('app_empresa_new');
}
$mysqli = new \mysqli($dbHost, "{$owner}_{$dbUser}", $dbPass, "{$owner}_{$dbName}", (int)$dbPort);
if ($mysqli->connect_error) {
$this->addFlash('danger', 'Error al conectar a la base de datos: ' . $mysqli->connect_error);
return $this->redirectToRoute('app_empresa_new');
}
$sql = file_get_contents($sqlFile);
// Eliminar lÃneas con DELIMITER
$sql = preg_replace('/DELIMITER\s+\$\$/', '', $sql);
$sql = preg_replace('/DELIMITER\s+;/', '', $sql);
// Separar por ';;' si los triggers usan ese delimitador (ajusta si es $$)
$statements = explode('$$', $sql);
foreach ($statements as $stmt) {
$stmt = trim($stmt);
if ($stmt) {
if (!$mysqli->multi_query($stmt)) {
$this->addFlash('danger', 'Error ejecutando SQL: ' . $mysqli->error);
return $this->redirectToRoute('app_empresa_new');
}
// Limpiar cualquier resultado intermedio
while ($mysqli->more_results() && $mysqli->next_result()) {
$mysqli->use_result();
}
}
}
$updateSql = "UPDATE users SET email = '".$data["user"]."' WHERE id = 1";
if (!$mysqli->query($updateSql)) {
$this->addFlash('danger', 'Error al actualizar usuario: ' . $mysqli->error);
return $this->redirectToRoute('app_empresa_new');
}
// Guardar parámetros (activeUsers + modulos_*) en la BD del cliente
$this->saveEmpresaParametros($mysqli, $data);
// SUBIR LOGO PERSONALIZADO (SI LO HAY) ===
$customLogoFile = null;
try {
$customLogoFile = $this->uploadEmpresaLogo($req);
} catch (\Throwable $e) {
// Aquí decides si quieres que esto sea fatal o solo un aviso
$this->addFlash('warning', 'El logo personalizado no se pudo subir: ' . $e->getMessage());
// Si quieres abortar todo el proceso por fallo de logo, haz return+redirect aquí.
}
// Guardar logo en la BD del cliente
$this->saveEmpresaLogo($mysqli, $data, $customLogoFile);
// Actualizar/insertar license (capacityGb y users) en la BD del cliente
$this->upsertLicense(
$mysqli,
$data,
isset($data['maxDiskQuota']) && $data['maxDiskQuota'] !== '' ? (int)$data['maxDiskQuota'] : 200,
isset($data['maxActiveUsers']) && $data['maxActiveUsers'] !== '' ? (int)$data['maxActiveUsers'] : 3
);
// Obtener modelo seleccionado del formulario (0 si placeholder)
$modelId = isset($data['extraction_model']) && $data['extraction_model'] !== '' ? (int)$data['extraction_model'] : 0;
if ((int)($data['modulo_extraccion'] ?? 0) === 1 && $modelId > 0) {
try {
$this->syncExtractionModelToClientDb($mysqli, $em, $modelId);
} catch (\Throwable $e) {
// log opcional
}
}
// Crear y persistir la empresa
$emp = new Empresa();
$emp->setName($data["name"]);
$emp->setMaxDiskQuota((int)$data["maxDiskQuota"]);
$em->persist($emp);
$em->flush();
$conexionBD = new ConexionBD();
$conexionBD->setDbName($owner."_".$dbName);
$conexionBD->setDbUser($owner."_".$dbUser);
$conexionBD->setDbPassword($dbPass);
$conexionBD->setDbUrl($dbHost);
$conexionBD->setDbPort($dbPort);
$em->persist($conexionBD);
$em->flush();
$emp->setConexionBD($conexionBD);
$em->persist($emp);
$em->flush();
//crear usuario
$user = new \App\Entity\Usuario();
$user->setEmail($data["user"]);
$user->setEmpresa($emp);
$user->setPassword("dscsdcsno2234dwvw");
$user->setStatus(1);
$user->setIsAdmin(2);
$user->setConnection($conexionBD->getId());
$em->persist($user);
$em->flush();
//crear el script de bash
$company_name = $emp->getId();
// "DOCU_MAX_THREADS=" por defecto 4
// "NO" al final es para desactivar FTP
// Crear archivo .service
$serviceContent = "<<<EOT
[Unit]
Description=".$data["name"]." DocuManager OCR
Requires=mariadb.service
After=mariadb.service
[Service]
Type=simple
ExecStart=$ocrBinary localhost/".$owner."_".$dbName." ".$owner."_".$dbUser." ".$dbPass." $filesPath/".$company_name."
Restart=always
User=root
[Install]
WantedBy=multi-user.target
EOT";
// Guardar contenido temporal en un archivo dentro de /tmp
$serviceName = $company_name."-documanager.service";
$tmpServicePath = "/tmp/$serviceName";
file_put_contents($tmpServicePath, $serviceContent);
\chmod($tmpServicePath, 0644);
// Mover el archivo y habilitar el servicio desde PHP con shell_exec
$commands = [
"sudo /bin/mv /tmp/$serviceName /etc/systemd/system/$serviceName",
"sudo /bin/systemctl daemon-reload",
"sudo /bin/systemctl enable $serviceName",
"sudo /bin/systemctl start $serviceName",
];
$errors = [];
foreach ($commands as $cmd) {
$output = \shell_exec($cmd . " 2>&1");
if ($output !== null) {
// Puedes loguearlo si quieres para ver errores
error_log("CMD OUTPUT: $cmd\n$output");
$errors[] = "CMD OUTPUT: $cmd\n$output";
}
}
// === Crear servicio AZURE DI por tenant ===
$azureBasePort = 12000;
$tenantId = (int)$emp->getId();
$port = $this->azurePortForTenant($tenantId, $azureBasePort, 20999);
$serviceName = $tenantId . "-azuredi.service";
$workdir = "/home/docunecta/web/platform.documanager.es/public_html/newdocu/services/azure-di";
$logsDir = $workdir . "/logs/" . $tenantId;
// Asegura carpeta de logs
@mkdir($logsDir, 0775, true);
$serviceContent = <<<EOT
[Unit]
Description=DocuManager Azure DI (tenant {$tenantId})
After=network.target
[Service]
User=root
WorkingDirectory={$workdir}
Environment=APP_HOST=127.0.0.1
Environment=APP_PORT={$port}
Environment=LOG_DIR={$logsDir}
Environment=PYTHONUNBUFFERED=1
Environment=MAX_CONCURRENT=20
Environment="PATH=/opt/azure-di/.venv/bin:/usr/local/bin:/usr/bin"
EnvironmentFile=-{$workdir}/.env
ExecStart=/opt/azure-di/.venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port {$port} --proxy-headers --workers 2
Restart=always
RestartSec=2
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOT;
$tmpServicePath = "/tmp/{$serviceName}";
file_put_contents($tmpServicePath, $serviceContent);
@chmod($tmpServicePath, 0644);
$cmds = [
"sudo /bin/mv {$tmpServicePath} /etc/systemd/system/{$serviceName}",
"sudo /bin/systemctl daemon-reload",
"sudo /bin/systemctl enable {$serviceName}",
"sudo /bin/systemctl start {$serviceName}",
];
foreach ($cmds as $cmd) {
$out = \shell_exec($cmd . " 2>&1");
if ($out !== null) { error_log("AZURE-DI CMD: $cmd\n$out"); }
}
if(count($errors)>0){
$this->addFlash('success', 'Empresa y base de datos creadas correctamente: '.implode(" | ",$errors));
}
$this->addFlash('success', 'Empresa y base de datos creadas correctamente.');
return $this->redirectToRoute('list');
} else {
// Lee los modelos usando la conexión principal de Doctrine
$extractionModels = [];
try {
$conn = $em->getConnection();
$sql = 'SELECT id, model_id FROM extraction_models ORDER BY model_id';
$extractionModels = $conn->executeQuery($sql)->fetchAllAssociative(); // ['id' => '1', 'model_name' => '...']
// Opcional: castear id a int
foreach ($extractionModels as &$m) { $m['id'] = (int)$m['id']; }
} catch (\Throwable $e) {
$extractionModels = [];
}
return $this->render('empresa/_add.html.twig', [
'extraction_models' => $extractionModels,
'modulos' => [
'extraction_model' => 0,
'limite_archivos' => 500,
],
]);
}
}
private function saveEmpresaParametros(\mysqli $mysqli, array $data): void
{
// Helpers
$getInt = fn(array $a, string $k, int $d) => (isset($a[$k]) && $a[$k] !== '') ? (int)$a[$k] : $d;
$getFlag = fn(array $a, string $k) => (isset($a[$k]) && (int)$a[$k] === 1) ? 1 : 0;
// Keys y valores tal y como quieres guardarlos
$paramMap = [
'activeUsers' => $getInt($data, 'maxActiveUsers', 3),
'modulo_etiquetas' => $getFlag($data, 'modulo_etiquetas'),
'modulo_calendario' => $getFlag($data, 'modulo_calendario'),
'modulo_calExt' => $getFlag($data, 'modulo_calendarioExterno'),
'modulo_estados' => $getFlag($data, 'modulo_estados'),
'modulo_subida' => $getFlag($data, 'modulo_subida'),
'modulo_extraccion' => $getFlag($data, 'modulo_extraccion'),
'modulo_lineas' => $getFlag($data, 'modulo_lineas'),
'limite_archivos' => $getInt($data, 'limite_archivos', 500),
'extraction_model' => $getInt($data, 'extraction_model', 0),
];
// RECOMENDADO en tu SQL base:
// ALTER TABLE parametros ADD UNIQUE KEY uniq_nombre (nombre);
$mysqli->begin_transaction();
try {
$stmt = $mysqli->prepare("
INSERT INTO parametros (nombre, valor)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE valor = VALUES(valor)
");
if (!$stmt) {
throw new \RuntimeException('Prepare parametros: ' . $mysqli->error);
}
foreach ($paramMap as $nombre => $valor) {
// valor es TEXT en tu esquema: bindeamos como string
$v = (string)$valor;
$stmt->bind_param('ss', $nombre, $v);
if (!$stmt->execute()) {
throw new \RuntimeException("Guardar parámetro $nombre: " . $stmt->error);
}
}
$stmt->close();
$mysqli->commit();
} catch (\Throwable $e) {
$mysqli->rollback();
// Si NO puedes añadir UNIQUE(nombre), usa fallback DELETE+INSERT:
// $this->saveParametrosFallback($mysqli, $paramMap);
throw $e;
}
}
private function saveEmpresaLogo(\mysqli $mysqli, array $data, ?string $customLogoFile = null): void
{
$this->logLogo('empresa_logo_save.log', '--- NUEVA LLAMADA saveEmpresaLogo ---');
$this->logLogo('empresa_logo_save.log', 'customLogoFile = '.var_export($customLogoFile, true));
$this->logLogo('empresa_logo_save.log', 'data[empresa_vendor] = '.var_export($data['empresa_vendor'] ?? null, true));
// 1) Decidir qué logo vamos a guardar
$logoFile = null;
// --- PRIORIDAD: LOGO PERSONALIZADO ---
if ($customLogoFile !== null && $customLogoFile !== '') {
$logoFile = $customLogoFile;
$this->logLogo('empresa_logo_save.log', 'Usando logo personalizado: '.$logoFile);
} else {
// --- SI NO HAY PERSONALIZADO, USAMOS EL SELECT DE EMPRESA ---
if (!isset($data['empresa_vendor']) || $data['empresa_vendor'] === '') {
$this->logLogo('empresa_logo_save.log', 'No hay empresa_vendor y no hay logo custom. No hago nada.');
return;
}
$empresa = $data['empresa_vendor'];
$logoMap = [
'docunecta' => 'DocuManager_transparente.png',
'docuindexa' => 'DocuIndexa.png',
];
if (!isset($logoMap[$empresa])) {
$this->logLogo('empresa_logo_save.log', "empresa_vendor $empresa no está en logoMap. No hago nada.");
return;
}
$logoFile = $logoMap[$empresa];
$this->logLogo('empresa_logo_save.log', 'Usando logo por vendor: '.$logoFile);
}
if ($logoFile === null || $logoFile === '') {
$this->logLogo('empresa_logo_save.log', 'logoFile está vacío. No hago nada.');
return;
}
$this->logLogo('empresa_logo_save.log', 'Voy a guardar en parametros.logo: '.$logoFile);
$mysqli->begin_transaction();
try {
$stmt = $mysqli->prepare("
INSERT INTO parametros (nombre, valor)
VALUES ('logo', ?)
ON DUPLICATE KEY UPDATE valor = VALUES(valor)
");
if (!$stmt) {
$this->logLogo('empresa_logo_save.log', 'Error prepare: '.$mysqli->error);
throw new \RuntimeException('Prepare logo: ' . $mysqli->error);
}
$stmt->bind_param('s', $logoFile);
if (!$stmt->execute()) {
$this->logLogo('empresa_logo_save.log', 'Error execute: '.$stmt->error);
throw new \RuntimeException('Guardar parámetro logo: ' . $stmt->error);
}
$stmt->close();
$mysqli->commit();
$this->logLogo('empresa_logo_save.log', 'Logo guardado correctamente en BD.');
} catch (\Throwable $e) {
$mysqli->rollback();
$this->logLogo('empresa_logo_save.log', 'EXCEPCIÓN: '.$e->getMessage());
throw $e;
}
}
private function uploadEmpresaLogo(Request $req): ?string
{
$this->logLogo('empresa_logo_upload.log', '--- NUEVA LLAMADA uploadEmpresaLogo ---');
// name del checkbox en el formulario (ajústalo si usas otro)
$useCustomLogo = $req->request->get('customLogoCheck');
$this->logLogo('empresa_logo_upload.log', 'customLogoCheck = '.var_export($useCustomLogo, true));
// Si no marcaron "usar logo personalizado", no hacemos nada
if (!$useCustomLogo) {
$this->logLogo('empresa_logo_upload.log', 'No se ha marcado customLogoCheck. Salgo sin subir.');
return null;
}
/** @var UploadedFile|null $file */
$file = $req->files->get('logo_personalizado'); // name="logo_personalizado" en el input file
$this->logLogo('empresa_logo_upload.log', 'FILES[logo_personalizado] = '.print_r($file, true));
if (!$file instanceof UploadedFile || !$file->isValid()) {
$this->logLogo('empresa_logo_upload.log', 'File no es UploadedFile válido. Salgo sin subir.');
return null;
}
// VALIDACIONES BÁSICAS
$maxSize = 2 * 1024 * 1024; // 2 MB por ejemplo
if ($file->getSize() > $maxSize) {
$this->logLogo('empresa_logo_upload.log', 'Tamaño demasiado grande: '.$file->getSize());
throw new \RuntimeException('El logo personalizado es demasiado grande (máx 2MB).');
}
$mime = $file->getMimeType();
$allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml'];
$this->logLogo('empresa_logo_upload.log', 'MIME = '.$mime);
if (!in_array($mime, $allowedMimeTypes, true)) {
$this->logLogo('empresa_logo_upload.log', 'MIME no permitido.');
throw new \RuntimeException('Formato de logo no permitido. Usa PNG, JPG, WEBP o SVG.');
}
// Directorio destino según entorno (configurado en .env/.env.local)
$targetDir = $_ENV['APP_LOGO_DIR'] ?? null;
$this->logLogo('empresa_logo_upload.log', 'APP_LOGO_DIR = '.var_export($targetDir, true));
if (!$targetDir) {
throw new \RuntimeException('APP_LOGO_DIR no está configurado en el entorno.');
}
if (!is_dir($targetDir)) {
$this->logLogo('empresa_logo_upload.log', "El directorio no existe: $targetDir");
throw new \RuntimeException("El directorio de logos no existe: $targetDir");
}
if (!is_writable($targetDir)) {
$this->logLogo('empresa_logo_upload.log', "El directorio no es escribible: $targetDir");
throw new \RuntimeException("El directorio de logos no es escribible: $targetDir");
}
// Nombre de archivo "seguro" y único
$ext = $file->guessExtension() ?: 'png';
$fileName = 'logo_empresa_' . bin2hex(random_bytes(6)) . '.' . $ext;
$this->logLogo('empresa_logo_upload.log', "Voy a mover archivo como: $fileName");
// Mover físicamente el archivo
$file->move($targetDir, $fileName);
$this->logLogo('empresa_logo_upload.log', "Fichero movido OK a $targetDir/$fileName");
// Devolvemos SOLO el nombre, que es lo que se guardará en parametros.logo
return $fileName;
}
private function logLogo(string $fileName, string $message): void
{
// Directorio de logs de Symfony (donde está dev.log/prod.log)
$logDir = $this->getParameter('kernel.logs_dir');
$fullPath = rtrim($logDir, '/').'/'.$fileName;
$line = sprintf(
"[%s] %s\n",
date('Y-m-d H:i:s'),
$message
);
file_put_contents($fullPath, $line, FILE_APPEND);
}
private function loadEmpresaLogo(\mysqli $mysqli): ?string
{
$sql = "SELECT valor FROM parametros WHERE nombre = 'logo' LIMIT 1";
$res = $mysqli->query($sql);
if (!$res) {
return null;
}
if ($row = $res->fetch_assoc()) {
return $row['valor'] ?? null;
}
return null;
}
private function upsertLicense(\mysqli $mysqli, array $data, int $capacityGb = 200, int $activeUsers = 3): void
{
$clientName = (string)($data['name'] ?? '');
$licenseStr = 'Documanager 1.0';
$initialDate = date('Y-m-d');
$price = 0;
$emailSender = 'Documanager.es';
$emailFrom = 'no-reply@documanager.es';
$emailName = 'Documanager';
$smtpHost = 'documanager.es';
$smtpUser = 'no-reply@documanager.es';
$smtpPort = 587;
$smtpPass = 'Documanager1!';
$ins = $mysqli->prepare("
INSERT INTO license
(client, license, initialDate, capacityGb, users, price, emailSender, emailFrom, emailName, smtpHost, smtpUser, smtpPort, smtpPass)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
if (!$ins) {
throw new \RuntimeException('Prepare INSERT license: ' . $mysqli->error);
}
$ins->bind_param(
'sssiiisssssis',
$clientName, $licenseStr, $initialDate, $capacityGb, $activeUsers, $price,
$emailSender, $emailFrom, $emailName, $smtpHost, $smtpUser, $smtpPort, $smtpPass
);
if (!$ins->execute()) {
$ins->close();
throw new \RuntimeException('Execute INSERT license: ' . $ins->error);
}
$ins->close();
}
private function syncExtractionModelToClientDb(\mysqli $clientMysqli, EntityManagerInterface $em, int $modelId): void
{
if ($modelId <= 0) {
return; // no hay selección
}
// Leer MODELO + DEFINICIONES (cabecera y líneas) de la BD principal
$conn = $em->getConnection();
// 1) Modelo
$rowModel = $conn->executeQuery(
'SELECT id, provider, model_id, endpoint, api_key, type, show_confidence_badges
FROM extraction_models
WHERE id = ?',
[$modelId]
)->fetchAssociative();
if (!$rowModel) {
return; // id inexistente
}
// 2) Campos de cabecera
$headers = $conn->executeQuery(
'SELECT id, model_id, field_key, label, value_type,
order_index, visibility, order_index_table, visibility_table
FROM definitions_header
WHERE model_id = ?',
[$modelId]
)->fetchAllAssociative();
// 3) Campos de líneas
$lines = $conn->executeQuery(
'SELECT id, model_id, field_key, label, value_type,
order_index, visibility
FROM definitions_lines
WHERE model_id = ?',
[$modelId]
)->fetchAllAssociative();
// ----- TRANSACCIÓN EN BD DEL CLIENTE -----
$clientMysqli->begin_transaction();
try {
// ------------------------------------------------
// 1) UPSERT extraction_models (cliente)
// ------------------------------------------------
$sqlModel = 'INSERT INTO extraction_models
(id, provider, model_id, endpoint, api_key, type, show_confidence_badges)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
provider = VALUES(provider),
model_id = VALUES(model_id),
endpoint = VALUES(endpoint),
api_key = VALUES(api_key),
type = VALUES(type),
show_confidence_badges = VALUES(show_confidence_badges)';
$stmtModel = $clientMysqli->prepare($sqlModel);
if (!$stmtModel) {
throw new \RuntimeException('Prepare INSERT extraction_models (cliente): ' . $clientMysqli->error);
}
$id = (int)$rowModel['id'];
$provider = (string)$rowModel['provider'];
$modelIdStr = (string)$rowModel['model_id'];
$endpoint = (string)$rowModel['endpoint'];
$apiKey = (string)$rowModel['api_key'];
$type = (string)$rowModel['type'];
$badges = (int)$rowModel['show_confidence_badges'];
if (
!$stmtModel->bind_param(
'isssssi',
$id,
$provider,
$modelIdStr,
$endpoint,
$apiKey,
$type,
$badges
)
|| !$stmtModel->execute()
) {
$stmtModel->close();
throw new \RuntimeException('Execute INSERT/UPSERT extraction_models (cliente): ' . $clientMysqli->error);
}
$stmtModel->close();
// ------------------------------------------------
// 2) Sincronizar definitions_header (cliente)
// - Primero borramos las filas del modelo
// - Luego insertamos las de la central
// ------------------------------------------------
$stmtDelHeaders = $clientMysqli->prepare(
'DELETE FROM definitions_header WHERE model_id = ?'
);
if (!$stmtDelHeaders) {
throw new \RuntimeException('Prepare DELETE definitions_header (cliente): ' . $clientMysqli->error);
}
$stmtDelHeaders->bind_param('i', $id);
if (!$stmtDelHeaders->execute()) {
$stmtDelHeaders->close();
throw new \RuntimeException('Execute DELETE definitions_header (cliente): ' . $clientMysqli->error);
}
$stmtDelHeaders->close();
if (!empty($headers)) {
$stmtInsHeaders = $clientMysqli->prepare(
'INSERT INTO definitions_header
(id, model_id, field_key, label, value_type,
order_index, visibility, order_index_table, visibility_table)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
);
if (!$stmtInsHeaders) {
throw new \RuntimeException('Prepare INSERT definitions_header (cliente): ' . $clientMysqli->error);
}
foreach ($headers as $h) {
$hId = (int)$h['id'];
$hModelId = (int)$h['model_id'];
$fieldKey = (string)$h['field_key'];
// si quieres respetar NULL tal cual, quita el (string) y pasa directamente $h['label']
$label = $h['label'] !== null ? (string)$h['label'] : null;
$valueType = (string)$h['value_type'];
$orderIndex = (int)$h['order_index'];
$visibility = (int)$h['visibility'];
$orderTable = (int)$h['order_index_table'];
$visTable = (int)$h['visibility_table'];
if (
!$stmtInsHeaders->bind_param(
'iisssiiii',
$hId,
$hModelId,
$fieldKey,
$label,
$valueType,
$orderIndex,
$visibility,
$orderTable,
$visTable
)
|| !$stmtInsHeaders->execute()
) {
$stmtInsHeaders->close();
throw new \RuntimeException('Execute INSERT definitions_header (cliente): ' . $clientMysqli->error);
}
}
$stmtInsHeaders->close();
}
// ------------------------------------------------
// 3) Sincronizar definitions_lines (cliente)
// ------------------------------------------------
$stmtDelLines = $clientMysqli->prepare(
'DELETE FROM definitions_lines WHERE model_id = ?'
);
if (!$stmtDelLines) {
throw new \RuntimeException('Prepare DELETE definitions_lines (cliente): ' . $clientMysqli->error);
}
$stmtDelLines->bind_param('i', $id);
if (!$stmtDelLines->execute()) {
$stmtDelLines->close();
throw new \RuntimeException('Execute DELETE definitions_lines (cliente): ' . $clientMysqli->error);
}
$stmtDelLines->close();
if (!empty($lines)) {
$stmtInsLines = $clientMysqli->prepare(
'INSERT INTO definitions_lines
(id, model_id, field_key, label, value_type,
order_index, visibility)
VALUES (?, ?, ?, ?, ?, ?, ?)'
);
if (!$stmtInsLines) {
throw new \RuntimeException('Prepare INSERT definitions_lines (cliente): ' . $clientMysqli->error);
}
foreach ($lines as $l) {
$lId = (int)$l['id'];
$lModelId = (int)$l['model_id'];
$lFieldKey = (string)$l['field_key'];
$lLabel = $l['label'] !== null ? (string)$l['label'] : null;
$lType = (string)$l['value_type'];
$lOrder = (int)$l['order_index'];
$lVis = (int)$l['visibility'];
if (
!$stmtInsLines->bind_param(
'iisssii',
$lId,
$lModelId,
$lFieldKey,
$lLabel,
$lType,
$lOrder,
$lVis
)
|| !$stmtInsLines->execute()
) {
$stmtInsLines->close();
throw new \RuntimeException('Execute INSERT definitions_lines (cliente): ' . $clientMysqli->error);
}
}
$stmtInsLines->close();
}
// Si todo ha ido bien
$clientMysqli->commit();
} catch (\Throwable $e) {
$clientMysqli->rollback();
throw $e;
}
}
public function Empresa(Request $req, EntityManagerInterface $em)
{
if (!$this->getUser() || !is_object($this->getUser())) {
return $this->redirectToRoute('logout');
}
$id = (int)$req->get("id");
if (!$id) {
$this->addFlash('warning', 'Empresa no encontrada.');
return $this->redirectToRoute("list");
}
$empresa = $em->getRepository(Empresa::class)->find($id);
if (!$empresa) {
$this->addFlash('warning', 'Empresa no encontrada.');
return $this->redirectToRoute("list");
}
$users = $em->getRepository(Usuario::class)->findBy([
"empresa" => $empresa->getId()
]);
// Valores por defecto por si algo falla al conectar con la BD del cliente
$activeUsers = null;
$modulos = [];
$empresaLogo = null;
$extractionModelLabel = null;
$diskUsedBytes = null;
$diskUsedGb = null;
try {
$cx = $empresa->getConexionBD();
if ($cx) {
$mysqli = @new \mysqli(
$cx->getDbUrl(),
$cx->getDbUser(),
$cx->getDbPassword(),
$cx->getDbName(),
(int)$cx->getDbPort()
);
if (!$mysqli->connect_error) {
// parámetros (modulos, límites, etc.)
[$activeUsers, $modulos] = $this->loadEmpresaParametros($mysqli);
// logo guardado en la BD del cliente
$empresaLogo = $this->loadEmpresaLogo($mysqli);
$diskUsedBytes = $this->loadEmpresaDiskUsageBytes($mysqli);
$diskUsedGb = ($diskUsedBytes !== null)
? round($diskUsedBytes / 1024 / 1024 / 1024, 2)
: null;
// Si tiene extracción y modelo seleccionado, buscamos el ID legible del modelo
if (
!empty($modulos['modulo_extraccion']) &&
!empty($modulos['extraction_model'])
) {
try {
$conn = $em->getConnection();
$row = $conn->fetchAssociative(
'SELECT model_id FROM extraction_models WHERE id = :id',
['id' => (int)$modulos['extraction_model']]
);
if ($row && isset($row['model_id'])) {
$extractionModelLabel = $row['model_id'];
}
} catch (\Throwable $e) {
// Si falla, simplemente no mostramos el texto bonito del modelo
$extractionModelLabel = null;
}
}
$mysqli->close();
}
}
} catch (\Throwable $e) {
// Aquí podrías loguear el error si quieres, pero no rompemos la pantalla
}
return $this->render('empresa_detail.html.twig', [
'empresa' => $empresa,
'users' => $users,
'activeUsers' => $activeUsers,
'modulos' => $modulos,
'empresaLogo' => $empresaLogo,
'extractionModelLabel' => $extractionModelLabel,
'diskUsedGb' => $diskUsedGb,
]);
}
public function deleteEmpresa(Request $request, EntityManagerInterface $em){
$id = $request->get("id");
$empresa = $em->getRepository(Empresa::class)->find($id);
$conexion = $empresa->getConexionBD();
$usuarios = $em->getRepository(Usuario::class)->findBy(array("empresa"=>$empresa->getId()));
$hestiaApiUrl = 'https://200.234.237.107:8083/api/';
$owner = 'docunecta'; // o el dueño del hosting
$postFields = http_build_query([
'user' => 'admin',
'password' => 'i9iQiSmxb2EpvgLq',
'returncode' => 'yes',
'cmd' => 'v-delete-database',
'arg1' => 'admin',
'arg2' => $conexion->getDbName(),
]);
$accessKeyId = 'cWYbt9ShyFQ3yVRsUE8u';
$secretKey = 'e2M_5wk2_jUAlPorF7V8zfwo3_0ihu90WoLPMKwj';
$headers = [
'Authorization: Bearer ' . $accessKeyId . ':' . $secretKey
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $hestiaApiUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Solo si usas certificados autofirmados
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if (($error || trim($response) !== '0') && trim($response) !== '3') {
$this->addFlash('danger', 'Error al eliminar la base de datos en HestiaCP: ' . ($error ?: $response));
return $this->redirectToRoute('list');
}
// Eliminar el servicio systemd asociado a la empresa
$company_name = $empresa->getId();
$serviceName = $company_name . "-documanager.service";
$servicePath = "/etc/systemd/system/$serviceName";
$cmds = [
"sudo /bin/systemctl stop $serviceName",
"sudo /bin/systemctl disable $serviceName",
"sudo /bin/rm $servicePath",
"sudo /bin/systemctl daemon-reload"
];
$serviceErrors = [];
foreach ($cmds as $cmd) {
$output = @\shell_exec($cmd . " 2>&1");
if ($output !== null && trim($output) !== '') {
$serviceErrors[] = "CMD OUTPUT: $cmd\n$output";
}
}
$azureService = $company_name . "-azuredi.service";
$servicePathAzure = "/etc/systemd/system/$azureService";
$cmdsAzure = [
"sudo /bin/systemctl stop $azureService",
"sudo /bin/systemctl disable $azureService",
"sudo /bin/rm $servicePathAzure",
"sudo /bin/systemctl daemon-reload",
];
foreach ($cmdsAzure as $cmd) {
$output = @\shell_exec($cmd . " 2>&1");
if ($output) {
$serviceErrors[] = "CMD OUTPUT: $cmd\n$output";
}
}
//eliminamos usuarios
foreach($usuarios as $user){
$em->remove($user);
$em->flush();
}
//eliminamos conexión
$em->remove($conexion);
$em->flush();
//eliminamos empresa
$em->remove($empresa);
$em->flush();
$msg = 'Empresa y base de datos eliminadas correctamente.';
if (count($serviceErrors) > 0) {
$msg .= ' ' . implode('<br>', $serviceErrors);
}
$this->addFlash('success', $msg);
return $this->redirectToRoute('list');
}
public function editEmpresa(Request $request, EntityManagerInterface $em)
{
$id = (int)$request->get('id');
$empresa = $em->getRepository(Empresa::class)->find($id);
if (!$empresa) {
throw $this->createNotFoundException('Empresa no encontrada');
}
// 1) Conectar a la BD del cliente con las credenciales de la central
$cx = $empresa->getConexionBD();
$mysqli = @new \mysqli(
$cx->getDbUrl(),
$cx->getDbUser(),
$cx->getDbPassword(),
$cx->getDbName(),
(int)$cx->getDbPort()
);
if ($mysqli->connect_error) {
$this->addFlash('danger', 'No se puede conectar a la BD del cliente: ' . $mysqli->connect_error);
return $this->redirectToRoute('list');
}
if ($request->isMethod('POST') && $request->request->get('submit') !== null) {
$data = $request->request->all();
// 2) Actualizar SOLO central
$empresa->setName((string)($data['name'] ?? $empresa->getName()));
$empresa->setMaxDiskQuota(
isset($data['maxDiskQuota']) && $data['maxDiskQuota'] !== ''
? (int)$data['maxDiskQuota']
: $empresa->getMaxDiskQuota()
);
$em->persist($empresa);
// ---- Normalización de POST (checkboxes / select) ----
$toBool = fn($v) => in_array(strtolower((string)$v), ['1','on','true','yes'], true);
$extractionModel = 0;
if (isset($data['extraction_model']) && $data['extraction_model'] !== '') {
$extractionModel = (int)$data['extraction_model'];
}
$data['extraction_model'] = $extractionModel;
$data['modulo_extraccion'] = isset($data['modulo_extraccion']) && $toBool($data['modulo_extraccion']) ? 1 : 0;
$data['modulo_etiquetas'] = isset($data['modulo_etiquetas']) && $toBool($data['modulo_etiquetas']) ? 1 : 0;
$data['modulo_calendario'] = isset($data['modulo_calendario']) && $toBool($data['modulo_calendario']) ? 1 : 0;
$data['modulo_calendarioExterno']= isset($data['modulo_calendarioExterno'])&& $toBool($data['modulo_calendarioExterno'])? 1 : 0;
$data['modulo_estados'] = isset($data['modulo_estados']) && $toBool($data['modulo_estados']) ? 1 : 0;
$data['modulo_lineas'] = isset($data['modulo_lineas']) && $toBool($data['modulo_lineas']) ? 1 : 0;
// Dependencias
if (!$data['modulo_calendario']) {
$data['modulo_calendarioExterno'] = 0;
}
if (!$data['modulo_extraccion']) {
$data['modulo_lineas'] = 0;
$data['extraction_model'] = 0;
}
// 3) Guardar en BD del cliente: parametros + license
$this->updateEmpresaParametros($mysqli, $data);
// 3.1) Si extracción activa y hay modelo, sincroniza modelo a BD del cliente
if ($data['modulo_extraccion'] === 1 && $data['extraction_model'] > 0) {
try {
$this->syncExtractionModelToClientDb($mysqli, $em, (int)$data['extraction_model']);
} catch (\Throwable $e) {
// log opcional
}
}
// si prefieres solo UPDATE en license, usa una función updateLicense(); si no, upsert/insert:
$this->updateLicense(
$mysqli,
$data,
$empresa->getName()
);
$em->flush();
$mysqli->close();
// Mensaje de éxito
$this->addFlash('success', 'Empresa editada correctamente.');
return $this->redirectToRoute('app_empresa_show', ['id' => $id]);
}
// 4) GET: precargar desde BD del cliente
[$activeUsers, $modulos] = $this->loadEmpresaParametros($mysqli);
$license = $this->loadLicense($mysqli, $empresa->getName()); // por si quieres mostrarlo
$extractionModels = [];
try {
$conn = $em->getConnection(); // principal
$sql = 'SELECT id, model_id FROM extraction_models ORDER BY model_id';
$extractionModels = $conn->executeQuery($sql)->fetchAllAssociative();
foreach ($extractionModels as &$m) { $m['id'] = (int)$m['id']; }
} catch (\Throwable $e) {
$extractionModels = [];
}
$mysqli->close();
return $this->render('empresa/_edit.html.twig', [
'empresa' => $empresa, // central: name + maxDiskQuota
'id' => $id,
'activeUsers' => $activeUsers, // cliente
'modulos' => $modulos, // cliente
'license' => $license, // opcional
'extraction_models' => $extractionModels,
]);
}
private function loadEmpresaParametros(\mysqli $mysqli): array
{
// claves que nos interesan en la tabla parametros
$keys = [
'activeUsers',
'modulo_etiquetas','modulo_calendario','modulo_calExt',
'modulo_estados', 'modulo_subida', 'modulo_extraccion','modulo_lineas','limite_archivos','extraction_model',
];
$placeholders = implode(',', array_fill(0, count($keys), '?'));
$sql = "SELECT nombre, valor FROM parametros WHERE nombre IN ($placeholders)";
$stmt = $mysqli->prepare($sql);
if (!$stmt) {
throw new \RuntimeException('Prepare SELECT parametros: ' . $mysqli->error);
}
// bind dinámico
$types = str_repeat('s', count($keys));
$stmt->bind_param($types, ...$keys);
if (!$stmt->execute()) {
$stmt->close();
throw new \RuntimeException('Execute SELECT parametros: ' . $stmt->error);
}
$res = $stmt->get_result();
$map = [];
while ($row = $res->fetch_assoc()) {
$map[$row['nombre']] = $row['valor'];
}
$stmt->close();
// defaults seguros
$activeUsers = isset($map['activeUsers']) ? (int)$map['activeUsers'] : 3;
$flags = [
'modulo_etiquetas' => isset($map['modulo_etiquetas']) ? (int)$map['modulo_etiquetas'] : 0,
'modulo_calendario' => isset($map['modulo_calendario']) ? (int)$map['modulo_calendario'] : 0,
'modulo_calExt' => isset($map['modulo_calExt']) ? (int)$map['modulo_calExt'] : 0,
'modulo_estados' => isset($map['modulo_estados']) ? (int)$map['modulo_estados'] : 0,
'modulo_subida' => isset($map['modulo_subida']) ? (int)$map['modulo_subida'] : 0,
'modulo_extraccion' => isset($map['modulo_extraccion']) ? (int)$map['modulo_extraccion'] : 0,
'modulo_lineas' => isset($map['modulo_lineas']) ? (int)$map['modulo_lineas'] : 0,
];
$limiteArchivos = isset($map['limite_archivos']) && $map['limite_archivos'] !== ''
? (int)$map['limite_archivos']
: 500;
// Lo añadimos a $flags para no cambiar la firma del return
$flags['limite_archivos'] = $limiteArchivos;
// extraction_model: default 0 (sin modelo)
$flags['extraction_model'] = isset($map['extraction_model']) && $map['extraction_model'] !== '' ? (int)$map['extraction_model'] : 0;
return [$activeUsers, $flags];
}
private function updateEmpresaParametros(\mysqli $mysqli, array $data): void
{
$toBool = fn($v) => in_array(strtolower((string)$v), ['1','on','true','yes'], true);
$getInt = fn(array $a, string $k, int $d) => (isset($a[$k]) && $a[$k] !== '') ? (int)$a[$k] : $d;
$getFlag = fn(array $a, string $k) => (isset($a[$k]) && (int)$a[$k] === 1) ? 1 : 0;
$paramMap = [
'activeUsers' => $getInt($data, 'maxActiveUsers', 3),
'modulo_etiquetas' => $getFlag($data, 'modulo_etiquetas'),
'modulo_calendario' => $getFlag($data, 'modulo_calendario'),
'modulo_calExt' => $getFlag($data, 'modulo_calendarioExterno'),
'modulo_estados' => $getFlag($data, 'modulo_estados'),
'modulo_subida' => $getFlag($data, 'modulo_subida'),
'modulo_extraccion' => $getFlag($data, 'modulo_extraccion'),
'modulo_lineas' => $getFlag($data, 'modulo_lineas'),
'limite_archivos' => $getInt($data, 'limite_archivos', 500),
'extraction_model' => $getInt($data, 'extraction_model', 0),
];
if ($paramMap['modulo_extraccion'] === 0) {
$paramMap['modulo_lineas'] = 0;
$paramMap['extraction_model'] = 0;
}
if ($paramMap['modulo_calendario'] === 0) {
$paramMap['modulo_calExt'] = 0;
}
$mysqli->begin_transaction();
try {
$stmt = $mysqli->prepare("
INSERT INTO parametros (nombre, valor)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE valor = VALUES(valor)
");
if (!$stmt) {
throw new \RuntimeException('Prepare UPSERT parametros: ' . $mysqli->error);
}
foreach ($paramMap as $nombre => $valor) {
$v = (string)$valor;
if (!$stmt->bind_param('ss', $nombre, $v) || !$stmt->execute()) {
$stmt->close();
throw new \RuntimeException("Guardar parámetro {$nombre}: {$stmt->error}");
}
}
$stmt->close();
$mysqli->commit();
} catch (\Throwable $e) {
$mysqli->rollback();
throw $e;
}
}
private function loadLicense(\mysqli $mysqli): array
{
// Carga opcional para mostrar en la edición: capacityGb/users (u otros)
$sql = "SELECT client, capacityGb, users FROM license LIMIT 1";
$res = $mysqli->query($sql);
if ($res && $row = $res->fetch_assoc()) {
return $row;
}
return [];
}
private function updateLicense(\mysqli $mysqli, array $data, string $clientName): void
{
$capacityGb = isset($data['maxDiskQuota']) ? (int)$data['maxDiskQuota'] : 200;
$activeUsers = isset($data['maxActiveUsers']) ? (int)$data['maxActiveUsers'] : 3;
$stmt = $mysqli->prepare("UPDATE license SET capacityGb = ?, users = ? WHERE client = ?");
if (!$stmt) {
throw new \RuntimeException('Prepare UPDATE license: ' . $mysqli->error);
}
$stmt->bind_param('iis', $capacityGb, $activeUsers, $clientName);
$stmt->execute();
$stmt->close();
}
public function usersListEmpresa(Request $request,EntityManagerInterface $em){
$id = $request->get("id");
$users_empresa = $em->getRepository(Usuario::class)->findBy(array("empresa"=>$id));
return $this->render('empresa/usersList.html.twig',array(
'users' => $users_empresa,
'id'=>$id
));
}
private function loadEmpresaDiskUsageBytes(\mysqli $mysqli): ?int
{
// Si la tabla no existe o hay error, devolvemos null para no romper la vista
$sql = "SELECT COALESCE(SUM(size_bytes), 0) AS total_bytes FROM files";
$res = $mysqli->query($sql);
if (!$res) {
return null;
}
$row = $res->fetch_assoc();
$res->free();
return isset($row['total_bytes']) ? (int)$row['total_bytes'] : 0;
}
}