Aplicaciones web orientadas a servicios y mashups

Versión para imprimir.

por Gilberto Pacheco Gallegos

1. Conocimientos requeridos

2. Instrucciones de navegación

  • Los siguientes controles te permitirán navegar por todo el contenido del sitio.

    Oculta el menú de navegación.

    Muestra el menú de navegación.

    skip_previous
    arrow_back (Tecla flecha a la izquierda)
    Swipe hacia la derecha

    Regresa a la página anterior.

    skip_next
    arrow_forward (Tecla flecha a la derecha)
    Swipe hacia la izquierda

    Avanza a la página siguiente.

3. Aplicaciones Orientadas a Servicios

Versión para imprimir.

A. Introduccion

  • En esta lección se presentan los conceptos de:

    • aplicaciones web orientadas a servicios y

    • mashups.

B. Nube computacional

  • Es el conjunto de servidores remotos en Internet para gestionar archivos y aplicaciones.

  • Una de las ventajas de una nube es que permite que los usuarios utilicen archivos y aplicaciones sin tener que instalar programas en una computadora con acceso a Internet.

  • El concepto de nube salió de los diagramas de red, donde Internet se representa como una nube, en una abstracción de la infraestructura que representa.

C. Aplicación web

D. Servicio en la nube

Utilidad o función que se utiliza a través de Internet.

E. Tipos de servicios

Software as a Service (SaaS)

El más utilizado. El software está alojado en servidores de los proveedores y el cliente accede a ellos a través del navegador web. Todo lo relacionado con mantenimiento, soporte y disponibilidad es manejado por el proveedor.

Platform as a Service (PaaS)

En este tipo de servicios en la nube el proveedor ofrece acceso a un entorno basado en cloud en el cual los usuarios pueden crear, ejecutar, distribuir y administrar sus propias aplicaciones.

Infrastructure as a Service (IaaS)

Un proveedor de servicios proporciona aceso a recursos como servidores con una capacidad de cómputo contratada, almacenamiento y acceso a red.

F. Servicios que se ofrecen en la nube

  • Almacenamiento

  • Herramientas de Productividad

  • Desarrollo de Aplicaciones

  • Hosting

  • Videoconferencias

  • Mapas

  • Streamming

G. Servicios de nube más usados

Amazon Web Services (AWS)

https://aws.amazon.com/

Microsoft Azure

https://azure.microsoft.com/.

Google Cloud

https://cloud.google.com/.

H. Aplicación orientada a servicios

Es una aplicación en la que algunas partes se realizan en un servidor independiente.

Características

  • El procesamiento se reparte en varias computadoras.

  • Un servicio puede ser utilizado por varias aplicaciones.

I. Aplicación web orientada a servicios

  • Aplicación web en la que algunas operaciones se realizan en el servidor web y sus resultados se descargan al navegador web sin cambiar la página desplegada.

J. Aplicación web hibrida (Mashup)

Es una aplicación que usa otras aplicaciones web.

Estructura

Proveedor de contenidos

Fuente de los datos. Los datos están disponibles vía una API y diferentes protocolos web como RSS, REST y Web Service.

El sitio mashup

Es la nueva aplicación web que provee un nuevo servicio utilizando diferente información y de la que no es dueña.

El navegador web cliente

Interfaz de usuario del mashup. En una aplicación web, el contenido puede ser mezclado por los web browser clientes usando lenguaje web del lado del cliente, por ejemplo, HTML y JavaScript.

Fuente: Wikipedia

K. Resumen

  • En esta lección se tocaron los siguientes temas:

    • Nube computacional.

    • Aplicación web.

    • Servicio en la nube.

    • Tipos de servicios.

    • Servicios que se ofrecen en la nube.

    • Servicios de nube más usados.

    • Aplicación orientada a servicios.

    • Aplicación web orientada a servicios.

    • Aplicación web hibrida (Mashup).

4. Ejemplo de servicio

Versión para imprimir.

A. Introducción

En esta lección se explica el funcionamiento de un servicio muy sencillo.

Puedes probar el ejemplo en https://replit.com/@GilbertoPachec5/srvejemplo?v=1. Hazle fork al proyecto y córrelo.

B. Diagrama de despliegue

Diagrama de despliegue

C. Funcionamiento

1. Iniciamos al ejecutar código en el cliente

index.html

const resp =
 await fetch("servicio.php")
if (resp.ok) {
 const texto =
  await resp.text()
 alert(texto)
} else {
 throw new Error(
  resp.statusText)
}

2. Se invoca el servicio en el servidor

index.html

const resp =
 await fetch("servicio.php")
if (resp.ok) {
 const texto =
  await resp.text()
 alert(texto)
} else {
 throw new Error(
  resp.statusText)
}

Ejecuta fetch y envía request (solicitud).

Request

URL

servicio.php

Method

GET

servicio.php

<?php

echo "Hola";

Despierta y recibe request.

3. El servicio procesa la request y genera la response

index.html

const resp =
 await fetch("servicio.php")
if (resp.ok) {
 const texto =
  await resp.text()
 alert(texto)
} else {
 throw new Error(
  resp.statusText)
}

Hace wait esperando response.

servicio.php

<?php

echo "Hola";

Procesa la request y genera
la response (respuesta).

Response

code
200
body
Hola

El code (o código) 200
indica que terminó
con éxito.

4. El servicio devuelve la response, que es recibida en el cliente

index.html

const resp =
 await fetch("servicio.php")
if (resp.ok) {
 const texto =
  await resp.text()
 alert(texto)
} else {
 throw new Error(
  resp.statusText)
}

Despierta y recibe response.

Response

code
200
body
Hola

Memoria

resp
ok
true
text
Hola

resp tiene lo mismo
que la response,
pero en un objeto
de JavaScript.

servicio.php

<?php

echo "Hola";

Devuelve response y se duerme.

5. Verifica si la conexión terminó con éxito

index.html

const resp =
 await fetch("servicio.php")
if (resp.ok) {
 const texto =
  await resp.text()
 alert(texto)
} else {
 throw new Error(
  resp.statusText)
}

Memoria

resp
ok
true
text
Hola

6. Como la conexión terminó con éxito, recupera el texto de response

index.html

const resp =
 await fetch("servicio.php")
if (resp.ok) {
 const texto =
  await resp.text()
 alert(texto)
} else {
 throw new Error(
  resp.statusText)
}

Memoria

resp
ok
true
text
Hola
texto
Hola

El texto recuperado
queda en la
constante texto.

7. Muestra el texto en un alert

index.html

const resp =
 await fetch("servicio.php")
if (resp.ok) {
 const texto =
  await resp.text()
 alert(texto)
} else {
 throw new Error(
  resp.statusText)
}

Memoria

resp
ok
true
text
Hola
texto
Hola

Alert

Hola

8. Al cerrar el alert, termina el evento

index.html

const resp =
 await fetch("servicio.php")
if (resp.ok) {
 const texto =
  await resp.text()
 alert(texto)
} else {
 throw new Error(
  resp.statusText)
}

D. Hazlo funcionar

E. Archivos

F. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Ejemplo de servicio</title>
10
11</head>
12
13<body>
14 <h1>Ejemplo de servicio</h1>
15
16 <button onclick="ejecuta()">
17 Ejecuta
18 </button>
19
20 <script>
21
22 async function ejecuta() {
23 try {
24 // Se conecta a servicio.php y recibe su respuesta.
25 const resp =
26 await fetch("servicio.php")
27 if (resp.ok) {
28 const texto =
29 await resp.text()
30 alert(texto)
31 } else {
32 throw new Error(
33 resp.statusText)
34 }
35 } catch (error) {
36 console.error(error)
37 alert(error.message)
38 }
39 }
40
41 </script>
42
43</body>
44
45</html>

G. servicio.php

1<?php
2
3echo "Hola";
4

H. Resumen

  • En esta lección se mostró el funcionamiento de un servicio.

5. Arquitectura orientada a servicios

Versión para imprimir.

A. Introducción

  • En esta lección se presentan el concepto y los componentes de una arquitectura orientada a servicios.

B. Arquitectura orientada a servicios

  • Estilo de arquitectura de TI que se apoya en la orientación a servicios.

  • La orientación a servicios es una forma de pensar en servicios, su construcción y sus resultados.

  • Un servicio es una representación lógica de una actividad de negocio que tiene un resultado de negocio específico (ejemplo: comprobar el crédito de un cliente, obtener datos de clima, consolidar reportes de perforación).

Fuente: https://es.wikipedia.org/wiki/Arquitectura_orientada_a_servicios

C. Ventajas

  • Reduce el nivel de acoplamiento.

  • Clara definición de roles de desarrollo.

  • Definición de seguridad más clara.

  • Fácil de probar.

  • Mejora el mantenimiento.

  • Favorece la reutilización.

  • Favorece el desarrollo en paralelo.

  • Favorece la escalabilidad.

  • Permite un mapeo directo entre los procesos y los sistemas.

  • Permite un monitoreo preciso.

  • Permite la interoperabilidad.

Fuente: http://soa-fpuna.blogspot.com/2011/11/ventajas-y-desventajas.html

D. Principios de diseño de un servicio

  • Definición clara y estandarizada mediante descriptores de servicio, donde se indique:

    • Nombre del Servicio.

    • Forma de accesos.

    • Funcionalidades que ofrece.

    • Descripción de los datos de entrada de cada funcionalidad que ofrece.

    • Descripción de los datos de salida de cada funcionalidad que ofrece.

  • Sin estado: no guardan valores para ser usados en otras llamadas. (es posible acceder a bases de datos.)

  • Pueden también ejecutar unidades discretas de trabajo como serían editar y procesar una transacción.

  • No dependen del estado de otras funciones o procesos.

Fuente: https://ederluisdsd.wordpress.com/2013/03/17/principios-de-diseno-soa/

E. Capas

Capas de SOA
Capa de acceso
  • Interfaz de usuario que invoca a los procesos de negocio.

  • Interfaces web, escritorio, móvil, etc.

  • Pueden ejecutarse en varios dispositivos.

Capa de procesos de negocio
  • Integración de uno o más servicios que resuelven un problema de negocio.

Capa de servicio
  • Componentes que se han desarrollado como servicios.

Capa de recursos
  • Recursos operacionales: CRM, ERP, HR, BD.

  • Tecnologías (Java, .NET, PHP).

Fuente: https://www.slideshare.net/imcinstitute/service-oriented-architecture-soa-15-introduction-to-soa

F. Diagrama de Arquitectura

Capas de SOA

G. Introducción a XML

1. XML

  • Siglas en inglés de eXtensible Markup Language, traducido como Lenguaje de Marcado Extensible o Lenguaje de Marcas Extensible.

  • Metalenguaje que permite definir lenguajes de marcas.

  • Desarrollado por el World Wide Web Consortium (W3C).

  • Utilizado para almacenar datos en forma legible.

Fuente: https://es.wikipedia.org/wiki/Extensible_Markup_Language

2. Ejemplo de XML

1<?xml version="1.0" encoding="UTF-8" ?>
2<!DOCTYPE MiContacto SYSTEM "MiContacto.dtd">
3<Contacto>
4 <Nombre>Juan</Nombre>
5 <Email>juan@gm.com</Email>
6 <Telefonos>
7 <Telefono>5555555555</Telefono>
8 <Telefono>8888888888</Telefono>
9 </Telefonos>
10</Contacto>

3. DTD MiContacto

1<?xml version="1.0" encoding="ISO-8859-1" ?>
2<!-- Este es el DTD de Contacto -->
3<!ELEMENT Contacto (Nombre, Email, Telefonos)>
4<!ELEMENT Nombre (#PCDATA)>
5<!ELEMENT Email (#PCDATA)>
6<!ELEMENT Telefonos (Telefono)*>
7<!ELEMENT Telefono (#PCDATA)>

H. Introducción a SOAP

1. SOAP

  • Originalmente siglas de Simple Object Access Protocol, Protocolo de Acceso Simple a Objetos.

  • Hoy en día solo es SOAP, sin un significado especial.

  • Protocolo estándar que define cómo dos objetos en diferentes procesos pueden comunicarse por medio de intercambio de datos XML.

  • Es uno de los protocolos utilizados para implementar servicios web.

Fuente: https://es.wikipedia.org/wiki/Simple_Object_Access_Protocol

2. Estructura de los mensajes de SOAP

Mensajes SOAP
  • Usan formato XML.

  • 3 elementos:

    Envelope o sobre

    El elemento raíz.

    Header o encabezado

    Especificación de como procesar los datos.

    Body o cuerpo

    El conenido del mensaje.

Fuente: https://es.wikipedia.org/wiki/Simple_Object_Access_Protocol

3. Ejemplo de solicitud SOAP

1<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
2 <soap:Header>
3 <t:Transaction xmlns:t="alguna-URI" soap:mustUnderstand="1">
4 5
5 </t:Transaction>
6 </soap:Header>
7 <soap:Body>
8 <getDetalleDeProducto xmlns="http://warehouse.example.com/ws">
9 <productoId>827635</productoId>
10 </getDetalleDeProducto>
11 </soap:Body>
12</soap:Envelope>

4. Ejemplo de respuesta SOAP

1<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
2 <soap:Body>
3 <getRespuestaDetalleDeProducto xmlns="http://warehouse.example.com/ws">
4 <getResultadoDetalleDeProducto>
5 <productId>827635</productId>
6 <nombre>Balón de Futbol</nombre>
7 <descripcion>Balón de futbol de 11” de vinil.</descripcion>
8 <precio>300.0</precio>
9 </getResultadoDetalleDeProducto>
10 </getRespuestaDetalleDeProducto>
11 </soap:Body>
12</soap:Envelope>

I. Introducción a WSDL

1. WSDL

  • Siglas de Web Services Description Language, es un formato de XML que se utiliza para describir servicios Web.

  • Describe los requisitos del protocolo y los formatos de los mensajes necesarios para interactuar con los servicios listados en su catálogo.

  • Las operaciones y mensajes que soporta se describen en abstracto y se ligan después al protocolo concreto de red y al formato del mensaje.

  • Se usa a menudo en combinación con SOAP y XML Schema.

  • Los tipos de datos especiales se incluyen en el archivo WSDL en forma de XML Schema.

  • El cliente puede usar SOAP para hacer la llamada a una de las funciones listadas en el WSDL.

Fuente: https://es.wikipedia.org/wiki/Simple_Object_Access_Protocol

2. Elementos de WSDL

types

Define los tipos de datos usados en los mensajes. Se utilizan los tipos definidos en la especificación XML Schema.

message

Define los elementos de mensaje. Cada mensaje puede consistir en una serie de partes lógicas. Las partes pueden ser de cualquiera de los tipos definidos en el elemento message.

portType

Define las operaciones permitidas y los mensajes intercambiados en el Servicio.

binding

Especifica los protocolos de comunicación usados.

service

Conjunto de puertos y dirección de los mismos. Esta parte final hace referencia a lo aportado por las secciones anteriores.

Con estos elementos no sabemos qué hace un servicio pero sí disponemos de la información necesaria para interactuar con él (funciones, mensajes de entrada/salida, protocolos, etcétera).

3. Ejemplo de WSDL

1<definitions name=“PrecioProd“
2 targetNamespace="http://example.com/stockquote.wsdl"
3 xmlns:tns="http://example.com/stockquote.wsdl"
4 xmlns:xsd1="http://example.com/stockquote.xsd"
5 xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
6 xmlns="http://schemas.xmlsoap.org/wsdl/">
7 <types>
8 <schema targetNamespace="http://example.com/stockquote.xsd"
9 xmlns="http://www.w3.org/2000/10/XMLSchema">
10 <element name="SolPrecioProd">
11 <complexType>
12 <all>
13 <element name="productoId" type="decimal"/>
14 </all>
15 </complexType>
16 </element>
17 <element name="PrecioProd">
18 <complexType>
19 <all>
20 <element name="precio" type="float"/>
21 </all>
22 </complexType>
23 </element>
24 </schema>
25 </types>
26 <message name="InputGetPrecioProd">
27 <part name="body" element="xsd1:SolPrecioProd"/>
28 </message>
29 <message name="OutputGetPrecioProd">
30 <part name="body" element="xsd1:PrecioProd"/>
31 </message>
32 <portType name="PortTypePrecioProd">
33 <operation name="GetPrecioProd">
34 <input message="tns:InputGetPrecioProd"/>
35 <output message="tns:OutputGetPrecioProd"/>
36 </operation>
37 </portType>
38 <binding name="BindPrecioProdSoap" type="tns:PortTypePrecioProd">
39 <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
40 <operation name="GetPrecioProd">
41 <soap:operation soapAction="http://example.com/GetPrecioProd"/>
42 <input>
43 <soap:body use="literal"/>
44 </input>
45 <output>
46 <soap:body use="literal"/>
47 </output>
48 </operation>
49 </binding>
50 <service name="ServPrecioProd">
51 <documentation>Devuelve el precio de un producto.</documentation>
52 <port name="PuertoPrecioProd" binding="tns:BindPrecioProdSoap">
53 <soap:address location="http://example.com/precioprod"/>
54 </port>
55 </service>
56</definitions>

J. UDDI

Este estándar no se usa.

  • Siglas del catálogo de negocios de Internet denominado Universal Description, Discovery and Integration.

  • El registro en el catálogo se hace en XML.

  • Es una iniciativa industrial abierta (sufragada por la OASIS) entroncada en el contexto de los servicios Web.

  • El registro de un negocio en UDDI tiene tres partes:

    Páginas blancas

    dirección, contacto y otros identificadores conocidos.

    Páginas amarillas

    categorización industrial basada en taxonomías.

    Páginas verdes

    información técnica sobre los servicios que aportan las propias empresas.

  • Es uno de los estándares básicos de los servicios web, cuyo objetivo es ser accedido por los mensajes SOAP y dar paso a documentos WSDL, en los que se describen los requisitos del protocolo y los formatos del mensaje solicitado para interactuar con los servicios Web del catálogo de registros.

Fuente: https://es.wikipedia.org/wiki/UDDI

K. Introducción a REST

1. REST

  • Siglas para Representational State Transfer o transferencia de estado representacional.

  • Estilo de arquitectura de software para sistemas distribuidos en la World Wide Web.

Fuentes: https://es.wikipedia.org/wiki/Transferencia_de_Estado_Representacional y https://bbvaopen4u.com/es/actualidad/api-rest-que-es-y-cuales-son-sus-ventajas-en-el-desarrollo-de-proyectos

2. Solicitud HTTP

  • Toda página web tiene una URL asociada, pero también puede representar una entidad, que puede ser una colección de datos, por ejemplo, https://mirest.com/contactos, o un objeto, por ejemplo, https://mirest.com/contactos/69.

  • Para interactuar con la URL hay que enviarle una solicitud, que consta de:

    URL

    Con parámetros opcionales que consisten en nombre y valor asociado, separados por & (como cuando buscas en Google).

    conjunto de encabezados

    Proporcionan información sobre la computadora y el programa que envían los datos, así como la informacion que esperan recibir.

    cuerpo
    carga útil

    Contiene datos adicionales,Pueden representarse en los siguientes formatos:

    • application/x-www-form-urlencoded

    • multipart/form-data

    • text/plain

    • XML

    • JSON

    • YAML

    Método de envío

    Indica la acción que debe realizarse sobre la URL, usando los datos adicionales.

Fuente: https://developer.mozilla.org/es/docs/Web/HTTP/Methods

3. Métodos de envío HTTP

TRACE

Realiza una prueba de enviar un mensaje mensaje al servidor de la URL y recibirlo de regreso sin modificaciones.

OPTIONS

Devuelve una descripción de las opciones de comunicación de laURL.

CONNECT

Establece un tunel bidireccional hacia el servidor.

GET

Devuelve los datos de la colección u objeto asociados con la URL, sin modificar el estado del servidor.

Puede tomarse en cuenta los parámetros que lleva la URL, como si fueran las condiciones de una cláusula WHERE de SQL.

HEAD

Devuelve una respuesta como la de GET, pero sin el cuerpo de la respuesta.

POST

Envía una entidad a una URL.

Si la URL representa una lista que no contiene esa entidad, la agrega.

Los datos de la entidad se indican en el cuerpo de la solicitud.

A menudo causa un cambio o efectos secundarios en el servidor.

PUT

Reemplaza toda la entidad indicada en la URL con el contenido del cuerpo de la solicitud.

PATCH

Modifica parcialmente la entidad de la URL, modificando solamente los datos enviados en el cuerpo de la solicitud.

DELETE

Eliminar la entidad de la URL

Fuente: https://developer.mozilla.org/es/docs/Web/HTTP/Methods

L. Resumen

  • En esta lección se introdujeron los siguientes temas:

    • Arquitectura orientada a servicios

    • Ventajas de una arquitectura orientada a servicios

    • Principios de diseño de un servici

    • Capas de una arquitectura orientada a servicios

    • Diagrama de Arquitectura

    • XML

    • SOAP

    • WSDL

    • UDDI

    • REST

6. Ejemplo de geolocalización

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width">
7 <title>Maps</title>
8</head>
9
10<body>
11 <h1>Maps</h1>
12 <iframe
13 src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d7526.355841517565!2d-98.98640932835467!3d19.40471709504528!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x85d1e319b9413c4f%3A0x1d391ad53ddf65d1!2sUniversidad%20Tecnol%C3%B3gica%20de%20Nezahualc%C3%B3yotl!5e0!3m2!1ses-419!2smx!4v1574352931317!5m2!1ses-419!2smx"
14 width="600" height="450" frameborder="0" style="border:0;"
15 allowfullscreen=""></iframe>
16</body>
17
18</html>

7. Ejemplo de redes sociales

Generado en https://developers.facebook.com/docs/plugins/page-plugin/

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8 <title>Face</title>
9</head>
10
11<body>
12 <h1>Face</h1>
13 <iframe
14 src="https://www.facebook.com/plugins/page.php?href=https%3A%2F%2Fwww.facebook.com%2Ffacebook&tabs=timeline&width=340&height=500&small_header=false&adapt_container_width=true&hide_cover=false&show_facepile=true&appId=437040163649483"
15 width="340" height="500"
16 style="border:none;overflow:hidden"
17 scrolling="no" frameborder="0"
18 allowTransparency="true"
19 allow="encrypted-media"></iframe>
20</body>
21
22</html>

8. Características de un e-commerce

Versión para imprimir.

A. Introducción

En esta lección se presentan las características principales de un e-commerce.

Fuente: https://www.actualidadecommerce.com/las-5-principales-caracteristicas-de-un-sitio-ecommerce-bien-disenado/

B. Facilidad de navegación

  • Cuando se trata de vender productos, el primer requisito a cumplir para una tienda e-commerce es que el comprador tiene que ser capaz de encontrar rápida y de forma especifica, lo que el o ella están buscando. Una navegación eficaz es fundamental para los sitios e-commerce ya que los visitantes que no pueden encontrar lo que buscan se traducen en ventas perdidas.

C. El diseño no destaca por encima de los productos

  • En un negocio e-commerce, el foco de atención siempre debe estar en los productos que están disponibles para su compra. Un diseño que es extravagante sin sentido, generalmente hace más daño que beneficio, puesto que al final, el diseño no es lo que se vende, sino los productos.

D. Fácil proceso de compra

  • La experiencia del usuario en sitios de comercio electrónico es fundamental para el éxito. Es decir, si el proceso de compra implica demasiados pasos o incluso es confuso, los compradores simplemente terminarán abandonando su carrito de compras. Por ello, el checkout siempre debe implicar la mínima cantidad de pasos y debe ser lo más fácil e intuitivo para los compradores.

E. Exhibir los productos más populares

  • Muchos negocios e-commerce se esfuerzan por mostrar los elementos que pueden ser de interés para los potenciales compradores. Un gran número de sitios de comercio electrónico están utilizando espacios muy amplios en la página de inicio para promover las ventas actuales, el lanzamiento de un nuevo producto o lo que sea que genere interés.

F. Fotos de productos a detalle

  • La venta de productos en Internet es diferente de vender en una tienda física porque el comprador no puede tocar o ver en persona el producto antes de tomar la decisión de compra. Al ofrecer fotos de los productos que resaltan a detalle las características principales del mismo, se consigue superar ese inconveniente y se hace más fácil para el comprador decidir si conviene o no comprar el producto.

G. Resumen

  • En esta lección se presentaron las características principales de un e-commerce:

    • Facilidad de navegación.

    • El diseño no destaca por encima de los productos.

    • El diseño no destaca por encima de los productos.

    • Fácil proceso de compra.

    • Exhibir los productos más populares.

    • Fotos de productos a detalle.

9. Servicio que devuelve JSON

Versión para imprimir.

A. Introducción

Este ejemplo recibe los datos JSON generados por un servicio.

Puedes probar el ejemplo en https://replit.com/@GilbertoPachec5/srvresultado?v=1. Hazle fork al proyecto y córrelo.

B. La función invocaServicio

  • El código que se muestra en el ejemplo de servicio se repite en muchos ejemplos posteriores; pero además debe realizar algunas cuastiones adicionales. Para simplificar el trabajo, se introduce la función invocaServicio, que realiza todo lo necesario para controlar la conxión desde el navegador web hacia el servidor.

  • Para usarla, debes importar el archivo donde está definida esta función y luego invocarla pasándole la url del servicio, o alternativamente, pasándole el resultado de invocar la función fetch que comunica al servicio.

  • A lo largo de esta lección y las siguientes, se profundiza en el uso y estructura de esta función.

C. La función ejecutaServicio

  • Para que el desarrollo de servicios del lado del servidor se realice de manera sencilla, uniforme y no repetir código, se introduce la función ejecutaServicio, escrita en PHP.

  • Para usarla, debes importar el archivo donde está definida esta función usando require o require_once y luego invocarla pasándole la función con el código del servicio.

  • A lo largo de esta lección y las siguientes, se profundiza en el uso y estructura de esta clase.

D. Las clases JsonResponse de JavaScript y PHP

  • Cuando un servicio que usa HTTP termina su ejecución, devuelve un conjunto de datos bien definidos por los estándares de internet, al cual llamaremos response o respuesta.

  • Entre los elementos de una response estám el código de estado, el texto de estado, los encabezados y el cuerpo de la respuesta.

  • En estos ejemplos, el cuerpo de la respuesta siempre usa el formato JSON.

  • Para poder manejar adecuadamente esta información, en JavaSvript y en PHP se han creado clases para manejar los datos necesarios.

  • Para usarla en JavaScript, debes importar el archivo donde está definida esta clase.

  • Para usarla en PHP, debes importar el archivo donde está definida esta clase usando require o require_once.

  • A lo largo de esta lección y las siguientes, se profundiza en el uso y estructura de estas clases.

E. Las clases ProblemDetails de JavaScript y PHP

  • Se acuerdo a los estándares de internet, cuando un servicio falla, debe devolver una estructura JSON conocida como Problem Details que informa los detalles del fallo.

  • El tipo de fallo se describe en el campo type, del problem details y, adicionalmente, todo tipo de fallo debe tener una pequeña página donde se describe el problema. En estos ejemplos, se usa la carpeta error para almacenar las páginas que decriben los errores.

  • Para usarla en JavaScript, debes importar el archivo donde está definida esta clase.

  • Para usarla en PHP, debes importar el archivo donde está definida esta clase usando require o require_once.

  • A lo largo de esta lección y las siguientes, se profundiza en el uso y estructura de estas clases.

F. La función muestraError

  • Manejamos los errores en el navegador web y de una manera uniforme con la función muestraError.

  • Cuando se tiene un error normal de JavaScript, se muestra la excepción correspondiente en la consola del navegador y el mensaje de la excepción en un cuadro de alert.

  • Cuando es un ProblemDetails, sus propiedades se muestran en la consola del navegador y en un cuadro de alert.

  • Para usarla, debes importar el archivo donde está definida esta función y colocarla dentro de un catch. Se le pasa como parámetro la excepción atrapada por el catch.

  • A lo largo de esta lección y las siguientes, se profundiza en el uso y estructura de esta función.

G. Diagrama de despliegue

Diagrama de despliegue

H. Funcionamiento

1. Iniciamos al ejecutar código en el cliente

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

2. Se invoca el servicio en el servidor

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

Ejecuta invocaServicio y
envía request (solicitud).

Request

URL
srv/devuelve.php
Method
GET

srv/devuelve.php

return [
 "nombre" => "pp",
 "mensaje" => "Hola."
];

Despierta y recibe request.

3. El servicio procesa la request y genera la response

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

Hace wait esperando response.

srv/devuelve.php

return [
 "nombre" => "pp",
 "mensaje" => "Hola."
];

Procesa la request y
genera response
(respuesta).

Response

code
200
body
{"nombre":"pp","mensaje":"Hola."}

4. El servicio devuelve la response, que es recibida en el cliente

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

Despierta y recibe response.

Response

code
200
body
{"nombre":"pp","mensaje":"Hola."}

Memoria

respuesta
status
200
body
nombre
"pp"
mensaje
"Hola."

srv/devuelve.php

return [
 "nombre" => "pp",
 "mensaje" => "Hola."
];

Devuelve response y se duerme.

5. Se crea la constante body, que apunta al cuerpo de la respuesta

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

Memoria

respuesta
status
200
body
nombre
"pp"
mensaje
"Hola."
body
ref->respuesta.body

6. Muestra el objeto recibido en un alert

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

Memoria

respuesta
status
200
body
nombre
"pp"
mensaje
"Hola."
body
ref->respuesta.body

Alert

Nombre: pp
Mensaje: Hola.

7. Al cerrar el alert, termina el evento

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

I. Hazlo funcionar

J. Archivos

K. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Servicio que devuelve un resultado</title>
10
11</head>
12
13<body>
14
15 <h1>Servicio que devuelve un resultado</h1>
16
17 <p><button onclick="resultado()">Resultado</button></p>
18
19 <script type="module">
20
21 import { invocaServicio } from "./lib/js/invocaServicio.js"
22 import { muestraError } from "./lib/js/muestraError.js"
23
24 async function resultado() {
25 try {
26 const respuesta =
27 await invocaServicio(
28 "srv/devuelve.php")
29 const body = respuesta.body
30 alert(`Nombre: ${body.nombre}
31Mensaje: ${body.mensaje}`)
32 } catch (error) {
33 muestraError(error)
34 }
35 }
36 // Permite que los eventos de html usen la función.
37 window["resultado"] = resultado
38
39 </script>
40
41</body>
42
43</html>

L. Carpeta « srv »

A. srv / devuelve.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4
5ejecutaServicio(function () {
6 return [
7 "nombre" => "pp",
8 "mensaje" => "Hola."
9 ];
10});
11

M. Carpeta « error »

A. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

B. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

N. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

2. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

3. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

4. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

O. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

P. Resumen

  • En esta lección se mostró el funcionamiento de un ejemplo recibe los datos JSON generados por un servicio.

10. Servicio que recibe y devuelve JSON

Versión para imprimir.

A. Introducción

B. La función enviaJson

  • Se introduce la función enviaJson, que convierte un dato en una cadena JSON y la manda desde el navegador web hacia el servidor.

  • Para usarla, debes importar el archivo donde está definida esta función y luego invocarla pasándole la URL, los datos y el método de envío, que por omisión es POST.

  • A lo largo de esta lección, se profundiza en el uso y estructura de esta función.

C. La función leeJson

  • Para que los servicios en el servidor puedan leer un texto en formato JSON, se introduce la función leeJson, escrita en PHP.

  • Para usarla, debes importarla con require o require_once, invocarla y recibir la referencia al objeto recibido.

  • A lo largo de esta lección, se profundiza en el uso y estructura de esta función.

D. Diagrama de despliegue

Diagrama de despliegue

E. Funcionamiento

1. Iniciamos al ejecutar código en el cliente

index.html

const datos = {
 saludo: "Hola",
 nombre: "pp"
}
const respuesta =
 await enviaJson(
  "srv/json.php", datos)
alert(respuesta.body)

2. Se crea la literal de objeto

index.html

const datos = {
 saludo: "Hola",
 nombre: "pp"
}
const respuesta =
 await enviaJson(
  "srv/json.php", datos)
alert(respuesta.body)

Memoria

datos
saludo
"Hola"
nombre
"pp"

3. Se invoca el servicio en el servidor

index.html

const datos = {
 saludo: "Hola",
 nombre: "pp"
}
const respuesta =
 await enviaJson(
  "srv/json.php", datos)
alert(respuesta.body)

Ejecuta fetch y envía
request (solicitud).

Memoria

datos
saludo
"Hola"
nombre
"pp"

Request

URL
srv/json.php
Method
POST
body
{"saludo":"Hola","nombre":"pp"}

srv/json.php

$json = leeJson();
$saludo = $json->saludo;
$nombre = $json->nombre;
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Despierta y recibe request.

4. El servicio lee los datos

index.html

const datos = {
 saludo: "Hola",
 nombre: "pp"
}
const respuesta =
 await enviaJson(
  "srv/json.php", datos)
alert(respuesta.body)

Hace wait esperando response.

Memoria

datos
saludo
"Hola"
nombre
"pp"

srv/json.php

$json = leeJson();
$saludo = $json->saludo;
$nombre = $json->nombre;
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Request

URL
srv/json.php
Method
POST
body
{"saludo":"Hola","nombre":"pp"}

Memoria (Servidor)

$json
saludo
"Hola"
nombre
"pp"
$saludo
"Hola"
$nombre
"pp"

5. El servicio procesa los datos

index.html

const datos = {
 saludo: "Hola",
 nombre: "pp"
}
const respuesta =
 await enviaJson(
  "srv/json.php", datos)
alert(respuesta.body)

Hace wait esperando response.

Memoria

datos
saludo
"Hola"
nombre
"pp"

srv/json.php

$json = leeJson();
$saludo = $json->saludo;
$nombre = $json->nombre;
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$json
saludo
"Hola"
nombre
"pp"
$saludo
"Hola"
$nombre
"pp"
$resultado
"Hola pp."

6. El servicio genera la response

index.html

const datos = {
 saludo: "Hola",
 nombre: "pp"
}
const respuesta =
 await enviaJson(
  "srv/json.php", datos)
alert(respuesta.body)

Hace wait esperando response.

Memoria

datos
saludo
"Hola"
nombre
"pp"

srv/json.php

$json = leeJson();
$saludo = $json->saludo;
$nombre = $json->nombre;
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$json
saludo
"Hola"
nombre
"pp"
$saludo
"Hola"
$nombre
"pp"
$resultado
"Hola pp."

Response

code
200
body
"Hola pp."

7. El servicio devuelve la response, que es recibida en el cliente

index.html

const datos = {
 saludo: "Hola",
 nombre: "pp"
}
const respuesta =
 await enviaJson(
  "srv/json.php", datos)
alert(respuesta.body)

Despierta y recibe response.

Memoria

datos
saludo
"Hola"
nombre
"pp"
respuesta
status
200
body
"Hola pp."

Response

code
200
body
"Hola pp."

srv/json.php

$json = leeJson();
$saludo = $json->saludo;
$nombre = $json->nombre;
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Devuelve response y se duerme.

8. Muestra el valor recibido recibido en un alert

index.html

const datos = {
 saludo: "Hola",
 nombre: "pp"
}
const respuesta =
 await enviaJson(
  "srv/json.php", datos)
alert(respuesta.body)

Memoria

datos
saludo
"Hola"
nombre
"pp"
respuesta
status
200
body
"Hola pp."

Alert

Hola pp.

9. Al cerrar el alert, termina el evento

index.html

const datos = {
 saludo: "Hola",
 nombre: "pp"
}
const respuesta =
 await enviaJson(
  "srv/json.php", datos)
alert(respuesta.body)

F. Hazlo funcionar

G. Archivos

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Servicio que recibe y devuelve JSON</title>
10
11</head>
12
13<body>
14
15 <h1>Servicio que recibe y devuelve JSON</h1>
16
17 <p><button onclick="envia()">Envía JSON</button></p>
18
19 <script type="module">
20
21 import { muestraError } from "./lib/js/muestraError.js"
22 import { enviaJson } from "./lib/js/enviaJson.js"
23
24 async function envia() {
25 try {
26 const datos = {
27 saludo: "Hola",
28 nombre: "pp"
29 }
30 const respuesta =
31 await enviaJson(
32 "srv/json.php", datos)
33 alert(respuesta.body)
34 } catch (error) {
35 muestraError(error)
36 }
37 }
38 // Permite que los eventos de html usen la función.
39 window["envia"] = envia
40
41 </script>
42
43</body>
44
45</html>

I. Carpeta « srv »

A. srv / json.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/leeJson.php";
5
6ejecutaServicio(function () {
7 $json = leeJson();
8 $saludo = $json->saludo;
9 $nombre = $json->nombre;
10 $resultado =
11 "{$saludo} {$nombre}.";
12 return $resultado;
13});
14

J. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / enviaJson.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * @param { string } url
5 * @param { Object } body
6 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
7 * | "CONNECT" | "HEAD" } metodoHttp
8 */
9export async function enviaJson(url, body, metodoHttp = "POST") {
10 return await invocaServicio(fetch(url, {
11 method: metodoHttp,
12 headers: {
13 "Content-Type": "application/json",
14 "Accept": "application/json, application/problem+json"
15 },
16 body: JSON.stringify(body)
17 }))
18}
19
20// Permite que los eventos de html usen la función.
21window["enviaJson"] = enviaJson

2. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

3. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

4. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

5. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeJson.php

1<?php
2
3function leeJson()
4{
5 return json_decode(file_get_contents("php://input"));
6}
7

4. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

K. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

L. Carpeta « error »

A. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

B. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

M. Resumen

  • En esta lección se mostró un ejemplo que envía JSON a un servicio y, como respuesta, recibe datos JSON de ese mismo servicio.

11. Servicio que procesa una forma

Versión para imprimir.

A. Introducción

B. La función submitForm

  • Se introduce la función submitForm, que permite enviar los datos capturados en un formulario hacia una URL.

  • Se utiliza el formato mutipart/form-data, lo cual permite transmitir el contenido de archivos.

  • Para usarla, debes importar el archivo donde está definida esta función y luego invocarla pasándole el evento generado al hacer submit a una form, la URL del servicio que precesa los datos y el método de envío, que por omisión es POST.

  • A lo largo de esta lección y las siguientes, se profundiza en el uso y estructura de esta función.

C. La función leeTexto

  • Para que los servicios en el servidor puedan leer los datos tipo texto enviados por un formulario, por cookies o parámetros en la URL, se introduce la función leeTexto, escrita en PHP.

  • Para usarla, debes importarla con require o require_once, invocarla pasándole el nombre del parámetro o campo que deseas leer y recibir el texto resultante. Si el nombre no está incluido en la solicitud, se recibe null.

  • A lo largo de esta lección y las siguientes, se profundiza en el uso y estructura de esta función.

D. Diagrama de despliegue

Diagrama de despliegue

E. Funcionamiento

1. El usuario captura datos y activa la forma

Forma

2. Se activa el código del evento submit.

Forma

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

3. Se invoca el servicio, incluyendo los datos de la forma

Forma

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Request

URL
srv/procesa.php
Method
POST
body
saludo
hola
nombre
pp

srv/procesa.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Despierta y recibe request.

4. El servicio lee los datos

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Hace wait esperando response.

srv/procesa.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Request

URL
srv/procesa.php
Method
POST
body
saludo
hola
nombre
pp

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"

5. El servicio procesa los datos

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Hace wait esperando response.

srv/procesa.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"
$resultado
"hola pp"

6. El servicio genera la response

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Hace wait esperando response.

srv/procesa.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"
$resultado
"hola pp"

Response

code
200
body
"hola pp"

7. El servicio devuelve la response, que es recibida en el cliente

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Despierta y recibe response.

Response

code
200
body
"hola pp"

Memoria

respuesta
status
200
body
"Hola pp."

srv/procesa.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Devuelve response y se duerme.

8. Muestra el texto recibido en un alert

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Memoria

respuesta
status
200
body
"Hola pp."

Alert

hola pp

9. Al cerrar el alert, termina el evento

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

F. Hazlo funcionar

G. Archivos

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Servicio que procesa un formulario</title>
10
11</head>
12
13<body>
14
15 <h1>Servicio que procesa un formulario</h1>
16
17 <form onsubmit="procesaForm(event) ">
18
19 <p>
20 <label>
21 Saludo:
22 <!-- Como este input tiene name="saludo", su valor se recupera en el
23 servidor con leeTexto("saludo") -->
24 <input name="saludo">
25 </label>
26 </p>
27
28 <p>
29 <label>
30 Nombre:
31 <!-- Como este input tiene name="nombre", su valor se recupera en el
32 servidor con leeTexto("nombre") -->
33 <input name="nombre">
34 </label>
35 </p>
36
37 <p><button type="submit">Procesa</button></p>
38
39 </form>
40
41 <script type="module">
42
43 import { muestraError } from "./lib/js/muestraError.js"
44 import { submitForm } from "./lib/js/submitForm.js"
45
46 /**
47 * @param {Event} event
48 */
49 async function procesaForm(event) {
50 try {
51 const respuesta =
52 await submitForm(
53 "srv/procesa.php", event)
54 alert(respuesta.body)
55 } catch (error) {
56 muestraError(error)
57 }
58 }
59 // Permite que los eventos de html usen la función.
60 window["procesaForm"] = procesaForm
61
62 </script>
63
64</body>
65
66</html>

I. Carpeta « srv »

A. srv / procesa.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/leeTexto.php";
5
6ejecutaServicio(function () {
7 $saludo = leeTexto("saludo");
8 $nombre = leeTexto("nombre");
9 $resultado =
10 "{$saludo} {$nombre}.";
11 return $resultado;
12});
13

J. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

2. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

3. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

4. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

5. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

4. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

K. Carpeta « error »

A. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

B. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

L. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

M. Resumen

  • En esta lección se mostró un servicio que procesa una forma.

12. Servicio que valida datos

Versión para imprimir.

A. Introducción

B. Diagrama de despliegue

Diagrama de despliegue

C. Escenario con datos incorrectos

1. El usuario activa la forma sin capturar datos

Forma

2. Se activa el código del evento submit

Forma

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

3. Se invoca el servicio, incluyendo los datos de la forma

Forma

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Request

URL
srv/valida.php
Method
POST
body
saludo
vacío
nombre
vacío

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Despierta y recibe request.

4. El servicio lee los datos

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Hace wait esperando response.

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Request

URL
srv/valida.php
Method
POST
body
saludo
vacío
nombre
vacío

Memoria (Servidor)

$saludo
""
$nombre
""

5. El servicio comprueba que el saludo sea válido

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Hace wait esperando response.

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$saludo
""
$nombre
""

6. Como el saludo no es válido, aborta y genera la response

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Hace wait esperando response.

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Response

code
400
body
{
"type": "/error/faltasaludo.html",
"title": "Falta el saludo."
}

7. El servicio devuelve la response, que es recibida en el cliente

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Despierta y recibe response.

Response

code
400
body
{
"type": "/error/faltasaludo.html",
"title": "Falta el saludo."
}

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Devuelve response y se duerme.

8. Como hay error, lanza una excepción que atrapa catch

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Response

code
400
body
{
"type": "/error/faltasaludo.html",
"title": "Falta el saludo."
}

Memoria

error
ProblemDetails
status
400
type
"/error/faltasaludo.html"
title
"Falta el saludo."

9. Muestra los detalles de la excepción en la consola y en un alert

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Memoria

error
ProblemDetails
status
400
type
"/error/faltasaludo.html"
title
"Falta el saludo."

Alert

Falta el saludo

Código 400 /error/faltasaludo.html

Consola

Falta el saludo

Código: 400 /error/faltasaludo.html
Error: Falta el saludo
  at invocaServicio (invocaServicio.js:48:10)
  at async procesaForma ((índice):52:6)

10. Al cerrar el alert, termina el evento

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Consola

Falta el saludo

Código: 400 /error/faltasaludo.html
Error: Falta el saludo
  at invocaServicio (invocaServicio.js:48:10)
  at async procesaForma ((índice):52:6)

D. Escenario con datos correctos

Versión para imprimir.

1. El usuario captura datos y activa la forma

Forma

2. Se activa el código del evento submit.

Forma

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

3. Se invoca el servicio, incluyendo los datos de la forma

Forma

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Request

URL
srv/valida.php
Method
POST
body
saludo
hola
nombre
pp

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Despierta y recibe request.

4. El servicio lee los datos

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Hace wait esperando response.

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Request

URL
srv/valida.php
Method
POST
body
saludo
hola
nombre
pp

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"

5. El servicio comprueba que el saludo sea válido

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Hace wait esperando response.

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"

6. Como el saludo es válido, se comprueba que el nombre sea válido

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Hace wait esperando response.

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"

7. Como el nombre es válido, procesa los datos

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Hace wait esperando response.

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"
$resultado
"hola pp"

8. El servicio genera la response

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Hace wait esperando response.

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"
$resultado
"hola pp"

Response

code
200
body
"hola pp"

9. El servicio devuelve la response, que es recibida en el cliente

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Despierta y recibe response.

Response

code
200
body
"hola pp"

Memoria

respuesta
status
200
body
"Hola pp."

srv/valida.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
if (
 $saludo === null
 || $saludo === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltasaludo.html",
  title: "Falta el saludo.",
 );
}
if (
 $nombre === null
 || $nombre === ""
) {
 throw new ProblemDetails(
  status: ProblemDetails::BadRequest,
  type: "/error/faltanombre.html",
  title: "Falta el nombre.",
 );
}
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Devuelve response y se duerme.

10. Muestra el texto recibido en un alert

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

Memoria

respuesta
status
200
body
"Hola pp."

Alert

hola pp

11. Al cerrar el alert, termina el evento

index.html

try {
 const respuesta =
  await submitForm(
   "srv/valida.php", event)
 alert(respuesta.body)
} catch (error) {
 muestraError(error)
}

E. Hazlo funcionar

  1. Revisa el proyecto en Replit con la URL https://replit.com/@GilbertoPachec5/srvvalida?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  2. Usa o crea una cuenta de Google.

  3. Crea una cuenta de Replit usando la cuenta de Google.

  4. Crea un proyecto PHP Web Server en Replit y edita o sube los archivos de este proyecto.

  5. Depura el proyecto.

  6. Crea la cover page o página de spotlight del proyecto.

F. Archivos

Haz clic en los triángulos para expandir las carpetas

G. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Servicio que valida datos</title>
10
11</head>
12
13<body>
14
15 <h1>Servicio que valida datos</h1>
16
17 <form onsubmit="procesaForma(event) ">
18
19 <p>
20 <label>
21 Saludo:
22 <!-- Como este input tiene name="saludo", su valor se recupera en el
23 servidor con leeTexto("saludo") -->
24 <input name="saludo">
25 </label>
26 </p>
27
28 <p>
29 <label>
30 Nombre:
31 <!-- Como este input tiene name="nombre", su valor se recupera en el
32 servidor con leeTexto("nombre") -->
33 <input name="nombre">
34 </label>
35 </p>
36
37 <p><button type="submit">Procesa</button></p>
38
39 </form>
40
41 <script type="module">
42
43 import { muestraError } from "./lib/js/muestraError.js"
44 import { submitForm } from "./lib/js/submitForm.js"
45
46 /**
47 * @param {Event} event
48 */
49 async function procesaForma(event) {
50 try {
51 const respuesta =
52 await submitForm(
53 "srv/valida.php", event)
54 alert(respuesta.body)
55 } catch (error) {
56 muestraError(error)
57 }
58 }
59 // Permite que los eventos de html usen la función.
60 window["procesaForma"] = procesaForma
61
62 </script>
63
64</body>
65
66</html>

H. Carpeta « srv »

Versión para imprimir.

A. srv / valida.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/leeTexto.php";
6
7ejecutaServicio(function () {
8 $saludo = leeTexto("saludo");
9 $nombre = leeTexto("nombre");
10 if (
11 $saludo === null
12 || $saludo === ""
13 ) {
14 throw new ProblemDetails(
15 status: ProblemDetails::BadRequest,
16 type: "/error/faltasaludo.html",
17 title: "Falta el saludo.",
18 );
19 }
20 if (
21 $nombre === null
22 || $nombre === ""
23 ) {
24 throw new ProblemDetails(
25 status: ProblemDetails::BadRequest,
26 type: "/error/faltanombre.html",
27 title: "Falta el nombre.",
28 );
29 }
30 $resultado =
31 "{$saludo} {$nombre}.";
32 return $resultado;
33});
34

I. Carpeta « error »

Versión para imprimir.

A. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17</body>
18
19</html>

B. error / faltasaludo.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el saludo</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el saludo</h1>
16
17</body>
18
19</html>

C. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

D. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

J. Carpeta « lib »

Versión para imprimir.

A. Carpeta « lib / js »

1. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

2. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

3. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

4. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

5. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

4. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

K. jsconfig.json

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

L. Resumen

13. Mostrar datos en el cliente

Versión para imprimir.

A. Introducción

B. Diagrama de despliegue

Diagrama de despliegue

C. Hazlo funcionar

D. Archivos

E. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width">
7 <title>Muestra datos</title>
8</head>
9
10<body onload="descargaDatos()">
11
12 <h1>Muestra datos</h1>
13
14 <!-- Al mostrar un dato, se usan los atributos "id", "name" o "data-name"
15 para buscar la propiedad a desplegar en la página. -->
16
17 <p>
18 <label>
19 Nombre
20 <!-- Muestra la propiedad nombre del objeto descargado, usando la propiedad
21 value del output. -->
22 <output name="nombre"></output>
23 </label>
24 </p>
25
26 <p>
27 <label>
28 Apellido
29 <!-- Muestra la propiedad apellido del objeto descargado, usando la propiedad
30 value del input. -->
31 <input name="apellido" type="text">
32 </label>
33 </p>
34
35 <p>
36 <label>
37 Género
38 <!-- Muestra la propiedad genero del objeto descargado, usando la propiedad
39 value del select. -->
40 <select name="genero">
41 <option value="">Sin selección</option>
42 <option value="pop">Pop</option>
43 <option value="reg">Reguetón</option>
44 </select>
45 </label>
46 </p>
47
48 <p>
49 <label>
50 Géneración
51 <!-- Muestra la propiedad generacion del objeto descargado, usando la
52 propiedad value del select. -->
53 <select name="generacion">
54 <option value="boom">Baby Boom</option>
55 <option value="X">X</option>
56 <option value="">Sin selección</option>
57 <option value="Y">Millenoals</option>
58 <option value="Z">Z</option>
59 <option value="alfa">Alfa</option>
60 </select>
61 </label>
62 </p>
63
64 <p>
65 <label>
66 Edad
67 <!-- Muestra la propiedad edad del objeto descargado, usando la propiedad
68 valueAsNumber del input. -->
69 <input data-name="edad" type="number">
70 </label>
71 </p>
72
73 <p>
74 <label>
75 Número de la suerte
76 <!-- Muestra la propiedad numero del objeto descargado, usando la propiedad
77 value del output. -->
78 <output id="numero"></output>
79 </label>
80 </p>
81
82 <p>
83 <label>
84 Avance
85 <!-- Muestra la propiedad avance del objeto descargado, usando la propiedad
86 value del progress. -->
87 <progress id="avance" max="100"></progress>
88 </label>
89 </p>
90
91 <p>
92 <label>
93 Capacidad
94 <!-- Muestra la propiedad capacidad del objeto descargado, usando la
95 propiedad value del meter. -->
96 <meter id="capacidad" min="50" max="80"></meter>
97 </label>
98 </p>
99
100 <p>
101 <label>
102 Temperatura
103 <!-- Muestra la propiedad temperatura del objeto descargado, usando la
104 propiedad valueAsNumber del meter. -->
105 <input id="temperatura" type="range" min="0" max="50">
106 </label>
107 </p>
108
109 <p>
110 <label>
111 <!-- Muestra la propiedad aprobado del objeto descargado, usando la propiedad
112 checked del input, porque el tipo del dato es boolean y el input tiene
113 type="checkbox". -->
114 <input id="aprobado" type="checkbox">
115 Aprobado
116 </label>
117 </p>
118
119 <p>
120 <label>
121 Gracioso
122 <!-- Muestra la propiedad gracioso del objeto descargado, usando la propiedad
123 value del output. -->
124 <output id="gracioso"></output>
125 </label>
126 </p>
127
128 <p>
129 <label>
130 Emplacado
131 <!-- Muestra la propiedad emplacado del objeto descargado, usando la
132 propiedad value del select. -->
133 <select name="emplacado">
134 <option value="">Sin selección</option>
135 <option value="true</span>">Si</option>
136 <option value="false</span>">No</option>
137 </select>
138 </label>
139 </p>
140
141 <label>Dirección</label>
142 <!-- Muestra la propiedad direccion del objeto descargado, usando la propiedad
143 textContent del pre. -->
144 <pre id="direccion"></pre>
145
146 <p>
147 <label>
148 Encabezado
149 <!-- Muestra la propiedad encabezado del objeto descargado, usando la
150 propiedad innerHTML del span. -->
151 <span id="encabezado"></span>
152 </label>
153 </p>
154
155 <p>
156 <label>
157 Nacimiento
158 <!-- Muestra la propiedad nacimiento del objeto descargado, usando la
159 propiedad value del input. Revisa la especificación de input para
160 los distintos formatos de fecha que se pueden usar. -->
161 <input id="nacimiento" type="date">
162 </label>
163 </p>
164
165 <figure>
166 <!-- Muestra la propiedad imagen1 del objeto descargado, usando la propiedad
167 src del img. -->
168 <img id="imagen1" alt="Imagen 2">
169 <figcaption>Imagen 1</figcaption>
170 </figure>
171
172 <figure>
173 <!-- Muestra la propiedad imagen2 del objeto descargado, usando la propiedad
174 src del img. Como el valor es "", se oculta el img usando hidden = true. -->
175 <img id="imagen2" alt="Imagen 2">
176 <figcaption>Imagen 2</figcaption>
177 </figure>
178
179 <!-- Muestra la propiedad pasatiempos[] del objeto descargado; como es un
180 array, usa los elementos con name="pasatiempos[]" y les pone la propiedad
181 checked en true si su value está en el array; de lo contrario se las pone en
182 false. Los corchetes([]), le indican a PHP que la propiedad es un array
183 que puede llegar a tener 0, 1 o más elementos. -->
184 <fieldset>
185 <legend>Pasatiempos</legend>
186 <label>
187 <input type="checkbox" name="pasatiempos[]" value="fut">Futbol
188 </label>
189 <label>
190 <input type="checkbox" name="pasatiempos[]" value="chess">Ajedrez
191 </label>
192 <label>
193 <input type="checkbox" data-name="pasatiempos[]" value="basket">Basketbol
194 </label>
195 </fieldset>
196
197 <!-- Muestra la propiedad madrugador del objeto descargado; como es un
198 array, usa los elementos con name="madrugador" y les pone la propiedad
199 checked en true si su value está en el array; de lo contrario se las pone en
200 false. Como el name no tiene [], el array solo tiene 0 o un elemento. -->
201 <fieldset>
202 <legend>Madrugador</legend>
203 <label><input type="radio" name="madrugador" value="si">Si</label>
204 <label><input type="radio" name="madrugador" value="no">No</label>
205 </fieldset>
206
207 <!-- Muestra la propiedad patos del objeto descargado; como es un
208 array, usa los las opciones del select con name="patos[]" y les pone la
209 propiedad selected en true si su value está en el array; de lo contrario se
210 las pone en false. Los corchetes([]), le indican a PHP que la propiedad es
211 un array que puede llegar a tener 0, 1 o más elementos. -->
212 <select name="patos[]" multiple size="3">
213 <option value="hugo">Hugo</option>
214 <option value="paco">Paco</option>
215 <option value="luis">Luis</option>
216 </select>
217
218</body>
219
220<script type="module">
221
222 import { invocaServicio } from "./lib/js/invocaServicio.js"
223 import { muestraObjeto } from "./lib/js/muestraObjeto.js"
224 import { muestraError } from "./lib/js/muestraError.js"
225
226 async function descargaDatos() {
227 try {
228 const respuesta = await invocaServicio("srv/datos.php")
229 await muestraObjeto(document, respuesta.body)
230 } catch (error) {
231 muestraError(error)
232 }
233 }
234 // Permite que los eventos de html usen la función.
235 window["descargaDatos"] = descargaDatos
236
237</script>
238
239</html>

F. Carpeta « srv »

A. srv / datos.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4
5ejecutaServicio(function () {
6 return [
7 "nombre" => ["value" => "pp"],
8 "apellido" => ["value" => "tkt"],
9 "genero" => ["value" => "pop"],
10 "generacion" => ["value" => ""],
11 "edad" => ["valueAsNumber" => 18],
12 "numero" => ["value" => 5],
13 "avance" => ["value" => 70],
14 "capacidad" => ["value" => 60],
15 "temperatura" => ["valueAsNumber" => 40],
16 "aprobado" => ["checked" => true],
17 "gracioso" => ["value" => false],
18 "emplacado" => ["value" => false],
19 "direccion" => ["textContent" => "Girasoles 23\ncolonia Rosales"],
20 "encabezado" => ["innerHTML" => "<em>Hola, soy <strong>pp</strong>"],
21 "nacimiento" => ["value" => "2000-07-04"],
22 "imagen1" => [
23 "src" => "https://gilpgawoas.github.io/img/icono/maskable_icon_x48.png"
24 ],
25 "imagen2" => ["src" => ""],
26 "pasatiempos[]" => ["fut", "basket"],
27 "madrugador" => ["no"],
28 "patos[]" => ["paco", "luis"],
29 ];
30});
31

G. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

2. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

3. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

4. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

5. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

H. Carpeta « error »

A. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

B. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

I. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

J. Resumen

  • En esta lección se indica como mostrar en el cliente los datos devueltos por un servicio.

14. Renderizado del lado del cliente

Versión para imprimir.

A. Introducción

  • Para algunas partes de una página no hay componentes gráficos que puedan mostrar valores, como en el caso de los listados. En ese caso se tiene que generar el código HTM desde el programa, proceso que se conoce como renderizado.

  • En esta lección se presenta un ejemplo de renderizado en el cliente.

  • Puedes probar el ejemplo en https://replit.com/@GilbertoPachec5/rendercli?v=1. Hazle fork al proyecto y córrelo.

B. Diagrama de despliegue

Diagrama de despliegue

C. Hazlo funcionar

D. Archivos

Haz clic en los triángulos para expandir las carpetas

E. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Render en el cliente</title>
10
11</head>
12
13<body>
14
15 <h1>Render en el cliente</h1>
16
17 <dl id="lista">
18 <dt>Cargando…</dt>
19 <dd><progress max="100">Cargando…</progress></dd>
20 </dl>
21
22 <script>
23
24 // Crea y pone en funcionamiento el worker del archivo "render.js".
25 const worker = new Worker("render.js", { type: "module" })
26 // Se invoca cuando el worker envía un mensaje a la página.
27 worker.onmessage = event => {
28 const respuesta = event.data
29 if (respuesta.resultado !== undefined) {
30 lista.innerHTML = respuesta.resultado
31 } else if (respuesta.error !== undefined) {
32 lista.innerHTML = ""
33 alert(respuesta.error)
34 }
35 }
36
37 </script>
38
39</body>
40
41</html>

F. render.js

1/* Ejemplo de render en el cliente. No se usa import
2 * porque Firefox no lo soporta en los web workers. */
3
4invocaServicio("srv/lista.php")
5 .then(respuesta => {
6
7 const lista = respuesta.body
8
9 if (Array.isArray(lista)) {
10 // Genera el código HTML de la lista.
11 let render = ""
12 for (const modelo of lista) {
13 /* Codifica nombre y color para que cambie los caracteres especiales y
14 * el texto no se pueda interpretar como HTML. Esta técnica evita la
15 * inyección de código. */
16 const nombre =
17 typeof modelo.nombre === "string" ? htmlentities(modelo.nombre) : ""
18 const color =
19 typeof modelo.color === "string" ? htmlentities(modelo.color) : ""
20 render += /* html */
21 `<dt>${nombre}</dt>
22 <dd>${color}</dd>`
23 }
24
25 // Verifica si el código corre dentro de un web worker.
26 if (self instanceof WorkerGlobalScope) {
27 // Envía el render a la página que invocó este web worker.
28 self.postMessage({ resultado: render })
29 }
30 }
31 })
32 .catch(muestraErrorEnWorker)
33
34const ProblemDetails_InternalServerError = 500
35
36class ProblemDetails extends Error {
37
38 /**
39 * @param {number} status
40 * @param {string} title
41 * @param {string} [detail]
42 * @param {string} [type]
43 */
44 constructor(status, title, detail, type) {
45 super(title)
46 /** @readonly */
47 this.status = status
48 /** @readonly */
49 this.type = type
50 /** @readonly */
51 this.title = title
52 /** @readonly */
53 this.detail = detail
54 }
55
56}
57
58/**
59 * Muestra un error en la consola y en un cuadro de
60 * alerta el mensaje de una excepción.
61 * @param { ProblemDetails | Error | null } error descripción del error.
62 */
63function muestraErrorEnWorker(error) {
64 if (error === null) {
65 console.log("Error")
66 self.postMessage({ error: "Error" })
67 } else if (error instanceof ProblemDetails) {
68 let mensaje = error.title
69 if (error.detail) {
70 mensaje += `\n\n${error.detail}`
71 }
72 mensaje += `\n\nCódigo: ${error.status}`
73 if (error.type) {
74 mensaje += ` ${error.type}`
75 }
76 console.error(mensaje)
77 console.error(error)
78 self.postMessage({ error: mensaje })
79 } else {
80 console.error(error)
81 self.postMessage({ error: error.message })
82 }
83}
84
85const JsonResponse_OK = 200
86const JsonResponse_Created = 201
87const JsonResponse_NoContent = 204
88
89class JsonResponse {
90
91 /**
92 * @param {number} status
93 * @param {any} [body]
94 * @param {string} [location]
95 */
96 constructor(status, body, location) {
97 /** @readonly */
98 this.status = status
99 /** @readonly */
100 this.body = body
101 /** @readonly */
102 this.location = location
103 }
104
105}
106
107/**
108 * Espera a que la promesa de un fetch termine. Si
109 * hay error, lanza una excepción. Si no hay error,
110 * interpreta la respuesta del servidor como JSON y
111 * la convierte en una literal de objeto.
112 * @param { string | Promise<Response> } servicio
113 */
114export async function invocaServicio(servicio) {
115 let f = servicio
116 if (typeof servicio === "string") {
117 f = fetch(servicio, {
118 headers: { "Accept": "application/json, application/problem+json" }
119 })
120 } else if (!(f instanceof Promise)) {
121 throw new Error("Servicio de tipo incorrecto.")
122 }
123 const respuesta = await f
124 if (respuesta.ok) {
125 if (respuesta.status === JsonResponse_NoContent) {
126 return new JsonResponse(JsonResponse_NoContent)
127 }
128 const texto = await respuesta.text()
129 try {
130 const body = JSON.parse(texto)
131 if (respuesta.status === JsonResponse_Created) {
132 const location = respuesta.headers.get("location")
133 return new JsonResponse(JsonResponse_Created, body,
134 location === null ? undefined : location)
135 } else {
136 return new JsonResponse(JsonResponse_OK, body)
137 }
138 } catch (error) {
139 // El contenido no es JSON. Probablemente sea texto.
140 throw new ProblemDetails(ProblemDetails_InternalServerError,
141 "Problema interno en el servidor.", texto)
142 }
143 } else {
144 const texto = await respuesta.text()
145 try {
146 const { type, title, detail } = JSON.parse(texto)
147 throw new ProblemDetails(respuesta.status,
148 typeof title === "string" ? title : "",
149 typeof detail === "string" ? detail : undefined,
150 typeof type === "string" ? type : undefined)
151 } catch (error) {
152 if (error instanceof ProblemDetails) {
153 throw error
154 } else {
155 // El contenido no es JSON. Probablemente sea texto.
156 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
157 }
158 }
159 }
160}
161
162/**
163 * Codifica un texto para que cambie los caracteres
164 * especiales y no se pueda interpretar como
165 * etiiqueta HTML. Esta técnica evita la inyección
166 * de código.
167 * @param { string } texto
168*/
169function htmlentities(texto) {
170 return texto.replace(/[<>"']/g, textoDetectado => {
171 switch (textoDetectado) {
172 case "<": return "<"
173 case ">": return ">"
174 case '"': return "&quot;"
175 case "'": return "&#039;"
176 default: return textoDetectado
177 }
178 })
179}

G. Carpeta « srv »

A. srv / lista.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4
5ejecutaServicio(function () {
6 return [
7 [
8 "nombre" => "pp",
9 "color" => "azul"
10 ],
11 [
12 "nombre" => "kq",
13 "color" => "rojo"
14 ],
15 [
16 "nombre" => "tt",
17 "color" => "rosa"
18 ],
19 [
20 "nombre" => "bb",
21 "color" => "azul"
22 ]
23 ];
24});
25

H. Carpeta « lib »

A. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

I. Carpeta « error »

A. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

B. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

J. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

K. Resumen

  • En esta lección se mostró un ejemplo de renderizado en el cliente.

15. Renderizado del lado del servidor

Versión para imprimir.

A. Introducción

  • Para algunas partes de una página no hay componentes gráficos que puedan mostrar valores, como en el caso de los listados. En ese caso se tiene que generar el código HTM desde el programa, proceso que se conoce como renderizado.

  • En esta lección se presenta un ejemplo de renderizado en el servidor.

  • Puedes probar el ejemplo en https://replit.com/@GilbertoPachec5/renderserv?v=1. Hazle fork al proyecto y córrelo.

B. Diagrama de despliegue

Diagrama de despliegue

C. Hazlo funcionar

D. Archivos

E. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Render en el servidor</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraObjeto.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14
15</head>
16
17<body onload="invocaServicio('srv/render.php')
18 .then(respuesta => muestraObjeto(document, respuesta.body))
19 .catch(muestraError)">
20
21 <h1>Render en el servidor</h1>
22
23 <dl id="lista">
24 <dt>Cargando…</dt>
25 <dd><progress max="100">Cargando…</progress></dd>
26 </dl>
27
28</body>
29
30</html>

F. Carpeta « srv »

A. srv / render.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4
5ejecutaServicio(function () {
6 $lista = [
7 [
8 "nombre" => "pp",
9 "color" => "azul"
10 ],
11 [
12 "nombre" => "qk",
13 "color" => "rojo"
14 ],
15 [
16 "nombre" => "tt",
17 "color" => "rosa"
18 ],
19 [
20 "nombre" => "bb",
21 "color" => "azul"
22 ]
23 ];
24
25 // Genera el código HTML de la lista.
26 $render = "";
27 foreach ($lista as $modelo) {
28 /* Codifica nombre y color para que cambie los caracteres
29 * especiales y el texto no se pueda interpretar como HTML.
30 * Esta técnica evita la inyección de código. */
31 $nombre = htmlentities($modelo["nombre"]);
32 $color = htmlentities($modelo["color"]);
33 $render .=
34 "<dt>{$nombre}</dt>
35 <dd>{$color}</dd>";
36 }
37
38 return ["lista" => ["innerHTML" => $render]];
39});
40

G. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

2. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

3. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

4. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

5. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

H. Carpeta « error »

A. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

B. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

I. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

J. Resumen

  • En esta lección se mostró un ejemplo de renderizado en el servidor.

16. Bases de datos

Versión para imprimir.

A. Introducción

B. Diagrama entidad relación

Diagrama entidad relaciónn

C. Diagrama relacional

Diagrama relacional

D. Diagrama de paquetes

  • Para este ejemplo se utilizan algunos principios de arquitecturas limpias.

  • Cada uno de los paquetes apunta con una flecha use a los que utiliza para realizar sus funciones.

  • Cada paquete oculta los detalles de su implementación y tecnología.

  • Los detalles de la base de datos, así como de su configuración, se mantienen dentro del paquete bd y no se exponen fuera de dicho paquete.

  • Los detalles de la interfaz gráfica, por ejemplo las api del navegador web, o de las interfaces en Android, se mantienen dentro del paquete access y no se exponen fuera de dicho paquete.

  • El intercambio de datos entre los paquetes access y service se realiza de acuerdo al contenido de las lecciones anteriores.

  • El intercambio de datos entre los paquetes service y bd se realiza con el contenido del paquete modelo.

Diagrama de paquetes

E. Diagrama de despliegue

Diagrama de despliegue

F. Hazlo funcionar

G. Archivos

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Acceso a base de datos</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraObjeto.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14
15</head>
16
17<body onload="invocaServicio('srv/srvPasatiempoConsulta.php')
18 .then(render => muestraObjeto(document, render.body))
19 .catch(muestraError)">
20
21 <h1>Acceso a base de datos</h1>
22
23 <p><a href="agrega.html">Agregar</a></p>
24
25 <ul id="lista">
26 <li><progress max="100">Cargando…</progress></li>
27 </ul>
28
29</body>
30
31</html>

I. agrega.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Agregar</title>
10
11 <script type="module" src="lib/js/submitForm.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13
14</head>
15
16<body>
17
18 <form onsubmit="submitForm('srv/srvPasatiempoAgrega.php', event)
19 .then(modelo => location.href = 'index.html')
20 .catch(muestraError)">
21
22 <h1>Agregar</h1>
23
24 <p><a href="index.html">Cancelar</a></p>
25
26 <p>
27 <label>
28 Nombre *
29 <input name="nombre">
30 </label>
31 </p>
32
33 <p>* Obligatorio</p>
34
35 <p><button type="submit">Agregar</button></p>
36
37 </form>
38
39</body>
40
41</html>

J. modifica.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport"
8 content="width=device-width">
9
10 <title>Modificar</title>
11
12 <script type="module" src="lib/js/invocaServicio.js"></script>
13 <script type="module" src="lib/js/submitForm.js"></script>
14 <script type="module" src="lib/js/muestraObjeto.js"></script>
15 <script type="module" src="lib/js/muestraError.js"></script>
16 <script type="module" src="lib/js/confirmaEliminar.js"></script>
17
18 <script>
19 // Obtiene los parámetros de la página.
20 const params = new URL(location.href).searchParams
21 </script>
22
23</head>
24
25<body onload="if (params.size > 0) {
26 invocaServicio('srv/srvPasatiempoBusca.php?' + params)
27 .then(modelo => muestraObjeto(forma, modelo.body))
28 .catch(muestraError)
29 }">
30
31 <form id="forma" onsubmit="submitForm('srv/srvPasatiempoModifica.php', event)
32 .then(modelo => location.href = 'index.html')
33 .catch(muestraError)">
34
35 <h1>Modificar</h1>
36
37 <p><a href="index.html">Cancelar</a></p>
38
39 <input name="id" type="hidden">
40
41 <p>
42 <label>
43 Nombre *
44 <input name="nombre" value="Cargando…">
45 </label>
46 </p>
47
48 <p>* Obligatorio</p>
49
50 <p>
51
52 <button type="submit">Guardar</button>
53
54 <button type="button" onclick="if (params.size > 0 && confirmaEliminar()) {
55 invocaServicio('srv/srvPasatiempoElimina.php?' + params)
56 .then(() => location.href = 'index.html')
57 .catch(muestraError)
58 }">
59 Eliminar
60 </button>
61
62 </p>
63
64 </form>
65
66</body>
67
68</html>

K. Carpeta « srv »

A. Carpeta « srv / modelo »

1. srv / modelo / Pasatiempo.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4
5class Pasatiempo
6{
7
8 public int $id;
9 public string $nombre;
10
11 public function __construct(string $nombre = "", int $id = 0)
12 {
13 $this->id = $id;
14 $this->nombre = $nombre;
15 }
16
17 public function valida()
18 {
19 if ($this->nombre === "")
20 throw new ProblemDetails(
21 status: ProblemDetails::BadRequest,
22 type: "/error/faltanombre.html",
23 title: "Falta el nombre.",
24 );
25 }
26}
27

B. srv / srvPasatiempoAgrega.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/leeTexto.php";
6require_once __DIR__ . "/modelo/Pasatiempo.php";
7require_once __DIR__ . "/bd/pasatiempoAgrega.php";
8
9ejecutaServicio(function () {
10 $nombre = leeTexto("nombre");
11 $modelo = new Pasatiempo(nombre: $nombre === null ? "" : trim($nombre));
12 pasatiempoAgrega($modelo);
13 $id = htmlentities($modelo->id);
14 return JsonResponse::created("/srv/srvPasatiempoBusca.php?id=$id", [
15 "id" => ["value" => $modelo->id],
16 "nombre" => ["value" => $modelo->nombre],
17 ]);
18});
19

C. srv / srvPasatiempoBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/pasatiempoBusca.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 $modelo = pasatiempoBusca($id);
13 if ($modelo === false) {
14 $htmlId = htmlentities($id);
15 throw new ProblemDetails(
16 status: ProblemDetails::NotFound,
17 type: "/error/pasatiemponoencontrado.html",
18 title: "Pasatiempo no encontrado.",
19 detail: "No se encontró ningún pasatiempo con el id $htmlId.",
20 );
21 } else {
22 return [
23 "id" => ["value" => $modelo->id],
24 "nombre" => ["value" => $modelo->nombre],
25 ];
26 }
27});
28

D. srv / srvPasatiempoConsulta.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/pasatiempoConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = pasatiempoConsulta();
8 $render = "";
9 foreach ($lista as $modelo) {
10 $id = htmlentities($modelo->id);
11 $nombre = htmlentities($modelo->nombre);
12 $render .=
13 "<li>
14 <p>
15 <a href='modifica.html?id=$id'>$nombre</a>
16 </p>
17 </li>";
18 }
19 return ["lista" => ["innerHTML" => $render]];
20});
21

E. srv / srvPasatiempoElimina.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/JsonResponse.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/pasatiempoElimina.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 pasatiempoElimina($id);
13 return JsonResponse::noContent();
14});
15

F. srv / srvPasatiempoModifica.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeTexto.php";
7require_once __DIR__ . "/modelo/Pasatiempo.php";
8require_once __DIR__ . "/bd/pasatiempoModifica.php";
9
10ejecutaServicio(function () {
11 $id = leeEntero("id");
12 if ($id === null) throw pdFaltaId();
13 $nombre = leeTexto("nombre");
14 $modelo =
15 new Pasatiempo(nombre: $nombre === null ? "" : trim($nombre), id: (int) $id);
16 pasatiempoModifica($modelo);
17 return [
18 "id" => ["value" => $modelo->id],
19 "nombre" => ["value" => $modelo->nombre],
20 ];
21});
22

G. Carpeta « srv / bd »

1. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS PASATIEMPO (
7 PAS_ID INTEGER,
8 PAS_NOMBRE TEXT NOT NULL,
9 CONSTRAINT PAS_PK
10 PRIMARY KEY(PAS_ID),
11 CONSTRAINT PAS_NOM_UNQ
12 UNIQUE(PAS_NOMBRE)
13 )'
14 );
15}
16

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/bdCrea.php";
4
5class Bd
6{
7
8 private static ?PDO $conexion = null;
9
10 static function getConexion(): PDO
11 {
12 if (self::$conexion === null) {
13
14 self::$conexion = new PDO(
15 // cadena de conexión
16 "sqlite:srvbd.db",
17 // usuario
18 null,
19 // contraseña
20 null,
21 // Opciones: conexiones persistentes y lanza excepciones.
22 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
23 );
24
25 bdCrea(self::$conexion);
26 }
27
28 return self::$conexion;
29 }
30}
31

3. srv / bd / pasatiempoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5
6function pasatiempoAgrega(Pasatiempo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO PASATIEMPO
12 (PAS_NOMBRE)
13 VALUES
14 (:nombre)"
15 );
16 $stmt->execute([":nombre" => $modelo->nombre]);
17 /* Si usas una secuencia para generar el id,
18 * pasa como parámetro de lastInsertId el
19 * nombre de dicha secuencia, debes
20 * ejecutarlo antes del INSERT y pasarle el
21 * id generado al SQL. */
22 $modelo->id = $con->lastInsertId();
23}
24

4. srv / bd / pasatiempoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5
6function pasatiempoBusca(int $id): false|Pasatiempo
7{
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "SELECT
11 PAS_ID AS id,
12 PAS_NOMBRE AS nombre
13 FROM PASATIEMPO
14 WHERE PAS_ID = :id"
15 );
16 $stmt->execute([":id" => $id]);
17 $stmt->setFetchMode(
18 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
19 Pasatiempo::class
20 );
21 return $stmt->fetch();
22}
23

5. srv / bd / pasatiempoConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Pasatiempo.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Pasatiempo[] */
8function pasatiempoConsulta()
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 PAS_ID AS id,
14 PAS_NOMBRE AS nombre
15 FROM PASATIEMPO
16 ORDER BY PAS_NOMBRE"
17 );
18 $resultado = $stmt->fetchAll(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Pasatiempo::class
21 );
22 return recibeFetchAll($resultado);
23}
24

6. srv / bd / pasatiempoElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4
5function pasatiempoElimina(int $id)
6{
7 $con = Bd::getConexion();
8 $stmt = $con->prepare(
9 "DELETE FROM PASATIEMPO
10 WHERE PAS_ID = :id"
11 );
12 $stmt->execute([":id" => $id]);
13}
14

7. srv / bd / pasatiempoModifica.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5
6function pasatiempoModifica(Pasatiempo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "UPDATE PASATIEMPO
12 SET PAS_NOMBRE = :nombre
13 WHERE PAS_ID = :id"
14 );
15 $stmt->execute([
16 ":id" => $modelo->id,
17 ":nombre" => $modelo->nombre
18 ]);
19}
20

L. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / confirmaEliminar.js

1export function confirmaEliminar() {
2 return confirm("Confirma la eliminación")
3}
4
5// Permite que los eventos de html usen la función.
6window["confirmaEliminar"] = confirmaEliminar

2. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

3. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

4. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

5. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

6. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

7. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / leeEntero.php

1<?php
2
3require_once __DIR__ . "/leeTexto.php";
4
5/**
6 * Devuelve el valor entero de un parámetro
7 * recibido en el servidor por medio de GET,
8 * POST o cookie. Si parámetro no se puede
9 * convertir a entero, se genera un error.
10 * Si el parámetro no se recibe, devuelve
11 * null.
12 * Para que las llaves foráneas fucionen bien,
13 * si se recibe una cadena vacía, se devuelve
14 * null.
15 */
16function leeEntero(string $parametro): ?int
17{
18 $valor = leeTexto($parametro);
19 return $valor === null || $valor === ""
20 ? null
21 : trim($valor);
22}
23

2. lib / php / pdFaltaId.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function pdFaltaId()
6{
7 return new ProblemDetails(
8 status: ProblemDetails::BadRequest,
9 type: "/error/faltaid.html",
10 title: "No se ha proporcionado el valor de id.",
11 );
12}
13

3. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

4. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

5. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

6. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

7. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

M. Carpeta « error »

A. error / faltaid.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el id</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el id</h1>
16
17 <p>No se ha proporcionado el valor de id.</p>
18
19</body>
20
21</html>

B. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17</body>
18
19</html>

C. error / pasatiemponoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Pasatiempo no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Pasatiempo no encontrado</h1>
16
17 <p>No se encontró ningún pasatiempo con el id solicitado.</p>
18
19</body>
20
21</html>

D. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

E. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

N. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

O. Resumen

  • En esta lección se mostró el acceso a bases de datos con servicios.

17. Chat sencillo - Protocolos de comunicación

Versión para imprimir.

A. Introducción

  • En esta lección se presentan un chat sencillo.

  • Puedes probar la app en https://replit.com/@GilbertoPachec5/chat?v=1. Hazle fork al proyecto y córrelo.

  • Copia la url de la app y pégala en varias pestañas, navegadores y dispositivos para que veas como entre todas estas vistas se puede chatear.

  • Este proyecto puede correr simultáneamente en varios navegadores y computadoras. Todos interactuan con el servidor test.mosquitto.org.

  • Mosquitto es de prueba, pero puedes contratar servidores de MQTT para producción y cambiar la configuración del código para usarlos.

B. Diagrama de despliegue

Diagrama de despliegue

C. Hazlo funcionar

  1. Revisa el proyecto en Replit con la URL https://replit.com/@GilbertoPachec5/chat?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  2. Copia la url de la app y pégala en varias pestañas, navegadores y dispositivos para que veas como entre todas estas vistas se puede chatear.

  3. Este proyecto puede correr simultáneamente en varios navegadores y computadoras. Todos interactuan con el servidor test.mosquitto.org.

  4. Usa o crea una cuenta de Google.

  5. Crea una cuenta de Replit usando la cuenta de Google.

  6. Crea un proyecto con la categoría HTML, CSS, JS en Replit y edita o sube los archivos de este proyecto.

  7. Depura el proyecto.

  8. Crea la cover page o página de spotlight del proyecto.

  9. El proyecto ya contiene la carpeta paho.javascript-1.0, pero se puede descargar de https://eclipse.dev/paho/index.php?page=clients/js/index.php

D. Archivos

E. index.html

1<!DOCTYPE html>
2<html>
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Chat</title>
10
11 <script src="paho.javascript-1.0.3/paho-mqtt-min.js"></script>
12
13</head>
14
15<body>
16
17 <form onsubmit="formActivada(event)">
18
19 <h1>Chat</h1>
20
21 <p>
22 <label>
23 Alias *
24 <input id="inputAlias" required>
25 </label>
26 </p>
27
28 <p>
29 <label>
30 Mensaje *
31 <input id="inputMensaje" required>
32 </label>
33 </p>
34
35 <p>* Obligatorio</p>
36
37 <p><button type="submit">Enviar</button></p>
38
39 <pre id="pre"></pre>
40
41 </form>
42
43 <script type="module">
44
45 import { creaClientIdMqtt } from "./lib/js/creaClientIdMqtt.js"
46 import { falloEnLaConexionMqtt } from "./lib/js/falloEnLaConexionMqtt.js"
47 import { conexionMqttPerdida } from "./lib/js/conexionMqttPerdida.js"
48 import { muestraError } from "./lib/js/muestraError.js"
49
50 const TOPICO_CHAT = "gilpgawoas/chat"
51
52 // Cambia por una raíz para tu proyecto.
53 const clientId = creaClientIdMqtt("gilpgawoasChat-")
54
55 // Si usas un servidor de MQTT diferente, necesitas cambiar los parámetros.
56 const cliente = new Paho.MQTT.Client("test.mosquitto.org", 8081, clientId)
57
58 /**
59 * @param {Event} event
60 */
61 function formActivada(event) {
62 try {
63 event.preventDefault()
64 const mensaje = `${inputAlias.value.trim()}
65${inputMensaje.value.trim()}`
66 enviaMensajeMqtt(mensaje, TOPICO_CHAT)
67 } catch (error) {
68 muestraError(error)
69 }
70 }
71 // Permite que los eventos de html usen la función.
72 window["formActivada"] = formActivada
73
74 // Acciones al recibir un mensaje.
75 cliente.onMessageArrived = mensaje => {
76 if (mensaje.destinationName === TOPICO_CHAT) {
77 pre.textContent += mensaje.payloadString + "\n\n"
78 }
79 }
80
81 // Acciones al perder la conexión.
82 cliente.onConnectionLost = conexionMqttPerdida
83
84 // Configura el cliente.
85 cliente.connect({
86
87 keepAliveInterval: 10,
88
89 useSSL: true,
90
91 // Acciones al fallar la conexión.
92 onFailure: falloEnLaConexionMqtt,
93
94 // Acciones al lograr la conexión.
95 onSuccess: () => {
96 console.log("Conectado")
97 // Se suscribe a uno o más tópicos.
98 cliente.subscribe(TOPICO_CHAT)
99 },
100
101 })
102
103 /**
104 * Envá un valor al servidor de MQTT y es reenviado a todos los dispositivos
105 * suscritos al tópico indicado
106 * @param {string} mensaje
107 * @param {string} topico
108 */
109 function enviaMensajeMqtt(mensaje, topico) {
110 const mensajeMqtt = new Paho.MQTT.Message(mensaje)
111 mensajeMqtt.destinationName = topico
112 cliente.send(mensajeMqtt)
113 }
114
115 </script>
116
117</body>
118
119</html>

F. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / conexionMqttPerdida.js

1/**
2 * @param { {
3 * errorCode: number,
4 * errorMessage: string
5 * } } responseObject
6 */
7export function conexionMqttPerdida(responseObject) {
8 if (responseObject.errorCode !== 0) {
9 const mensaje = "Conexión terminada " + responseObject.errorMessage
10 console.error(mensaje)
11 alert(mensaje)
12 }
13}
14
15// Permite que los eventos de html usen la función.
16window["conexionMqttPerdida"] = conexionMqttPerdida

2. lib / js / creaClientIdMqtt.js

1/**
2 * Añade caracteres al azar a una raíz, para obtener un clientId único por cada
3 * instancia que se conecte al servidor de mqtt.
4 * @param {string} raiz
5 */
6export function creaClientIdMqtt(raiz) {
7 const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
8 for (var i = 0; i < 15; i++) {
9 raiz += chars.charAt(Math.floor(Math.random() * chars.length))
10 }
11 return raiz
12}
13
14// Permite que los eventos de html usen la función.
15window["creaClientIdMqtt"] = creaClientIdMqtt

3. lib / js / falloEnLaConexionMqtt.js

1/**
2 * @param { { errorMessage: string } } res
3 */
4export function falloEnLaConexionMqtt(res) {
5 const mensaje = "Fallo en conexión:" + res.errorMessage
6 console.error(mensaje)
7 alert(mensaje)
8}
9
10// Permite que los eventos de html usen la función.
11window["falloEnLaConexionMqtt"] = falloEnLaConexionMqtt

4. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

5. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

G. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

H. Carpeta « paho.javascript-1.0.3 »

A. paho.javascript-1.0.3 / about.html

1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2<html xmlns="http://www.w3.org/1999/xhtml"><head>
3<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
4<title>About</title>
5</head>
6<body lang="EN-US">
7<h2>About This Content</h2>
8
9<p><em>December 9, 2013</em></p>
10<h3>License</h3>
11
12<p>The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise
13indicated below, the Content is provided to you under the terms and conditions of the
14Eclipse Public License Version 1.0 ("EPL") and Eclipse Distribution License Version 1.0 ("EDL").
15A copy of the EPL is available at
16<a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a>
17and a copy of the EDL is available at
18<a href="http://www.eclipse.org/org/documents/edl-v10.php">http://www.eclipse.org/org/documents/edl-v10.php</a>.
19For purposes of the EPL, "Program" will mean the Content.</p>
20
21<p>If you did not receive this Content directly from the Eclipse Foundation, the Content is
22being redistributed by another party ("Redistributor") and different terms and conditions may
23apply to your use of any object code in the Content. Check the Redistributor's license that was
24provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise
25indicated below, the terms and conditions of the EPL still apply to any source code in the Content
26and such source code may be obtained at <a href="http://www.eclipse.org/">http://www.eclipse.org</a>.</p>
27
28</body></html>
29

B. paho.javascript-1.0.3 / CONTRIBUTING.md

1# Contributing to Paho
2
3Thanks for your interest in this project!
4
5You can contribute bugfixes and new features by sending pull requests through GitHub.
6
7## Legal
8
9In order for your contribution to be accepted, it must comply with the Eclipse Foundation IP policy.
10
11Please read the [Eclipse Foundation policy on accepting contributions via Git](http://wiki.eclipse.org/Development_Resources/Contributing_via_Git).
12
131. Sign the [Eclipse CLA](http://www.eclipse.org/legal/CLA.php)
14 1. Register for an Eclipse Foundation User ID. You can register [here](https://dev.eclipse.org/site_login/createaccount.php).
15 2. Log into the [Projects Portal](https://projects.eclipse.org/), and click on the '[Eclipse CLA](https://projects.eclipse.org/user/sign/cla)' link.
162. Go to your [account settings](https://dev.eclipse.org/site_login/myaccount.php#open_tab_accountsettings) and add your GitHub username to your account.
173. Make sure that you _sign-off_ your Git commits in the following format:
18 ``` Signed-off-by: John Smith ``` This is usually at the bottom of the commit message. You can automate this by adding the '-s' flag when you make the commits. e.g. ```git commit -s -m "Adding a cool feature"```
194. Ensure that the email address that you make your commits with is the same one you used to sign up to the Eclipse Foundation website with.
20
21## Contributing a change
22
23## Contributing a change
24
251. [Fork the repository on GitHub](https://github.com/eclipse/paho.mqtt.javascript/fork)
262. Clone the forked repository onto your computer: ``` git clone https://github.com//paho.mqtt.javascript.git ```
273. Create a new branch from the latest ```develop``` branch with ```git checkout -b YOUR_BRANCH_NAME origin/develop```
284. Make your changes
295. If developing a new feature, make sure to include JUnit tests.
306. Ensure that all new and existing tests pass.
317. Commit the changes into the branch: ``` git commit -s ``` Make sure that your commit message is meaningful and describes your changes correctly.
328. If you have a lot of commits for the change, squash them into a single / few commits.
339. Push the changes in your branch to your forked repository.
3410. Finally, go to [https://github.com/eclipse/paho.mqtt.javascript](https://github.com/eclipse/paho.mqtt.javascript) and create a pull request from your "YOUR_BRANCH_NAME" branch to the ```develop``` one to request review and merge of the commits in your pushed branch.
35
36
37What happens next depends on the content of the patch. If it is 100% authored
38by the contributor and is less than 1000 lines (and meets the needs of the
39project), then it can be pulled into the main repository. If not, more steps
40are required. These are detailed in the
41[legal process poster](http://www.eclipse.org/legal/EclipseLegalProcessPoster.pdf).
42
43
44
45## Developer resources:
46
47
48Information regarding source code management, builds, coding standards, and more.
49
50- [https://projects.eclipse.org/projects/iot.paho/developer](https://projects.eclipse.org/projects/iot.paho/developer)
51
52Contact:
53--------
54
55Contact the project developers via the project's development
56[mailing list](https://dev.eclipse.org/mailman/listinfo/paho-dev).
57
58Search for bugs:
59----------------
60
61This project uses GitHub Issues here: [github.com/eclipse/paho.mqtt.javascript/issues](https://github.com/eclipse/paho.mqtt.javascript/issues) to track ongoing development and issues.
62
63Create a new bug:
64-----------------
65
66Be sure to search for existing bugs before you create another one. Remember that contributions are always welcome!
67
68- [Create new Paho bug](https://github.com/eclipse/paho.mqtt.javascript/issues/new)
69

C. paho.javascript-1.0.3 / edl-v10

1
2Eclipse Distribution License - v 1.0
3
4Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors.
5
6All rights reserved.
7
8Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
9
10 Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
11 Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
12 Neither the name of the Eclipse Foundation, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
13
14THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
16

D. paho.javascript-1.0.3 / epl-v10

1Eclipse Public License - v 1.0
2
3THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
4
51. DEFINITIONS
6
7"Contribution" means:
8
9a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and
10b) in the case of each subsequent Contributor:
11i) changes to the Program, and
12ii) additions to the Program;
13where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program.
14"Contributor" means any person or entity that distributes the Program.
15
16"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program.
17
18"Program" means the Contributions distributed in accordance with this Agreement.
19
20"Recipient" means anyone who receives the Program under this Agreement, including all Contributors.
21
222. GRANT OF RIGHTS
23
24a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form.
25b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder.
26c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program.
27d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement.
283. REQUIREMENTS
29
30A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that:
31
32a) it complies with the terms and conditions of this Agreement; and
33b) its license agreement:
34i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose;
35ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits;
36iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and
37iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange.
38When the Program is made available in source code form:
39
40a) it must be made available under this Agreement; and
41b) a copy of this Agreement must be included with each copy of the Program.
42Contributors may not remove or alter any copyright notices contained within the Program.
43
44Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution.
45
464. COMMERCIAL DISTRIBUTION
47
48Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense.
49
50For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages.
51
525. NO WARRANTY
53
54EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations.
55
566. DISCLAIMER OF LIABILITY
57
58EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
59
607. GENERAL
61
62If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
63
64If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed.
65
66All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive.
67
68Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved.
69
70This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation.
71

E. paho.javascript-1.0.3 / paho-mqtt-min.js

1/*******************************************************************************
2 * Copyright (c) 2013, 2016 IBM Corp.
3 *
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * and Eclipse Distribution License v1.0 which accompany this distribution.
7 *
8 * The Eclipse Public License is available at
9 * http://www.eclipse.org/legal/epl-v10.html
10 * and the Eclipse Distribution License is available at
11 * http://www.eclipse.org/org/documents/edl-v10.php.
12 *
13 *******************************************************************************/
14(function(p,s){"object"===typeof exports&&"object"===typeof module?module.exports=s():"function"===typeof define&&define.amd?define(s):"object"===typeof exports?exports=s():("undefined"===typeof p.Paho&&(p.Paho={}),p.Paho.MQTT=s())})(this,function(){return function(p){function s(a,b,c){b[c++]=a>>8;b[c++]=a%256;return c}function u(a,b,c,k){k=s(b,c,k);D(a,c,k);return k+b}function n(a){for(var b=0,c=0;c<a.length;c++){var k=a.charCodeAt(c);2047<k?(55296<=k&&56319>=k&&(c++,b++),b+=3):127<k?b+=2:b++}return b}
15function D(a,b,c){for(var k=0;k<a.length;k++){var e=a.charCodeAt(k);if(55296<=e&&56319>=e){var g=a.charCodeAt(++k);if(isNaN(g))throw Error(f(h.MALFORMED_UNICODE,[e,g]));e=(e-55296<<10)+(g-56320)+65536}127>=e?b[c++]=e:(2047>=e?b[c++]=e>>6&31|192:(65535>=e?b[c++]=e>>12&15|224:(b[c++]=e>>18&7|240,b[c++]=e>>12&63|128),b[c++]=e>>6&63|128),b[c++]=e&63|128)}return b}function E(a,b,c){for(var k="",e,g=b;g<b+c;){e=a[g++];if(!(128>e)){var m=a[g++]-128;if(0>m)throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),
16""]));if(224>e)e=64*(e-192)+m;else{var d=a[g++]-128;if(0>d)throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),d.toString(16)]));if(240>e)e=4096*(e-224)+64*m+d;else{var l=a[g++]-128;if(0>l)throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),d.toString(16),l.toString(16)]));if(248>e)e=262144*(e-240)+4096*m+64*d+l;else throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),d.toString(16),l.toString(16)]));}}}65535<e&&(e-=65536,k+=String.fromCharCode(55296+(e>>10)),e=56320+(e&
171023));k+=String.fromCharCode(e)}return k}var z=function(a,b){for(var c in a)if(a.hasOwnProperty(c))if(b.hasOwnProperty(c)){if(typeof a[c]!==b[c])throw Error(f(h.INVALID_TYPE,[typeof a[c],c]));}else{c="Unknown property, "+c+". Valid properties are:";for(var k in b)b.hasOwnProperty(k)&&(c=c+" "+k);throw Error(c);}},v=function(a,b){return function(){return a.apply(b,arguments)}},h={OK:{code:0,text:"AMQJSC0000I OK."},CONNECT_TIMEOUT:{code:1,text:"AMQJSC0001E Connect timed out."},SUBSCRIBE_TIMEOUT:{code:2,
18text:"AMQJS0002E Subscribe timed out."},UNSUBSCRIBE_TIMEOUT:{code:3,text:"AMQJS0003E Unsubscribe timed out."},PING_TIMEOUT:{code:4,text:"AMQJS0004E Ping timed out."},INTERNAL_ERROR:{code:5,text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"},CONNACK_RETURNCODE:{code:6,text:"AMQJS0006E Bad Connack return code:{0} {1}."},SOCKET_ERROR:{code:7,text:"AMQJS0007E Socket error:{0}."},SOCKET_CLOSE:{code:8,text:"AMQJS0008I Socket closed."},MALFORMED_UTF:{code:9,text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."},
19UNSUPPORTED:{code:10,text:"AMQJS0010E {0} is not supported by this browser."},INVALID_STATE:{code:11,text:"AMQJS0011E Invalid state {0}."},INVALID_TYPE:{code:12,text:"AMQJS0012E Invalid type {0} for {1}."},INVALID_ARGUMENT:{code:13,text:"AMQJS0013E Invalid argument {0} for {1}."},UNSUPPORTED_OPERATION:{code:14,text:"AMQJS0014E Unsupported operation."},INVALID_STORED_DATA:{code:15,text:"AMQJS0015E Invalid data in local storage key\x3d{0} value\x3d{1}."},INVALID_MQTT_MESSAGE_TYPE:{code:16,text:"AMQJS0016E Invalid MQTT message type {0}."},
20MALFORMED_UNICODE:{code:17,text:"AMQJS0017E Malformed Unicode string:{0} {1}."},BUFFER_FULL:{code:18,text:"AMQJS0018E Message buffer is full, maximum buffer size: {0}."}},H={0:"Connection Accepted",1:"Connection Refused: unacceptable protocol version",2:"Connection Refused: identifier rejected",3:"Connection Refused: server unavailable",4:"Connection Refused: bad user name or password",5:"Connection Refused: not authorized"},f=function(a,b){var c=a.text;if(b)for(var k,e,g=0;g<b.length;g++)if(k="{"+
21g+"}",e=c.indexOf(k),0<e)var h=c.substring(0,e),c=c.substring(e+k.length),c=h+b[g]+c;return c},A=[0,6,77,81,73,115,100,112,3],B=[0,4,77,81,84,84,4],q=function(a,b){this.type=a;for(var c in b)b.hasOwnProperty(c)&&(this[c]=b[c])};q.prototype.encode=function(){var a=(this.type&15)<<4,b=0,c=[],k=0,e;void 0!==this.messageIdentifier&&(b+=2);switch(this.type){case 1:switch(this.mqttVersion){case 3:b+=A.length+3;break;case 4:b+=B.length+3}b+=n(this.clientId)+2;void 0!==this.willMessage&&(b+=n(this.willMessage.destinationName)+
222,e=this.willMessage.payloadBytes,e instanceof Uint8Array||(e=new Uint8Array(h)),b+=e.byteLength+2);void 0!==this.userName&&(b+=n(this.userName)+2);void 0!==this.password&&(b+=n(this.password)+2);break;case 8:for(var a=a|2,g=0;g<this.topics.length;g++)c[g]=n(this.topics[g]),b+=c[g]+2;b+=this.requestedQos.length;break;case 10:a|=2;for(g=0;g<this.topics.length;g++)c[g]=n(this.topics[g]),b+=c[g]+2;break;case 6:a|=2;break;case 3:this.payloadMessage.duplicate&&(a|=8);a=a|=this.payloadMessage.qos<<1;this.payloadMessage.retained&&
23(a|=1);var k=n(this.payloadMessage.destinationName),h=this.payloadMessage.payloadBytes,b=b+(k+2)+h.byteLength;h instanceof ArrayBuffer?h=new Uint8Array(h):h instanceof Uint8Array||(h=new Uint8Array(h.buffer))}var f=b,g=Array(1),d=0;do{var t=f%128,f=f>>7;0<f&&(t|=128);g[d++]=t}while(0<f&&4>d);f=g.length+1;b=new ArrayBuffer(b+f);d=new Uint8Array(b);d[0]=a;d.set(g,1);if(3==this.type)f=u(this.payloadMessage.destinationName,k,d,f);else if(1==this.type){switch(this.mqttVersion){case 3:d.set(A,f);f+=A.length;
24break;case 4:d.set(B,f),f+=B.length}a=0;this.cleanSession&&(a=2);void 0!==this.willMessage&&(a=a|4|this.willMessage.qos<<3,this.willMessage.retained&&(a|=32));void 0!==this.userName&&(a|=128);void 0!==this.password&&(a|=64);d[f++]=a;f=s(this.keepAliveInterval,d,f)}void 0!==this.messageIdentifier&&(f=s(this.messageIdentifier,d,f));switch(this.type){case 1:f=u(this.clientId,n(this.clientId),d,f);void 0!==this.willMessage&&(f=u(this.willMessage.destinationName,n(this.willMessage.destinationName),d,f),
25f=s(e.byteLength,d,f),d.set(e,f),f+=e.byteLength);void 0!==this.userName&&(f=u(this.userName,n(this.userName),d,f));void 0!==this.password&&u(this.password,n(this.password),d,f);break;case 3:d.set(h,f);break;case 8:for(g=0;g<this.topics.length;g++)f=u(this.topics[g],c[g],d,f),d[f++]=this.requestedQos[g];break;case 10:for(g=0;g<this.topics.length;g++)f=u(this.topics[g],c[g],d,f)}return b};var F=function(a,b,c){this._client=a;this._window=b;this._keepAliveInterval=1E3*c;this.isReset=!1;var k=(new q(12)).encode(),
26e=function(a){return function(){return g.apply(a)}},g=function(){this.isReset?(this.isReset=!1,this._client._trace("Pinger.doPing","send PINGREQ"),this._client.socket.send(k),this.timeout=this._window.setTimeout(e(this),this._keepAliveInterval)):(this._client._trace("Pinger.doPing","Timed out"),this._client._disconnected(h.PING_TIMEOUT.code,f(h.PING_TIMEOUT)))};this.reset=function(){this.isReset=!0;this._window.clearTimeout(this.timeout);0<this._keepAliveInterval&&(this.timeout=setTimeout(e(this),
27this._keepAliveInterval))};this.cancel=function(){this._window.clearTimeout(this.timeout)}},w=function(a,b,c,f,e){this._window=b;c||(c=30);this.timeout=setTimeout(function(a,b,c){return function(){return a.apply(b,c)}}(f,a,e),1E3*c);this.cancel=function(){this._window.clearTimeout(this.timeout)}},d=function(a,b,c,d,e){if(!("WebSocket"in p&&null!==p.WebSocket))throw Error(f(h.UNSUPPORTED,["WebSocket"]));if(!("localStorage"in p&&null!==p.localStorage))throw Error(f(h.UNSUPPORTED,["localStorage"]));
28if(!("ArrayBuffer"in p&&null!==p.ArrayBuffer))throw Error(f(h.UNSUPPORTED,["ArrayBuffer"]));this._trace("Paho.MQTT.Client",a,b,c,d,e);this.host=b;this.port=c;this.path=d;this.uri=a;this.clientId=e;this._wsuri=null;this._localKey=b+":"+c+("/mqtt"!=d?":"+d:"")+":"+e+":";this._msg_queue=[];this._buffered_msg_queue=[];this._sentMessages={};this._receivedMessages={};this._notify_msg_sent={};this._message_identifier=1;this._sequence=0;for(var g in localStorage)0!==g.indexOf("Sent:"+this._localKey)&&0!==
29g.indexOf("Received:"+this._localKey)||this.restore(g)};d.prototype.host=null;d.prototype.port=null;d.prototype.path=null;d.prototype.uri=null;d.prototype.clientId=null;d.prototype.socket=null;d.prototype.connected=!1;d.prototype.maxMessageIdentifier=65536;d.prototype.connectOptions=null;d.prototype.hostIndex=null;d.prototype.onConnected=null;d.prototype.onConnectionLost=null;d.prototype.onMessageDelivered=null;d.prototype.onMessageArrived=null;d.prototype.traceFunction=null;d.prototype._msg_queue=
30null;d.prototype._buffered_msg_queue=null;d.prototype._connectTimeout=null;d.prototype.sendPinger=null;d.prototype.receivePinger=null;d.prototype._reconnectInterval=1;d.prototype._reconnecting=!1;d.prototype._reconnectTimeout=null;d.prototype.disconnectedPublishing=!1;d.prototype.disconnectedBufferSize=5E3;d.prototype.receiveBuffer=null;d.prototype._traceBuffer=null;d.prototype._MAX_TRACE_ENTRIES=100;d.prototype.connect=function(a){var b=this._traceMask(a,"password");this._trace("Client.connect",
31b,this.socket,this.connected);if(this.connected)throw Error(f(h.INVALID_STATE,["already connected"]));if(this.socket)throw Error(f(h.INVALID_STATE,["already connected"]));this._reconnecting&&(this._reconnectTimeout.cancel(),this._reconnectTimeout=null,this._reconnecting=!1);this.connectOptions=a;this._reconnectInterval=1;this._reconnecting=!1;a.uris?(this.hostIndex=0,this._doConnect(a.uris[0])):this._doConnect(this.uri)};d.prototype.subscribe=function(a,b){this._trace("Client.subscribe",a,b);if(!this.connected)throw Error(f(h.INVALID_STATE,
32["not connected"]));var c=new q(8);c.topics=[a];c.requestedQos=void 0!==b.qos?[b.qos]:[0];b.onSuccess&&(c.onSuccess=function(a){b.onSuccess({invocationContext:b.invocationContext,grantedQos:a})});b.onFailure&&(c.onFailure=function(a){b.onFailure({invocationContext:b.invocationContext,errorCode:a,errorMessage:f(a)})});b.timeout&&(c.timeOut=new w(this,window,b.timeout,b.onFailure,[{invocationContext:b.invocationContext,errorCode:h.SUBSCRIBE_TIMEOUT.code,errorMessage:f(h.SUBSCRIBE_TIMEOUT)}]));this._requires_ack(c);
33this._schedule_message(c)};d.prototype.unsubscribe=function(a,b){this._trace("Client.unsubscribe",a,b);if(!this.connected)throw Error(f(h.INVALID_STATE,["not connected"]));var c=new q(10);c.topics=[a];b.onSuccess&&(c.callback=function(){b.onSuccess({invocationContext:b.invocationContext})});b.timeout&&(c.timeOut=new w(this,window,b.timeout,b.onFailure,[{invocationContext:b.invocationContext,errorCode:h.UNSUBSCRIBE_TIMEOUT.code,errorMessage:f(h.UNSUBSCRIBE_TIMEOUT)}]));this._requires_ack(c);this._schedule_message(c)};
34d.prototype.send=function(a){this._trace("Client.send",a);wireMessage=new q(3);wireMessage.payloadMessage=a;if(this.connected)0<a.qos?this._requires_ack(wireMessage):this.onMessageDelivered&&(this._notify_msg_sent[wireMessage]=this.onMessageDelivered(wireMessage.payloadMessage)),this._schedule_message(wireMessage);else if(this._reconnecting&&this.disconnectedPublishing){if(Object.keys(this._sentMessages).length+this._buffered_msg_queue.length>this.disconnectedBufferSize)throw Error(f(h.BUFFER_FULL,
35[this.disconnectedBufferSize]));0<a.qos?this._requires_ack(wireMessage):(wireMessage.sequence=++this._sequence,this._buffered_msg_queue.push(wireMessage))}else throw Error(f(h.INVALID_STATE,["not connected"]));};d.prototype.disconnect=function(){this._trace("Client.disconnect");this._reconnecting&&(this._reconnectTimeout.cancel(),this._reconnectTimeout=null,this._reconnecting=!1);if(!this.socket)throw Error(f(h.INVALID_STATE,["not connecting or connected"]));wireMessage=new q(14);this._notify_msg_sent[wireMessage]=
36v(this._disconnected,this);this._schedule_message(wireMessage)};d.prototype.getTraceLog=function(){if(null!==this._traceBuffer){this._trace("Client.getTraceLog",new Date);this._trace("Client.getTraceLog in flight messages",this._sentMessages.length);for(var a in this._sentMessages)this._trace("_sentMessages ",a,this._sentMessages[a]);for(a in this._receivedMessages)this._trace("_receivedMessages ",a,this._receivedMessages[a]);return this._traceBuffer}};d.prototype.startTrace=function(){null===this._traceBuffer&&
37(this._traceBuffer=[]);this._trace("Client.startTrace",new Date,"1.0.3")};d.prototype.stopTrace=function(){delete this._traceBuffer};d.prototype._doConnect=function(a){this.connectOptions.useSSL&&(a=a.split(":"),a[0]="wss",a=a.join(":"));this._wsuri=a;this.connected=!1;this.socket=4>this.connectOptions.mqttVersion?new WebSocket(a,["mqttv3.1"]):new WebSocket(a,["mqtt"]);this.socket.binaryType="arraybuffer";this.socket.onopen=v(this._on_socket_open,this);this.socket.onmessage=v(this._on_socket_message,
38this);this.socket.onerror=v(this._on_socket_error,this);this.socket.onclose=v(this._on_socket_close,this);this.sendPinger=new F(this,window,this.connectOptions.keepAliveInterval);this.receivePinger=new F(this,window,this.connectOptions.keepAliveInterval);this._connectTimeout&&(this._connectTimeout.cancel(),this._connectTimeout=null);this._connectTimeout=new w(this,window,this.connectOptions.timeout,this._disconnected,[h.CONNECT_TIMEOUT.code,f(h.CONNECT_TIMEOUT)])};d.prototype._schedule_message=function(a){this._msg_queue.push(a);
39this.connected&&this._process_queue()};d.prototype.store=function(a,b){var c={type:b.type,messageIdentifier:b.messageIdentifier,version:1};switch(b.type){case 3:b.pubRecReceived&&(c.pubRecReceived=!0);c.payloadMessage={};for(var d="",e=b.payloadMessage.payloadBytes,g=0;g<e.length;g++)d=15>=e[g]?d+"0"+e[g].toString(16):d+e[g].toString(16);c.payloadMessage.payloadHex=d;c.payloadMessage.qos=b.payloadMessage.qos;c.payloadMessage.destinationName=b.payloadMessage.destinationName;b.payloadMessage.duplicate&&
40(c.payloadMessage.duplicate=!0);b.payloadMessage.retained&&(c.payloadMessage.retained=!0);0===a.indexOf("Sent:")&&(void 0===b.sequence&&(b.sequence=++this._sequence),c.sequence=b.sequence);break;default:throw Error(f(h.INVALID_STORED_DATA,[key,c]));}localStorage.setItem(a+this._localKey+b.messageIdentifier,JSON.stringify(c))};d.prototype.restore=function(a){var b=localStorage.getItem(a),c=JSON.parse(b),d=new q(c.type,c);switch(c.type){case 3:for(var b=c.payloadMessage.payloadHex,e=new ArrayBuffer(b.length/
412),e=new Uint8Array(e),g=0;2<=b.length;){var m=parseInt(b.substring(0,2),16),b=b.substring(2,b.length);e[g++]=m}b=new Paho.MQTT.Message(e);b.qos=c.payloadMessage.qos;b.destinationName=c.payloadMessage.destinationName;c.payloadMessage.duplicate&&(b.duplicate=!0);c.payloadMessage.retained&&(b.retained=!0);d.payloadMessage=b;break;default:throw Error(f(h.INVALID_STORED_DATA,[a,b]));}0===a.indexOf("Sent:"+this._localKey)?(d.payloadMessage.duplicate=!0,this._sentMessages[d.messageIdentifier]=d):0===a.indexOf("Received:"+
42this._localKey)&&(this._receivedMessages[d.messageIdentifier]=d)};d.prototype._process_queue=function(){for(var a=null,b=this._msg_queue.reverse();a=b.pop();)this._socket_send(a),this._notify_msg_sent[a]&&(this._notify_msg_sent[a](),delete this._notify_msg_sent[a])};d.prototype._requires_ack=function(a){var b=Object.keys(this._sentMessages).length;if(b>this.maxMessageIdentifier)throw Error("Too many messages:"+b);for(;void 0!==this._sentMessages[this._message_identifier];)this._message_identifier++;
43a.messageIdentifier=this._message_identifier;this._sentMessages[a.messageIdentifier]=a;3===a.type&&this.store("Sent:",a);this._message_identifier===this.maxMessageIdentifier&&(this._message_identifier=1)};d.prototype._on_socket_open=function(){var a=new q(1,this.connectOptions);a.clientId=this.clientId;this._socket_send(a)};d.prototype._on_socket_message=function(a){this._trace("Client._on_socket_message",a.data);a=this._deframeMessages(a.data);for(var b=0;b<a.length;b+=1)this._handleMessage(a[b])};
44d.prototype._deframeMessages=function(a){a=new Uint8Array(a);var b=[];if(this.receiveBuffer){var c=new Uint8Array(this.receiveBuffer.length+a.length);c.set(this.receiveBuffer);c.set(a,this.receiveBuffer.length);a=c;delete this.receiveBuffer}try{for(c=0;c<a.length;){var d;a:{var e=a,g=c,m=g,n=e[g],l=n>>4,t=n&15,g=g+1,x=void 0,C=0,p=1;do{if(g==e.length){d=[null,m];break a}x=e[g++];C+=(x&127)*p;p*=128}while(0!==(x&128));x=g+C;if(x>e.length)d=[null,m];else{var y=new q(l);switch(l){case 2:e[g++]&1&&(y.sessionPresent=
45!0);y.returnCode=e[g++];break;case 3:var m=t>>1&3,s=256*e[g]+e[g+1],g=g+2,u=E(e,g,s),g=g+s;0<m&&(y.messageIdentifier=256*e[g]+e[g+1],g+=2);var r=new Paho.MQTT.Message(e.subarray(g,x));1==(t&1)&&(r.retained=!0);8==(t&8)&&(r.duplicate=!0);r.qos=m;r.destinationName=u;y.payloadMessage=r;break;case 4:case 5:case 6:case 7:case 11:y.messageIdentifier=256*e[g]+e[g+1];break;case 9:y.messageIdentifier=256*e[g]+e[g+1],g+=2,y.returnCode=e.subarray(g,x)}d=[y,x]}}var v=d[0],c=d[1];if(null!==v)b.push(v);else break}c<
46a.length&&(this.receiveBuffer=a.subarray(c))}catch(w){d="undefined"==w.hasOwnProperty("stack")?w.stack.toString():"No Error Stack Available";this._disconnected(h.INTERNAL_ERROR.code,f(h.INTERNAL_ERROR,[w.message,d]));return}return b};d.prototype._handleMessage=function(a){this._trace("Client._handleMessage",a);try{switch(a.type){case 2:this._connectTimeout.cancel();this._reconnectTimeout&&this._reconnectTimeout.cancel();if(this.connectOptions.cleanSession){for(var b in this._sentMessages){var c=this._sentMessages[b];
47localStorage.removeItem("Sent:"+this._localKey+c.messageIdentifier)}this._sentMessages={};for(b in this._receivedMessages){var d=this._receivedMessages[b];localStorage.removeItem("Received:"+this._localKey+d.messageIdentifier)}this._receivedMessages={}}if(0===a.returnCode)this.connected=!0,this.connectOptions.uris&&(this.hostIndex=this.connectOptions.uris.length);else{this._disconnected(h.CONNACK_RETURNCODE.code,f(h.CONNACK_RETURNCODE,[a.returnCode,H[a.returnCode]]));break}a=[];for(var e in this._sentMessages)this._sentMessages.hasOwnProperty(e)&&
48a.push(this._sentMessages[e]);if(0<this._buffered_msg_queue.length){e=null;for(var g=this._buffered_msg_queue.reverse();e=g.pop();)a.push(e),this.onMessageDelivered&&(this._notify_msg_sent[e]=this.onMessageDelivered(e.payloadMessage))}a=a.sort(function(a,b){return a.sequence-b.sequence});for(var g=0,m=a.length;g<m;g++)if(c=a[g],3==c.type&&c.pubRecReceived){var n=new q(6,{messageIdentifier:c.messageIdentifier});this._schedule_message(n)}else this._schedule_message(c);if(this.connectOptions.onSuccess)this.connectOptions.onSuccess({invocationContext:this.connectOptions.invocationContext});
49c=!1;this._reconnecting&&(c=!0,this._reconnectInterval=1,this._reconnecting=!1);this._connected(c,this._wsuri);this._process_queue();break;case 3:this._receivePublish(a);break;case 4:if(c=this._sentMessages[a.messageIdentifier])if(delete this._sentMessages[a.messageIdentifier],localStorage.removeItem("Sent:"+this._localKey+a.messageIdentifier),this.onMessageDelivered)this.onMessageDelivered(c.payloadMessage);break;case 5:if(c=this._sentMessages[a.messageIdentifier])c.pubRecReceived=!0,n=new q(6,{messageIdentifier:a.messageIdentifier}),
50this.store("Sent:",c),this._schedule_message(n);break;case 6:d=this._receivedMessages[a.messageIdentifier];localStorage.removeItem("Received:"+this._localKey+a.messageIdentifier);d&&(this._receiveMessage(d),delete this._receivedMessages[a.messageIdentifier]);var l=new q(7,{messageIdentifier:a.messageIdentifier});this._schedule_message(l);break;case 7:c=this._sentMessages[a.messageIdentifier];delete this._sentMessages[a.messageIdentifier];localStorage.removeItem("Sent:"+this._localKey+a.messageIdentifier);
51if(this.onMessageDelivered)this.onMessageDelivered(c.payloadMessage);break;case 9:if(c=this._sentMessages[a.messageIdentifier]){c.timeOut&&c.timeOut.cancel();if(128===a.returnCode[0]){if(c.onFailure)c.onFailure(a.returnCode)}else if(c.onSuccess)c.onSuccess(a.returnCode);delete this._sentMessages[a.messageIdentifier]}break;case 11:if(c=this._sentMessages[a.messageIdentifier])c.timeOut&&c.timeOut.cancel(),c.callback&&c.callback(),delete this._sentMessages[a.messageIdentifier];break;case 13:this.sendPinger.reset();
52break;case 14:this._disconnected(h.INVALID_MQTT_MESSAGE_TYPE.code,f(h.INVALID_MQTT_MESSAGE_TYPE,[a.type]));break;default:this._disconnected(h.INVALID_MQTT_MESSAGE_TYPE.code,f(h.INVALID_MQTT_MESSAGE_TYPE,[a.type]))}}catch(t){c="undefined"==t.hasOwnProperty("stack")?t.stack.toString():"No Error Stack Available",this._disconnected(h.INTERNAL_ERROR.code,f(h.INTERNAL_ERROR,[t.message,c]))}};d.prototype._on_socket_error=function(a){this._reconnecting||this._disconnected(h.SOCKET_ERROR.code,f(h.SOCKET_ERROR,
53[a.data]))};d.prototype._on_socket_close=function(){this._reconnecting||this._disconnected(h.SOCKET_CLOSE.code,f(h.SOCKET_CLOSE))};d.prototype._socket_send=function(a){if(1==a.type){var b=this._traceMask(a,"password");this._trace("Client._socket_send",b)}else this._trace("Client._socket_send",a);this.socket.send(a.encode());this.sendPinger.reset()};d.prototype._receivePublish=function(a){switch(a.payloadMessage.qos){case "undefined":case 0:this._receiveMessage(a);break;case 1:var b=new q(4,{messageIdentifier:a.messageIdentifier});
54this._schedule_message(b);this._receiveMessage(a);break;case 2:this._receivedMessages[a.messageIdentifier]=a;this.store("Received:",a);a=new q(5,{messageIdentifier:a.messageIdentifier});this._schedule_message(a);break;default:throw Error("Invaild qos\x3d"+wireMmessage.payloadMessage.qos);}};d.prototype._receiveMessage=function(a){if(this.onMessageArrived)this.onMessageArrived(a.payloadMessage)};d.prototype._connected=function(a,b){if(this.onConnected)this.onConnected(a,b)};d.prototype._reconnect=
55function(){this._trace("Client._reconnect");this.connected||(this._reconnecting=!0,this.sendPinger.cancel(),this.receivePinger.cancel(),128>this._reconnectInterval&&(this._reconnectInterval*=2),this.connectOptions.uris?(this.hostIndex=0,this._doConnect(this.connectOptions.uris[0])):this._doConnect(this.uri))};d.prototype._disconnected=function(a,b){this._trace("Client._disconnected",a,b);if(void 0!==a&&this._reconnecting)this._reconnectTimeout=new w(this,window,this._reconnectInterval,this._reconnect);
56else if(this.sendPinger.cancel(),this.receivePinger.cancel(),this._connectTimeout&&(this._connectTimeout.cancel(),this._connectTimeout=null),this._msg_queue=[],this._buffered_msg_queue=[],this._notify_msg_sent={},this.socket&&(this.socket.onopen=null,this.socket.onmessage=null,this.socket.onerror=null,this.socket.onclose=null,1===this.socket.readyState&&this.socket.close(),delete this.socket),this.connectOptions.uris&&this.hostIndex<this.connectOptions.uris.length-1)this.hostIndex++,this._doConnect(this.connectOptions.uris[this.hostIndex]);
57else if(void 0===a&&(a=h.OK.code,b=f(h.OK)),this.connected){this.connected=!1;if(this.onConnectionLost)this.onConnectionLost({errorCode:a,errorMessage:b,reconnect:this.connectOptions.reconnect,uri:this._wsuri});a!==h.OK.code&&this.connectOptions.reconnect&&(this._reconnectInterval=1,this._reconnect())}else if(4===this.connectOptions.mqttVersion&&!1===this.connectOptions.mqttVersionExplicit)this._trace("Failed to connect V4, dropping back to V3"),this.connectOptions.mqttVersion=3,this.connectOptions.uris?
58(this.hostIndex=0,this._doConnect(this.connectOptions.uris[0])):this._doConnect(this.uri);else if(this.connectOptions.onFailure)this.connectOptions.onFailure({invocationContext:this.connectOptions.invocationContext,errorCode:a,errorMessage:b})};d.prototype._trace=function(){if(this.traceFunction){for(var a in arguments)"undefined"!==typeof arguments[a]&&arguments.splice(a,1,JSON.stringify(arguments[a]));a=Array.prototype.slice.call(arguments).join("");this.traceFunction({severity:"Debug",message:a})}if(null!==
59this._traceBuffer){a=0;for(var b=arguments.length;a<b;a++)this._traceBuffer.length==this._MAX_TRACE_ENTRIES&&this._traceBuffer.shift(),0===a?this._traceBuffer.push(arguments[a]):"undefined"===typeof arguments[a]?this._traceBuffer.push(arguments[a]):this._traceBuffer.push(" "+JSON.stringify(arguments[a]))}};d.prototype._traceMask=function(a,b){var c={},d;for(d in a)a.hasOwnProperty(d)&&(c[d]=d==b?"******":a[d]);return c};var G=function(a,b,c,k){var e;if("string"!==typeof a)throw Error(f(h.INVALID_TYPE,
60[typeof a,"host"]));if(2==arguments.length){k=b;e=a;var g=e.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/);if(g)a=g[4]||g[2],b=parseInt(g[7]),c=g[8];else throw Error(f(h.INVALID_ARGUMENT,[a,"host"]));}else{3==arguments.length&&(k=c,c="/mqtt");if("number"!==typeof b||0>b)throw Error(f(h.INVALID_TYPE,[typeof b,"port"]));if("string"!==typeof c)throw Error(f(h.INVALID_TYPE,[typeof c,"path"]));e="ws://"+(-1!==a.indexOf(":")&&"["!==a.slice(0,1)&&"]"!==a.slice(-1)?"["+a+"]":a)+":"+b+c}for(var m=
61g=0;m<k.length;m++){var n=k.charCodeAt(m);55296<=n&&56319>=n&&m++;g++}if("string"!==typeof k||65535<g)throw Error(f(h.INVALID_ARGUMENT,[k,"clientId"]));var l=new d(e,a,b,c,k);this._getHost=function(){return a};this._setHost=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getPort=function(){return b};this._setPort=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getPath=function(){return c};this._setPath=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getURI=function(){return e};
62this._setURI=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getClientId=function(){return l.clientId};this._setClientId=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getOnConnected=function(){return l.onConnected};this._setOnConnected=function(a){if("function"===typeof a)l.onConnected=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onConnected"]));};this._getDisconnectedPublishing=function(){return l.disconnectedPublishing};this._setDisconnectedPublishing=function(a){l.disconnectedPublishing=
63a};this._getDisconnectedBufferSize=function(){return l.disconnectedBufferSize};this._setDisconnectedBufferSize=function(a){l.disconnectedBufferSize=a};this._getOnConnectionLost=function(){return l.onConnectionLost};this._setOnConnectionLost=function(a){if("function"===typeof a)l.onConnectionLost=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onConnectionLost"]));};this._getOnMessageDelivered=function(){return l.onMessageDelivered};this._setOnMessageDelivered=function(a){if("function"===typeof a)l.onMessageDelivered=
64a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onMessageDelivered"]));};this._getOnMessageArrived=function(){return l.onMessageArrived};this._setOnMessageArrived=function(a){if("function"===typeof a)l.onMessageArrived=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onMessageArrived"]));};this._getTrace=function(){return l.traceFunction};this._setTrace=function(a){if("function"===typeof a)l.traceFunction=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onTrace"]));};this.connect=function(a){a=a||{};z(a,
65{timeout:"number",userName:"string",password:"string",willMessage:"object",keepAliveInterval:"number",cleanSession:"boolean",useSSL:"boolean",invocationContext:"object",onSuccess:"function",onFailure:"function",hosts:"object",ports:"object",reconnect:"boolean",mqttVersion:"number",mqttVersionExplicit:"boolean",uris:"object"});void 0===a.keepAliveInterval&&(a.keepAliveInterval=60);if(4<a.mqttVersion||3>a.mqttVersion)throw Error(f(h.INVALID_ARGUMENT,[a.mqttVersion,"connectOptions.mqttVersion"]));void 0===
66a.mqttVersion?(a.mqttVersionExplicit=!1,a.mqttVersion=4):a.mqttVersionExplicit=!0;if(void 0!==a.password&&void 0===a.userName)throw Error(f(h.INVALID_ARGUMENT,[a.password,"connectOptions.password"]));if(a.willMessage){if(!(a.willMessage instanceof r))throw Error(f(h.INVALID_TYPE,[a.willMessage,"connectOptions.willMessage"]));a.willMessage.stringPayload=null;if("undefined"===typeof a.willMessage.destinationName)throw Error(f(h.INVALID_TYPE,[typeof a.willMessage.destinationName,"connectOptions.willMessage.destinationName"]));
67}"undefined"===typeof a.cleanSession&&(a.cleanSession=!0);if(a.hosts){if(!(a.hosts instanceof Array))throw Error(f(h.INVALID_ARGUMENT,[a.hosts,"connectOptions.hosts"]));if(1>a.hosts.length)throw Error(f(h.INVALID_ARGUMENT,[a.hosts,"connectOptions.hosts"]));for(var b=!1,d=0;d<a.hosts.length;d++){if("string"!==typeof a.hosts[d])throw Error(f(h.INVALID_TYPE,[typeof a.hosts[d],"connectOptions.hosts["+d+"]"]));if(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/.test(a.hosts[d]))if(0===d)b=!0;else{if(!b)throw Error(f(h.INVALID_ARGUMENT,
68[a.hosts[d],"connectOptions.hosts["+d+"]"]));}else if(b)throw Error(f(h.INVALID_ARGUMENT,[a.hosts[d],"connectOptions.hosts["+d+"]"]));}if(b)a.uris=a.hosts;else{if(!a.ports)throw Error(f(h.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));if(!(a.ports instanceof Array))throw Error(f(h.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));if(a.hosts.length!==a.ports.length)throw Error(f(h.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));a.uris=[];for(d=0;d<a.hosts.length;d++){if("number"!==typeof a.ports[d]||
690>a.ports[d])throw Error(f(h.INVALID_TYPE,[typeof a.ports[d],"connectOptions.ports["+d+"]"]));var b=a.hosts[d],g=a.ports[d];e="ws://"+(-1!==b.indexOf(":")?"["+b+"]":b)+":"+g+c;a.uris.push(e)}}}l.connect(a)};this.subscribe=function(a,b){if("string"!==typeof a)throw Error("Invalid argument:"+a);b=b||{};z(b,{qos:"number",invocationContext:"object",onSuccess:"function",onFailure:"function",timeout:"number"});if(b.timeout&&!b.onFailure)throw Error("subscribeOptions.timeout specified with no onFailure callback.");
70if("undefined"!==typeof b.qos&&0!==b.qos&&1!==b.qos&&2!==b.qos)throw Error(f(h.INVALID_ARGUMENT,[b.qos,"subscribeOptions.qos"]));l.subscribe(a,b)};this.unsubscribe=function(a,b){if("string"!==typeof a)throw Error("Invalid argument:"+a);b=b||{};z(b,{invocationContext:"object",onSuccess:"function",onFailure:"function",timeout:"number"});if(b.timeout&&!b.onFailure)throw Error("unsubscribeOptions.timeout specified with no onFailure callback.");l.unsubscribe(a,b)};this.send=function(a,b,c,d){var e;if(0===
71arguments.length)throw Error("Invalid argument.length");if(1==arguments.length){if(!(a instanceof r)&&"string"!==typeof a)throw Error("Invalid argument:"+typeof a);e=a;if("undefined"===typeof e.destinationName)throw Error(f(h.INVALID_ARGUMENT,[e.destinationName,"Message.destinationName"]));}else e=new r(b),e.destinationName=a,3<=arguments.length&&(e.qos=c),4<=arguments.length&&(e.retained=d);l.send(e)};this.publish=function(a,b,c,d){console.log("Publising message to: ",a);var e;if(0===arguments.length)throw Error("Invalid argument.length");
72if(1==arguments.length){if(!(a instanceof r)&&"string"!==typeof a)throw Error("Invalid argument:"+typeof a);e=a;if("undefined"===typeof e.destinationName)throw Error(f(h.INVALID_ARGUMENT,[e.destinationName,"Message.destinationName"]));}else e=new r(b),e.destinationName=a,3<=arguments.length&&(e.qos=c),4<=arguments.length&&(e.retained=d);l.send(e)};this.disconnect=function(){l.disconnect()};this.getTraceLog=function(){return l.getTraceLog()};this.startTrace=function(){l.startTrace()};this.stopTrace=
73function(){l.stopTrace()};this.isConnected=function(){return l.connected}};G.prototype={get host(){return this._getHost()},set host(a){this._setHost(a)},get port(){return this._getPort()},set port(a){this._setPort(a)},get path(){return this._getPath()},set path(a){this._setPath(a)},get clientId(){return this._getClientId()},set clientId(a){this._setClientId(a)},get onConnected(){return this._getOnConnected()},set onConnected(a){this._setOnConnected(a)},get disconnectedPublishing(){return this._getDisconnectedPublishing()},
74set disconnectedPublishing(a){this._setDisconnectedPublishing(a)},get disconnectedBufferSize(){return this._getDisconnectedBufferSize()},set disconnectedBufferSize(a){this._setDisconnectedBufferSize(a)},get onConnectionLost(){return this._getOnConnectionLost()},set onConnectionLost(a){this._setOnConnectionLost(a)},get onMessageDelivered(){return this._getOnMessageDelivered()},set onMessageDelivered(a){this._setOnMessageDelivered(a)},get onMessageArrived(){return this._getOnMessageArrived()},set onMessageArrived(a){this._setOnMessageArrived(a)},
75get trace(){return this._getTrace()},set trace(a){this._setTrace(a)}};var r=function(a){var b;if("string"===typeof a||a instanceof ArrayBuffer||a instanceof Int8Array||a instanceof Uint8Array||a instanceof Int16Array||a instanceof Uint16Array||a instanceof Int32Array||a instanceof Uint32Array||a instanceof Float32Array||a instanceof Float64Array)b=a;else throw f(h.INVALID_ARGUMENT,[a,"newPayload"]);this._getPayloadString=function(){return"string"===typeof b?b:E(b,0,b.length)};this._getPayloadBytes=
76function(){if("string"===typeof b){var a=new ArrayBuffer(n(b)),a=new Uint8Array(a);D(b,a,0);return a}return b};var c;this._getDestinationName=function(){return c};this._setDestinationName=function(a){if("string"===typeof a)c=a;else throw Error(f(h.INVALID_ARGUMENT,[a,"newDestinationName"]));};var d=0;this._getQos=function(){return d};this._setQos=function(a){if(0===a||1===a||2===a)d=a;else throw Error("Invalid argument:"+a);};var e=!1;this._getRetained=function(){return e};this._setRetained=function(a){if("boolean"===
77typeof a)e=a;else throw Error(f(h.INVALID_ARGUMENT,[a,"newRetained"]));};var g=!1;this._getDuplicate=function(){return g};this._setDuplicate=function(a){g=a}};r.prototype={get payloadString(){return this._getPayloadString()},get payloadBytes(){return this._getPayloadBytes()},get destinationName(){return this._getDestinationName()},set destinationName(a){this._setDestinationName(a)},get topic(){return this._getDestinationName()},set topic(a){this._setDestinationName(a)},get qos(){return this._getQos()},
78set qos(a){this._setQos(a)},get retained(){return this._getRetained()},set retained(a){this._setRetained(a)},get duplicate(){return this._getDuplicate()},set duplicate(a){this._setDuplicate(a)}};return{Client:G,Message:r}}(window)});

F. paho.javascript-1.0.3 / paho-mqtt.js

1/*******************************************************************************
2 * Copyright (c) 2013 IBM Corp.
3 *
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * and Eclipse Distribution License v1.0 which accompany this distribution.
7 *
8 * The Eclipse Public License is available at
9 * http://www.eclipse.org/legal/epl-v10.html
10 * and the Eclipse Distribution License is available at
11 * http://www.eclipse.org/org/documents/edl-v10.php.
12 *
13 * Contributors:
14 * Andrew Banks - initial API and implementation and initial documentation
15 *******************************************************************************/
16
17
18// Only expose a single object name in the global namespace.
19// Everything must go through this module. Global Paho.MQTT module
20// only has a single public function, client, which returns
21// a Paho.MQTT client object given connection details.
22
23/**
24 * Send and receive messages using web browsers.
25 * <p>
26 * This programming interface lets a JavaScript client application use the MQTT V3.1 or
27 * V3.1.1 protocol to connect to an MQTT-supporting messaging server.
28 *
29 * The function supported includes:
30 * <ol>
31 * <li>Connecting to and disconnecting from a server. The server is identified by its host name and port number.
32 * <li>Specifying options that relate to the communications link with the server,
33 * for example the frequency of keep-alive heartbeats, and whether SSL/TLS is required.
34 * <li>Subscribing to and receiving messages from MQTT Topics.
35 * <li>Publishing messages to MQTT Topics.
36 * </ol>
37 * <p>
38 * The API consists of two main objects:
39 * <dl>
40 * <dt><b>{@link Paho.MQTT.Client}</b></dt>
41 * <dd>This contains methods that provide the functionality of the API,
42 * including provision of callbacks that notify the application when a message
43 * arrives from or is delivered to the messaging server,
44 * or when the status of its connection to the messaging server changes.</dd>
45 * <dt><b>{@link Paho.MQTT.Message}</b></dt>
46 * <dd>This encapsulates the payload of the message along with various attributes
47 * associated with its delivery, in particular the destination to which it has
48 * been (or is about to be) sent.</dd>
49 * </dl>
50 * <p>
51 * The programming interface validates parameters passed to it, and will throw
52 * an Error containing an error message intended for developer use, if it detects
53 * an error with any parameter.
54 * <p>
55 * Example:
56 *
57 * <code><pre>
58client = new Paho.MQTT.Client(location.hostname, Number(location.port), "clientId");
59client.onConnectionLost = onConnectionLost;
60client.onMessageArrived = onMessageArrived;
61client.connect({onSuccess:onConnect});
62
63function onConnect() {
64 // Once a connection has been made, make a subscription and send a message.
65 console.log("onConnect");
66 client.subscribe("/World");
67 message = new Paho.MQTT.Message("Hello");
68 message.destinationName = "/World";
69 client.send(message);
70};
71function onConnectionLost(responseObject) {
72 if (responseObject.errorCode !== 0)
73 console.log("onConnectionLost:"+responseObject.errorMessage);
74};
75function onMessageArrived(message) {
76 console.log("onMessageArrived:"+message.payloadString);
77 client.disconnect();
78};
79 * </pre></code>
80 * @namespace Paho.MQTT
81 */
82
83/* jshint shadow:true */
84(function ExportLibrary(root, factory) {
85 if(typeof exports === 'object' && typeof module === 'object'){
86 module.exports = factory();
87 } else if (typeof define === 'function' && define.amd){
88 define(factory);
89 } else if (typeof exports === 'object'){
90 exports = factory();
91 } else {
92 if (typeof root.Paho === 'undefined'){
93 root.Paho = {};
94 }
95 root.Paho.MQTT = factory();
96 }
97})(this, function LibraryFactory(){
98
99
100var PahoMQTT = (function (global) {
101
102 // Private variables below, these are only visible inside the function closure
103 // which is used to define the module.
104
105 var version = "@VERSION@";
106 var buildLevel = "@BUILDLEVEL@";
107
108 /**
109 * Unique message type identifiers, with associated
110 * associated integer values.
111 * @private
112 */
113 var MESSAGE_TYPE = {
114 CONNECT: 1,
115 CONNACK: 2,
116 PUBLISH: 3,
117 PUBACK: 4,
118 PUBREC: 5,
119 PUBREL: 6,
120 PUBCOMP: 7,
121 SUBSCRIBE: 8,
122 SUBACK: 9,
123 UNSUBSCRIBE: 10,
124 UNSUBACK: 11,
125 PINGREQ: 12,
126 PINGRESP: 13,
127 DISCONNECT: 14
128 };
129
130 // Collection of utility methods used to simplify module code
131 // and promote the DRY pattern.
132
133 /**
134 * Validate an object's parameter names to ensure they
135 * match a list of expected variables name for this option
136 * type. Used to ensure option object passed into the API don't
137 * contain erroneous parameters.
138 * @param {Object} obj - User options object
139 * @param {Object} keys - valid keys and types that may exist in obj.
140 * @throws {Error} Invalid option parameter found.
141 * @private
142 */
143 var validate = function(obj, keys) {
144 for (var key in obj) {
145 if (obj.hasOwnProperty(key)) {
146 if (keys.hasOwnProperty(key)) {
147 if (typeof obj[key] !== keys[key])
148 throw new Error(format(ERROR.INVALID_TYPE, [typeof obj[key], key]));
149 } else {
150 var errorStr = "Unknown property, " + key + ". Valid properties are:";
151 for (var validKey in keys)
152 if (keys.hasOwnProperty(validKey))
153 errorStr = errorStr+" "+validKey;
154 throw new Error(errorStr);
155 }
156 }
157 }
158 };
159
160 /**
161 * Return a new function which runs the user function bound
162 * to a fixed scope.
163 * @param {function} User function
164 * @param {object} Function scope
165 * @return {function} User function bound to another scope
166 * @private
167 */
168 var scope = function (f, scope) {
169 return function () {
170 return f.apply(scope, arguments);
171 };
172 };
173
174 /**
175 * Unique message type identifiers, with associated
176 * associated integer values.
177 * @private
178 */
179 var ERROR = {
180 OK: {code:0, text:"AMQJSC0000I OK."},
181 CONNECT_TIMEOUT: {code:1, text:"AMQJSC0001E Connect timed out."},
182 SUBSCRIBE_TIMEOUT: {code:2, text:"AMQJS0002E Subscribe timed out."},
183 UNSUBSCRIBE_TIMEOUT: {code:3, text:"AMQJS0003E Unsubscribe timed out."},
184 PING_TIMEOUT: {code:4, text:"AMQJS0004E Ping timed out."},
185 INTERNAL_ERROR: {code:5, text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"},
186 CONNACK_RETURNCODE: {code:6, text:"AMQJS0006E Bad Connack return code:{0} {1}."},
187 SOCKET_ERROR: {code:7, text:"AMQJS0007E Socket error:{0}."},
188 SOCKET_CLOSE: {code:8, text:"AMQJS0008I Socket closed."},
189 MALFORMED_UTF: {code:9, text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."},
190 UNSUPPORTED: {code:10, text:"AMQJS0010E {0} is not supported by this browser."},
191 INVALID_STATE: {code:11, text:"AMQJS0011E Invalid state {0}."},
192 INVALID_TYPE: {code:12, text:"AMQJS0012E Invalid type {0} for {1}."},
193 INVALID_ARGUMENT: {code:13, text:"AMQJS0013E Invalid argument {0} for {1}."},
194 UNSUPPORTED_OPERATION: {code:14, text:"AMQJS0014E Unsupported operation."},
195 INVALID_STORED_DATA: {code:15, text:"AMQJS0015E Invalid data in local storage key={0} value={1}."},
196 INVALID_MQTT_MESSAGE_TYPE: {code:16, text:"AMQJS0016E Invalid MQTT message type {0}."},
197 MALFORMED_UNICODE: {code:17, text:"AMQJS0017E Malformed Unicode string:{0} {1}."},
198 BUFFER_FULL: {code:18, text:"AMQJS0018E Message buffer is full, maximum buffer size: {0}."},
199 };
200
201 /** CONNACK RC Meaning. */
202 var CONNACK_RC = {
203 0:"Connection Accepted",
204 1:"Connection Refused: unacceptable protocol version",
205 2:"Connection Refused: identifier rejected",
206 3:"Connection Refused: server unavailable",
207 4:"Connection Refused: bad user name or password",
208 5:"Connection Refused: not authorized"
209 };
210
211 /**
212 * Format an error message text.
213 * @private
214 * @param {error} ERROR.KEY value above.
215 * @param {substitutions} [array] substituted into the text.
216 * @return the text with the substitutions made.
217 */
218 var format = function(error, substitutions) {
219 var text = error.text;
220 if (substitutions) {
221 var field,start;
222 for (var i=0; i<substitutions.length; i++) {
223 field = "{"+i+"}";
224 start = text.indexOf(field);
225 if(start > 0) {
226 var part1 = text.substring(0,start);
227 var part2 = text.substring(start+field.length);
228 text = part1+substitutions[i]+part2;
229 }
230 }
231 }
232 return text;
233 };
234
235 //MQTT protocol and version 6 M Q I s d p 3
236 var MqttProtoIdentifierv3 = [0x00,0x06,0x4d,0x51,0x49,0x73,0x64,0x70,0x03];
237 //MQTT proto/version for 311 4 M Q T T 4
238 var MqttProtoIdentifierv4 = [0x00,0x04,0x4d,0x51,0x54,0x54,0x04];
239
240 /**
241 * Construct an MQTT wire protocol message.
242 * @param type MQTT packet type.
243 * @param options optional wire message attributes.
244 *
245 * Optional properties
246 *
247 * messageIdentifier: message ID in the range [0..65535]
248 * payloadMessage: Application Message - PUBLISH only
249 * connectStrings: array of 0 or more Strings to be put into the CONNECT payload
250 * topics: array of strings (SUBSCRIBE, UNSUBSCRIBE)
251 * requestQoS: array of QoS values [0..2]
252 *
253 * "Flag" properties
254 * cleanSession: true if present / false if absent (CONNECT)
255 * willMessage: true if present / false if absent (CONNECT)
256 * isRetained: true if present / false if absent (CONNECT)
257 * userName: true if present / false if absent (CONNECT)
258 * password: true if present / false if absent (CONNECT)
259 * keepAliveInterval: integer [0..65535] (CONNECT)
260 *
261 * @private
262 * @ignore
263 */
264 var WireMessage = function (type, options) {
265 this.type = type;
266 for (var name in options) {
267 if (options.hasOwnProperty(name)) {
268 this[name] = options[name];
269 }
270 }
271 };
272
273 WireMessage.prototype.encode = function() {
274 // Compute the first byte of the fixed header
275 var first = ((this.type & 0x0f) << 4);
276
277 /*
278 * Now calculate the length of the variable header + payload by adding up the lengths
279 * of all the component parts
280 */
281
282 var remLength = 0;
283 var topicStrLength = [];
284 var destinationNameLength = 0;
285 var willMessagePayloadBytes;
286
287 // if the message contains a messageIdentifier then we need two bytes for that
288 if (this.messageIdentifier !== undefined)
289 remLength += 2;
290
291 switch(this.type) {
292 // If this a Connect then we need to include 12 bytes for its header
293 case MESSAGE_TYPE.CONNECT:
294 switch(this.mqttVersion) {
295 case 3:
296 remLength += MqttProtoIdentifierv3.length + 3;
297 break;
298 case 4:
299 remLength += MqttProtoIdentifierv4.length + 3;
300 break;
301 }
302
303 remLength += UTF8Length(this.clientId) + 2;
304 if (this.willMessage !== undefined) {
305 remLength += UTF8Length(this.willMessage.destinationName) + 2;
306 // Will message is always a string, sent as UTF-8 characters with a preceding length.
307 willMessagePayloadBytes = this.willMessage.payloadBytes;
308 if (!(willMessagePayloadBytes instanceof Uint8Array))
309 willMessagePayloadBytes = new Uint8Array(payloadBytes);
310 remLength += willMessagePayloadBytes.byteLength +2;
311 }
312 if (this.userName !== undefined)
313 remLength += UTF8Length(this.userName) + 2;
314 if (this.password !== undefined)
315 remLength += UTF8Length(this.password) + 2;
316 break;
317
318 // Subscribe, Unsubscribe can both contain topic strings
319 case MESSAGE_TYPE.SUBSCRIBE:
320 first |= 0x02; // Qos = 1;
321 for ( var i = 0; i < this.topics.length; i++) {
322 topicStrLength[i] = UTF8Length(this.topics[i]);
323 remLength += topicStrLength[i] + 2;
324 }
325 remLength += this.requestedQos.length; // 1 byte for each topic's Qos
326 // QoS on Subscribe only
327 break;
328
329 case MESSAGE_TYPE.UNSUBSCRIBE:
330 first |= 0x02; // Qos = 1;
331 for ( var i = 0; i < this.topics.length; i++) {
332 topicStrLength[i] = UTF8Length(this.topics[i]);
333 remLength += topicStrLength[i] + 2;
334 }
335 break;
336
337 case MESSAGE_TYPE.PUBREL:
338 first |= 0x02; // Qos = 1;
339 break;
340
341 case MESSAGE_TYPE.PUBLISH:
342 if (this.payloadMessage.duplicate) first |= 0x08;
343 first = first |= (this.payloadMessage.qos << 1);
344 if (this.payloadMessage.retained) first |= 0x01;
345 destinationNameLength = UTF8Length(this.payloadMessage.destinationName);
346 remLength += destinationNameLength + 2;
347 var payloadBytes = this.payloadMessage.payloadBytes;
348 remLength += payloadBytes.byteLength;
349 if (payloadBytes instanceof ArrayBuffer)
350 payloadBytes = new Uint8Array(payloadBytes);
351 else if (!(payloadBytes instanceof Uint8Array))
352 payloadBytes = new Uint8Array(payloadBytes.buffer);
353 break;
354
355 case MESSAGE_TYPE.DISCONNECT:
356 break;
357
358 default:
359 break;
360 }
361
362 // Now we can allocate a buffer for the message
363
364 var mbi = encodeMBI(remLength); // Convert the length to MQTT MBI format
365 var pos = mbi.length + 1; // Offset of start of variable header
366 var buffer = new ArrayBuffer(remLength + pos);
367 var byteStream = new Uint8Array(buffer); // view it as a sequence of bytes
368
369 //Write the fixed header into the buffer
370 byteStream[0] = first;
371 byteStream.set(mbi,1);
372
373 // If this is a PUBLISH then the variable header starts with a topic
374 if (this.type == MESSAGE_TYPE.PUBLISH)
375 pos = writeString(this.payloadMessage.destinationName, destinationNameLength, byteStream, pos);
376 // If this is a CONNECT then the variable header contains the protocol name/version, flags and keepalive time
377
378 else if (this.type == MESSAGE_TYPE.CONNECT) {
379 switch (this.mqttVersion) {
380 case 3:
381 byteStream.set(MqttProtoIdentifierv3, pos);
382 pos += MqttProtoIdentifierv3.length;
383 break;
384 case 4:
385 byteStream.set(MqttProtoIdentifierv4, pos);
386 pos += MqttProtoIdentifierv4.length;
387 break;
388 }
389 var connectFlags = 0;
390 if (this.cleanSession)
391 connectFlags = 0x02;
392 if (this.willMessage !== undefined ) {
393 connectFlags |= 0x04;
394 connectFlags |= (this.willMessage.qos<<3);
395 if (this.willMessage.retained) {
396 connectFlags |= 0x20;
397 }
398 }
399 if (this.userName !== undefined)
400 connectFlags |= 0x80;
401 if (this.password !== undefined)
402 connectFlags |= 0x40;
403 byteStream[pos++] = connectFlags;
404 pos = writeUint16 (this.keepAliveInterval, byteStream, pos);
405 }
406
407 // Output the messageIdentifier - if there is one
408 if (this.messageIdentifier !== undefined)
409 pos = writeUint16 (this.messageIdentifier, byteStream, pos);
410
411 switch(this.type) {
412 case MESSAGE_TYPE.CONNECT:
413 pos = writeString(this.clientId, UTF8Length(this.clientId), byteStream, pos);
414 if (this.willMessage !== undefined) {
415 pos = writeString(this.willMessage.destinationName, UTF8Length(this.willMessage.destinationName), byteStream, pos);
416 pos = writeUint16(willMessagePayloadBytes.byteLength, byteStream, pos);
417 byteStream.set(willMessagePayloadBytes, pos);
418 pos += willMessagePayloadBytes.byteLength;
419
420 }
421 if (this.userName !== undefined)
422 pos = writeString(this.userName, UTF8Length(this.userName), byteStream, pos);
423 if (this.password !== undefined)
424 pos = writeString(this.password, UTF8Length(this.password), byteStream, pos);
425 break;
426
427 case MESSAGE_TYPE.PUBLISH:
428 // PUBLISH has a text or binary payload, if text do not add a 2 byte length field, just the UTF characters.
429 byteStream.set(payloadBytes, pos);
430
431 break;
432
433// case MESSAGE_TYPE.PUBREC:
434// case MESSAGE_TYPE.PUBREL:
435// case MESSAGE_TYPE.PUBCOMP:
436// break;
437
438 case MESSAGE_TYPE.SUBSCRIBE:
439 // SUBSCRIBE has a list of topic strings and request QoS
440 for (var i=0; i<this.topics.length; i++) {
441 pos = writeString(this.topics[i], topicStrLength[i], byteStream, pos);
442 byteStream[pos++] = this.requestedQos[i];
443 }
444 break;
445
446 case MESSAGE_TYPE.UNSUBSCRIBE:
447 // UNSUBSCRIBE has a list of topic strings
448 for (var i=0; i<this.topics.length; i++)
449 pos = writeString(this.topics[i], topicStrLength[i], byteStream, pos);
450 break;
451
452 default:
453 // Do nothing.
454 }
455
456 return buffer;
457 };
458
459 function decodeMessage(input,pos) {
460 var startingPos = pos;
461 var first = input[pos];
462 var type = first >> 4;
463 var messageInfo = first &= 0x0f;
464 pos += 1;
465
466
467 // Decode the remaining length (MBI format)
468
469 var digit;
470 var remLength = 0;
471 var multiplier = 1;
472 do {
473 if (pos == input.length) {
474 return [null,startingPos];
475 }
476 digit = input[pos++];
477 remLength += ((digit & 0x7F) * multiplier);
478 multiplier *= 128;
479 } while ((digit & 0x80) !== 0);
480
481 var endPos = pos+remLength;
482 if (endPos > input.length) {
483 return [null,startingPos];
484 }
485
486 var wireMessage = new WireMessage(type);
487 switch(type) {
488 case MESSAGE_TYPE.CONNACK:
489 var connectAcknowledgeFlags = input[pos++];
490 if (connectAcknowledgeFlags & 0x01)
491 wireMessage.sessionPresent = true;
492 wireMessage.returnCode = input[pos++];
493 break;
494
495 case MESSAGE_TYPE.PUBLISH:
496 var qos = (messageInfo >> 1) & 0x03;
497
498 var len = readUint16(input, pos);
499 pos += 2;
500 var topicName = parseUTF8(input, pos, len);
501 pos += len;
502 // If QoS 1 or 2 there will be a messageIdentifier
503 if (qos > 0) {
504 wireMessage.messageIdentifier = readUint16(input, pos);
505 pos += 2;
506 }
507
508 var message = new Paho.MQTT.Message(input.subarray(pos, endPos));
509 if ((messageInfo & 0x01) == 0x01)
510 message.retained = true;
511 if ((messageInfo & 0x08) == 0x08)
512 message.duplicate = true;
513 message.qos = qos;
514 message.destinationName = topicName;
515 wireMessage.payloadMessage = message;
516 break;
517
518 case MESSAGE_TYPE.PUBACK:
519 case MESSAGE_TYPE.PUBREC:
520 case MESSAGE_TYPE.PUBREL:
521 case MESSAGE_TYPE.PUBCOMP:
522 case MESSAGE_TYPE.UNSUBACK:
523 wireMessage.messageIdentifier = readUint16(input, pos);
524 break;
525
526 case MESSAGE_TYPE.SUBACK:
527 wireMessage.messageIdentifier = readUint16(input, pos);
528 pos += 2;
529 wireMessage.returnCode = input.subarray(pos, endPos);
530 break;
531
532 default:
533 break;
534 }
535
536 return [wireMessage,endPos];
537 }
538
539 function writeUint16(input, buffer, offset) {
540 buffer[offset++] = input >> 8; //MSB
541 buffer[offset++] = input % 256; //LSB
542 return offset;
543 }
544
545 function writeString(input, utf8Length, buffer, offset) {
546 offset = writeUint16(utf8Length, buffer, offset);
547 stringToUTF8(input, buffer, offset);
548 return offset + utf8Length;
549 }
550
551 function readUint16(buffer, offset) {
552 return 256*buffer[offset] + buffer[offset+1];
553 }
554
555 /**
556 * Encodes an MQTT Multi-Byte Integer
557 * @private
558 */
559 function encodeMBI(number) {
560 var output = new Array(1);
561 var numBytes = 0;
562
563 do {
564 var digit = number % 128;
565 number = number >> 7;
566 if (number > 0) {
567 digit |= 0x80;
568 }
569 output[numBytes++] = digit;
570 } while ( (number > 0) && (numBytes<4) );
571
572 return output;
573 }
574
575 /**
576 * Takes a String and calculates its length in bytes when encoded in UTF8.
577 * @private
578 */
579 function UTF8Length(input) {
580 var output = 0;
581 for (var i = 0; i<input.length; i++)
582 {
583 var charCode = input.charCodeAt(i);
584 if (charCode > 0x7FF)
585 {
586 // Surrogate pair means its a 4 byte character
587 if (0xD800 <= charCode && charCode <= 0xDBFF)
588 {
589 i++;
590 output++;
591 }
592 output +=3;
593 }
594 else if (charCode > 0x7F)
595 output +=2;
596 else
597 output++;
598 }
599 return output;
600 }
601
602 /**
603 * Takes a String and writes it into an array as UTF8 encoded bytes.
604 * @private
605 */
606 function stringToUTF8(input, output, start) {
607 var pos = start;
608 for (var i = 0; i<input.length; i++) {
609 var charCode = input.charCodeAt(i);
610
611 // Check for a surrogate pair.
612 if (0xD800 <= charCode && charCode <= 0xDBFF) {
613 var lowCharCode = input.charCodeAt(++i);
614 if (isNaN(lowCharCode)) {
615 throw new Error(format(ERROR.MALFORMED_UNICODE, [charCode, lowCharCode]));
616 }
617 charCode = ((charCode - 0xD800)<<10) + (lowCharCode - 0xDC00) + 0x10000;
618
619 }
620
621 if (charCode <= 0x7F) {
622 output[pos++] = charCode;
623 } else if (charCode <= 0x7FF) {
624 output[pos++] = charCode>>6 & 0x1F | 0xC0;
625 output[pos++] = charCode & 0x3F | 0x80;
626 } else if (charCode <= 0xFFFF) {
627 output[pos++] = charCode>>12 & 0x0F | 0xE0;
628 output[pos++] = charCode>>6 & 0x3F | 0x80;
629 output[pos++] = charCode & 0x3F | 0x80;
630 } else {
631 output[pos++] = charCode>>18 & 0x07 | 0xF0;
632 output[pos++] = charCode>>12 & 0x3F | 0x80;
633 output[pos++] = charCode>>6 & 0x3F | 0x80;
634 output[pos++] = charCode & 0x3F | 0x80;
635 }
636 }
637 return output;
638 }
639
640 function parseUTF8(input, offset, length) {
641 var output = "";
642 var utf16;
643 var pos = offset;
644
645 while (pos < offset+length)
646 {
647 var byte1 = input[pos++];
648 if (byte1 < 128)
649 utf16 = byte1;
650 else
651 {
652 var byte2 = input[pos++]-128;
653 if (byte2 < 0)
654 throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16),""]));
655 if (byte1 < 0xE0) // 2 byte character
656 utf16 = 64*(byte1-0xC0) + byte2;
657 else
658 {
659 var byte3 = input[pos++]-128;
660 if (byte3 < 0)
661 throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16)]));
662 if (byte1 < 0xF0) // 3 byte character
663 utf16 = 4096*(byte1-0xE0) + 64*byte2 + byte3;
664 else
665 {
666 var byte4 = input[pos++]-128;
667 if (byte4 < 0)
668 throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)]));
669 if (byte1 < 0xF8) // 4 byte character
670 utf16 = 262144*(byte1-0xF0) + 4096*byte2 + 64*byte3 + byte4;
671 else // longer encodings are not supported
672 throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)]));
673 }
674 }
675 }
676
677 if (utf16 > 0xFFFF) // 4 byte character - express as a surrogate pair
678 {
679 utf16 -= 0x10000;
680 output += String.fromCharCode(0xD800 + (utf16 >> 10)); // lead character
681 utf16 = 0xDC00 + (utf16 & 0x3FF); // trail character
682 }
683 output += String.fromCharCode(utf16);
684 }
685 return output;
686 }
687
688 /**
689 * Repeat keepalive requests, monitor responses.
690 * @ignore
691 */
692 var Pinger = function(client, window, keepAliveInterval) {
693 this._client = client;
694 this._window = window;
695 this._keepAliveInterval = keepAliveInterval*1000;
696 this.isReset = false;
697
698 var pingReq = new WireMessage(MESSAGE_TYPE.PINGREQ).encode();
699
700 var doTimeout = function (pinger) {
701 return function () {
702 return doPing.apply(pinger);
703 };
704 };
705
706 /** @ignore */
707 var doPing = function() {
708 if (!this.isReset) {
709 this._client._trace("Pinger.doPing", "Timed out");
710 this._client._disconnected( ERROR.PING_TIMEOUT.code , format(ERROR.PING_TIMEOUT));
711 } else {
712 this.isReset = false;
713 this._client._trace("Pinger.doPing", "send PINGREQ");
714 this._client.socket.send(pingReq);
715 this.timeout = this._window.setTimeout(doTimeout(this), this._keepAliveInterval);
716 }
717 };
718
719 this.reset = function() {
720 this.isReset = true;
721 this._window.clearTimeout(this.timeout);
722 if (this._keepAliveInterval > 0)
723 this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval);
724 };
725
726 this.cancel = function() {
727 this._window.clearTimeout(this.timeout);
728 };
729 };
730
731 /**
732 * Monitor request completion.
733 * @ignore
734 */
735 var Timeout = function(client, window, timeoutSeconds, action, args) {
736 this._window = window;
737 if (!timeoutSeconds)
738 timeoutSeconds = 30;
739
740 var doTimeout = function (action, client, args) {
741 return function () {
742 return action.apply(client, args);
743 };
744 };
745 this.timeout = setTimeout(doTimeout(action, client, args), timeoutSeconds * 1000);
746
747 this.cancel = function() {
748 this._window.clearTimeout(this.timeout);
749 };
750 };
751
752 /*
753 * Internal implementation of the Websockets MQTT V3.1 client.
754 *
755 * @name Paho.MQTT.ClientImpl @constructor
756 * @param {String} host the DNS nameof the webSocket host.
757 * @param {Number} port the port number for that host.
758 * @param {String} clientId the MQ client identifier.
759 */
760 var ClientImpl = function (uri, host, port, path, clientId) {
761 // Check dependencies are satisfied in this browser.
762 if (!("WebSocket" in global && global.WebSocket !== null)) {
763 throw new Error(format(ERROR.UNSUPPORTED, ["WebSocket"]));
764 }
765 if (!("localStorage" in global && global.localStorage !== null)) {
766 throw new Error(format(ERROR.UNSUPPORTED, ["localStorage"]));
767 }
768 if (!("ArrayBuffer" in global && global.ArrayBuffer !== null)) {
769 throw new Error(format(ERROR.UNSUPPORTED, ["ArrayBuffer"]));
770 }
771 this._trace("Paho.MQTT.Client", uri, host, port, path, clientId);
772
773 this.host = host;
774 this.port = port;
775 this.path = path;
776 this.uri = uri;
777 this.clientId = clientId;
778 this._wsuri = null;
779
780 // Local storagekeys are qualified with the following string.
781 // The conditional inclusion of path in the key is for backward
782 // compatibility to when the path was not configurable and assumed to
783 // be /mqtt
784 this._localKey=host+":"+port+(path!="/mqtt"?":"+path:"")+":"+clientId+":";
785
786 // Create private instance-only message queue
787 // Internal queue of messages to be sent, in sending order.
788 this._msg_queue = [];
789 this._buffered_msg_queue = [];
790
791 // Messages we have sent and are expecting a response for, indexed by their respective message ids.
792 this._sentMessages = {};
793
794 // Messages we have received and acknowleged and are expecting a confirm message for
795 // indexed by their respective message ids.
796 this._receivedMessages = {};
797
798 // Internal list of callbacks to be executed when messages
799 // have been successfully sent over web socket, e.g. disconnect
800 // when it doesn't have to wait for ACK, just message is dispatched.
801 this._notify_msg_sent = {};
802
803 // Unique identifier for SEND messages, incrementing
804 // counter as messages are sent.
805 this._message_identifier = 1;
806
807 // Used to determine the transmission sequence of stored sent messages.
808 this._sequence = 0;
809
810
811 // Load the local state, if any, from the saved version, only restore state relevant to this client.
812 for (var key in localStorage)
813 if ( key.indexOf("Sent:"+this._localKey) === 0 || key.indexOf("Received:"+this._localKey) === 0)
814 this.restore(key);
815 };
816
817 // Messaging Client public instance members.
818 ClientImpl.prototype.host = null;
819 ClientImpl.prototype.port = null;
820 ClientImpl.prototype.path = null;
821 ClientImpl.prototype.uri = null;
822 ClientImpl.prototype.clientId = null;
823
824 // Messaging Client private instance members.
825 ClientImpl.prototype.socket = null;
826 /* true once we have received an acknowledgement to a CONNECT packet. */
827 ClientImpl.prototype.connected = false;
828 /* The largest message identifier allowed, may not be larger than 2**16 but
829 * if set smaller reduces the maximum number of outbound messages allowed.
830 */
831 ClientImpl.prototype.maxMessageIdentifier = 65536;
832 ClientImpl.prototype.connectOptions = null;
833 ClientImpl.prototype.hostIndex = null;
834 ClientImpl.prototype.onConnected = null;
835 ClientImpl.prototype.onConnectionLost = null;
836 ClientImpl.prototype.onMessageDelivered = null;
837 ClientImpl.prototype.onMessageArrived = null;
838 ClientImpl.prototype.traceFunction = null;
839 ClientImpl.prototype._msg_queue = null;
840 ClientImpl.prototype._buffered_msg_queue = null;
841 ClientImpl.prototype._connectTimeout = null;
842 /* The sendPinger monitors how long we allow before we send data to prove to the server that we are alive. */
843 ClientImpl.prototype.sendPinger = null;
844 /* The receivePinger monitors how long we allow before we require evidence that the server is alive. */
845 ClientImpl.prototype.receivePinger = null;
846 ClientImpl.prototype._reconnectInterval = 1; // Reconnect Delay, starts at 1 second
847 ClientImpl.prototype._reconnecting = false;
848 ClientImpl.prototype._reconnectTimeout = null;
849 ClientImpl.prototype.disconnectedPublishing = false;
850 ClientImpl.prototype.disconnectedBufferSize = 5000;
851
852 ClientImpl.prototype.receiveBuffer = null;
853
854 ClientImpl.prototype._traceBuffer = null;
855 ClientImpl.prototype._MAX_TRACE_ENTRIES = 100;
856
857 ClientImpl.prototype.connect = function (connectOptions) {
858 var connectOptionsMasked = this._traceMask(connectOptions, "password");
859 this._trace("Client.connect", connectOptionsMasked, this.socket, this.connected);
860
861 if (this.connected)
862 throw new Error(format(ERROR.INVALID_STATE, ["already connected"]));
863 if (this.socket)
864 throw new Error(format(ERROR.INVALID_STATE, ["already connected"]));
865
866 if (this._reconnecting) {
867 // connect() function is called while reconnect is in progress.
868 // Terminate the auto reconnect process to use new connect options.
869 this._reconnectTimeout.cancel();
870 this._reconnectTimeout = null;
871 this._reconnecting = false;
872 }
873
874 this.connectOptions = connectOptions;
875 this._reconnectInterval = 1;
876 this._reconnecting = false;
877 if (connectOptions.uris) {
878 this.hostIndex = 0;
879 this._doConnect(connectOptions.uris[0]);
880 } else {
881 this._doConnect(this.uri);
882 }
883
884 };
885
886 ClientImpl.prototype.subscribe = function (filter, subscribeOptions) {
887 this._trace("Client.subscribe", filter, subscribeOptions);
888
889 if (!this.connected)
890 throw new Error(format(ERROR.INVALID_STATE, ["not connected"]));
891
892 var wireMessage = new WireMessage(MESSAGE_TYPE.SUBSCRIBE);
893 wireMessage.topics=[filter];
894 if (subscribeOptions.qos !== undefined)
895 wireMessage.requestedQos = [subscribeOptions.qos];
896 else
897 wireMessage.requestedQos = [0];
898
899 if (subscribeOptions.onSuccess) {
900 wireMessage.onSuccess = function(grantedQos) {subscribeOptions.onSuccess({invocationContext:subscribeOptions.invocationContext,grantedQos:grantedQos});};
901 }
902
903 if (subscribeOptions.onFailure) {
904 wireMessage.onFailure = function(errorCode) {subscribeOptions.onFailure({invocationContext:subscribeOptions.invocationContext,errorCode:errorCode, errorMessage:format(errorCode)});};
905 }
906
907 if (subscribeOptions.timeout) {
908 wireMessage.timeOut = new Timeout(this, window, subscribeOptions.timeout, subscribeOptions.onFailure,
909 [{invocationContext:subscribeOptions.invocationContext,
910 errorCode:ERROR.SUBSCRIBE_TIMEOUT.code,
911 errorMessage:format(ERROR.SUBSCRIBE_TIMEOUT)}]);
912 }
913
914 // All subscriptions return a SUBACK.
915 this._requires_ack(wireMessage);
916 this._schedule_message(wireMessage);
917 };
918
919 /** @ignore */
920 ClientImpl.prototype.unsubscribe = function(filter, unsubscribeOptions) {
921 this._trace("Client.unsubscribe", filter, unsubscribeOptions);
922
923 if (!this.connected)
924 throw new Error(format(ERROR.INVALID_STATE, ["not connected"]));
925
926 var wireMessage = new WireMessage(MESSAGE_TYPE.UNSUBSCRIBE);
927 wireMessage.topics = [filter];
928
929 if (unsubscribeOptions.onSuccess) {
930 wireMessage.callback = function() {unsubscribeOptions.onSuccess({invocationContext:unsubscribeOptions.invocationContext});};
931 }
932 if (unsubscribeOptions.timeout) {
933 wireMessage.timeOut = new Timeout(this, window, unsubscribeOptions.timeout, unsubscribeOptions.onFailure,
934 [{invocationContext:unsubscribeOptions.invocationContext,
935 errorCode:ERROR.UNSUBSCRIBE_TIMEOUT.code,
936 errorMessage:format(ERROR.UNSUBSCRIBE_TIMEOUT)}]);
937 }
938
939 // All unsubscribes return a SUBACK.
940 this._requires_ack(wireMessage);
941 this._schedule_message(wireMessage);
942 };
943
944 ClientImpl.prototype.send = function (message) {
945 this._trace("Client.send", message);
946
947 wireMessage = new WireMessage(MESSAGE_TYPE.PUBLISH);
948 wireMessage.payloadMessage = message;
949
950 if (this.connected) {
951 // Mark qos 1 & 2 message as "ACK required"
952 // For qos 0 message, invoke onMessageDelivered callback if there is one.
953 // Then schedule the message.
954 if (message.qos > 0) {
955 this._requires_ack(wireMessage);
956 } else if (this.onMessageDelivered) {
957 this._notify_msg_sent[wireMessage] = this.onMessageDelivered(wireMessage.payloadMessage);
958 }
959 this._schedule_message(wireMessage);
960 } else {
961 // Currently disconnected, will not schedule this message
962 // Check if reconnecting is in progress and disconnected publish is enabled.
963 if (this._reconnecting && this.disconnectedPublishing) {
964 // Check the limit which include the "required ACK" messages
965 var messageCount = Object.keys(this._sentMessages).length + this._buffered_msg_queue.length;
966 if (messageCount > this.disconnectedBufferSize) {
967 throw new Error(format(ERROR.BUFFER_FULL, [this.disconnectedBufferSize]));
968 } else {
969 if (message.qos > 0) {
970 // Mark this message as "ACK required"
971 this._requires_ack(wireMessage);
972 } else {
973 wireMessage.sequence = ++this._sequence;
974 this._buffered_msg_queue.push(wireMessage);
975 }
976 }
977 } else {
978 throw new Error(format(ERROR.INVALID_STATE, ["not connected"]));
979 }
980 }
981 };
982
983 ClientImpl.prototype.disconnect = function () {
984 this._trace("Client.disconnect");
985
986 if (this._reconnecting) {
987 // disconnect() function is called while reconnect is in progress.
988 // Terminate the auto reconnect process.
989 this._reconnectTimeout.cancel();
990 this._reconnectTimeout = null;
991 this._reconnecting = false;
992 }
993
994 if (!this.socket)
995 throw new Error(format(ERROR.INVALID_STATE, ["not connecting or connected"]));
996
997 wireMessage = new WireMessage(MESSAGE_TYPE.DISCONNECT);
998
999 // Run the disconnected call back as soon as the message has been sent,
1000 // in case of a failure later on in the disconnect processing.
1001 // as a consequence, the _disconected call back may be run several times.
1002 this._notify_msg_sent[wireMessage] = scope(this._disconnected, this);
1003
1004 this._schedule_message(wireMessage);
1005 };
1006
1007 ClientImpl.prototype.getTraceLog = function () {
1008 if ( this._traceBuffer !== null ) {
1009 this._trace("Client.getTraceLog", new Date());
1010 this._trace("Client.getTraceLog in flight messages", this._sentMessages.length);
1011 for (var key in this._sentMessages)
1012 this._trace("_sentMessages ",key, this._sentMessages[key]);
1013 for (var key in this._receivedMessages)
1014 this._trace("_receivedMessages ",key, this._receivedMessages[key]);
1015
1016 return this._traceBuffer;
1017 }
1018 };
1019
1020 ClientImpl.prototype.startTrace = function () {
1021 if ( this._traceBuffer === null ) {
1022 this._traceBuffer = [];
1023 }
1024 this._trace("Client.startTrace", new Date(), version);
1025 };
1026
1027 ClientImpl.prototype.stopTrace = function () {
1028 delete this._traceBuffer;
1029 };
1030
1031 ClientImpl.prototype._doConnect = function (wsurl) {
1032 // When the socket is open, this client will send the CONNECT WireMessage using the saved parameters.
1033 if (this.connectOptions.useSSL) {
1034 var uriParts = wsurl.split(":");
1035 uriParts[0] = "wss";
1036 wsurl = uriParts.join(":");
1037 }
1038 this._wsuri = wsurl;
1039 this.connected = false;
1040
1041
1042
1043 if (this.connectOptions.mqttVersion < 4) {
1044 this.socket = new WebSocket(wsurl, ["mqttv3.1"]);
1045 } else {
1046 this.socket = new WebSocket(wsurl, ["mqtt"]);
1047 }
1048 this.socket.binaryType = 'arraybuffer';
1049 this.socket.onopen = scope(this._on_socket_open, this);
1050 this.socket.onmessage = scope(this._on_socket_message, this);
1051 this.socket.onerror = scope(this._on_socket_error, this);
1052 this.socket.onclose = scope(this._on_socket_close, this);
1053
1054 this.sendPinger = new Pinger(this, window, this.connectOptions.keepAliveInterval);
1055 this.receivePinger = new Pinger(this, window, this.connectOptions.keepAliveInterval);
1056 if (this._connectTimeout) {
1057 this._connectTimeout.cancel();
1058 this._connectTimeout = null;
1059 }
1060 this._connectTimeout = new Timeout(this, window, this.connectOptions.timeout, this._disconnected, [ERROR.CONNECT_TIMEOUT.code, format(ERROR.CONNECT_TIMEOUT)]);
1061 };
1062
1063
1064 // Schedule a new message to be sent over the WebSockets
1065 // connection. CONNECT messages cause WebSocket connection
1066 // to be started. All other messages are queued internally
1067 // until this has happened. When WS connection starts, process
1068 // all outstanding messages.
1069 ClientImpl.prototype._schedule_message = function (message) {
1070 this._msg_queue.push(message);
1071 // Process outstanding messages in the queue if we have an open socket, and have received CONNACK.
1072 if (this.connected) {
1073 this._process_queue();
1074 }
1075 };
1076
1077 ClientImpl.prototype.store = function(prefix, wireMessage) {
1078 var storedMessage = {type:wireMessage.type, messageIdentifier:wireMessage.messageIdentifier, version:1};
1079
1080 switch(wireMessage.type) {
1081 case MESSAGE_TYPE.PUBLISH:
1082 if(wireMessage.pubRecReceived)
1083 storedMessage.pubRecReceived = true;
1084
1085 // Convert the payload to a hex string.
1086 storedMessage.payloadMessage = {};
1087 var hex = "";
1088 var messageBytes = wireMessage.payloadMessage.payloadBytes;
1089 for (var i=0; i<messageBytes.length; i++) {
1090 if (messageBytes[i] <= 0xF)
1091 hex = hex+"0"+messageBytes[i].toString(16);
1092 else
1093 hex = hex+messageBytes[i].toString(16);
1094 }
1095 storedMessage.payloadMessage.payloadHex = hex;
1096
1097 storedMessage.payloadMessage.qos = wireMessage.payloadMessage.qos;
1098 storedMessage.payloadMessage.destinationName = wireMessage.payloadMessage.destinationName;
1099 if (wireMessage.payloadMessage.duplicate)
1100 storedMessage.payloadMessage.duplicate = true;
1101 if (wireMessage.payloadMessage.retained)
1102 storedMessage.payloadMessage.retained = true;
1103
1104 // Add a sequence number to sent messages.
1105 if ( prefix.indexOf("Sent:") === 0 ) {
1106 if ( wireMessage.sequence === undefined )
1107 wireMessage.sequence = ++this._sequence;
1108 storedMessage.sequence = wireMessage.sequence;
1109 }
1110 break;
1111
1112 default:
1113 throw Error(format(ERROR.INVALID_STORED_DATA, [key, storedMessage]));
1114 }
1115 localStorage.setItem(prefix+this._localKey+wireMessage.messageIdentifier, JSON.stringify(storedMessage));
1116 };
1117
1118 ClientImpl.prototype.restore = function(key) {
1119 var value = localStorage.getItem(key);
1120 var storedMessage = JSON.parse(value);
1121
1122 var wireMessage = new WireMessage(storedMessage.type, storedMessage);
1123
1124 switch(storedMessage.type) {
1125 case MESSAGE_TYPE.PUBLISH:
1126 // Replace the payload message with a Message object.
1127 var hex = storedMessage.payloadMessage.payloadHex;
1128 var buffer = new ArrayBuffer((hex.length)/2);
1129 var byteStream = new Uint8Array(buffer);
1130 var i = 0;
1131 while (hex.length >= 2) {
1132 var x = parseInt(hex.substring(0, 2), 16);
1133 hex = hex.substring(2, hex.length);
1134 byteStream[i++] = x;
1135 }
1136 var payloadMessage = new Paho.MQTT.Message(byteStream);
1137
1138 payloadMessage.qos = storedMessage.payloadMessage.qos;
1139 payloadMessage.destinationName = storedMessage.payloadMessage.destinationName;
1140 if (storedMessage.payloadMessage.duplicate)
1141 payloadMessage.duplicate = true;
1142 if (storedMessage.payloadMessage.retained)
1143 payloadMessage.retained = true;
1144 wireMessage.payloadMessage = payloadMessage;
1145
1146 break;
1147
1148 default:
1149 throw Error(format(ERROR.INVALID_STORED_DATA, [key, value]));
1150 }
1151
1152 if (key.indexOf("Sent:"+this._localKey) === 0) {
1153 wireMessage.payloadMessage.duplicate = true;
1154 this._sentMessages[wireMessage.messageIdentifier] = wireMessage;
1155 } else if (key.indexOf("Received:"+this._localKey) === 0) {
1156 this._receivedMessages[wireMessage.messageIdentifier] = wireMessage;
1157 }
1158 };
1159
1160 ClientImpl.prototype._process_queue = function () {
1161 var message = null;
1162 // Process messages in order they were added
1163 var fifo = this._msg_queue.reverse();
1164
1165 // Send all queued messages down socket connection
1166 while ((message = fifo.pop())) {
1167 this._socket_send(message);
1168 // Notify listeners that message was successfully sent
1169 if (this._notify_msg_sent[message]) {
1170 this._notify_msg_sent[message]();
1171 delete this._notify_msg_sent[message];
1172 }
1173 }
1174 };
1175
1176 /**
1177 * Expect an ACK response for this message. Add message to the set of in progress
1178 * messages and set an unused identifier in this message.
1179 * @ignore
1180 */
1181 ClientImpl.prototype._requires_ack = function (wireMessage) {
1182 var messageCount = Object.keys(this._sentMessages).length;
1183 if (messageCount > this.maxMessageIdentifier)
1184 throw Error ("Too many messages:"+messageCount);
1185
1186 while(this._sentMessages[this._message_identifier] !== undefined) {
1187 this._message_identifier++;
1188 }
1189 wireMessage.messageIdentifier = this._message_identifier;
1190 this._sentMessages[wireMessage.messageIdentifier] = wireMessage;
1191 if (wireMessage.type === MESSAGE_TYPE.PUBLISH) {
1192 this.store("Sent:", wireMessage);
1193 }
1194 if (this._message_identifier === this.maxMessageIdentifier) {
1195 this._message_identifier = 1;
1196 }
1197 };
1198
1199 /**
1200 * Called when the underlying websocket has been opened.
1201 * @ignore
1202 */
1203 ClientImpl.prototype._on_socket_open = function () {
1204 // Create the CONNECT message object.
1205 var wireMessage = new WireMessage(MESSAGE_TYPE.CONNECT, this.connectOptions);
1206 wireMessage.clientId = this.clientId;
1207 this._socket_send(wireMessage);
1208 };
1209
1210 /**
1211 * Called when the underlying websocket has received a complete packet.
1212 * @ignore
1213 */
1214 ClientImpl.prototype._on_socket_message = function (event) {
1215 this._trace("Client._on_socket_message", event.data);
1216 var messages = this._deframeMessages(event.data);
1217 for (var i = 0; i < messages.length; i+=1) {
1218 this._handleMessage(messages[i]);
1219 }
1220 };
1221
1222 ClientImpl.prototype._deframeMessages = function(data) {
1223 var byteArray = new Uint8Array(data);
1224 var messages = [];
1225 if (this.receiveBuffer) {
1226 var newData = new Uint8Array(this.receiveBuffer.length+byteArray.length);
1227 newData.set(this.receiveBuffer);
1228 newData.set(byteArray,this.receiveBuffer.length);
1229 byteArray = newData;
1230 delete this.receiveBuffer;
1231 }
1232 try {
1233 var offset = 0;
1234 while(offset < byteArray.length) {
1235 var result = decodeMessage(byteArray,offset);
1236 var wireMessage = result[0];
1237 offset = result[1];
1238 if (wireMessage !== null) {
1239 messages.push(wireMessage);
1240 } else {
1241 break;
1242 }
1243 }
1244 if (offset < byteArray.length) {
1245 this.receiveBuffer = byteArray.subarray(offset);
1246 }
1247 } catch (error) {
1248 var errorStack = ((error.hasOwnProperty('stack') == 'undefined') ? error.stack.toString() : "No Error Stack Available");
1249 this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message,errorStack]));
1250 return;
1251 }
1252 return messages;
1253 };
1254
1255 ClientImpl.prototype._handleMessage = function(wireMessage) {
1256
1257 this._trace("Client._handleMessage", wireMessage);
1258
1259 try {
1260 switch(wireMessage.type) {
1261 case MESSAGE_TYPE.CONNACK:
1262 this._connectTimeout.cancel();
1263 if (this._reconnectTimeout)
1264 this._reconnectTimeout.cancel();
1265
1266 // If we have started using clean session then clear up the local state.
1267 if (this.connectOptions.cleanSession) {
1268 for (var key in this._sentMessages) {
1269 var sentMessage = this._sentMessages[key];
1270 localStorage.removeItem("Sent:"+this._localKey+sentMessage.messageIdentifier);
1271 }
1272 this._sentMessages = {};
1273
1274 for (var key in this._receivedMessages) {
1275 var receivedMessage = this._receivedMessages[key];
1276 localStorage.removeItem("Received:"+this._localKey+receivedMessage.messageIdentifier);
1277 }
1278 this._receivedMessages = {};
1279 }
1280 // Client connected and ready for business.
1281 if (wireMessage.returnCode === 0) {
1282
1283 this.connected = true;
1284 // Jump to the end of the list of uris and stop looking for a good host.
1285
1286 if (this.connectOptions.uris)
1287 this.hostIndex = this.connectOptions.uris.length;
1288
1289 } else {
1290 this._disconnected(ERROR.CONNACK_RETURNCODE.code , format(ERROR.CONNACK_RETURNCODE, [wireMessage.returnCode, CONNACK_RC[wireMessage.returnCode]]));
1291 break;
1292 }
1293
1294 // Resend messages.
1295 var sequencedMessages = [];
1296 for (var msgId in this._sentMessages) {
1297 if (this._sentMessages.hasOwnProperty(msgId))
1298 sequencedMessages.push(this._sentMessages[msgId]);
1299 }
1300
1301 // Also schedule qos 0 buffered messages if any
1302 if (this._buffered_msg_queue.length > 0) {
1303 var msg = null;
1304 var fifo = this._buffered_msg_queue.reverse();
1305 while ((msg = fifo.pop())) {
1306 sequencedMessages.push(msg);
1307 if (this.onMessageDelivered)
1308 this._notify_msg_sent[msg] = this.onMessageDelivered(msg.payloadMessage);
1309 }
1310 }
1311
1312 // Sort sentMessages into the original sent order.
1313 var sequencedMessages = sequencedMessages.sort(function(a,b) {return a.sequence - b.sequence;} );
1314 for (var i=0, len=sequencedMessages.length; i<len; i++) {
1315 var sentMessage = sequencedMessages[i];
1316 if (sentMessage.type == MESSAGE_TYPE.PUBLISH && sentMessage.pubRecReceived) {
1317 var pubRelMessage = new WireMessage(MESSAGE_TYPE.PUBREL, {messageIdentifier:sentMessage.messageIdentifier});
1318 this._schedule_message(pubRelMessage);
1319 } else {
1320 this._schedule_message(sentMessage);
1321 }
1322 }
1323
1324 // Execute the connectOptions.onSuccess callback if there is one.
1325 // Will also now return if this connection was the result of an automatic
1326 // reconnect and which URI was successfully connected to.
1327 if (this.connectOptions.onSuccess) {
1328 this.connectOptions.onSuccess({invocationContext:this.connectOptions.invocationContext});
1329 }
1330
1331 var reconnected = false;
1332 if (this._reconnecting) {
1333 reconnected = true;
1334 this._reconnectInterval = 1;
1335 this._reconnecting = false;
1336 }
1337
1338 // Execute the onConnected callback if there is one.
1339 this._connected(reconnected, this._wsuri);
1340
1341 // Process all queued messages now that the connection is established.
1342 this._process_queue();
1343 break;
1344
1345 case MESSAGE_TYPE.PUBLISH:
1346 this._receivePublish(wireMessage);
1347 break;
1348
1349 case MESSAGE_TYPE.PUBACK:
1350 var sentMessage = this._sentMessages[wireMessage.messageIdentifier];
1351 // If this is a re flow of a PUBACK after we have restarted receivedMessage will not exist.
1352 if (sentMessage) {
1353 delete this._sentMessages[wireMessage.messageIdentifier];
1354 localStorage.removeItem("Sent:"+this._localKey+wireMessage.messageIdentifier);
1355 if (this.onMessageDelivered)
1356 this.onMessageDelivered(sentMessage.payloadMessage);
1357 }
1358 break;
1359
1360 case MESSAGE_TYPE.PUBREC:
1361 var sentMessage = this._sentMessages[wireMessage.messageIdentifier];
1362 // If this is a re flow of a PUBREC after we have restarted receivedMessage will not exist.
1363 if (sentMessage) {
1364 sentMessage.pubRecReceived = true;
1365 var pubRelMessage = new WireMessage(MESSAGE_TYPE.PUBREL, {messageIdentifier:wireMessage.messageIdentifier});
1366 this.store("Sent:", sentMessage);
1367 this._schedule_message(pubRelMessage);
1368 }
1369 break;
1370
1371 case MESSAGE_TYPE.PUBREL:
1372 var receivedMessage = this._receivedMessages[wireMessage.messageIdentifier];
1373 localStorage.removeItem("Received:"+this._localKey+wireMessage.messageIdentifier);
1374 // If this is a re flow of a PUBREL after we have restarted receivedMessage will not exist.
1375 if (receivedMessage) {
1376 this._receiveMessage(receivedMessage);
1377 delete this._receivedMessages[wireMessage.messageIdentifier];
1378 }
1379 // Always flow PubComp, we may have previously flowed PubComp but the server lost it and restarted.
1380 var pubCompMessage = new WireMessage(MESSAGE_TYPE.PUBCOMP, {messageIdentifier:wireMessage.messageIdentifier});
1381 this._schedule_message(pubCompMessage);
1382
1383
1384 break;
1385
1386 case MESSAGE_TYPE.PUBCOMP:
1387 var sentMessage = this._sentMessages[wireMessage.messageIdentifier];
1388 delete this._sentMessages[wireMessage.messageIdentifier];
1389 localStorage.removeItem("Sent:"+this._localKey+wireMessage.messageIdentifier);
1390 if (this.onMessageDelivered)
1391 this.onMessageDelivered(sentMessage.payloadMessage);
1392 break;
1393
1394 case MESSAGE_TYPE.SUBACK:
1395 var sentMessage = this._sentMessages[wireMessage.messageIdentifier];
1396 if (sentMessage) {
1397 if(sentMessage.timeOut)
1398 sentMessage.timeOut.cancel();
1399 // This will need to be fixed when we add multiple topic support
1400 if (wireMessage.returnCode[0] === 0x80) {
1401 if (sentMessage.onFailure) {
1402 sentMessage.onFailure(wireMessage.returnCode);
1403 }
1404 } else if (sentMessage.onSuccess) {
1405 sentMessage.onSuccess(wireMessage.returnCode);
1406 }
1407 delete this._sentMessages[wireMessage.messageIdentifier];
1408 }
1409 break;
1410
1411 case MESSAGE_TYPE.UNSUBACK:
1412 var sentMessage = this._sentMessages[wireMessage.messageIdentifier];
1413 if (sentMessage) {
1414 if (sentMessage.timeOut)
1415 sentMessage.timeOut.cancel();
1416 if (sentMessage.callback) {
1417 sentMessage.callback();
1418 }
1419 delete this._sentMessages[wireMessage.messageIdentifier];
1420 }
1421
1422 break;
1423
1424 case MESSAGE_TYPE.PINGRESP:
1425 /* The sendPinger or receivePinger may have sent a ping, the receivePinger has already been reset. */
1426 this.sendPinger.reset();
1427 break;
1428
1429 case MESSAGE_TYPE.DISCONNECT:
1430 // Clients do not expect to receive disconnect packets.
1431 this._disconnected(ERROR.INVALID_MQTT_MESSAGE_TYPE.code , format(ERROR.INVALID_MQTT_MESSAGE_TYPE, [wireMessage.type]));
1432 break;
1433
1434 default:
1435 this._disconnected(ERROR.INVALID_MQTT_MESSAGE_TYPE.code , format(ERROR.INVALID_MQTT_MESSAGE_TYPE, [wireMessage.type]));
1436 }
1437 } catch (error) {
1438 var errorStack = ((error.hasOwnProperty('stack') == 'undefined') ? error.stack.toString() : "No Error Stack Available");
1439 this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message,errorStack]));
1440 return;
1441 }
1442 };
1443
1444 /** @ignore */
1445 ClientImpl.prototype._on_socket_error = function (error) {
1446 if (!this._reconnecting) {
1447 this._disconnected(ERROR.SOCKET_ERROR.code , format(ERROR.SOCKET_ERROR, [error.data]));
1448 }
1449 };
1450
1451 /** @ignore */
1452 ClientImpl.prototype._on_socket_close = function () {
1453 if (!this._reconnecting) {
1454 this._disconnected(ERROR.SOCKET_CLOSE.code , format(ERROR.SOCKET_CLOSE));
1455 }
1456 };
1457
1458 /** @ignore */
1459 ClientImpl.prototype._socket_send = function (wireMessage) {
1460
1461 if (wireMessage.type == 1) {
1462 var wireMessageMasked = this._traceMask(wireMessage, "password");
1463 this._trace("Client._socket_send", wireMessageMasked);
1464 }
1465 else this._trace("Client._socket_send", wireMessage);
1466
1467 this.socket.send(wireMessage.encode());
1468 /* We have proved to the server we are alive. */
1469 this.sendPinger.reset();
1470 };
1471
1472 /** @ignore */
1473 ClientImpl.prototype._receivePublish = function (wireMessage) {
1474 switch(wireMessage.payloadMessage.qos) {
1475 case "undefined":
1476 case 0:
1477 this._receiveMessage(wireMessage);
1478 break;
1479
1480 case 1:
1481 var pubAckMessage = new WireMessage(MESSAGE_TYPE.PUBACK, {messageIdentifier:wireMessage.messageIdentifier});
1482 this._schedule_message(pubAckMessage);
1483 this._receiveMessage(wireMessage);
1484 break;
1485
1486 case 2:
1487 this._receivedMessages[wireMessage.messageIdentifier] = wireMessage;
1488 this.store("Received:", wireMessage);
1489 var pubRecMessage = new WireMessage(MESSAGE_TYPE.PUBREC, {messageIdentifier:wireMessage.messageIdentifier});
1490 this._schedule_message(pubRecMessage);
1491
1492 break;
1493
1494 default:
1495 throw Error("Invaild qos="+wireMmessage.payloadMessage.qos);
1496 }
1497 };
1498
1499 /** @ignore */
1500 ClientImpl.prototype._receiveMessage = function (wireMessage) {
1501 if (this.onMessageArrived) {
1502 this.onMessageArrived(wireMessage.payloadMessage);
1503 }
1504 };
1505
1506 /**
1507 * Client has connected.
1508 * @param {reconnect} [boolean] indicate if this was a result of reconnect operation.
1509 * @param {uri} [string] fully qualified WebSocket URI of the server.
1510 */
1511 ClientImpl.prototype._connected = function (reconnect, uri) {
1512 // Execute the onConnected callback if there is one.
1513 if (this.onConnected)
1514 this.onConnected(reconnect, uri);
1515 };
1516
1517 /**
1518 * Attempts to reconnect the client to the server.
1519 * For each reconnect attempt, will double the reconnect interval
1520 * up to 128 seconds.
1521 */
1522 ClientImpl.prototype._reconnect = function () {
1523 this._trace("Client._reconnect");
1524 if (!this.connected) {
1525 this._reconnecting = true;
1526 this.sendPinger.cancel();
1527 this.receivePinger.cancel();
1528 if (this._reconnectInterval < 128)
1529 this._reconnectInterval = this._reconnectInterval * 2;
1530 if (this.connectOptions.uris) {
1531 this.hostIndex = 0;
1532 this._doConnect(this.connectOptions.uris[0]);
1533 } else {
1534 this._doConnect(this.uri);
1535 }
1536 }
1537 };
1538
1539 /**
1540 * Client has disconnected either at its own request or because the server
1541 * or network disconnected it. Remove all non-durable state.
1542 * @param {errorCode} [number] the error number.
1543 * @param {errorText} [string] the error text.
1544 * @ignore
1545 */
1546 ClientImpl.prototype._disconnected = function (errorCode, errorText) {
1547 this._trace("Client._disconnected", errorCode, errorText);
1548
1549 if (errorCode !== undefined && this._reconnecting) {
1550 //Continue automatic reconnect process
1551 this._reconnectTimeout = new Timeout(this, window, this._reconnectInterval, this._reconnect);
1552 return;
1553 }
1554
1555 this.sendPinger.cancel();
1556 this.receivePinger.cancel();
1557 if (this._connectTimeout) {
1558 this._connectTimeout.cancel();
1559 this._connectTimeout = null;
1560 }
1561
1562 // Clear message buffers.
1563 this._msg_queue = [];
1564 this._buffered_msg_queue = [];
1565 this._notify_msg_sent = {};
1566
1567 if (this.socket) {
1568 // Cancel all socket callbacks so that they cannot be driven again by this socket.
1569 this.socket.onopen = null;
1570 this.socket.onmessage = null;
1571 this.socket.onerror = null;
1572 this.socket.onclose = null;
1573 if (this.socket.readyState === 1)
1574 this.socket.close();
1575 delete this.socket;
1576 }
1577
1578 if (this.connectOptions.uris && this.hostIndex < this.connectOptions.uris.length-1) {
1579 // Try the next host.
1580 this.hostIndex++;
1581 this._doConnect(this.connectOptions.uris[this.hostIndex]);
1582 } else {
1583
1584 if (errorCode === undefined) {
1585 errorCode = ERROR.OK.code;
1586 errorText = format(ERROR.OK);
1587 }
1588
1589 // Run any application callbacks last as they may attempt to reconnect and hence create a new socket.
1590 if (this.connected) {
1591 this.connected = false;
1592 // Execute the connectionLostCallback if there is one, and we were connected.
1593 if (this.onConnectionLost) {
1594 this.onConnectionLost({errorCode:errorCode, errorMessage:errorText, reconnect:this.connectOptions.reconnect, uri:this._wsuri});
1595 }
1596 if (errorCode !== ERROR.OK.code && this.connectOptions.reconnect) {
1597 // Start automatic reconnect process for the very first time since last successful connect.
1598 this._reconnectInterval = 1;
1599 this._reconnect();
1600 return;
1601 }
1602 } else {
1603 // Otherwise we never had a connection, so indicate that the connect has failed.
1604 if (this.connectOptions.mqttVersion === 4 && this.connectOptions.mqttVersionExplicit === false) {
1605 this._trace("Failed to connect V4, dropping back to V3");
1606 this.connectOptions.mqttVersion = 3;
1607 if (this.connectOptions.uris) {
1608 this.hostIndex = 0;
1609 this._doConnect(this.connectOptions.uris[0]);
1610 } else {
1611 this._doConnect(this.uri);
1612 }
1613 } else if(this.connectOptions.onFailure) {
1614 this.connectOptions.onFailure({invocationContext:this.connectOptions.invocationContext, errorCode:errorCode, errorMessage:errorText});
1615 }
1616 }
1617 }
1618 };
1619
1620 /** @ignore */
1621 ClientImpl.prototype._trace = function () {
1622 // Pass trace message back to client's callback function
1623 if (this.traceFunction) {
1624 for (var i in arguments)
1625 {
1626 if (typeof arguments[i] !== "undefined")
1627 arguments.splice(i, 1, JSON.stringify(arguments[i]));
1628 }
1629 var record = Array.prototype.slice.call(arguments).join("");
1630 this.traceFunction ({severity: "Debug", message: record });
1631 }
1632
1633 //buffer style trace
1634 if ( this._traceBuffer !== null ) {
1635 for (var i = 0, max = arguments.length; i < max; i++) {
1636 if ( this._traceBuffer.length == this._MAX_TRACE_ENTRIES ) {
1637 this._traceBuffer.shift();
1638 }
1639 if (i === 0) this._traceBuffer.push(arguments[i]);
1640 else if (typeof arguments[i] === "undefined" ) this._traceBuffer.push(arguments[i]);
1641 else this._traceBuffer.push(" "+JSON.stringify(arguments[i]));
1642 }
1643 }
1644 };
1645
1646 /** @ignore */
1647 ClientImpl.prototype._traceMask = function (traceObject, masked) {
1648 var traceObjectMasked = {};
1649 for (var attr in traceObject) {
1650 if (traceObject.hasOwnProperty(attr)) {
1651 if (attr == masked)
1652 traceObjectMasked[attr] = "******";
1653 else
1654 traceObjectMasked[attr] = traceObject[attr];
1655 }
1656 }
1657 return traceObjectMasked;
1658 };
1659
1660 // ------------------------------------------------------------------------
1661 // Public Programming interface.
1662 // ------------------------------------------------------------------------
1663
1664 /**
1665 * The JavaScript application communicates to the server using a {@link Paho.MQTT.Client} object.
1666 * <p>
1667 * Most applications will create just one Client object and then call its connect() method,
1668 * however applications can create more than one Client object if they wish.
1669 * In this case the combination of host, port and clientId attributes must be different for each Client object.
1670 * <p>
1671 * The send, subscribe and unsubscribe methods are implemented as asynchronous JavaScript methods
1672 * (even though the underlying protocol exchange might be synchronous in nature).
1673 * This means they signal their completion by calling back to the application,
1674 * via Success or Failure callback functions provided by the application on the method in question.
1675 * Such callbacks are called at most once per method invocation and do not persist beyond the lifetime
1676 * of the script that made the invocation.
1677 * <p>
1678 * In contrast there are some callback functions, most notably <i>onMessageArrived</i>,
1679 * that are defined on the {@link Paho.MQTT.Client} object.
1680 * These may get called multiple times, and aren't directly related to specific method invocations made by the client.
1681 *
1682 * @name Paho.MQTT.Client
1683 *
1684 * @constructor
1685 *
1686 * @param {string} host - the address of the messaging server, as a fully qualified WebSocket URI, as a DNS name or dotted decimal IP address.
1687 * @param {number} port - the port number to connect to - only required if host is not a URI
1688 * @param {string} path - the path on the host to connect to - only used if host is not a URI. Default: '/mqtt'.
1689 * @param {string} clientId - the Messaging client identifier, between 1 and 23 characters in length.
1690 *
1691 * @property {string} host - <i>read only</i> the server's DNS hostname or dotted decimal IP address.
1692 * @property {number} port - <i>read only</i> the server's port.
1693 * @property {string} path - <i>read only</i> the server's path.
1694 * @property {string} clientId - <i>read only</i> used when connecting to the server.
1695 * @property {function} onConnectionLost - called when a connection has been lost.
1696 * after a connect() method has succeeded.
1697 * Establish the call back used when a connection has been lost. The connection may be
1698 * lost because the client initiates a disconnect or because the server or network
1699 * cause the client to be disconnected. The disconnect call back may be called without
1700 * the connectionComplete call back being invoked if, for example the client fails to
1701 * connect.
1702 * A single response object parameter is passed to the onConnectionLost callback containing the following fields:
1703 * <ol>
1704 * <li>errorCode
1705 * <li>errorMessage
1706 * </ol>
1707 * @property {function} onMessageDelivered - called when a message has been delivered.
1708 * All processing that this Client will ever do has been completed. So, for example,
1709 * in the case of a Qos=2 message sent by this client, the PubComp flow has been received from the server
1710 * and the message has been removed from persistent storage before this callback is invoked.
1711 * Parameters passed to the onMessageDelivered callback are:
1712 * <ol>
1713 * <li>{@link Paho.MQTT.Message} that was delivered.
1714 * </ol>
1715 * @property {function} onMessageArrived - called when a message has arrived in this Paho.MQTT.client.
1716 * Parameters passed to the onMessageArrived callback are:
1717 * <ol>
1718 * <li>{@link Paho.MQTT.Message} that has arrived.
1719 * </ol>
1720 * @property {function} onConnected - called when a connection is successfully made to the server.
1721 * after a connect() method.
1722 * Parameters passed to the onConnected callback are:
1723 * <ol>
1724 * <li>reconnect (boolean) - If true, the connection was the result of a reconnect.</li>
1725 * <li>URI (string) - The URI used to connect to the server.</li>
1726 * </ol>
1727 * @property {boolean} disconnectedPublishing - if set, will enable disconnected publishing in
1728 * in the event that the connection to the server is lost.
1729 * @property {number} disconnectedBufferSize - Used to set the maximum number of messages that the disconnected
1730 * buffer will hold before rejecting new messages. Default size: 5000 messages
1731 * @property {function} trace - called whenever trace is called. TODO
1732 */
1733 var Client = function (host, port, path, clientId) {
1734
1735 var uri;
1736
1737 if (typeof host !== "string")
1738 throw new Error(format(ERROR.INVALID_TYPE, [typeof host, "host"]));
1739
1740 if (arguments.length == 2) {
1741 // host: must be full ws:// uri
1742 // port: clientId
1743 clientId = port;
1744 uri = host;
1745 var match = uri.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/);
1746 if (match) {
1747 host = match[4]||match[2];
1748 port = parseInt(match[7]);
1749 path = match[8];
1750 } else {
1751 throw new Error(format(ERROR.INVALID_ARGUMENT,[host,"host"]));
1752 }
1753 } else {
1754 if (arguments.length == 3) {
1755 clientId = path;
1756 path = "/mqtt";
1757 }
1758 if (typeof port !== "number" || port < 0)
1759 throw new Error(format(ERROR.INVALID_TYPE, [typeof port, "port"]));
1760 if (typeof path !== "string")
1761 throw new Error(format(ERROR.INVALID_TYPE, [typeof path, "path"]));
1762
1763 var ipv6AddSBracket = (host.indexOf(":") !== -1 && host.slice(0,1) !== "[" && host.slice(-1) !== "]");
1764 uri = "ws://"+(ipv6AddSBracket?"["+host+"]":host)+":"+port+path;
1765 }
1766
1767 var clientIdLength = 0;
1768 for (var i = 0; i<clientId.length; i++) {
1769 var charCode = clientId.charCodeAt(i);
1770 if (0xD800 <= charCode && charCode <= 0xDBFF) {
1771 i++; // Surrogate pair.
1772 }
1773 clientIdLength++;
1774 }
1775 if (typeof clientId !== "string" || clientIdLength > 65535)
1776 throw new Error(format(ERROR.INVALID_ARGUMENT, [clientId, "clientId"]));
1777
1778 var client = new ClientImpl(uri, host, port, path, clientId);
1779 this._getHost = function() { return host; };
1780 this._setHost = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
1781
1782 this._getPort = function() { return port; };
1783 this._setPort = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
1784
1785 this._getPath = function() { return path; };
1786 this._setPath = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
1787
1788 this._getURI = function() { return uri; };
1789 this._setURI = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
1790
1791 this._getClientId = function() { return client.clientId; };
1792 this._setClientId = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); };
1793
1794 this._getOnConnected = function() { return client.onConnected; };
1795 this._setOnConnected = function(newOnConnected) {
1796 if (typeof newOnConnected === "function")
1797 client.onConnected = newOnConnected;
1798 else
1799 throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnected, "onConnected"]));
1800 };
1801
1802 this._getDisconnectedPublishing = function() { return client.disconnectedPublishing; };
1803 this._setDisconnectedPublishing = function(newDisconnectedPublishing) {
1804 client.disconnectedPublishing = newDisconnectedPublishing;
1805 };
1806
1807 this._getDisconnectedBufferSize = function() { return client.disconnectedBufferSize; };
1808 this._setDisconnectedBufferSize = function(newDisconnectedBufferSize) {
1809 client.disconnectedBufferSize = newDisconnectedBufferSize;
1810 };
1811
1812 this._getOnConnectionLost = function() { return client.onConnectionLost; };
1813 this._setOnConnectionLost = function(newOnConnectionLost) {
1814 if (typeof newOnConnectionLost === "function")
1815 client.onConnectionLost = newOnConnectionLost;
1816 else
1817 throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnectionLost, "onConnectionLost"]));
1818 };
1819
1820 this._getOnMessageDelivered = function() { return client.onMessageDelivered; };
1821 this._setOnMessageDelivered = function(newOnMessageDelivered) {
1822 if (typeof newOnMessageDelivered === "function")
1823 client.onMessageDelivered = newOnMessageDelivered;
1824 else
1825 throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageDelivered, "onMessageDelivered"]));
1826 };
1827
1828 this._getOnMessageArrived = function() { return client.onMessageArrived; };
1829 this._setOnMessageArrived = function(newOnMessageArrived) {
1830 if (typeof newOnMessageArrived === "function")
1831 client.onMessageArrived = newOnMessageArrived;
1832 else
1833 throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageArrived, "onMessageArrived"]));
1834 };
1835
1836 this._getTrace = function() { return client.traceFunction; };
1837 this._setTrace = function(trace) {
1838 if(typeof trace === "function"){
1839 client.traceFunction = trace;
1840 }else{
1841 throw new Error(format(ERROR.INVALID_TYPE, [typeof trace, "onTrace"]));
1842 }
1843 };
1844
1845 /**
1846 * Connect this Messaging client to its server.
1847 *
1848 * @name Paho.MQTT.Client#connect
1849 * @function
1850 * @param {object} connectOptions - Attributes used with the connection.
1851 * @param {number} connectOptions.timeout - If the connect has not succeeded within this
1852 * number of seconds, it is deemed to have failed.
1853 * The default is 30 seconds.
1854 * @param {string} connectOptions.userName - Authentication username for this connection.
1855 * @param {string} connectOptions.password - Authentication password for this connection.
1856 * @param {Paho.MQTT.Message} connectOptions.willMessage - sent by the server when the client
1857 * disconnects abnormally.
1858 * @param {number} connectOptions.keepAliveInterval - the server disconnects this client if
1859 * there is no activity for this number of seconds.
1860 * The default value of 60 seconds is assumed if not set.
1861 * @param {boolean} connectOptions.cleanSession - if true(default) the client and server
1862 * persistent state is deleted on successful connect.
1863 * @param {boolean} connectOptions.useSSL - if present and true, use an SSL Websocket connection.
1864 * @param {object} connectOptions.invocationContext - passed to the onSuccess callback or onFailure callback.
1865 * @param {function} connectOptions.onSuccess - called when the connect acknowledgement
1866 * has been received from the server.
1867 * A single response object parameter is passed to the onSuccess callback containing the following fields:
1868 * <ol>
1869 * <li>invocationContext as passed in to the onSuccess method in the connectOptions.
1870 * </ol>
1871 * @param {function} connectOptions.onFailure - called when the connect request has failed or timed out.
1872 * A single response object parameter is passed to the onFailure callback containing the following fields:
1873 * <ol>
1874 * <li>invocationContext as passed in to the onFailure method in the connectOptions.
1875 * <li>errorCode a number indicating the nature of the error.
1876 * <li>errorMessage text describing the error.
1877 * </ol>
1878 * @param {array} connectOptions.hosts - If present this contains either a set of hostnames or fully qualified
1879 * WebSocket URIs (ws://iot.eclipse.org:80/ws), that are tried in order in place
1880 * of the host and port paramater on the construtor. The hosts are tried one at at time in order until
1881 * one of then succeeds.
1882 * @param {array} connectOptions.ports - If present the set of ports matching the hosts. If hosts contains URIs, this property
1883 * is not used.
1884 * @param {boolean} connectOptions.reconnect - Sets whether the client will automatically attempt to reconnect
1885 * to the server if the connection is lost.
1886 *<ul>
1887 *<li>If set to false, the client will not attempt to automatically reconnect to the server in the event that the
1888 * connection is lost.</li>
1889 *<li>If set to true, in the event that the connection is lost, the client will attempt to reconnect to the server.
1890 * It will initially wait 1 second before it attempts to reconnect, for every failed reconnect attempt, the delay
1891 * will double until it is at 2 minutes at which point the delay will stay at 2 minutes.</li>
1892 *</ul>
1893 * @param {number} connectOptions.mqttVersion - The version of MQTT to use to connect to the MQTT Broker.
1894 *<ul>
1895 *<li>3 - MQTT V3.1</li>
1896 *<li>4 - MQTT V3.1.1</li>
1897 *</ul>
1898 * @param {boolean} connectOptions.mqttVersionExplicit - If set to true, will force the connection to use the
1899 * selected MQTT Version or will fail to connect.
1900 * @param {array} connectOptions.uris - If present, should contain a list of fully qualified WebSocket uris
1901 * (e.g. ws://iot.eclipse.org:80/ws), that are tried in order in place of the host and port parameter of the construtor.
1902 * The uris are tried one at a time in order until one of them succeeds. Do not use this in conjunction with hosts as
1903 * the hosts array will be converted to uris and will overwrite this property.
1904 * @throws {InvalidState} If the client is not in disconnected state. The client must have received connectionLost
1905 * or disconnected before calling connect for a second or subsequent time.
1906 */
1907 this.connect = function (connectOptions) {
1908 connectOptions = connectOptions || {} ;
1909 validate(connectOptions, {timeout:"number",
1910 userName:"string",
1911 password:"string",
1912 willMessage:"object",
1913 keepAliveInterval:"number",
1914 cleanSession:"boolean",
1915 useSSL:"boolean",
1916 invocationContext:"object",
1917 onSuccess:"function",
1918 onFailure:"function",
1919 hosts:"object",
1920 ports:"object",
1921 reconnect:"boolean",
1922 mqttVersion:"number",
1923 mqttVersionExplicit:"boolean",
1924 uris: "object"});
1925
1926 // If no keep alive interval is set, assume 60 seconds.
1927 if (connectOptions.keepAliveInterval === undefined)
1928 connectOptions.keepAliveInterval = 60;
1929
1930 if (connectOptions.mqttVersion > 4 || connectOptions.mqttVersion < 3) {
1931 throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.mqttVersion, "connectOptions.mqttVersion"]));
1932 }
1933
1934 if (connectOptions.mqttVersion === undefined) {
1935 connectOptions.mqttVersionExplicit = false;
1936 connectOptions.mqttVersion = 4;
1937 } else {
1938 connectOptions.mqttVersionExplicit = true;
1939 }
1940
1941 //Check that if password is set, so is username
1942 if (connectOptions.password !== undefined && connectOptions.userName === undefined)
1943 throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.password, "connectOptions.password"]));
1944
1945 if (connectOptions.willMessage) {
1946 if (!(connectOptions.willMessage instanceof Message))
1947 throw new Error(format(ERROR.INVALID_TYPE, [connectOptions.willMessage, "connectOptions.willMessage"]));
1948 // The will message must have a payload that can be represented as a string.
1949 // Cause the willMessage to throw an exception if this is not the case.
1950 connectOptions.willMessage.stringPayload = null;
1951
1952 if (typeof connectOptions.willMessage.destinationName === "undefined")
1953 throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.willMessage.destinationName, "connectOptions.willMessage.destinationName"]));
1954 }
1955 if (typeof connectOptions.cleanSession === "undefined")
1956 connectOptions.cleanSession = true;
1957 if (connectOptions.hosts) {
1958
1959 if (!(connectOptions.hosts instanceof Array) )
1960 throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"]));
1961 if (connectOptions.hosts.length <1 )
1962 throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"]));
1963
1964 var usingURIs = false;
1965 for (var i = 0; i<connectOptions.hosts.length; i++) {
1966 if (typeof connectOptions.hosts[i] !== "string")
1967 throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.hosts[i], "connectOptions.hosts["+i+"]"]));
1968 if (/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/.test(connectOptions.hosts[i])) {
1969 if (i === 0) {
1970 usingURIs = true;
1971 } else if (!usingURIs) {
1972 throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts[i], "connectOptions.hosts["+i+"]"]));
1973 }
1974 } else if (usingURIs) {
1975 throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts[i], "connectOptions.hosts["+i+"]"]));
1976 }
1977 }
1978
1979 if (!usingURIs) {
1980 if (!connectOptions.ports)
1981 throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.ports, "connectOptions.ports"]));
1982 if (!(connectOptions.ports instanceof Array) )
1983 throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.ports, "connectOptions.ports"]));
1984 if (connectOptions.hosts.length !== connectOptions.ports.length)
1985 throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.ports, "connectOptions.ports"]));
1986
1987 connectOptions.uris = [];
1988
1989 for (var i = 0; i<connectOptions.hosts.length; i++) {
1990 if (typeof connectOptions.ports[i] !== "number" || connectOptions.ports[i] < 0)
1991 throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.ports[i], "connectOptions.ports["+i+"]"]));
1992 var host = connectOptions.hosts[i];
1993 var port = connectOptions.ports[i];
1994
1995 var ipv6 = (host.indexOf(":") !== -1);
1996 uri = "ws://"+(ipv6?"["+host+"]":host)+":"+port+path;
1997 connectOptions.uris.push(uri);
1998 }
1999 } else {
2000 connectOptions.uris = connectOptions.hosts;
2001 }
2002 }
2003
2004 client.connect(connectOptions);
2005 };
2006
2007 /**
2008 * Subscribe for messages, request receipt of a copy of messages sent to the destinations described by the filter.
2009 *
2010 * @name Paho.MQTT.Client#subscribe
2011 * @function
2012 * @param {string} filter describing the destinations to receive messages from.
2013 * <br>
2014 * @param {object} subscribeOptions - used to control the subscription
2015 *
2016 * @param {number} subscribeOptions.qos - the maiximum qos of any publications sent
2017 * as a result of making this subscription.
2018 * @param {object} subscribeOptions.invocationContext - passed to the onSuccess callback
2019 * or onFailure callback.
2020 * @param {function} subscribeOptions.onSuccess - called when the subscribe acknowledgement
2021 * has been received from the server.
2022 * A single response object parameter is passed to the onSuccess callback containing the following fields:
2023 * <ol>
2024 * <li>invocationContext if set in the subscribeOptions.
2025 * </ol>
2026 * @param {function} subscribeOptions.onFailure - called when the subscribe request has failed or timed out.
2027 * A single response object parameter is passed to the onFailure callback containing the following fields:
2028 * <ol>
2029 * <li>invocationContext - if set in the subscribeOptions.
2030 * <li>errorCode - a number indicating the nature of the error.
2031 * <li>errorMessage - text describing the error.
2032 * </ol>
2033 * @param {number} subscribeOptions.timeout - which, if present, determines the number of
2034 * seconds after which the onFailure calback is called.
2035 * The presence of a timeout does not prevent the onSuccess
2036 * callback from being called when the subscribe completes.
2037 * @throws {InvalidState} if the client is not in connected state.
2038 */
2039 this.subscribe = function (filter, subscribeOptions) {
2040 if (typeof filter !== "string")
2041 throw new Error("Invalid argument:"+filter);
2042 subscribeOptions = subscribeOptions || {} ;
2043 validate(subscribeOptions, {qos:"number",
2044 invocationContext:"object",
2045 onSuccess:"function",
2046 onFailure:"function",
2047 timeout:"number"
2048 });
2049 if (subscribeOptions.timeout && !subscribeOptions.onFailure)
2050 throw new Error("subscribeOptions.timeout specified with no onFailure callback.");
2051 if (typeof subscribeOptions.qos !== "undefined" && !(subscribeOptions.qos === 0 || subscribeOptions.qos === 1 || subscribeOptions.qos === 2 ))
2052 throw new Error(format(ERROR.INVALID_ARGUMENT, [subscribeOptions.qos, "subscribeOptions.qos"]));
2053 client.subscribe(filter, subscribeOptions);
2054 };
2055
2056 /**
2057 * Unsubscribe for messages, stop receiving messages sent to destinations described by the filter.
2058 *
2059 * @name Paho.MQTT.Client#unsubscribe
2060 * @function
2061 * @param {string} filter - describing the destinations to receive messages from.
2062 * @param {object} unsubscribeOptions - used to control the subscription
2063 * @param {object} unsubscribeOptions.invocationContext - passed to the onSuccess callback
2064 or onFailure callback.
2065 * @param {function} unsubscribeOptions.onSuccess - called when the unsubscribe acknowledgement has been received from the server.
2066 * A single response object parameter is passed to the
2067 * onSuccess callback containing the following fields:
2068 * <ol>
2069 * <li>invocationContext - if set in the unsubscribeOptions.
2070 * </ol>
2071 * @param {function} unsubscribeOptions.onFailure called when the unsubscribe request has failed or timed out.
2072 * A single response object parameter is passed to the onFailure callback containing the following fields:
2073 * <ol>
2074 * <li>invocationContext - if set in the unsubscribeOptions.
2075 * <li>errorCode - a number indicating the nature of the error.
2076 * <li>errorMessage - text describing the error.
2077 * </ol>
2078 * @param {number} unsubscribeOptions.timeout - which, if present, determines the number of seconds
2079 * after which the onFailure callback is called. The presence of
2080 * a timeout does not prevent the onSuccess callback from being
2081 * called when the unsubscribe completes
2082 * @throws {InvalidState} if the client is not in connected state.
2083 */
2084 this.unsubscribe = function (filter, unsubscribeOptions) {
2085 if (typeof filter !== "string")
2086 throw new Error("Invalid argument:"+filter);
2087 unsubscribeOptions = unsubscribeOptions || {} ;
2088 validate(unsubscribeOptions, {invocationContext:"object",
2089 onSuccess:"function",
2090 onFailure:"function",
2091 timeout:"number"
2092 });
2093 if (unsubscribeOptions.timeout && !unsubscribeOptions.onFailure)
2094 throw new Error("unsubscribeOptions.timeout specified with no onFailure callback.");
2095 client.unsubscribe(filter, unsubscribeOptions);
2096 };
2097
2098 /**
2099 * Send a message to the consumers of the destination in the Message.
2100 *
2101 * @name Paho.MQTT.Client#send
2102 * @function
2103 * @param {string|Paho.MQTT.Message} topic - <b>mandatory</b> The name of the destination to which the message is to be sent.
2104 * - If it is the only parameter, used as Paho.MQTT.Message object.
2105 * @param {String|ArrayBuffer} payload - The message data to be sent.
2106 * @param {number} qos The Quality of Service used to deliver the message.
2107 * <dl>
2108 * <dt>0 Best effort (default).
2109 * <dt>1 At least once.
2110 * <dt>2 Exactly once.
2111 * </dl>
2112 * @param {Boolean} retained If true, the message is to be retained by the server and delivered
2113 * to both current and future subscriptions.
2114 * If false the server only delivers the message to current subscribers, this is the default for new Messages.
2115 * A received message has the retained boolean set to true if the message was published
2116 * with the retained boolean set to true
2117 * and the subscrption was made after the message has been published.
2118 * @throws {InvalidState} if the client is not connected.
2119 */
2120 this.send = function (topic,payload,qos,retained) {
2121 var message ;
2122
2123 if(arguments.length === 0){
2124 throw new Error("Invalid argument."+"length");
2125
2126 }else if(arguments.length == 1) {
2127
2128 if (!(topic instanceof Message) && (typeof topic !== "string"))
2129 throw new Error("Invalid argument:"+ typeof topic);
2130
2131 message = topic;
2132 if (typeof message.destinationName === "undefined")
2133 throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"]));
2134 client.send(message);
2135
2136 }else {
2137 //parameter checking in Message object
2138 message = new Message(payload);
2139 message.destinationName = topic;
2140 if(arguments.length >= 3)
2141 message.qos = qos;
2142 if(arguments.length >= 4)
2143 message.retained = retained;
2144 client.send(message);
2145 }
2146 };
2147
2148 /**
2149 * Publish a message to the consumers of the destination in the Message.
2150 * Synonym for Paho.Mqtt.Client#send
2151 *
2152 * @name Paho.MQTT.Client#publish
2153 * @function
2154 * @param {string|Paho.MQTT.Message} topic - <b>mandatory</b> The name of the topic to which the message is to be published.
2155 * - If it is the only parameter, used as Paho.MQTT.Message object.
2156 * @param {String|ArrayBuffer} payload - The message data to be published.
2157 * @param {number} qos The Quality of Service used to deliver the message.
2158 * <dl>
2159 * <dt>0 Best effort (default).
2160 * <dt>1 At least once.
2161 * <dt>2 Exactly once.
2162 * </dl>
2163 * @param {Boolean} retained If true, the message is to be retained by the server and delivered
2164 * to both current and future subscriptions.
2165 * If false the server only delivers the message to current subscribers, this is the default for new Messages.
2166 * A received message has the retained boolean set to true if the message was published
2167 * with the retained boolean set to true
2168 * and the subscrption was made after the message has been published.
2169 * @throws {InvalidState} if the client is not connected.
2170 */
2171 this.publish = function(topic,payload,qos,retained) {
2172 console.log("Publising message to: ", topic);
2173 var message ;
2174
2175 if(arguments.length === 0){
2176 throw new Error("Invalid argument."+"length");
2177
2178 }else if(arguments.length == 1) {
2179
2180 if (!(topic instanceof Message) && (typeof topic !== "string"))
2181 throw new Error("Invalid argument:"+ typeof topic);
2182
2183 message = topic;
2184 if (typeof message.destinationName === "undefined")
2185 throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"]));
2186 client.send(message);
2187
2188 }else {
2189 //parameter checking in Message object
2190 message = new Message(payload);
2191 message.destinationName = topic;
2192 if(arguments.length >= 3)
2193 message.qos = qos;
2194 if(arguments.length >= 4)
2195 message.retained = retained;
2196 client.send(message);
2197 }
2198 };
2199
2200 /**
2201 * Normal disconnect of this Messaging client from its server.
2202 *
2203 * @name Paho.MQTT.Client#disconnect
2204 * @function
2205 * @throws {InvalidState} if the client is already disconnected.
2206 */
2207 this.disconnect = function () {
2208 client.disconnect();
2209 };
2210
2211 /**
2212 * Get the contents of the trace log.
2213 *
2214 * @name Paho.MQTT.Client#getTraceLog
2215 * @function
2216 * @return {Object[]} tracebuffer containing the time ordered trace records.
2217 */
2218 this.getTraceLog = function () {
2219 return client.getTraceLog();
2220 };
2221
2222 /**
2223 * Start tracing.
2224 *
2225 * @name Paho.MQTT.Client#startTrace
2226 * @function
2227 */
2228 this.startTrace = function () {
2229 client.startTrace();
2230 };
2231
2232 /**
2233 * Stop tracing.
2234 *
2235 * @name Paho.MQTT.Client#stopTrace
2236 * @function
2237 */
2238 this.stopTrace = function () {
2239 client.stopTrace();
2240 };
2241
2242 this.isConnected = function() {
2243 return client.connected;
2244 };
2245 };
2246
2247 Client.prototype = {
2248 get host() { return this._getHost(); },
2249 set host(newHost) { this._setHost(newHost); },
2250
2251 get port() { return this._getPort(); },
2252 set port(newPort) { this._setPort(newPort); },
2253
2254 get path() { return this._getPath(); },
2255 set path(newPath) { this._setPath(newPath); },
2256
2257 get clientId() { return this._getClientId(); },
2258 set clientId(newClientId) { this._setClientId(newClientId); },
2259
2260 get onConnected() { return this._getOnConnected(); },
2261 set onConnected(newOnConnected) { this._setOnConnected(newOnConnected); },
2262
2263 get disconnectedPublishing() { return this._getDisconnectedPublishing(); },
2264 set disconnectedPublishing(newDisconnectedPublishing) { this._setDisconnectedPublishing(newDisconnectedPublishing); },
2265
2266 get disconnectedBufferSize() { return this._getDisconnectedBufferSize(); },
2267 set disconnectedBufferSize(newDisconnectedBufferSize) { this._setDisconnectedBufferSize(newDisconnectedBufferSize); },
2268
2269 get onConnectionLost() { return this._getOnConnectionLost(); },
2270 set onConnectionLost(newOnConnectionLost) { this._setOnConnectionLost(newOnConnectionLost); },
2271
2272 get onMessageDelivered() { return this._getOnMessageDelivered(); },
2273 set onMessageDelivered(newOnMessageDelivered) { this._setOnMessageDelivered(newOnMessageDelivered); },
2274
2275 get onMessageArrived() { return this._getOnMessageArrived(); },
2276 set onMessageArrived(newOnMessageArrived) { this._setOnMessageArrived(newOnMessageArrived); },
2277
2278 get trace() { return this._getTrace(); },
2279 set trace(newTraceFunction) { this._setTrace(newTraceFunction); }
2280
2281 };
2282
2283 /**
2284 * An application message, sent or received.
2285 * <p>
2286 * All attributes may be null, which implies the default values.
2287 *
2288 * @name Paho.MQTT.Message
2289 * @constructor
2290 * @param {String|ArrayBuffer} payload The message data to be sent.
2291 * <p>
2292 * @property {string} payloadString <i>read only</i> The payload as a string if the payload consists of valid UTF-8 characters.
2293 * @property {ArrayBuffer} payloadBytes <i>read only</i> The payload as an ArrayBuffer.
2294 * <p>
2295 * @property {string} destinationName <b>mandatory</b> The name of the destination to which the message is to be sent
2296 * (for messages about to be sent) or the name of the destination from which the message has been received.
2297 * (for messages received by the onMessage function).
2298 * <p>
2299 * @property {number} qos The Quality of Service used to deliver the message.
2300 * <dl>
2301 * <dt>0 Best effort (default).
2302 * <dt>1 At least once.
2303 * <dt>2 Exactly once.
2304 * </dl>
2305 * <p>
2306 * @property {Boolean} retained If true, the message is to be retained by the server and delivered
2307 * to both current and future subscriptions.
2308 * If false the server only delivers the message to current subscribers, this is the default for new Messages.
2309 * A received message has the retained boolean set to true if the message was published
2310 * with the retained boolean set to true
2311 * and the subscrption was made after the message has been published.
2312 * <p>
2313 * @property {Boolean} duplicate <i>read only</i> If true, this message might be a duplicate of one which has already been received.
2314 * This is only set on messages received from the server.
2315 *
2316 */
2317 var Message = function (newPayload) {
2318 var payload;
2319 if ( typeof newPayload === "string" ||
2320 newPayload instanceof ArrayBuffer ||
2321 newPayload instanceof Int8Array ||
2322 newPayload instanceof Uint8Array ||
2323 newPayload instanceof Int16Array ||
2324 newPayload instanceof Uint16Array ||
2325 newPayload instanceof Int32Array ||
2326 newPayload instanceof Uint32Array ||
2327 newPayload instanceof Float32Array ||
2328 newPayload instanceof Float64Array
2329 ) {
2330 payload = newPayload;
2331 } else {
2332 throw (format(ERROR.INVALID_ARGUMENT, [newPayload, "newPayload"]));
2333 }
2334
2335 this._getPayloadString = function () {
2336 if (typeof payload === "string")
2337 return payload;
2338 else
2339 return parseUTF8(payload, 0, payload.length);
2340 };
2341
2342 this._getPayloadBytes = function() {
2343 if (typeof payload === "string") {
2344 var buffer = new ArrayBuffer(UTF8Length(payload));
2345 var byteStream = new Uint8Array(buffer);
2346 stringToUTF8(payload, byteStream, 0);
2347
2348 return byteStream;
2349 } else {
2350 return payload;
2351 }
2352 };
2353
2354 var destinationName;
2355 this._getDestinationName = function() { return destinationName; };
2356 this._setDestinationName = function(newDestinationName) {
2357 if (typeof newDestinationName === "string")
2358 destinationName = newDestinationName;
2359 else
2360 throw new Error(format(ERROR.INVALID_ARGUMENT, [newDestinationName, "newDestinationName"]));
2361 };
2362
2363 var qos = 0;
2364 this._getQos = function() { return qos; };
2365 this._setQos = function(newQos) {
2366 if (newQos === 0 || newQos === 1 || newQos === 2 )
2367 qos = newQos;
2368 else
2369 throw new Error("Invalid argument:"+newQos);
2370 };
2371
2372 var retained = false;
2373 this._getRetained = function() { return retained; };
2374 this._setRetained = function(newRetained) {
2375 if (typeof newRetained === "boolean")
2376 retained = newRetained;
2377 else
2378 throw new Error(format(ERROR.INVALID_ARGUMENT, [newRetained, "newRetained"]));
2379 };
2380
2381 var duplicate = false;
2382 this._getDuplicate = function() { return duplicate; };
2383 this._setDuplicate = function(newDuplicate) { duplicate = newDuplicate; };
2384 };
2385
2386 Message.prototype = {
2387 get payloadString() { return this._getPayloadString(); },
2388 get payloadBytes() { return this._getPayloadBytes(); },
2389
2390 get destinationName() { return this._getDestinationName(); },
2391 set destinationName(newDestinationName) { this._setDestinationName(newDestinationName); },
2392
2393 get topic() { return this._getDestinationName(); },
2394 set topic(newTopic) { this._setDestinationName(newTopic); },
2395
2396 get qos() { return this._getQos(); },
2397 set qos(newQos) { this._setQos(newQos); },
2398
2399 get retained() { return this._getRetained(); },
2400 set retained(newRetained) { this._setRetained(newRetained); },
2401
2402 get duplicate() { return this._getDuplicate(); },
2403 set duplicate(newDuplicate) { this._setDuplicate(newDuplicate); }
2404 };
2405
2406 // Module contents.
2407 return {
2408 Client: Client,
2409 Message: Message
2410 };
2411})(window);
2412return PahoMQTT;
2413});
2414

I. Resumen

  • En esta lección se mostró un chat sencillo.

18. Ejemplo de streaming

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8 <title>YouTube</title>
9</head>
10
11<body>
12 <h1>YouTube</h1>
13 <iframe width="560" height="315"
14 src="https://www.youtube.com/embed/eB6txyhHFG4?si=u6wVhxbxBNSVtKFA"
15 title="YouTube video player" frameborder="0"
16 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
17 referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
18</body>
19
20</html>

19. Relaciones a uno en la base de datos

Versión para imprimir.

A. Introducción

  • En esta lección se muestra el acceso a bases de datos que implementan relaciones a uno, usando servicios.

  • Se usa un select para seleccionar objetos de la base de datos.

  • La técnica aquí mostrada se usa en el lado con la llave foránea de las asocioaciones uno a uno y uno a muchos.

  • Puedes probar la app en https://replit.com/@GilbertoPachec5/srvauno?v=1. Hazle fork al proyecto y córrelo.

B. Diagrama entidad relación

Diagrama entidad relación

C. Diagrama relacional

Diagrama relacional

D. Diagrama de paquetes

  • Para este ejemplo se utilizan algunos principios de arquitecturas limpias.

  • Cada uno de los paquetes apunta con una flecha use a los que utiliza para realizar sus funciones.

  • Cada paquete oculta los detalles de su implementación y tecnología.

  • Los detalles de la base de datos, así como de su configuración, se mantienen dentro del paquete bd y no se exponen fuera de dicho paquete.

  • Los detalles de la interfaz gráfica, por ejemplo las api del navegador web, o de las interfaces en Android, se mantienen dentro del paquete access y no se exponen fuera de dicho paquete.

  • El intercambio de datos entre los paquetes access y service se realiza de acuerdo al contenido de las lecciones anteriores.

  • El intercambio de datos entre los paquetes service y bd se realiza con el contenido del paquete modelo.

Diagrama de paquetes

E. Diagrama de despliegue

Diagrama de despliegue

F. Hazlo funcionar

G. Archivos

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Relaciones a uno</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraObjeto.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14
15</head>
16
17<body onload="invocaServicio('srv/srvAmigoConsulta.php')
18 .then(render => muestraObjeto(document, render.body))
19 .catch(muestraError)">
20
21 <h1>Relaciones a uno</h1>
22
23 <p><a href="agrega.html">Agregar</a></p>
24
25 <dl id="lista">
26 <dt>Cargando…</dt>
27 <dd><progress max="100">Cargando…</progress></dd>
28 </dl>
29
30</body>
31
32</html>

I. agrega.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Agregar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraObjeto.js"></script>
14 <script type="module" src="lib/js/muestraError.js"></script>
15
16</head>
17
18<body onload="invocaServicio('srv/srvPasatiempoOptions.php')
19 .then(options => muestraObjeto(document, options.body))
20 .catch(muestraError)">
21
22 <form onsubmit="submitForm('srv/srvAmigoAgrega.php', event)
23 .then(modelo => location.href = 'index.html')
24 .catch(muestraError)">
25
26 <h1>Agregar</h1>
27
28 <p><a href="index.html">Cancelar</a></p>
29
30 <p>
31 <label>
32 Nombre *
33 <input name="nombre">
34 </label>
35 </p>
36
37 <p>
38 <label>
39 Pasatiempo
40 <select name="pasId">
41 <option value="">Cargando…</option>
42 </select>
43 </label>
44 </p>
45
46 <p>* Obligatorio</p>
47
48 <p><button type="submit">Agregar</button></p>
49
50 </form>
51
52</body>
53
54</html>

J. modifica.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Modificar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14 <script type="module" src="lib/js/muestraObjeto.js"></script>
15 <script type="module" src="lib/js/confirmaEliminar.js"></script>
16
17 <script>
18 // Obtiene los parámetros de la página.
19 const params = new URL(location.href).searchParams
20 </script>
21
22</head>
23
24<body onload="if (params.size > 0) {
25 invocaServicio('srv/srvPasatiempoOptions.php')
26 .then(async options => {
27 const modelo = await invocaServicio('srv/srvAmigoBusca.php?' + params)
28 await muestraObjeto(document, options.body)
29 await muestraObjeto(document, modelo.body)
30 })
31 .catch(muestraError)
32 }">
33
34 <form onsubmit="submitForm('srv/srvAmigoModifica.php', event)
35 .then(modelo => location.href = 'index.html')
36 .catch(muestraError)">
37
38 <h1>Modificar</h1>
39
40 <p><a href="index.html">Cancelar</a></p>
41
42 <input type="hidden" name="id">
43
44 <p>
45 <label>
46 Nombre *
47 <input name="nombre" value="Cargando…">
48 </label>
49 </p>
50
51 <p>
52 <label>
53 Pasatiempo
54 <select name="pasId">
55 <option value="">Cargando…</option>
56 </select>
57 </label>
58 </p>
59
60 <p>* Obligatorio</p>
61
62 <p>
63
64 <button type="submit">Guardar</button>
65
66 <button type="button" onclick="if (params.size > 0 && confirmaEliminar()) {
67 invocaServicio('srv/srvAmigoElimina.php?' + params)
68 .then(() => location.href = 'index.html')
69 .catch(muestraError)
70 }">
71 Eliminar
72 </button>
73
74 </p>
75
76 </form>
77
78</body>
79
80</html>

K. Carpeta « srv »

A. Carpeta « srv / modelo »

1. srv / modelo / Amigo.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/validaNombre.php";
4
5class Amigo
6{
7
8 public int $id;
9 public string $nombre;
10 public ?Pasatiempo $pasatiempo;
11
12 public function __construct(
13 string $nombre = "",
14 ?Pasatiempo $pasatiempo = null,
15 int $id = 0
16 ) {
17 $this->id = $id;
18 $this->nombre = $nombre;
19 $this->pasatiempo = $pasatiempo;
20 }
21
22 public function valida()
23 {
24 validaNombre($this->nombre);
25 }
26}
27

2. srv / modelo / Pasatiempo.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/validaNombre.php";
4
5class Pasatiempo
6{
7
8 public int $id;
9 public string $nombre;
10
11 public function __construct(string $nombre = "", int $id = 0)
12 {
13 $this->id = $id;
14 $this->nombre = $nombre;
15 }
16
17 public function valida()
18 {
19 validaNombre($this->nombre);
20 }
21}
22

B. srv / srvAmigoAgrega.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeTexto.php";
7require_once __DIR__ . "/modelo/Amigo.php";
8require_once __DIR__ . "/modelo/Pasatiempo.php";
9require_once __DIR__ . "/bd/amigoAgrega.php";
10
11ejecutaServicio(function () {
12 $nombre = leeTexto("nombre");
13 $pasId = leeEntero("pasId");
14 $pasatiempo = $pasId === null
15 ? null
16 : new Pasatiempo(id: $pasId);
17 $modelo = new Amigo(
18 nombre: $nombre === null ? "" : trim($nombre),
19 pasatiempo: $pasatiempo
20 );
21 amigoAgrega($modelo);
22 $id = htmlentities($modelo->id);
23 $pasatiempo = $modelo->pasatiempo;
24 return JsonResponse::created("/srv/srvAmigoBusca.php?id=$id", [
25 "id" => ["value" => $modelo->id],
26 "nombre" => ["value" => $modelo->nombre],
27 "pasId" => ["value" => $pasatiempo === null ? "" : $pasatiempo->id]
28 ]);
29});
30

C. srv / srvAmigoBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/amigoBusca.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 $modelo = amigoBusca($id);
13 if ($modelo === false) {
14 $htmlId = htmlentities($id);
15 throw new ProblemDetails(
16 status: ProblemDetails::NotFound,
17 type: "/error/amigonoencontrado.html",
18 title: "Amigo no encontrado.",
19 detail: "No se encontró ningún amigo con el id $htmlId.",
20 );
21 } else {
22 $pasatiempo = $modelo->pasatiempo;
23 return [
24 "id" => ["value" => $modelo->id],
25 "nombre" => ["value" => $modelo->nombre],
26 "pasId" => ["value" => $pasatiempo === null ? "" : $pasatiempo->id]
27 ];
28 }
29});
30

D. srv / srvAmigoConsulta.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/amigoConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = amigoConsulta();
8 $render = "";
9 foreach ($lista as $modelo) {
10 $amiId = htmlentities($modelo->amiId);
11 $amiNombre = htmlentities($modelo->amiNombre);
12 $pasNombre = $modelo->pasNombre === null
13 ? "<em>-- Sin pasatiempo --</em>"
14 : htmlentities($modelo->pasNombre);
15 $render .=
16 "<dt><a href='modifica.html?id=$amiId'>$amiNombre</a></dt>
17 <dd><a href='modifica.html?id=$amiId'>$pasNombre</a></dd>";
18 }
19 return ["lista" => ["innerHTML" => $render]];
20});
21

E. srv / srvAmigoElimina.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/amigoElimina.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 amigoElimina($id);
13 return JsonResponse::noContent();
14});
15

F. srv / srvAmigoModifica.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeTexto.php";
7require_once __DIR__ . "/modelo/Amigo.php";
8require_once __DIR__ . "/modelo/Pasatiempo.php";
9require_once __DIR__ . "/bd/amigoModifica.php";
10
11ejecutaServicio(function () {
12 $id = leeEntero("id");
13 if ($id === null) throw pdFaltaId();
14 $nombre = leeTexto("nombre");
15 $pasId = leeEntero("pasId");
16 $pasatiempo = $pasId === null
17 ? null
18 : new Pasatiempo(id: $pasId);
19 $modelo = new Amigo(
20 nombre: $nombre === null ? "" : trim($nombre),
21 pasatiempo: $pasatiempo,
22 id: $id
23 );
24 amigoModifica($modelo);
25 return [
26 "id" => ["value" => $modelo->id],
27 "nombre" => ["value" => $modelo->nombre],
28 "pasId" => ["value" => $pasatiempo === null ? "" : $pasatiempo->id]
29 ];
30});
31

G. srv / srvPasatiempoOptions.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/pasatiempoConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = pasatiempoConsulta();
8 $render = "<option value=''>-- Sin pasatiempo --</option>";
9 foreach ($lista as $modelo) {
10 $id = htmlentities($modelo->id);
11 $nombre = htmlentities($modelo->nombre);
12 $render .= "<option value='$id'>{$nombre}</option>";
13 }
14 return ["pasId" => ["innerHTML" => $render]];
15});
16

H. Carpeta « srv / bd »

1. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS PASATIEMPO (
7 PAS_ID INTEGER,
8 PAS_NOMBRE TEXT NOT NULL,
9 CONSTRAINT PAS_PK
10 PRIMARY KEY(PAS_ID),
11 CONSTRAINT PAS_NOM_UNQ
12 UNIQUE(PAS_NOMBRE)
13 )'
14 );
15 $con->exec(
16 'CREATE TABLE IF NOT EXISTS AMIGO (
17 AMI_ID INTEGER,
18 AMI_NOMBRE TEXT NOT NULL,
19 PAS_ID INTEGER,
20 CONSTRAINT AMI_PK
21 PRIMARY KEY(AMI_ID),
22 CONSTRAINT AMI_NOM_UNQ
23 UNIQUE(AMI_NOMBRE)
24 CONSTRAINT AMI_PAS_FK
25 FOREIGN KEY (PAS_ID) REFERENCES PASATIEMPO(PAS_ID)
26 )'
27 );
28}
29

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/bdCrea.php";
5require_once __DIR__ . "/pasatiempoConsulta.php";
6require_once __DIR__ . "/pasatiempoAgrega.php";
7
8class Bd
9{
10
11 private static ?PDO $conexion = null;
12
13 static function getConexion(): PDO
14 {
15 if (self::$conexion === null) {
16
17 self::$conexion = new PDO(
18 // cadena de conexión
19 "sqlite:srvauno.db",
20 // usuario
21 null,
22 // contraseña
23 null,
24 // Opciones: conexiones persistentes y lanza excepciones.
25 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
26 );
27
28 bdCrea(self::$conexion);
29 $pasatiempo = pasatiempoConsulta();
30 if (count($pasatiempo) === 0) {
31 $pasatiempo = new Pasatiempo(nombre: "Futbol");
32 pasatiempoAgrega($pasatiempo);
33
34 $pasatiempo = new Pasatiempo(nombre: "Videojuegos");
35 pasatiempoAgrega($pasatiempo);
36 }
37 }
38
39 return self::$conexion;
40 }
41}
42

3. srv / bd / amigoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Amigo.php";
4require_once __DIR__ . "/Bd.php";
5
6function amigoAgrega(Amigo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO AMIGO
12 (AMI_NOMBRE, PAS_ID)
13 VALUES
14 (:nombre, :pasId)"
15 );
16 $stmt->execute([
17 ":nombre" => $modelo->nombre,
18 ":pasId" => $modelo->pasatiempo === null
19 ? null
20 : $modelo->pasatiempo->id
21 ]);
22 /* Si usas una secuencia para generar el id,
23 * pasa como parámetro de lastInsertId el
24 * nombre de dicha secuencia, debes
25 * ejecutarlo antes del INSERT y pasarle el
26 * id generado al SQL. */
27 $modelo->id = $con->lastInsertId();
28 }
29

4. srv / bd / amigoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Amigo.php";
4require_once __DIR__ . "/../modelo/Pasatiempo.php";
5require_once __DIR__ . "/Bd.php";
6
7function amigoBusca(int $amiId)
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 A.AMI_ID AS amiId,
13 A.AMI_NOMBRE AS amiNombre,
14 A.PAS_ID AS pasId,
15 P.PAS_NOMBRE AS pasNombre
16 FROM AMIGO A
17 LEFT JOIN PASATIEMPO P
18 ON A.PAS_ID = P.PAS_ID
19 WHERE A.AMI_ID = :amiId"
20 );
21 $stmt->execute([":amiId" => $amiId]);
22 $stmt->setFetchMode(PDO::FETCH_OBJ);
23 $obj = $stmt->fetch();
24 if ($obj === false) {
25 return false;
26 } else {
27 $pasatiempo = $obj->pasId === null
28 ? null
29 : new Pasatiempo(nombre: $obj->pasNombre, id: $obj->pasId);
30 $amigo = new Amigo(
31 nombre: $obj->amiNombre,
32 pasatiempo: $pasatiempo,
33 id: $obj->amiId
34 );
35 return $amigo;
36 }
37}
38

5. srv / bd / amigoConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/Bd.php";
5
6function amigoConsulta()
7{
8 $con = Bd::getConexion();
9 $stmt = $con->query(
10 "SELECT
11 A.AMI_ID AS amiId,
12 A.AMI_NOMBRE AS amiNombre,
13 P.PAS_NOMBRE AS pasNombre
14 FROM AMIGO A
15 LEFT JOIN PASATIEMPO P
16 ON A.PAS_ID = P.PAS_ID
17 ORDER BY A.AMI_NOMBRE"
18 );
19 $resultado = $stmt->fetchAll(PDO::FETCH_OBJ);
20 return recibeFetchAll($resultado);
21}
22

6. srv / bd / amigoElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4
5function amigoElimina(int $id)
6{
7 $con = Bd::getConexion();
8 $stmt = $con->prepare(
9 "DELETE FROM AMIGO
10 WHERE AMI_ID = :id"
11 );
12 $stmt->execute([":id" => $id]);
13}
14

7. srv / bd / amigoModifica.php

1<?php
2
3require_once __DIR__ . "/../modelo/Amigo.php";
4require_once __DIR__ . "/Bd.php";
5
6function amigoModifica(Amigo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "UPDATE AMIGO
12 SET
13 AMI_NOMBRE = :nombre,
14 PAS_ID = :pasId
15 WHERE AMI_ID = :id"
16 );
17 $stmt->execute([
18 ":id" => $modelo->id,
19 ":nombre" => $modelo->nombre,
20 ":pasId" => $modelo->pasatiempo === null
21 ? null
22 : $modelo->pasatiempo->id
23 ]);
24}
25

8. srv / bd / pasatiempoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5
6function pasatiempoAgrega(Pasatiempo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO PASATIEMPO
12 (PAS_NOMBRE)
13 VALUES
14 (:nombre)"
15 );
16 $stmt->execute([":nombre" => $modelo->nombre]);
17 /* Si usas una secuencia para generar el id,
18 * pasa como parámetro de lastInsertId el
19 * nombre de dicha secuencia, debes
20 * ejecutarlo antes del INSERT y pasarle el
21 * id generado al SQL. */
22 $modelo->id = $con->lastInsertId();
23}
24

9. srv / bd / pasatiempoConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Pasatiempo.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Pasatiempo[] */
8function pasatiempoConsulta()
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 PAS_ID AS id,
14 PAS_NOMBRE AS nombre
15 FROM PASATIEMPO
16 ORDER BY PAS_NOMBRE"
17 );
18 $resultado = $stmt->fetchAll(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Pasatiempo::class
21 );
22 return recibeFetchAll($resultado);
23}
24

L. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / confirmaEliminar.js

1export function confirmaEliminar() {
2 return confirm("Confirma la eliminación")
3}
4
5// Permite que los eventos de html usen la función.
6window["confirmaEliminar"] = confirmaEliminar

2. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

3. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

4. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

5. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

6. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

7. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeEntero.php

1<?php
2
3require_once __DIR__ . "/leeTexto.php";
4
5/**
6 * Devuelve el valor entero de un parámetro
7 * recibido en el servidor por medio de GET,
8 * POST o cookie. Si parámetro no se puede
9 * convertir a entero, se genera un error.
10 * Si el parámetro no se recibe, devuelve
11 * null.
12 * Para que las llaves foráneas fucionen bien,
13 * si se recibe una cadena vacía, se devuelve
14 * null.
15 */
16function leeEntero(string $parametro): ?int
17{
18 $valor = leeTexto($parametro);
19 return $valor === null || $valor === ""
20 ? null
21 : trim($valor);
22}
23

4. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

5. lib / php / pdFaltaId.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function pdFaltaId()
6{
7 return new ProblemDetails(
8 status: ProblemDetails::BadRequest,
9 type: "/error/faltaid.html",
10 title: "No se ha proporcionado el valor de id.",
11 );
12}
13

6. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

7. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

8. lib / php / validaNombre.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function validaNombre(string $nombre)
6{
7 if ($nombre === "")
8 throw new ProblemDetails(
9 status: ProblemDetails::BadRequest,
10 type: "/error/faltanombre.html",
11 title: "Falta el nombre.",
12 );
13}
14

M. Carpeta « error »

A. error / amigonoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Amigo no encontrada</title>
10
11</head>
12
13<body>
14
15 <h1>Amigo no encontrada</h1>
16
17 <p>No se encontró ningún amigo con el id solicitado.</p>
18
19</body>
20
21</html>

B. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

C. error / faltaid.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el id</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el id</h1>
16
17 <p>No se ha proporcionado el valor de id.</p>
18
19</body>
20
21</html>

D. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17</body>
18
19</html>

E. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

N. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

O. Resumen

  • En esta lección se mostró el acceso a bases de datos que implementan relaciones a uno, usando servicios..

20. Relaciones a muchos - Usuarios y roles

Versión para imprimir.

A. Introducción

  • En esta lección se muestra el acceso a bases de datos que implementan relaciones a muchos, usando servicios.

  • Se usa varios checkbox para seleccionar objetos de la base de datos.

  • La técnica aquí mostrada se usa para el lado muchos de una asociación uno a muchos y en cualquiera de los lados de una asociación muchos a muchos.

  • El ejemplo es una estructura de usuarios y roles.

  • Puedes probar la app en https://replit.com/@GilbertoPachec5/srvamuchos?v=1. Hazle fork al proyecto y córrelo.

B. Diagrama entidad relación

  • Algunos navegadores web bloquean las interfaces que muestran etiquetas como usuario, id, email, etcétera, suponiendo que es un intento de engañar al usuario; para evitar este problema, al campo con el nombre de usuario se le llama cue.

Diagrama entidad relación

C. Diagrama relacional

Diagrama relacional

D. Diagrama de paquetes

  • Para este ejemplo se utilizan algunos principios de arquitecturas limpias.

  • Cada uno de los paquetes apunta con una flecha use a los que utiliza para realizar sus funciones.

  • Cada paquete oculta los detalles de su implementación y tecnología.

  • Los detalles de la base de datos, así como de su configuración, se mantienen dentro del paquete bd y no se exponen fuera de dicho paquete.

  • Los detalles de la interfaz gráfica, por ejemplo las api del navegador web, o de las interfaces en Android, se mantienen dentro del paquete access y no se exponen fuera de dicho paquete.

  • El intercambio de datos entre los paquetes access y service se realiza de acuerdo al contenido de las lecciones anteriores.

  • El intercambio de datos entre los paquetes service y bd se realiza con el contenido del paquete modelo.

Diagrama de paquetes

E. Diagrama de despliegue

Diagrama de despliegue

F. Hazlo funcionar

G. Archivos

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Relaciones a muchos</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraObjeto.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14
15</head>
16
17<body onload="invocaServicio('srv/srvUsuarioConsulta.php')
18 .then(render => muestraObjeto(document, render.body))
19 .catch(muestraError)">
20
21 <h1>Relaciones a muchos</h1>
22
23 <p><a href="agrega.html">Agregar</a></p>
24
25 <dl id="lista">
26 <dt>Cargando…</dt>
27 <dd><progress max="100">Cargando…</progress></dd>
28 </dl>
29
30</body>
31
32</html>

I. agrega.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Agregar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraObjeto.js"></script>
14 <script type="module" src="lib/js/muestraError.js"></script>
15
16</head>
17
18<body onload="invocaServicio('srv/srvRolCheckBoxes.php')
19 .then(chackBoxes => muestraObjeto(document, chackBoxes.body))
20 .catch(muestraError)">
21
22 <form onsubmit="submitForm('srv/srvUsuarioAgrega.php', event)
23 .then(modelo => location.href = 'index.html')
24 .catch(muestraError)">
25
26 <h1>Agregar</h1>
27
28 <p><a href="index.html">Cancelar</a></p>
29
30 <p>
31 <label>
32 <!-- Usamos cue para que los navegadores no bloqueen la página. -->
33 Cue *
34 <input name="cue">
35 </label>
36 </p>
37
38 <fieldset>
39
40 <legend>Roles</legend>
41
42 <div id="roles">
43 <progress max="100">Cargando…</progress>
44 </div>
45
46 </fieldset>
47
48 <p>* Obligatorio</p>
49
50 <p><button type="submit">Agregar</button></p>
51
52 </form>
53
54</body>
55
56</html>

J. modifica.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Modificar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14 <script type="module" src="lib/js/muestraObjeto.js"></script>
15 <script type="module" src="lib/js/confirmaEliminar.js"></script>
16
17 <script>
18 // Obtiene los parámetros de la página.
19 const params = new URL(location.href).searchParams
20 </script>
21
22</head>
23
24<body onload="if (params.size > 0) {
25 invocaServicio('srv/srvRolCheckBoxes.php')
26 .then(async checkBoxes => {
27 const modelo = await invocaServicio('srv/srvUsuarioBusca.php?' + params)
28 await muestraObjeto(document, checkBoxes.body)
29 await muestraObjeto(document, modelo.body)
30 })
31 .catch(muestraError)
32 }">
33
34 <form onsubmit="submitForm('srv/srvUsuarioModifica.php', event)
35 .then(modelo => location.href = 'index.html')
36 .catch(muestraError)">
37
38 <h1>Modificar</h1>
39
40 <p><a href="index.html">Cancelar</a></p>
41
42 <input type="hidden" name="id">
43
44 <p>
45 <label>
46 <!-- Usamos cue para que los navegadores no bloqueen la página. -->
47 Cue *
48 <input name="cue" value="Cargando…">
49 </label>
50 </p>
51
52 <fieldset>
53 <legend>Roles</legend>
54
55 <div id="roles">
56 <progress max="100">Cargando…</progress>
57 </div>
58
59 </fieldset>
60
61 <p>* Obligatorio</p>
62
63 <p>
64
65 <button type="submit">Guardar</button>
66
67 <button type="button" onclick="if (params.size > 0 && confirmaEliminar()) {
68 invocaServicio('srv/srvUsuarioElimina.php?' + params)
69 .then(() => location.href = 'index.html')
70 .catch(muestraError)
71 }">
72 Eliminar
73 </button>
74
75 </p>
76
77 </form>
78
79</body>
80
81</html>

K. Carpeta « srv »

A. Carpeta « srv / modelo »

1. srv / modelo / Rol.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4
5class Rol
6{
7
8 public string $id;
9 public string $descripcion;
10
11 public function __construct(string $descripcion = "", string $id = "")
12 {
13 $this->id = $id;
14 $this->descripcion = $descripcion;
15 }
16
17 public function valida()
18 {
19
20 if ($this->id === "")
21 throw new ProblemDetails(
22 status: ProblemDetails::BadRequest,
23 type: "/error/faltaid.html",
24 title: "Falta el id.",
25 );
26
27 if ($this->descripcion === "")
28 throw new ProblemDetails(
29 status: ProblemDetails::BadRequest,
30 type: "/error/faltadescripcion.html",
31 title: "Falta la descripción.",
32 );
33 }
34}
35

2. srv / modelo / Usuario.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/Rol.php";
5
6class Usuario
7{
8
9 public int $id;
10 public string $cue;
11 /** @var Rol[] */
12 public array $roles;
13
14 public function __construct(
15 string $cue = "",
16 array $roles = [],
17 int $id = 0
18 ) {
19 $this->id = $id;
20 $this->cue = $cue;
21 $this->roles = $roles;
22 }
23
24 public function valida()
25 {
26
27 if ($this->cue === "")
28 throw new ProblemDetails(
29 status: ProblemDetails::BadRequest,
30 type: "/error/faltacue.html",
31 title: "Falta el cue.",
32 );
33
34 foreach ($this->roles as $rol) {
35 if (!($rol instanceof Rol))
36 throw new ProblemDetails(
37 status: ProblemDetails::BadRequest,
38 type: "/error/rolincorrecto.html",
39 title: "Tipo incorrecto para un rol.",
40 );
41 }
42 }
43}
44

B. srv / srvRolCheckBoxes.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/rolConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = rolConsulta();
8 $render = "";
9 foreach ($lista as $modelo) {
10 $id = htmlentities($modelo->id);
11 $descripcion = htmlentities($modelo->descripcion);
12 $render .=
13 "<p>
14 <label style='display: flex'>
15 <input type='checkbox' name='rolIds[]' value='$id'>
16 <span>
17 <strong>$id</strong>
18 <br>$descripcion
19 </span>
20 </label>
21 </p>";
22 }
23 return ["roles" => ["innerHTML" => $render]];
24});
25

C. srv / srvUsuarioAgrega.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/leeTexto.php";
6require_once __DIR__ . "/../lib/php/leeArray.php";
7require_once __DIR__ . "/modelo/Rol.php";
8require_once __DIR__ . "/modelo/Usuario.php";
9require_once __DIR__ . "/bd/usuarioAgrega.php";
10
11ejecutaServicio(function () {
12 $cue = leeTexto("cue");
13 $rolIds = leeArray("rolIds");
14 /** @var Rol[] $roles */
15 $roles = [];
16 if ($rolIds !== null) {
17 foreach ($rolIds as $rolId) {
18 $roles[] = new Rol(id: $rolId);
19 }
20 }
21 $modelo = new Usuario(cue: $cue === null ? "" : trim($cue), roles: $roles);
22 usuarioAgrega($modelo);
23 $id = htmlentities($modelo->id);
24 return JsonResponse::created("/srv/srvUsuarioBusca.php?id=$id", $modelo);
25});
26

D. srv / srvUsuarioBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/usuarioBusca.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 $modelo = usuarioBusca($id);
13 if ($modelo === false) {
14 $htmlId = htmlentities($id);
15 throw new ProblemDetails(
16 status: ProblemDetails::NotFound,
17 type: "/error/usuarionoencontrado.html",
18 title: "Usuario no encontrado.",
19 detail: "No se encontró ningún usuario con el id $htmlId.",
20 );
21 }
22 $rolIds = [];
23 foreach ($modelo->roles as $rol) {
24 $rolIds[] = $rol->id;
25 }
26 return [
27 "id" => ["value" => $modelo->id],
28 "cue" => ["value" => $modelo->cue],
29 "rolIds[]" => $rolIds
30 ];
31});
32

E. srv / srvUsuarioConsulta.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/usuarioConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = usuarioConsulta();
8 $render = "";
9 foreach ($lista as $modelo) {
10 $usuId = htmlentities($modelo->usuId);
11 $usuCue = htmlentities($modelo->usuCue);
12 $roles = $modelo->roles === null || $modelo->roles === ""
13 ? "<em>-- Sin roles --</em>"
14 : htmlentities($modelo->roles);
15 $render .=
16 "<dt><a href='modifica.html?id=$usuId'>$usuCue</a></dt>
17 <dd><a href='modifica.html?id=$usuId'>$roles</a></dd>";
18 }
19 return ["lista" => ["innerHTML" => $render]];
20});
21

F. srv / srvUsuarioElimina.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/usuarioElimina.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 usuarioElimina($id);
13 return JsonResponse::noContent();
14});
15

G. srv / srvUsuarioModifica.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeTexto.php";
7require_once __DIR__ . "/../lib/php/leeArray.php";
8require_once __DIR__ . "/modelo/Usuario.php";
9require_once __DIR__ . "/bd/usuarioModifica.php";
10
11ejecutaServicio(function () {
12 $id = leeEntero("id");
13 if ($id === null) throw pdFaltaId();
14 $cue = leeTexto("cue");
15 $rolIds = leeArray("rolIds");
16 /** @var Rol[] $roles */
17 $roles = [];
18 if ($rolIds !== null) {
19 foreach ($rolIds as $rolId) {
20 $roles[] = new Rol(id: $rolId);
21 }
22 }
23 $usuario = new Usuario(
24 cue: $cue === null ? "" : trim($cue),
25 roles: $roles,
26 id: $id
27 );
28 usuarioModifica($usuario);
29 return $usuario;
30});
31

H. Carpeta « srv / bd »

1. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS USUARIO (
7 USU_ID INTEGER,
8 USU_CUE TEXT NOT NULL,
9 CONSTRAINT USU_PK
10 PRIMARY KEY(USU_ID),
11 CONSTRAINT USU_CUE_UNQ
12 UNIQUE(USU_CUE)
13 )'
14 );
15 $con->exec(
16 'CREATE TABLE IF NOT EXISTS ROL (
17 ROL_ID TEXT NOT NULL,
18 ROL_DESCRIPCION TEXT NOT NULL,
19 CONSTRAINT ROL_PK
20 PRIMARY KEY(ROL_ID),
21 CONSTRAINT ROL_DESCR_UNQ
22 UNIQUE(ROL_DESCRIPCION)
23 )'
24 );
25 $con->exec(
26 'CREATE TABLE IF NOT EXISTS USU_ROL (
27 USU_ID INTEGER NOT NULL,
28 ROL_ID TEXT NOT NULL,
29 CONSTRAINT USU_ROL_PK
30 PRIMARY KEY(USU_ID, ROL_ID),
31 CONSTRAINT USU_ROL_USU_FK
32 FOREIGN KEY (USU_ID) REFERENCES USUARIO(USU_ID),
33 CONSTRAINT USU_ROL_ROL_FK
34 FOREIGN KEY (ROL_ID) REFERENCES ROL(ROL_ID)
35 )'
36 );
37}
38

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/../modelo/Rol.php";
4require_once __DIR__ . "/bdCrea.php";
5require_once __DIR__ . "/rolBusca.php";
6require_once __DIR__ . "/rolAgrega.php";
7
8class Bd
9{
10
11 private static ?PDO $conexion = null;
12
13 static function getConexion(): PDO
14 {
15 if (self::$conexion === null) {
16
17 self::$conexion = new PDO(
18 // cadena de conexión
19 "sqlite:srvamuchos.db",
20 // usuario
21 null,
22 // contraseña
23 null,
24 // Opciones: conexiones persistentes y lanza excepciones.
25 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
26 );
27
28 bdCrea(self::$conexion);
29
30 if (rolBusca("Administrador") === false) {
31 $administrador = new Rol(
32 id: "Administrador",
33 descripcion: "Administra el sistema."
34 );
35 rolAgrega($administrador);
36 }
37
38 if (rolBusca("Cliente") === false) {
39 $cliente = new Rol(
40 id: "Cliente",
41 descripcion: "Realiza compras."
42 );
43 rolAgrega($cliente);
44 }
45 }
46
47 return self::$conexion;
48 }
49}
50

3. srv / bd / rolAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Rol.php";
4require_once __DIR__ . "/Bd.php";
5
6function rolAgrega(Rol $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO ROL
12 (ROL_ID, ROL_DESCRIPCION)
13 VALUES
14 (:id, :descripcion)"
15 );
16 $stmt->execute([
17 ":id" => $modelo->id,
18 ":descripcion" => $modelo->descripcion
19 ]);
20}
21

4. srv / bd / rolBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Rol.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolConsulta.php";
6
7function rolBusca(string $id) : false|Rol
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 ROL_ID as id,
13 ROL_DESCRIPCION as descripcion
14 FROM ROL
15 WHERE ROL_ID = :id"
16 );
17 $stmt->execute([":id" => $id]);
18 $stmt->setFetchMode(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Rol::class
21 );
22 return $stmt->fetch();
23}
24

5. srv / bd / rolConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Rol.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Rol[] */
8function rolConsulta()
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 ROL_ID as id,
14 ROL_DESCRIPCION as descripcion
15 FROM ROL
16 ORDER BY ROL_ID"
17 );
18 $resultado = $stmt->fetchAll(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Rol::class
21 );
22 return recibeFetchAll($resultado);
23}
24

6. srv / bd / usuarioAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolAgrega.php";
6
7function usuarioAgrega(Usuario $modelo)
8{
9 $modelo->valida();
10 $con = Bd::getConexion();
11 $con->beginTransaction();
12 $stmt = $con->prepare(
13 "INSERT INTO USUARIO
14 (USU_CUE)
15 VALUES
16 (:cue)"
17 );
18 $stmt->execute([
19 ":cue" => $modelo->cue,
20 ]);
21 /* Si usas una secuencia para generar el id,
22 * pasa como parámetro de lastInsertId el
23 * nombre de dicha secuencia, debes
24 * ejecutarlo antes del INSERT y pasarle el
25 * id generado al SQL. */
26 $modelo->id = $con->lastInsertId();
27 usuRolAgrega($modelo);
28 $con->commit();
29}
30

7. srv / bd / usuarioBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolConsulta.php";
6
7function usuarioBusca(int $usuId)
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 USU_ID as id,
13 USU_CUE as cue
14 FROM USUARIO
15 WHERE USU_ID = :usuId"
16 );
17 $stmt->execute([":usuId" => $usuId]);
18 $stmt->setFetchMode(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Usuario::class
21 );
22 /** @var false|Usuario */
23 $usuario = $stmt->fetch();
24 if ($usuario === false) {
25 return false;
26 } else {
27 $usuario->roles = usuRolConsulta($usuId);
28 return $usuario;
29 }
30}
31

8. srv / bd / usuarioConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/Bd.php";
5
6function usuarioConsulta()
7{
8 $con = Bd::getConexion();
9 $stmt = $con->query(
10 "SELECT
11 U.USU_ID AS usuId,
12 U.USU_CUE AS usuCue,
13 GROUP_CONCAT(R.ROL_ID, ', ') AS roles
14 FROM USUARIO U
15 LEFT JOIN USU_ROL UR
16 ON U.USU_ID = UR.USU_ID
17 LEFT JOIN ROL R
18 ON UR.ROL_ID = R.ROL_ID
19 GROUP BY U.USU_CUE
20 ORDER BY U.USU_CUE"
21 );
22 $resultado = $stmt->fetchAll(PDO::FETCH_OBJ);
23 return recibeFetchAll($resultado);
24}
25

9. srv / bd / usuarioElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4require_once __DIR__ . "/usuRolElimina.php";
5
6function usuarioElimina(int $id)
7{
8 $con = Bd::getConexion();
9 $con->beginTransaction();
10 usuRolElimina($id);
11 $stmt = $con->prepare(
12 "DELETE FROM USUARIO
13 WHERE USU_ID = :id"
14 );
15 $stmt->execute([":id" => $id]);
16 $con->commit();
17}
18

10. srv / bd / usuarioModifica.php

1<?php
2
3 require_once __DIR__ . "/../modelo/Usuario.php";
4 require_once __DIR__ . "/Bd.php";
5 require_once __DIR__ . "/usuRolAgrega.php";
6 require_once __DIR__ . "/usuRolElimina.php";
7
8function usuarioModifica(Usuario $modelo) {
9 $modelo->valida();
10 $con = Bd::getConexion();
11 $con->beginTransaction();
12 $stmt = $con->prepare(
13 "UPDATE USUARIO
14 SET USU_CUE = :cue
15 WHERE USU_ID = :id"
16 );
17 $stmt->execute([
18 ":id" => $modelo->id,
19 ":cue" => $modelo->cue
20 ]);
21 usuRolElimina($modelo->id);
22 usuRolAgrega($modelo);
23 $con->commit();
24}
25

11. srv / bd / usuRolAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5
6function usuRolAgrega(Usuario $usuario) {
7 $roles = $usuario->roles;
8 if (sizeof($roles) > 0) {
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO USU_ROL
12 (USU_ID, ROL_ID)
13 VALUES
14 (:usuId, :rolId)"
15 );
16 foreach ($roles as $rol) {
17 $stmt->execute(
18 [
19 ":usuId" => $usuario->id,
20 ":rolId" => $rol->id
21 ]
22 );
23 }
24 }
25}
26

12. srv / bd / usuRolConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Rol.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Rol[] */
8function usuRolConsulta(int $usuId)
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 UR.ROL_ID AS id,
14 R.ROL_DESCRIPCION AS descripcion
15 FROM USU_ROL UR, ROL R
16 WHERE
17 UR.ROL_ID = R.ROL_ID
18 AND UR.USU_ID = :usuId
19 ORDER BY UR.ROL_ID"
20 );
21 $stmt->execute([":usuId" => $usuId]);
22 $resultado = $stmt->fetchAll(
23 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
24 Rol::class
25 );
26 return recibeFetchAll($resultado);
27}
28

13. srv / bd / usuRolElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4
5function usuRolElimina(int $usuId)
6{
7 $con = Bd::getConexion();
8 $stmt = $con->prepare(
9 "DELETE FROM USU_ROL
10 WHERE USU_ID = :usuId"
11 );
12 $stmt->execute([":usuId" => $usuId]);
13}
14

L. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / confirmaEliminar.js

1export function confirmaEliminar() {
2 return confirm("Confirma la eliminación")
3}
4
5// Permite que los eventos de html usen la función.
6window["confirmaEliminar"] = confirmaEliminar

2. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

3. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

4. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

5. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

6. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

7. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeArray.php

1<?php
2
3/**
4 * Devuelve los valores asociados a un
5 * parámetro multivaluado; por ejemplo, un
6 * grupo de checkbox, recibido en el servidor
7 * por medio de GET, POST o cookie. Si no se
8 * recibe el parámetro, devuelve null. Si el
9 * valor recibido no es un arreglo, lo coloca
10 * dentro de uno.
11 */
12function leeArray(string $parametro)
13{
14 if (isset($_REQUEST[$parametro])) {
15 $valor = $_REQUEST[$parametro];
16 return is_array($valor)
17 ? $valor
18 : [$valor];
19 } else {
20 return null;
21 }
22}
23

4. lib / php / leeEntero.php

1<?php
2
3require_once __DIR__ . "/leeTexto.php";
4
5/**
6 * Devuelve el valor entero de un parámetro
7 * recibido en el servidor por medio de GET,
8 * POST o cookie. Si parámetro no se puede
9 * convertir a entero, se genera un error.
10 * Si el parámetro no se recibe, devuelve
11 * null.
12 * Para que las llaves foráneas fucionen bien,
13 * si se recibe una cadena vacía, se devuelve
14 * null.
15 */
16function leeEntero(string $parametro): ?int
17{
18 $valor = leeTexto($parametro);
19 return $valor === null || $valor === ""
20 ? null
21 : trim($valor);
22}
23

5. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

6. lib / php / pdFaltaId.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function pdFaltaId()
6{
7 return new ProblemDetails(
8 status: ProblemDetails::BadRequest,
9 type: "/error/faltaid.html",
10 title: "No se ha proporcionado el valor de id.",
11 );
12}
13

7. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

8. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

M. Carpeta « error »

A. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

B. error / faltacue.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el cue</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el cue</h1>
16
17</body>
18
19</html>

C. error / faltadescripcion.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta la descripción</title>
10
11</head>
12
13<body>
14
15 <h1>Falta la descripción</h1>
16
17</body>
18
19</html>

D. error / faltaid.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el id</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el id</h1>
16
17 <p>No se ha proporcionado el valor de id.</p>
18
19</body>
20
21</html>

E. error / idincorrecto.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Valor de id incorrecto</title>
10
11</head>
12
13<body>
14
15 <h1>Valor de id incorrecto</h1>
16
17 <p>
18 O no se ha proporcionado el valor para id, o el valor proporcionado no es un
19 número entero.
20 </p>
21
22</body>
23
24</html>

F. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

G. error / rolincorrecto.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Tipo incorrecto para un rol</title>
10
11</head>
12
13<body>
14
15 <h1>Tipo incorrecto para un rol</h1>
16
17</body>
18
19</html>

H. error / usuarionoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Usuario no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Usuario no encontrado</h1>
16
17 <p>No se encontró ninguna usuario con el id solicitado.</p>
18
19</body>
20
21</html>

N. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

O. Resumen

  • En esta lección se mostró el acceso a bases de datos que implementan relaciones a muchos, usando servicios. El ejemplo también implementa una estructura de usuarios y roles.

21. Autenticación

Versión para imprimir.

A. Introducción

  • En esta lección se amplía el ejemplo de la lección anterior para controlar el acceso a una app.

  • Puedes probar la app en https://replit.com/@GilbertoPachec5/srvaut?v=1. Hazle fork al proyecto y córrelo.

  • Prueba con los siguientes usarios y contraseñas:

    pepito
    cuentos
    susana
    alegria
    bebe
    saurio
  • Este ejmplo se puede complementar usando reCaptcha..

  • Este ejemplo usa sesión en el navegador, que es un archivo en el servidor que permite guardar la información del usuario. Este mecanismo está cayendo en desuso, porque limita la veocidad del servidor y lo expone a fallas de seguridad.

  • Hoy en día se prefiere usar servidores de OAuth 2 para autenticar usuarios. Los principales servidores para este tipo de autenticación son Google, Facebook y Microsoft.

B. Diagrama entidad relación

  • Algunos navegadores web bloquean las interfaces que muestran etiquetas como contraseña, usuario, id, email, etcétera, suponiendo que es un intento de engañar al usuario; para evitar este problema, al campo con el nombre de usuario se le llama cue y a la conreaseña match.

Diagrama entidad relación

C. Diagrama relacional

Diagrama relacional

D. Diagrama de paquetes

  • Para este ejemplo se utilizan algunos principios de arquitecturas limpias.

  • Cada uno de los paquetes apunta con una flecha use a los que utiliza para realizar sus funciones.

  • Cada paquete oculta los detalles de su implementación y tecnología.

  • Los detalles de la base de datos, así como de su configuración, se mantienen dentro del paquete bd y no se exponen fuera de dicho paquete.

  • Los detalles de la interfaz gráfica, por ejemplo las api del navegador web, o de las interfaces en Android, se mantienen dentro del paquete access y no se exponen fuera de dicho paquete.

  • El intercambio de datos entre los paquetes access y service se realiza de acuerdo al contenido de las lecciones anteriores.

  • El intercambio de datos entre los paquetes service y bd se realiza con el contenido del paquete modelo.

Diagrama de paquetes

E. Diagrama de despliegue

Diagrama de despliegue

F. Hazlo funcionar

G. Archivos

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Autenticación</title>
10
11 <script type="module" src="lib/js/muestraError.js"></script>
12 <script type="module" src="./js/protege.js"></script>
13 <script type="module" src="./js/custom/mi-nav.js"></script>
14
15</head>
16
17<body onload="protege('srv/srvSesion.php')
18 .then(sesion => nav.sesion = sesion)
19 .catch(muestraError)">
20
21 <mi-nav id="nav"></mi-nav>
22
23 <h1>Autenticación</h1>
24
25 <p>Bienvenid@.</p>
26
27</body>
28
29</html>

I. perfil.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Perfil</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13 <script type="module" src="./js/protege.js"></script>
14 <script type="module" src="./js/custom/mi-nav.js"></script>
15
16</head>
17
18<body onload="protege('srv/srvSesion.php')
19 .then(sesion => {
20 nav.sesion = sesion
21 const cue = sesion.cue
22 if (cue === '') {
23 login.hidden = false
24 outputCue.value = 'No ha iniciado sesión.'
25 outputRoles.value = ''
26 } else {
27 logout.hidden = false
28 outputCue.value = cue
29 const rolIds = sesion.rolIds
30 outputRoles.value = rolIds.size === 0
31 ? 'Sin roles'
32 : Array.from(rolIds).join(', ')
33 }
34 })
35 .catch(muestraError)">
36
37 <mi-nav id="nav"></mi-nav>
38
39 <h1>Perfil</h1>
40
41 <p>
42 <output id="outputCue">
43 <progress max="100">Cargando…</progress>
44 </output>
45 </p>
46
47 <p>
48 <output id="outputRoles">
49 <progress max="100">Cargando…</progress>
50 </output>
51 </p>
52
53 <p>
54
55 <a id="login" hidden href="login.html">Iniciar sesión</a>
56
57 <button type="button" id="logout" hidden
58 onclick="invocaServicio('srv/srvLogout.php')
59 .then(json => location.reload())
60 .catch(muestraError)">
61 Terminar sesión
62 </button>
63
64 </p>
65
66</body>
67
68</html>

J. login.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Iniciar sesión</title>
10
11 <script type="module" src="lib/js/submitForm.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13 <script type="module" src="./js/protege.js"></script>
14
15</head>
16
17<body onload="protege('srv/srvSesion.php')
18 .then(sesion => {
19 if (sesion.cue !== '') {
20 location.href = 'perfil.html'
21 }
22 })
23 .catch(muestraError)">
24
25 <form id="login" onsubmit="submitForm('srv/srvLogin.php', event)
26 .then(sesion => location.href = 'perfil.html')
27 .catch(muestraError)">
28
29 <h1>Iniciar Sesión</h1>
30
31 <p>
32 <label>
33 Cue
34 <input name="cue">
35 </label>
36 </p>
37
38 <p>
39 <label>
40 Match
41 <input type="password" name="match">
42 </label>
43 </p>
44
45 <p>
46 <a href="perfil.html">Cancelar</a>
47 <button type="submit">Iniciar sesión</button>
48 </p>
49
50 </form>
51
52</body>
53
54</html>

K. admin.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Solo Administradores</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13 <script type="module" src="./js/const/ROL_ADMINISTRADOR.js"></script>
14 <script type="module" src="./js/protege.js"></script>
15 <script type="module" src="./js/custom/mi-nav.js"></script>
16
17</head>
18
19<body onload="protege('srv/srvSesion.php', [ROL_ADMINISTRADOR], 'index.html')
20 .then(sesion => {
21 nav.sesion = sesion
22 main.hidden = false
23 })
24 .catch(muestraError)">
25
26 <mi-nav id="nav"></mi-nav>
27
28 <main id="main" hidden>
29
30 <h1>Solo Administradores</h1>
31
32 <p>Hola.</p>
33
34 <p>
35 <button type="button" onclick="invocaServicio('srv/srvSaludoCliente.php')
36 .then(saludo => alert(saludo.body))
37 .catch(muestraError)">
38 Ejecuta servicio
39 </button>
40 </p>
41
42 </main>
43
44</body>
45
46</html>

L. cliente.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Solo Clientes</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13 <script type="module" src="./js/const/ROL_CLIENTE.js"></script>
14 <script type="module" src="./js/protege.js"></script>
15 <script type="module" src="./js/custom/mi-nav.js"></script>
16
17</head>
18
19<body onload="protege('srv/srvSesion.php', [ROL_CLIENTE], 'index.html')
20 .then(sesion => {
21 nav.sesion = sesion
22 main.hidden = false
23 })
24 .catch(muestraError)">
25
26 <mi-nav id="nav"></mi-nav>
27
28 <main id="main" hidden>
29
30 <h1>Solo Clientes</h1>
31
32 <p>Hola.</p>
33
34 <p>
35 <button type="button" onclick="invocaServicio('srv/srvSaludoCliente.php')
36 .then(saludo => alert(saludo.body))
37 .catch(muestraError)">
38 Ejecuta servicio
39 </button>
40 </p>
41
42 </main>
43
44</body>
45
46</html>

M. Carpeta « js »

A. Carpeta « js / const »

1. js / const / CUE.js

1export const CUE = "cue"

2. js / const / ROL_ADMINISTRADOR.js

1export const ROL_ADMINISTRADOR = "Administrador"
2
3// Permite que los eventos de html usen la constante.
4window["ROL_ADMINISTRADOR"] = ROL_ADMINISTRADOR

3. js / const / ROL_CLIENTE.js

1export const ROL_CLIENTE = "Cliente"
2
3// Permite que los eventos de html usen la constante.
4window["ROL_CLIENTE"] = ROL_CLIENTE

4. js / const / ROL_IDS.js

1export const ROL_IDS = "rolIds"

B. js / Sesion.js

1import { CUE } from "./const/CUE.js"
2import { ROL_IDS } from "./const/ROL_IDS.js"
3
4export class Sesion {
5
6 /**
7 * @param { any } objeto
8 */
9 constructor(objeto) {
10
11 /** @readonly */
12 this.cue = objeto[CUE]
13 if (typeof this.cue !== "string")
14 throw new Error("cue debe ser string.")
15
16 /** @readonly */
17 const rolIds = objeto[ROL_IDS]
18 if (!Array.isArray(rolIds))
19 throw new Error("rolIds debe ser arreglo.")
20 /** @readonly */
21 this.rolIds = new Set(rolIds)
22
23 }
24
25}
26
27// Permite que los eventos de html usen la clase.
28window["Sesion"] = Sesion

C. js / protege.js

1import { invocaServicio } from "../lib/js/invocaServicio.js"
2import { Sesion } from "./Sesion.js"
3
4/**
5 * @param {string} servicio
6 * @param {string[]} [rolIdsPermitidos]
7 * @param {string} [urlDeProtección]
8 */
9export async function protege(servicio, rolIdsPermitidos, urlDeProtección) {
10 const respuesta = await invocaServicio('srv/srvSesion.php')
11 const sesion = new Sesion(respuesta.body)
12 if (rolIdsPermitidos === undefined) {
13 return sesion
14 } else {
15 const rolIds = sesion.rolIds
16 for (const rolId of rolIdsPermitidos) {
17 if (rolIds.has(rolId)) {
18 return sesion
19 }
20 }
21 if (urlDeProtección !== undefined) {
22 location.href = urlDeProtección
23 }
24 throw new Error("No autorizado.")
25 }
26}
27
28// Permite que los eventos de html usen la función.
29window["protege"] = protege

D. Carpeta « js / custom »

1. js / custom / mi-nav.js

1import { htmlentities } from "../../lib/js/htmlentities.js"
2import { Sesion } from "../Sesion.js"
3import { ROL_ADMINISTRADOR } from "../const/ROL_ADMINISTRADOR.js"
4import { ROL_CLIENTE } from "../const/ROL_CLIENTE.js"
5
6export class MiNav extends HTMLElement {
7
8 connectedCallback() {
9
10 this.style.display = "block"
11
12 this.innerHTML = /* html */
13 `<nav>
14 <ul style="display: flex;
15 flex-wrap: wrap;
16 padding:0;
17 gap: 0.5em;
18 list-style-type: none">
19 <li><progress max="100">Cargando…</progress></li>
20 </ul>
21 </nav>`
22
23 }
24
25 /**
26 * @param {Sesion} sesion
27 */
28 set sesion(sesion) {
29 const cue = sesion.cue
30 const rolIds = sesion.rolIds
31 let innerHTML = /* html */ `<li><a href="index.html">Inicio</a></li>`
32 innerHTML += this.hipervinculosAdmin(rolIds)
33 innerHTML += this.hipervinculosCliente(rolIds)
34 const cueHtml = htmlentities(cue)
35 if (cue !== "") {
36 innerHTML += /* html */ `<li>${cueHtml}</li>`
37 }
38 innerHTML += /* html */ `<li><a href="perfil.html">Perfil</a></li>`
39 const ul = this.querySelector("ul")
40 if (ul !== null) {
41 ul.innerHTML = innerHTML
42 }
43 }
44
45 /**
46 * @param {Set<string>} rolIds
47 */
48 hipervinculosAdmin(rolIds) {
49 return rolIds.has(ROL_ADMINISTRADOR) ?
50 /* html */ `<li><a href="admin.html">Para administradores</a></li>`
51 : ""
52 }
53
54 /**
55 * @param {Set<string>} rolIds
56 */
57 hipervinculosCliente(rolIds) {
58 return rolIds.has(ROL_CLIENTE) ?
59 /* html */ `<li><a href="cliente.html">Para clientes</a></li>`
60 : ""
61 }
62}
63
64customElements.define("mi-nav", MiNav)

N. Carpeta « srv »

A. Carpeta « srv / const »

1. srv / const / CUE.php

1<?php
2
3const CUE = "cue";

2. srv / const / ROL_ADMINISTRADOR.php

1<?php
2
3const ROL_ADMINISTRADOR = "Administrador";
4

3. srv / const / ROL_CLIENTE.php

1<?php
2
3const ROL_CLIENTE = "Cliente";

4. srv / const / ROL_IDS.php

1<?php
2
3const ROL_IDS = "rolIds";

B. Carpeta « srv / modelo »

1. srv / modelo / Rol.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4
5class Rol
6{
7
8 public string $id;
9 public string $descripcion;
10
11 public function __construct(string $descripcion = "", string $id = "")
12 {
13 $this->id = $id;
14 $this->descripcion = $descripcion;
15 }
16
17 public function valida()
18 {
19
20 if ($this->id === "")
21 throw new ProblemDetails(
22 status: ProblemDetails::BadRequest,
23 type: "/error/faltaid.html",
24 title: "Falta el id.",
25 );
26
27 if ($this->descripcion === "")
28 throw new ProblemDetails(
29 status: ProblemDetails::BadRequest,
30 type: "/error/faltadescripcion.html",
31 title: "Falta la descripción.",
32 );
33 }
34}
35

2. srv / modelo / Usuario.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/Rol.php";
5
6class Usuario
7{
8
9 public int $id;
10 public string $cue;
11 public string $match;
12 /** @var Rol[] */
13 public array $roles;
14
15 public function __construct(
16 string $cue = "",
17 string $match = "",
18 array $roles = [],
19 int $id = 0
20 ) {
21 $this->id = $id;
22 $this->cue = $cue;
23 $this->match = $match;
24 $this->roles = $roles;
25 }
26
27 public function valida()
28 {
29
30 if ($this->cue === "")
31 throw new ProblemDetails(
32 status: ProblemDetails::BadRequest,
33 type: "/error/faltacue.html",
34 title: "Falta el cue.",
35 );
36
37 if ($this->match === "")
38 throw new ProblemDetails(
39 status: ProblemDetails::BadRequest,
40 type: "/error/faltamatch.html",
41 title: "Falta el match.",
42 );
43
44 foreach ($this->roles as $rol) {
45 if (!($rol instanceof Rol))
46 throw new ProblemDetails(
47 status: ProblemDetails::BadRequest,
48 type: "/error/rolincorrecto.html",
49 title: "Tipo incorrecto para un rol.",
50 );
51 }
52 }
53}
54

C. srv / protege.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/const/CUE.php";
6require_once __DIR__ . "/const/ROL_IDS.php";
7require_once __DIR__ . "/const/ROL_CLIENTE.php";
8require_once __DIR__ . "/Sesion.php";
9
10const NO_AUTORIZADO = 401;
11
12function protege(?array $rolIdsPermitidos = null)
13{
14 session_start();
15 $cue = isset($_SESSION[CUE])
16 ? $_SESSION[CUE]
17 : "";
18 $rolIds = isset($_SESSION[ROL_IDS])
19 ? $_SESSION[ROL_IDS]
20 : [];
21 $sesion = new Sesion($cue, $rolIds);
22 if ($rolIdsPermitidos === null) {
23 return $sesion;
24 } else {
25 foreach ($rolIdsPermitidos as $rolId) {
26 if (array_search($rolId, $rolIds, true) !== false) {
27 return $sesion;
28 }
29 }
30 throw new ProblemDetails(
31 status: NO_AUTORIZADO,
32 type: "/error/noautorizado.html",
33 title: "No autorizado.",
34 detail: "No está autorizado para usar este recurso.",
35 );
36 }
37}
38

D. srv / Sesion.php

1<?php
2
3class Sesion
4{
5
6 public string $cue;
7 public array $rolIds;
8
9 public function __construct(string $cue, array $rolIds)
10 {
11 $this->cue = $cue;
12 $this->rolIds = $rolIds;
13 }
14}
15

E. srv / srvLogin.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/leeTexto.php";
6require_once __DIR__ . "/const/CUE.php";
7require_once __DIR__ . "/const/ROL_IDS.php";
8require_once __DIR__ . "/modelo/Rol.php";
9require_once __DIR__ . "/bd/usuarioVerifica.php";
10require_once __DIR__ . "/protege.php";
11
12ejecutaServicio(function () {
13 $sesion = protege();
14 if ($sesion->cue !== "") {
15 throw new ProblemDetails(
16 status: NO_AUTORIZADO,
17 type: "/error/sesioniniciada.html",
18 title: "Sesión iniciada.",
19 detail: "La sesión ya está iniciada.",
20 );
21 }
22 $cue = leeTexto("cue");
23 $match = leeTexto("match");
24 if ($cue === null || $cue === "")
25 throw new ProblemDetails(
26 status: ProblemDetails::BadRequest,
27 type: "/error/faltacue.html",
28 title: "Falta el cue.",
29 );
30
31 if ($match === null || $match === "")
32 throw new ProblemDetails(
33 status: ProblemDetails::BadRequest,
34 type: "/error/faltamatch.html",
35 title: "Falta el match.",
36 );
37
38 $usuario = usuarioVerifica(trim($cue), trim($match));
39 if ($usuario === false) {
40 throw new ProblemDetails(
41 status: ProblemDetails::BadRequest,
42 type: "/error/datosincorrectos.html",
43 title: "Datos incorrectos.",
44 detail: "El cue y/o el match proporcionados son incorrectos.",
45 );
46 } else {
47 $rolIds = [];
48 foreach ($usuario->roles as $rol) {
49 $rolIds[] = $rol->id;
50 }
51 $_SESSION[CUE] = $cue;
52 $_SESSION[ROL_IDS] = $rolIds;
53 return [
54 CUE => $cue,
55 ROL_IDS => $rolIds
56 ];
57 }
58});
59

F. srv / srvLogout.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/const/CUE.php";
5require_once __DIR__ . "/const/ROL_IDS.php";
6
7ejecutaServicio(function () {
8 session_start();
9 if (isset($_SESSION[CUE])) {
10 unset($_SESSION[CUE]);
11 }
12 if (isset($_SESSION[ROL_IDS])) {
13 unset($_SESSION[ROL_IDS]);
14 }
15 session_destroy();
16 return [];
17});
18

G. srv / srvSaludoCliente.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/const/ROL_CLIENTE.php";
5require_once __DIR__ . "/protege.php";
6
7ejecutaServicio(function () {
8 $sesion = protege([ROL_CLIENTE]);
9 return "Hola " . $sesion->cue;
10});
11

H. srv / srvSesion.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/protege.php";
5
6ejecutaServicio(function () {
7 return protege();
8});
9

I. Carpeta « srv / bd »

1. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS USUARIO (
7 USU_ID INTEGER,
8 USU_CUE TEXT NOT NULL,
9 USU_MATCH TEXT NOT NULL,
10 CONSTRAINT USU_PK
11 PRIMARY KEY(USU_ID),
12 CONSTRAINT USU_CUE_UNQ
13 UNIQUE(USU_CUE)
14 )'
15 );
16 $con->exec(
17 'CREATE TABLE IF NOT EXISTS ROL (
18 ROL_ID TEXT,
19 ROL_DESCRIPCION TEXT NOT NULL,
20 CONSTRAINT ROL_PK
21 PRIMARY KEY(ROL_ID),
22 CONSTRAINT ROL_DESCR_UNQ
23 UNIQUE(ROL_DESCRIPCION)
24 )'
25 );
26 $con->exec(
27 'CREATE TABLE IF NOT EXISTS USU_ROL (
28 USU_ID INTEGER NOT NULL,
29 ROL_ID TEXT NOT NULL,
30 CONSTRAINT USU_ROL_PK
31 PRIMARY KEY(USU_ID, ROL_ID),
32 CONSTRAINT USU_ROL_USU_FK
33 FOREIGN KEY (USU_ID) REFERENCES USUARIO(USU_ID),
34 CONSTRAINT USU_ROL_ROL_FK
35 FOREIGN KEY (ROL_ID) REFERENCES ROL(ROL_ID)
36 )'
37 );
38}
39

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/../const/ROL_CLIENTE.php";
4require_once __DIR__ . "/../const/ROL_ADMINISTRADOR.php";
5require_once __DIR__ . "/../modelo/Rol.php";
6require_once __DIR__ . "/../modelo/Usuario.php";
7require_once __DIR__ . "/bdCrea.php";
8require_once __DIR__ . "/usuarioBuscaCue.php";
9require_once __DIR__ . "/usuarioAgrega.php";
10require_once __DIR__ . "/rolConsulta.php";
11require_once __DIR__ . "/rolAgrega.php";
12require_once __DIR__ . "/rolBusca.php";
13
14class Bd
15{
16
17 private static ?PDO $conexion = null;
18
19 static function getConexion(): PDO
20 {
21 if (self::$conexion === null) {
22
23 self::$conexion = new PDO(
24 // cadena de conexión
25 "sqlite:srvaut.db",
26 // usuario
27 null,
28 // contraseña
29 null,
30 // Opciones: conexiones persistentes y lanza excepciones.
31 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
32 );
33
34 bdCrea(self::$conexion);
35
36 if (rolBusca(ROL_ADMINISTRADOR) === false) {
37 $administrador = new Rol(
38 id: ROL_ADMINISTRADOR,
39 descripcion: "Administra el sistema."
40 );
41 rolAgrega($administrador);
42 }
43
44 if (rolBusca("Cliente") === false) {
45 $cliente = new Rol(
46 id: "Cliente",
47 descripcion: "Realiza compras."
48 );
49 rolAgrega($cliente);
50 }
51
52 if (usuarioBuscaCue("pepito") === false) {
53 $usuario = new Usuario(
54 cue: "pepito",
55 match: "cuentos",
56 roles: [$cliente]
57 );
58 usuarioAgrega($usuario);
59 }
60
61 if (usuarioBuscaCue("susana") === false) {
62 $usuario = new Usuario(
63 cue: "susana",
64 match: "alegria",
65 roles: [$administrador]
66 );
67 usuarioAgrega($usuario);
68 }
69
70 if (usuarioBuscaCue("bebe") === false) {
71 $usuario = new Usuario(
72 cue: "bebe",
73 match: "saurio",
74 roles: [$administrador, $cliente]
75 );
76 usuarioAgrega($usuario);
77 }
78 }
79
80 return self::$conexion;
81 }
82}
83

3. srv / bd / rolAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Rol.php";
4require_once __DIR__ . "/Bd.php";
5
6function rolAgrega(Rol $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO ROL
12 (ROL_ID, ROL_DESCRIPCION)
13 VALUES
14 (:id, :descripcion)"
15 );
16 $stmt->execute([
17 ":id" => $modelo->id,
18 ":descripcion" => $modelo->descripcion
19 ]);
20}
21

4. srv / bd / rolBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Rol.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolConsulta.php";
6
7function rolBusca(string $id) : false|Rol
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 ROL_ID as id,
13 ROL_DESCRIPCION as descripcion
14 FROM ROL
15 WHERE ROL_ID = :id"
16 );
17 $stmt->execute([":id" => $id]);
18 $stmt->setFetchMode(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Rol::class
21 );
22 return $stmt->fetch();
23}
24

5. srv / bd / rolConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Rol.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Rol[] */
8function rolConsulta()
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 ROL_ID as id,
14 ROL_DESCRIPCION as descripcion
15 FROM ROL
16 ORDER BY ROL_ID"
17 );
18 $resultado = $stmt->fetchAll(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Rol::class
21 );
22 return recibeFetchAll($resultado);
23}
24

6. srv / bd / usuarioAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolAgrega.php";
6
7function usuarioAgrega(Usuario $modelo)
8{
9 $modelo->valida();
10 $con = Bd::getConexion();
11 $con->beginTransaction();
12 $stmt = $con->prepare(
13 "INSERT INTO USUARIO
14 (USU_CUE, USU_MATCH)
15 VALUES
16 (:cue, :match)"
17 );
18 $stmt->execute([
19 ":cue" => $modelo->cue,
20 ":match" => password_hash($modelo->match, PASSWORD_DEFAULT)
21 ]);
22 /* Si usas una secuencia para generar el id,
23 * pasa como parámetro de lastInsertId el
24 * nombre de dicha secuencia. */
25 $modelo->id = $con->lastInsertId();
26 usuRolAgrega($modelo);
27 $con->commit();
28}
29

7. srv / bd / usuarioBuscaCue.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolConsulta.php";
6
7function usuarioBuscaCue(string $cue)
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 USU_ID as id,
13 USU_CUE as cue,
14 USU_MATCH as match
15 FROM USUARIO
16 WHERE USU_CUE = :cue"
17 );
18 $stmt->execute([":cue" => $cue]);
19 $stmt->setFetchMode(
20 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
21 Usuario::class
22 );
23 /** @var false|Usuario */
24 $usuario = $stmt->fetch();
25 if ($usuario === false) {
26 return false;
27 } else {
28 $usuario->roles = usuRolConsulta($usuario->id);
29 return $usuario;
30 }
31}
32

8. srv / bd / usuarioVerifica.php

1<?php
2
3require_once __DIR__ . "/usuarioBuscaCue.php";
4
5function usuarioVerifica(string $cue, string $match)
6{
7 $usuario = usuarioBuscaCue($cue);
8 if ($usuario !== false && password_verify($match, $usuario->match)) {
9 return $usuario;
10 } else {
11 return false;
12 }
13}
14

9. srv / bd / usuRolAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5
6function usuRolAgrega(Usuario $usuario) {
7 $roles = $usuario->roles;
8 if (sizeof($roles) > 0) {
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO USU_ROL
12 (USU_ID, ROL_ID)
13 VALUES
14 (:usuId, :rolId)"
15 );
16 foreach ($roles as $rol) {
17 $stmt->execute(
18 [
19 ":usuId" => $usuario->id,
20 ":rolId" => $rol->id
21 ]
22 );
23 }
24 }
25}
26

10. srv / bd / usuRolConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Rol.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Rol[] */
8function usuRolConsulta(int $usuId)
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 UR.ROL_ID AS id,
14 R.ROL_DESCRIPCION AS descripcion
15 FROM USU_ROL UR, ROL R
16 WHERE
17 UR.ROL_ID = R.ROL_ID
18 AND UR.USU_ID = :usuId
19 ORDER BY UR.ROL_ID"
20 );
21 $stmt->execute([":usuId" => $usuId]);
22 $resultado = $stmt->fetchAll(
23 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
24 Rol::class
25 );
26 return recibeFetchAll($resultado);
27}
28

O. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / htmlentities.js

1/**
2 * Codifica un texto para que cambie los caracteres
3 * especiales y no se pueda interpretar como
4 * etiiqueta HTML. Esta técnica evita la inyección
5 * de código.
6 * @param { string } texto
7 * @returns { string } un texto que no puede
8 * interpretarse como HTML. */
9export function htmlentities(texto) {
10 return texto.replace(/[<>"']/g, textoDetectado => {
11 switch (textoDetectado) {
12 case "<": return "<"
13 case ">": return ">"
14 case '"': return "&quot;"
15 case "'": return "&#039;"
16 default: return textoDetectado
17 }
18 })
19}
20// Permite que los eventos de html usen la función.
21window["htmlentities"] = htmlentities

2. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

3. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

4. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

5. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

6. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

4. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

5. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

P. Carpeta « error »

A. error / datosincorrectos.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Datos incorrectos</title>
10
11</head>
12
13<body>
14
15 <h1>Datos incorrectos</h1>
16
17 <p>El cue y/o el match proporcionados son incorrectos.</p>
18</body>
19
20</html>

B. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

C. error / faltacue.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el cue</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el cue</h1>
16
17</body>
18
19</html>

D. error / faltadescripcion.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta la descripción</title>
10
11</head>
12
13<body>
14
15 <h1>Falta la descripción</h1>
16
17</body>
18
19</html>

E. error / faltaid.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el id</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el id</h1>
16
17 <p>No se ha proporcionado el valor de id.</p>
18
19</body>
20
21</html>

F. error / faltamatch.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el match</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el match</h1>
16
17</body>
18
19</html>

G. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17</body>
18
19</html>

H. error / noautorizado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>No autorizado</title>
10
11</head>
12
13<body>
14
15 <h1>No autorizado</h1>
16
17 <p>No está autorizado para usar este recurso.</p>
18
19</body>
20
21</html>

I. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

J. error / pasatiemponoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Pasatiempo no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Pasatiempo no encontrado</h1>
16
17 <p>No se encontró ningún pasatiempo con el id solicitado.</p>
18
19</body>
20
21</html>

K. error / rolincorrecto.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Tipo incorrecto para un rol</title>
10
11</head>
12
13<body>
14
15 <h1>Tipo incorrecto para un rol</h1>
16
17</body>
18
19</html>

L. error / sesioniniciada.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Sesión iniciada</title>
10
11</head>
12
13<body>
14
15 <h1>Sesión iniciada</h1>
16
17 <p>La sesión ya está iniciada.</p>
18
19</body>
20
21</html>

Q. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

R. Resumen

  • En esta lección se amplió el ejemplo de la lección anterior para controlar el acceso a una app.

22. Archivos en la base de datos

Versión para imprimir.

A. Introducción

  • En esta lección se muestra el almacenamiento de un archivo por registro de la base de datos, usando servicios.

  • Puedes probar la app en https://replit.com/@GilbertoPachec5/srvarchivos?v=1. Hazle fork al proyecto y córrelo.

  • Este ejemplo toma como base la lección de asociaciones a uno, pero en caso de necesitar varios archivos por producto, puedes tomar como base la lección de asociaciones a muchos.

B. Diagrama entidad relación

Diagrama entidad relación

C. Diagrama relacional

Diagrama relacional

D. Diagrama de paquetes

  • Para este ejemplo se utilizan algunos principios de arquitecturas limpias.

  • Cada uno de los paquetes apunta con una flecha use a los que utiliza para realizar sus funciones.

  • Cada paquete oculta los detalles de su implementación y tecnología.

  • Los detalles de la base de datos, así como de su configuración, se mantienen dentro del paquete bd y no se exponen fuera de dicho paquete.

  • Los detalles de la interfaz gráfica, por ejemplo las api del navegador web, o de las interfaces en Android, se mantienen dentro del paquete access y no se exponen fuera de dicho paquete.

  • El intercambio de datos entre los paquetes access y service se realiza de acuerdo al contenido de las lecciones anteriores.

  • El intercambio de datos entre los paquetes service y bd se realiza con el contenido del paquete modelo.

Diagrama de paquetes

E. Diagrama de despliegue

Diagrama de despliegue

F. Hazlo funcionar

G. Archivos

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Archivos</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraObjeto.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14
15</head>
16
17<body onload="invocaServicio(fetch('srv/srvProductoConsulta.php'))
18 .then(render => muestraObjeto(document, render.body))
19 .catch(muestraError)">
20
21 <h1>Archivos</h1>
22
23 <p><a href="agrega.html">Agregar</a></p>
24
25 <dl id="lista">
26 <dt>Cargando…</dt>
27 <dd><progress max="100">Cargando…</progress></dd>
28 </dl>
29
30</body>
31
32</html>

I. agrega.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Agregar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14 <script type="module" src="lib/js/imagenNuevaSeleccionada.js"></script>
15
16</head>
17
18<body onload="imagenNuevaSeleccionada(forma.bytes, imagen).catch(muestraError)">
19
20 <form id="forma" onsubmit="submitForm('srv/srvProductoAgrega.php', event)
21 .then(modelo => location.href = 'index.html')
22 .catch(muestraError)">
23
24 <h1>Agregar</h1>
25
26 <p><a href="index.html">Cancelar</a></p>
27
28 <p>
29 <label>
30 Nombre *
31 <input name="nombre">
32 </label>
33 </p>
34
35 <p>
36 <label>
37 Imagen *
38 <input name="bytes" type="file" accept="image/*"
39 oninput="imagenNuevaSeleccionada(this, imagen).catch(muestraError)">
40 </label>
41 </p>
42
43 <p>* Obligatorio</p>
44
45 <p><button type="submit">Agregar</button></p>
46
47 <figure>
48 <img id="imagen" hidden alt="Imagen del producto" style="max-width: 100%;">
49 </figure>
50
51 </form>
52
53</body>
54
55</html>

J. modifica.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Modificar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14 <script type="module" src="lib/js/muestraObjeto.js"></script>
15 <script type="module" src="lib/js/confirmaEliminar.js"></script>
16 <script type="module" src="lib/js/imagenSeleccionada.js"></script>
17
18 <script>
19 // Obtiene los parámetros de la página.
20 const params = new URL(location.href).searchParams
21 </script>
22
23</head>
24
25<body onload="if (params.size > 0) {
26 invocaServicio('srv/srvProductoBusca.php?' + params)
27 .then(modelo => muestraObjeto(document, modelo.body))
28 .catch(muestraError)
29 }">
30
31 <form onsubmit="submitForm('srv/srvProductoModifica.php', event)
32 .then(modelo => location.href = 'index.html')
33 .catch(muestraError)">
34
35 <h1>Modificar</h1>
36
37 <p><a href="index.html">Cancelar</a></p>
38
39 <input type="hidden" name="id">
40
41 <p>
42 <label>
43 Nombre *
44 <input name="nombre" value="Cargando…">
45 </label>
46 </p>
47
48 <p>
49 <label>
50 Imagen
51 <input name="bytes" type="file" accept="image/*"
52 oninput="imagenSeleccionada(this, imagen).catch(muestraError)">
53 </label>
54 </p>
55
56 <p>* Obligatorio</p>
57
58 <p>
59
60 <button type="submit">Guardar</button>
61
62 <button type="button" onclick="if (params.size > 0 && confirmaEliminar()) {
63 invocaServicio('srv/srvProductoElimina.php?' + params)
64 .then(() => location.href = 'index.html')
65 .catch(muestraError)
66 }">
67 Eliminar
68 </button>
69
70 </p>
71
72 <figure>
73 <img id="imagen" hidden alt="Imagen del producto" style="max-width: 100%;"
74 data-input="bytes">
75 </figure>
76
77 </form>
78
79</body>
80
81</html>

K. Carpeta « srv »

A. Carpeta « srv / modelo »

1. srv / modelo / Archivo.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4
5class Archivo
6{
7
8 public int $id;
9 public string $bytes;
10
11 public function __construct(string $bytes = "", int $id = 0)
12 {
13 $this->id = $id;
14 $this->bytes = $bytes;
15 }
16
17 public function valida()
18 {
19 if ($this->bytes === "")
20 throw new ProblemDetails(
21 status: ProblemDetails::BadRequest,
22 type: "/error/archivovacio.html",
23 title: "Archivo vacío.",
24 detail: "Selecciona un archivo que no esté vacío."
25 );
26 }
27}
28

2. srv / modelo / Producto.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/validaNombre.php";
4require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/Archivo.php";
6
7class Producto
8{
9
10 public int $id;
11 public string $nombre;
12 public ?Archivo $archivo;
13
14 public function __construct(
15 string $nombre = "",
16 Archivo $archivo = null,
17 int $id = 0,
18 ) {
19 $this->id = $id;
20 $this->nombre = $nombre;
21 $this->archivo = $archivo;
22 }
23
24 public function validaNuevo()
25 {
26 validaNombre($this->nombre);
27 if ($this->archivo === null)
28 throw new ProblemDetails(
29 status: ProblemDetails::BadRequest,
30 type: "/error/faltaarchivo.html",
31 title: "Falta el archivo.",
32 detail: "Selecciona un archivo que no esté vacío."
33 );
34 $this->archivo->valida();
35 }
36
37 public function valida()
38 {
39 validaNombre($this->nombre);
40 }
41}
42

B. srv / srvArchivo.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/ProblemDetails.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/archivoBusca.php";
8
9mb_internal_encoding("UTF-8");
10try {
11 // Evita que la imagen se cargue en el caché de la computadora.
12 header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
13 header("Cache-Control: post-check=0, pre-check=0", false);
14 header("Pragma: no-cache");
15 $id = leeEntero("id");
16 if ($id === null) throw pdFaltaId();
17 $archivo = archivoBusca($id);
18 if ($archivo === false) {
19 throw new ProblemDetails(
20 status: ProblemDetails::BadRequest,
21 type: "/error/archivonoencontrado.html",
22 title: "Archivo no encontrado.",
23 detail: "No se encontró ningún archivo con el id solicitado."
24 );
25 }
26 echo $archivo->bytes;
27} catch (ProblemDetails $details) {
28 procesa_problem_details($details);
29} catch (Throwable $throwable) {
30 procesa_problem_details(new ProblemDetails(
31 status: ProblemDetails::InternalServerError,
32 type: "/error/errorinterno.html",
33 title: "Error interno del servidor.",
34 detail: $throwable->getMessage()
35 ));
36}
37

C. srv / srvProductoAgrega.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/leeBytes.php";
6require_once __DIR__ . "/../lib/php/leeTexto.php";
7require_once __DIR__ . "/modelo/Archivo.php";
8require_once __DIR__ . "/modelo/Producto.php";
9require_once __DIR__ . "/bd/productoAgrega.php";
10
11ejecutaServicio(function () {
12 $bytes = leeBytes("bytes");
13 $archivo = $bytes === "" ? null : new Archivo(bytes: $bytes);
14
15 $nombre = leeTexto("nombre");
16 $modelo = new Producto(
17 nombre: $nombre === null ? "" : trim($nombre),
18 archivo: $archivo
19 );
20
21 productoAgrega($modelo);
22
23 $id = htmlentities($modelo->id);
24 // Los bytes se descargan con SrvArchivo; no desde aquí.
25 return JsonResponse::created("/srv/srvProductoBusca.php?id=$id", [
26 "id" => ["value" => $modelo->id],
27 "nombre" => ["value" => $modelo->nombre],
28 "imagen" => [
29 "src" => $archivo === null
30 ? ""
31 : "srv/srvArchivo.php?id=" . $archivo->id,
32 ]
33 ]);
34});
35

D. srv / srvProductoBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/ProblemDetails.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/productoBusca.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 $modelo = productoBusca($id);
13 if ($modelo === false) {
14 $htmlId = htmlentities($id);
15 throw new ProblemDetails(
16 status: ProblemDetails::NotFound,
17 type: "/error/productonoencontrado.html",
18 title: "Producto no encontrado.",
19 detail: "No se encontró ningún producto con el id $htmlId.",
20 );
21 } else {
22 $archivo = $modelo->archivo;
23 return [
24 "id" => ["value" => $modelo->id],
25 "nombre" => ["value" => $modelo->nombre],
26 "imagen" => [
27 "src" => $archivo === null
28 ? ""
29 : "srv/srvArchivo.php?id=" . $archivo->id
30 ]
31 ];
32 }
33});
34

E. srv / srvProductoConsulta.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/productoConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = productoConsulta();
8 $render = "";
9 foreach ($lista as $modelo) {
10 $prodId = htmlentities($modelo->prodId);
11 $prodNombre = htmlentities($modelo->prodNombre);
12 $archId = $modelo->archId === null
13 ? ""
14 : htmlentities($modelo->archId);
15 $render .=
16 "<div style='display: flex; flex-direction: row-reverse;
17 align-items: center; gap: 0.5rem'>
18 <dt style='flex: 1 1 0'>
19 <a href='modifica.html?id=$prodId'>$prodNombre</a>
20 </dt>
21 <dd style='flex: 1 1 0; margin: 0'>
22 <a href='modifica.html?id=$prodId'><img style='width: 100%'
23 alt='Imagen del producto' src='srv/srvArchivo.php?id=$archId'></a>
24 </dd>
25 </div>";
26 }
27 return ["lista" => ["innerHTML" => $render]];
28});
29

F. srv / srvProductoElimina.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/productoElimina.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 productoElimina($id);
13 return JsonResponse::noContent();
14});
15

G. srv / srvProductoModifica.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeBytes.php";
7require_once __DIR__ . "/../lib/php/leeTexto.php";
8require_once __DIR__ . "/modelo/Archivo.php";
9require_once __DIR__ . "/modelo/Producto.php";
10require_once __DIR__ . "/bd/productoModifica.php";
11
12ejecutaServicio(function () {
13 $id = leeEntero("id");
14 if ($id === null) throw pdFaltaId();
15 $bytes = leeBytes("bytes");
16 $archivo = $bytes === "" ? null : new Archivo(bytes: $bytes);
17
18 $nombre = leeTexto("nombre");
19 $modelo = new Producto(
20 $nombre === null ? "" : trim($nombre),
21 archivo: $archivo,
22 id: $id
23 );
24
25 productoModifica($modelo);
26
27 // Los bytes se descargan con SrvArchivo; no desde aquí.
28 $archivo = $modelo->archivo;
29 return [
30 "id" => ["value" => $modelo->id],
31 "nombre" => ["value" => $modelo->nombre],
32 "imagen" => [
33 "src" => $archivo === null
34 ? ""
35 : "srv/srvArchivo.php?id=" . $archivo->id,
36 ]
37 ];
38});
39

H. Carpeta « srv / bd »

1. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS ARCHIVO (
7 ARCH_ID INTEGER,
8 ARCH_BYTES BLOB NOT NULL,
9 CONSTRAINT ARCH_PK
10 PRIMARY KEY(ARCH_ID)
11 )'
12 );
13 $con->exec(
14 'CREATE TABLE IF NOT EXISTS PRODUCTO (
15 PROD_ID INTEGER,
16 PROD_NOMBRE TEXT NOT NULL,
17 ARCH_ID INTEGER NOT NULL,
18 CONSTRAINT PROD_PK
19 PRIMARY KEY(PROD_ID),
20 CONSTRAINT PROD_NOM_UNQ
21 UNIQUE(PROD_NOMBRE)
22 CONSTRAINT PROD_ARCH_FK
23 FOREIGN KEY (ARCH_ID) REFERENCES ARCHIVO(ARCH_ID)
24 )'
25 );
26}
27

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/bdCrea.php";
4
5class Bd
6{
7
8 private static ?PDO $conexion = null;
9
10 static function getConexion(): PDO
11 {
12 if (self::$conexion === null) {
13
14 self::$conexion = new PDO(
15 // cadena de conexión
16 "sqlite:srvarchivos.db",
17 // usuario
18 null,
19 // contraseña
20 null,
21 // Opciones: conexiones persistentes y lanza excepciones.
22 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
23 );
24
25 bdCrea(self::$conexion);
26 }
27
28 return self::$conexion;
29 }
30}
31

3. srv / bd / archivoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Archivo.php";
4require_once __DIR__ . "/Bd.php";
5
6function archivoAgrega(Archivo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO ARCHIVO
12 (ARCH_BYTES)
13 VALUES
14 (:bytes)"
15 );
16 $stmt->execute([
17 ":bytes" => $modelo->bytes
18 ]);
19 /* Si usas una secuencia para generar el id,
20 * pasa como parámetro de lastInsertId el
21 * nombre de dicha secuencia, debes
22 * ejecutarlo antes del INSERT y pasarle el
23 * id generado al SQL. */
24 $modelo->id = $con->lastInsertId();
25}
26

4. srv / bd / archivoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Archivo.php";
4require_once __DIR__ . "/Bd.php";
5
6function archivoBusca(int $id): false|Archivo
7{
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "SELECT
11 ARCH_ID AS id,
12 ARCH_BYTES AS bytes
13 FROM ARCHIVO
14 WHERE ARCH_ID = :id"
15 );
16 $stmt->execute([":id" => $id]);
17 $stmt->setFetchMode(
18 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
19 Archivo::class
20 );
21 return $stmt->fetch();
22}
23

5. srv / bd / archivoElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4
5function archivoElimina(int $id)
6{
7 $con = Bd::getConexion();
8 $stmt = $con->prepare(
9 "DELETE FROM ARCHIVO
10 WHERE ARCH_ID = :id"
11 );
12 $stmt->execute([":id" => $id]);
13}
14

6. srv / bd / archivoModifica.php

1<?php
2
3require_once __DIR__ . "/../modelo/Archivo.php";
4require_once __DIR__ . "/Bd.php";
5
6function archivoModifica(Archivo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "UPDATE ARCHIVO
12 SET ARCH_BYTES = :bytes
13 WHERE ARCH_ID = :id"
14 );
15 $stmt->execute([
16 ":bytes" => $modelo->bytes,
17 ":id" => $modelo->id
18 ]);
19}
20

7. srv / bd / productoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Archivo.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/archivoAgrega.php";
6
7function productoAgrega(Producto $modelo)
8{
9 $modelo->validaNuevo();
10 $con = Bd::getConexion();
11 $con->beginTransaction();
12 archivoAgrega($modelo->archivo);
13 $stmt = $con->prepare(
14 "INSERT INTO PRODUCTO
15 (PROD_NOMBRE, ARCH_ID)
16 VALUES
17 (:nombre, :archId)"
18 );
19 $stmt->execute([
20 ":nombre" => $modelo->nombre,
21 ":archId" => $modelo->archivo->id
22 ]);
23 /* Si usas una secuencia para generar el id,
24 * pasa como parámetro de lastInsertId el
25 * nombre de dicha secuencia, debes
26 * ejecutarlo antes del INSERT y pasarle el
27 * id generado al SQL. */
28 $modelo->id = $con->lastInsertId();
29 $con->commit();
30}
31

8. srv / bd / productoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Archivo.php";
4require_once __DIR__ . "/../modelo/Producto.php";
5require_once __DIR__ . "/Bd.php";
6
7function productoBusca(int $prodId)
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 P.PROD_ID AS prodId,
13 P.PROD_NOMBRE AS prodNombre,
14 A.ARCH_ID AS archId
15 FROM PRODUCTO P
16 LEFT JOIN ARCHIVO A
17 ON P.ARCH_ID = A.ARCH_ID
18 WHERE P.PROD_ID = :prodId"
19 );
20 $stmt->execute([
21 ":prodId" => $prodId
22 ]);
23 $stmt->setFetchMode(PDO::FETCH_OBJ);
24 $obj = $stmt->fetch();
25 if ($obj === false) {
26 return false;
27 } else {
28 $id = $obj->prodId;
29 $nombre = $obj->prodNombre;
30 $archId = $obj->archId;
31 $archivo = $archId === null ? null : new Archivo(id: $archId);
32 $producto = new Producto(
33 id: $id,
34 nombre: $nombre,
35 archivo: $archivo
36 );
37 return $producto;
38 }
39}
40

9. srv / bd / productoConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Producto.php";
5require_once __DIR__ . "/Bd.php";
6
7function productoConsulta()
8{
9 $con = Bd::getConexion();
10 $stmt = $con->query(
11 "SELECT
12 P.PROD_ID AS prodId,
13 P.PROD_NOMBRE AS prodNombre,
14 A.ARCH_ID AS archId
15 FROM PRODUCTO P
16 LEFT JOIN ARCHIVO A
17 ON P.ARCH_ID = A.ARCH_ID
18 ORDER BY P.PROD_NOMBRE"
19 );
20 $resultado = $stmt->fetchAll(PDO::FETCH_OBJ);
21 return recibeFetchAll($resultado);
22}
23

10. srv / bd / productoElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4require_once __DIR__ . "/archivoElimina.php";
5require_once __DIR__ . "/productoBusca.php";
6
7function productoElimina(int $id)
8{
9 $con = Bd::getConexion();
10 $con->beginTransaction();
11 $modelo = productoBusca($id);
12 if ($modelo === false) {
13 $con->rollBack();
14 } else {
15 archivoElimina($modelo->archivo->id);
16 $stmt = $con->prepare(
17 "DELETE FROM PRODUCTO
18 WHERE PROD_ID = :id"
19 );
20 $stmt->execute([":id" => $modelo->id]);
21 $con->commit();
22 }
23}
24

11. srv / bd / productoModifica.php

1<?php
2
3require_once __DIR__ . "/../modelo/Producto.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/productoBusca.php";
6require_once __DIR__ . "/archivoModifica.php";
7
8function productoModifica(Producto $modelo)
9{
10 $modelo->valida();
11 $con = Bd::getConexion();
12 $con->beginTransaction();
13 $archivo = $modelo->archivo;
14 $anterior = productoBusca($modelo->id);
15 if ($anterior === false) {
16 throw new Exception("Producto no encontrado.");
17 }
18 if ($anterior->archivo === null) {
19 throw new Exception("Falta el archivo anterior.");
20 }
21 if ($archivo === null) {
22 $archivo = $anterior->archivo;
23 $modelo->archivo = $archivo;
24 } else {
25 $archivo->id = $anterior->archivo->id;
26 archivoModifica($archivo);
27 }
28 $stmt = $con->prepare(
29 "UPDATE PRODUCTO
30 SET
31 PROD_NOMBRE = :nombre,
32 ARCH_ID = :archId
33 WHERE PROD_ID = :id"
34 );
35 $stmt->execute([
36 ":id" => $modelo->id,
37 ":nombre" => $modelo->nombre,
38 ":archId" => $archivo->id
39 ]);
40 $con->commit();
41}
42

L. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / confirmaEliminar.js

1export function confirmaEliminar() {
2 return confirm("Confirma la eliminación")
3}
4
5// Permite que los eventos de html usen la función.
6window["confirmaEliminar"] = confirmaEliminar

2. lib / js / imagenNuevaSeleccionada.js

1import { getDataUrlDeSeleccion } from "./muestraObjeto.js"
2
3/**
4 * @param {HTMLInputElement} input
5 * @param {HTMLImageElement} img
6 */
7export function imagenNuevaSeleccionada(input, img) {
8 return new Promise((resolve, reject) => {
9 setTimeout(async () => {
10 try {
11 const dataUrl = await getDataUrlDeSeleccion(input)
12 if (dataUrl === "") {
13 img.hidden = true
14 img.src = ""
15 } else {
16 img.hidden = false
17 img.src = dataUrl
18 }
19 resolve(true)
20 } catch (error) {
21 img.hidden = true
22 reject(error)
23 }
24 },
25 500)
26 })
27}
28
29// Permite que los eventos de html usen la función.
30window["imagenNuevaSeleccionada"] = imagenNuevaSeleccionada

3. lib / js / imagenSeleccionada.js

1import { getDataUrlDeSeleccion } from "./muestraObjeto.js"
2
3/**
4 * @param {HTMLInputElement} input
5 * @param {HTMLImageElement} img
6 */
7export async function imagenSeleccionada(input, img) {
8 try {
9 const dataUrl = await getDataUrlDeSeleccion(input)
10 if (dataUrl === '') {
11 const imagenInicial = img.dataset.inicial
12 if (imagenInicial === undefined || imagenInicial === '') {
13 img.hidden = true
14 img.src = ""
15 } else {
16 img.hidden = false
17 img.src = imagenInicial
18 }
19 } else {
20 img.hidden = false
21 img.src = dataUrl
22 }
23 } catch (error) {
24 img.hidden = true
25 throw error
26 }
27}
28
29// Permite que los eventos de html usen la función.
30window["imagenSeleccionada"] = imagenSeleccionada

4. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

5. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

6. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

7. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

8. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

9. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeBytes.php

1<?php
2
3function leeBytes(string $parametro): string
4{
5 /* Si el archivo se recibió y contiene bytes, los recupera;
6 * en caso contrario, devuelve false. */
7 $contents = isset($_FILES[$parametro]) && $_FILES[$parametro]["size"] > 0
8 ? file_get_contents($_FILES[$parametro]["tmp_name"])
9 : false;
10 return $contents === false
11 ? ""
12 : $contents;
13}
14

4. lib / php / leeEntero.php

1<?php
2
3require_once __DIR__ . "/leeTexto.php";
4
5/**
6 * Devuelve el valor entero de un parámetro
7 * recibido en el servidor por medio de GET,
8 * POST o cookie. Si parámetro no se puede
9 * convertir a entero, se genera un error.
10 * Si el parámetro no se recibe, devuelve
11 * null.
12 * Para que las llaves foráneas fucionen bien,
13 * si se recibe una cadena vacía, se devuelve
14 * null.
15 */
16function leeEntero(string $parametro): ?int
17{
18 $valor = leeTexto($parametro);
19 return $valor === null || $valor === ""
20 ? null
21 : trim($valor);
22}
23

5. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

6. lib / php / pdFaltaId.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function pdFaltaId()
6{
7 return new ProblemDetails(
8 status: ProblemDetails::BadRequest,
9 type: "/error/faltaid.html",
10 title: "No se ha proporcionado el valor de id.",
11 );
12}
13

7. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

8. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

9. lib / php / validaNombre.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function validaNombre(string $nombre)
6{
7 if ($nombre === "")
8 throw new ProblemDetails(
9 status: ProblemDetails::BadRequest,
10 type: "/error/faltanombre.html",
11 title: "Falta el nombre.",
12 );
13}
14

M. Carpeta « error »

A. error / amigonoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Amigo no encontrada</title>
10
11</head>
12
13<body>
14
15 <h1>Amigo no encontrada</h1>
16
17 <p>No se encontró ningún amigo con el id solicitado.</p>
18
19</body>
20
21</html>

B. error / archivovacio.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Archivo vacío</title>
10
11</head>
12
13<body>
14
15 <h1>Archivo vacío</h1>
16
17 <p>Selecciona un archivo que no esté vacío.</p>
18
19</body>
20
21</html>

C. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

D. error / faltaarchivo.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el archivo</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el archivo</h1>
16
17 <p>Selecciona un archivo que no esté vacío.</p>
18
19</body>
20
21</html>

E. error / faltaid.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el id</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el id</h1>
16
17 <p>No se ha proporcionado el valor de id.</p>
18
19</body>
20
21</html>

F. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17</body>
18
19</html>

G. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

H. error / productonoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Producto no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Producto no encontrado</h1>
16
17 <p>No se encontró ningún producto con el id solicitado.</p>
18
19</body>
20
21</html>

N. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

O. Resumen

  • En esta lección se mostró el almacenamiento de un archivo por registro de la base de datos, usando servicios.

23. Compras sencillas

Versión para imprimir.

A. Introducción

  • En esta lección se muestra la base de una aplicación que vende o compra.

  • Para poder manejar compras en línea, es necesario que la venta incluya la referencia al cliente.

  • Puedes probar la app en https://replit.com/@GilbertoPachec5/srvcompras?v=1. Hazle fork al proyecto y córrelo.

B. Diagrama entidad relación

Diagrama entidad relación

C. Diagrama relacional

Diagrama relacional

D. Diagrama de paquetes

  • Para este ejemplo se utilizan algunos principios de arquitecturas limpias.

  • Cada uno de los paquetes apunta con una flecha use a los que utiliza para realizar sus funciones.

  • Cada paquete oculta los detalles de su implementación y tecnología.

  • Los detalles de la base de datos, así como de su configuración, se mantienen dentro del paquete bd y no se exponen fuera de dicho paquete.

  • Los detalles de la interfaz gráfica, por ejemplo las api del navegador web, o de las interfaces en Android, se mantienen dentro del paquete access y no se exponen fuera de dicho paquete.

  • El intercambio de datos entre los paquetes access y service se realiza de acuerdo al contenido de las lecciones anteriores.

  • El intercambio de datos entre los paquetes service y bd se realiza con el contenido del paquete modelo.

Diagrama de paquetes

E. Diagrama de despliegue

Diagrama de despliegue

F. Hazlo funcionar

G. Archivos

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Productos</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraObjeto.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14
15</head>
16
17<body onload="invocaServicio('srv/srvProductoConsulta.php')
18 .then(render => muestraObjeto(document, render.body))
19 .catch(muestraError)">
20
21 <h1>Productos</h1>
22
23 <p><a href="carrito.html">Ver carrito</a></p>
24
25 <dl id="lista">
26 <dt>Cargando…</dt>
27 <dd><progress max="100">Cargando…</progress></dd>
28 </dl>
29
30</body>
31
32</html>

I. agrega.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Agregar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraObjeto.js"></script>
14 <script type="module" src="lib/js/muestraError.js"></script>
15
16 <script>
17 // Obtiene los parámetros de la página.
18 const params = new URL(location.href).searchParams
19 </script>
20
21</head>
22
23<body onload="if (params.size > 0) {
24 invocaServicio('srv/srvProductoBusca.php?' + params)
25 .then(producto => muestraObjeto(document, producto.body))
26 .catch(muestraError)
27 }">
28
29 <form onsubmit="submitForm('srv/srvDetalleDeVentaAgrega.php', event)
30 .then(modelo => location.href = 'index.html')
31 .catch(muestraError)">
32
33 <h1>Agregar</h1>
34
35 <p><a href="index.html">Cancelar</a></p>
36
37 <input type="hidden" name="id">
38
39 <p>
40 <label>
41 Producto
42 <output name="producto">
43 <progress max="100">Cargando…</progress>
44 </output>
45 </label>
46 </p>
47
48 <p>
49 <label>
50 Precio
51 <output name="precio">
52 <progress max="100">Cargando…</progress>
53 </output>
54 </label>
55 </p>
56
57 <p>
58 <label>
59 Cantidad *
60 <input name="cantidad" type="number" min="0" step="0.01">
61 </label>
62 </p>
63
64 <p>* Obligatorio</p>
65
66 <p><button type="submit">Agregar</button></p>
67
68 </form>
69
70</body>
71
72</html>

J. carrito.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Carrito</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13 <script type="module" src="lib/js/muestraObjeto.js"></script>
14
15</head>
16
17<body onload="invocaServicio('srv/srvVentaEnCapturaBusca.php')
18 .then(venta => muestraObjeto(document, venta.body))
19 .catch(muestraError)">
20
21 <h1>Carrito</h1>
22
23 <p>
24
25 <a href="index.html">Productos</a>
26
27 <button type='button' onclick="if (confirm('Confirma procesar')) {
28 invocaServicio('srv/srvVentaEnCapturaProcesa.php')
29 .then(() => location.href = 'index.html')
30 .catch(muestraError)
31 }">
32 Procesar compra
33 </button>
34
35 </p>
36
37 <p>
38 <label>
39 Folio
40 <output id="folio">
41 <progress max="100">Cargando…</progress>
42 </output>
43 </label>
44 </p>
45
46 <fieldset>
47
48 <legend>Detalle</legend>
49
50 <dl id="detalles">
51 <dt>Cargando…</dt>
52 <dd><progress max="100">Cargando…</progress></dd>
53 </dl>
54
55 </fieldset>
56
57</body>
58
59</html>

K. modifica.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Modificar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14 <script type="module" src="lib/js/muestraObjeto.js"></script>
15 <script type="module" src="lib/js/confirmaEliminar.js"></script>
16
17 <script>
18 // Obtiene los parámetros de la página.
19 const params = new URL(location.href).searchParams
20 </script>
21
22</head>
23
24<body onload="if (params.size > 0) {
25 invocaServicio('srv/srvDetalleDeVentaBusca.php?' + params)
26 .then(modelo => muestraObjeto(document, modelo.body))
27 .catch(muestraError)
28 }">
29
30 <form onsubmit="submitForm('srv/srvDetalleDeVentaModifica.php', event)
31 .then(modelo => location.href = 'carrito.html')
32 .catch(muestraError)">
33
34 <h1>Modificar</h1>
35
36 <p><a href="carrito.html">Cancelar</a></p>
37
38 <input type="hidden" name="prodId">
39
40 <p>
41 <label>
42 Producto
43 <output name="prodNombre">
44 <progress max="100">Cargando…</progress>
45 </output>
46 </label>
47 </p>
48
49 <p>
50 <label>
51 Precio
52 <output name="precio">
53 <progress max="100">Cargando…</progress>
54 </output>
55 </label>
56 </p>
57
58 <p>
59 <label>
60 Cantidad *
61 <input name="cantidad" type="number" min="0" step="0.01">
62 </label>
63 </p>
64
65 <p>* Obligatorio</p>
66
67 <p>
68
69 <button type="submit">Guardar</button>
70
71 <button type="button" onclick="if (params.size > 0 && confirmaEliminar()) {
72 invocaServicio('srv/srvDetalleDeVentaElimina.php?' + params)
73 .then(() => location.href = 'carrito.html')
74 .catch(muestraError)
75 }">
76 Eliminar
77 </button>
78
79 </p>
80
81 </form>
82
83</body>
84
85</html>

L. Carpeta « srv »

A. Carpeta « srv / modelo »

1. srv / modelo / DetalleDeVenta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/Venta.php";
5require_once __DIR__ . "/Producto.php";
6
7class DetalleDeVenta
8{
9
10 public ?Venta $venta;
11 public ?Producto $producto;
12 public float $cantidad;
13 public float $precio;
14
15 public function __construct(
16 float $cantidad = NAN,
17 float $precio = NAN,
18 ?Producto $producto = null,
19 ?Venta $venta = null
20 ) {
21 $this->cantidad = $cantidad;
22 $this->precio = $precio;
23 $this->producto = $producto;
24 $this->venta = $venta;
25 }
26
27 public function valida()
28 {
29 if ($this->producto === null)
30 throw new Exception("Detalle de venta sin producto.");
31 if (is_nan($this->cantidad))
32 throw new ProblemDetails(
33 status: ProblemDetails::BadRequest,
34 type: "/error/cantidadincorrecta.html",
35 title: "La cantidad no puede ser NAN.",
36 );
37 if (is_nan($this->precio))
38 throw new ProblemDetails(
39 status: ProblemDetails::BadRequest,
40 type: "/error/precioincorrecto.html",
41 title: "El precio no puede ser NAN.",
42 );
43 }
44}
45

2. srv / modelo / Producto.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/../../lib/php/validaNombre.php";
5
6class Producto
7{
8
9 public int $id;
10 public string $nombre;
11 public float $existencias;
12 public float $precio;
13
14 public function __construct(
15 string $nombre = "",
16 float $existencias = NAN,
17 float $precio = NAN,
18 int $id = 0
19 ) {
20 $this->id = $id;
21 $this->nombre = $nombre;
22 $this->existencias = $existencias;
23 $this->precio = $precio;
24 }
25
26 public function valida()
27 {
28 validaNombre($this->nombre);
29 if (is_nan($this->existencias))
30 throw new ProblemDetails(
31 status: ProblemDetails::BadRequest,
32 type: "/error/existenciasincorrectas.html",
33 title: "Las existencias no pueden ser NAN.",
34 );
35 if (is_nan($this->precio))
36 throw new ProblemDetails(
37 status: ProblemDetails::BadRequest,
38 type: "/error/precioincorrecto.html",
39 title: "El precio no puede ser NAN.",
40 );
41 }
42}
43

3. srv / modelo / Venta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/DetalleDeVenta.php";
5
6class Venta
7{
8
9 public int $id;
10 public bool $enCaptura;
11 /** @var DetalleDeVenta[] */
12 public array $detalles;
13
14 public function __construct(
15 bool $enCaptura = false,
16 array $detalles = [],
17 int $id = 0
18 ) {
19 $this->id = $id;
20 $this->enCaptura = $enCaptura;
21 $this->detalles = $detalles;
22 }
23
24 public function valida()
25 {
26 foreach ($this->detalles as $detalle) {
27 if (!($detalle instanceof DetalleDeVenta))
28 throw new ProblemDetails(
29 status: ProblemDetails::BadRequest,
30 type: "/error/detalledeventaincorrecto.html",
31 title: "Tipo incorrecto para un etalle de venta.",
32 );
33 $detalle->valida();
34 }
35 }
36}
37

B. srv / srvDetalleDeVentaAgrega.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeDecimal.php";
7require_once __DIR__ . "/../lib/php/pdFaltaId.php";
8require_once __DIR__ . "/modelo/Producto.php";
9require_once __DIR__ . "/modelo/DetalleDeVenta.php";
10require_once __DIR__ . "/bd/detalleDeVentaAgrega.php";
11
12ejecutaServicio(function () {
13 $id = leeEntero("id");
14 if ($id === null) throw pdFaltaId();
15 $cantidad = leeDecimal("cantidad");
16 if ($cantidad === null)
17 throw new ProblemDetails(
18 status: ProblemDetails::BadRequest,
19 type: "/error/faltacantidad.html",
20 title: "Falta la cantidad."
21 );
22 $producto = new Producto(id: $id);
23 $modelo = new DetalleDeVenta(producto: $producto, cantidad: $cantidad);
24 detalleDeVentaAgrega($modelo);
25 $producto = $modelo->producto;
26 $id = htmlentities($producto->id);
27 return JsonResponse::created("/srv/srvDetalleDeVentaBusca.php?id=$id", [
28 "prodId" => ["value" => $producto->id],
29 "prodNombre" => ["value" => $producto->nombre],
30 "precio" => ["value" => "$" . number_format($modelo->precio, 2)],
31 "cantidad" => ["valueAsNumber" => $modelo->cantidad],
32 ]);
33});
34

C. srv / srvDetalleDeVentaBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/detalleDeVentaBusca.php";
8
9ejecutaServicio(function () {
10 $prodId = leeEntero("prodId");
11 if ($prodId === null) throw pdFaltaId();
12 $modelo = detalleDeVentaBusca($prodId);
13 if ($modelo === false) {
14 $htmlId = htmlentities($prodId);
15 throw new ProblemDetails(
16 status: ProblemDetails::NotFound,
17 type: "/error/detalledeventanoencontrado.html",
18 title: "Detalle de venta no encontrado.",
19 detail: "No se encontró ningún detalle de venta con el id de producto "
20 . $htmlId . ".",
21 );
22 }
23 $producto = $modelo->producto;
24 return [
25 "prodId" => ["value" => $producto->id],
26 "prodNombre" => ["value" => $producto->nombre],
27 "precio" => ["value" => "$" . number_format($modelo->precio, 2)],
28 "cantidad" => ["valueAsNumber" => $modelo->cantidad],
29 ];
30});
31

D. srv / srvDetalleDeVentaElimina.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/detalleDeVentaElimina.php";
8
9ejecutaServicio(function () {
10 $prodId = leeEntero("prodId");
11 if ($prodId === null) throw pdFaltaId();
12 detalleDeVentaElimina($prodId);
13 return JsonResponse::noContent();
14});
15

E. srv / srvDetalleDeVentaModifica.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeDecimal.php";
7require_once __DIR__ . "/modelo/Producto.php";
8require_once __DIR__ . "/modelo/DetalleDeVenta.php";
9require_once __DIR__ . "/bd/detalleDeVentaModifica.php";
10
11ejecutaServicio(function () {
12 $prodId = leeEntero("prodId");
13 if ($prodId === null) throw pdFaltaId();
14 $producto = new Producto(id: $prodId);
15 $cantidad = leeDecimal("cantidad");
16 if ($cantidad === null)
17 throw new ProblemDetails(
18 status: ProblemDetails::BadRequest,
19 type: "/error/faltacantidad.html",
20 title: "Falta la cantidad."
21 );
22 $modelo = new DetalleDeVenta(producto: $producto, cantidad: $cantidad);
23 detalleDeVentaModifica($modelo);
24 $producto = $modelo->producto;
25 return [
26 "prodId" => ["value" => $producto->id],
27 "prodNombre" => ["value" => $producto->nombre],
28 "precio" => ["value" => "$" . number_format($modelo->precio, 2)],
29 "cantidad" => ["valueAsNumber" => $modelo->cantidad],
30 ];
31});
32

F. srv / srvProductoBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/productoBusca.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 $modelo = productoBusca($id);
13 if ($modelo === false) {
14 $htmlId = htmlentities($id);
15 throw new ProblemDetails(
16 status: ProblemDetails::BadRequest,
17 type: "/error/productonoencontrado.html",
18 title: "Producto no encontrado.",
19 detail: "No se encontró ningún producto con el id $htmlId.",
20 );
21 } else {
22 return [
23 "id" => ["value" => $modelo->id],
24 "producto" => ["value" => $modelo->nombre],
25 "precio" => ["value" => "$" . number_format($modelo->precio, 2)],
26 ];
27 }
28});
29

G. srv / srvProductoConsulta.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/productoConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = productoConsulta();
8 $render = "";
9 foreach ($lista as $modelo) {
10 $id = htmlentities($modelo->id);
11 $nombre = htmlentities($modelo->nombre);
12 $precio = htmlentities("$" . number_format($modelo->precio, 2));
13 $existencias = htmlentities(number_format($modelo->existencias, 2));
14 $render .=
15 "<dt>$nombre</dt>
16 <dd>
17 <a href='agrega.html?id=$id'>Agregar al carrito</a>
18 </dd>
19 <dd>
20 <dl>
21 <dt>Precio</dt>
22 <dd>$precio</dd>
23 <dt>Existencias</dt>
24 <dd>$existencias</dd>
25 </dl>
26 </dd>";
27 }
28 return ["lista" => ["innerHTML" => $render]];
29});
30

H. srv / srvVentaEnCapturaBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/modelo/DetalleDeVenta.php";
6require_once __DIR__ . "/bd/ventaEnCapturaBusca.php";
7
8ejecutaServicio(function () {
9 $modelo = ventaEnCapturaBusca();
10 if ($modelo === false)
11 throw new ProblemDetails(
12 status: ProblemDetails::BadRequest,
13 type: "/error/ventaencapturanoencontrada.html",
14 title: "Venta en captura no encontrada.",
15 detail: "No se encontró ninguna venta en captura.",
16 );
17 $detalles = $modelo->detalles;
18 $productoIds = [];
19 foreach ($detalles as $detalle) {
20 $id = $detalle->producto->id;
21 $productoIds[$id] = true;
22 }
23 $renderDetalles = "";
24 foreach ($detalles as $detalle) {
25 $producto = $detalle->producto;
26 $prodId = htmlentities($producto->id);
27 $prodNombre = htmlentities($producto->nombre);
28 $precio = htmlentities("$" . number_format($detalle->precio, 2));
29 $cantidad = htmlentities(number_format($detalle->cantidad, 2));
30 $renderDetalles .=
31 "<dt>$prodNombre</dt>
32 <dd>
33 <a href= 'modifica.html?prodId=$prodId'>Modificar o eliminar</a>
34 </dd>
35 <dd>
36 <dl>
37 <dt>Cantidad</dt>
38 <dd>$cantidad</dd>
39 <dt>Precio</dt>
40 <dd>$precio</dd>
41 </dl>
42 </dd>";
43 }
44 $modelo->detalles = [];
45 return [
46 "folio" => ["value" => $modelo->id],
47 "detalles" => ["innerHTML" => $renderDetalles]
48 ];
49});
50

I. srv / srvVentaEnCapturaProcesa.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/ventaEnCapturaProcesa.php";
5
6ejecutaServicio(function () {
7 ventaEnCapturaProcesa();
8 return JsonResponse::created("/srv/srvVentaEnCapturaBusca.php", []);
9});
10

J. Carpeta « srv / bd »

1. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS VENTA (
7 VENT_ID INTEGER,
8 VENT_EN_CAPTURA INTEGER NOT NULL,
9 CONSTRAINT VENT_PK
10 PRIMARY KEY(VENT_ID)
11 )'
12 );
13 $con->exec(
14 'CREATE TABLE IF NOT EXISTS PRODUCTO (
15 PROD_ID INTEGER,
16 PROD_NOMBRE TEXT NOT NULL,
17 PROD_EXISTENCIAS REAL NOT NULL,
18 PROD_PRECIO REAL NOT NULL,
19 CONSTRAINT PROD_PK
20 PRIMARY KEY(PROD_ID),
21 CONSTRAINT PROD_NOM_UNQ
22 UNIQUE(PROD_NOMBRE)
23 )'
24 );
25 $con->exec(
26 'CREATE TABLE IF NOT EXISTS DET_VENTA (
27 VENT_ID INTEGER NOT NULL,
28 PROD_ID INTEGER NOT NULL,
29 DTV_CANTIDAD REAL NOT NULL,
30 DTV_PRECIO REAL NOT NULL,
31 CONSTRAINT DTV_PK
32 PRIMARY KEY (VENT_ID, PROD_ID),
33 CONSTRAINT DTV_VENT_FK
34 FOREIGN KEY (VENT_ID) REFERENCES VENTA(VENT_ID),
35 CONSTRAINT DTV_PROD_FK
36 FOREIGN KEY (PROD_ID) REFERENCES PRODUCTO(PROD_ID)
37 )'
38 );
39}
40

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/../modelo/Venta.php";
4require_once __DIR__ . "/../modelo/Producto.php";
5require_once __DIR__ . "/bdCrea.php";
6require_once __DIR__ . "/productoCuenta.php";
7require_once __DIR__ . "/productoAgrega.php";
8require_once __DIR__ . "/ventaCuenta.php";
9require_once __DIR__ . "/ventaAgrega.php";
10
11class Bd
12{
13
14 private static ?PDO $conexion = null;
15
16 public static function getConexion(): PDO
17 {
18 if (self::$conexion === null) {
19 self::$conexion = new PDO(
20 // cadena de conexión
21 "sqlite:srvcompras.db",
22 // usuario
23 null,
24 // contraseña
25 null,
26 // Opciones: conexiones persistentes y lanza excepciones.
27 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
28 );
29
30 bdCrea(self::$conexion);
31 if (productoCuenta() === 0) {
32 productoAgrega(
33 new Producto(nombre: "Sandwich", existencias: 50, precio: 15)
34 );
35 productoAgrega(
36 new Producto(nombre: "Hot dog", existencias: 40, precio: 30)
37 );
38 productoAgrega(
39 new Producto(nombre: "Hamburguesa", existencias: 30, precio: 40)
40 );
41 }
42
43 if (ventaCuenta() === 0) {
44 ventaAgrega(new Venta(enCaptura: true));
45 }
46 }
47
48 return self::$conexion;
49 }
50}
51

3. srv / bd / detalleDeVentaAgrega.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/../modelo/DetalleDeVenta.php";
5require_once __DIR__ . "/Bd.php";
6require_once __DIR__ . "/ventaEnCapturaBusca.php";
7require_once __DIR__ . "/productoBusca.php";
8
9function detalleDeVentaAgrega(DetalleDeVenta $modelo)
10{
11 $con = Bd::getConexion();
12 $producto = productoBusca($modelo->producto->id);
13 if ($producto === false) {
14 $htmlId = htmlentities($modelo->producto->id);
15 throw new ProblemDetails(
16 status: ProblemDetails::BadRequest,
17 type: "/error/productonoencontrado.html",
18 title: "Producto no encontrado.",
19 detail: "No se encontró ningún producto con el id $htmlId.",
20 );
21 }
22 $venta = ventaEnCapturaBusca();
23 if ($venta === false)
24 throw new ProblemDetails(
25 status: ProblemDetails::BadRequest,
26 type: "/error/ventaencapturanoencontrada.html",
27 title: "Venta en captura no encontrada.",
28 detail: "No se encontró ninguna venta en captura.",
29 );
30 $modelo->venta = $venta;
31 $modelo->precio = $producto->precio;
32 $modelo->producto = $producto;
33 $modelo->valida();
34 $stmt = $con->prepare(
35 "INSERT INTO DET_VENTA
36 (VENT_ID, PROD_ID, DTV_CANTIDAD, DTV_PRECIO)
37 VALUES
38 (:ventId, :prodId, :cantidad, :precio)"
39 );
40 $stmt->execute(
41 [
42 ":ventId" => $venta->id,
43 ":prodId" => $producto->id,
44 ":cantidad" => $modelo->cantidad,
45 ":precio" => $producto->precio
46 ]
47 );
48}
49

4. srv / bd / detalleDeVentaBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/DetalleDeVenta.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/ventaEnCapturaBusca.php";
6require_once __DIR__ . "/productoBusca.php";
7
8function detalleDeVentaBusca(int $prodId)
9{
10 $venta = ventaEnCapturaBusca();
11 if ($venta === false) {
12 return false;
13 }
14 $producto = productoBusca($prodId);
15 if ($producto === false) {
16 return false;
17 }
18 $con = Bd::getConexion();
19 $stmt = $con->prepare(
20 "SELECT
21 DV.PROD_ID AS prodId,
22 P.PROD_NOMBRE AS prodNombre,
23 DV.DTV_CANTIDAD AS cantidad,
24 DV.DTV_PRECIO AS precio
25 FROM DET_VENTA DV, PRODUCTO P
26 WHERE
27 DV.PROD_ID = P.PROD_ID
28 AND DV.VENT_ID = :ventId
29 AND DV.PROD_ID = :prodId"
30 );
31 $stmt->execute([
32 ":ventId" => $venta->id,
33 ":prodId" => $prodId
34 ]);
35 $stmt->setFetchMode(PDO::FETCH_OBJ);
36 $obj = $stmt->fetch();
37 if ($obj === false) {
38 return false;
39 } else {
40 $dtv = new DetalleDeVenta();
41 $dtv->venta = $venta;
42 $dtv->producto = $producto;
43 $dtv->cantidad = $obj->cantidad;
44 $dtv->precio = $obj->precio;
45 return $dtv;
46 }
47}
48

5. srv / bd / detalleDeVentaConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Venta.php";
5require_once __DIR__ . "/../modelo/DetalleDeVenta.php";
6require_once __DIR__ . "/../modelo/Producto.php";
7require_once __DIR__ . "/Bd.php";
8
9function detalleDeVentaConsulta(Venta $venta)
10{
11 $con = Bd::getConexion();
12 $stmt = $con->query(
13 "SELECT
14 DV.PROD_ID AS prodId,
15 P.PROD_NOMBRE AS prodNombre,
16 P.PROD_EXISTENCIAS AS prodExistencias,
17 P.PROD_PRECIO AS prodPrecio,
18 DV.DTV_CANTIDAD AS cantidad,
19 DV.DTV_PRECIO AS precio
20 FROM DET_VENTA DV, PRODUCTO P
21 WHERE
22 DV.PROD_ID = P.PROD_ID
23 AND DV.VENT_ID = :ventId
24 ORDER BY P.PROD_NOMBRE"
25 );
26 $stmt->execute([":ventId" => $venta->id]);
27 $resultado = $stmt->fetchAll(PDO::FETCH_OBJ);
28 $objs = recibeFetchAll($resultado);
29 /** @var DetalleDeVenta[] */
30 $detalles = [];
31 foreach ($objs as $obj) {
32 $producto = new Producto(
33 id: $obj->prodId,
34 nombre: $obj->prodNombre,
35 existencias: $obj->prodExistencias,
36 precio: $obj->prodPrecio
37 );
38 $detalle = new DetalleDeVenta(
39 venta: $venta,
40 producto: $producto,
41 cantidad: $obj->cantidad,
42 precio: $obj->precio
43 );
44 $detalles[] = $detalle;
45 }
46 return $detalles;
47}
48

6. srv / bd / detalleDeVentaElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4require_once __DIR__ . "/ventaEnCapturaBusca.php";
5
6function detalleDeVentaElimina(int $prodId)
7{
8 $venta = ventaEnCapturaBusca();
9 if ($venta !== false) {
10 $con = Bd::getConexion();
11 $stmt = $con->prepare(
12 "DELETE FROM DET_VENTA
13 WHERE VENT_ID = :ventId
14 AND PROD_ID = :prodId"
15 );
16 $stmt->execute([
17 ":ventId" => $venta->id,
18 ":prodId" => $prodId,
19 ]);
20 }
21}
22

7. srv / bd / detalleDeVentaModifica.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/../modelo/DetalleDeVenta.php";
5require_once __DIR__ . "/Bd.php";
6require_once __DIR__ . "/ventaEnCapturaBusca.php";
7require_once __DIR__ . "/productoBusca.php";
8
9
10function detalleDeVentaModifica(DetalleDeVenta $modelo)
11{
12 $con = Bd::getConexion();
13 $producto = productoBusca($modelo->producto->id);
14 if ($producto === false) {
15 $htmlId = htmlentities($modelo->producto->id);
16 throw new ProblemDetails(
17 status: ProblemDetails::BadRequest,
18 type: "/error/productonoencontrado.html",
19 title: "Producto no encontrado.",
20 detail: "No se encontró ningún producto con el id $htmlId.",
21 );
22 }
23 $venta = ventaEnCapturaBusca();
24 $venta = ventaEnCapturaBusca();
25 if ($venta === false)
26 throw new ProblemDetails(
27 status: ProblemDetails::BadRequest,
28 type: "/error/ventaencapturanoencontrada.html",
29 title: "Venta en captura no encontrada.",
30 detail: "No se encontró ninguna venta en captura.",
31 );
32 $modelo->venta = $venta;
33 $modelo->producto = $producto;
34 $modelo->precio = $producto->precio;
35 $modelo->valida();
36 $stmt = $con->prepare(
37 "UPDATE DET_VENTA
38 SET
39 DTV_CANTIDAD = :cantidad,
40 DTV_PRECIO = :precio
41 WHERE
42 VENT_ID = :ventId
43 AND PROD_ID = :prodId"
44 );
45 $stmt->execute(
46 [
47 ":ventId" => $venta->id,
48 ":prodId" => $producto->id,
49 ":cantidad" => $modelo->cantidad,
50 ":precio" => $modelo->precio
51 ]
52 );
53}
54

8. srv / bd / productoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Producto.php";
4require_once __DIR__ . "/Bd.php";
5
6function productoAgrega(Producto $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO PRODUCTO
12 (PROD_NOMBRE, PROD_EXISTENCIAS, PROD_PRECIO)
13 VALUES
14 (:nombre, :existencias, :precio)"
15 );
16 $stmt->execute([
17 ":nombre" => $modelo->nombre,
18 ":existencias" => $modelo->existencias,
19 ":precio" => $modelo->precio
20 ]);
21 /* Si usas una secuencia para generar el id,
22 * pasa como parámetro de lastInsertId el
23 * nombre de dicha secuencia, debes
24 * ejecutarlo antes del INSERT y pasarle el
25 * id generado al SQL. */
26 $modelo->id = $con->lastInsertId();
27}
28

9. srv / bd / productoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Producto.php";
4require_once __DIR__ . "/Bd.php";
5
6function productoBusca(int $id): false|Producto
7{
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "SELECT
11 PROD_ID AS id,
12 PROD_NOMBRE AS nombre,
13 PROD_PRECIO AS precio,
14 PROD_EXISTENCIAS AS existencias
15 FROM PRODUCTO
16 WHERE PROD_ID = :id"
17 );
18 $stmt->execute([":id" => $id]);
19 $stmt->setFetchMode(
20 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
21 Producto::class
22 );
23 return $stmt->fetch();
24}
25

10. srv / bd / productoConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Producto.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Producto[] */
8function productoConsulta(): array
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 PROD_ID as id,
14 PROD_NOMBRE as nombre,
15 PROD_PRECIO as precio,
16 PROD_EXISTENCIAS as existencias
17 FROM PRODUCTO
18 ORDER BY PROD_NOMBRE"
19 );
20 $resultado = $stmt->fetchAll(
21 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
22 Producto::class
23 );
24 return recibeFetchAll($resultado);
25}
26

11. srv / bd / productoCuenta.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4
5function productoCuenta(): false|int
6{
7 $con = Bd::getConexion();
8 $stmt = $con->query("SELECT COUNT(*) FROM PRODUCTO");
9 return $stmt->fetchColumn();
10}
11

12. srv / bd / ventaAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Venta.php";
4require_once __DIR__ . "/Bd.php";
5
6function ventaAgrega(Venta $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO VENTA
12 (VENT_EN_CAPTURA)
13 VALUES
14 (:enCaptura)"
15 );
16 $stmt->execute(([":enCaptura" => $modelo->enCaptura]));
17 /* Si usas una secuencia para generar el id,
18 * pasa como parámetro de lastInsertId el
19 * nombre de dicha secuencia, debes
20 * ejecutarlo antes del INSERT y pasarle el
21 * id generado al SQL. */
22 $modelo->id = $con->lastInsertId();
23}
24

13. srv / bd / ventaCuenta.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4
5function ventaCuenta(): false|int
6{
7 $con = Bd::getConexion();
8 $stmt = $con->query("SELECT COUNT(*) FROM VENTA");
9 return $stmt->fetchColumn();
10}
11

14. srv / bd / ventaEnCapturaBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Venta.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/detalleDeVentaConsulta.php";
6
7function ventaEnCapturaBusca()
8{
9 $con = Bd::getConexion();
10 $stmt = $con->query(
11 "SELECT VENT_ID as id
12 FROM VENTA
13 WHERE VENT_EN_CAPTURA = 1"
14 );
15 $stmt->setFetchMode(PDO::FETCH_OBJ);
16 $obj = $stmt->fetch();
17 if ($obj === false) {
18 return false;
19 } else {
20 $venta = new Venta(id: $obj->id, enCaptura: true);
21 $venta->detalles = detalleDeVentaConsulta($venta);
22 return $venta;
23 }
24}
25

15. srv / bd / ventaEnCapturaProcesa.php

1<?php
2
3require_once __DIR__ . "/../modelo/Venta.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/ventaEnCapturaBusca.php";
6require_once __DIR__ . "/ventaAgrega.php";
7
8function ventaEnCapturaProcesa()
9{
10 $con = Bd::getConexion();
11 $con->beginTransaction();
12 $modelo = ventaEnCapturaBusca();
13 if ($modelo === false)
14 throw new Exception("Venta no encontrada.");
15 $modelo->valida();
16 $detalles = $modelo->detalles;
17 $stmt = $con->prepare(
18 "UPDATE PRODUCTO
19 SET PROD_EXISTENCIAS = :existencias
20 WHERE PROD_ID = :prodId"
21 );
22 foreach ($detalles as $dtv) {
23 $producto = $dtv->producto;
24 $stmt->execute(([
25 ":prodId" => $producto->id,
26 ":existencias" => $producto->existencias - $dtv->cantidad
27 ]));
28 }
29 $stmt = $con->prepare(
30 "UPDATE VENTA
31 SET VENT_EN_CAPTURA = 0
32 WHERE VENT_ID = :id"
33 );
34 $stmt->execute([":id" => $modelo->id]);
35 ventaAgrega(new Venta(enCaptura: true));
36 $con->commit();
37}
38

M. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / confirmaEliminar.js

1export function confirmaEliminar() {
2 return confirm("Confirma la eliminación")
3}
4
5// Permite que los eventos de html usen la función.
6window["confirmaEliminar"] = confirmaEliminar

2. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

3. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

4. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

5. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

6. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

7. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeArray.php

1<?php
2
3/**
4 * Devuelve los valores asociados a un
5 * parámetro multivaluado; por ejemplo, un
6 * grupo de checkbox, recibido en el servidor
7 * por medio de GET, POST o cookie. Si no se
8 * recibe el parámetro, devuelve null. Si el
9 * valor recibido no es un arreglo, lo coloca
10 * dentro de uno.
11 */
12function leeArray(string $parametro)
13{
14 if (isset($_REQUEST[$parametro])) {
15 $valor = $_REQUEST[$parametro];
16 return is_array($valor)
17 ? $valor
18 : [$valor];
19 } else {
20 return null;
21 }
22}
23

4. lib / php / leeDecimal.php

1<?php
2
3require_once __DIR__ . "/leeTexto.php";
4
5/**
6 * Recupera el valor decimal de un parámetro
7 * (que puede tener fracciones) enviado al
8 * servidor por medio de GET, POST o cookie.
9 * Si parámetro no se puede convertir a decimal,
10 * se genera un error. Si el parámetro no se
11 * recibe, devuelve null.
12 * Para que funcione parecido a leeEntero,
13 * si se recibe una cadena vacía, se devuelve
14 * null.
15 */
16function leeDecimal(string $parametro): ?float
17{
18 $valor = leeTexto($parametro);
19 return $valor === null|| $valor === ""
20 ? null
21 : trim($valor);
22}
23

5. lib / php / leeEntero.php

1<?php
2
3require_once __DIR__ . "/leeTexto.php";
4
5/**
6 * Devuelve el valor entero de un parámetro
7 * recibido en el servidor por medio de GET,
8 * POST o cookie. Si parámetro no se puede
9 * convertir a entero, se genera un error.
10 * Si el parámetro no se recibe, devuelve
11 * null.
12 * Para que las llaves foráneas fucionen bien,
13 * si se recibe una cadena vacía, se devuelve
14 * null.
15 */
16function leeEntero(string $parametro): ?int
17{
18 $valor = leeTexto($parametro);
19 return $valor === null || $valor === ""
20 ? null
21 : trim($valor);
22}
23

6. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

7. lib / php / pdFaltaId.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function pdFaltaId()
6{
7 return new ProblemDetails(
8 status: ProblemDetails::BadRequest,
9 type: "/error/faltaid.html",
10 title: "No se ha proporcionado el valor de id.",
11 );
12}
13

8. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

9. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

10. lib / php / validaNombre.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function validaNombre(string $nombre)
6{
7 if ($nombre === "")
8 throw new ProblemDetails(
9 status: ProblemDetails::BadRequest,
10 type: "/error/faltanombre.html",
11 title: "Falta el nombre.",
12 );
13}
14

N. Carpeta « error »

A. error / cantidadincorrecta.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>La cantidad no puede ser NAN</title>
10
11</head>
12
13<body>
14
15 <h1>La cantidad no puede ser NAN</h1>
16
17</body>
18
19</html>

B. error / detalledeventaincorrecto.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Tipo incorrecto para un etalle de venta</title>
10
11</head>
12
13<body>
14
15 <h1>Tipo incorrecto para un etalle de venta</h1>
16
17</body>
18
19</html>

C. error / detalledeventanoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Detalle de venta no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Detalle de venta no encontrado</h1>
16
17 <p>No se encontró ningún detalle de venta con el id de producto solicitado.</p>
18
19</body>
20
21</html>

D. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

E. error / existenciasincorrectas.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Las existencias no pueden ser NAN</title>
10
11</head>
12
13<body>
14
15 <h1>Las existencias no pueden ser NAN</h1>
16
17</body>
18
19</html>

F. error / faltacantidad.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta la cantidad</title>
10
11</head>
12
13<body>
14
15 <h1>Falta la cantidad</h1>
16
17</body>
18
19</html>

G. error / faltaid.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el id</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el id</h1>
16
17 <p>No se ha proporcionado el valor de id.</p>
18
19</body>
20
21</html>

H. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17</body>
18
19</html>

I. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

J. error / precioincorrecto.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El precio no puede ser NAN</title>
10
11</head>
12
13<body>
14
15 <h1>El precio no puede ser NAN</h1>
16
17</body>
18
19</html>

K. error / productonoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Producto no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Producto no encontrado</h1>
16
17 <p>No se encontró ningún producto con el id solicitado.</p>
18
19</body>
20
21</html>

L. error / ventaencapturanoencontrada.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Venta en captura no encontrada</title>
10
11</head>
12
13<body>
14
15 <h1>Venta en captura no encontrada</h1>
16
17 <p>No se encontró ninguna venta en captura.</p>
18
19</body>
20
21</html>

O. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

P. Resumen

  • En esta lección se mostró la base de una aplicación que vende o compra.

24. Pruebas

Versión para imprimir.

A. Introducción

  • En esta lección se muestra como probar una aplicación web.

B. Pruebas de rendimiento y soporte de estándares

C. PHPUnit

  • Las pruebas de unidad permiten asegurar que una parte del código cumple correctamente con sus funciones.

  • A continuación se muestra un como realizar pruebas del servidor con PHPunit.

  • En la práctica, los sistemas deben pasar todas las pruebas. En este ejemplo se fallan algunas.

  • Puedes correr las pruebas en https://replit.com/@GilbertoPachec5/phpunit?v=1. Hazle fork al proyecto y córrelo.

1. Hazlo funcionar

2. Salida de las pruebas realizadas

Esta salida muestra algunos fallos para que te des cuenta como se ven. En la práctica, todas la pruebas deben aprobarse.


  PHPUnit 10.5.5 by Sebastian Bergmann and contributors.

  Runtime:       PHP 8.2.7
  
  ..F                                                                 3 / 3 (100%)
  
  Time: 00:00.091, Memory: 8.00 MB
  
  There was 1 failure:
  
  1) RecomiendaTest::testReg2
  Probando reg 2
  Failed asserting that two strings are equal.
  --- Expected
  +++ Actual
  @@ @@
  -'Daddy Yankee.'
  +'Bad Bunny.'
  
  /home/runner/phpunit/tests/RecomiendaTest.php:19

3. Archivos

4. Carpeta « src »

A. src / recomienda.php

1<?php
2
3function recomienda(string $genero): string
4{
5 if ($genero === "pop") {
6 return "Dua Lipa.";
7 } elseif ($genero === "reg") {
8 return "Bad Bunny.";
9 } else {
10 return "De eso no conozco";
11 }
12}
13

5. Carpeta « tests »

A. tests / RecomiendaTest.php

1<?php
2
3use PHPUnit\Framework\TestCase;
4
5require_once __DIR__ . "/../src/recomienda.php";
6
7final class RecomiendaTest extends TestCase
8{
9 public function testPop()
10 {
11 $this->assertEquals("Dua Lipa.", recomienda("pop"), "Probando pop");
12 }
13 public function testReg()
14 {
15 $this->assertEquals("Bad Bunny.", recomienda("reg"), "Probando reg");
16 }
17 public function testReg2()
18 {
19 $this->assertEquals("Daddy Yankee.", recomienda("reg"), "Probando reg 2");
20 }
21}
22

6. composer.json

1{
2 "require-dev": {
3 "phpunit/phpunit": "^10"
4 }
5}

7. composer.lock

1-- No se muestra eñ contenido de este archivo --

8. Carpeta « vendor »

A. vendor / -- No se muestra el contenido de esta carpeta --

1

9. .replit

1run = ["./vendor/bin/phpunit", "tests"]
2
3entrypoint = "tests/RecomiendaTest.php"
4
5[nix]
6channel = "stable-23_05"

10. replit.nix

1{ pkgs }: {
2 deps = [
3 pkgs.php82
4 pkgs.php82Packages.composer
5 ];
6}

D. Jasmine

  • Las pruebas de unidad permiten asegurar que una parte del código cumple correctamente con sus funciones.

  • A continuación se muestra un como realizar pruebas del cliente con Jasmine.

  • En la práctica, los sistemas deben pasar todas las pruebas. En este ejemplo se fallan algunas.

  • Puedes correr las pruebas en https://replit.com/@GilbertoPachec5/jasmine?v=1. Hazle fork al proyecto y córrelo.

1. Hazlo funcionar

  1. Este ejercicio usa la librería Jasmine para JavaScript. Puedes profundizar en este tema en la URL https://jasmine.github.io/

  2. Revisa el proyecto en Replit con la URL https://replit.com/@GilbertoPachec5/jasmine?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  3. Usa o crea una cuenta de Google.

  4. Crea una cuenta de Replit usando la cuenta de Google.

  5. Crear un proyecto de tipo Node.js en https://replit.com. Algunos archivos se generan con las instrucciones que siguen, por lo que no se incluyen en el zip, pero si se muestra su contenido en las páginas siguientes.

  6. Si trabajas desde Windows, instala Node.js desde https://nodejs.org/en. Crea una carpeta y pásate a ella desde el símbolo del sistema. o con Visual Studio Code abre la carpeta y muestra una terminal. Inicializa un proyecto de node con la instrucción
    npm init -y

  7. Edita el archivo packege.json e inserta en la linea 6, sin borrar ni sobreescribir nada, la siguiente línea
    "type": "module",

  8. En el mismo archivo packege.json cambia la línea que dice "test" a que diga:
    "test": "jasmine"

  9. Instala Jasmine en el proyecto con la orden:
    npm install --save-dev jasmine

  10. Inicializa Jasmine con la orden:
    npx jasmine init

  11. Añade los archivos que vienen en el proyecto; esto incluye modificar el archivo .replit. El código a probar se coloca en la carpeta src y las pruebas en la carpeta spec.

  12. Haz clic en el botón Run. En la página de desarrollo, en la pestaña Console aparece la salida de las pruebas.

2. Salida de las pruebas realizadas

Esta salida muestra algunos fallos para que te des cuenta como se ven. En la práctica, todas la pruebas deben aprobarse.


> nodejs@1.0.0 test
> jasmine

Randomized with seed 87173
Started
.F.

Failures:
1) recomienda reg2
  Message:
  Expected 'Bad Bunny.' to be 'Daddy Yankee.'.
  Stack:
        at <Jasmine>
        at UserContext.<anonymous> (file:///D:/gilpg/Documents/Sitios/awoas/jasminewin/spec/recomiendaSpec.js:6:45)
        at <Jasmine>

3 specs, 1 failure
Finished in 0.012 seconds
Randomized with seed 87173 (jasmine --random=true --seed=87173)

3. Archivos

4. Carpeta « src »

A. src / recomienda.js

1/**
2 * @param {string} genero
3 */
4export function recomienda(genero) {
5 if (genero === "pop") {
6 return "Dua Lipa."
7 } else if (genero === "reg") {
8 return "Bad Bunny."
9 } else {
10 return "De eso no conozco."
11 }
12}

5. Carpeta « spec »

A. spec / recomiendaSpec.js

1import { recomienda } from '../src/recomienda.js'
2
3describe("recomienda", () => {
4 it("pop", () => expect(recomienda("pop")).toBe("Dua Lipa."))
5 it("reg", () => expect(recomienda("reg")).toBe("Bad Bunny."))
6 it("reg2", () => expect(recomienda("reg")).toBe("Daddy Yankee."))
7})

B. Carpeta « spec / support »

1. spec / support / jasmine.json

1{
2 "spec_dir": "spec",
3 "spec_files": [
4 "**/*[sS]pec.?(m)js"
5 ],
6 "helpers": [
7 "helpers/**/*.?(m)js"
8 ],
9 "env": {
10 "stopSpecOnExpectationFailure": false,
11 "random": true
12 }
13}
14

6. package.json

1{
2 "name": "nodejs",
3 "version": "1.0.0",
4 "description": ,
5 "main": "index.js",
6 "type": "module",
7 "scripts": {
8 "test": "jasmine"
9 },
10 "keywords": [],
11 "author": ,
12 "license": "ISC",
13 "dependencies": {
14 "@types/node": "^18.0.6",
15 "node-fetch": "^3.2.6"
16 },
17 "devDependencies": {
18 "jasmine": "^5.1.0"
19 }
20}
21

7. package-lock.json

1-- No se muestra el contenido de este archivo. --

8. Carpeta « node_modules »

A. node_modules / -- No se muestra el contenido de esta carpeta --

1

9. .replit

1run = ["npx", "jasmine"]
2entrypoint = "src/recomienda.js"
3modules = ["nodejs-20:v8-20230920-bd784b9"]
4hidden = [".config", "package-lock.json"]
5
6[nix]
7channel = "stable-23_05"
8
9[unitTest]
10language = "nodejs"
11
12[deployment]
13run = ["node", "index.js"]
14deploymentTarget = "cloudrun"
15ignorePorts = false
16

10. replit.nix

1{pkgs}: {
2 deps = [ ];
3}
4

11. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

E. Prueba de los servicios

F. Pruebas de la app

G. Resumen

  • En esta lección se mostró como probar una aplicación web.

25. Servicio con token

Versión para imprimir.

A. Introducción

B. Diagrama de despliegue

Diagrama de despliegue

C. Hazlo funcionar

D. Archivos

E. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Uso de tokens</title>
10
11</head>
12
13<body>
14
15 <h1>Uso de tokens</h1>
16
17 <p>
18 <a href="formulario.html">Servicio que procesa un formulario con token.</a>
19 </p>
20
21 </form>
22
23</body>
24
25</html>

F. formulario.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Servicio que procesa un formulario con token</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14
15 <script>
16
17 let token = ""
18
19 </script>
20
21</head>
22
23<!-- Al cargar, registra este formulario en la sesión y recibe un token. -->
24
25<body onload="invocaServicio('srv/srvRegistra.php')
26 .then(token => forma.token.value = token.body)
27 .catch(muestraError)">
28
29 <h1>Servicio que procesa un formulario con token</h1>
30
31 <p><a href="index.html">Cancela</a></p>
32
33 <form id="forma" onsubmit="submitForm('srv/srvProcesa.php', event)
34 .then(resultado => {
35 alert(resultado.body)
36 location.href = 'index.html'
37 })
38 .catch(muestraError)">
39
40 <!-- Al enviar la forma, se envía el token recibido. -->
41 <input type="hidden" name="token">
42
43 <p>
44 <label>
45 Saludo:
46 <input name="saludo">
47 </label>
48 </p>
49
50 <p>
51 <label>
52 Nombre:
53 <input name="nombre">
54 </label>
55 </p>
56
57 <p><button type="submit">Procesa</button></p>
58
59 </form>
60
61</body>
62
63</html>

G. Carpeta « srv »

A. srv / srvProcesa.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/leeTexto.php";
6require_once __DIR__ . "/../lib/php/validaToken.php";
7
8ejecutaServicio(function () {
9
10 session_start();
11
12 $token = leeTexto("token");
13 if ($token === null) {
14 throw new ProblemDetails(
15 status: ProblemDetails::BadRequest,
16 type: "/error/faltatoken.html",
17 title: "Falta el token.",
18 );
19 }
20 validaToken("formulario", $token);
21
22 // Si el token se halló, precesa normalmente la forma.
23 $saludo = leeTexto("saludo");
24 $nombre = leeTexto("nombre");
25 if ($saludo === null) {
26 throw new ProblemDetails(
27 status: ProblemDetails::BadRequest,
28 type: "/error/faltasaludo.html",
29 title: "Falta el saludo.",
30 detail: "La solicitud no tiene el valor de saludo.",
31 );
32 }
33 if ($saludo === "") {
34 throw new ProblemDetails(
35 status: ProblemDetails::BadRequest,
36 type: "/error/saludoenblanco.html",
37 title: "El saludo está en blanco.",
38 detail: "Pon texto en el saludo.",
39 );
40 }
41 if ($nombre === null) {
42 throw new ProblemDetails(
43 status: ProblemDetails::BadRequest,
44 type: "/error/faltanombre.html",
45 title: "Falta el nombre.",
46 detail: "La solicitud no tiene el valor de nombre.",
47 );
48 }
49 if ($nombre === "") {
50 throw new ProblemDetails(
51 status: ProblemDetails::BadRequest,
52 type: "/error/nombreenblanco.html",
53 title: "El nombre está en blanco.",
54 detail: "Pon texto en el nombre.",
55 );
56 }
57 $resultado = "{$saludo} {$nombre}.";
58 return $resultado;
59});
60

B. srv / srvRegistra.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/creaToken.php";
5require_once __DIR__ . "/../lib/php/leeTexto.php";
6
7ejecutaServicio(function () {
8 session_start();
9 // Crea un token para la página "formulario" que expira en 5 minutos.
10 return creaToken("formulario", 5);
11});
12

H. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

2. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

3. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

4. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

5. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / creaToken.php

1<?php
2
3function creaToken(string $pagina, int $duracionEnMinutos)
4{
5 $criptografiaFuerte = true;
6
7 // Crea el token
8 $token = [
9 "expiracion" => time() + 60 * $duracionEnMinutos,
10 // El token es de 80 caracteres, criptográficamente fuerte.
11 "texto" => bin2hex(openssl_random_pseudo_bytes(80, $criptografiaFuerte))
12 ];
13
14 // Verifica que ya haya tokens $pagina.
15 if (isset($_SESSION[$pagina])) {
16
17 $tokensParaPagina = $_SESSION[$pagina];
18
19 // Como ya existe el arreglo, elimina los tokens expirados para esta pagina.
20 foreach ($tokensParaPagina as $llave => $tokenParaPagina) {
21 if ($tokenParaPagina["expiracion"] > time()) {
22 unset($tokensParaPagina[$llave]);
23 }
24 }
25
26 // Se puede usar uno o varios tokens por pagina.
27 $tokensParaPagina[] = $token;
28 $_SESSION[$pagina] = $tokensParaPagina;
29 } else {
30
31 // Se puede usar uno o varios tokens por pagina
32 $_SESSION[$pagina] = [$token];
33 }
34
35 return $token["texto"];
36}
37

2. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

3. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

4. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

5. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

6. lib / php / validaToken.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5const FORBIDDEN = 403;
6
7function validaToken(string $pagina, string $token)
8{
9
10 if (!isset($_SESSION[$pagina]))
11 throw new ProblemDetails(
12 status: FORBIDDEN,
13 type: "/error/paginanoregistrada.html",
14 title: "Página no registrada.",
15 );
16
17 $tokensParaPagina = $_SESSION[$pagina];
18
19 if (!is_array($tokensParaPagina))
20 throw new ProblemDetails(
21 status: FORBIDDEN,
22 type: "/error/sintokens.html",
23 title: "No hay arereglo de tokens.",
24 );
25
26 $hallado = false;
27
28 // Valida que el token se haya registrado.
29 foreach ($tokensParaPagina as $llave => $tokenParaPagina) {
30
31 if (strcmp($token, $tokenParaPagina["texto"]) === 0) {
32
33 if ($tokenParaPagina["expiracion"] < time()) {
34 unset($tokensParaPagina[$llave]);
35 $_SESSION[$pagina] = $tokensParaPagina;
36 throw new ProblemDetails(
37 status: FORBIDDEN,
38 type: "/error/paginaexpirada.html",
39 title: "Tiempo de expiración excedido.",
40 );
41 }
42
43 $hallado = true;
44 } elseif ($tokenParaPagina["expiracion"] > time()) {
45
46 // Elimina tokens expirados
47 unset($tokensParaPagina[$llave]);
48 }
49 }
50
51 $_SESSION[$pagina] = $tokensParaPagina;
52
53 if ($hallado === false)
54 throw new ProblemDetails(
55 status: FORBIDDEN,
56 type: "/error/paginanoregistrada.html",
57 title: "Página no registrada.",
58 );
59}
60

I. Carpeta « error »

A. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

B. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17</body>
18
19</html>

C. error / faltasaludo.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el saludo</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el saludo</h1>
16
17 <p>La solicitud no tiene el valor de saludo.</p>
18
19</body>
20
21</html>

D. error / faltatoken.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el token</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el token</h1>
16
17</body>
18
19</html>

E. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

F. error / nombreenblanco.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El nombre está en blanco</title>
10
11</head>
12
13<body>
14
15 <h1>El nombre está en blanco</h1>
16
17 <p>Pon texto en el nombre.</p>
18
19</body>
20
21</html>

G. error / paginaexpirada.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Tiempo de expiración excedido</title>
10
11</head>
12
13<body>
14
15 <h1>Tiempo de expiración excedido</h1>
16
17</body>
18
19</html>

H. error / paginanoregistrada.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Página no registrada</title>
10
11</head>
12
13<body>
14
15 <h1>Página no registrada</h1>
16
17</body>
18
19</html>

I. error / saludoenblanco.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El saludo está en blanco</title>
10
11</head>
12
13<body>
14
15 <h1>El saludo está en blanco</h1>
16
17 <p>Pon texto en el saludo.</p>
18
19</body>
20
21</html>

J. error / sintokens.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>No hay arereglo de tokens</title>
10
11</head>
12
13<body>
14
15 <h1>No hay arereglo de tokens</h1>
16
17</body>
18
19</html>

J. jsconfig.json

  • Este archivo ayuda a detectar errores en los archivos del proyecto.

  • Lo utiliza principalmente Visual Studio Code.

  • No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

K. Resumen

  • En esta lección se mostró como proteger servicios y formas con el uso de tokens.