PortLab

Platform for Open Research Through Live Analysis and Broadcasting

PortLAB

PortLAB es la plataforma con la que, en el CNR-IFAC (Instituto de Física Aplicada «Nello Carrara»), se desarrollan y se gestionan portales científicos: un único motor, escrito en PHP con base de datos PostgreSQL, del que nacen sitios distintos — multilingües, adaptativos y ricos en contenidos interactivos — compartiendo estructura, estilo y herramientas.

Qué es

PortLAB es un framework ligero para la construcción de portales. Cada página se define mediante una pequeña configuración declarativa (en formato JSON) que establece su estructura, sus idiomas, sus bibliotecas y sus niveles de acceso, mientras que los contenidos viven en ficheros separados por idioma. Esto permite añadir páginas y sitios enteros con rapidez, manteniendo la coherencia gráfica y funcional y sin necesidad de tocar el motor.

Proyectos realizados con PortLAB

  • WebNIR (webnir.eu) — portal del CNR-IFAC sobre las radiaciones no ionizantes: campos electromagnéticos (CEM), radiaciones ópticas artificiales (ROA) y dispositivos médicos implantables activos (DMIA), con herramientas para la evaluación de la exposición y del apantallamiento.
  • Purple (purplecodex.eu) — portal sobre los métodos analíticos no invasivos aplicados a los bienes culturales, para el estudio y la catalogación de los códices purpúreos, manuscritos en pergamino teñido de púrpura.
  • UISH — Urban Intelligence Science Hub (uish.ifac.cnr.it) — proyecto del CNR para el gemelo digital de la ciudad: la actividad del CNR-IFAC sobre la calidad del aire en Catania y la validación del modelo AirPortal.
  • Eclipsis (eclipsis.ifac.cnr.it) — portal dedicado al eclipse de Sol del 12 de agosto de 2026 observado desde La Coruña: la cadena de reducción de las fotografías RAW, de los metadatos de las tomas a las efemérides, ejecutada en el servidor.
  • Satellite (satellite.ifac.cnr.it) — el espacio de trabajo reservado de una propuesta de proyecto con varias entidades: entidades participantes, paquetes de trabajo, personas y horas-persona, costes, con el diagrama de Gantt construido a partir de los datos.
  • Consola de administración — la interfaz de gestión de la propia plataforma, que alberga también una sección didáctica con ejemplos interactivos.

Arquitectura por proyectos y personalización

PortLAB se organiza en torno a un sitio principal que hace de base compartida para todos los proyectos secundarios alojados en la plataforma. Cada proyecto dispone de sus propios contenidos y de sus propias personalizaciones locales, pero hereda automáticamente del sitio principal tanto el código común como las bibliotecas de terceros, compartidas una sola vez y mantenidas de forma centralizada: sin duplicación y con actualizaciones inmediatas para todos. Al mismo tiempo, cada proyecto puede personalizar cualquier elemento en cualquier punto, sustituyendo de forma selectiva e independiente la versión compartida por una versión local propia. El resultado es una arquitectura por capas — elementos locales, comunes y bibliotecas compartidas, libremente combinables — que permite poner en marcha nuevos portales con rapidez manteniendo para cada uno una identidad y un aspecto a medida.

Además del código común y de las bibliotecas, también los servicios y las herramientas que ofrece PortLAB (gestión y visualización de la base de datos, diagrama del esquema, gestión de usuarios, consulta de los registros y demás) pueden heredarse en los proyectos secundarios mediante enlaces simbólicos al proyecto principal: cada nuevo portal dispone así de todo el instrumental sin duplicación, pudiendo en cualquier caso sustituir o ampliar localmente cualquiera de sus partes.

Arquitectura por capas de PortLAB El sitio principal comparte código común, bibliotecas de terceros y servicios con los proyectos secundarios, que añaden contenidos y personalizaciones locales. Proyecto A Contenidos locales Personalizaciones Proyecto B Contenidos locales Personalizaciones Proyecto C Contenidos locales Personalizaciones hereda por enlaces simbólicos SITIO PRINCIPAL — base compartida Código común Bibliotecas de terceros Servicios y herramientas
Arquitectura por capas: base compartida por el sitio principal, personalizable en local por cada proyecto.
Interfaz gráfica (GUI)

La interfaz se compone de elementos estándar basados en Bootstrap — cabeceras, barras de navegación, menús, migas de pan, pies de página y ventanas modales: piezas independientes y reutilizables que se combinan hasta obtener la estructura de página deseada, con coherencia visual y plena adaptabilidad.

Plantillas

La plantilla es el modelo de página: establece qué elementos de interfaz utilizar y cómo maquetarlos, incluido el tipo de menú (horizontal arriba o lateral) y el estilo general. Cada plantilla es una combinación predefinida y reutilizable de componentes, invocada por una página con una simple referencia; al cambiar de plantilla cambian el aspecto y la disposición de toda la página sin tocar sus contenidos.

Anatomía de una página La plantilla combina las piezas de interfaz: cabecera, barra de navegación con menú, migas de pan, cuerpo con menú lateral opcional y pie de página. Header Navbar + menú Migas de pan Menú lateral(opcional) Cuerpo de la página Footer
La plantilla elige y combina las piezas de interfaz (GUI) para formar la página.
Descriptor de página

Cada página se declara mediante un pequeño fichero JSON, y la declaración no es un trámite: sin ese fichero la página no existe para la plataforma y su dirección responde «no encontrada». Es el primer eslabón del control de acceso. El descriptor indica qué plantilla usar, quién puede abrir la página, qué cargar y en qué orden, y cómo se presenta la página en el menú.

{
  "template": 18,
  "profiles": [1],
  "flags": { "menu_order": 3 },
  "libs": {
    "css":    ["/css/common/editor_svg.css", "/css/common/diagram.css"],
    "js":     ["/js/libs/ace-builds-master/src-min-noconflict/ace.js"],
    "js_end": ["/js/common/editor_svg.js", "/js/common/diagramma.js", "/js/common/diagram_ui.js"]
  }
}
  • template — el modelo de página descrito más arriba.
  • profiles — los perfiles autorizados. La lista vacía significa página pública, no página inaccesible: es la única apertura, y hay que escribirla a propósito.
  • libs — las hojas de estilo y los scripts propios de la página, en el orden exacto en que deben cargarse, con js antes del cuerpo de la página y js_end después. La base común a todas las páginas (jQuery, Bootstrap, select2, el menú, la búsqueda, el acceso, el idioma, las ventanas) no se enumera: la escribe la plataforma antes de estas, y así se actualiza en un solo lugar.
  • flags.hide_in_menu — la página sigue siendo accesible por dirección pero no aparece en el menú.
  • flags.menu_order — el lugar de la página entre los ficheros de su carpeta: número creciente y, a igualdad o en ausencia de número, orden por nombre. El nombre deshace siempre el empate, de modo que un número equivocado desplaza una entrada y no reordena la sección. Sirve para mantener el orden fuera de los nombres de los ficheros, que son también direcciones. Las carpetas, que no tienen json, toman el número del mapa menu_order de protect/struct.json del proyecto; con un número se mezclan con los ficheros, y así una página puede ir también después de una carpeta.

Todo lo demás no es configuración sino ficheros reconocidos por el nombre: el cuerpo de la página, uno por idioma; un diccionario para las cadenas de la interfaz Javascript; y, si hace falta, un fichero de lógica en el servidor. Añadir una página significa por tanto crear ficheros, no modificar un registro central — y es la razón por la que el menú, las migas de pan y el motor de búsqueda la encuentran solos.

Usuarios, accesos y navegación

Gestión de usuarios multiperfil

A cada usuario se le pueden asociar varios perfiles de autorización. Un recurso es accesible cuando al menos uno de los perfiles del usuario coincide con uno de los perfiles exigidos por el recurso; las páginas sin perfiles siguen siendo públicas. El mismo criterio gobierna de manera uniforme el acceso a páginas y secciones, las entradas de menú, los ficheros descargables e incluso la visibilidad de porciones concretas de una página: así, una misma página puede mostrar contenidos distintos a usuarios con perfiles distintos.

Autenticación y contraseñas

Cada sitio pertenece a un clúster de autenticación, que indica dónde se guardan las credenciales de los usuarios, con qué proveedores de identidad externos se puede entrar y qué reglas valen para las contraseñas. Los sitios de un mismo clúster comparten las cuentas; un clúster libre no tiene autenticación, y sus sitios no tienen ni acceso ni registro.

Las reglas de las contraseñas se eligen por clúster: la longitud mínima, las clases de caracteres exigidas (contadas en Unicode, así que valen también las letras con acento y otros alfabetos), la prohibición de los datos personales y de las contraseñas más comunes, la caducidad, el historial de las últimas contraseñas y cuánto debe diferir la nueva de la anterior. El hash se calcula con Argon2id donde PHP lo ofrece, y una contraseña no se trunca nunca. Quien entra con una contraseña caducada, o que ya no cumple las reglas, debe cambiarla antes de recibir la sesión.

Acceso único y proveedores externos

Un sitio puede hacer de centro del acceso único: los sitios de su clúster que se adhieren reconocen a quien ha entrado allí y reciben la sesión mediante un billete de un solo uso válido durante sesenta segundos. Cada sitio mantiene su propia cookie de sesión, y la salida vale para todos. En el centro se puede entrar también con un proveedor de identidad externo (OpenID Connect; están previstos ORCID y Google). El proveedor no crea cuentas: una identidad externa abre la sesión solo si el usuario, tras entrar con la contraseña, la ha vinculado a su propia cuenta.

Área reservada

Las páginas públicas están netamente separadas del código de la aplicación y de los contenidos reservados, custodiados en un área situada fuera de la raíz web y, por tanto, inalcanzable directamente desde el navegador. Los documentos reservados nunca se exponen como URL estáticas: se entregan exclusivamente mediante procedimientos del servidor que, antes del envío, comprueban la identidad y los perfiles del usuario.

Seguridad y cifrado

Las defensas son transversales: sesiones protegidas con testigo anti-CSRF, regeneración del identificador de sesión y cookies con atributos de seguridad (HttpOnly, Secure, SameSite); contraseñas custodiadas solo como hash; parámetros de las consultas validados y acotados. Los contenidos reservados y los enlaces «de capacidad» están protegidos con cifrado estándar (AES-256-GCM: cada testigo lleva su finalidad y su caducidad, y no vale para otro uso) y claves mantenidas fuera del código fuente. El control de acceso es fail-closed: una página es accesible solo si está configurada explícitamente, y también los puntos de cálculo en el servidor están sujetos a los perfiles del usuario. Los errores tienen páginas propias (400, 403, 404, 413, 500 y 503), que no dependen ni de la sesión ni de la base de datos; la de página no encontrada devuelve al inicio.

Contra quien prueba contraseñas está el bloqueo de los intentos: cinco errores en quince minutos bloquean la cuenta durante quince minutos, pero solo desde los navegadores que el sitio no conoce. El navegador desde el que el usuario ya ha entrado se reconoce por una cookie cifrada y tiene su propio contador, de modo que quien prueba contraseñas en una cuenta no bloquea a su titular, que entra desde su navegador habitual. Ninguna respuesta dice si una dirección está registrada.

Cada respuesta lleva las cabeceras de seguridad: HSTS y nosniff desde Apache, X-Frame-Options, Referrer-Policy, Permissions-Policy y la Content Security Policy desde PHP. La política admite scripts, estilos, imágenes, fuentes y conexiones solo del propio sitio y de los orígenes que declara; empieza en modo solo informe, con las violaciones recogidas en un registro que el Diagnóstico resume, y se aplica cuando el registro queda limpio. Junto a ella se prueba otra más severa, que exige en cada script de la página un nonce nuevo en cada petición.

Generación dinámica de los menús de navegación

El menú no se escribe a mano: se construye automáticamente explorando el árbol de carpetas y páginas. Cada página nueva aparece sola, organizada en varios niveles según el anidamiento; las entradas se filtran según el perfil del usuario y una marca de exclusión específica, y los títulos se toman de la página en el idioma elegido. Sin números van primero los ficheros y después las subcarpetas, cada grupo por orden de nombre. Una página toma un número de su descriptor, una carpeta del mapa del proyecto: las entradas numeradas, ficheros o carpetas, van primero, por número y, a igualdad, por nombre. Una sección se reordena así cambiando un número, sin renombrar ficheros ni mover direcciones.

Contenidos, búsqueda y privacidad

Soporte multilingüe

Los idiomas disponibles se declaran página por página. El cuerpo de cada página reside en un fichero separado para cada idioma, mientras que las cadenas de la interfaz se gestionan mediante un sistema de traducción por diccionarios que funciona de manera idéntica en PHP y en Javascript: la misma función y los mismos diccionarios están disponibles tanto en el servidor como en el navegador. Así es posible traducir al vuelo cualquier cadena, procedente de cualquier sección de la página — incluidas las producidas por programas en Python o C++ ejecutados a través de la pasarela (gw2py) y devueltas a la página.

Se dispone además de una extensión para las cadenas paramétricas: el texto traducido contiene marcadores numerados (p. ej. {0}, {1}) sustituidos en tiempo de ejecución por los valores reales. Es un detalle esencial, porque en idiomas distintos el parámetro puede ocupar una posición distinta dentro de la frase: el marcador se coloca donde hace falta en cada traducción, de modo que el texto sigue siendo natural (por ejemplo «{0} elementi trovati» en italiano pasa a ser «found {0} items» en inglés, con el valor en lugares distintos). Toda la cadena de tratamiento está en UTF-8, por lo que admite cualquier alfabeto; los idiomas en uso hoy son el italiano, el inglés y el español, y cada portal tiene uno, dos o tres, con un selector de idioma con bandera y sigla.

Búsqueda textual interna

Al teclear al menos tres caracteres, el sistema examina las páginas del sitio en el idioma seleccionado (o en todos, con la opción correspondiente) y muestra los resultados en una ventana dedicada. De cada página se presenta un extracto con título, palabras clave, descripción y texto en el que aparece el término buscado, resaltado en su contexto, con enlace directo a la página. Además del texto, la búsqueda mira los datos que la página incorpora y, con la opción «Incluir SVG», los textos de los dibujos SVG que la página muestra. La búsqueda trata correctamente los alfabetos y los caracteres internacionales (UTF-8) y puede dirigirse, página por página, solo a los párrafos relevantes.

Gestión del RGPD

PortLAB incluye una gestión del RGPD lista para usar: una política de privacidad y cookies específica y un aviso de consentimiento reutilizable por todos los sitios. La plataforma está preparada para el uso de solo cookies técnicas, sin perfilado ni terceros; el aviso es puramente informativo, no bloquea la navegación y recuerda que se ha leído durante un periodo preestablecido.

Herramientas para la base de datos (PostgreSQL)

Gestión de las tablas

Una única interfaz reutilizable permite crear, modificar, eliminar y consultar las filas de las tablas de la base de datos, con herramientas de filtrado multicriterio, ordenación personalizable, paginación e importación/exportación de los criterios en formato JSON. Las consolas que lo prevén exportan a CSV las filas filtradas, para los perfiles que el sitio autoriza.

Visualización y exploración de los datos

Un visualizador completo de la base de datos reúne cuatro herramientas: un navegador que explora bases de datos, esquemas, tablas, vistas, vistas materializadas y funciones mediante un árbol interactivo y filtros en cascada (mostrando estructura, tipos, claves, tamaños y definiciones); un ejecutor de consultas SQL con editor avanzado, ejecución directa y exportación de los resultados en CSV; una biblioteca de consultas guardadas invocables con un clic; y una búsqueda por palabras clave en todos los campos, con filtros por esquema, tabla, campo y tipo de dato.

Diagrama gráfico del esquema

Un visualizador gráfico genera un diagrama interactivo del esquema: cada tabla o vista es un nodo con la lista de los campos y la clave primaria resaltada. Los nodos se arrastran libremente por el plano de trabajo y los enlaces entre los campos relacionados se trazan como líneas; disposición, colores y enlaces se guardan y se restauran en cada acceso.

Administración y diagnóstico

Consulta de los registros del servidor

Una consola de administración permite consultar los registros de Apache — accesos y errores — filtrando las líneas por inclusión o exclusión de una cadena, con la posibilidad de ocultar los mensajes informativos o de vaciar el registro; una sección específica muestra la configuración PHP del servidor. El acceso está reservado a los administradores.

Mensajes de error normalizados

Cada punto crítico del código — consultas SQL, operaciones PHP, llamadas a la pasarela Python, scripts en el cliente — está marcado con un código/índice: cuando algo va mal aparece un mensaje uniforme que indica ese código, lo que permite remontarse al punto exacto en que se ha producido el problema. Los mensajes están diferenciados por perfil: el usuario ve un aviso esencial con el código, mientras que al administrador se le muestran además los detalles técnicos (tipo, fichero, línea, texto de la consulta) útiles para la depuración. El mecanismo es uniforme para las páginas y para las llamadas AJAX.

Tecnologías

El núcleo del funcionamiento en el servidor es una pasarela que permite a las páginas invocar, de manera uniforme, programas y procesos escritos en lenguajes distintos — hasta ahora Python y C++ — devolviendo sus resultados (por lo general en formato JSON) directamente en la página. La pasarela se usa de forma generalizada junto con la interfaz AJAX de jQuery: esto ha normalizado de hecho las llamadas en todo el portal, con un único esquema petición/respuesta para cualquier operación dinámica.

  • Motor en PHP sobre Apache/Linux, base de datos PostgreSQL; front-end sobre Bootstrap 5 y jQuery.
  • Páginas configuradas de forma declarativa (JSON): plantilla, metadatos, bibliotecas y perfiles de acceso, sin modificar el motor.
  • Componentes reutilizables y cálculo en el servidor mediante una pasarela Python: las páginas pueden lanzar simulaciones y procesos y mostrar sus resultados.
  • Contenidos científicos interactivos: gráficos (ECharts), fórmulas en LaTeX (KaTeX), visualizaciones 3D (three.js), código con resaltado de sintaxis, tablas de resultados que se descargan en PNG o XLS, compuestas en el navegador sin pasar por el servidor.
  • Asistente para poner en marcha nuevos portales; diseño adaptativo para escritorio y móvil.
Pila tecnológica El navegador dialoga por HTTP/AJAX con el servidor Apache/PHP, que consulta la base de datos PostgreSQL y delega los cálculos en una pasarela que ejecuta programas en distintos lenguajes. Navegador Bootstrap · jQuery ECharts · KaTeXthree.js · highlight Servidor Apache + PHP Motor PortLAB Perfiles · sesión PostgreSQL Datos · usuariosesquemas, tablas, vistas Pasarela (gw2py) cálculo: Python, C++ HTTP / AJAXSQL
Del navegador a la base de datos: el motor PHP orquesta datos y cálculo; la pasarela ejecuta programas en varios lenguajes.

PortLAB se instala en un servidor propio con un script. Aquí preparas el zip: la copia de PortLAB y los parámetros de tu instalación, en un solo archivo.

Lo que escribes en esta página se queda en tu navegador. Los campos no se envían a ningún servidor, tampoco al nuestro. El zip lo compone tu navegador: toma del sitio la copia de PortLAB, que es igual para todos, y le añade el archivo de los parámetros. El zip contiene las contraseñas que escribes: guárdalo como una contraseña y bórralo después de la instalación.

En tres pasos

  1. Rellena los parámetros aquí abajo y pulsa «Preparar el zip».
  2. Copia el zip al servidor y descomprímelo.
  3. Lanza el script como root. Instala los paquetes, crea la base de datos, configura Apache y el certificado, y al final prueba la página de inicio.
La descarga de la copia está reservada a los administradores del portal. Las instrucciones de abajo valen para todos.

Instrucciones

Qué hace falta
  • Un servidor con Ubuntu 26.04 LTS y acceso como root. Ubuntu 26.04 tiene en su archivo PHP 8.5 y PostgreSQL 18, así que no hacen falta repositorios externos.
  • Un nombre de dominio que apunte al servidor solo hace falta para el certificado de Let's Encrypt, que comprueba el dominio en el puerto 80: ese puerto tiene que ser accesible desde Internet. Sin él, el script pone un certificado autofirmado. El sitio responde en el 443, que tiene que estar abierto para quien lo usa.
  • Salida a Internet durante la instalación: el script descarga los paquetes de Ubuntu (apt), las bibliotecas PHP (Composer) y las de Python (pip).
  • Un servidor de correo para los correos del sitio, con host, puerto y cifrado y, si los pide, usuario y contraseña. Sin él, el sitio funciona, pero el registro y la recuperación de la contraseña no envían correos.
Los comandos

Desde tu ordenador, copia el zip al servidor y entra:

scp portlab-<nombre>.zip usuario@servidor:
ssh usuario@servidor

En el servidor, descomprime y lanza el script como root:

python3 -m zipfile -e portlab-<nombre>.zip .
sudo bash portlab/webadmin/install/install.sh
rm portlab-<nombre>.zip

Para descomprimir basta Python, que Ubuntu ya tiene. La última línea borra el zip, que contiene las contraseñas; el archivo de los parámetros lo borra el script cuando termina.

Qué hace el script
  1. Comprueba los parámetros. Si alguno no vale, se detiene y los enumera, antes de tocar nada. La contraseña del administrador sigue las reglas iniciales del sitio: al menos 8 caracteres, con una minúscula, una mayúscula, una cifra y un carácter que no sea letra ni cifra, y como máximo 72 bytes si el PHP del servidor no tiene Argon2id.
  2. Instala los paquetes: Apache con PHP y la pasarela Python, PostgreSQL, Composer, WeasyPrint, las fuentes para los PDF, pdftotext y certbot.
  3. Crea el usuario y copia los archivos en la carpeta elegida, que pasa a ser la carpeta personal del usuario, con las bibliotecas PHP y Python.
  4. Crea la base de datos: los cuatro roles (el propietario sin login, el usuario personal, los roles con los que las páginas escriben y leen), el esquema, los datos de referencia (idiomas, convenciones locales, expresiones regulares, perfiles), el clúster de autenticación con las reglas iniciales de las contraseñas, el sitio con sus idiomas y sus perfiles, la conexión a la base de datos y el administrador.
  5. Escribe la configuración: la clave de los tokens, las credenciales de la base de datos para el servidor web, el nombre del sitio, el correo, robots.txt y security.txt.
  6. Configura Apache con el vhost que espera el Diagnóstico, le abre la carpeta de PortLAB para escribir con una regla de systemd (hace falta si la carpeta está bajo /home, que Ubuntu 26.04 deja de solo lectura para Apache; en otro sitio no cambia nada) y pide el certificado.
  7. Ajusta los permisos de las carpetas en las que escribe el servidor web y programa los trabajos nocturnos: las limpiezas y el índice de la búsqueda.
  8. Prueba la página de inicio e imprime la dirección del sitio. Si la contraseña del administrador la generó el script, la imprime aquí, una sola vez.
Después de la instalación

Abre el sitio, entra con la dirección y la contraseña del administrador y abre Gestión > Proyectos > Diagnóstico. Dice lo que queda por arreglar, y para muchas comprobaciones da el comando que lo arregla.

Si algo no va
  • Se puede volver a lanzar. El script repite los pasos sin borrar nada: la clave de los tokens sigue siendo la misma, y el esquema de la base de datos no se vuelve a cargar si ya está. Los parámetros, eso sí, hay que volver a ponerlos, porque el archivo se ha borrado.
  • El certificado no llega cuando el dominio todavía no apunta al servidor o el puerto 80 está cerrado. Se queda el autofirmado; arreglado el DNS, basta con volver a lanzar el script.
  • La página de inicio no responde: el motivo está en /var/log/apache2/error.log.
  • El registro dice «Read-only file system»: la carpeta de PortLAB está bajo /home, /usr o /etc y a Apache le falta la regla de systemd que se la abre (véase Configurar a mano).
Los paquetes

El script los instala por sí solo. Quien prepare el servidor a mano, o quiera saber qué se instala, encuentra aquí la lista completa.

Paquetes del sistema (Ubuntu 26.04)
PaquetePara qué sirve
apache2el servidor web
libapache2-mod-php8.5PHP dentro de Apache, para las páginas
libapache2-mod-wsgi-py3la pasarela Python gw2py dentro de Apache
php8.5-cliPHP en línea de comandos: el script, el índice de la búsqueda, los trabajos nocturnos
php8.5-pgsqlPHP con PostgreSQL
php8.5-gdlas imágenes: miniaturas y gráficos
php8.5-mbstringlos textos en UTF-8
php8.5-curllas peticiones del servidor a otros sitios: Diagnóstico, acceso con proveedores externos
php8.5-xmlXML, para las hojas de cálculo y las importaciones
php8.5-ziplos archivos zip de las hojas de cálculo
php8.5-bcmathla aritmética de precisión arbitraria
php8.5-intlfechas y números escritos según el idioma
postgresqlla base de datos, con las extensiones pg_trgm, dblink y postgres_fdw
python3-venv, python3-pipel entorno Python de la pasarela y sus paquetes
composer, unziplas bibliotecas PHP: PHPMailer para el correo, PhpSpreadsheet para las hojas de cálculo
rsyncla copia de los archivos en la carpeta elegida
ssl-certel certificado autofirmado, si Let's Encrypt no responde
certbotel certificado de Let's Encrypt y su renovación
librsvg2-binrsvg-convert, que hace los PDF de los editores de diagramas y de Gantt
fonts-ibm-plex, fonts-roboto, fonts-jetbrains-mono, fonts-nunitolas fuentes de esos PDF
weasyprintlos PDF hechos a partir de páginas HTML, para las exportaciones de los proyectos
poppler-utilspdftotext, que lee el texto de los PDF del sitio para la búsqueda
g++, makelos programas C++ que la pasarela puede ejecutar
curlla prueba de la página de inicio al final de la instalación
ca-certificateslas autoridades que firman los certificados: correo, Let's Encrypt, CDN

A mano, todos juntos:

sudo apt install apache2 libapache2-mod-php8.5 libapache2-mod-wsgi-py3 \
  php8.5-cli php8.5-pgsql php8.5-gd php8.5-mbstring php8.5-curl php8.5-xml php8.5-zip \
  php8.5-bcmath php8.5-intl postgresql python3-venv python3-pip composer unzip rsync ssl-cert \
  certbot librsvg2-bin fonts-ibm-plex fonts-roboto fonts-jetbrains-mono fonts-nunito poppler-utils \
  weasyprint g++ make curl ca-certificates
Paquetes Python de la pasarela gw2py

La pasarela funciona en un entorno Python propio, <carpeta>/venvs/webadmin, con el mismo Python que mod_wsgi. Los paquetes están en wsgi-scripts/requirements.txt:

PaqueteVersiónPara qué sirve
multipart2.0.0lee los formularios y archivos que las páginas envían a la pasarela: es el único paquete que necesita gw2py
psycopg2-binary2.9.12PostgreSQL desde Python, para el generador de las consolas (gen_table_page.py)
waitress3.0.2solo para el desarrollo: la pasarela sin Apache (pyapp_server.py)

El resto de la pasarela (la autorización, los trabajos largos, el conversor de PDF, el control de las imágenes) usa solo la biblioteca estándar de Python. A mano:

python3 -m venv /srv/portlab/venvs/webadmin
/srv/portlab/venvs/webadmin/bin/pip install -r /srv/portlab/wsgi-scripts/requirements.txt
Configurar a mano Apache, PHP y PostgreSQL

El script lo hace todo por sí solo. Para quien prefiera hacerlo a mano, o quiera ver qué escribe, el zip tiene en la carpeta portlab/conf los archivos ya rellenados con los parámetros del formulario. Los comandos de abajo usan los valores predeterminados (la carpeta /srv/portlab, la base de datos boot): con otros valores hay que cambiarlos.

Apache
  • conf/apache/<nombre>.conf: el vhost http, que manda todo a https salvo las pruebas con las que Let's Encrypt comprueba el dominio;
  • conf/apache/<nombre>-ssl.conf: el vhost https, con la pasarela Python, las cabeceras de seguridad y las carpetas protegidas. Es el que espera el Diagnóstico.

Primero el vhost http y el certificado, luego el https, que ya nombra los archivos del certificado:

sudo a2dismod mpm_event
sudo a2enmod mpm_prefork ssl headers wsgi php8.5
sudo cp portlab/conf/apache/*.conf /etc/apache2/sites-available/
sudo a2dissite 000-default
sudo a2ensite <nombre> && sudo systemctl reload apache2
sudo certbot certonly --webroot -w /srv/portlab/website -d <dominio> -m <correo> --agree-tos
sudo a2ensite <nombre>-ssl && sudo apachectl configtest && sudo systemctl reload apache2

Con el certificado autofirmado el paso de certbot no hace falta: el vhost https usa el del paquete ssl-cert.

En Ubuntu 26.04 systemd encierra a Apache en una jaula: entre otras cosas /home, /usr y /etc son de solo lectura, y el sitio no puede escribir en sus carpetas aunque los permisos sean los correctos (en el registro: «Read-only file system»). La regla conf/systemd/portlab-<nombre>.conf le vuelve a abrir la carpeta de PortLAB; dónde se escribe lo siguen decidiendo los permisos de los archivos. Con la carpeta por defecto, /srv/portlab, la regla no hace falta y no cambia nada: el script la escribe de todos modos.

sudo mkdir -p /etc/systemd/system/apache2.service.d
sudo cp portlab/conf/systemd/portlab-<nombre>.conf /etc/systemd/system/apache2.service.d/
sudo systemctl daemon-reload && sudo systemctl restart apache2
PHP

La configuración está en dos archivos, uno para el sitio y uno para la línea de comandos:

sudo cp portlab/conf/php/apache2/99-portlab.ini /etc/php/8.5/apache2/conf.d/
sudo cp portlab/conf/php/cli/99-portlab.ini /etc/php/8.5/cli/conf.d/
sudo systemctl reload apache2

El archivo del sitio tiene también session.upload_progress.enabled = Off: el avance de las subidas escrito en la sesión es la manera clásica de hacer que PHP escriba bytes elegidos por el atacante en un archivo de sesión (lo señaló también el CSIRT italiano en octubre de 2026); no es un defecto que una actualización corrija, es el funcionamiento previsto, y PortLAB no lo usa: el porcentaje lo calcula el navegador. Hay que apagarlo donde quiera que corra PHP. Con PHP-FPM basta un archivo cualquiera en /etc/php/<versión>/fpm/conf.d/ con esa línea, y luego systemctl reload php<versión>-fpm; para cerrarlo también ante un .user.ini se escribe en el pool php_admin_flag[session.upload_progress.enabled] = off. Con mod_php y AllowOverride None un .htaccess no puede volver a encenderlo. Se comprueba con php-fpm<versión> -i | grep upload_progress, o en la página phpinfo del sitio.

AjusteValorPor qué
post_max_size, upload_max_filesize200Mlos archivos de medida y las imágenes que se suben
max_file_uploads200muchos archivos en una sola subida
memory_limit512Mel límite de una petición; las páginas que leen archivos grandes lo suben por sí solas
session.gc_maxlifetime43200la sesión dura 12 horas sin actividad, como quiere PortLAB
expose_php, display_errorsOffni versión en las respuestas ni errores en las páginas: van al registro de Apache
date.timezonela zona horaria del formulariolas fechas de los correos y de los registros
pcre.jit0solo para el sitio: la jaula de systemd de Apache no da memoria ejecutable, y la compilación de las expresiones regulares llenaría el registro de avisos

Las extensiones necesarias son pgsql, gd, mbstring, curl, xml, zip, bcmath e intl (los paquetes php8.5-*), más sodium y openssl, que PHP ya tiene. El Diagnóstico las comprueba una por una.

PostgreSQL

PortLAB quiere PostgreSQL 18 en la misma máquina, accesible por TCP en localhost con una contraseña. En Ubuntu los valores predeterminados ya sirven:

  • listen_addresses = 'localhost' en postgresql.conf: la base de datos no se abre a la red;
  • host all all 127.0.0.1/32 scram-sha-256 en pg_hba.conf: las páginas entran con las contraseñas del .pgpass.

La base de datos la crean los archivos de webadmin/install/sql, en este orden. Cada uno dice al principio qué variables de psql necesita (nombres y contraseñas de los roles, el sitio, el administrador): install/database.sh las pasa por sí solo.

  1. 01_database.sql, como postgres: los cuatro roles, la base de datos (del propietario) y las extensiones pg_trgm, dblink y postgres_fdw. El propietario no tiene login ni contraseña; el usuario personal es miembro suyo sin heredar sus privilegios, y el rol fijado para la base de datos lo hace trabajar como propietario en cuanto entra (pgAdmin, QGIS, psql); los roles de las páginas no pueden convertirse en él;
  2. 02_schema.sql, con el rol del propietario, que es dueño de todo;
  3. 03_permessi.sql, como postgres: filas, no tablas, para el rol que escribe (SELECT, INSERT, UPDATE, DELETE, TRUNCATE y MAINTAIN, nada de CREATE, ALTER, DROP), solo lectura para el rol que lee, también para los objetos que el propietario creará;
  4. 04_dati.sql, como postgres: idiomas, banderas, convenciones locales, zonas horarias, las magnitudes físicas con sus unidades y los prefijos del SI (esquema metrology), expresiones regulares y perfiles;
  5. 05_sito.sql, con el rol que escribe: el clúster de autenticación 1 «PortLAB», con los usuarios en el esquema users de boot, las reglas iniciales de las contraseñas y ningún proveedor externo; el sitio, sus idiomas y sus perfiles; la conexión boot; el administrador.

El servidor web encuentra las credenciales en /var/www/.pgpass, de www-data con permisos 0600. Cada línea lleva encima una cabecera: #conn_p para el rol que lee, #conn_a para el que escribe.

#conn_p
localhost:5432:boot:portlab_ro:<contraseña>
#conn_a
localhost:5432:boot:portlab_rw:<contraseña>

En una máquina dedicada se le puede dar a shared_buffers un cuarto de la memoria; para lo demás bastan los valores de Ubuntu. Las copias de la base de datos se hacen con pg_dump, por ejemplo cada noche: sudo -u postgres pg_dump -Fc boot > /var/backups/boot.dump.

Licencias

El código de PortLAB se distribuye con la licencia EUPL-1.2, los contenidos con CC BY 4.0; las bibliotecas de terceros tienen las suyas, enumeradas en la carpeta LICENSES de la copia. Las bibliotecas JavaScript llegan de jsDelivr, con la huella que comprueba el navegador.

Algunos mecanismos de PortLAB se pueden probar aquí, en esta misma página: lo que ocurre al pulsar los botones es el comportamiento real del sitio, no una simulación.

La búsqueda por palabras clave y $id_search

Cada página declara qué parte de sí misma entra en la búsqueda: el título, las palabras clave, la descripción y solo los trozos del cuerpo enumerados en la variable $id_search. Así los textos se buscan, mientras los controles de la interfaz, las fórmulas y los bloques de código quedan fuera y no contaminan los resultados. Vale para la búsqueda en vivo y para el índice que el sitio construye cada noche. Los dos recuadros de abajo contienen cada uno una palabra inventada, que no aparece en ninguna otra página: el primero está en un trozo del cuerpo enumerado en $id_search, el segundo no. Pulsa los dos botones: la búsqueda del sitio, la de la barra superior, encuentra la primera palabra en esta página y de la segunda no sabe nada.

Trozo enumerado en $id_search

Este recuadro está en $body[2], que la página enumera entre los trozos que se buscan. Su palabra marcador es zampillorio.

Trozo fuera de $id_search

Este recuadro está en $body[3], que la página no enumera. Su palabra marcador es brugnafolo.

Alfabetos y caracteres multibyte

La búsqueda cuenta caracteres y no bytes, y todo el texto del sitio es UTF-8 en forma NFC: funciona con cualquier alfabeto. Las palabras marcador de este recuadro están en griego, ζαμπιλλόριο, en cirílico, зампиллорио, en árabe, زامبيلوريو, y en japonés, ざんぴろりお.

La gestión de los errores

Un error llega a quien usa el sitio en una ventana, con un código que lo identifica en el código fuente (COD. …) y un texto; el detalle técnico, es decir la consulta, el mensaje de PHP, el archivo y la línea, o la excepción del gateway, lo ve solo el administrador (perfil 1), los demás solo el código. Ningún camino devuelve la página HTML de error de Apache dentro de una respuesta: también el gateway Python responde en JSON, con err y txt, y el texto se traduce con el diccionario del sitio. Los botones provocan un error en cada uno de los caminos que puede tomar una petición.

  • La página pide al servidor una lista de tres valores y recibe una cadena: gest_result comprueba la forma de cada respuesta antes de usarla, y se detiene.
  • Una rama del archivo top- ejecuta una consulta sobre una tabla que no existe: error_alert responde en JSON con el código COD. HF-01, y el detalle solo al perfil 1.
  • La misma petición, enviada sin el token de la sesión: el servidor la rechaza con un 403 antes de que llegue a la página, como haría con una petición iniciada desde otro sitio.
  • El módulo Python que calcula la hipotenusa recibe un cateto negativo y responde {"error": 1, "message": …}: el error previsto es un dato como los demás, y la página lo muestra.
  • El gateway responde 403 con {"err": "FORBIDDEN", "txt": …}, antes de importar nada; la misma forma tiene el 500 de un módulo que lanza una excepción, con el traceback en el registro del servidor.
  • Las páginas de error HTTP (400, 403, 404, 413, 500 y 503) son páginas autónomas del sitio, en su idioma: la 404 vuelve a la portada a los seis segundos, y los descriptores .json de las páginas el servidor se los niega a todos.

Keywords: PortLAB, CMS, framework PHP, portales científicos, multilingüe, PostgreSQL, CNR-IFAC

Moreno Comelli, CNR-IFAC, 2022-2026


Code & Design by CNR-IFAC - Core by PortLab