13. Carpeta « lib »

Versión para imprimir.

A. Carpeta « lib / js »

Versión para imprimir.

1. lib / js / activaNotificacionesPush.js

1/**
2 * @param { string | URL } urlDeServiceWorkerQueRecibeNotificaciones
3 */
4export async function activaNotificacionesPush(
5
6 urlDeServiceWorkerQueRecibeNotificaciones) {
7
8 // Valida que el navegador soporte notificaciones push.
9 if (!('PushManager' in window))
10 throw new Error("Este navegador no soporta notificaciones push.")
11
12 // Valida que el navegador soporte notificaciones,
13 if (!("Notification" in window))
14 throw new Error("Este navegador no soporta notificaciones push.")
15
16 // Valida que el navegador soporte service workers,
17 if (!("serviceWorker" in navigator))
18 throw new Error("Este navegador no soporta service workers.")
19
20 // Recupera el permiso para usar notificaciones
21 let permiso = Notification.permission
22 if (permiso === "default") {
23 // Permiso no asignado. Pide al usuario su autorización.
24 permiso = await Notification.requestPermission()
25 }
26
27 // Valida que el usuario haya permitido usar notificaciones..
28 if (permiso === "denied")
29 throw new Error("Notificaciones bloqueadas.")
30
31 const registro = await navigator.serviceWorker.register(
32 urlDeServiceWorkerQueRecibeNotificaciones)
33 console.log(urlDeServiceWorkerQueRecibeNotificaciones, "registrado.")
34 console.log(registro)
35
36 if (!("showNotification" in registro))
37 throw new Error("Este navegador no soporta notificaciones.")
38}

2. lib / js / calculaDtoParaSuscripcion.js

1/**
2 * Devuelve una literal de objeto que se puede usar para enviar
3 * en formato JSON al servidor.
4 * DTO es un acrónimo para Data Transder Object, u
5 * objeto para transferencia de datos.
6 * @param { PushSubscription } suscripcion
7 */
8export function calculaDtoParaSuscripcion(suscripcion) {
9 const key = suscripcion.getKey("p256dh")
10 const token = suscripcion.getKey("auth")
11 const supported = PushManager.supportedContentEncodings
12 const encodings = Array.isArray(supported) && supported.length > 0
13 ? supported
14 : ["aesgcm"]
15 const endpoint = suscripcion.endpoint
16 const publicKey = key === null
17 ? null
18 : btoa(String.fromCharCode.apply(null, new Uint8Array(key)))
19 const authToken = token === null
20 ? null
21 : btoa(String.fromCharCode.apply(null, new Uint8Array(token)))
22 const contentEncoding = encodings[0]
23 return {
24 endpoint,
25 publicKey,
26 authToken,
27 contentEncoding
28 }
29}
30

3. lib / js / cancelaSuscripcionPush.js

1import { getSuscripcionPush } from "./getSuscripcionPush.js"
2
3export async function cancelaSuscripcionPush() {
4 const suscripcion = await getSuscripcionPush()
5 const resultado = suscripcion === null
6 ? false
7 : await suscripcion.unsubscribe()
8 return resultado === true ? suscripcion : null
9}

4. lib / js / consumeJson.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Espera a que la promesa de un fetch termine. Si
5 * hay error, lanza una excepción. Si no hay error,
6 * interpreta la respuesta del servidor como JSON y
7 * la convierte en una literal de objeto.
8 *
9 * @param { string | Promise<Response> } servicio
10 */
11export async function consumeJson(servicio) {
12
13 if (typeof servicio === "string") {
14 servicio = fetch(servicio, {
15 headers: { "Accept": "application/json, application/problem+json" }
16 })
17 } else if (!(servicio instanceof Promise)) {
18 throw new Error("Servicio de tipo incorrecto.")
19 }
20
21 const respuesta = await servicio
22
23 const headers = respuesta.headers
24
25 if (respuesta.ok) {
26 // Aparentemente el servidor tuvo éxito.
27
28 if (respuesta.status === 204) {
29 // No contiene texto de respuesta.
30
31 return { headers, body: {} }
32
33 } else {
34
35 const texto = await respuesta.text()
36
37 try {
38
39 return { headers, body: JSON.parse(texto) }
40
41 } catch (error) {
42
43 // El contenido no es JSON. Probablemente sea texto de un error.
44 throw new ProblemDetails(respuesta.status, headers, texto,
45 "/error/errorinterno.html")
46
47 }
48
49 }
50
51 } else {
52 // Hay un error.
53
54 const texto = await respuesta.text()
55
56 if (texto === "") {
57
58 // No hay texto. Se usa el texto predeterminado.
59 throw new ProblemDetails(respuesta.status, headers, respuesta.statusText)
60
61 } else {
62 // Debiera se un ProblemDetails en JSON.
63
64 try {
65
66 const { title, type, detail } = JSON.parse(texto)
67
68 throw new ProblemDetails(respuesta.status, headers,
69 typeof title === "string" ? title : respuesta.statusText,
70 typeof type === "string" ? type : undefined,
71 typeof detail === "string" ? detail : undefined)
72
73 } catch (error) {
74
75 if (error instanceof ProblemDetails) {
76 // El error si era un ProblemDetails
77
78 throw error
79
80 } else {
81
82 throw new ProblemDetails(respuesta.status, headers, respuesta.statusText,
83 undefined, texto)
84
85 }
86
87 }
88
89 }
90
91 }
92
93}
94
95// Permite que los eventos de html usen la función.
96window["consumeJson"] = consumeJson

5. lib / js / enviaJson.js

1import { consumeJson } from "./consumeJson.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 consumeJson(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

6. lib / js / getSuscripcionPush.js

1export async function getSuscripcionPush() {
2 // Recupera el service worker registrado.
3 const registro = await navigator.serviceWorker.ready
4 return registro.pushManager.getSubscription()
5}

7. 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
10 if (error === null) {
11
12 console.log("Error")
13 alert("Error")
14
15 } else if (error instanceof ProblemDetails) {
16
17 let mensaje = error.title
18 if (error.detail) {
19 mensaje += `\n\n${error.detail}`
20 }
21 mensaje += `\n\nCódigo: ${error.status}`
22 if (error.type) {
23 mensaje += ` ${error.type}`
24 }
25
26 console.error(mensaje)
27 console.error(error)
28 console.error("Headers:")
29 error.headers.forEach((valor, llave) => console.error(llave, "=", valor))
30 alert(mensaje)
31
32 } else {
33
34 console.error(error)
35 alert(error.message)
36
37 }
38
39}
40
41// Permite que los eventos de html usen la función.
42window["muestraError"] = muestraError

8. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export function muestraObjeto(raizHtml, objeto) {
6
7 for (const [nombre, definiciones] of Object.entries(objeto)) {
8
9 if (Array.isArray(definiciones)) {
10
11 muestraArray(raizHtml, nombre, definiciones)
12
13 } else if (definiciones !== undefined && definiciones !== null) {
14
15 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
16
17 if (elementoHtml instanceof HTMLInputElement) {
18
19 muestraInput(raizHtml, elementoHtml, definiciones)
20
21 } else if (elementoHtml !== null) {
22
23 for (const [atributo, valor] of Object.entries(definiciones)) {
24 if (atributo in elementoHtml) {
25 elementoHtml[atributo] = valor
26 }
27 }
28
29 }
30
31 }
32
33 }
34
35}
36// Permite que los eventos de html usen la función.
37window["muestraObjeto"] = muestraObjeto
38
39/**
40 * @param { Document | HTMLElement } raizHtml
41 * @param { string } nombre
42 */
43export function buscaElementoHtml(raizHtml, nombre) {
44 return raizHtml.querySelector(
45 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
46}
47
48/**
49 * @param { Document | HTMLElement } raizHtml
50 * @param { string } propiedad
51 * @param {any[]} valores
52 */
53function muestraArray(raizHtml, propiedad, valores) {
54
55 const conjunto = new Set(valores)
56 const elementos =
57 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
58
59 if (elementos.length === 1) {
60 const elemento = elementos[0]
61
62 if (elemento instanceof HTMLSelectElement) {
63 const options = elemento.options
64 for (let i = 0, len = options.length; i < len; i++) {
65 const option = options[i]
66 option.selected = conjunto.has(option.value)
67 }
68 return
69 }
70
71 }
72
73 for (let i = 0, len = elementos.length; i < len; i++) {
74 const elemento = elementos[i]
75 if (elemento instanceof HTMLInputElement) {
76 elemento.checked = conjunto.has(elemento.value)
77 }
78 }
79
80}
81
82/**
83 * @param { Document | HTMLElement } raizHtml
84 * @param { HTMLInputElement } input
85 * @param { any } definiciones
86 */
87function muestraInput(raizHtml, input, definiciones) {
88
89 for (const [atributo, valor] of Object.entries(definiciones)) {
90
91 if (atributo == "data-file") {
92
93 const img = getImgParaElementoHtml(raizHtml, input)
94 if (img !== null) {
95 input.dataset.file = valor
96 input.value = ""
97 if (valor === "") {
98 img.src = ""
99 img.hidden = true
100 } else {
101 img.src = valor
102 img.hidden = false
103 }
104 }
105
106 } else if (atributo in input) {
107
108 input[atributo] = valor
109
110 }
111 }
112
113}
114
115/**
116 * @param { Document | HTMLElement } raizHtml
117 * @param { HTMLElement } elementoHtml
118 */
119export function getImgParaElementoHtml(raizHtml, elementoHtml) {
120 const imgId = elementoHtml.getAttribute("data-img")
121 if (imgId === null) {
122 return null
123 } else {
124 const input = buscaElementoHtml(raizHtml, imgId)
125 if (input instanceof HTMLImageElement) {
126 return input
127 } else {
128 return null
129 }
130 }
131}

9. lib / js / ProblemDetails.js

1/** Detalle de los errores devueltos por un servicio. */
2export class ProblemDetails extends Error {
3
4 /**
5 * @param {number} status
6 * @param {Headers} headers
7 * @param {string} title
8 * @param {string} [type]
9 * @param {string} [detail]
10 */
11 constructor(status, headers, title, type, detail) {
12 super(title)
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.headers = headers
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.detail = detail
21 /** @readonly */
22 this.title = title
23 }
24
25}

10. lib / js / suscribeAPush.js

1/**
2 * @param { Uint8Array } applicationServerKey
3 */
4export async function suscribeAPush(applicationServerKey) {
5 // Recupera el service worker registrado.
6 const registro = await navigator.serviceWorker.ready
7 return registro.pushManager.subscribe({
8 userVisibleOnly: true,
9 applicationServerKey
10 })
11}

11. lib / js / urlBase64ToUint8Array.js

1/**
2 * @param { string } base64String
3 */
4export function urlBase64ToUint8Array(base64String) {
5 const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
6 const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/')
7 const rawData = atob(base64)
8 const outputArray = new Uint8Array(rawData.length)
9 for (let i = 0; i < rawData.length; ++i) {
10 outputArray[i] = rawData.charCodeAt(i)
11 }
12 return outputArray
13}

B. Carpeta « lib / php »

Versión para imprimir.

1. lib / php / BAD_REQUEST.php

1<?php
2
3const BAD_REQUEST = 400;
4

2. lib / php / devuelveCreated.php

1<?php
2
3require_once __DIR__ . "/devuelveResultadoNoJson.php";
4
5function devuelveCreated($urlDelNuevo, $resultado)
6{
7
8 $json = json_encode($resultado);
9
10 if ($json === false) {
11
12 devuelveResultadoNoJson();
13 } else {
14
15 http_response_code(201);
16 header("Location: {$urlDelNuevo}");
17 header("Content-Type: application/json");
18 echo $json;
19 }
20}
21

3. lib / php / devuelveErrorInterno.php

1<?php
2
3require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4require_once __DIR__ . "/devuelveProblemDetails.php";
5require_once __DIR__ . "/devuelveProblemDetails.php";
6
7function devuelveErrorInterno(Throwable $error)
8{
9 devuelveProblemDetails(new ProblemDetails(
10 status: INTERNAL_SERVER_ERROR,
11 title: $error->getMessage(),
12 type: "/error/errorinterno.html"
13 ));
14}
15

4. lib / php / devuelveJson.php

1<?php
2
3require_once __DIR__ . "/devuelveResultadoNoJson.php";
4
5function devuelveJson($resultado)
6{
7
8 $json = json_encode($resultado);
9
10 if ($json === false) {
11
12 devuelveResultadoNoJson();
13 } else {
14
15 http_response_code(200);
16 header("Content-Type: application/json");
17 echo $json;
18 }
19}
20

5. lib / php / devuelveNoContent.php

1<?php
2
3function devuelveNoContent()
4{
5 http_response_code(204);
6}
7

6. lib / php / devuelveProblemDetails.php

1<?php
2
3require_once __DIR__ . "/devuelveResultadoNoJson.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6function devuelveProblemDetails(ProblemDetails $details)
7{
8
9 $body = ["title" => $details->title];
10 if ($details->type !== null) {
11 $body["type"] = $details->type;
12 }
13 if ($details->detail !== null) {
14 $body["detail"] = $details->detail;
15 }
16
17 $json = json_encode($body);
18
19 if ($json === false) {
20
21 devuelveResultadoNoJson();
22 } else {
23
24 http_response_code($details->status);
25 header("Content-Type: application/problem+json");
26 echo $json;
27 }
28}
29

7. lib / php / devuelveResultadoNoJson.php

1<?php
2
3require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5function devuelveResultadoNoJson()
6{
7
8 http_response_code(INTERNAL_SERVER_ERROR);
9 header("Content-Type: application/problem+json");
10 echo '{' .
11 '"title": "El resultado no puede representarse como JSON."' .
12 '"type": "/error/resultadonojson.html"' .
13 '}';
14}
15

8. lib / php / fetchAll.php

1<?php
2
3function fetchAll(
4 PDOStatement|false $statement,
5 $parametros = [],
6 int $mode = PDO::FETCH_OBJ,
7 $opcional = null
8): array {
9
10 if ($statement === false) {
11
12 return [];
13 } else {
14
15 if (sizeof($parametros) > 0) {
16 $statement->execute($parametros);
17 }
18
19 $resultado = $opcional === null
20 ? $statement->fetchAll($mode)
21 : $statement->fetchAll($mode, $opcional);
22
23 if ($resultado === false) {
24 return [];
25 } else {
26 return $resultado;
27 }
28 }
29}
30

9. lib / php / INTERNAL_SERVER_ERROR.php

1<?php
2
3const INTERNAL_SERVER_ERROR = 500;

10. lib / php / ProblemDetails.php

1<?php
2
3/** Detalle de los errores devueltos por un servicio. */
4class ProblemDetails extends Exception
5{
6
7 public int $status;
8 public string $title;
9 public ?string $type;
10 public ?string $detail;
11
12 public function __construct(
13 int $status,
14 string $title,
15 ?string $type = null,
16 ?string $detail = null,
17 Throwable $previous = null
18 ) {
19 parent::__construct($title, $status, $previous);
20 $this->status = $status;
21 $this->type = $type;
22 $this->title = $title;
23 $this->detail = $detail;
24 }
25}
26

11. lib / php / recuperaJson.php

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

12. lib / php / validaJson.php

1<?php
2
3require_once __DIR__ . "/BAD_REQUEST.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6function validaJson($objeto)
7{
8
9 if ($objeto === null)
10 throw new ProblemDetails(
11 status: BAD_REQUEST,
12 title: "Los datos recibidos no son JSON.",
13 type: "/error/datosnojson.html",
14 detail: "Los datos recibidos no están en formato JSON.O",
15 );
16
17 return $objeto;
18}
19