| |

Laboratorio: modificar urls para CodeIgniter con mod_rewrite

codeigniter.pngCodeIgniter es uno de los frameworks PHP de los que más se habla últimamente, a mí personalmente me gusta bastante, aunque hay algunas cosas que no son como me gustaría.

Una de ellas es la estructura de las URLs, las URLs siguen el formato:

http://dominio/controlador/metodo/parametro/parametro/...

El controlador es la clase que se encarga de hacer las operaciones y el método es el método de la clase que realiza una función concreta.

Por ejemplo si tenemos una tienda online, podemos tener un controlador para productos y un método que sea editar, con el que se podrá modificar las características del producto. La url sería la siguiente:

http://tienda.com/articulo/editar/cafetera

Con esta url podemos editar un artículo llamado cafetera y además es bastante entendible. Pero, ¿qué pasa si queremos mostrar el artículo cafetera?, pues que la url debería ser la siguiente:

http://tienda.com/articulo/ver/cafetera

Pero lo de ver no queda demasiado bien y queda mejor si es directamente:

http://tienda.com/articulo/cafetera

El problema nos lo encontramos cuando queremos usar una URL que no indique el método y si un parámetro. Si no usamos ni método ni parámetro, CodeIgniter toma por defecto el método index, pero si no usamos método y si parámetro, CodeIgniter no es capaz de saber que lo que mandamos es un parámetro, por lo que hay que usar el Apache para que siga la estructura de CodeIgniter.

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^articulo/(.+)$ index.php/articulo/ver/$1 [L]
RewriteCond $1 !^(index\.php|favicon\.ico)
RewriteRule ^(.*)$ index.php/$1 [L]

Hay que tener en cuenta que este ejemplo es válido únicamente si solo se va a usar siempre dos segmentos en la url, uno para el controlador y otro para el parámetro.

La segunda parte y el uso de index.php es debido a que CodeIgniter usa este script para gestionar toda la aplicación, y para que no aparezca, debemos redireccionarlo todo a index.php, menos los ficheros que existen como el favicon, el robot.txt, etc.

| | |

Laboratorio: indicador de calidad de password

Cuando un usuario se registra en una aplicación web, suele haber casos que cuando se está introduciendo la contraseña, el sistema indica si la password cumple unos mínimos de seguridad o no. En algunas ocasiones suele darnos solo un aviso informativo, pero en otros o cumple todos los requisitos o no admite la contraseña.

En este caso vamos a crear un script que modificará los input password para añadirle una funcionalidad que indique la calidad de la contraseña que se va introduciendo.

password.png

El script añadirá el evento de control de tecla pulsada (onkeyup) y realizará una serie de comprobaciones para conocer la calidad de la contraseña, una contraseña válida será la que cumpla todas las condiciones. Las condiciones que incluimos en el ejemplo son las típicas que suelen pedir: que existan mayúsculas y minúsculas, algún número, caracteres especiales y una longitud mayor de 6.

Para chequear cada condición usaremos expresiones regulares y llevaremos un contador para saber cuántas se cumplen. Cuando hayamos finalizado las comprobaciones calcularemos el porcentaje de calidad y lo indicaremos con una barra de progreso. Esta barra se crecerá según aumente la calidad de la contraseña e irá cambiando de color desde un color rojo para la poca calidad hasta un color verde que indique mucha calidad.

El efecto de barra de progreso lo vamos a realizar de la siguiente manera, tenemos una imagen que va a funcionar como máscara, tendrá dos partes, de igual tamaño y cada parte del mismo tamaño que el ancho del input. Cada mitad tendrá una funcionalidad, la mitad de la derecha servirá para ocultar y la de la izquierda para mostrar, según queramos que se vaya mostrando la barra de progreso, iremos desplazando el fondo hacia la derecha para que se vaya viendo la barra de progreso. Inicialmente la línea discontinua de la imagen que mostramos como ejemplo estará en el lado izquierdo del input.

mascara.gif

El código sería el siguiente:

var __PASSWORD__ = {
colorKO: [255, 0, 0], // Color de contraseña no válida
color50: [127, 127, 0], // Color para el 50% para que no haya un cambio tan brusco
colorOK: [0, 255, 0], // Color de contraseña válida
width: 150,
// Añade el evento onkeyup a la caja input:password y le añade el evento checkpassword
init: function() {
var inputs = document.getElementsByTagName("INPUT");
for (var i=0; i<inputs.length; i++) {
if (inputs[i].type == 'password') {
inputs[i].addEventListener("keyup", __PASSWORD__.checkPassword, false);
inputs[i].className = 'password';
}
}
},
// Obtiene el color porcentual entre un color inicial y otro final, teniendo en cuenta un color intermedio
getColor: function(porc) {
var color = new Array();
var color1 = porc < 50? __PASSWORD__.colorKO:__PASSWORD__.color50;
var color2 = porc < 50? __PASSWORD__.color50:__PASSWORD__.colorOK;
color[0] = parseInt(color1[0]-((color1[0] -color2[0])*porc/100));
color[1] = parseInt(color1[1]-((color1[1] -color2[1])*porc/100));
color[2] = parseInt(color1[2]-((color1[2] -color2[2])*porc/100));
return color;
},
// Comprueba la calidad de la contraseña
checkPassword: function() {
var valor = this.value;
var cont = 0;
var comprobaciones = new Array(
/[A-Z]/, // mayúsculas
/[a-z]/, // minúsculas
/\d/,    // números
/.{6}/,  // más de 6 caracteres
/(\s|\\|\/|!|"|·|\$|%|&|\(|\)|=|\?|¿|\||@|#|¬|€|\^|`|\[|\]|\+|\*|¨|´|\{|\}|\-|_|\.|:|,|;|>|<)/ // Caracteres especiales
);
// Miro todas las condiciones
for (var i=0; i<comprobaciones.length; i++) {
if (valor.match(comprobaciones[i])) {
cont++;
}
}
// Valores posibles en caso de 5 condiciones: 0, 20, 40, 60, 80, 100, pero el valor mínimo tiene que ser 20 para que coincida con el colorKO, ya que si no, al pulsar una tecla ya cambia de color a un paso colorKO+1
var porc = parseInt((cont*100/(comprobaciones.length-1))-(1/(comprobaciones.length-1)*100));
// Desplazo el background tratándolo como una máscara
this.style.backgroundPosition = (parseInt(cont*__PASSWORD__.width/comprobaciones.length)-(__PASSWORD__.width))+"px 0px";
// Cambio el color de fondo
var color = __PASSWORD__.getColor(porc);
if (porc < 0) {
this.style.backgroundColor = '';
} else {
this.style.backgroundColor = 'rgb('+color[0]+', '+color[1]+', '+color[2]+')';
}
}
};
window.addEventListener("load", function() {__PASSWORD__.init();}, false);

Demo

|

Laboratorio: triggers y procedimientos almacenados en MySQL

Unas opciones no muy usadas cuando se trabaja con MySQL son los triggers y los procedimientos almacenados (stored procedures), supongo que a que es algo nuevo en MySQL y la gente no está acostumbrada a estas dos funcionalidades.

Vamos a mostrar un ejemplo de uso de triggers y procedimientos almacenados paso a paso. Quizás el ejemplo no sea el más correcto, o simplemente sea poco útil, pero lo importante es el uso en sí, no el ejemplo.

Nuestro sistema tendrá dos tablas, una con ventas y la otra con comisiones. En la primera se almacenarán cada venta que se realiza en un comercio, y en la segunda las comisiones que le corresponden a cada vendedor en el momento.

CREATE TABLE `ventas` (
`vendedor` int(11),
`producto` int(11),
`importe` float
)
CREATE TABLE `comisiones` (
`vendedor` int(11),
`comision` float
)

Las comisiones se calcula de una forma especial, le corresponde un porcentaje de las ventas según el tipo de producto, y es importante para los vendedores el que se sepa en cada momento qué comisiones lleva ganadas (esto es una justificación para no usar un cron o algo parecido).

Para calcular qué comisión le corresponde a un vendedor, calcularemos los porcentajes para cada tipo de producto vendido y luego lo añadiremos/actualizaremos en la tabla de comisiones. Todo se realizará en un procedimiento almacenado.

DELIMITER $$
DROP PROCEDURE IF EXISTS `test`.`sp_comisiones`$$
CREATE PROCEDURE `test`.`sp_comisiones` (IN mivendedor INT)
BEGIN
DECLARE micomision INT DEFAULT 0;
DECLARE suma INT;
DECLARE existe BOOL;
select IFNULL(sum(importe),0) into suma from ventas where producto = 1 and vendedor = mivendedor;
SET micomision = micomision + (suma * 0.15);
select IFNULL(sum(importe),0) into suma from ventas where producto = 2 and vendedor = mivendedor;
SET micomision = micomision + (suma * 0.1);
select IFNULL(sum(importe),0) into suma from ventas where producto = 3 and vendedor = mivendedor;
SET micomision = micomision + (suma * 0.2);
select count(1)>0 into existe from comisiones where vendedor = mivendedor;
if existe then
UPDATE comisiones set comision = comision+micomision where vendedor = mivendedor;
else
insert into comisiones (vendedor, comision) values (mivendedor, micomision);
end if;
END$$
DELIMITER ;

Ahora, para actualizar los datos de las comisiones usaremos un trigguer, así cuando se haga una venta (insert en la tabla ventas), se llamará al procedimiento almacenado (sp_comisiones), que recalculará la comisión para ese vendedor.

DELIMITER $$
DROP TRIGGER `test`.`tr_ventas_insert`$$
CREATE TRIGGER `test`.`tr_ventas_insert` AFTER INSERT on `test`.`ventas`
FOR EACH ROW BEGIN
call sp_comisiones(new.vendedor);
END$$
DELIMITER ;
|

Laboratorio: navegación por teclado

Ya en varios sitios se ha mostrado como realizar la navegación mediante el teclado, esto significa usar las teclas para acceder a contenidos o para realizar acciones.

Nosotros queremos darle un toque de automatización, que tan solo sea necesario añadir un enlace interno en el documento para acceder a la zona del documento.

Crearemos un script que obtenga todos los enlaces internos del documento y si tienen el atributo rel y su valor tiene el formato “formato:[letra]“, se considerará un objetivo para la navegación por teclado. La letra será la que se usará para acceder y pueden repetirse, por lo que si se repiten las teclas para el shortcut, se irán alternando uno a uno los distintos enlaces.

El script consta de dos partes, uno que se ejecuta después de la carga del documento y que recupera todos los enlaces internos y los agrupa por shortcut, y otro que captura el evento onkeydown del document para acceder a la zona del documento (que será modificando el location).

var __SHORTCUTS__ = {
// Guarda los shortcuts con los enlaces
shortcuts: new Array(),
// Índice del enlace actual organizado por shortcuts
idx_shortcuts: new Array(),
// Obtiene todos los enlaces internos y los que corresponden a un shortcut los almacena
leer_shortcuts: function(e) {
var objs = document.getElementsByTagName("A");
for (var i=0; i<objs.length; i++) {
var rel = objs[i].rel;
if (rel && rel.match(/shortcut:[a-z]/i) && objs[i].name) {
var tipo = (rel.substring(rel.indexOf(":")+1)+"").toUpperCase().substring(0, 1);
if (!__SHORTCUTS__.shortcuts[tipo]) {
__SHORTCUTS__.shortcuts[tipo] = new Array();
__SHORTCUTS__.idx_shortcuts[tipo] = 0;
}
__SHORTCUTS__.shortcuts[tipo][__SHORTCUTS__.shortcuts[tipo].length] = objs[i];
}
}
// Capturo el evento de pulsado de teclado en el document
document.onkeydown = __SHORTCUTS__.controlar_keypress;
},
// Captura una tecla pulsada y accede al shortcut que tenga asociado
controlar_keypress: function(e) {
try {
if (!e) e = event;
var key = e.keyCode;
// Obtengo el caracter correspondiente, quizás esto falle, no he hecho pruebas suficientes
var c = String.fromCharCode(key);
// Si es una letra
if (c.match(/^[a-z]$/i)) {
// Obtengo el enlace para el shortcut
var obj = __SHORTCUTS__.shortcuts[c.toUpperCase()][__SHORTCUTS__.idx_shortcuts[c.toUpperCase()]];
// Acceso a esa parte del documento
document.location = "#"+obj.name;
// Incremento el indice para que acceda al siguiente
__SHORTCUTS__.idx_shortcuts[c.toUpperCase()] = (__SHORTCUTS__.idx_shortcuts[c.toUpperCase()]+1)%__SHORTCUTS__.shortcuts[c.toUpperCase()].length;
}
} catch (e) {} // En IE me da un error que no llego a comprender, ¡cómo no!
}
}
// Cargo el proceso en el onload
if (window.addEventListener) {
window.addEventListener("load", function(event) {__SHORTCUTS__.leer_shortcuts(event);}, false);
} else if (window.attachEvent) {
window.attachEvent("onload", function(event) {__SHORTCUTS__.leer_shortcuts(event);});
} else {
document.onload = function(event) {__SHORTCUTS__.leer_shortcuts(event);}
}

Para indicar qué elementos tienen shortcut, modificamos los estilos para mostrar subrayada la primera letra del elemento, para lo cual el elemento debe tener el atributo title con valor “SHORTCUT: [letra]“. Mejorando así la accesibilidad ya que no añadimos ninguna etiqueta para la primera letra. Por supuesto en IE6 no funciona y con la etiqueta legend tampoco me funciona.

*[title~="SHORTCUT:"]:first-letter {
padding-bottom: 2px;
border-bottom: 1px dotted #000000;
}

Para este ejemplo tenemos dos tipos de shortcuts: ‘P’ para ciertos párrafos y ‘C’ para acceder al formulario de comentarios.

Ejemplo

| |

Laboratorio: no permitir que accedan directamente a las urls empleadas en Ajax

Cuando usamos Ajax en nuestras aplicaciones solemos usar un script específico que devuelva los datos preparados para que el cliente los interprete y los muestre.

En algunas ocasiones no suele ser de nuestro agrado que usuarios accedan directamente a las URLs empleadas para Ajax.

Para evitar esta situación se puede usar un script sencillo. Se tomará en cuenta el valor de la variable de entorno HTTP_REFERER, la cual nos devuelve desde qué página se accede a nuestro script.

$ref = getenv('HTTP_REFERER');

Si el contenido de esta variable es false entonces es que se ha accedido directamente desde el navegador, si no, es que se accede desde una página, también habrá que comprobar que la página es la que nosotros queremos.

<?php
$ref = getenv('HTTP_REFERER');
if (!$ref || $ref != '[nuestra url]') {
?>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"xhtml1-transitional.dtd">
<html>
<head>
<title>Sentido Web - Redireccionamiento Ajax</title>
</head>
<body>
<h1>Sentido Web - Redireccionamiento Ajax</h1>
<p><?php echo "Hola, son las ".date("H:i:s"); ?></p>
</body>
</html>
<?php
} else {
echo "Hola, son las ".date("H:i:s");
}
?>

En este caso mostraremos una página cuando no haya referer o no sea el que nosotros esperamos. También se podría hacer para que el referer estuviera en el servidor, pero eso lo dejo para que lo haga el que lo necesita.

Una funcionalidad parecida se podría usar por temas de accesibilidad, dependiendo de un parámetro que indique si se trata de una llamada Ajax o no, se devuelva la información para el script del cliente o se muestre la información en una página.

<a href="url-ajax.php" onclick="ajax('url-ajax.php?ajax=1')">Enlace</a>

En el caso de haber problemas de accesibilidad, el enlace accede a la página con la información. En caso contrario, ejecuta la funciona ajax(), indicándole que se trata de una llamada Ajax para que nos devuelva la información formateada, ya sea en XML o JSON o como queramos.

|

Laboratorio: mostrar que thumbnails has visitado

Una de las cosas que más me ha gustado de Design Meltdown es que te indica que thumbnails has visitado.

Se trata de lo siguiente, tienes un enlace que contiene una imagen en miniatura que accede a la imagen con el tamaño original (thumbnail). Cuando ya has visitado la imagen el thumbnail cambia y te dice que ya lo has visitado.

imagenes-visitadas.png

Para hacerlo es sencillo, creas una capa con un tamaño específico (el del thumbnail), le indicas mediante estilo dentro de la propia etiqueta HTML la imagen de fondo, que será el thumbnail en sí.

Dentro de la capa incluyes un enlace que se mostrará como un bloque (display: block) y que no mostrará nada, bueno, en este caso si mostrará un texto que referencie a lo que enlaza, pero mediante estilos el texto no se verá, esto lo hacemos así para que los dispositivos que no admitan estilos puedan ver algo.

Modificaremos el estilo de enlace visitado para que muestre como fondo una imagen parcialmente transparente y que muestre el texto “visitada”.

.contenedor-imagen {
width: 150px;
height: 85px;
}
.contenedor-imagen a {
text-indent: -100000px;
display: block;
width: 150px;
height: 85px;
border: 2px solid #FFFFFF;
}
.contenedor-imagen a:hover {
border: 2px solid #FF0000;
}
.contenedor-imagen a:visited {
background: url(visitada.gif);
}
<div class="contenedor-imagen" style="background: url(miniatura.png)">
<a href="http://sentidoweb.com?ejemplo1">Sentido Web</a>
</div>

Ejemplo

| |

FireMarker: marca los textos mediante Firefox

firemarker.pngFireMarker es una extensión para Firefox que hemos desarrollado en Sentido Web que permite marcar partes del texto de la página que estamos visitando.

Tan solo es necesario seleccionar el texto y pinchar en el icono que aparecerá en la barra de estado de nuestro navegador.

firemarker3.png

Una vez realizada esta función, el texto se marcará según los colores que sea hayan seleccionado, que por defecto serán amarillo para el fondo y negro para el color de letra.

firemarker2.png

Los textos marcados seguirán en este estado cuando se refresque la página o se reinicie el navegador, ya que la información marcada se almacena en las preferencias del navegador.

Entre las opciones que nos ofrece esta extensión de Firefox nos encontramos con:

  • Marcar texto de distintos elementos HTML a la vez.
  • Borrar el texto marcado de todas las páginas (también de las que no se están visualizando en ese momento).
  • Borrar el texto marcado de la página actual.
  • Copiar al portapapeles el texto seleccionado de la página actual. En este caso he añadido un retorno de carro después de cada bloque de texto seleccionado.
  • Cambiar el color de fondo y de letra del texto seleccionado.

Si alguien encuentra algún problema o error sería de gran ayuda que nos lo comunicara.

Descargar FireMarker 1.0

|

Post2PDF 1.4: error XSS corregido

Post2PDFNueva versión de Post2PDF, plugin de WordPress para exportar los plugins a formato PDF.
Esta versión corre a cargo de Alexander Concha, quien detectó un error de XSS en el plugin (¡ya me vale!) y además de solucionarlo, le ha dado un buen repaso a todo el plugin mejorándolo en muchos aspectos, por lo cual le estoy muy agradecido, ya que a parte de solucionar un error importante, he podido aprender bastante de su experiencia en WordPress.
Como novedad importante, hay que decir que el plugin solo es compatible con WordPress 2.x, las pruebas realizadas con versiones anteriores no han sido satisfactorias.
Post2PDF

|

Post2PDF 1.3: compatibilidad con WordPress 2.0

Post2PDFNueva versión de Post2PDF, plugin de WordPress para exportar los plugins a formato PDF.
En esta ocasión no era compatible con WordPress 2.x ya que las librerías han sido modificadas y fallaba a la hora de acceder a una de ellas que ya no existe.
Aquellos que quieran usarlo en su WordPress 2.x deben bajarse la última versión, mientras que aquellos que no hayan actualizado su versión de WordPress no es necesario que lo actualicen.
Post2PDF

|

Laboratorio: ejemplo de CAPTCHA

El otro día ví en Rapidshare un ejemplo de CAPTCHA que parecía fácil de desarrollar. Se trata de las letras (tres en este caso), giradas ángulos aleatorios, pero no demasiado para que se pueda leer con facilidad, sobre un fondo de letras de tonalidad parecida, de menor tamaño y también giradas aleatoriamente.

Un ejemplo dinámico es el siguiente:

CAPTCHA

El método es muy sencillo, usamos una cadena de texto con todas las letras que queremos que aparezcan, para poder acceder aleatoriamente a cada una de ellas. Después nos crearemos el fondo de la imagen, siendo un gris claro y unas letras pequeñas, de un gris un poco más oscuro sobre ellas, que llenan toda la imagen. Y por último, creamos las tres letras del CAPTCHA para mostrarlas en el centro de la imagen y de un tamaño considerable.

En este caso he cogido la fuente de daFont, pero se puede usar la que se quiera. Yo recomiendo usar alguna que esté distorsionada para que la lectura para nosotros no presente complicaciones, pero si para algún proceso automático.

El script sería el siguiente, teniendo en cuenta que no se trata el almacenamiento en sesión de las letras del CAPTCHA para luego poder compararlas.

<?php
// Juego de letras para usar
$letras = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
// Configuración tamaño imagen y tamaño fuente
$ancho_caja = 200;
$alto_caja = 100;
$tam_letra = 16;
$tam_letra_grande = 60;
// angulo máximo que rota (izq y der) cada letra
$angmax = 20;
// Establecer el tipo de contenido
header("Content-type: image/png");
// Creamos una imagen
$im = imagecreate($ancho_caja, $alto_caja);
// Creo el color del texto, del texto del fondo y del fondo de la imagen
$gris = ImageColorAllocate($im, 200, 200, 200);
$colorLetra = ImageColorAllocate($im, 175, 175, 175);
$colorLetraFondo = ImageColorAllocate($im, 180, 180, 180);
// tipo de letra obtenido en dafont.net
$fuente = './Hotel Coral Essex.ttf';
// Calculo el número de líneas que entran
$caja_texto = imagettfbbox($tam_letra, 0, $fuente , $letras);
$alto_linea = abs($caja_texto[7]-$caja_texto[1]);
$num_lineas = intval($alto_caja / $alto_linea)+1;
// Dibujo las letras del fondo
// Cada letra de escribe de una en una para poder
// darle una rotación independiente al resto
$pos = 0;
for ($i = 0; $i<$num_lineas; $i++) {
$x = 0;
for ($j = 0; $j<30; $j++) {
$texto_linea = $letras[rand(0, strlen($letras)-1)].' ';
$caja_texto = imagettfbbox($tam_letra, 0, $fuente , $texto_linea);
imagettftext($im, $tam_letra, rand(-$angmax, $angmax), $x, $alto_linea*$i, $colorLetraFondo, $fuente , $texto_linea);
// Posicion x de la siguiente letra
$x += $caja_texto[2] - $caja_texto[0];
}
}
// Escribo las tres letras del CAPTCHA
$res = $letras[rand(0, strlen($letras)-1)];
$ang1 = rand(-$angmax, $angmax);
$caja_texto = imagettfbbox($tam_letra_grande, $ang1, $fuente , $res);
$y1 = abs($caja_texto[7]-$caja_texto[1]);
$x1 = abs($caja_texto[2]-$caja_texto[0]);
$res .= $letras[rand(0, strlen($letras)-1)];
$ang2 = rand(-$angmax, $angmax);
$caja_texto = imagettfbbox($tam_letra_grande, $ang2, $fuente , $res[1]);
$y2 = abs($caja_texto[7]-$caja_texto[1]);
$x2 = abs($caja_texto[2]-$caja_texto[0]);
$res .= $letras[rand(0, strlen($letras)-1)];
$ang3 = rand(-$angmax, $angmax);
$caja_texto = imagettfbbox($tam_letra_grande, $ang3, $fuente , $res[2]);
$y3 = abs($caja_texto[7]-$caja_texto[1]);
$x3 = abs($caja_texto[2]-$caja_texto[0]);
imagettftext($im, $tam_letra_grande, $ang1, ($ancho_caja/2)-(($x1+$x2+$x3)/2), $y1+($alto_caja-$y1)/2, $colorLetra, $fuente , $res[0]);
imagettftext($im, $tam_letra_grande, $ang2, ($ancho_caja/2)-(($x1+$x2+$x3)/2)+($x1), $y2+($alto_caja-$y2)/2, $colorLetra, $fuente , $res[1]);
imagettftext($im, $tam_letra_grande, $ang3, ($ancho_caja/2)-(($x1+$x2+$x3)/2)+($x1+$x2), $y3+($alto_caja-$y3)/2, $colorLetra, $fuente , $res[2]);
imagepng($im);
imagedestroy($im);
imagedestroy($im2);
?> 

Un ejemplo lo podéis ver en la misma imagen que aparece arriba, ya que es dinámica y se modifica al recargar la página.

Código