En esta lección se amplía el ejemplo de la lección anterior para controlar el acceso a una app.
Puedes probar el ejemplo en https://srvaut.rf.gd/.
Prueba con los siguientes usarios y contraseñas:
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.
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
.
Prueba el ejemplo en https://srvaut.rf.gd/.
Descarga el archivo /src/srvaut.zip y descompáctalo.
Crea tu proyecto en GitHub:
Crea una cuenta de email, por ejemplo, pepito@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo pepito.
Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.
En la página Create a new repository introduce los siguientes datos:
Proporciona el nombre de tu repositorio debajo de donde dice Repository name *.
Mantén la selección Public para que otros usuarios puedan ver tu proyecto.
Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.
Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.
Cliquea Create repository.
Importa el proyecto en GitHub:
En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.
En Visual Studio Code, usa el botón de la izquierda para Source Control.
Cliquea el botón Clone Repository.
Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.
Selecciona la carpeta donde se guardará la carpeta del proyecto.
Abre la carpeta del proyecto importado.
Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.
Edita los archivos que desees.
Haz clic derecho en index.html
, selecciona
PHP Server: serve project y se abre el navegador para que puedas
probar localmente el ejemplo.
Para depurar paso a paso haz lo siguiente:
En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.
Recarga la página, de preferencia haciendo clic derecho en el ícono de volver a cargar la página y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página . Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.
Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).
Selecciona el archivo donde vas a empezar a depurar.
Haz clic en el número de la línea donde vas a empezar a depurar.
En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.
Haz clic en Run and Debug .
Si no está configurada la depuración, haz clic en create a launch json file.
Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe decir Listen for Xdebug .
Aparece un cuadro con los controles de depuración
Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.
Regresa al navegador, recarga la página y empieza a usarla.
Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.
Sube el proyecto al hosting que elijas sin incluir el archivo
.htaccess
. En algunos casos puedes usar filezilla
(https://filezilla-project.org/)
En algunos host como InfinityFree, tienes que configurar el certificado SSL.
En algunos host como InfinityFree, debes subir el
archivo .htaccess
cuando el certificado SSL se ha creado e
instalado. Sirve para forzar el uso de https.
Abre un navegador y prueba el proyecto en tu hosting.
En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.
Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.
Haz clic en los triángulos para expandir las carpetas
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/sesion-actual.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> |
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/consumeJson.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/sesion-actual.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="consumeJson('srv/logout.php') |
59 | .then(json => location.reload()) |
60 | .catch(muestraError)"> |
61 | Terminar sesión |
62 | </button> |
63 | |
64 | </p> |
65 | |
66 | </body> |
67 | |
68 | </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/sesion-actual.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/login.php', event) |
26 | .then(sesion => location.href = 'perfil.html') |
27 | .catch(muestraError)"> |
28 | |
29 | <h1>Iniciar Sesión</h1> |
30 | |
31 | <p><a href="perfil.html">Cancelar</a></p> |
32 | |
33 | <p> |
34 | <label> |
35 | Cue |
36 | <input name="cue"> |
37 | </label> |
38 | </p> |
39 | |
40 | <p> |
41 | <label> |
42 | Match |
43 | <input type="password" name="match"> |
44 | </label> |
45 | </p> |
46 | |
47 | <p><button type="submit">Iniciar sesión</button></p> |
48 | |
49 | </form> |
50 | |
51 | </body> |
52 | |
53 | </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/consumeJson.js"></script> |
12 | <script type="module" src="lib/js/muestraError.js"></script> |
13 | <script type="module" src="./js/ROL_ID_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 |
20 | onload="protege('srv/sesion-actual.php', [ROL_ID_ADMINISTRADOR], 'index.html') |
21 | .then(sesion => { |
22 | nav.sesion = sesion |
23 | main.hidden = false |
24 | }) |
25 | .catch(muestraError)"> |
26 | |
27 | <mi-nav id="nav"></mi-nav> |
28 | |
29 | <main id="main" hidden> |
30 | |
31 | <h1>Solo Administradores</h1> |
32 | |
33 | <p>Hola.</p> |
34 | |
35 | <p> |
36 | <button type="button" onclick="consumeJson('srv/saludo-cliente.php') |
37 | .then(saludo => alert(saludo.body)) |
38 | .catch(muestraError)"> |
39 | Ejecuta servicio |
40 | </button> |
41 | </p> |
42 | |
43 | </main> |
44 | |
45 | </body> |
46 | |
47 | </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/consumeJson.js"></script> |
12 | <script type="module" src="lib/js/muestraError.js"></script> |
13 | <script type="module" src="./js/ROL_ID_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/sesion-actual.php', [ROL_ID_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="consumeJson('srv/saludo-cliente.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> |
1 | export const CUE = "cue" |
1 | import { consumeJson } from "../lib/js/consumeJson.js" |
2 | import { exportaAHtml } from "../lib/js/exportaAHtml.js" |
3 | import { Sesion } from "./Sesion.js" |
4 | |
5 | /** |
6 | * @param {string} servicio |
7 | * @param {string[]} [rolIdsPermitidos] |
8 | * @param {string} [urlDeProtección] |
9 | */ |
10 | export async function protege(servicio, rolIdsPermitidos, urlDeProtección) { |
11 | const respuesta = await consumeJson(servicio) |
12 | const sesion = new Sesion(respuesta.body) |
13 | if (rolIdsPermitidos === undefined) { |
14 | return sesion |
15 | } else { |
16 | const rolIds = sesion.rolIds |
17 | for (const rolId of rolIdsPermitidos) { |
18 | if (rolIds.has(rolId)) { |
19 | return sesion |
20 | } |
21 | } |
22 | if (urlDeProtección !== undefined) { |
23 | location.href = urlDeProtección |
24 | } |
25 | throw new Error("No autorizado.") |
26 | } |
27 | } |
28 | |
29 | exportaAHtml(protege) |
1 | export const ROL_IDS = "rolIds" |
1 | export const ROL_ID_ADMINISTRADOR = "Administrador" |
2 | |
3 | // Permite que los eventos de html usen la constante. |
4 | window["ROL_ID_ADMINISTRADOR"] = ROL_ID_ADMINISTRADOR |
1 | export const ROL_ID_CLIENTE = "Cliente" |
2 | |
3 | // Permite que los eventos de html usen la constante. |
4 | window["ROL_ID_CLIENTE"] = ROL_ID_CLIENTE |
1 | import { exportaAHtml } from "../lib/js/exportaAHtml.js" |
2 | import { CUE } from "./CUE.js" |
3 | import { ROL_IDS } from "./ROL_IDS.js" |
4 | |
5 | export class Sesion { |
6 | |
7 | /** |
8 | * @param { any } objeto |
9 | */ |
10 | constructor(objeto) { |
11 | |
12 | /** |
13 | * @readonly |
14 | */ |
15 | this.cue = objeto[CUE] |
16 | if (typeof this.cue !== "string") |
17 | throw new Error("cue debe ser string.") |
18 | |
19 | /** |
20 | * @readonly |
21 | */ |
22 | const rolIds = objeto[ROL_IDS] |
23 | if (!Array.isArray(rolIds)) |
24 | throw new Error("rolIds debe ser arreglo.") |
25 | /** |
26 | * @readonly |
27 | */ |
28 | this.rolIds = new Set(rolIds) |
29 | |
30 | } |
31 | |
32 | } |
33 | |
34 | exportaAHtml(Sesion) |
1 | import { htmlentities } from "../../lib/js/htmlentities.js" |
2 | import { Sesion } from "../Sesion.js" |
3 | import { ROL_ID_ADMINISTRADOR } from "../ROL_ID_ADMINISTRADOR.js" |
4 | import { ROL_ID_CLIENTE } from "../ROL_ID_CLIENTE.js" |
5 | |
6 | export 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_ID_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_ID_CLIENTE) ? |
59 | /* html */ `<li><a href="cliente.html">Para clientes</a></li>` |
60 | : "" |
61 | } |
62 | } |
63 | |
64 | customElements.define("mi-nav", MiNav) |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/selectFirst.php"; |
4 | require_once __DIR__ . "/../lib/php/insert.php"; |
5 | require_once __DIR__ . "/../lib/php/insertBridges.php"; |
6 | require_once __DIR__ . "/../lib/php/insert.php"; |
7 | require_once __DIR__ . "/TABLA_USUARIO.php"; |
8 | require_once __DIR__ . "/TABLA_ROL.php"; |
9 | require_once __DIR__ . "/TABLA_USU_ROL.php"; |
10 | require_once __DIR__ . "/ROL_ID_CLIENTE.php"; |
11 | require_once __DIR__ . "/ROL_ID_ADMINISTRADOR.php"; |
12 | |
13 | class Bd |
14 | { |
15 | |
16 | private static ?PDO $pdo = null; |
17 | |
18 | static function pdo(): PDO |
19 | { |
20 | if (self::$pdo === null) { |
21 | |
22 | self::$pdo = new PDO( |
23 | // cadena de conexión |
24 | "sqlite:srvaut.db", |
25 | // usuario |
26 | null, |
27 | // contraseña |
28 | null, |
29 | // Opciones: pdos no persistentes y lanza excepciones. |
30 | [PDO::ATTR_PERSISTENT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] |
31 | ); |
32 | |
33 | self::$pdo->exec( |
34 | 'CREATE TABLE IF NOT EXISTS USUARIO ( |
35 | USU_ID INTEGER, |
36 | USU_CUE TEXT NOT NULL, |
37 | USU_MATCH TEXT NOT NULL, |
38 | CONSTRAINT USU_PK |
39 | PRIMARY KEY(USU_ID), |
40 | CONSTRAINT USU_CUE_UNQ |
41 | UNIQUE(USU_CUE), |
42 | CONSTRAINT USU_CUE_NV |
43 | CHECK(LENGTH(USU_CUE) > 0) |
44 | )' |
45 | ); |
46 | self::$pdo->exec( |
47 | 'CREATE TABLE IF NOT EXISTS ROL ( |
48 | ROL_ID TEXT NOT NULL, |
49 | ROL_DESCRIPCION TEXT NOT NULL, |
50 | CONSTRAINT ROL_PK |
51 | PRIMARY KEY(ROL_ID), |
52 | CONSTRAINT ROL_ID_NV |
53 | CHECK(LENGTH(ROL_ID) > 0), |
54 | CONSTRAINT ROL_DESCR_UNQ |
55 | UNIQUE(ROL_DESCRIPCION), |
56 | CONSTRAINT ROL_DESCR_NV |
57 | CHECK(LENGTH(ROL_DESCRIPCION) > 0) |
58 | )' |
59 | ); |
60 | self::$pdo->exec( |
61 | 'CREATE TABLE IF NOT EXISTS USU_ROL ( |
62 | USU_ID INTEGER NOT NULL, |
63 | ROL_ID TEXT NOT NULL, |
64 | CONSTRAINT USU_ROL_PK |
65 | PRIMARY KEY(USU_ID, ROL_ID), |
66 | CONSTRAINT USU_ROL_USU_FK |
67 | FOREIGN KEY (USU_ID) REFERENCES USUARIO(USU_ID), |
68 | CONSTRAINT USU_ROL_ROL_FK |
69 | FOREIGN KEY (ROL_ID) REFERENCES ROL(ROL_ID) |
70 | )' |
71 | ); |
72 | |
73 | if (selectFirst( |
74 | pdo: self::$pdo, |
75 | from: ROL, |
76 | where: [ROL_ID => ROL_ID_ADMINISTRADOR] |
77 | ) === false) { |
78 | insert( |
79 | pdo: self::$pdo, |
80 | into: ROL, |
81 | values: [ |
82 | ROL_ID => ROL_ID_ADMINISTRADOR, |
83 | ROL_DESCRIPCION => "Administra el sistema." |
84 | ] |
85 | ); |
86 | } |
87 | |
88 | if (selectFirst(self::$pdo, ROL, [ROL_ID => ROL_ID_CLIENTE]) === false) { |
89 | insert( |
90 | pdo: self::$pdo, |
91 | into: ROL, |
92 | values: [ |
93 | ROL_ID => ROL_ID_CLIENTE, |
94 | ROL_DESCRIPCION => "Realiza compras." |
95 | ] |
96 | ); |
97 | } |
98 | } |
99 | |
100 | if (selectFirst(self::$pdo, USUARIO, [USU_CUE => "pepito"]) === false) { |
101 | insert( |
102 | pdo: self::$pdo, |
103 | into: USUARIO, |
104 | values: [ |
105 | USU_CUE => "pepito", |
106 | USU_MATCH => password_hash("cuentos", PASSWORD_DEFAULT) |
107 | ] |
108 | ); |
109 | $usuId = self::$pdo->lastInsertId(); |
110 | insertBridges( |
111 | pdo: self::$pdo, |
112 | into: USU_ROL, |
113 | valuesDePadre: [USU_ID => $usuId], |
114 | valueDeHijos: [ROL_ID => [ROL_ID_CLIENTE]] |
115 | ); |
116 | } |
117 | |
118 | if (selectFirst(self::$pdo, USUARIO, [USU_CUE => "susana"]) === false) { |
119 | insert( |
120 | pdo: self::$pdo, |
121 | into: USUARIO, |
122 | values: [ |
123 | USU_CUE => "susana", |
124 | USU_MATCH => password_hash("alegria", PASSWORD_DEFAULT) |
125 | ] |
126 | ); |
127 | $usuId = self::$pdo->lastInsertId(); |
128 | insertBridges( |
129 | pdo: self::$pdo, |
130 | into: USU_ROL, |
131 | valuesDePadre: [USU_ID => $usuId], |
132 | valueDeHijos: [ROL_ID => [ROL_ID_ADMINISTRADOR]] |
133 | ); |
134 | } |
135 | |
136 | if (selectFirst(self::$pdo, USUARIO, [USU_CUE => "bebe"]) === false) { |
137 | insert( |
138 | pdo: self::$pdo, |
139 | into: USUARIO, |
140 | values: [ |
141 | USU_CUE => "bebe", |
142 | USU_MATCH => password_hash("saurio", PASSWORD_DEFAULT) |
143 | ] |
144 | ); |
145 | $usuId = self::$pdo->lastInsertId(); |
146 | insertBridges( |
147 | pdo: self::$pdo, |
148 | into: USU_ROL, |
149 | valuesDePadre: [USU_ID => $usuId], |
150 | valueDeHijos: [ROL_ID => [ROL_ID_ADMINISTRADOR, ROL_ID_CLIENTE]] |
151 | ); |
152 | } |
153 | |
154 | return self::$pdo; |
155 | } |
156 | } |
157 |
1 | <?php |
2 | |
3 | const CUE = "cue"; |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
5 | require_once __DIR__ . "/../lib/php/recuperaTexto.php"; |
6 | require_once __DIR__ . "/../lib/php/validaCue.php"; |
7 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
8 | require_once __DIR__ . "/../lib/php/selectFirst.php"; |
9 | require_once __DIR__ . "/../lib/php/fetchAll.php"; |
10 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
11 | require_once __DIR__ . "/CUE.php"; |
12 | require_once __DIR__ . "/ROL_IDS.php"; |
13 | require_once __DIR__ . "/Bd.php"; |
14 | require_once __DIR__ . "/TABLA_USUARIO.php"; |
15 | require_once __DIR__ . "/protege.php"; |
16 | |
17 | ejecutaServicio(function () { |
18 | |
19 | $sesion = protege(); |
20 | |
21 | if ($sesion->cue !== "") |
22 | throw new ProblemDetails( |
23 | status: NO_AUTORIZADO, |
24 | type: "/error/sesioniniciada.html", |
25 | title: "Sesión iniciada.", |
26 | detail: "La sesión ya está iniciada.", |
27 | ); |
28 | |
29 | $cue = recuperaTexto("cue"); |
30 | $match = recuperaTexto("match"); |
31 | |
32 | $cue = validaCue($cue); |
33 | |
34 | if ($match === false) |
35 | throw new ProblemDetails( |
36 | status: BAD_REQUEST, |
37 | title: "Falta el match.", |
38 | type: "/error/faltamatch.html", |
39 | detail: "La solicitud no tiene el valor de match.", |
40 | ); |
41 | |
42 | if ($match === "") |
43 | throw new ProblemDetails( |
44 | status: BAD_REQUEST, |
45 | title: "Match en blanco.", |
46 | type: "/error/matchenblanco.html", |
47 | detail: "Pon texto en el campo match.", |
48 | ); |
49 | |
50 | $pdo = Bd::pdo(); |
51 | |
52 | $usuario = |
53 | selectFirst(pdo: $pdo, from: USUARIO, where: [USU_CUE => $cue]); |
54 | |
55 | if ($usuario === false || !password_verify($match, $usuario[USU_MATCH])) |
56 | throw new ProblemDetails( |
57 | status: BAD_REQUEST, |
58 | type: "/error/datosincorrectos.html", |
59 | title: "Datos incorrectos.", |
60 | detail: "El cue y/o el match proporcionados son incorrectos.", |
61 | ); |
62 | |
63 | $rolIds = fetchAll( |
64 | $pdo->query( |
65 | "SELECT ROL_ID |
66 | FROM USU_ROL |
67 | WHERE USU_ID = :USU_ID |
68 | ORDER BY USU_ID" |
69 | ), |
70 | [":USU_ID" => $usuario[USU_ID]], |
71 | PDO::FETCH_COLUMN |
72 | ); |
73 | |
74 | $_SESSION[CUE] = $cue; |
75 | $_SESSION[ROL_IDS] = $rolIds; |
76 | devuelveJson([ |
77 | CUE => $cue, |
78 | ROL_IDS => $rolIds |
79 | ]); |
80 | }); |
81 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
5 | require_once __DIR__ . "/CUE.php"; |
6 | require_once __DIR__ . "/ROL_IDS.php"; |
7 | |
8 | ejecutaServicio(function () { |
9 | |
10 | session_start(); |
11 | |
12 | if (isset($_SESSION[CUE])) { |
13 | unset($_SESSION[CUE]); |
14 | } |
15 | if (isset($_SESSION[ROL_IDS])) { |
16 | unset($_SESSION[ROL_IDS]); |
17 | } |
18 | |
19 | session_destroy(); |
20 | |
21 | devuelveJson([]); |
22 | }); |
23 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
4 | require_once __DIR__ . "/CUE.php"; |
5 | require_once __DIR__ . "/ROL_IDS.php"; |
6 | require_once __DIR__ . "/ROL_ID_CLIENTE.php"; |
7 | require_once __DIR__ . "/Sesion.php"; |
8 | |
9 | const NO_AUTORIZADO = 401; |
10 | |
11 | function protege(?array $rolIdsPermitidos = null) |
12 | { |
13 | |
14 | session_start(); |
15 | |
16 | $cue = isset($_SESSION[CUE]) ? $_SESSION[CUE] : ""; |
17 | $rolIds = isset($_SESSION[ROL_IDS]) ? $_SESSION[ROL_IDS] : []; |
18 | $sesion = new Sesion($cue, $rolIds); |
19 | |
20 | if ($rolIdsPermitidos === null) { |
21 | |
22 | return $sesion; |
23 | } else { |
24 | |
25 | foreach ($rolIdsPermitidos as $rolId) { |
26 | if (array_search($rolId, $rolIds, true) !== false) { |
27 | return $sesion; |
28 | } |
29 | } |
30 | |
31 | throw new ProblemDetails( |
32 | status: NO_AUTORIZADO, |
33 | type: "/error/noautorizado.html", |
34 | title: "No autorizado.", |
35 | detail: "No está autorizado para usar este recurso.", |
36 | ); |
37 | } |
38 | } |
39 |
1 | <?php |
2 | |
3 | const ROL_IDS = "rolIds"; |
1 | <?php |
2 | |
3 | const ROL_ID_ADMINISTRADOR = "Administrador"; |
4 |
1 | <?php |
2 | |
3 | const ROL_ID_CLIENTE = "Cliente"; |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
5 | require_once __DIR__ . "/ROL_ID_CLIENTE.php"; |
6 | require_once __DIR__ . "/protege.php"; |
7 | |
8 | ejecutaServicio(function () { |
9 | $sesion = protege([ROL_ID_CLIENTE]); |
10 | devuelveJson("Hola " . $sesion->cue); |
11 | }); |
12 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
5 | require_once __DIR__ . "/protege.php"; |
6 | |
7 | ejecutaServicio(function () { |
8 | devuelveJson(protege()); |
9 | }); |
10 |
1 | <?php |
2 | |
3 | class 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 |
1 | <?php |
2 | |
3 | const ROL = "ROL"; |
4 | const ROL_ID = "ROL_ID"; |
5 | const ROL_DESCRIPCION = "ROL_DESCRIPCION"; |
6 |
1 | <?php |
2 | |
3 | const USUARIO = "USUARIO"; |
4 | const USU_ID = "USU_ID"; |
5 | const USU_CUE = "USU_CUE"; |
6 | const USU_MATCH = "USU_MATCH"; |
7 |
1 | <?php |
2 | |
3 | const USU_ROL = "USU_ROL"; |
4 |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Espera a que la promesa de un fetch termine. Si |
6 | * hay error, lanza una excepción. Si no hay error, |
7 | * interpreta la respuesta del servidor como JSON y |
8 | * la convierte en una literal de objeto. |
9 | * |
10 | * @param { string | Promise<Response> } servicio |
11 | */ |
12 | export async function consumeJson(servicio) { |
13 | |
14 | if (typeof servicio === "string") { |
15 | servicio = fetch(servicio, { |
16 | headers: { "Accept": "application/json, application/problem+json" } |
17 | }) |
18 | } else if (!(servicio instanceof Promise)) { |
19 | throw new Error("Servicio de tipo incorrecto.") |
20 | } |
21 | |
22 | const respuesta = await servicio |
23 | |
24 | const headers = respuesta.headers |
25 | |
26 | if (respuesta.ok) { |
27 | // Aparentemente el servidor tuvo éxito. |
28 | |
29 | if (respuesta.status === 204) { |
30 | // No contiene texto de respuesta. |
31 | |
32 | return { headers, body: {} } |
33 | |
34 | } else { |
35 | |
36 | const texto = await respuesta.text() |
37 | |
38 | try { |
39 | |
40 | return { headers, body: JSON.parse(texto) } |
41 | |
42 | } catch (error) { |
43 | |
44 | // El contenido no es JSON. Probablemente sea texto de un error. |
45 | throw new ProblemDetails(respuesta.status, headers, texto, |
46 | "/error/errorinterno.html") |
47 | |
48 | } |
49 | |
50 | } |
51 | |
52 | } else { |
53 | // Hay un error. |
54 | |
55 | const texto = await respuesta.text() |
56 | |
57 | if (texto === "") { |
58 | |
59 | // No hay texto. Se usa el texto predeterminado. |
60 | throw new ProblemDetails(respuesta.status, headers, respuesta.statusText) |
61 | |
62 | } else { |
63 | // Debiera se un ProblemDetails en JSON. |
64 | |
65 | try { |
66 | |
67 | const { title, type, detail } = JSON.parse(texto) |
68 | |
69 | throw new ProblemDetails(respuesta.status, headers, |
70 | typeof title === "string" ? title : respuesta.statusText, |
71 | typeof type === "string" ? type : undefined, |
72 | typeof detail === "string" ? detail : undefined) |
73 | |
74 | } catch (error) { |
75 | |
76 | if (error instanceof ProblemDetails) { |
77 | // El error si era un ProblemDetails |
78 | |
79 | throw error |
80 | |
81 | } else { |
82 | |
83 | throw new ProblemDetails(respuesta.status, headers, respuesta.statusText, |
84 | undefined, texto) |
85 | |
86 | } |
87 | |
88 | } |
89 | |
90 | } |
91 | |
92 | } |
93 | |
94 | } |
95 | |
96 | exportaAHtml(consumeJson) |
1 | /** |
2 | * Permite que los eventos de html usen la función. |
3 | * @param {function} functionInstance |
4 | */ |
5 | export function exportaAHtml(functionInstance) { |
6 | window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance |
7 | } |
8 | |
9 | /** |
10 | * @param {function} valor |
11 | */ |
12 | export function nombreDeFuncionParaHtml(valor) { |
13 | const names = valor.name.split(/\s+/g) |
14 | return names[names.length - 1] |
15 | } |
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 | */ |
8 | export function htmlentities(texto) { |
9 | return texto.replace(/[<>"']/g, textoDetectado => { |
10 | switch (textoDetectado) { |
11 | case "<": return "<" |
12 | case ">": return ">" |
13 | case '"': return """ |
14 | case "'": return "'" |
15 | default: return textoDetectado |
16 | } |
17 | }) |
18 | } |
19 |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Muestra un error en la consola y en un cuadro de |
6 | * alerta el mensaje de una excepción. |
7 | * @param { ProblemDetails | Error | null } error descripción del error. |
8 | */ |
9 | export function muestraError(error) { |
10 | |
11 | if (error === null) { |
12 | |
13 | console.error("Error") |
14 | alert("Error") |
15 | |
16 | } else if (error instanceof ProblemDetails) { |
17 | |
18 | let mensaje = error.title |
19 | if (error.detail) { |
20 | mensaje += `\n\n${error.detail}` |
21 | } |
22 | mensaje += `\n\nCódigo: ${error.status}` |
23 | if (error.type) { |
24 | mensaje += ` ${error.type}` |
25 | } |
26 | |
27 | console.error(mensaje) |
28 | console.error(error) |
29 | console.error("Headers:") |
30 | error.headers.forEach((valor, llave) => console.error(llave, "=", valor)) |
31 | alert(mensaje) |
32 | |
33 | } else { |
34 | |
35 | console.error(error) |
36 | alert(error.message) |
37 | |
38 | } |
39 | |
40 | } |
41 | |
42 | exportaAHtml(muestraError) |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | |
3 | /** |
4 | * @param { Document | HTMLElement } raizHtml |
5 | * @param { any } objeto |
6 | */ |
7 | export function muestraObjeto(raizHtml, objeto) { |
8 | |
9 | for (const [nombre, definiciones] of Object.entries(objeto)) { |
10 | |
11 | if (Array.isArray(definiciones)) { |
12 | |
13 | muestraArray(raizHtml, nombre, definiciones) |
14 | |
15 | } else if (definiciones !== undefined && definiciones !== null) { |
16 | |
17 | const elementoHtml = buscaElementoHtml(raizHtml, nombre) |
18 | |
19 | if (elementoHtml instanceof HTMLInputElement) { |
20 | |
21 | muestraInput(raizHtml, elementoHtml, definiciones) |
22 | |
23 | } else if (elementoHtml !== null) { |
24 | |
25 | for (const [atributo, valor] of Object.entries(definiciones)) { |
26 | if (atributo in elementoHtml) { |
27 | elementoHtml[atributo] = valor |
28 | } |
29 | } |
30 | |
31 | } |
32 | |
33 | } |
34 | |
35 | } |
36 | |
37 | } |
38 | exportaAHtml(muestraObjeto) |
39 | |
40 | /** |
41 | * @param { Document | HTMLElement } raizHtml |
42 | * @param { string } nombre |
43 | */ |
44 | export function buscaElementoHtml(raizHtml, nombre) { |
45 | return raizHtml.querySelector( |
46 | `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`) |
47 | } |
48 | |
49 | /** |
50 | * @param { Document | HTMLElement } raizHtml |
51 | * @param { string } propiedad |
52 | * @param {any[]} valores |
53 | */ |
54 | function muestraArray(raizHtml, propiedad, valores) { |
55 | |
56 | const conjunto = new Set(valores) |
57 | const elementos = |
58 | raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`) |
59 | |
60 | if (elementos.length === 1) { |
61 | const elemento = elementos[0] |
62 | |
63 | if (elemento instanceof HTMLSelectElement) { |
64 | const options = elemento.options |
65 | for (let i = 0, len = options.length; i < len; i++) { |
66 | const option = options[i] |
67 | option.selected = conjunto.has(option.value) |
68 | } |
69 | return |
70 | } |
71 | |
72 | } |
73 | |
74 | for (let i = 0, len = elementos.length; i < len; i++) { |
75 | const elemento = elementos[i] |
76 | if (elemento instanceof HTMLInputElement) { |
77 | elemento.checked = conjunto.has(elemento.value) |
78 | } |
79 | } |
80 | |
81 | } |
82 | |
83 | /** |
84 | * @param { Document | HTMLElement } raizHtml |
85 | * @param { HTMLInputElement } input |
86 | * @param { any } definiciones |
87 | */ |
88 | function muestraInput(raizHtml, input, definiciones) { |
89 | |
90 | for (const [atributo, valor] of Object.entries(definiciones)) { |
91 | |
92 | if (atributo == "data-file") { |
93 | |
94 | const img = getImgParaElementoHtml(raizHtml, input) |
95 | if (img !== null) { |
96 | input.dataset.file = valor |
97 | input.value = "" |
98 | if (valor === "") { |
99 | img.src = "" |
100 | img.hidden = true |
101 | } else { |
102 | img.src = valor |
103 | img.hidden = false |
104 | } |
105 | } |
106 | |
107 | } else if (atributo in input) { |
108 | |
109 | input[atributo] = valor |
110 | |
111 | } |
112 | } |
113 | |
114 | } |
115 | |
116 | /** |
117 | * @param { Document | HTMLElement } raizHtml |
118 | * @param { HTMLElement } elementoHtml |
119 | */ |
120 | export function getImgParaElementoHtml(raizHtml, elementoHtml) { |
121 | const imgId = elementoHtml.getAttribute("data-img") |
122 | if (imgId === null) { |
123 | return null |
124 | } else { |
125 | const input = buscaElementoHtml(raizHtml, imgId) |
126 | if (input instanceof HTMLImageElement) { |
127 | return input |
128 | } else { |
129 | return null |
130 | } |
131 | } |
132 | } |
1 | /** |
2 | * Detalle de los errores devueltos por un servicio. |
3 | */ |
4 | export class ProblemDetails extends Error { |
5 | |
6 | /** |
7 | * @param {number} status |
8 | * @param {Headers} headers |
9 | * @param {string} title |
10 | * @param {string} [type] |
11 | * @param {string} [detail] |
12 | */ |
13 | constructor(status, headers, title, type, detail) { |
14 | super(title) |
15 | /** |
16 | * @readonly |
17 | */ |
18 | this.status = status |
19 | /** |
20 | * @readonly |
21 | */ |
22 | this.headers = headers |
23 | /** |
24 | * @readonly |
25 | */ |
26 | this.type = type |
27 | /** |
28 | * @readonly |
29 | */ |
30 | this.detail = detail |
31 | /** |
32 | * @readonly |
33 | */ |
34 | this.title = title |
35 | } |
36 | |
37 | } |
1 | import { consumeJson } from "./consumeJson.js" |
2 | import { exportaAHtml } from "./exportaAHtml.js" |
3 | |
4 | /** |
5 | * Envía los datos de la forma a la url usando la codificación |
6 | * multipart/form-data. |
7 | * @param {string} url |
8 | * @param {Event} event |
9 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
10 | * | "CONNECT" | "HEAD" } metodoHttp |
11 | */ |
12 | export function submitForm(url, event, metodoHttp = "POST") { |
13 | |
14 | event.preventDefault() |
15 | |
16 | const form = event.target |
17 | |
18 | if (!(form instanceof HTMLFormElement)) |
19 | throw new Error("event.target no es un elemento de tipo form.") |
20 | |
21 | return consumeJson(fetch(url, { |
22 | method: metodoHttp, |
23 | headers: { "Accept": "application/json, application/problem+json" }, |
24 | body: new FormData(form) |
25 | })) |
26 | |
27 | } |
28 | |
29 | exportaAHtml(submitForm) |
1 | <?php |
2 | |
3 | const BAD_REQUEST = 400; |
4 |
1 | <?php |
2 | |
3 | function calculaArregloDeParametros(array $arreglo) |
4 | { |
5 | $parametros = []; |
6 | foreach ($arreglo as $llave => $valor) { |
7 | $parametros[":$llave"] = $valor; |
8 | } |
9 | return $parametros; |
10 | } |
11 |
1 | <?php |
2 | |
3 | function calculaSqlDeAsignaciones(string $separador, array $arreglo) |
4 | { |
5 | $primerElemento = true; |
6 | $sqlDeAsignacion = ""; |
7 | foreach ($arreglo as $llave => $valor) { |
8 | $sqlDeAsignacion .= |
9 | ($primerElemento === true ? "" : $separador) . "$llave=:$llave"; |
10 | $primerElemento = false; |
11 | } |
12 | return $sqlDeAsignacion; |
13 | } |
14 |
1 | <?php |
2 | |
3 | function calculaSqlDeCamposDeInsert(array $values) |
4 | { |
5 | $primerCampo = true; |
6 | $sqlDeCampos = ""; |
7 | foreach ($values as $nombreDeValue => $valorDeValue) { |
8 | $sqlDeCampos .= ($primerCampo === true ? "" : ",") . "$nombreDeValue"; |
9 | $primerCampo = false; |
10 | } |
11 | return $sqlDeCampos; |
12 | } |
13 |
1 | <?php |
2 | |
3 | function calculaSqlDeValues(array $values) |
4 | { |
5 | $primerValue = true; |
6 | $sqlDeValues = ""; |
7 | foreach ($values as $nombreDeValue => $valorDeValue) { |
8 | $sqlDeValues .= ($primerValue === true ? "" : ",") . ":$nombreDeValue"; |
9 | $primerValue = false; |
10 | } |
11 | return $sqlDeValues; |
12 | } |
13 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
4 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
5 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
6 | |
7 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | |
5 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
4 | |
5 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/ProblemDetails.php"; |
4 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
5 | require_once __DIR__ . "/devuelveErrorInterno.php"; |
6 | |
7 | function ejecutaServicio(callable $codigo) |
8 | { |
9 | try { |
10 | $codigo(); |
11 | } catch (ProblemDetails $details) { |
12 | devuelveProblemDetails($details); |
13 | } catch (Throwable $error) { |
14 | devuelveErrorInterno($error); |
15 | } |
16 | } |
17 |
1 | <?php |
2 | |
3 | function fetch( |
4 | PDOStatement|false $statement, |
5 | $parametros = [], |
6 | int $mode = PDO::FETCH_ASSOC, |
7 | $opcional = null |
8 | ) { |
9 | |
10 | if ($statement === false) { |
11 | |
12 | return false; |
13 | } else { |
14 | |
15 | if (sizeof($parametros) > 0) { |
16 | $statement->execute($parametros); |
17 | } |
18 | |
19 | if ($opcional === null) { |
20 | return $statement->fetch($mode); |
21 | } else { |
22 | $statement->setFetchMode($mode, $opcional); |
23 | return $statement->fetch(); |
24 | } |
25 | } |
26 | } |
27 |
1 | <?php |
2 | |
3 | function fetchAll( |
4 | PDOStatement|false $statement, |
5 | $parametros = [], |
6 | int $mode = PDO::FETCH_ASSOC, |
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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaSqlDeCamposDeInsert.php"; |
4 | require_once __DIR__ . "/calculaSqlDeValues.php"; |
5 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
6 | |
7 | function insert(PDO $pdo, string $into, array $values) |
8 | { |
9 | $sqlDeCampos = calculaSqlDeCamposDeInsert($values); |
10 | $sqlDeValues = calculaSqlDeValues($values); |
11 | $sql = "INSERT INTO $into ($sqlDeCampos) VALUES ($sqlDeValues)"; |
12 | $parametros = calculaArregloDeParametros($values); |
13 | $pdo->prepare($sql)->execute($parametros); |
14 | } |
15 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaSqlDeCamposDeInsert.php"; |
4 | require_once __DIR__ . "/calculaSqlDeValues.php"; |
5 | |
6 | function insertBridges( |
7 | PDO $pdo, |
8 | string $into, |
9 | array $valuesDePadre, |
10 | array $valueDeHijos |
11 | ) { |
12 | if (sizeof($valueDeHijos) > 0) { |
13 | $sqlDeCamposDePadre = calculaSqlDeCamposDeInsert($valuesDePadre); |
14 | $sqlDeCampoDeHijos = calculaSqlDeCamposDeInsert($valueDeHijos); |
15 | $sqlDeValuesDePadre = calculaSqlDeValues($valuesDePadre); |
16 | $sqlDeValueDeHijos = calculaSqlDeValues($valueDeHijos); |
17 | $insert = $pdo->prepare( |
18 | "INSERT INTO $into ($sqlDeCamposDePadre, $sqlDeCampoDeHijos) |
19 | VALUES ($sqlDeValuesDePadre, $sqlDeValueDeHijos)" |
20 | ); |
21 | $parametros = calculaArregloDeParametros($valuesDePadre); |
22 | foreach ($valueDeHijos as $nombreDeValueDeHijo => $valoresDeValueDeHijo) { |
23 | foreach ($valoresDeValueDeHijo as $valorDeValueDeHijo) { |
24 | $parametros[":$nombreDeValueDeHijo"] = $valorDeValueDeHijo; |
25 | $insert->execute($parametros); |
26 | } |
27 | break; |
28 | } |
29 | } |
30 | } |
31 |
1 | <?php |
2 | |
3 | const INTERNAL_SERVER_ERROR = 500; |
1 | <?php |
2 | |
3 | /** Detalle de los errores devueltos por un servicio. */ |
4 | class 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 |
1 | <?php |
2 | |
3 | /** |
4 | * Recupera el texto de un parámetro enviado al |
5 | * servidor por medio de GET, POST o cookie. |
6 | * |
7 | * Si el parámetro no se recibe, devuelve false. |
8 | */ |
9 | function recuperaTexto(string $parametro): false|string |
10 | { |
11 | /* Si el parámetro está asignado en $_REQUEST, |
12 | * devuelve su valor; de lo contrario, devuelve false. |
13 | */ |
14 | $valor = isset($_REQUEST[$parametro]) |
15 | ? $_REQUEST[$parametro] |
16 | : false; |
17 | return $valor; |
18 | } |
19 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/fetch.php"; |
4 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
5 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
6 | |
7 | function selectFirst( |
8 | PDO $pdo, |
9 | string $from, |
10 | array $where = [], |
11 | string $orderBy = "", |
12 | int $mode = PDO::FETCH_ASSOC, |
13 | $opcional = null |
14 | ) { |
15 | $sql = "SELECT * FROM $from"; |
16 | |
17 | if (sizeof($where) > 0) { |
18 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
19 | $sql .= " WHERE $sqlDeWhere"; |
20 | } |
21 | |
22 | if ($orderBy !== "") { |
23 | $sql .= " ORDER BY $orderBy"; |
24 | } |
25 | |
26 | if (sizeof($where) === 0) { |
27 | $statement = $pdo->query($sql); |
28 | return fetch($statement, [], $mode, $opcional); |
29 | } else { |
30 | $statement = $pdo->prepare($sql); |
31 | $parametros = calculaArregloDeParametros($where); |
32 | return fetch($statement, $parametros, $mode, $opcional); |
33 | } |
34 | } |
35 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function validaCue($cue) |
7 | { |
8 | |
9 | if ($cue === false) |
10 | throw new ProblemDetails( |
11 | status: BAD_REQUEST, |
12 | title: "Falta el cue.", |
13 | type: "/error/faltacue.html", |
14 | detail: "La solicitud no tiene el valor de cue.", |
15 | ); |
16 | |
17 | $trimCue = trim($cue); |
18 | |
19 | if ($trimCue === "") |
20 | throw new ProblemDetails( |
21 | status: BAD_REQUEST, |
22 | title: "Cue en blanco.", |
23 | type: "/error/cuenblanco.html", |
24 | detail: "Pon texto en el campo cue.", |
25 | ); |
26 | |
27 | return $trimCue; |
28 | } |
29 |
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>Cue en blanco</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Cue en blanco</h1> |
16 | |
17 | <h1>Pon texto en el campo cue.</h1> |
18 | |
19 | </body> |
20 | |
21 | </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> |
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> |
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 | <p>La solicitud no tiene el valor de cue.</p> |
18 | |
19 | </body> |
20 | |
21 | </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> |
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>La solicitud no tiene el valor de id.</p> |
18 | |
19 | </body> |
20 | |
21 | </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 | <p>La solicitud no tiene el valor de match.</p> |
18 | |
19 | </body> |
20 | |
21 | </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 | <p>La solicitud no tiene el valor de nombre.</p> |
18 | |
19 | </body> |
20 | |
21 | </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>Id en blanco</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Id en blanco</h1> |
16 | |
17 | </body> |
18 | |
19 | </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>Match en blanco</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Match en blanco</h1> |
16 | |
17 | <h1>Pon texto en el campo match.</h1> |
18 | |
19 | </body> |
20 | |
21 | </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> |
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> |
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>Nombre en blanco</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Nombre en blanco</h1> |
16 | |
17 | <p>Pon texto en el campo nombre.</p> |
18 | |
19 | </body> |
20 | |
21 | </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> |
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 resultado no puede representarse como JSON</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>El resultado no puede representarse como JSON</h1> |
16 | |
17 | <p> |
18 | Debido a un error interno del servidor, el resultado generado, no se puede |
19 | recuperar. |
20 | </p> |
21 | |
22 | </body> |
23 | |
24 | </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> |
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> |
1 | RewriteEngine On |
2 | RewriteCond %{HTTP:X-Forwarded-Proto} !https |
3 | RewriteCond %{HTTPS} off |
4 | RewriteCond %{HTTP:CF-Visitor} !{"scheme":"https"} |
5 | RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] |
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": "Node16", |
7 | "moduleResolution": "Node16", |
8 | "lib": [ |
9 | "ES2017", |
10 | "WebWorker", |
11 | "DOM" |
12 | ] |
13 | } |
14 | } |
En esta lección se amplió el ejemplo de la lección anterior para controlar el acceso a una app.