Esta página contiene comentarios ampliados con fragmentos de código PHP de los ejercicios Base de datos (3) Identificación de usuarios.
En todo estos ejercicios, las páginas de gestión de la tabla deben mostrarse únicamente a los usuarios identificados. En este primer ejercicio, la identificación es automática, no se necesita indicar ningún nombre de usuario ni contraseña.
Distribuimos los archivos en varios directorios:
El nombre de la sesión se define para todas las páginas.
// Nombre de sesión
$cfg["sessionName"] = "mclibre-bases-de-datos-3-1"; // Nombre de sesión
Al identificarse correctamente el usuario, establecemos una variable de sesión y redirigimos a la página principal. En este primer ejercicio, la identificación es automática, es decir, al entrar en login.php, la variable de sesión se establece siempre.
session_name($cfg["sessionName"]);
session_start();
$_SESSION["conectado"] = true;
header("Location:../index.php");
Para desconectar al usuario, simplemente destruimos la sesión.
session_name($cfg["sessionName"]);
session_start();
session_destroy();
header("Location:../index.php");
En las páginas que sólo queremos mostrar al usuario identificado, comprobamos que la variable de sesión existe y si no existe, redirigimos a la página principal.
session_name($cfg["sessionName"]);
session_start();
if (!isset($_SESSION["conectado"])) {
header("Location:../index.php");
exit;
}
Al haber organizado los ficheros en directorios, el enlace a la hoja de estilo cambiará, en función de si la página está en un directorio o de si está en el directorio raíz.
<link rel="stylesheet" href="comunes/mclibre-php-proyectos.css" title="Color">
<link rel="stylesheet" href="../comunes/mclibre-php-proyectos.css" title="Color">
Para conseguir enlaces correctos a la hoja de estilo, tendremos que modificar la función cabecera(), incluyendo un nuevo argumento que le indique a la función qué enlace escribir. Ese tercer argumento será vacío o la cadena "../" y se insertará al principio del camino:
function cabecera($texto, $menu, $profundidadDirectorio)
{
...
print " <link rel=\"stylesheet\" href=\"{$profundidadDirectorio}comunes/mclibre-php-proyectos.css\" title=\"Color\">\n";
...
Para ello, definiremos las constantes PROFUNDIDAD_N, que utilizaremos como argumento para la función cabecera().
// Constantes configurables por el programador de la aplicación
define("PROFUNDIDAD_0", ""); // Profundidad de nivel de la página: directorio raíz
define("PROFUNDIDAD_1", "../"); // Profundidad de nivel de la página: subdirectorio
Todas las llamadas a la función cabecera() deberán incluir el tercer argumento:
cabecera("Inicio", MENU_PRINCIPAL, PROFUNDIDAD_0);
cabecera("Personas - Borrar 1", MENU_PERSONAS, PROFUNDIDAD_1);
Definimos la constante MENU_PERSONAS. La constante MENU_PERSONAS corresponderá al menú que se muestra en las páginas relacionadas con la gestión de la tabla personas (insertar, listar, etc.). En este ejercicio no se utiliza el menú MENÚ_VOLVER, pero lo mantenemos porque sí que lo utilizaremos en los ejercicios siguientes.
// Constantes configurables por el programador de la aplicación
define("MENU_PRINCIPAL", 1); // Menú principal sin conectar
define("MENU_VOLVER", 2); // Menú Volver a inicio
define("MENU_PERSONAS", 3); // Menú Personas
La página principal index.php debe mostrar menús distintos, dependiendo de si el usuario está conectado o no. Pero al llamar a la función cabecera, pediremos el MENU_PRINCIPAL sin tener en cuenta si el usuario está o no conectado. Será la función cabecera() la que distinguirá esos casos. Por tanto, todas las páginas que incluyan la función cabecera deberán conectarse a la sesión.
session_name($cfg["sessionName"]);
session_start();
cabecera("Inicio", MENU_PRINCIPAL, PROFUNDIDAD_0);
El usuario que no está conectado sólo puede abrir la página principal index.php y el menú de esa página únicamente debe incluir un enlace (a la página acceso/login.php). Pero si el usuario está conectado, además de que puede abrir cualquier página, al abrir la página principal index.php el menú debe incluir dos enlaces (a la página acceso/logout.php y al índice la páginas de la tabla tabla-personas/index.php). Esto lo podemos conseguir escribiendo bloques if else anidados, comprobando primero si el usuario está o no conectado y en cada caso comprobando el menú solicitado:
if (!isset($_SESSION["conectado"])) {
if ($menu == MENU_PRINCIPAL) {
print " <li><a href=\"acceso/login.php\">Conectarse</a></li>\n";
} else {
print " <li>Error en la selección de menú</li>\n";
}
} else {
if ($menu == MENU_PRINCIPAL) {
print " <li><a href=\"tabla-personas/index.php\">Personas</a></li>\n";
print " <li><a href=\"acceso/logout.php\">Desconectarse</a></li>\n";
} elseif ($menu == MENU_VOLVER) {
print " <li><a href=\"../index.php\">Volver</a></li>\n";
} elseif ($menu == MENU_PERSONAS) {
print " <li><a href=\"../index.php\">Volver</a></li>\n";
print " <li><a href=\"insertar-1.php\">Añadir registro</a></li>\n";
print " <li><a href=\"listar.php\">Listar</a></li>\n";
print " <li><a href=\"borrar-1.php\">Borrar</a></li>\n";
print " <li><a href=\"buscar-1.php\">Buscar</a></li>\n";
print " <li><a href=\"modificar-1.php\">Modificar</a></li>\n";
print " <li><a href=\"borrar-todo-1.php\">Borrar todo</a></li>\n";
} else {
print " <li>Error en la selección de menú</li>\n";
}
}
Todas las páginas de gestión de la tabla de personas pedirán el mismo menú y profundidad de menú que les correspondan (además de conectarse a la sesión, como se ha comentado antes). Por ejemplo, la página listar.php
cabecera("Personas - Listar", MENU_PERSONAS, PROFUNDIDAD_1);
Las imágenes de las flechas (arriba.svg y abajo.svg) ya no se encuentran en el mismo directorio que las páginas, por lo que habrá que escribir el camino correcto hasta ellas. Por ejemplo:
print " <img src=\"../img/abajo.svg\" alt=\"A-Z\" title=\"A-Z\" width=\"15\" height=\"12\">\n";
En este ejercicio, el usuario debe identificarse indicando su nombre y contraseña, pero sólo existe un usuario, con nombre y contraseña fijos. La contraseña de este usuario no se guarda directamente, lo que se guarda es un hash de la contraseña.
Definiremos el usuario único en el fichero de configuración:
// Usuario Administrador de la aplicación
$cfg["rootName"] = "root"; // Nombre del Usuario Administrador de la aplicación
$cfg["rootPassword"] = "4813494d137e1631bba301d5acab6e7bb7aa74ce1185d456565ef51d737677b2"; // Contraseña encriptada del Usuario Administrador de la aplicación
Para el formulario en login-1.php y la comprobación en login-2.php definiremos los tamaños de forma similar a los de la tabla Personas. En este ejercicio no hay una tabla de usuarios, pero en los ejercicios posteriores sí, así que definiremos ya en este ejercicio las constantes siguiendo el esquema de las constantes definidas para la tabla Personas.
Es importante señalar que los tamaños relacionados con el campo Password no siguen el patrón del resto:
// Tamaño de los campos en la tabla Usuarios
$cfg["tablaUsuariosTamUsuario"] = 20; // Tamaño de la columna Usuarios > Nombre de usuario
$cfg["tablaUsuariosTamPassword"] = 64; // Tamaño de la columna Usuarios > Contraseña de usuario (cifrada)
// Tamaño de los controles en los formularios
$cfg["formUsuariosTamUsuario"] = $cfg["tablaUsuariosTamUsuario"]; // Tamaño de la caja de texto Usuarios > Nombre de usuario
$cfg["formUsuariosTamPassword"] = 20; // Tamaño de la caja de texto Usuarios > Contraseña
// Tamaño máximo admitido por los controles en los formularios
$cfg["formUsuariosMaxUsuario"] = $cfg["tablaUsuariosTamUsuario"]; // Tamaño máximo admitido por la caja de texto Usuarios > Nombre de usuario
$cfg["formUsuariosMaxPassword"] = $cfg["formUsuariosTamPassword"]; // Tamaño máximo admitido por la caja de texto Usuarios > Contraseña
El hash se calcula en una función:
function encripta($cadena)
{
global $cfg;
return hash($cfg["hashAlgorithm"], $cadena);
}
El tipo de hash se define en el fichero de configuración:
// Algoritmo hash para encriptar la contraseña de usuario
$cfg["hashAlgorithm"] = "sha256"; // Algoritmo hash para encriptar la contraseña de usuario
// Los posibles algoritmos son https://www.php.net/manual/en/function.hash-algos.php
// Si se cambia el algoritmo puede ser necesario cambiar $cfg["tablaUsuariosTamPassword"]
En este ejercicio el login se divide en dos páginas. La primera página (login-1.php) es un formulario que pide el nombre de usuario y la contraseña. La segunda página (login-2.php) comprueba si los datos introducidos coinciden con los del fichero de configuración.
Para mejorar la usabilidad del formulario, cuando la segunda página detecte un error (usuario y/o contraseña incorrecta), este se mostrará en la página 1.
La página login-1.php incluirá el formulario:
print " <form action=\"login-2.php\" method=\"$cfg[formMethod]\">\n";
print " <p>Escriba su nombre de usuario y contraseña:</p>\n";
print "\n";
print " <table>\n";
print " <tr>\n";
print " <td>Usuario:</td>\n";
print " <td><input type=\"text\" name=\"usuario\" size=\"$cfg[formUsuariosTamUsuario]\" maxlength=\"$cfg[formUsuariosMaxUsuario]\" autofocus/></td>\n";
print " </tr>\n";
print " <tr>\n";
print " <td>Contraseña:</td>\n";
print " <td><input type=\"password\" name=\"password\" size=\"$cfg[formUsuariosTamPassword]\" maxlength=\"$cfg[formUsuariosMaxPassword]\"/></td>\n";
print " </tr>\n";
print " </table>\n";
print "\n";
print " <p>\n";
print " <input type=\"submit\" value=\"Identificar\">\n";
print " <input type=\"reset\" value=\"Borrar\">\n";
print " </p>\n";
print " </form>\n";
La página login-2.php recoge los controles, comprueba que no son demasiado largos, comprueba que el nombre de usuario y la contraseña son root y root y, si todo es correcto, crea la variable de sesión $_SESSION["conectado"].
En caso de detectar un problema en vez de escribir el aviso en la propia página, redirigiremos a la página 1 enviando el aviso en la URL.
$usuario = recoge("usuario");
$password = recoge("password");
// Comprobamos los datos recibidos procedentes de un formulario
$usuarioOk = false;
$passwordOk = false;
if (mb_strlen($usuario, "UTF-8") > $cfg["tablaUsuariosTamUsuario"]) {
header("Location:login-1.php?aviso=El nombre de usuario no puede tener más de $cfg[tablaUsuariosTamUsuario] caracteres.");
} else {
$usuarioOk = true;
}
if (mb_strlen($password, "UTF-8") > $cfg["formUsuariosTamPassword"]) {
header("Location:login-1.php?aviso=La contraseña no puede tener más de $cfg[formUsuariosMaxPassword] caracteres.");
} else {
$passwordOk = true;
}
Para comprobar que se ha escrito el nombre de usuario y la contraseña correcta, comparamos los datos recibidos con las constantes correspondientes. En el caso de la contraseña debemos comparar el hash del dato recibido con la constante correspondiente. Si el usuario o la contraseña no son correctas, redirigimos a la página login-1.php enviando el mensaje del aviso.
// Comprobamos que el usuario recibido con la contraseña recibida existe en la base de datos
$passwordCorrectoOk = false;
if ($usuarioOk && $passwordOk) {
if ($usuario != $cfg["rootName"] || encripta($password) != $cfg["rootPassword"]) {
header("Location:login-1.php?aviso=Error: Nombre de usuario y/o contraseña incorrectos.");
} else {
$passwordCorrectoOk = true;
}
}
Por último, si todo es correcto, creamos la variable de sesión "conectado" y redirigimos a la página principal.
if ($usuarioOk && $passwordOk && $passwordCorrectoOk) {
$_SESSION["conectado"] = true;
header("Location:../index.php");
}
Como la página login-1.php puede recibir mensajes de error desde la página login-2.php, la página login-1.php debe recoger y mostrar en su caso el mensaje recibido encima del formulario.
$aviso = recoge("aviso");
if ($aviso != "") {
print " <p class=\"aviso\">$aviso</p>\n";
print "\n";
}
En este ejercicio se deben poder crear muchos usuarios distintos, con su nombre y contraseña. Todos los usuarios pueden realizar las mismas acciones.
Definimos el número máximo de registros que se podrán guardar en la tabla de usuarios.
$cfg["tablaUsuariosMaxReg"] = 20; // Número máximo de registros en la tabla Usuarios
Modificamos la función borraTodo() para que borre y cree todas las tablas.
Una vez creadas las tablas, insertaremos un registro en la tabla de usuarios para que haya algún usuario que pueda conectarse. Los datos de ese usuario se encuentran como antes en el archivo de configuración. Indicaremos el campo id para asegurar el valor del campo. En este caso, no es algo estrictamente necesario, pero sí que lo sería si insertáramos registros que estuvieran relacionados con registros de otras tablas (como se hace en ejercicios posteriores).
function borraTodo()
{
...
$consulta = "DROP TABLE IF EXISTS $cfg[tablaUsuarios]";
...
$consulta = "DROP TABLE IF EXISTS $cfg[tablaPersonas]";
...
$consulta = "CREATE TABLE $cfg[tablaUsuarios] (
id INTEGER PRIMARY KEY,
usuario VARCHAR($cfg[tablaUsuariosTamUsuario]),
password VARCHAR($cfg[tablaUsuariosTamPassword])
)";
...
$consulta = "INSERT INTO $cfg[tablaUsuarios]
(id, usuario, password)
VALUES (1, '$cfg[rootName]', '$cfg[rootPassword]')";
...
$consulta = "CREATE TABLE $cfg[tablaPersonas] (
id INTEGER PRIMARY KEY,
nombre VARCHAR($cfg[tablaPersonasTamNombre]) COLLATE NOCASE,
apellidos VARCHAR($cfg[tablaPersonasTamApellidos]) COLLATE NOCASE,
telefono VARCHAR($cfg[tablaPersonasTamTelefono]) COLLATE NOCASE,
correo VARCHAR($cfg[tablaPersonasTamCorreo]) COLLATE NOCASE
)";
}
Modificaríamos esta biblioteca de forma similar.
function borraTodo()
{
...
$consulta = "DROP DATABASE IF EXISTS $cfg[mysqlDatabase]";
...
$consulta = "CREATE DATABASE $cfg[mysqlDatabase]
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci";
...
$consulta = "CREATE TABLE $cfg[tablaUsuarios] (
id INTEGER UNSIGNED AUTO_INCREMENT,
usuario VARCHAR($cfg[tablaUsuariosTamUsuario]),
password VARCHAR($cfg[tablaUsuariosTamPassword]),
PRIMARY KEY(id)
)";
...
$consulta = "INSERT INTO $cfg[tablaUsuarios]
(usuario, password)
VALUES ($cfg[rootName]', '$cfg[rootPassword]')";
...
$consulta = "CREATE TABLE $cfg[tablaPersonas] (
id INTEGER UNSIGNED AUTO_INCREMENT,
nombre VARCHAR($cfg[tablaPersonasTamNombre]),
apellidos VARCHAR($cfg[tablaPersonasTamApellidos]),
telefono VARCHAR($cfg[tablaPersonasTamTelefono]),
correo VARCHAR($cfg[tablaPersonasTamCorreo]),
PRIMARY KEY(id)
)";
}
Añadimos la información sobre la tabla de usuarios
// Nombres de las tablas
$cfg["tablaUsuarios"] = "usuarios"; // Nombre de la tabla Usuarios
Definimos los posibles valores de ordenación (aunque en realidad ordenar por contraseña no tiene apenas interés porque los valores son cadenas hash, pero lo implementaremos de todas formas):
// Valores de ordenación de las tablas
$cfg["tablaUsuariosColumnasOrden"] = [
"usuario ASC", "usuario DESC",
"password ASC", "password DESC",
];
Definimos una matriz que contiene los nombres de las tablas.
$cfg["dbTablas"] = [
$cfg["tablaPersonas"],
$cfg["tablaUsuarios"],
];
Esta es la función que detecta si no existe la base de datos en SQLite:
function existenTablas()
{
global $pdo, $cfg;
$existe = true;
foreach ($cfg["dbTablas"] as $tabla) {
$consulta = "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = '$tabla'";
$resultado = $pdo->query($consulta);
if (!$resultado) {
$existe = false;
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
print "\n";
} else {
if ($resultado->fetchColumn() == 0) {
$existe = false;
}
}
}
return $existe;
}
Esta es la función que detecta si no existe la base de datos en MySQL:
function existenTablas()
{
global $pdo, $cfg;
$existe = true;
$consulta = "SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = '$cfg[mysqlDatabase]'";
$resultado = $pdo->query($consulta);
if (!$resultado) {
$existe = false;
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
print "\n";
} else {
if ($resultado->fetchColumn() == 0) {
$existe = false;
} else {
foreach ($cfg["dbTablas"] as $tabla) {
$consulta = "SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = '$cfg[mysqlDatabase]'
AND table_name = '$tabla'";
$resultado = $pdo->query($consulta);
if (!$resultado) {
$existe = false;
print " <p class=\"aviso\">Error en la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
print "\n";
} else {
if ($resultado->fetchColumn() == 0) {
$existe = false;
}
}
}
}
}
return $existe;
}
Al entrar a identificarse, se comprueba si existen la base de datos y las tablas. Si no existen, se crean de nuevo.
if (!existenTablas()) {
print "<p>La base de datos no está creada. Se creará la base de datos.</p>\n";
print "\n";
borraTodo();
}
Para comprobar que se ha escrito el nombre de usuario y la contraseña correcta, comparamos los datos recibidos con los registros de la tabla de Usuario.
// Comprobamos que el usuario recibido con la contraseña recibida existe en la base de datos
$passwordCorrectoOk = false;
if ($usuarioOk && $passwordOk) {
$consulta = "SELECT COUNT(*) FROM $cfg[tablaUsuarios]
WHERE usuario = :usuario
AND password = :password";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
header("Location:login-1.php?aviso=Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}");
} elseif (!$resultado->execute([":usuario" => $usuario, ":password" => encripta($password)])) {
header("Location:login-1.php?aviso=Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}");
} elseif ($resultado->fetchColumn() == 0) {
header("Location:login-1.php?aviso=Error: Nombre de usuario y/o contraseña incorrectos.");
} else {
$passwordCorrectoOk = true;
}
}
Definimos dos nuevos menús:
define("MENU_ADMINISTRADOR", 3); // Menú Administrador
define("MENU_USUARIOS", 4); // Menú Usuarios
define("MENU_PERSONAS", 5); // Menú Personas
Modificamos los menús que se muestran al usuario conectado.
if ($menu == MENU_PRINCIPAL) {
print " <li><a href=\"db/tabla-personas/index.php\">Personas</a></li>\n";
print " <li><a href=\"db/tabla-usuarios/index.php\">Usuarios</a></li>\n";
print " <li><a href=\"administrador/index.php\">Administrador</a></li>\n";
print " <li><a href=\"acceso/logout.php\">Desconectarse</a></li>\n";
} elseif ($menu == MENU_VOLVER) {
print " <li><a href=\"../index.php\">Volver</a></li>\n";
} elseif ($menu == MENU_ADMINISTRADOR) {
print " <li><a href=\"../index.php\">Volver</a></li>\n";
print " <li><a href=\"borrar-todo-1.php\">Borrar todo</a></li>\n";
} elseif ($menu == MENU_USUARIOS) {
print " <li><a href=\"../../index.php\">Volver</a></li>\n";
print " <li><a href=\"insertar-1.php\">Añadir registro</a></li>\n";
print " <li><a href=\"listar.php\">Listar</a></li>\n";
print " <li><a href=\"borrar-1.php\">Borrar</a></li>\n";
print " <li><a href=\"buscar-1.php\">Buscar</a></li>\n";
print " <li><a href=\"modificar-1.php\">Modificar</a></li>\n";
} elseif ($menu == MENU_PERSONAS) {
print " <li><a href=\"../../index.php\">Volver</a></li>\n";
print " <li><a href=\"insertar-1.php\">Añadir registro</a></li>\n";
print " <li><a href=\"listar.php\">Listar</a></li>\n";
print " <li><a href=\"borrar-1.php\">Borrar</a></li>\n";
print " <li><a href=\"buscar-1.php\">Buscar</a></li>\n";
print " <li><a href=\"modificar-1.php\">Modificar</a></li>\n";
} else {
...
Añadimos una variable de configuración para permitir o no modificar la contraseña del usuario Administrador:
$cfg["rootPasswordModificable"] = false; // Contraseña del usuario Administrador se puede cambiar o no
Definimos un nivel más de profundidad:
define("PROFUNDIDAD_0", ""); // Profundidad de nivel de la página: directorio raíz
define("PROFUNDIDAD_1", "../"); // Profundidad de nivel de la página: subdirectorio
define("PROFUNDIDAD_2", "../../"); // Profundidad de nivel de la página: sub-subdirectorio
Para insertar un registro de usuario, el campo nombre de usuario es obligatorio:
if ($usuario == "") {
print " <p class=\"aviso\">Hay que escribir un nombre de usuario.</p>\n";
print "\n";
} elseif (mb_strlen($usuario, "UTF-8") > $cfg["tablaUsuariosMaxUsuario"]) {
print " <p class=\"aviso\">El nombre de usuario no puede tener más de $cfg[formUsuariosMaxUsuario] caracteres.</p>\n";
print "\n";
} else {
$usuarioOk = true;
}
No puede haber dos usuarios con el mismo nombre de usuario:
// Comprobamos que no se intenta crear un registro idéntico a uno que ya existe
$registroDistintoOk = false;
if ($usuarioOk && $passwordOk) {
$consulta = "SELECT COUNT(*) FROM $cfg[tablaUsuarios]
WHERE usuario = :usuario";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":usuario" => $usuario])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif ($resultado->fetchColumn() > 0) {
print " <p class=\"aviso\">Ya existe un usuario con ese nombre.</p>\n";
} else {
$registroDistintoOk = true;
}
}
En la tabla guardamos la contraseña encriptada:
$consulta = "INSERT INTO $cfg[tablaUsuarios]
(usuario, password)
VALUES (:usuario, :password)";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":usuario" => $usuario, ":password" => encripta($password)])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Registro creado correctamente.</p>\n";
}
El usuario inicial no se puede borrar (esto no es estrictamente necesario, pero puede ser útil para evitar que se borren por error todos los usuarios y eso haga imposible entrar en la aplicación):
// Comprobamos que el usuario con el id recibido no es el usuario Administrador inicial
$registroNoRootOk = false;
if ($registroEncontradoOk) {
$consulta = "SELECT COUNT(*) FROM $cfg[tablaUsuarios]
WHERE id = :indice
AND usuario = '$cfg[rootName]'";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":indice" => $indice])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif ($resultado->fetchColumn() > 0) {
print " <p class=\"aviso\">Este usuario no se puede borrar.</p>\n";
} else {
$registroNoRootOk = true;
}
}
No es necesario incluir el campo contraseña en el formulario. Si se decide incluir el campo contraseña, habría que decidir si lo que se incluye en la consulta es el término de búsqueda o el hash del término de búsqueda.
El único motivo para buscar por contraseñas podría ser para ver qué usuarios tienen la contraseña en blanco (o contraseñas triviales como 1234, por ejemplo).
El nombre del Administrador inicial no se puede cambiar (su nombre se establece en la variable de configuración $cfg["rootName"]). Su contraseña se puede cambiar o no dependiendo del valor de la variable de configuración $cfg["rootPasswordModificable"] (true o false). El bloqueo del cambio de contraseña lo he incluido para que la aplicación que tengo incluida en la página de ejercicios de los apuntes esté siempre accesible a los visitantes de la web:
// Comprobamos que el registro con el id recibido no es el registro del usuario root
$registroNoRootOk = false;
if ($idOk && $registroEncontradoOk) {
$consulta = "SELECT * FROM $cfg[tablaUsuarios]
WHERE id = :id";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":id" => $id])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
$registro = $resultado->fetch();
if ($registro["usuario"] == $cfg["rootName"] && !$cfg["rootPasswordModificable"]) {
print " <p class=\"aviso\">Este usuario no se puede modificar.</p>\n";
} else {
$registroNoRootOk = true;
}
}
}
Al cambiar el nombre del usuario hay que comprobar antes que el nuevo nombre no esté ya registrado
// Comprobamos que no se intenta crear un registro idéntico a uno que ya existe
$registroDistintoOk = false;
if ($usuarioOk && $passwordOk && $idOk && $registroEncontradoOk) {
// La consulta cuenta los registros con un id diferente porque MySQL no distingue
// mayúsculas de minúsculas y si en un registro sólo se cambian mayúsculas por
// minúsculas MySQL diría que ya hay un registro como el que se quiere guardar.
$consulta = "SELECT COUNT(*) FROM $cfg[tablaUsuarios]
WHERE usuario = :usuario
AND id <> :id";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":usuario" => $usuario, ":id" => $id])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif ($resultado->fetchColumn() > 0) {
print " <p class=\"aviso\">Ya existe un registro con esos mismos valores. No se ha guardado la modificación.</p>\n";
} else {
$registroDistintoOk = true;
}
}
El nombre del Administrador inicial no se puede cambiar y la contraseña solo puede cambiarse si la variable de configuración $cfg["rootPasswordModificable"] es true.:
// Comprobamos que el usuario con el id recibido no es el usuario Administrador inicial
$registroNoRootOk = false;
if ($usuarioOk && $passwordOk && $idOk && $registroEncontradoOk && $registroDistintoOk) {
$consulta = "SELECT * FROM $cfg[tablaUsuarios]
WHERE id = :id";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":id" => $id])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
$registro = $resultado->fetch();
if ($registro["usuario"] == $cfg["rootName"] && (!$cfg["rootPasswordModificable"] || $registro["usuario"] != $usuario)) {
print " <p class=\"aviso\">Del usuario Administrador inicial sólo se puede cambiar la contraseña.</p>\n";
} else {
$registroNoRootOk = true;
}
}
}
En este ejercicio se deben definir dos categorías de usuarios:
Definimos los niveles de usuario mediante constantes. Los valores son números aunque están definidos como cadenas debido a la manera de funcionar de la función recoge() que trabaja con cadenas en los argumentos default y allowed. Es importante que los niveles vayan en orden numérico creciente porque para comprobar si un usuario tiene nivel suficiente para abrir una página haremos comparaciones con desigualdades (mayor que, etc.).
define("NIVEL_USUARIO_BASICO", "10"); // Usuario web de nivel Usuario Básico
define("NIVEL_ADMINISTRADOR", "20"); // Usuario web de nivel Administrador
Definimos dos matrices con estos niveles. Una es simplemente la lista de valores de niveles que podemos utilizar al recibir valores de un formulario para comprobar que hemos recibido un valor válido. La otra matriz hace corresponder un texto a cada valor de nivel. Esta matriz la utilizaremos para mostrar el texto cuando haya que mostrar los niveles en la pantalla (en un formulario o en un listado).
// Niveles de usuario
$cfg["usuariosNivelesValores"] = [
NIVEL_USUARIO_BASICO,
NIVEL_ADMINISTRADOR,
];
$cfg["usuariosNiveles"] = [
NIVEL_USUARIO_BASICO => "Usuario Básico",
NIVEL_ADMINISTRADOR => "Administrador",
];
Añadimos el campo nivel en los posibles valores de ordenación de la tabla Usuarios.
$cfg["tablaUsuariosColumnasOrden"] = [
"usuario ASC", "usuario DESC",
"password ASC", "password DESC",
"nivel ASC", "nivel DESC",
];
Añadimos un campo más a la tabla que indique el nivel del usuario y al crear el registro de usuario inicial le damos nivel de administrador:
$consulta = "CREATE TABLE $cfg[tablaUsuarios] (
id INTEGER PRIMARY KEY,
usuario VARCHAR($cfg[tablaUsuariosTamUsuario]),
password VARCHAR($cfg[tablaUsuariosTamPassword]),
nivel INTEGER
)";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al crear la tabla Usuarios. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Tabla Usuarios creada correctamente.</p>\n";
$consulta = "INSERT INTO $cfg[tablaUsuarios]
(id, usuario, password, nivel)
VALUES (1, '$cfg[rootName]', '$cfg[rootPassword]', " . NIVEL_ADMINISTRADOR . ")";
...
Añadimos un campo más a la tabla que indique el nivel del usuario y al crear el registro de usuario inicial le damos nivel de administrador:
$consulta = "CREATE TABLE $cfg[tablaUsuarios] (
id INTEGER UNSIGNED AUTO_INCREMENT,
usuario VARCHAR($cfg[tablaUsuariosTamUsuario]),
password VARCHAR($cfg[tablaUsuariosTamPassword]),
nivel INTEGER NOT NULL,
PRIMARY KEY(id)
)";
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al crear la tabla Usuarios. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Tabla creada correctamente.</p>\n";
$consulta = "INSERT INTO $cfg[tablaUsuarios]
(id, usuario, password, nivel)
VALUES (1, '$cfg[rootName]', '$cfg[rootPassword]', " . NIVEL_ADMINISTRADOR . ")";
...
Si el usuario se identifica correctamente, guardamos otra variable de sesión con el nivel del usuario:
$_SESSION["conectado"] = true;
$_SESSION["nivel"] = $registro["nivel"];
session_name($cfg["sessionName"]);
session_start();
if (!isset($_SESSION["conectado"]) || $_SESSION["nivel"] < NIVEL_ADMINISTRADOR) {
header("Location:../../index.php");
exit;
}
En las páginas accesibles a los usuarios básicos y administradores [/db/tabla-personas] comprobamos si la variable de sesión tiene el nivel requerido (aunque realmente sería suficiente con comprobar que existe $_SESSION["conectado"] pero de esta manera explicitamos el nivel requerido, por si en algún momento definimos un nivel inferior al de usuario básico):
session_name($cfg["sessionName"]);
session_start();
if (!isset($_SESSION["conectado"]) || $_SESSION["nivel"] < NIVEL_USUARIO_BASICO) {
header("Location:../../index.php");
exit;
}
En la función cabecera añadimos los menús permitidos al usuario básico:
...
} elseif ($_SESSION["nivel"] == NIVEL_USUARIO_BASICO) {
if ($menu == MENU_PRINCIPAL) {
print " <li><a href=\"db/tabla-personas/index.php\">Personas</a></li>\n";
print " <li><a href=\"acceso/logout.php\">Desconectarse</a></li>\n";
} elseif ($menu == MENU_VOLVER) {
print " <li><a href=\"../index.php\">Volver</a></li>\n";
} elseif ($menu == MENU_PERSONAS) {
print " <li><a href=\"../../index.php\">Volver</a></li>\n";
print " <li><a href=\"insertar-1.php\">Añadir registro</a></li>\n";
print " <li><a href=\"listar.php\">Listar</a></li>\n";
print " <li><a href=\"borrar-1.php\">Borrar</a></li>\n";
print " <li><a href=\"buscar-1.php\">Buscar</a></li>\n";
print " <li><a href=\"modificar-1.php\">Modificar</a></li>\n";
} else {
print " <li>Error en la selección de menú</li>\n";
}
} elseif ($_SESSION["nivel"] == NIVEL_ADMINISTRADOR) {
...
} else {
print " <li>Error en la selección de menú</li>\n";
}
Debemos añadir en las diferentes páginas el campo nivel. Por ejemplo:
El nivel se puede elegir con un cuadro de selección (que incluya automáticamente todos los niveles, por si en el futuro se incluyen más niveles):
print " <tr>\n";
print " <td>Nivel:</td>\n";
print " <td>\n";
print " <select name=\"nivel\">\n";
foreach ($cfg["usuariosNiveles"] as $indice => $valor) {
print " <option value=\"$indice\">$valor</option>\n";
}
print " </select>\n";
print " </td>\n";
print " </tr>\n";
Recogemos el nivel indicando los valores permitidos y dando como valor predeterminado el usuario básico:
$nivel = recoge("nivel", default: NIVEL_USUARIO_BASICO, allowed: $cfg["usuariosNivelesValores"]);
...
// Si todas las comprobaciones han tenido éxito ...
if ($usuarioOk && $passwordOk && $registroDistintoOk && $limiteRegistrosOk) {
// Insertamos el registro en la tabla
$consulta = "INSERT INTO $cfg[tablaUsuarios]
(usuario, password, nivel)
VALUES (:usuario, :password, $nivel)";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":usuario" => $usuario, ":password" => encripta($password)])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Registro creado correctamente.</p>\n";
}
}
Al listar se muestra el texto, no el valor numérico del nivel:
print " <th>\n";
print " <button name=\"ordena\" value=\"nivel ASC\" class=\"boton-invisible\">\n";
print " <img src=\"../../img/abajo.svg\" alt=\"A-Z\" title=\"A-Z\" width=\"15\" height=\"12\">\n";
print " </button>\n";
print " Nivel\n";
print " <button name=\"ordena\" value=\"nivel DESC\" class=\"boton-invisible\">\n";
print " <img src=\"../../img/arriba.svg\" alt=\"Z-A\" title=\"Z-A\" width=\"15\" height=\"12\">\n";
print " </button>\n";
print " </th>\n";
...
foreach ($resultado as $registro) {
print " <tr>\n";
print " <td>$registro[usuario]</td>\n";
print " <td>$registro[password]</td>\n";
print " <td>{$cfg["usuariosNiveles"][$registro["nivel"]]}</td>\n";
print " </tr>\n";
}
...
En el menú de selección del nivel de usuario incluimos como primera opción una opción vacía para indicar que nos da igual el nivel de usuario:
print " <tr>\n";
print " <td>Nivel:</td>\n";
print " <td>\n";
print " <select name=\"nivel\">\n";
print " <option value=\"\"></option>\n";
foreach ($cfg["usuariosNiveles"] as $indice => $valor) {
print " <option value=\"$indice\">$valor</option>\n";
}
print " </select>\n";
print " </td>\n";
print " </tr>\n";
En esta página el nivel puede ser una cadena vacía (que significa que buscamos usuarios con cualquier nivel). El valor cadena vacía debe incluirse en la matriz allowed. Normalmente, los valores permitidos para el nivel son los de la matriz $cfg["usuariosNivelesValores"], así que añadiremos ese valor a la matriz con la función array_merge():
$nivel = recoge("nivel", default: "", allowed: array_merge($cfg["usuariosNivelesValores"], [""]));
Para poder hacer una búsqueda con comodines, algunos sistemas gestores de bases de datos (como PostgreSQL) necesitan que el campo sea de tipo texto. Como nivel es de tipo numérico, debemos hacer la conversión (SQLite o MySQL no la necesitan, pero la admiten):
$consulta = "SELECT * FROM $cfg[tablaUsuarios]
WHERE usuario LIKE :usuario
AND CAST(nivel AS VARCHAR) LIKE '%$nivel%'
ORDER BY $ordena";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":usuario" => "%$usuario%"])) {
...
Al mostrar los valores de los campos, el campo nivel es un menú <select>. Para que se muestre la opción correspondiente al nivel actual del usuario debemos añadir el atributo selected al <option> correspondiente:
foreach ($cfg["usuariosNiveles"] as $indice => $valor) {
if ($registro["nivel"] == $indice) {
print " <option value=\"$indice\" selected>$valor</option>\n";
} else {
print " <option value=\"$indice\">$valor</option>\n";
}
}
Podemos incluir un control para que el usuario indique si quiere incluir registros de prueba en la base de datos ...
print " <p>Por favor, confirme que desea borrar todos los datos de la base de datos.</p>\n";
print "\n";
print " <p>También puede incluir en la base de datos unos datos de prueba.</p>\n";
print "\n";
print " <p><label><input type=\"checkbox\" name=\"demo\" value=\"Sí\"> Incluir datos de prueba</label></p>\n";
print "\n";
print " <p>Haga clic en Sí para borrar todos los datos.</p>\n";
... recogerlo y en caso afirmativo, llamar a una función que incluya los registros de prueba:
$demo = recoge("demo", default: "No", allowed: ["No", "Sí"]);
...
if ($demo == "Sí") {
insertaDemo();
}
Podemos indicar los registros a crear en una matriz e insertarlos recorriendo la matriz. En el ejemplo, los registros incluyen incluso el valor del id. En este caso no es necesario, pero sí lo sería si los registros de una tabla hicieran referencia a los registros de otra tabla, como ocurre en el ejercicio 4, por lo que preparamos ya el código para esa situación.
// Registros de prueba opcionales
$cfg["registrosDemo"] = [
[$cfg["tablaUsuarios"], [2, "usuario1", encripta("usuario1"), NIVEL_USUARIO_BASICO]],
[$cfg["tablaUsuarios"], [3, "usuario2", encripta("usuario2"), NIVEL_USUARIO_BASICO]],
[$cfg["tablaUsuarios"], [4, "admin1", encripta("admin1"), NIVEL_ADMINISTRADOR]],
[$cfg["tablaPersonas"], [1, "Pepito", "Conejo", "271828182", "pepito.conejo@example.com"]],
[$cfg["tablaPersonas"], [2, "Numa", "Nigerio", "161803398", "numa.nigerio@example.com"]],
[$cfg["tablaPersonas"], [3, "Fulanito", "Mengánez", "314159265", "fulanito.menganez@example.com"]],
];
function insertaDemo()
{
global $cfg, $pdo;
print " <p>Insertando registros de prueba ...</p>\n";
print "\n";
foreach ($cfg["registrosDemo"] as $registro) {
if ($registro[0] == $cfg["tablaUsuarios"]) {
$consulta = "INSERT INTO $cfg[tablaUsuarios]
(id, usuario, password, nivel)
VALUES ({$registro[1][0]}, '{$registro[1][1]}', '{$registro[1][2]}', {$registro[1][3]})";
} elseif ($registro[0] == $cfg["tablaPersonas"]) {
$consulta = "INSERT INTO $cfg[tablaPersonas]
(id, nombre, apellidos, telefono, correo)
VALUES ({$registro[1][0]}, '{$registro[1][1]}', '{$registro[1][2]}', '{$registro[1][3]}', '{$registro[1][4]}')";
}
if (!$pdo->query($consulta)) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Registro creado correctamente.</p>\n";
}
print "\n";
}
}
Podemos aplicar la función dirname(), que devuelve el camino padre (en este caso, elimina el nombre del archivo), a la variable de configuración $cfg["sqliteDatabase"]. Y a su resultado la función is_dir(), que indica si el camino es un directorio (es decir, si existe).
function conectaDb()
{
global $cfg;
if (!is_dir(dirname($cfg["sqliteDatabase"]))) {
print " <p class=\"aviso\">Error: El directorio <strong>" . dirname($cfg["sqliteDatabase"]) . "</strong> no está disponible.</p>\n";
exit;
} else {
...
En el formulario incluimos una casilla de verificación ...
print " <tr>\n";
print " <td>Contraseña:</td>\n";
print " <td>\n";
print " <input type=\"text\" name=\"password\" size=\"$cfg[formUsuariosTamPassword]\" maxlength=\"$cfg[formUsuariosMaxPassword]\">\n";
print " <input type=\"checkbox\" name=\"mantenerPassword\" value=\"Sí\"> Mantener contraseña actual\n";
print " </td>\n";
print " </tr>\n";
... recogemos el control y en función de su valor mantenemos o cambiamos la contraseña:
$mantenerPassword = recoge("mantenerPassword", default: "No", allowed: ["No", "Sí"]);
...
// Si todas las comprobaciones han tenido éxito ...
if ($usuarioOk && $passwordOk && $idOk && $registroEncontradoOk && $registroDistintoOk && $registroNoRootOk) {
// Si nos han pedido mantener la contraseña del usuario
if ($mantenerPassword == "Sí") {
// Actualizamos el registro con los datos recibidos (excepto la contraseña)
$consulta = "UPDATE $cfg[tablaUsuarios]
SET usuario = :usuario, nivel = $nivel
WHERE id = :id";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":usuario" => $usuario, ":id" => $id])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Registro modificado correctamente.</p>\n";
}
} else {
// Y si no actualizamos el registro con los datos recibidos (incluida la contraseña)
$consulta = "UPDATE $cfg[tablaUsuarios]
SET usuario = :usuario, password = :password, nivel = $nivel
WHERE id = :id";
$resultado = $pdo->prepare($consulta);
if (!$resultado) {
print " <p class=\"aviso\">Error al preparar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} elseif (!$resultado->execute([":usuario" => $usuario, ":password" => encripta($password), ":id" => $id])) {
print " <p class=\"aviso\">Error al ejecutar la consulta. SQLSTATE[{$pdo->errorCode()}]: {$pdo->errorInfo()[2]}</p>\n";
} else {
print " <p>Registro modificado correctamente.</p>\n";
}
}
}
Modificamos la condición de superación del número máximo de registros para que no se cumpla si la variable de configuración no es mayor que 0:
...
} elseif ($resultado->fetchColumn() >= $cfg["tablaUsuariosMaxReg"] && $cfg["tablaUsuariosMaxReg"] > 0) {
...