1 | export function confirmaEliminar() { |
2 | return confirm("Confirma la eliminación") |
3 | } |
4 | |
5 | // Permite que los eventos de html usen la función. |
6 | window["confirmaEliminar"] = confirmaEliminar |
1 | import { getDataUrlDeSeleccion } from "./muestraObjeto.js" |
2 | |
3 | /** |
4 | * @param {HTMLInputElement} input |
5 | * @param {HTMLImageElement} img |
6 | */ |
7 | export 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. |
30 | window["imagenNuevaSeleccionada"] = imagenNuevaSeleccionada |
1 | import { getDataUrlDeSeleccion } from "./muestraObjeto.js" |
2 | |
3 | /** |
4 | * @param {HTMLInputElement} input |
5 | * @param {HTMLImageElement} img |
6 | */ |
7 | export 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. |
30 | window["imagenSeleccionada"] = imagenSeleccionada |
1 | import { |
2 | JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK |
3 | } from "./JsonResponse.js" |
4 | import { |
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 | */ |
15 | export 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. |
64 | window["invocaServicio"] = invocaServicio |
1 | export const JsonResponse_OK = 200 |
2 | export const JsonResponse_Created = 201 |
3 | export const JsonResponse_NoContent = 204 |
4 | |
5 | export 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 | } |
1 | import { 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 | */ |
8 | export 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. |
31 | window["muestraError"] = muestraError |
1 | /** |
2 | * @param { Document | HTMLElement } raizHtml |
3 | * @param { any } objeto |
4 | */ |
5 | export 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. |
24 | window["muestraObjeto"] = muestraObjeto |
25 | |
26 | /** |
27 | * @param { Document | HTMLElement } raizHtml |
28 | * @param { string } nombre |
29 | */ |
30 | export 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 | */ |
40 | function 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 | */ |
68 | async 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 | */ |
104 | export 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. |
113 | window["getArchivoSeleccionado"] = getArchivoSeleccionado |
114 | |
115 | |
116 | /** |
117 | * @param {HTMLInputElement} input |
118 | * @returns {Promise<string>} |
119 | */ |
120 | export 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. |
145 | window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion |
146 | |
147 | /** |
148 | * @param { Document | HTMLElement } raizHtml |
149 | * @param { HTMLElement } elementoHtml |
150 | */ |
151 | export 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 | } |
1 | export const ProblemDetails_BadRequest = 400 |
2 | export const ProblemDetails_NotFound = 404 |
3 | export const ProblemDetails_InternalServerError = 500 |
4 | |
5 | export 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 | } |
1 | import { 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 | */ |
11 | export 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. |
24 | window["submitForm"] = submitForm |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/JsonResponse.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | /** |
7 | * Ejecuta una funcion que implementa un servicio. |
8 | */ |
9 | function 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 | |
29 | function 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 | |
50 | function 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 | |
69 | function 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 |
1 | <?php |
2 | |
3 | const JsonResponse_OK = 200; |
4 | const JsonResponse_Created = 201; |
5 | const JsonResponse_NoContent = 204; |
6 | |
7 | class 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 |
1 | <?php |
2 | |
3 | function 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 |
1 | <?php |
2 | |
3 | require_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 | */ |
16 | function leeEntero(string $parametro): ?int |
17 | { |
18 | $valor = leeTexto($parametro); |
19 | return $valor === null || $valor === "" |
20 | ? null |
21 | : trim($valor); |
22 | } |
23 |
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 | */ |
8 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/ProblemDetails.php"; |
4 | |
5 | function 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 |
1 | <?php |
2 | |
3 | class 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 |
1 | <?php |
2 | |
3 | function recibeFetchAll(false|array $resultado): array |
4 | { |
5 | if ($resultado === false) { |
6 | return []; |
7 | } else { |
8 | return $resultado; |
9 | } |
10 | } |
11 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/ProblemDetails.php"; |
4 | |
5 | function 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 |