Skip to content

7. Caso de estudio: gestión de una base de datos de artículos en la web

Los códigos de este caso de estudio están disponibles |ICI|.

Objetivos:

  • escribir una clase para administrar una base de datos de artículos
  • escribir una aplicación web basada en esta clase
  • introducir las hojas de estilo
  • proponer un primer paso en la metodología de desarrollo para aplicaciones web sencillas
  • introducir JavaScript en el navegador del usuario

Créditos: La esencia de este caso de estudio se encuentra en el libro «Les cahiers du programmeur - PHP/MySQL», de Jean-Philippe Leboeuf, publicado por la editorial Eyrolles.

7.1. Introducción

Un comerciante desea administrar los artículos que vende en su tienda. Ya cuenta con una aplicación ACCESS que realiza esta tarea, pero le atrae la idea de aventurarse en la web. Tiene una cuenta con un proveedor de acceso a Internet que permite a sus clientes instalar scripts PHP en sus carpetas personales. Esto les permite crear sitios web dinámicos. Además, estos mismos clientes cuentan con una cuenta MySQL que les permite crear tablas que pueden proporcionar datos a sus scripts PHP. Así, el comerciante cuenta con una cuenta MySQL con el nombre de usuario «adarticles» y la contraseña «mdparticles». Posee una base de datos «dbarticles» sobre la cual tiene todos los derechos. Por lo tanto, nuestro comerciante cuenta con los elementos suficientes para publicar su sistema de gestión de artículos en la web. Con la ayuda de ustedes, que cuentan con conocimientos en desarrollo web, se lanza a la aventura.

7.2. La base de datos

Nuestro comerciante diseña el siguiente boceto de la interfaz web de inicio que le gustaría tener:

Image

Habría dos tipos de usuarios:

  • administradores que podrían realizar todas las acciones en la tabla de artículos (agregar, modificar, eliminar, consultar, etc.). Estos podrán utilizar todos los elementos del menú anterior. En particular, podrán emitir cualquier consulta SQL a través de la opción [Requête SQL].
  • los usuarios normales (que no son administradores), quienes tendrían derechos restringidos: derechos para agregar, modificar, eliminar y consultar. Es posible que solo tengan algunos de estos derechos; por ejemplo, únicamente el derecho de consulta.

Dado que existen diversos tipos de usuarios de la base de datos que no tienen los mismos derechos, es necesario realizar una autenticación. Por eso, la página de inicio comienza con este paso. Para saber quién es quién y quién tiene derecho a hacer qué, se utilizarán dos tablas: USERS y DROITS. La tabla USERS tendría la siguiente estructura:

login
El nombre de usuario que identifica al usuario de manera única. Este campo es la clave primaria de la tabla.
mdp
la contraseña sin cifrar del usuario
admin 
el carácter «y» (sí) si el usuario es administrador; de lo contrario, el carácter «n» (no).

El contenido de la tabla podría ser el siguiente:

Image

La tabla DROITS especifica los derechos que tienen los usuarios que no son administradores y que figuran en la tabla USERS. Su estructura es la siguiente:

login
El nombre de usuario que lo identifica de manera única.
Este campo es una clave externa de la tabla DROITS y hace referencia
a la columna «login» de la tabla USERS.
table
el nombre de la tabla sobre la que el usuario tiene derechos.
ajouter
el carácter «y» (sí) si el usuario tiene derecho a agregar datos en la tabla,
de lo contrario, el carácter «n» (no).
modifier
derecho de modificación: «y» o «n»
supprimer
derecho a eliminar: «y» o «n»
consulter
derecho de consulta: 's' o 'n'

El contenido de la tabla podría ser el siguiente:

Image

Notas:

  • Un usuario U que figure en la tabla USERS y no aparezca en la tabla DROITS no tiene ningún derecho.
  • En nuestro ejemplo, los usuarios solo tendrán acceso a una tabla, la tabla ARTICLES. Sin embargo, nuestro comerciante, previsor, ha agregado el campo «tabla» a la estructura de la tabla DROITS para tener la posibilidad de agregar nuevas tablas a su aplicación en el futuro.
  • ¿Por qué administrar derechos en nuestras propias tablas si partimos de la hipótesis de que utilizaremos una base de datos MySQL que es capaz por sí misma (y mejor que nosotros) de administrar esos derechos en sus propias tablas? Simplemente porque nuestro comerciante no cuenta con los derechos de administración sobre la base de datos MySQL que le permitirían crear usuarios y otorgarles derechos. No olvidemos, de hecho, que la base de datos MySQL está alojada en un proveedor de acceso y que el comerciante es solo un simple usuario de la misma sin ningún derecho de administración (afortunadamente). Sin embargo, sí cuenta con todos los derechos sobre una base de datos llamada dbarticles, a la que accede por el momento con el nombre de usuario admarticles y la contraseña mdparticles. Es en esta base de datos donde se encuentran todas las tablas de la aplicación.

La tabla ARTICLES recopila la información sobre los artículos vendidos por el comerciante. Su estructura es la siguiente:

code
código del artículo — clave primaria de la tabla
- exactamente 4 caracteres
nom
nombre del artículo
prix
su precio
stockActuel
nivel actual de existencias
stockMinimum
el nivel por debajo del cual se debe realizar un pedido
de reabastecimiento

Su contenido, que se utilizará inicialmente como prueba, podría ser el siguiente:

Image

7.3. Las limitaciones del proyecto

El comerciante está migrando aquí una aplicación local ACCESS a una aplicación web. No sabe en qué se convertirá esta ni cómo evolucionará. Sin embargo, le gustaría que la nueva aplicación fuera fácil de usar y escalable. Por esta razón, su asesor de TI imaginó para él, al diseñar las tablas, que podría haber:

  • varios usuarios con diferentes derechos: esto le permitirá al comerciante delegar ciertas tareas a otras personas sin necesidad de otorgarles derechos de administración
  • en el futuro, otras tablas además de la tabla ARTICLES

El mismo asesor hace otras propuestas:

  • sabe que, en el desarrollo de software, es necesario separar claramente las capas de presentación de las de procesamiento. La arquitectura de una aplicación web suele ser la siguiente:

La interfaz de usuario aquí es un navegador web, pero también podría ser una aplicación independiente que, a través de la red, enviara solicitudes HTTP al servicio web y diera formato a los resultados que este le envía. La lógica de la aplicación está formada por los scripts que procesan las solicitudes del usuario, en este caso los scripts PHP. La fuente de datos suele ser una base de datos, pero también puede ser un directorio LDAP o un servicio web remoto. Es recomendable que el desarrollador mantenga una gran independencia entre estas tres entidades, de modo que, si una de ellas cambia, las otras dos no tengan que cambiar, o solo lo hagan mínimamente. El asesor de TI del comerciante propone entonces lo siguiente:

  • Se colocará la lógica de negocio de la aplicación en una clase PHP. De este modo, el bloque [Logique applicative] anterior estará compuesto por los siguientes elementos:

En el bloque [Logique Applicative], se podrá distinguir

  • el bloque [IE=Interface d'Entrée], que es la puerta de entrada a la aplicación. Es la misma independientemente del tipo de cliente.
  • el bloque [Classes métier], que agrupa las clases necesarias para la lógica de la aplicación. Estas son independientes del cliente.
  • el bloque de generadores de páginas de respuesta [IS1 IS2 ... IS=Interface de Sortie]. Cada generador se encarga de dar formato a los resultados proporcionados por la lógica de la aplicación para un tipo específico de cliente: código HTML para un navegador o un teléfono (WAP), código XML para una aplicación autónoma, etc.

Este modelo garantiza una buena independencia respecto a los clientes. Ya sea que cambie el cliente o que se desee modificar la forma de presentar los resultados, serán los generadores de salida [IS] los que habrá que crear o adaptar.

  • En una aplicación web, la independencia entre la capa de presentación y la capa de procesamiento puede mejorarse mediante el uso de hojas de estilo. Estas controlan la presentación de una página web dentro de un navegador. Para cambiar esta presentación, basta con modificar la hoja de estilo asociada. No es necesario modificar la lógica de procesamiento. Por lo tanto, aquí se utilizará una hoja de estilo.
  • En el diagrama anterior, es la clase de negocio la que servirá de interfaz con la fuente de datos. Suponemos que esta fuente es, en este caso, una base de datos MySQL. Para permitir una migración a otra base de datos, utilizaremos la biblioteca PEAR, que ofrece clases de acceso a bases de datos independientes del tipo real de estas. De este modo, si nuestro comerciante prospera hasta el punto de poder instalar un servidor web IIS de Microsoft en su empresa, podrá reemplazar la base MySQL por SQL Server sin tener que modificar (o con muy pocos cambios) la clase de negocio.

7.4. La clase «artículos»

La clase de artículos podría definirse de la siguiente manera:

<?php

     // clase de artículos que opera sobre una base de datos de artículos compuesta por las siguientes tablas
     // artículos: (código, nombre, precio, stockActuel, stockMinimum)
     // usuarios: (nombre de usuario, contraseña, administrador)
     // permisos: (nombre de usuario, tabla, agregar, modificar, eliminar, consultar)

     //: este es el usuario de la clase que debe proporcionar el nombre de usuario y la contraseña que permiten realizar cualquier operación en la base de datos
     // por lo que ya cuenta con todos los derechos sobre la base de datos. Esto implica que no es necesario tomar
     // tomar precauciones de seguridad especiales aquí

     // bibliotecas
  require_once 'DB.php';

  class articles{

           // atributos
    var $sDSN;                        // la cadena de conexión
          var $sDatabase;            // el nombre de la base de datos
    var $oDB;                        // conexión a la base de datos
    var $aErreurs;                // lista de errores
    var $oRésultats;            // resultado de una consulta SELECT
        var $connecté;                // valor booleano que indica si se está conectado o no a la base de datos
        var $sQuery;                    // la última consulta ejecutada
    var $sUser;                    // identidad del usuario de la conexión
    var $bAdmin;                    // toma el valor «verdadero» si el usuario es administrador
    var $dDroits;                // el diccionario de sus derechos: tabla ->> array(consultar, agregar, eliminar, modificar)

     // constructor
    function articles($dDSN,$sUser,$sMdp){

             // $dDSN: diccionario que define la conexión que se debe establecer
       // $dDSN['sgbd']: el tipo de SGBD al que hay que conectarse
       // $dDSN['host']: el nombre de la máquina host que lo aloja      
       // $dDSN['database']: el nombre de la base de datos a la que hay que conectarse      
       // $dDSN['admin']: el nombre de usuario del propietario de la base de datos a la que hay que conectarse
       // $dDSN['mdpadmin']: su contraseña
       // $sUser: el nombre de usuario de quien desea utilizar la base de datos de artículos
       // $sMdp: su contraseña

       // crea en $oDB una conexión a la base de datos definida por $dDSN con la identidad de $dDSN['admin']
       // si la conexión se realiza con éxito y se autentica al usuario $sUser  
           //carga los permisos en $bAdmin y $dDroits los permisos del usuario $sUser
           // almacena en $sDSN la cadena de conexión a la base de datos
           // asigna a $sDataBase el nombre de la base de datos a la que se conecta
         // establece $connecté como verdadero
       // si la conexión falla o si el usuario $sUser no se identifica correctamente
           // agrega los mensajes de error correspondientes a la lista $aErreurs
         // cierra la conexión si es necesario
         // establece $connecté como falso 

  ...
    }//constructor

    // ------------------------------------------------------------------
    function connect(){
             // (re)conexión a la base
...
    }//conectado

    // ------------------------------------------------------------------
    function disconnect(){
      // se cierra la conexión con la base $sDSN
...
    }//desconectar

    // -------------------------------------------------------------------
    function execute($sQuery,$bAdmin){
            // $sQuery: consulta para ejecutar
       // $bAdmin: verdadero si se solicita la ejecución como administrador
...
    }//ejecutar

    // --------------------------------------------------------------------------
    function addArticle($dArticle){
         // agrega un artículo $dArticle (código, nombre, precio, stockActuel, stockMinimum) a la tabla de artículos
   ...
    }//agregar          

    // ----------------------------------------------------------------------
    function modifyArticle($dArticle){
             // modifica un artículo $dArticle (código, nombre, precio, stockActuel, stockMinimum) de la tabla de artículos
...
    }//actualización

    // ----------------------------------------------------------------------
    function deleteArticle($sCode){
             // elimina un artículo de la tabla de artículos
       // cuyo código es $sCode
...
    }//eliminar

    // ----------------------------------------------------------------------
    function vérifierArticle(&$dArticle){
         // verifica la validez de un artículo $dArticle (código, nombre, precio, stockActuel, stockMinimum)
...
    }//verificar

    // --------------------------------------------------------------------------
    function selectArticles($dQuery){
             // ejecuta una consulta SELECT en la tabla de artículos
       // esta tiene tres componentes
       // lista de columnas en $dQuery['colonnes']
       // filtro en $dQuery['where']
       // orden de presentación en $dQuery['orderby']
...
    }//selectArticles            

        // --------------------------------
    function existeArticle($sCode){
         // devuelve TRUE si el artículo con código $sCode existe en la tabla de artículos
...
    }//existeArticle

    // --------------------------------------
    function existeUser($sUser,$sMdp){
             // verifica si existe el usuario $sUser con la contraseña $sMdp
       // devuelve (int $iErreur, cadena $sAdmin, tabla hash $dDroits)
       // $iErreur = -1 ante cualquier error de operación de la base de datos; en ese caso, se completa la lista $aErreurs
       // $iErreur = 1 si no se encuentra al usuario (no existe o la contraseña es incorrecta)
       // $iErreur = 2 si el usuario existe pero no tiene ningún derecho en la tabla de derechos
       // $iErreur = 3 si el usuario existe y es administrador
       // $iErreur = 0 si el usuario existe y no es administrador
       // $sAdmin = «y» si el usuario existe y es administrador ($iErreur == 3); de lo contrario, es igual a la cadena vacía
       // $dDroits es el diccionario de derechos del usuario si no es administrador ($iErreur==0)
       //; de lo contrario, es una tabla vacía
       // las claves del diccionario son las tablas sobre las que el usuario tiene derechos
       // el valor asociado a esta tabla es, a su vez, un diccionario en el que las claves son los derechos
       // (consultar, agregar, modificar, eliminar) y los valores son las cadenas «y» (sí) o «n» (no), según el caso
...
    }//existeUser

    // --------------------------------------
    function getCodes(){
         // devuelve la tabla de códigos
....
    }//getCodes    

  }//clasifica
?>      

Comentarios

  • La clase «artículos» utiliza la biblioteca PEAR::DB para acceder a la base de datos, de ahí el comando
require_once 'DB.php';

Esta inclusión supone que el script DB.php se encuentre en uno de los directorios de la opción include_path del archivo de configuración de PHP.

  • El generador necesita saber a qué base de datos se conecta y con qué identidad. Esta información se le proporciona en el diccionario $dDSN. Recordemos que la hipótesis inicial era que la base de datos se llamaba dbarticles y que pertenecía a un usuario llamado admarticles con la contraseña mdparticles. Recordemos también que esta aplicación permite el acceso a varios usuarios con diferentes derechos. Aquí hay una ambigüedad que hay que aclarar. La conexión se abre efectivamente con la identidad de admarticles y, al final,es con esta identidad con la que se realizarán todas las operaciones en la base de datos dbarticles, ya que es el único nombre que conoce el usuario SGBD MySQL que cuenta con los derechos suficientes para administrar la base de datos dbarticles. Para «simular» la existencia de diferentes usuarios, haremos que el usuario admarticles opere con los derechos del usuario cuyo nombre de usuario ($sUser) y contraseña ($sMdp) se pasan como parámetros al constructor. De este modo, antes de realizar una operación en la base de datos de artículos, se verificará que el usuario ($sUser, $sMdp) cuente efectivamente con los permisos para llevarla a cabo. Si es así, será el usuario admarticles quien la realice en su nombre.
  • El nombre de usuario y la contraseña del administrador de la base de datos de artículos deben pasarse al constructor. Es una precaución sensata. Si se incluyeran «de forma estática» en el código de la clase estos dos datos, cualquier usuario de la clase podría hacerse pasar fácilmente por el administrador de la base de datos de artículos. De hecho, una clase PHP no está protegida. Asimismo, el atributo $bAdmin de la clase, que indica si el usuario ($sUser, $sMdp) para el que se está trabajando es administrador o no, podría muy bien configurarse directamente desde el exterior, como en el siguiente ejemplo:
$oArticles=new articles($dDSN,$sUser,$sMdp)
// aquí, $sUser ha sido reconocido como un usuario no administrador de la base
$oArticle->bAdmin=TRUE;
// ahora $sUser se ha convertido en administrador

PHP no es JAVA ni C# y una clase PHP es solo una estructura de datos un poco más avanzada que un diccionario, pero que no ofrece la seguridad de una clase verdadera, en la que el atributo bAdmin se habría declarado privado o protegido, lo que impediría su modificación desde el exterior. Dado que el usuario de la clase debe conocer el nombre de usuario y la contraseña del administrador de la base de datos de artículos, solo este último puede utilizar la clase. Por lo tanto, la operación anterior ya no tiene ningún interés para él. La clase existe únicamente para facilitarle el desarrollo. Una consecuencia importante es que no es necesario tomar precauciones de seguridad. Una vez más, quien utiliza la clase articles es necesariamente el administrador de la base de datos de artículos.

  • La clase gestiona los errores de conexión a la base de datos o cualquier otro error de manera única, al completar el atributo $aErreurs con el mensaje o mensajes de error correspondientes. Por lo tanto, después de cada operación, el usuario de la clase debe revisar esta lista.
  • Los métodos addArticle, updateArticle, deleteArticle, selectArticles y execute se derivan directamente del diseño de la interfaz web presentado anteriormente. De hecho, corresponden a las opciones del menú propuesto. Los métodos addArticle y modifyArticle se basan en el método vérifierArticle para verificar que el artículo que se va a agregar o modificar tenga datos correctos. Siguiendo la misma lógica, el método existeArticle permite verificar que no se esté a punto de agregar un artículo que ya existe. Se podría prescindir de este método si se utiliza una tabla de artículos en la que el código sea la clave primaria. En ese caso, será el propio SGBD el que señale el error en la adición debido a un duplicado. Probablemente lo indicará con un mensaje de error poco claro y en inglés.
  • Un artículo que se va a modificar o eliminar se identificará por su código, que es único. El método getCodes permite obtener todos estos códigos.
  • El método disconnect cierra la conexión con la base de datos, conexión que se abrió al crear el objeto. Aquí no se ve la utilidad del método connect, que volverá a establecer una conexión con la base de datos. Esto permitirá abrir y cerrar dicha conexión a voluntad con un mismo objeto. La utilidad solo se hace evidente en combinación con la aplicación web. Esta creará un objeto «artículos» que almacenará en una sesión. Si bien la sesión podrá conservar la mayoría de los atributos del objeto a lo largo de los sucesivos intercambios entre el cliente y el servidor, no es capaz de mantener el atributo que representa la conexión abierta. Por lo tanto, esta deberá reabrirse en cada nuevo intercambio entre el cliente y el servidor. Se solicitará una conexión persistente para que la conexión abierta se almacene en un grupo de conexiones y permanezca abierta de manera permanente. De este modo, cuando el script solicite una nueva conexión, esta se recuperará del grupo de conexiones. Así se obtiene el mismo resultado que si la sesión hubiera podido almacenar la conexión abierta.
  • El método existeUser permite al fabricante verificar si el usuario $sUser, identificado con la contraseña $sMdp, existe realmente. Si es así, el método permite determinar si es administrador o no (según se indica en la tabla USERS) y almacena esta información en el atributo $bAdmin. Si no es administrador, el método recuperará sus derechos de la tabla DROITS y los colocará en el atributo $dDroits, que es un diccionario de doble indexación: $dDroits[$table][$droit] es igual a 'y' si el usuario $sUser tiene el derecho $droit sobre la tabla $table y vale 'n' en caso contrario.

Escribe la clase «artículos». Los accesos a la base de datos se realizarán mediante la biblioteca PEAR::DB, que permite prescindir del tipo exacto de la base de datos.

7.5. La estructura de la aplicación WEB

Ahora que contamos con la clase «de negocio» para la gestión de la base de artículos, podemos utilizarla en diferentes entornos. Aquí se propone utilizarla en una aplicación web. Descubramos esta aplicación a través de las siguientes páginas:

7.5.1. La página de inicio de la aplicación

Volvamos a la página de inicio que ya se presentó:

1234

Image

Todas las páginas de la aplicación tendrán la estructura anterior, la de una tabla de dos filas y tres columnas que comprende cuatro zonas:

  • la zona 1 forma la primera fila de la tabla. Está reservada para el título, que puede ir acompañado de una imagen. Las tres columnas de la fila están fusionadas aquí.
  • La segunda fila tiene tres zonas, una por columna:
    • la zona 2 contiene las opciones del menú. A su vez, contiene una tabla de una columna y varias filas. Las opciones del menú se colocan en las filas de la tabla.
    • La zona 3 está vacía y solo sirve para separar las zonas 2 y 4. Se podría haber procedido de otra manera para lograr esta separación.
    • La zona 4 es la que contiene la parte dinámica de la página. Es esta parte la que cambia de una acción a otra, mientras que las demás permanecen idénticas.

El script PHP que genera esta página tipo se llamará main.php y podría ser el siguiente:


<html>
  <head>
      <title>Gestion d'articles</title>
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
   </head>
  <body background="<?php echo $dConfig['urlBackGround'] ?>">
    <table>
      <tr height="60">
        <td colspan="3" align="left" valign="top" >
          <h1><?php echo $main["title"] ?></h1>
        </td>
      </tr>
      <tr>
        <td>
          <table>
            <tr>
              <td class="menutitle" >
                                    <a href="<?php echo  $main["liens"]["login"] ?>" ?>Authentification</a>
              </td>
            </tr>
            <tr>
                <td><br /></td>
            </tr>
            <tr>
              <td class="menutitle" >
                Utilisation
              </td>
            </tr>
            <tr height="10"></tr>
            <tr>
              <td class="menublock" >
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo  $main["liens"]["addArticle"] ?>" ?>
                  Ajouter un article
                </a>
                 </td>
            </tr>
            <tr>
              <td class="menublock" >                    
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["updateArticle"] ?>">
                  Modifier un article
                </a>
                    </td>
            </tr>
              <td class="menublock" >                
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["deleteArticle"] ?>">
                  Supprimer un article
                </a>
              </td>
            </tr>
            <tr>
              <td class="menublock" >
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["selectArticle"] ?>">
                  Lister des articles
                </a>
              </td>
            </tr>
            <tr>
                <td><br /></td>
            </tr>                
            <tr>
              <td class="menutitle" >
                Administration
              </td>
            </tr>
            <tr height="10"></tr>
            <tr>
              <td class="menublock" >                
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["sql"] ?>" >
                  Requête SQL
                </a>
              </td>
            </tr>
          </table>
        </td>
            <td>  
            <img alt="/" src="../images/pix.gif" width="10" height="1" />
            </td>
            <td>
            <fieldset>
              <legend><?php echo $main["légende"] ?></legend>
            <?php
                include $main["contenu"];
            ?>
          </fieldset>
        </td>
      </tr>
    </table>
  </body>
</html>

Las zonas configuradas de la página se han resaltado en la lista anterior. La página tipo se configura de varias maneras:

  • mediante un diccionario $main con las siguientes claves:
    • title: título que se colocará en la zona 1 de la página
    • enlaces: diccionarios de los enlaces que se generarán en la columna del menú. Estos enlaces están asociados a las opciones del menú de la zona 2
    • contenido: URL de la página que se mostrará en la zona 4
  • mediante un diccionario $dConfig que recopila información extraída de un archivo de configuración de la aplicación llamado config.php
  • mediante clases que forman parte de la hoja de estilo utilizada por la página:
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />

La página utiliza aquí las siguientes clases de estilo:

  • menutitle: para una opción principal del menú
  • menublock: para una opción secundaria del menú

Al modificar cualquiera de los parámetros, cambia el aspecto de la página. Por ejemplo, al cambiar $main['title'], se modificará el título del área 1.

7.5.2. El procesamiento típico de una solicitud de un cliente

El cliente interactúa con la aplicación a través de los enlaces de la zona 2 de la página tipo. Estos enlaces serán del siguiente tipo:

apparticles.php?action=xx&phase=y&PHPSESSID=zzzzzzzzzzzz
action
indica la acción en curso entre las siguientes:
authentifier
autenticación del cliente
selectArticles
selección de artículos (consulta)
updateArticle
Modificación de un artículo
deleteArticle
Eliminación de un artículo
sql
Emisión de cualquier solicitud SQL (administrador)
phase
una acción puede realizarse en varios pasos; indica el paso actual
PHPSESSID
token de sesión al inicio de la misma: permite al servidor recuperar la información almacenada en la sesión durante los intercambios anteriores

Del mismo modo, el atributo «action» en los formularios tendrá el mismo formato. Por ejemplo, en la página de inicio hay un formulario de inicio de sesión en la zona 4. La etiqueta HTML de este formulario se define de la siguiente manera:

<form name="frmLogin" method="post" action="apparticles.php?action=authentifier&phase=1">

El procesamiento de la solicitud del cliente lo realiza el script principal de la aplicación, llamado apparticles.php. Su función es generar la respuesta para el cliente. Siempre procederá de la misma manera:

  • Gracias al nombre de la acción y a la fase en curso, delegará la solicitud a una función especializada. Esta procesará la solicitud y generará la página de respuesta adecuada. Para cada solicitud del cliente, puede haber varias páginas de respuesta posibles: página1, página2, ..., página n. Estas páginas contienen información que debe ser calculada por la función. Por lo tanto, se trata de páginas parametrizadas. Serán generadas por los scripts page1.php, page2.php, ..., pagen.php.
  • Para mantener la uniformidad, las partes variables de las páginas que se mostrarán en la zona 4 de la página tipo también se colocarán en el diccionario $main.

Supongamos que, en respuesta a una solicitud, el servidor debe enviar la página pagex.php al cliente. Procederá de la siguiente manera:

  • colocará en el diccionario $main los valores necesarios para la página pagex.php
  • introducirá en $main['contenu'] —que designa a URL— la página que se debe mostrar en la zona 4 de la página tipo, el URL de pagex.php
  • solicitará la visualización de la página tipo con la instrucción
include "main.php";

La página tipo se mostrará entonces con el código del script pagex.php en la zona 4, el cual se evaluará para generar el contenido de la zona 4. Recordemos que esta es una simple celda de una tabla. Por lo tanto, el código HTML generado por pagex.php no debe comenzar con las etiquetas <HTML>, <HEAD>, <BODY>, ... Estas ya se han emitido al inicio de la página tipo. A continuación se muestra, por ejemplo, cómo podría verse el script login.php que genera la zona 4 de la página de inicio:


<form name="frmLogin" method="post" action="<?php echo $main["post"] ?>">
    <table>
        <tr>
            <td>login</td>
            <td><input type="text" value="<?php echo $main["login"] ?>" name="txtLogin" class="text"></td>
        </tr>
        <tr>
            <td>mot de passe</td>
            <td><input type="password" value="" name="txtMdp" class="text"></td>
      <td><input type="submit" value="Connexion" class="submit"></td>      
        </tr>
    </table>
</form>

Se observa que la página:

  • se reduce a un formulario
  • está configurada tanto por el diccionario $main como por la hoja de estilo.

7.5.3. El archivo de configuración

Siempre es recomendable configurar las aplicaciones lo más posible para evitar tener que modificar el código simplemente porque, por ejemplo, se decidió cambiar la ruta de un script o de una imagen. Por lo tanto, la aplicación principal apparticles.php cargará un archivo de configuración config.php al iniciarse:

     // cargando el archivo de configuración
  include "config.php";

En este archivo se incluirán directivas de configuración destinadas a PHP e inicializaciones de variables globales:

<?php

     // Configuración de PHP
  ini_set("register_globals","off");
  ini_set("display_errors","off");
  ini_set("expose_php","off");
    ini_set("session.use_cookies","0");    // sin cookies

     // configuración básica de artículos
    $dConfig["DSN"]=array(
        "sgbd"=>"mysql",
        "admin"=>"admarticles",
        "mdpadmin"=>"mdparticles",
        "host"=>"localhost",
        "database"=>"dbarticles"
    );

   // URL de las páginas
    $dConfig['urlBackGround']="../images/standard.jpg";  
  $dConfig["urlPageStyle"]="mystyle.css";  
  $dConfig["urlAppArticles"]="apparticles.php";
  $dConfig["urlPageMain"]="main.php";
  $dConfig["urlPageLogin"]="login.php";
  $dConfig["urlPageErreurs"]="erreurs.php";
  $dConfig["urlPageInfos"]="infos.php";
  $dConfig["urlPageAddArticle"]="addarticle.php";
  $dConfig["urlPageUpdateArticle1"]="updatearticle1.php";
  $dConfig["urlPageUpdateArticle2"]="updatearticle2.php";
  $dConfig["urlPageDeleteArticle1"]="deletearticle1.php";
  $dConfig["urlPageDeleteArticle2"]="deletearticle2.php";
  $dConfig["urlPageSelectArticle1"]="selectarticle1.php";
  $dConfig["urlPageSelectArticle2"]="selectarticle2.php";
  $dConfig["urlPageSQL1"]="sql1.php";
  $dConfig["urlPageSQL2"]="sql2.php";
  $dConfig["urlPageSQL3"]="sql3.php";

   // enlaces de la página principal
  $main["liens"]["login"]="$sUrlAppArticles?action=authentifier&phase=0";  
  $main["liens"]["addArticle"]="$sUrlAppArticles?action=addArticle&phase=0";
  $main["liens"]["updateArticle"]="$sUrlAppArticles?action=updateArticle&phase=0";
  $main["liens"]["deleteArticle"]="$sUrlAppArticles?action=deleteArticle&phase=0";
  $main["liens"]["selectArticle"]="$sUrlAppArticles?action=selectArticle&phase=0";
  $main["liens"]["sql"]="$sUrlAppArticles?action=sql&phase=0";

   // se guarda $main en la configuración
  $dConfig["main"]=$main;    
?>

7.5.4. La hoja de estilo asociada a la página tipo

Hemos visto que la respuesta del servidor tenía un formato único: main.php. Se habrá notado que este script genera una página sin formato, carente de efectos de presentación. Esto es positivo por varias razones:

  • el desarrollador no tiene que preocuparse por la presentación gráfica de la página que crea. De hecho, no necesariamente cuenta con las habilidades para crear páginas gráficas atractivas. Aquí puede concentrarse por completo en el código.
  • se facilita el mantenimiento de los scripts. Si estos incluyeran atributos de presentación, ni la estructura del código ni la de la presentación quedarían claras. El aspecto gráfico de las hojas a menudo se delega a un diseñador gráfico. Probablemente a este no le gustaría tener que buscar en un script que no entiende dónde están los atributos de presentación que debe modificar.

Sin embargo, es importante prestar atención al aspecto gráfico de las páginas. De hecho, es esto lo que atrae a los usuarios a un sitio web. Aquí, la presentación se delega a una hoja de estilo. La página main.php indica en su código la hoja de estilo que se debe utilizar para mostrarla:

  <head>
      <title>Gestion d'articles</title>
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
   </head>

La hoja de estilo utilizada en este documento es la siguiente:

BODY {
    background : url(../images/standard.jpg);
    border : 2px none #FFDAB9;
    font-family : Garamond;
    font-size : 16px;
    margin-left : 0px;
    padding-left : 20px;
}

INPUT {
    background : #EEE8AA;
    border : 1px solid #EE82EE;
    font-family : Garamond;
    font-size : 18px;
}

INPUT.submit{
    font-family : "Times New Roman";
    font-size : 16px;
    background : #FA8072;
    border : 2px double Green;
    font-weight : bold;
    text-align : center;
    vertical-align : middle;
    cursor : pointer;
}

TD.menutitle{
    background-image : url(../images/menugelgd.gif);
    height : 23px;
    text-align : center;
    vertical-align : middle;
    background : url(../images/menugelgd.gif) no-repeat center;
}

TD.menublock{
    background : url(../images/bandegrismenugd.gif) repeat-x;
    text-align : left;
    vertical-align : middle;
}

A {
    font-family : "Comic Sans MS";
    color : #FF7F50;
    font-size : 15px;
    text-decoration : none;
}

A:HOVER {
    background : #FFA07A;
    color : Red;
}

FIELDSET {
    border : 1px solid #A0522D;
    background : #FFE4C4;
    margin : 10px 10px 10px 10px;
    padding-left : 10px;
    padding-right : 10px;
    padding-bottom : 10px;
}

LEGEND{
    background : #FFA500;
}

TH {
    background : #228B22;
    text-align : center;
    vertical-align : middle;
}

TD.libellé{
    border : 1px solid #008B8B;
    color : #339966;
}

H1 {
    font : bold 20px/30px Garamond;
    color : #FF7F50;
    background : #D1E1F8;
    background-attachment : fixed;
    text-align : center;
    vertical-align : middle;
    font-family : Garamond;
}

SELECT.TEXT {
    background : #6495ED;
    text-align : center;
    color : Aqua;
}

No entraremos en detalles sobre esta hoja de estilo. La aceptaremos tal como está. Más adelante veremos cómo crearla y modificarla. Existen programas para ello. No obstante, señalemos la función de los atributos de presentación utilizados en la hoja:

Atributo:
controla la presentación de la etiqueta HTML:
BODY
<BODY>
H1
<H1> (Encabezado 1)
A
<A> (Ancla)
A:HOVER
establece los atributos de presentación del enlace cuando el usuario pasa el ratón por encima
FIELDSET
<FIELDSET>: esta etiqueta no es reconocida por todos los navegadores
LEGEND
<LEGEND> - esta etiqueta no es reconocida por todos los navegadores
INPUT
<INPUT>
INPUT.TEXT
<INPUT class="TEXT">
INPUT.SUBMIT
<INPUT class="SUBMIT">
TH
<TH> (Encabezado de la tabla)
TD.menutitle
<TD class="menutitle"> (Datos de la tabla)
TD.menublock
<TD class="menublock">
TD.libellé
<TD class="libellé">

Veamos con un ejemplo cómo se pueden escribir estas reglas de presentación. En este ejemplo, utilizaremos el software TopStyle Lite, disponible de forma gratuita en http://www.bradsoft.com. Una vez cargada la hoja de estilo, aparece una ventana con tres secciones:

  1. un área de edición de texto. Los atributos de presentación se pueden definir manualmente, siempre y cuando se conozcan las reglas de escritura de las hojas de estilo, que siguen un estándar llamado CSS (Cascading Style Sheets).
  2. la zona 2 muestra las propiedades editables del atributo que se está creando. Este es el método más sencillo. Evita tener que conocer el nombre exacto de los atributos de presentación, que son muy numerosos
  3. La zona 3 muestra el aspecto visual del atributo que se está creando

En la zona 1 anterior, copiemos y peguemos el atributo INPUT.submit en un atributo INPUT.fantaisie. Este atributo definirá la presentación de la etiqueta HTML <INPUT class="fantaisie">

Usemos la zona 2 para modificar algunas de las propiedades del atributo INPUT.fantaisie:

A partir de ahora, cualquier etiqueta <INPUT ... class="fantaisie"> que se encuentre en una página HTML asociada a la hoja de estilo anterior se mostrará como en el ejemplo de la zona 3 anterior.

Las hojas de estilo ofrecen grandes ventajas. Su uso permite cambiar el «aspecto» de una aplicación web modificando únicamente un elemento: su hoja de estilo. Las hojas de estilo no son compatibles con los navegadores antiguos. La directiva <link ..> que se muestra a continuación será ignorada por algunos de ellos:

  <head>
      <title>Gestion d'articles</title>
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
   </head>

En nuestra aplicación, esto generará la siguiente página de inicio:

Image

Aquí tenemos una página mínima sin elementos gráficos. Podría ser peor. Algunas versiones de navegadores reconocen las hojas de estilo, pero las interpretan mal. En ese caso, la página puede aparecer deformada e inutilizable. Por lo tanto, surge la cuestión del tipo de navegador del usuario. Existen técnicas que ayudan a determinar el tipo de navegador del usuario, pero no son totalmente confiables. Por lo tanto, se pueden escribir diferentes hojas de estilo para distintos navegadores o incluso crear una versión sin hojas de estilo para los navegadores que las ignoran. Esto, por supuesto, complica la tarea de desarrollo. Este importante problema se ha ignorado aquí.

Con las hojas de estilo, podemos ofrecer un entorno personalizado a los usuarios de nuestra aplicación. Podríamos mostrarles una página con varios estilos de presentación posibles. Ellos podrían elegir el que más les convenga. Esa elección se podría guardar en una base de datos. Cuando el usuario vuelva a iniciar sesión, podríamos iniciar la aplicación con la hoja de estilo que haya elegido.

7.5.5. El módulo de entrada de la aplicación

Los clientes solo conocerán el módulo de entrada de la aplicación: apparticles.php. Las líneas generales de su funcionamiento son las siguientes:

  • Se recupera y analiza la solicitud del cliente. Esta puede estar configurada o no. Cuando está parametrizada, los parámetros esperados son los siguientes: action=[action]&phase=[phase]&PHPSESSID=[PHPSESSID]
  • Si la solicitud no contiene parámetros o si los parámetros recuperados no son los esperados, el servidor envía como respuesta la página de autenticación (nombre de usuario, contraseña). Tan pronto como el usuario se haya identificado correctamente, se crea una sesión. Esta servirá para almacenar información a lo largo de las comunicaciones entre el cliente y el servidor.
  • Si una solicitud se reconoce correctamente, es procesada por un módulo que depende tanto de la acción como de la fase en curso.
  • Todos los accesos a la base de datos se realizan a través de la clase de negocio articles.php.
  • El procesamiento de una solicitud siempre concluye con el envío al cliente de la página main.php, en la que se ha especificado en $main['contenu'] elURL de la página que se debe colocar en la zona 4 de la página tipo.

La estructura básica del script apparticles.php podría ser la siguiente:

<?php
     // gestión de una tabla de artículos
  include "config.php";
  include "articles.php";  

  // medida a tomar
  $sAction=$_POST["action"] ? $_POST["action"] : $_GET["action"] ? $_GET["action"] : "authentifier";
  $sAction=strtolower($sAction);
   // fase eventual
  $sPhase=$_POST["phase"] ? $_POST["phase"] : $_GET["phase"] ? $_GET["phase"] : "0";

  // sesión
  session_start();
  $dSession=$_SESSION["session"];

     // ¿Hay una sesión en curso?
  if(! isset($dSession)){
      // autenticación del usuario
    if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
    if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);
    if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);
     // solicitud incorrecta
    authentifier_0($dConfig);        
  }//si no hay sesión

     // se recupera la sesión
  $dSession=unserialize($dSession);

     // procesamiento de la solicitud
     // ----- autenticación
  if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
  if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);
  if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);  
     // ----- agregación de artículo
  if($sAction=="addarticle" && $sPhase=="0") addArticle_0($dConfig,$dSession);
  if($sAction=="addarticle" && $sPhase=="1") addArticle_1($dConfig,$dSession);
  if($sAction=="addarticle" && $sPhase=="2") addArticle_2($dConfig,$dSession);
     // ----- actualización de artículo
  if($sAction=="updatearticle" && $sPhase=="0") updateArticle_0($dConfig,$dSession);
  if($sAction=="updatearticle" && $sPhase=="1") updateArticle_1($dConfig,$dSession);
  if($sAction=="updatearticle" && $sPhase=="2") updateArticle_2($dConfig,$dSession);
  if($sAction=="updatearticle" && $sPhase=="3") updateArticle_3($dConfig,$dSession);
     // ----- eliminación de artículo
  if($sAction=="deletearticle" && $sPhase=="0") deleteArticle_0($dConfig,$dSession);
  if($sAction=="deletearticle" && $sPhase=="1") deleteArticle_1($dConfig,$dSession);
  if($sAction=="deletearticle" && $sPhase=="2") deleteArticle_2($dConfig,$dSession);
    // ----- consulta de artículos
  if($sAction=="selectarticle" && $sPhase=="0") selectArticle_0($dConfig,$dSession);
  if($sAction=="selectarticle" && $sPhase=="1") selectArticle_1($dConfig,$dSession);
  if($sAction=="selectarticle" && $sPhase=="2") selectArticle_2($dConfig,$dSession);
     // ----- envío de una solicitud SQL
  if($sAction=="sql" && $sPhase=="0") sql_0($dConfig,$dSession);
  if($sAction=="sql" && $sPhase=="1") sql_1($dConfig,$dSession);
  if($sAction=="sql" && $sPhase=="2") sql_2($dConfig,$dSession);


    // acción incorrecta: se muestra la página de autenticación
  session_destroy();
  authentifier_0($dConfig,"0");
...
?>

Cabe destacar los siguientes puntos:

  • las funciones que procesan una solicitud específica del cliente concluyen con la generación de la página de respuesta y con una instrucción exit que finaliza la ejecución del script apparticles.php. En otras palabras, no se «regresa» de estas funciones.
  • Las funciones admiten uno o dos parámetros:
    • $dConfig es un diccionario que contiene información procedente del archivo de configuración config.php. Todas las funciones lo utilizan.
    • $dSession es un diccionario que contiene información de la sesión. Solo existe cuando se ha creado la sesión, es decir, después de que la autenticación del usuario se haya realizado con éxito. Por eso las funciones de autenticación no tienen este parámetro.

7.5.6. La página de errores

Toda aplicación de software debe saber manejar correctamente los errores que puedan surgir. Una aplicación web no es la excepción a esta regla. En este caso, cuando se produzca un error, colocaremos la siguiente página erreurs.php en la zona 4 de la página tipo:

Les erreurs suivantes se sont produites :
<ul>
    <?php
        for($i=0;$i<count($main["erreurs"]);$i++){
            echo "<li>".$main["erreurs"][$i]."</li>\n";
        }//para
    ?>
</ul>
<a href="<?php echo $main["href"] ?>"><?php echo $main["lien"] ?></a>

Muestra la lista de errores definida en $main['erreurs']. Además, puede ofrecer un enlace de retorno, generalmente a la página que precedió a la página de errores. Este enlace se definirá mediante un texto de $main['lien'] y un URL $main['href']. Para evitar que aparezca este enlace, basta con poner la cadena vacía en $main['lien']. A continuación se muestra un ejemplo de página de errores en el caso de que el usuario se identifique incorrectamente:

Image

7.5.7. La página de información

A veces se querrá proporcionar al usuario una simple información como respuesta, por ejemplo, que su inicio de sesión se realizó con éxito. Para ello, se utilizará la siguiente página: infos.php:

<?php echo $main["infos"] ?>

Para mostrar información en respuesta a una solicitud de un cliente,

  • se colocará la información en $main['infos']
  • se colocará el URL de infos.php en $main['contenu']

A continuación, por ejemplo, se muestra la información que se devuelve cuando el usuario se ha identificado correctamente:

Image

7.6. El funcionamiento de la aplicación

Ahora tenemos una idea clara de la estructura general de la aplicación que debemos desarrollar. Nos queda por presentar los recorridos del usuario dentro de la aplicación, las acciones que puede realizar y las respuestas que recibe del servidor. Una vez hecho esto, podremos escribir las funciones que procesan las diferentes solicitudes de un cliente. A continuación, presentaremos el funcionamiento de la aplicación a través de las páginas que se le muestran al usuario en respuesta a algunas de estas acciones. En cada caso, especificaremos los siguientes puntos:

action utilisateur
acción inicial del usuario que dio lugar a la respuesta mostrada
paramètres envoyés
los parámetros enviados por el navegador del cliente al servidor en respuesta a la acción manual del usuario
page réponse
el script que genera la zona 4 de la página tipo

7.6.1. La autenticación

Antes de poder utilizar la aplicación, el usuario deberá identificarse mediante la siguiente página:

Image

action utilisateur
1 - solicitud inicial de URL apparticles.php
2 - uso de la opción «Autenticación» del menú
3 - Solicitud directa del URL articles.php con parámetros incorrectos
paramètres envoyés
1 - Sin parámetros
2 - acción=autenticar?fase=0
3 - una lista de parámetros incorrectos
page réponse
login.php

En la página de inicio, el enlace [Ajouter un article] tiene el siguiente formato: action=addarticle?phase=0. Los demás enlaces tienen el mismo formato con action=(authentifier, updatearticle, deletearticle, selectarticle, sql). El usuario llena el formulario y utiliza el botón [Connexion]:

Image

La respuesta es la siguiente:

Image

action utilisateur
bouton [Connexion]
paramètres envoyés
acción=autenticar?fase=1
page réponse
infos.php

El título de la página se ha modificado para indicar el nombre de usuario y sus derechos de administrador o usuario. Además, todos los enlaces de la zona 2 se han modificado para reflejar que se ha iniciado una sesión. Se les ha agregado el parámetro PHPSESSID=[PHPSESSID].

Si el servidor no pudo identificar al cliente, este recibirá una respuesta diferente:

Image

action utilisateur
bouton [Connexion]
paramètres envoyés
acción=autenticar?fase=1
page réponse
erreurs.php

El enlace [Retour à la page de login] es un enlace a URL apparticles.php?action=authentifier&phase=2&txtLogin=x. Este enlace redirige al cliente a la página de inicio de sesión, donde el campo de inicio de sesión se completa con el valor del parámetro txtLogin:

Image

action utilisateur
lien [Retour à la page de login]
paramètres envoyés
action=autenticar?fase=2&txtLogin=x
page réponse
login.php

7.6.2. Agregar un artículo

El enlace del menú [Ajouter un article] lleva a la siguiente página en la zona 4 de la página tipo:

Image

action utilisateur
lien [Ajouter un article]
paramètres envoyés
action=addArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
addarticle.php

El usuario llena los campos y envía todo al servidor con el botón [Ajouter], que es del tipo submit. No se realiza ninguna verificación del lado del cliente. Es el servidor el que las realiza. Puede enviar como respuesta una página de errores como en el ejemplo a continuación:

Solicitud
Respuesta
action utilisateur
bouton [Ajouter]
paramètres envoyés
action=addArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
erreurs.php

El enlace [Retour à la page d'ajout d'article] permite regresar a la página de ingreso:

Solicitud
Respuesta
action utilisateur
lien [Retour à la page d'ajout d'article]
paramètres envoyés
action=addArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
article.php

Si la adición se realiza sin errores, el usuario recibe un mensaje de confirmación:

Solicitud
Respuesta
action utilisateur
bouton [Ajouter]
paramètres envoyés
action=addArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
infos.php

7.6.3. Consulta de artículos

El enlace del menú [Lister des articles] lleva a la siguiente página en la zona 4 de la página tipo:

Image

action utilisateur
enlace del menú [Lister des articles]
paramètres envoyés
action=selectArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
select1.php

Se emitirá una consulta select [colonnes] from articles where [where] orderby [orderby] sobre la tabla de artículos, donde [colonnes], [where] y [orderby] son el contenido de los campos anteriores. Por ejemplo:

Solicitud
Respuesta
action utilisateur
bouton [Afficher]
paramètres envoyés
action=selectArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
select2.php

La solicitud puede ser incorrecta, en cuyo caso el cliente recibe una página de error:

Solicitud
Respuesta

En ambos casos (con o sin errores), el enlace [Retour à la page de sélection d'articles] permite regresar a la página select1.php:

Solicitud
Respuesta
action utilisateur
lien [Retour à la page de sélection d'articles]
paramètres envoyés
action=selectArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
select1.php

7.6.4. Modificación de artículos

El enlace del menú [Modifier un article] lleva a la siguiente página en la zona 4 de la página tipo:

Image

action utilisateur
enlace de menú [Modifier un article]
paramètres envoyés
action=updateArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
updatearticle1.php

Seleccionamos el código del artículo que queremos modificar en la lista desplegable y escribimos [OK] para modificar el artículo con ese código:

Solicitud
Respuesta
action utilisateur
bouton [OK]
paramètres envoyés
action=updateArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
updatearticle2.php

Una vez que se ha obtenido la ficha del artículo que se va a modificar, el usuario puede realizar sus modificaciones:

Solicitud
Respuesta
action utilisateur
bouton [Modifier]
paramètres envoyés
action=updateArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
infos.php

El usuario puede cometer errores al realizar modificaciones:

Solicitud
Respuesta

El enlace [Retour à la page de modification d'article] permite regresar a la página de entrada:

Image

action utilisateur
lien [Retour à la page de modification d'article]
paramètres envoyés
action=updateArticle?phase=3&PHPSESSID=[PHPSESSID]
page réponse
updatearticle2.php

7.6.5. Eliminación de un artículo

El enlace del menú [Supprimer un article] lleva a la siguiente página en la zona 4 de la página tipo:

Image

action utilisateur
enlace del menú [Supprimer un article]
paramètres envoyés
action=deleteArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
deletearticle1.php

El usuario selecciona el código del artículo que desea eliminar de una lista desplegable:

Solicitud
Respuesta
action utilisateur
bouton [OK]
paramètres envoyés
action=deleteArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
deletearticle2.php

El usuario confirma la eliminación del artículo con el botón [Supprimer]:

Solicitud
Respuesta
action utilisateur
bouton [Supprimer]
paramètres envoyés
action=deleteArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
infos.php

7.6.6. Envío de solicitudes de administrador

El enlace del menú [Requête SQL] lleva a la siguiente página en la zona 4 de la página tipo:

Image

action utilisateur
enlace de menú [Requête SQL]
paramètres envoyés
action=sql?phase=0&PHPSESSID=[PHPSESSID]
page réponse
sql1.php

Se escribe el texto de la consulta SQL en el campo de entrada y se utiliza el botón [Exécuter] para ejecutarla. Solo un administrador puede emitir estas consultas, como se muestra en el siguiente ejemplo:

Solicitud
Respuesta
action utilisateur
bouton [Exécuter]
paramètres envoyés
action=sql?phase=1&PHPSESSID=[PHPSESSID]
page réponse
erreurs.php

El enlace [Retour à la page d'émission de requêtes SQL] permite regresar a la página de ingreso:

Image

action utilisateur
lien [Retour à la page d'émission de requêtes SQL]
paramètres envoyés
action=sql?phase=2&PHPSESSID=[PHPSESSID]
page réponse
sql1.php

Si eres administrador y la consulta es sintácticamente correcta:

Solicitud

se obtiene el resultado de la consulta:

Respuesta
action utilisateur
bouton [Exécuter]
paramètres envoyés
action=sql?phase=1&PHPSESSID=[PHPSESSID]
page réponse
sql2.php

Se pueden enviar consultas para actualizar las tablas:

Solicitud
Respuesta
action utilisateur
bouton [Exécuter]
paramètres envoyés
action=sql?phase=1&PHPSESSID=[PHPSESSID]
page réponse
infos.php

7.6.7. Tareas por realizar

Escribir los scripts y funciones necesarios para la aplicación:

identificador
tipo
rol
apparticles.php
script
el punto de entrada para el procesamiento de las solicitudes de los clientes
authentifier_0
función
procesa la solicitud configurada con acción=autenticar&fase=0
authentifier_1
función
procesa la solicitud con los parámetros action=autenticar&fase=1
authentifier_2
función
procesa la solicitud con los parámetros action=autenticar&fase=2
addarticle_0
función
procesa la solicitud con los parámetros action=addArticle&phase=0
addarticle_1
función
procesa la solicitud configurada con action=addArticle&phase=1
addarticle_2
función
procesa la solicitud configurada con action=addArticle&phase=2
updatearticle_0
función
procesa la solicitud configurada con action=updatearticle&phase=0
updatearticle_1
función
procesa la solicitud con los parámetros action=updatearticle&phase=1
updatearticle_2
función
procesa la solicitud configurada con los parámetros action=updatearticle&phase=2
updatearticle_3
función
procesa la solicitud configurada con los parámetros action=updatearticle&phase=3
deletearticle_0
función
procesa la solicitud configurada con los parámetros action=deletearticle&phase=0
deletearticle_1
función
procesa la solicitud con los parámetros action=deletearticle&phase=1
deletearticle_2
función
procesa la solicitud con los parámetros action=deletearticle&phase=2
selectarticle_0
función
procesa la solicitud con los parámetros action=selectarticle&phase=0
selectarticle_1
función
procesa la solicitud configurada con los parámetros action=selectarticle&phase=1
selectarticle_2
función
procesa la solicitud configurada con los parámetros action=selectarticle&phase=2
sql_0
función
procesa la solicitud configurada con los parámetros action=sql&phase=0
sql_1
función
procesa la solicitud configurada con los parámetros action=sql&phase=1
sql_2
función
procesa la solicitud configurada con action=sql&phase=2
main.php
script
genera la página tipo
login.php
script
genera la página de inicio de sesión
erreurs.php
script
genera la página de errores
infos.php
script
genera la página de información
addarticle.php
script
genera la página para agregar un artículo
updatearticle1.php
script
genera la página 1 para modificar un artículo
updatearticle2.php
script
genera la página 2 de la modificación de un artículo
deletearticle1.php
script
genera la página 1 de la eliminación de un artículo
deletearticle2.php
script
genera la página 2 de la eliminación de un artículo
select1.php
script
genera la página 1 de la selección de artículos
select2.php
script
genera la página 2 de la selección de artículos
sql1.php
script
genera la página 1 de la emisión de consultas
sql2.php
script
genera la página 2 de la emisión de solicitudes

7.7. Mejorar la aplicación

Hasta este momento, contamos con una aplicación que cumple con su función y ofrece una usabilidad aceptable. Vamos a mejorarla en varios aspectos:

  • el SGBD
  • su seguridad
  • su apariencia
  • su rendimiento

7.7.1. Cambiar el tipo de base de datos

Nuestro estudio suponía que el SGBD utilizado era MySQL. Cambie a SGBD y demuestre que la única modificación que hay que hacer es en la definición de la variable $dDSN en el archivo de configuración config.php.

7.7.2. Mejorar la seguridad

Al desarrollar una aplicación web, nunca se debe dar por sentado que el cliente es un navegador y que la solicitud que nos envía está controlada por el formulario que le enviamos antes de dicha solicitud. Cualquier programa puede ser cliente de una aplicación web y, por lo tanto, enviar cualquier solicitud, ya sea parametrizada o no, a la aplicación. Por lo tanto, esta debe verificar todo.

Si nos fijamos en el código del script apparticles.php, observamos

  • que no puede realizarse ninguna acción, salvo la autenticación, sin una sesión. Esta solo existe si el usuario ha logrado autenticarse. Recordemos que una sesión se identifica mediante una cadena de caracteres bastante larga llamada «token de sesión», que tiene el siguiente formato: 176a43609572907333118333edf6d1fb. Este token puede enviarse a la aplicación de diversas maneras, por ejemplo, utilizando un URL configurado:

apparticles.php?PHPSESSID=176a43609572907333118333edf6d1fb. 

Un programa que solicitara repetidamente el URL anterior, variando el token de manera aleatoria con la esperanza de encontrar el token correcto, probablemente tardaría muchos días en generar la combinación correcta, dado que el número de combinaciones posibles es enorme. Para entonces, como la sesión tiene una duración limitada, es muy probable que ya haya finalizado. Otro riesgo sería que el token, al transmitirse sin cifrar por la red, fuera interceptado. El riesgo es real. Por lo tanto, se puede utilizar una conexión cifrada entre el servidor y su cliente.

  • una vez iniciada la sesión, solo se permiten ciertas acciones. Un token URL configurado con action=tricher&phase=0&PHPSESSID=[PHPSESSID] sería rechazado porque la acción «tricher» no es una acción autorizada. Cuando no se reconocen los parámetros (acción, fase), nuestra aplicación responde con la página de autenticación.

Sin embargo, la aplicación no verifica si las acciones autorizadas se encadenan correctamente. Por ejemplo, las dos acciones siguientes:

  1. action=addArticle&phase=0&PHPSESSID=[PHPSESSID]
  2. action=updateArticle&phase=1&PHPSESSID=[PHPSESSID]

son dos acciones autorizadas. Sin embargo, la acción 2 no está autorizada a seguir a la acción 1.

¿Cómo se puede seguir la secuencia de las solicitudes URL realizadas por el navegador del cliente?

Podemos utilizar dos variables PHP: $_SERVER['REQUEST_URI] y $_SERVER['HTTP_REFERER], que son dos datos enviados por los navegadores de los clientes en sus encabezados HTTP.

$_SERVER['REQUEST_URI]: Es el URI solicitado por el cliente. Por ejemplo

/apparticles.php?action=addArticle&phase=0&PHPSESSID=[PHPSESSID]

$_SERVER['HTTP_REFERER]: Este es el URL que se estaba visualizando en el navegador antes del nuevo URL que el navegador está solicitando (el URI anterior). Por ejemplo, si el navegador que visualizó el archivo URI mencionado anteriormente realiza una nueva solicitud a un servidor, la variable $_SERVER['HTTP_REFERER'] de este tendrá como valor

http://máquina:puerto//apparticles.php?action=addArticle&phase=0&PHPSESSID=[PHPSESSID]

Para verificar que dos acciones de nuestra aplicación se sucedan en el orden correcto, podemos proceder de la siguiente manera:

En la acción 1:

  • se toma nota del URI solicitado (URI1) y se registra en la sesión

En la acción 2:

  • se recupera el HTTP-REFERER de la acción 2. A partir de ahí, se deduce el URI (URI2) a partir del URL que se había visualizado previamente en el navegador que realiza la solicitud.
  • Se recupera el URI (URI1) que estaba almacenado en la sesión y que corresponde al URI de la acción solicitada previamente al servidor
  • Si la acción 2 sigue a la acción 1, entonces debe cumplirse que URI2 = URI1. De no ser así, se rechazaría la acción solicitada y se mostraría la página de autenticación.
  • Se registra en la sesión el URI y el URI2 de la acción en curso para verificar la siguiente acción. Y así sucesivamente.

A continuación, un ejemplo. Tras la autenticación, se selecciona el enlace [Ajouter un article]:

Image

El URL de esta página es:

http://localhost:81/st/php/articles/gestion/articles8/apparticles.php?action=addArticle&phase=0&PHPSESSID=006a63e6027f16c70b63cdae93405eeb

Directamente en el campo [Adresse] del navegador, modificamos el URL de la siguiente manera:

http://localhost:81/st/php/articles/gestion/articles8/apparticles.php?action=deleteArticle&fase=1&PHPSESSID=006a63e6027f16c70b63cdae93405eeb

A continuación, aparece la página de autenticación:

Image

Esto merece una explicación. Cuando solicitamos un URL al escribir directamente su identidad en el campo de dirección del navegador, este no envía el encabezado HTTP_REFERER. Por lo tanto, nuestra aplicación no encuentra allí el URI de la acción anterior, URI, que había almacenado en la sesión. En consecuencia, devuelve la página de autenticación como respuesta.

Este mecanismo es eficaz para los navegadores, pero no lo es en absoluto para un cliente programado. Este último puede enviar el encabezado HTTP_REFERER que desee. Por lo tanto, puede “engañar” al afirmar que sí ha pasado por tal etapa cuando en realidad no lo ha hecho. Por lo tanto, hay que asegurarse de que se respete la secuencia de pasos. Así, si la acción solicitada es action=addArticle&phase=1 (ingreso), entonces la acción anterior debe ser necesariamente action=deleteArticle&phase=0 (solicitud inicial de la página de ingreso) o action=addArticle&phase=2 (regreso al ingreso tras un agregado erróneo). Del mismo modo, si la acción solicitada es action=addArticle&phase=2 (agregar), entonces la acción anterior debe ser action=addArticle&phase=1 (ingreso). Se puede obligar al usuario a respetar estas secuencias.

Mientras que el primer mecanismo es general y puede aplicarse a cualquier aplicación, el segundo requiere una programación específica para cada aplicación y es más complejo: hay que revisar todas las acciones posibles del usuario y sus secuencias. Estas últimas se pueden almacenar en un diccionario, como lo muestra el siguiente código:

  // autenticación
  $dPrec['authentifier']['0']=array();
  $dPrec['authentifier']['1']=array(
         array('action'=>'authentifier','phase'=>'0'),
    array('action'=>'authentifier','phase'=>'2')
  );
  $dPrec['authentifier']['2']=array(
         array('action'=>'authentifier','phase'=>'1'),
  );

   // agregar artículo
  $dPrec['addarticle']['0']=array();  
  $dPrec['addarticle']['1']=array(
         array('action'=>'addarticle','phase'=>'0'),
    array('action'=>'addarticle','phase'=>'2')
  );
  $dPrec['addarticle']['2']=array(
         array('action'=>'addarticle','phase'=>'1'),
  );

   // modificación de artículo
  $dPrec['updatearticle']['0']=array();  
  $dPrec['updatearticle']['1']=array(
         array('action'=>'updatearticle','phase'=>'0'),
  );
  $dPrec['updatearticle']['2']=array(
         array('action'=>'updatearticle','phase'=>'1'),
    array('action'=>'updatearticle','phase'=>'3')
  );
  $dPrec['updatearticle']['3']=array(
         array('action'=>'updatearticle','phase'=>'2'),
  );

   // eliminación de artículo
  $dPrec['deletearticle']['0']=array();  
  $dPrec['deletearticle']['1']=array(
         array('action'=>'deletearticle','phase'=>'0'),
  );
  $dPrec['deletearticle']['2']=array(
         array('action'=>'deletearticle','phase'=>'1'),
  );

      // selección de artículos
  $dPrec['selectarticle']['0']=array();  
  $dPrec['selectarticle']['1']=array(
         array('action'=>'selectarticle','phase'=>'0'),
    array('action'=>'selectarticle','phase'=>'2')
  );
  $dPrec['selectarticle']['2']=array(
         array('action'=>'selectarticle','phase'=>'1'),
  );

      // solicitud de administrador
  $dPrec['sql']['0']=array();  
  $dPrec['sql']['1']=array(
         array('action'=>'sql','phase'=>'0'),
    array('action'=>'sql','phase'=>'2')
  );
  $dPrec['sql']['2']=array(
         array('action'=>'sql','phase'=>'1'),
  );

$dPrec['action']['phase'] es una tabla que contiene las acciones que pueden preceder a la acción y la fase que sirven como índice para el diccionario. Estas acciones previas también se representan mediante un diccionario con dos claves: «acción» y «fase». Si una acción puede ser precedida por cualquier acción, entonces $dPrec['action']['phase'] será una tabla vacía. La ausencia de una acción en el diccionario significa que no está permitida. Consideremos la acción «autenticar» mencionada anteriormente:

  // autenticación
  $dPrec['authentifier']['0']=array();
  $dPrec['authentifier']['1']=array(
         array('action'=>'authentifier','phase'=>'0'),
    array('action'=>'authentifier','phase'=>'2')
  );
  $dPrec['authentifier']['2']=array(
         array('action'=>'authentifier','phase'=>'1'),
  );

El código anterior significa que la acción action=authentifier&phase=0 puede ir precedida de cualquier acción, que la acción action=authentifier&phase=1 puede ir precedida de action=authentifier&phase=0 o de action=authentifier&phase=2, y que la acción action=authentifier&phase=2 puede ir precedida de la acción action=authentifier&phase=1.

Escribe la siguiente función:

  // ---------------------------------------------------------------
  function enchainementOK(&$dConfig, &$dSession, $sAction, $sPhase){
       // verifica si la acción en curso ($sAction, $sPhase) puede seguir a la acción anterior
         // almacenada en $dSession['précédent']
         // el diccionario de secuencias permitidas se encuentra en $dConfig['précédents']
         // devuelve TRUE si la secuencia es posible, FALSE en caso contrario
....

Esta función permite a la aplicación principal verificar que la secuencia de acciones sea correcta:

<?php
     // administración de una tabla de artículos
  include "config.php";
  include "articles.php";  

  // sesión
  session_start();
  $dSession=$_SESSION["session"];

   // acción a realizar
  $sAction=$_POST["action"] ? $_POST["action"] : $_GET["action"] ? $_GET["action"] : "authentifier";
  $sAction=strtolower($sAction);
   // fase eventual de la acción
  $sPhase=$_POST["phase"] ? $_POST["phase"] : $_GET["phase"] ? $_GET["phase"] : "0";

     // ¿Hay una sesión en curso?  
  if(! isset($dSession)){
      // autenticación del usuario
    if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
    if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);   
    if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);    
     // acción anómala
    authentifier_0($dConfig);        
  }//si no hay sesión

     // se recupera la sesión
  $dSession=unserialize($dSession);

     // ¿Es normal la secuencia de acciones?
  if( ! enchainementOK($dConfig,$dSession,$sAction,$sPhase)){
     // secuencia anómala
    authentifier_0($dConfig);        
  }//si

     // procesamiento de las acciones
  if($sAction=="authentifier"){
   if($sPhase=="0") authentifier_0($dConfig);  
   if($sPhase=="1") authentifier_1($dConfig);  
   if($sPhase=="2") authentifier_2($dConfig);  
  }//if     
  if($sAction=="addarticle"){
...

7.7.3. Actualizar el «aspecto»

Recordemos que una de las condiciones establecidas al diseñar esta aplicación era que debía ser evolutiva. Supongamos que, al cabo de unas semanas, nos damos cuenta de que hay que mejorar la ergonomía de la aplicación. Modifica la aplicación de tal manera que cambien la estructura y la presentación de la página tipo. Las modificaciones se realizarán en dos lugares:

  • en el script main.php, que define la estructura de la página tipo. Actualice esta estructura.
  • en la hoja de estilo que determina el «aspecto» de la aplicación. Modifíquela.

7.7.4. Mejorar el rendimiento

Por el momento, hemos optado por un navegador cliente ligero: no hace más que encargarse de la presentación. Se le puede hacer realizar procesamiento incluyendo scripts en las páginas web que se le envían. Estos pueden estar en diferentes lenguajes, en particular VBScript y JavaScript. Internet Explorer y Netscape dominan el mercado de los navegadores en una proporción cercana a 60/40. Por otra parte, IE solo existe en el entorno de Windows y no en Unix, por ejemplo, donde predomina Netscape. Netscape no ejecuta de forma nativa los scripts de VBScript, mientras que ambos navegadores ejecutan los scripts de JavaScript. Dado que Netscape aún ocupa una parte significativa del mercado de los navegadores, se deben evitar los scripts de VBScript. Por lo tanto, JavaScript es el lenguaje que se utiliza generalmente en los scripts del lado del cliente.

Se delegan a los scripts del lado del cliente aquellas tareas en las que el servidor no tiene que intervenir. En nuestra aplicación, sería conveniente que el navegador del usuario solo enviara una solicitud al servidor después de haberla verificado. De esta manera, no tiene sentido enviar al servidor una solicitud de autenticación si el usuario ha dejado en blanco el campo [login] en el formulario de autenticación. Es preferible avisarle al usuario que su solicitud es incorrecta:

Image

Cabe señalar que esto no impedirá que el servidor verifique que el campo de inicio de sesión no esté vacío, ya que su cliente no es necesariamente un navegador y, por lo tanto, es posible que la verificación anterior no se haya realizado. Suponer que el cliente es un navegador representa un riesgo importante para la seguridad de la aplicación.

Identifica los distintos momentos en los que el navegador envía información al servidor y, cuando sea posible verificarla, escribe una o varias funciones de JavaScript que permitan al navegador verificar la validez de la información antes de enviarla al servidor.

Retomando el ejemplo anterior, el script login.php que genera la página de autenticación queda así:


<script language="javascript">
    function check(){
       // se verifica que haya un inicio de sesión
    with(document.frmLogin){
        champs=/^\s*$/.exec(txtLogin.value);
      if(champs!=null){
          // no hay inicio de sesión
        alert("Vous n'avez pas indiqué de login");
        txtLogin.focus();
        return;
      }//si
       // los datos están ahí; se envían al servidor
      submit();
    }//con
  }//verificar
</script>   
    
<form name="frmLogin" method="post" action="<?php echo $main["post"] ?>">
    <table>
        <tr>
            <td>login</td>
            <td><input type="text" value="<?php echo $main["login"] ?>" name="txtLogin" class="text"></td>
        </tr>
        <tr>
            <td>mot de passe</td>
            <td><input type="password" value="" name="txtMdp" class="text"></td>
      <td><input type="button" onclick="check()" value="Connexion" class="submit"></td>      
        </tr>
    </table>
</form>    

7.8. Para profundizar

Para concluir, señalamos algunas ideas para profundizar en este caso de estudio:

  • sería interesante ver si la página tipo de esta aplicación podría convertirse en una clase. De esta manera, podría utilizarse en otras aplicaciones.
  • Nuestra aplicación está bien adaptada a clientes de tipo navegador, pero menos a clientes de tipo «aplicación autónoma». Estos deben:
    • crear una conexión TCP con el servidor
    • «comunicarse» con él mediante HTTP
    • analizar sus respuestas HTML para encontrar la información deseada, ya que el cliente autónomo probablemente no estará interesado en el código de presentación HTML destinado a los navegadores.

Sería interesante que nuestra aplicación generara XML en lugar de HTML. De esta manera, sus clientes podrían ser indistintamente navegadores (bastante recientes, claro está) o aplicaciones autónomas. Estas últimas no tendrían ninguna dificultad para encontrar la información que buscan, ya que la respuesta XML del servidor no contendría ninguna información de presentación, solo contenido.

  • Sin duda, habría que prestar atención a los accesos simultáneos a la base de datos de artículos. Hay al menos dos puntos que aclarar:
  1. ¿El SGBD que utiliza la aplicación gestiona correctamente el acceso simultáneo a un mismo artículo? Por ejemplo, ¿qué sucede si dos usuarios modifican el mismo artículo al mismo tiempo (presionan el botón [Modifier] al mismo tiempo)? Probablemente eso dependa del SGBD subyacente.
  2. Actualmente, nuestra aplicación no maneja los accesos simultáneos. Sin embargo, la base de datos debería mantenerse en un estado coherente, aunque cabe esperar algunas sorpresas. Consideremos la siguiente secuencia de eventos:
      • el usuario U1 ingresa para modificar un artículo
      • el usuario U2 inicia la eliminación del mismo artículo poco después
      • Cada una de las dos acciones requiere intercambios entre el cliente y el servidor. Dependiendo de la forma de trabajar de cada uno, el usuario U2 podría terminar su trabajo antes que U1. Cuando este último termine sus modificaciones y las valide con [Modifier], recibirá la página de información como respuesta, en la que SGBD le indicará que [0 ligne(s) ont été modifiées], ya que la página que quería modificar fue eliminada mientras tanto. Sin duda, el usuario se sorprenderá. Desde el punto de vista de la ergonomía, sería preferible mostrar una página que indicara mejor el error. Por otra parte, se podría considerar ofrecer al usuario acceso exclusivo a un artículo tan pronto como inicie su actualización. A otro usuario que desee actualizar el mismo artículo se le respondería que ya hay otra actualización en curso. Esto planteará un problema si el primer usuario tarda en validar su actualización: los demás quedarán bloqueados. Hay que encontrar soluciones para esto, las cuales dependerán en gran medida de las capacidades del SGBD utilizado. Oracle, por ejemplo, tiene más capacidades en este ámbito que el MySQL.