mod_perl

Installation
Installation de PostgreSQL et DBI
Sécurisation de PostgreSQL
Utilisation de PostgreSQL
Ce que je ne connais pas sur postgreSQL
Utilisation de DBI
Apache::DBI
mod_perl : ça ressemble à du CGI, mais ça n'en est pas.
Exemple : Knowledge Base
Exemple : autentification
Débugguage des pseudo-CGI de mod_perl
Caveats
Divers
Modules
Modules Apache
D'autres manières d'utiliser Perl dans les pages Web : Apache::Run
Autre manière d'utiliser Perl dans les pages Web : SSI
D'autres manières d'utiliser Perl dans les pages Web : Apache::ASP
D'autres manières d'utiliser Perl dans les pages Web : HTML::Embperl
D'autres manières d'utiliser Perl dans les pages Web : HTML::Template
D'autres manières d'utiliser Perl dans les pages Web : HTML::Mason
PerlHandlers
Autres utilisations de mod_perl

mod_perl est un module pour Apache (écrit en C), permettant d'écrire d'autres modules pour Apache, mais en Perl, cette fois-ci.

Il ne concernerait qu'un petit nombre de programmeurs s'il n'était accompagné d'un module (et même plusieurs, en fait) pour écrire des CGI _rapides_ en Perl (s'ils sont invoqués plusieures fois, ils ne sont compilés qu'une fois et les éventuelles initialisations ne sont effectuées qu'une seule fois).

Installation

  tar zxvf mod_perl*tar.gz
  tar zxvf apache*tar.gz
  cd mod_perl*(/)
  perldoc INSTALL.apaci

  APACHE=$HOME/gnu/`uname`
  rm -rf $APACHE/conf/
  perl Makefile.PL \
       APACHE_SRC=../apache_1.3.23/src \
       APACHE_PREFIX=$APACHE \
       DO_HTTPD=1 \
       USE_APACI=1 \
       PREP_HTTPD=1 \
       EVERYTHING=1
  make
  make test
  make install
  cd ../apache*(/)
  ./configure \
    --prefix=$APACHE \
    --activate-module=src/modules/perl/libperl.a \
    --enable-shared=perl
  make
  make install

Il ne faut surtout pas oublier de mettre « USE_APACI=1 » ou d'effacer $APACHE/conf/httpd.conf.

On constate que le module perl apparait dans le fichier de configuration d'Apache. Il est bien chargé, mais il n'est pas utilisé. Rajoutons donc ces quelques lignes.

  vi $APACHE/conf/httpd.conf

  AddModule modules/perl/libperl.a
  <Files *.pl>
    SetHandler perl-script
    PerlHandler Apache::Registry
    Options ExecCGI
  </Files>

Et lançons le serveur.

  $APACHE/bin/apachectl stop
  $APACHE/bin/apachectl start

On peut maintenant l'essayer :

  vi $APACHE/htdocs/1.pl

  #!/usr/local/bin/perl -Tw
  use strict;
  use CGI qw(:all);
  $|++;
  print header;
  print start_html('Essai'), h1('Essai'), hr, p('Ceci est un essai.'), end_html;

Et on n'oublie surtout pas de rendre le fichier exécutable

  chmod +x $APACHE/htdocs/1.pl

Si on demande (comme le serveur n'est pas installé par root, il est sur le port 80 et pas 8080) (il faut une ligne blanche après GET) :

  telnet localhost 8080
  GET /1.pl HTTP/1.0

On nous répond (c'est une très vielle version de CGI.pm : maintenant, ça donne du xhtml).

  HTTP/1.1 200 OK
  Date: Thu, 21 Feb 2002 19:24:39 GMT
  Server: Apache/1.3.23 (Unix) mod_perl/1.26
  Connection: close
  Content-Type: text/html

  <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
  <HTML><HEAD><TITLE>Essai</TITLE>
  </HEAD><BODY><H1>Essai</H1><HR><P>Ceci est un essai.</P></BODY></HTML>

Installation de PostgreSQL et DBI

Rajoutons une base de données. Comme la dernière fois j'avais pris mysql, je vais maintenant me tourner vers PostgreSQL. (C'est surtout que je n'arrive plus à compiler mysql : quand bien même je lui demande de s'installer dans $HOME/gnu/`uname`, il persiste à vouloir écrire dans /var/log -- ce qui m'est bien naturellement interdit.)

  tar zxvf postgresql-7.2b5.tar.gz
  tar zxvf postgresql-base-7.2b5.tar.gz
  tar zxvf postgresql-docs-7.2b5.tar.gz
  tar zxvf postgresql-opt-7.2b5.tar.gz
  tar zxvf postgresql-test-7.2b5.tar.gz

  cd postgres*(/)
  ./configure --prefix=$HOME/gnu/`uname`
  make
  make install

N'oublions pas les modules Perl. D'une part DBI (DataBase Interface), qui est une interface à toutes les bases de données SQL, d'autre part, DBD-Pg (DataBase Driver), le pilote de PostgreSQL.

  cd DBI*(/)
  perl Makefile.PL
  make
  make test
  make install

Sécurisation de PostgreSQL

Pour l'instant, PostgreSQL se contente d'écouter sur une socket Unix. Mais on peut vouloir le mettre sur Internet.

Connections autorisées

Dans le fichier data/pg_hba.conf, on dit quels sont les machines qui ont le droit de se connecter au serveur. Voici la configuration par défaut : on accepte tout ce qui vient de la machine sur laquelle on est, sans aucune authentification.

  # TYPE     DATABASE    IP_ADDRESS    MASK               AUTH_TYPE  AUTH_ARGUMENT
  local      all                                          trust
  host       all         127.0.0.1     255.255.255.255    trust

Il est préférable de rajouter un peu d'authentification.

  # TYPE     DATABASE    IP_ADDRESS    MASK               AUTH_TYPE  AUTH_ARGUMENT
  local      all                                          md5
  host       all         127.0.0.1     255.255.255.255    md5

Les mots de passe ne circulent pas en clair sur le réseau, ils cont codés par md5 (ce qui est à peine mieux).

On fixe les mots de passe à l'aide de la commande pg_passwd.

  A FAIRE

En cas de paranoïa aiguë, on peut demander que les connections soient cryptées par SSL (il faut l'avoir précisé lors de la compilation de PostgreSQL).

  # TYPE     DATABASE    IP_ADDRESS    MASK               AUTH_TYPE  AUTH_ARGUMENT
  local      all                                          md5
  host       all         127.0.0.1     255.255.255.255    md5
  hostssl    all         134.157.0.0   255.255.0.0        md5

Au lieu d'utiliser des mots de passe, on peut demander à PAM de s'occuper de l'authentification (là encore, il faut l'avoir précisé lors de la compilation).

  # TYPE     DATABASE    IP_ADDRESS    MASK               AUTH_TYPE  AUTH_ARGUMENT
  local      all                                          pam
  host       all         127.0.0.1     255.255.255.255    pam
  hostssl    all         134.157.0.0   255.255.0.0        pam

Pour les FAI, on peut créer une base de données par utilisateur et ne les autoriser à se connecter qu'à leur base de données.

  # TYPE     DATABASE    IP_ADDRESS    MASK               AUTH_TYPE  AUTH_ARGUMENT
  local      sameuser                                     md5
  host       sameuser    127.0.0.1     255.255.255.255    md5
  hostssl    sameuser    0.0.0.0       0.0.0.0            md5

On peut aussi regarder le fichier de configuration de PosrgreSQL, data/postgresql.conf. Par exemple, on constate (je ne l'ai pas mentionné) que PostgreSQL, quand il écoute Internet, se trouve sur le port 5432.

Lancement

Il faut donner l'option -i à postmaster pour qu'il accepte les connections internet.

La première fois, il faut initialiser les bases de données. Ne pas oublier le -W qui fixe un mot de passe pour l'administrateur de la base de données.

  DATA=$APACHE/data
  mkdir $DATA
  initdb -W -D $DATA

On modifie le fichier $DATA/pg_hba.conf

  # TYPE     DATABASE    IP_ADDRESS    MASK               AUTH_TYPE  AUTH_ARGUMENT
  local      all                                          md5
  host       all         127.0.0.1     255.255.255.255    md5

On peut ensuite lancer le démon.

  pg_ctl -D $DATA -l $APACHE/logs/postgres start -o -i
  sleep 3

On peut aussi le lancer directement :

  postmaster -i -D $DATA >>$APACHE/logs/postgres 2>&1 &
  sleep 3

Puis créer notre première base de données.

  createdb test
  psql test

On peut regarder ce qui est déjà présent dans la base de données à l'aide de la commande pg_dumpall (qui nous demande le mot de passe une demi-douzaine de fois).

  --
  -- pg_dumpall (7.2b5)  
  --
  \connect template1
  DELETE FROM pg_shadow WHERE usesysid <> (SELECT datdba FROM pg_database WHERE datname = 'template0');
  
  connected to template1...
  
  DELETE FROM pg_group;
  
  
  --
  -- Database template1
  --
  \connect template1 zoonek
  \connect template1 zoonek
  dumping database "template1"...
  --
  -- Selected TOC Entries:
  --
  --
  -- TOC Entry ID 2 (OID 1)
  --
  -- Name: DATABASE "template1" Type: COMMENT Owner: 
  --
  
  COMMENT ON DATABASE "template1" IS 'Default template database';
  
  UPDATE pg_database SET datistemplate = 't' WHERE datname = 'template1';

La commande psql -l nous donne la liste des bases de données.

   List of databases
     Name    | Owner  
  -----------+--------
   template0 | zoonek
   template1 | zoonek
  (2 rows)

La base de données template1 sert de modèle lors de la création d'une nouvelle base de données. On peut la modifier.

La base de données template0 est le modèle initial des bases de données, celui qui avait cours lorsque l'on a lancé initdb. Il ne faut pas la modifier.

Création d'un nouvel utilisateur, d'une nouvelle base, permissions

Un utilisateur normal

  CREATE USER dvdhk PASSWORD 'eweil'

Un utilisateur avec le droit de créer des utilisateurs et des bases de données.

  CREATE USER god PASSWORD 'hpe' CREATEUSER CREATEDB

On crée une base de données

  CREATE DATABASE dvdhk

On en fixe les permissions.

  GRANT SELECT, UPDATE, INSERT, DELETE ON dvdhk TO dvdhk;
  GRANT SELECT ON dvdhk TO guest;

On peut révoquer les permissions à l'aide de la commande REVOKE.

Ménage

Il faut faire le ménage de temps à autre : quand on modifie ou efface des lignes d'un tableau, la place disque est perdue. Il faut lancer un ramasse miettes de temps à autre.

  Recommended practice for most sites is to schedule a database-wide
  VACUUM once a day at a low-usage time of day, supplemented by more
  frequent vacuuming of heavily-updated tables if necessary. (If you
  have multiple databases in an installation, don't forget to vacuum
  each one; the vacuumdb script may be helpful.)

PostgreSQL utilise des numéros de transaction, qui sont en nombre très limité. Si jamais on déborde, on risque de perdre des données. C'est complètement aberrant comme comportement, mais c'est comme ça.

  Every table in the database must be vacuumed at least once every
  billion transactions.

Remarques diverses

On peut créer des bases de données dans un codage particulier.

  createdb -E UNICODE kanji

Il faut avoir compilé PostgreSQL avec l'option --enable-multibyte=UNICODE.

Utilisation de PostgreSQL

Voici un peu de documentation pour PostgreSQL.

   http://www.PostgreSQL.org/users-lounge/docs/.
   http://www.PostgreSQL.org/docs/awbook.html.

Pour ceux qui ne connaitraient pas préalablement SQL :

  http://www.intermedia.net/support/sql/sqltut.shtm
  http://ourworld.compuserve.com/homepages/graeme_birchall/HTM_COOK.HTM.
  http://members.tripod.com/er4ebus/sql/index.htm

Les principales différences entre mySQL et PostgreSQL sont les suiventes.

mySQL implémente une partie beaucoup plus réduite de SQL que PostgreSQL (en particulier, il n'y a pas de SELECT imbriqués -- mais ça n'est pas trop génant).

D'autre part, mySQL n'est pas fiable, i.e., ne garantit pas la non-corruption de la base de données en cas d'erreur (erreur SQL ou plantage complet du système).

Au niveau place disque, mySQL est beaucoup plus économe. Donc si on veut quelque chose de simple, petit, rapide, et si la fiabilité n'est pas primordiale.

PostgreSQL est orienté objet (quoi que cela veuille dire).

  http://openacs.org/why-not-mysql.html

Passons aux modules Perl.

  cd ../DBD-Pg*(/)
  perl Makefile.PL
  POSTGRES=$HOME/gnu/`uname`
  export POSTGRES_INCLUDE=$POSTGRES/include
  export POSTGRES_LIB=$POSTGRES/lib
  perl Makefile.PL
  make
  make test
  make install

On lance PostgreSQL

  DATA=$APACHE/data
  mkdir $DATA
  initdb -D $DATA
  postmaster -D $DATA >>$APACHE/logs/postgres 2>&1 &
  sleep 3
  createdb test
  psql test

On a alors une sorte de shell à l'aide duquel on peut manipuler la base de données. Comme sous mySQL, une base de données (la notre s'appelle test) est un ensemble de tables (aucune pour l'instant).

Je ne sais pas trop comment on est sensé lire la documentation : il y a bien un répertoire doc/src/sgml, mais il ne contient que du sgml et je n'ai pas de quoi le convertir en quelque chose de lisible.

  perl -p -e 's#<.*?>##g' perform.sgml  | less

J'ai trouvé : la documentation au format html a été installée dans $APACHE/doc/postgresql/html/index.html

Je ne fais pas de rappels sur SQL (voir pour cela les notes que j'ai prises en découvrant php et mySQL), je me contenterai de quelques remarques sur ses spécificités.

La commande COPY permet de remplir une table beaucoup plus rapidement que la commande INSERT.

Il y a une commande VACUUM pour mettre de l'ordre dans une table (c'est un peu comme la défragmentation sous Windoze...).

On a le droit d'imbriquer les SELECTs.

  SELECT city FROM weather
  WHERE temp_lo = (SELECT max(temp_lo) FROM weather);

Dans le même ordre d'idées (on peut réécrire cela à l'aide de SELECT imbriqués) (la commande HAVING est très semblable à WHERE, mais elle s'utilise après aggrégation par MAX? MIN, AVG, etc.) :

  SELECT city, max(temp_lo)
  FROM weather
  GROUP BY city
  HAVING max(temp_lo) < 40;

Il est possible de créer des VIEWs, i.e., des requêtes précompilées. Elles s'utilisent ensuite comme des tables. Elles permettent aussi de changer la structure de la base de données sans qu'il soit nécessaire de modifier les programmes qui y font appel : on fait des recherches dans une VIEW, qui est construite d'une manière ou d'une autre à partir des tables.

  CREATE VIEW myview AS
  SELECT city, temp_lo, temp_hi, prcp, date, location
  FROM weather, cities
  WHERE city = name;

  SELECT * FROM myview;

On peut demander à PostgreSQL de vérifier pour nous la cohérence des tables, en s'assurant par exemple qu'une colonne d'une table prend bien ses valeurs dans celles d'une colonne d'une autre table.

  CREATE TABLE cities (
    name            varchar(80) primary key,
    location        point
  );

  CREATE TABLE weather (
    city            varchar(80) references cities,
    temp_lo         int,
    temp_hi         int,
    prcp            real,
    date            date
  );

PostgreSQL sait ce qu'est une transaction.

  BEGIN;
    UPDATE accounts SET balance = balance - 100.00
      WHERE name = 'Alice';
    UPDATE branches SET balance = balance - 100.00
      WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Alice');
    UPDATE accounts SET balance = balance + 100.00
      WHERE name = 'Bob';
    UPDATE branches SET balance = balance + 100.00
      WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Bob');
  COMMIT;

PostgreSQL est orienté objet.

  CREATE TABLE cities (
    name            text,
    population      real,
    altitude        int     -- (in ft)
  );
  CREATE TABLE capitals (
    state           char(2)
  ) INHERITS (cities);
  -- search in all cities, including capitals:
  SELECT name, altitude
    FROM cities
    WHERE altitude > 500;
  -- search in non-capitals
  SELECT name, altitude
    FROM ONLY cities
    WHERE altitude > 500;

Être orienté objet, ça veut dire qu'il est très facile d'implémenter des relations du type « est un » (« is a » en anglais). Par contre, si on a besoin de types de données plus complexes, du genre « a un » (« has a » en anglais), on s'en tire comme d'habitude en SQL, à l'aide de plusieurs tableaux. Par exemple, pour mettre dans une base de données des choses du genre

  { 
    text     => 'foo bar', 
    numbers  => [ 1, 5, 19 ], 
    keywords => [qw/perl postgreSQL/],
  }

on crée plusieurs tableaux :

  CREATE TABLE data (
    id    int,
    text  varchar(80)
  );
  CREATE TABLE data_numbers (
    data_id  int references data, 
    number   int
  );
  CREATE TABLE keywords (
    id       int,
    keyword  varchar(80)
  );
  CREATE TABLE data_keywords (
    data_id    int references data,
    keyword_id int references keywords
  );
  CREATE INDEX data_id_index ON data (id);
  CREATE INDEX data_numbers_data_id_index   ON data_numbers (data_id);
  CREATE INDEX data_numbers_number_id_index ON data_numbers (number_id);
  CREATE INDEX keywords_id_index ON keywords (id);
  CREATE INDEX data_keywords_data_id_index    ON data_keywords (data_id);
  CREATE INDEX data_keywords_keyword_id_index ON data_keywords (keyword_id);

Une autre solution consiste à mettre des tableaux à l'intérieur des tables (si, si, c'est possible...)

  CREATE TABLE sal_emp (
    name            text,
    pay_by_quarter  integer[],
    schedule        text[][]
  );
  -- populating
  INSERT INTO sal_emp
    VALUES ('Bill',
    '{10000, 10000, 10000, 10000}',
    '{{"meeting", "lunch"}, {}}');
  INSERT INTO sal_emp
    VALUES ('Carol',
    '{20000, 25000, 25000, 25000}',
    '{{"talk", "consult"}, {"meeting"}}');
  -- selecting
  SELECT name FROM sal_emp WHERE pay_by_quarter[1] <> pay_by_quarter[2];
   -- array slices:
  SELECT schedule[1:2][1:1] FROM sal_emp WHERE name = 'Bill';
  -- updating
  UPDATE sal_emp SET pay_by_quarter = '{25000,25000,27000,27000}'
    WHERE name = 'Carol';
  UPDATE sal_emp SET pay_by_quarter[4] = 15000
    WHERE name = 'Bill';
  UPDATE sal_emp SET pay_by_quarter[1:2] = '{27000,27000}'
    WHERE name = 'Carol';

Pour avoir 30 réponses sauf les 10 premières :

  SELECT ... ORDER BY ... LIMIT 30 OFFSET 10

Les types utiles à mes yeux sont les suivants.

  BYTEA                  pour les données binaires (images, etc.)
  VARCHAR(n)             chaine de caractères d'au plus n caractères
  TEXT                   chaine de caractères de longueur quelconque
  FLOAT4, FLOAT8         nombre (float, double) à virgule flottante
  SMALLINT, INT, BIGINT  entier (sur 2, 4 ou 8 octets)
  TIMESTAMP, TIMESTAMPTZ date et heure

On peut mettre la date courrante ainsi :

  CREATE TABLE test (x int, modtime timestamp DEFAULT CURRENT_TIMESTAMP );

Il y a aussi plein de types géométriques (pour la géométrie dans le plan : points droites, rectangles, etc.), ainsi que les opérations qu'on peut vouloir leur faire subir.

Les fonctions LIKE et ILIKE permettent de faire des recherche avec des caractères génériques en respectant ou non la casse. Mais on peut aussi utiliser des expressions régulières POSIX (celles de egrep).

  'thomas'  ~   '.*thomas.*'  -- case sensitive
  'thomas'  ~*  '.*Thomas.*'  -- case insensitive

Comparaisons qui ne tiennent pas compte de la casse :

  SELECT *
  FROM tab
  WHERE lower(col) = 'abc'

On peut faire des opérations sur les dates.

  timestamp '2001-09-28 23:00' - interval '23 hours'
  timestamp '2001-09-28'
  timestamp '2001-09-28' < now

Des conditionnelles :

  SELECT a,
         CASE WHEN a=1 THEN 'one'
              WHEN a=2 THEN 'two'
              ELSE 'other'
         END
  FROM test;

La documentation de mySQL suggérait de créer les INDEX lors de la création de la table (sans dire qu'il est en fait possible de les créer ou de les effacer par la suite) ; celle de PostgreSQL suggère de les créer après.

  CREATE TABLE test1 (
    id integer,
    content varchar
  );
  CREATE INDEX test1_id_index ON test1 (id);

Par défaut il utilise des B-arbres (pertinents lors de comparaisons), mais on peut lui demander d'utiliser des R-arbres (idem, mais en dimensions supérieures) ou des tables de hachage (pertinents pour des recherches utilisant uniquement =). Restons avec les B-arbres.

Il est possible de créer un INDEX sur plusieures colonnes ou sur le résultat d'une fonction.

  CREATE INDEX test2_mm_idx ON test2 (major, minor);
  CREATE INDEX test1_lower_col1_idx ON test1 (lower(col1));

On peut aussi créer des INDEX partiels.

  CREATE INDEX access_log_client_ip_ix ON access_log (client_ip)
    WHERE NOT (client_ip > inet '192.168.100.0' AND client_ip < inet '192.168.100.255');
  SELECT * FROM access_log WHERE url = '/index.html' AND client_ip = inet '212.78.10.32';

Le manuel d'utilisation indique comment examiner l'utilisation des INDEX, à l'aide de fonctions comme EXPLAIN, ANALYZE

Ce que je ne connais pas sur postgreSQL

Il est possible de définir des fonctions (je n'ai pas essayé).

  http://www.linuxfrench.net/article.php3?id_article=837

Il est possible de stocker des procédures dans la base de données. L'intéret est double : d'une part, ces suites d'instructions SQL sont déjà compilées ; d'autre part, une seule connection suffit à effectuer l'opération, alros que sinon, il en faudrait plusieures, séparées par quelques calculs (boucles, conditionnelles) du côté du client.

Ces procédures sont écrites dans le langage pl/pgsql, variante du pl/sql d'Oracle (Oracle est LA base de donnée professionnelle de référence : rapide, fiable, complète et très chère).

On commence par autoriser le stockage de procédures dans la base de donnée (ici, elle s'appelle "test").

  export PGLIB=`locate plplsql.so | head -1 | perl -p -e 's#/[^/]*$##'`
  createlang plpgsql test

On peut ensuite définir des fonctions.

  CREATE FUNCTION doit (integer, decimal)
  RETURNS integer
  AS '
      DECLARE foo ALIAS $1;
      DECLARE bar ALIAS $2;
      DECLARE baz decimal;
      BEGIN
        SELECT ... INTO baz FROM ... WHERE ...;
        IF (foo > baz)
        THEN
          ...
        END IF;
        RETURN 0;
      END;
     '
  LANGUAGE 'plpgsql';

Généralement, on met cela dans une transaction, i.e., un bloc BEGIN...COMMIT.

Si la fonction renvoie des choses intéressantes (le plus souvent en cherchant dans les tables, mais sans les modifier), on peut l'utiliser dans une clause WHERE.

Si la fonction se contente d'effectuer des modifications dans les bases de données, sans rien renvoyer d'intéressant, on peut l'utiliser directement avec SELECT.

  SELECT doit(1, 3.14);

Utilisation de DBI

C'est le module Perl qui permet d'accéder aux bases de données SQL. Il ne parle pas directement à la base de données (car bien que toutes les bases de données SQL soient SQL, chacune a sa propre API), mais s'addresse au DBD (DataBase Driver) correspondant.

Ce n'est pas exactement la même chose que le module Apache::DBI, que nous allons utiliser plus loin, et qui permet de gérer des connections persistantes.

Le programme commence par

  #! perl -w
  use strict;
  use DBI;

On ne charge pas nous-même le module pour accéder à la base de données (DBD::Pg). On se connecte ensuite à la base de données

  $dbh = DBI->connect("dbi:Pg:database=test;host=localhost;port="
                      "zoonek", "appofl", 
                      { RaiseError => 1, AutoCommit => 0 })
    or die "Cannot connect to db: $DBI::errstr";

Ça, c'est ce que dit le manuel, mais ça na marche pas (?). J'utilise donc.

  $dbh = DBI->connect("dbi:Pg:dbname=test", "zoonek", "aaaaaa", 
                      { RaiseError => 1, AutoCommit => 1 })
    or die "Cannot connect to db: $DBI::errstr";

Si on n'utilise pas AutoCommit, les changements effectués ne sont pas appliqués tout de suite. Il faut explicitement le demander, à chaque fois qu'on vetu qu'ils soient répercutés.

  $dbh->commit;

L'option RaiseError => 1 demande que les erreurs SQL soient des erreurs fatales. On n'est donc pas obligé de mettre des « or die "..." » partout.

Généralement, on ne donne pas directement la commande SQL complète : on commence par la préparer, et ensuite seulement, on l'utilise, en faisant éventuellement varier les arguments.

  $sth = $dbh->prepare("INSERT INTO table(foo,bar,baz) VALUES (?,?,?)");
  while(<CSV>) {
    chomp;
    my ($foo,$bar,$baz) = split /,/;
    $sth->execute( $foo, $bar, $baz )
      or die "Can't execute statement: $DBI::errstr";
  }

Si on sait qu'on va réutiliser la même requête un peu plus loin, on peut demander à l'ordinateur de se souvenir des requêtes déjà préparées. A utilkiser avec soin, pour éviter les fuites de mémoire.

  $sth = $dbh->prepare_cached("...");

On peut récupérer les résultats d'une recherche sous forme d'un tableau (il faut donc bien faire attention à l'ordre et au nombre de colonnes).

  $sth = $dbh->prepare("SELECT foo, bar FROM table WHERE baz=?");
  $sth->execute( $baz )
    or die "Can't execute statement: $DBI::errstr";
  while ( $row = $sth->fetchrow_arrayref ) {
    print "@$row\n";
  }

On peut aussi les récupérer sous forme de table de hachage (c'est plus lent, mais plus lisible).

  fetchrow_hashref

Il est aussi possible de récupérer toutes les lignes de la réponse en une seule fois.

  fetchall_arrayref
  fetchall_hashref

S'il n'y a qu'une seule commande du même type, il n'est pas nécessaire de la compiler (par contre, si elle avait des paramètres, il serait préférable d'utiliser la commande prepare avec des ?, pour ne pas avoir à se préoccuper des guillemets ou des antislashes).

  $rows_affected = $dbh->do("UPDATE your_table SET foo = foo + 1");

Il y a toutefois une version de do avec des arguments

  my $rows_deleted = $dbh->do(q{
    DELETE FROM table
      WHERE status = ?
    }, undef, 'DONE') or die $dbh->errstr;

Quand on récupère la réponse d'une recherche, on examine les lignes une à une et elles sont aussitôt oubliées. On ne peut pas revenir sur les lignes déjà extraites. Pour cette raison, je ne peux pas traduire directement la fonction de débugguage que j'utilisais avec php.

  function my_sql($q){
    global $db;
    global $debug;
    $result = mysql_query($q, $db);
    if( mysql_errno() ){
      html_debug("QUERY:<PRE>$q</PRE>\nERROR: ". mysql_error());
    } else {
      if($debug){
        html_debug("QUERY:<PRE>$q</PRE>\n".
                   "Affected rows: ". mysql_affected_rows()."<br>\n".
                   "Rows: ". mysql_num_rows($result)."<br>\n".
                   "Columns: ". mysql_num_fields($result)
                  );
        html_table_start();
        // titre des colonnes
        $a = array();
        $i = 0;
        while ($i < mysql_num_fields ($result)) {
          $meta = mysql_fetch_field ($result);
          if (!$meta) { $a[$i] = "?"; }
          else { $a[$i] = $meta->name ."<br>(". $meta->type .")"; } 
          $i++;
        }
        html_table_row($a);
        // valeurs
        while( $myrow = mysql_fetch_row($result) ){
          html_table_row($myrow);
        }
        mysql_data_seek($result,0);
        html_table_end();
      }
    }
    return $result;
  }

Voici une fonction que l'on peut utiliser pour le débugguage. (Il faudrait peut-être la compléter pour afficher toutes les réponses).

  # SQL Query
  sub sql {
    my $sql = shift;
    print STDERR "(SQL) $sql\n";
    my $sth = $dbh->prepare($sql);
    print STDERR "(SQL)   executing with arguments: ".
      ((scalar @_)? join(', ', @_) : "") 
        ."\n";
    $sth->execute(@_);
    print STDERR "(SQL)   affected rows: ".$sth->rows."\n";
    print STDERR "(SQL)   message: ".$sth->errstr."\n" if $sth->errstr;
    if($sth->rows>0 and $sql =~ m/^select/i){
      my $ans = $sth->fetchall_arrayref;
      my $pr;
      eval { $pr = utf8_string($ans->[0]->[0]); };
      $pr = "undef" unless $pr;
      print STDERR "(SQL)   top left element: `$pr'\n";
      return $ans;
    }
    return undef;
  }

  # SQL Query returning the first column as a list
  sub sql1 {
    my $a = sql(@_);
    return () unless defined $a;
    my @ans = ();
    foreach my $line (@$a) {
      push @ans, $line->[0];
    }
    return @ans;
  }

  # SQL Query returning the top left element
  sub sql11 {
    my $a = sql(@_);
    return "" unless defined $a;
    eval { return $a->[0]->[0]; };
  }

Quand on a fini, on peut se déconnecter.

  $rc  = $dbh->disconnect;

Voici un exemple complet (pas très utile : on écrit la table de multiplication de Z/21Z et on regarde comment on peut obtenir 0, 3, 7 et 10).

  #! perl -w
  use strict;
  use DBI;
  
  my $dbh = DBI->connect("dbi:Pg:dbname=test", undef, undef,
                         { RaiseError => 1, AutoCommit => 0 })
    or die $DBI::errstr;
  
  $dbh->do("DROP TABLE dbi_test");
  $dbh->do("CREATE TABLE dbi_test (foo int, bar int, baz int)")
    or die "Cannot create table: $DBI::errstr";
  
  my $sth = $dbh->prepare("INSERT INTO dbi_test(foo,bar,baz) VALUES (?,?,?)");
  
  # Table de multiplication de Z/21Z (pas intègre)
  foreach my $i (0..20) {
    foreach my $j (0..20) {
      $sth->execute( $i, $j, ($i*$j) % 21 )
        or die "Can't execute statement: $DBI::errstr";
    }
  }
  
  $dbh->commit;
  
  $sth = $dbh->prepare("SELECT foo,bar,baz FROM dbi_test WHERE baz=?");
  foreach my $i (0,3,7,10) {
    $sth->execute($i)
      or die "Can't execute statement: $DBI::errstr";
    while ( my $row = $sth->fetchrow_arrayref ) {
      print "$row->[2] = $row->[0] x $row->[1]\n";
    }
  }

  $dbh->disconnect;

Apache::DBI

Il est préférable d'utiliser directement le module Apache::DBI, qui ne va ouvrir la connection qu'une seule fois. On le charge dans le fichier de configuration d'Apache.

mod_perl : ça ressemble à du CGI, mais ça n'en est pas.

Rappelons qu'apache est un serveur Web et que mod_perl est juste un moyen de le configurer, de le contrôler (en perl). On peut modifier, à l'aide de quelques lignes en Perl, n'importe quelle étape de la transaction. En particulier, on peut utiliser mod_perl pour dire à Apache comment interpréter certains types de fichiers (ce qui intervient en fait tout à la fin de la transaction). Par exemple, on peut lui dire que les fichiers *.pl sont des programmes en Perl, qu'il faut exécuter à l'aide de Apache::Registry.

  <Files *.pl>
    SetHandler perl-script
    PerlHandler Apache::Registry
    Options ExecCGI
  </Files>

On peut programmer comme je l'ai fait jusqu'à présent : ça ressemble à du CGI, mais ça n'en est pas. C'est un peu plus rapide (l'interpréteur perl est déjà lancé, et il ne va compiler le script que lors du premier appel, certaines initialisations (lecture des modules, blocs BEGIN) ne seront effectuées que lors de ce premier appel).

En fait, comme le module Apache::Registry est automatiquement chargé, on n'a pas besoin de dire que c'est du perl, ni de charger les modules précisés dans httpd.conf. Ainsi, ce qui suit est bien un programme COMPLET, bien qu'il n'y ait pas de « #!perl » ni de « use ... ».

  my $r = Apache->request;
  $r->content_type("text/html");
  $r->send_http_header;
  $r->print("Hi There!");

Voici un exemple moins trivial.

  #! perl -Tw
  use strict;
  use CGI qw(:all);
  use Apache::Util qw(escape_html);
  
  # debug
  use CGI::Carp 'fatalsToBrowser';
  $|++;
  
  print header,
        start_html('Example');
  
  print p('Here is a sample form -- you may fill it in');
  print start_form,
        p("Enter some text: ", textfield('text') ),
        p("What are your favourite prime numbers? ",
          checkbox_group( -name     => 'number',
                          -values   => [3, 3, 5, 7, 11, 13, 17, 19],
                          -defaults => [7, 17] )),
        p("What is your favorite color? ",
          popup_menu( -name    => 'color',
                      -values  => ['red','green','blue'],
                      -default => 'blue' )),
        submit, 
        end_form,
        hr;
  
  print p('The script was invoked with the following parameters');
  my @names = param;
  print table( Tr(th('Name'),th('Value')),
               map { Tr(td($_),td(param($_))) } @names
             );
   
  my $r = Apache->request;
  print p('Here are the parameters of the connection');
  my $c = $r->connection;
  print table( Tr(td('auth type'),      td($c->auth_type)),
               Tr(td('local addr'),     td($c->local_addr)),
               Tr(td('remote addr'),    td($c->remote_addr)),
               Tr(td('remote host'),    td($c->remote_host)),
               Tr(td('remote ip'),      td($c->remote_ip)),
               Tr(td('remote logname'), td($c->remote_logname)),
               Tr(td('user'),           td($c->user)),
             );
  
  print p('The request was the following');
  print pre(escape_html($r->as_string));
  
  print end_html;

Exemple : Knowledge Base

Une base de données dans laquelle n'importe qui peut ajouter n'importe quoi. Elle est conçue sur le modèle de http://crookshanks.free.fr/dvdhk/ (qui en en php).

  #! perl -Tw
  use strict;
  use CGI qw(:all);
  use Apache::Util qw(escape_html);
  use DBI;
  
  use constant TRUE  => (0==0);
  use constant FALSE => (0==1);
  
  # debug
  use vars qw/$DEBUG/;
  #$DEBUG = TRUE;
  use CGI::Carp 'fatalsToBrowser';
  $|++;
  
  # User name, as provided by GET or POST
  my $user = param('pseudo');
  # has authentification succeeded? (there is no athentification)
  my $auth = FALSE;
  
  ######################################################################
  ##
  ## Configuration section
  ##
  ##
  
  ## Some text
  my $title = "Votre avis sur la qualité des sous-titres anglais des DVDHK";
  my $preamble = 
    p(a({-href=>url(-relative=>1)."?action=view"}, 
        "Liste de toutes les fiches")).
    p(a({-href=>url(-relative=>1).'?action=new'}, 
        "Ajouter un titre")).
    p("Recherche d'un titre: ", 
      start_form(-action => url(-relative=>1)."?action=view"), 
      textfield(qw/-name search/), submit, end_form).
    hr;
  ## Database structure
  my $table = "dvdhk";
  my $field = "title";
  my @database = ({ TEXT => "Pseudo", 
  		  SUB  => \&textfield,
  		  ARG  => [qw/-name pseudo -size 50 -maxlength 255 -default/, $user],
  		},
  		{ TEXT => "Titre de l'anime", 
  		  SUB  => \&textfield,
  		  ARG => [qw/-name title -size 50 -maxlength 255/],
  		},
  		{ TEXT => "Qualité des sous-titres anglais", 
  		  SUB  => \&popup_menu,
  		  ARG  => ["-name", "subtitles", 
  			   "-default", "je ne sais pas", 
  			   "-values", ["bon (fansub)", "bon", "inégal", "mauvais", 
  				       "je ne sais pas"]
  			  ],
  		},
  		{ TEXT => "Référence", 
  		  SUB  => \&textfield, 
  		  ARG  => [qw/-name ref -size 50 -maxlength 255 -default DVD-???/],
  		},
  		{ TEXT => "Commentaires", 
  		  SUB  => \&textarea,
  		  ARG  => [qw/-name comments -rows 10 -columns 50/],
  		},
  	       );
  my @field_names = map { my $a = {@{$_->{ARG}}}; $a->{"-name"} } @database;
  
  # Connection to the database (remember to update DBD, DB name, user name and password)
  my $dbh = DBI->connect("dbi:Pg:dbname=dvdhk", "zoonek", "aaaaaa", 
  		       { RaiseError => 1, AutoCommit => 1 })
    or die "Cannot connect to db: $DBI::errstr";
  
  ##
  ##
  ## no more user-serviceable parts beyond this point
  ##
  ######################################################################
  
  # Redirect if necessary
  if( request_method eq "POST" ){
    # If GET says action=post and POST says otherwise, we follow GET
    if( url_param('action') eq 'post'){ param(action => 'post'); }
    # In other circumstances, POST prevails on GET
    else {
      my %param;
      foreach my $p (url_param) { $param{$p} = url_param($p) }
      foreach my $p (param)     { $param{$p} = param($p) }
      redirect(url."?".join('&', map {"$_=$param{$_}"} (keys %param)));
    }
  }
  
  # Default action
  param(action => 'view') unless param('action');
  
  # HTML header
  print header, start_html($title);
  my @names = param;
  print table( Tr(th(tt('Name')),th(tt('Value'))),
  	     map { Tr(td(tt($_)),td(tt(param($_)))) } @names
  	   ) if $DEBUG;
  print pre("", url(-relative=>)) if $DEBUG;
  print $preamble;
  
  if( param('action') eq "new" ){
    # New entry (if someone has a clue as to how make this readable...)
    print start_form(-action => url."?action=post");
    print table((map { Tr( td($_->{TEXT}), td( &{ $_->{SUB} }(@{ $_->{ARG} }) )) } 
  	       @database), 
  	      Tr(td(submit)));
    print end_form;
  
  } elsif( param('action') eq "post" ){
    # A new entry is being submitted
    my @field_names = map { my $a = {@{$_->{ARG}}}; $a->{"-name"} } @database;
    my $sql = "INSERT INTO $table (". join(',', @field_names).
        ") VALUES (". join(',', map { "?" } @field_names). ");";
    print pre($sql) if $DEBUG;
    print pre("(", join(', ', map {param($_)||''} @field_names), ")") if $DEBUG;
    my $sth = $dbh->prepare($sql);
    $sth->execute( map {param($_)||''} @field_names )
    
  } elsif( param('action') eq "view" ){
    # lists all the entries (or only those matching certain criteria)
    my $sql = "SELECT ". join(', ', @field_names) ." FROM $table";
    $sql .= " WHERE $field LIKE ?" if param('search');
    my $sth = $dbh->prepare($sql);
    print pre($sql) if $DEBUG;
    $sth->execute(param('search')? '%'.param('search').'%' : ());
    if( param('search') ){
      print p("Il y a ", $sth->rows, " fiches contenant `". param('search'). "'");
    } else {
      print p("Il y a ", $sth->rows, " fiches dans la base");
    }
    while ( my $row = $sth->fetchrow_hashref ) {
      print p(table( map {Tr(td($_),td($row->{$_}))} @field_names ));
    }
  
  } else {
    # This shouldn't happen
    print p("Unknown action", param('action'));
  }
    
  print end_html;
  
  =comment
  
  CREATE TABLE dvdhk (
    pseudo    VARCHAR(255),
    title     VARCHAR(255),
    subtitles VARCHAR(255),
    ref       VARCHAR(255),
    comments  TEXT
  );
  CREATE INDEX dvdhk_title_key ON dvdhk(title);
  
  =cut

Exemple : autentification

On pourrait modifier le script précédent pour que n'importe qui ne puisse pas usurper le nom d'un autre. Cela permettrait aussi aux utilisateurs de corriger et mettre à jour les informations qu'ils rentrent. Ce qui suit n'est pas un programme complet, mais juste la partie autentification. Comme je n'ai pas l'habitude de jouer avec des cookies, ce n'est pas très bien écrit (pour bien faire, il faudrait mettre ça dans un module : ce serait toujours mal écrit, mais mes maladresses seraient encapsulées).

  #! perl -Tw
  use strict;
  use CGI qw(:all);
  use Apache::Util qw(escape_html);
  use DBI;
  
  use constant TRUE  => (0==0);
  use constant FALSE => (0==1);
  
  # debug
  use vars qw/$DEBUG/;
  #$DEBUG = TRUE;
  use CGI::Carp 'fatalsToBrowser';
  $|++;
  
  ## Constants
  my @cookie_args = qw(-expires +5y -domain localhost -path /);
  
  ##
  ## Connection to the database
  ##
  my $dbh = DBI->connect("dbi:Pg:dbname=dvdhk", "zoonek", "aaaaaa", 
                         { RaiseError => 1, AutoCommit => 1 })
    or die "Cannot connect to db: $DBI::errstr";
  
  ##
  ## Tidying parameters
  ##
  # CGI.pm makes a difference between POST and GET parameters.
  # I do not want this difference.
  # The value of the 'action' parameter should be taken from the POST method
  # If no value is given, it defaults to 'view'
  # For the other parameters, the value from POST prevails
  if( request_method eq "POST" ){
    param(action => url_param('action')) if url_param('action');
    my %param;
    foreach my $p (url_param) { 
      param($p, url_param($p)) unless param($p);
    }
  }
  param(action => 'view') unless param('action');
  
  ##
  ## Silent authentification (from cookie or login/password)
  ##
  # In case action=login and login succeeds, we set action=view
  my $wander = p(a({-href=>url."?action=login"}, "Please try again to login"), 
  	       "or", a({-href=>url."?action=newaccount"},"Create a new account"));
  my $user = param('pseudo') || cookie(-name => 'pseudo');
  my $auth = FALSE;
  my $cookie;
  my $error;
  if( param('pseudo') and param('action') ne 'newaccount' ){
    my $sth = $dbh->prepare("SELECT password, cookie FROM dvdhk_user WHERE pseudo=?");
    $sth->execute(param('pseudo'));
    if($sth->rows>0){
      my ($correct_password, $correct_cookie) = $sth->fetchrow_array;
      if( param('password') eq $correct_password 
  	or cookie(-name => 'password') eq $correct_cookie ){
        $auth = TRUE;
        param(action => 'view') if param('action') eq 'login';
        $cookie = [ cookie(-name => 'password', -value => $correct_cookie, @cookie_args),
  		  cookie(-name => 'pseudo',   -value => $user, @cookie_args) ];
      } else { $error = "Incorrect password for user `".param('pseudo')."'"; }
    } else { $error = "No such user `".param('pseudo')."'"; }    
    print header(-cookie => $cookie);
  } else {
    print header;
  }
  if($error){ 
    print start_html("Error"), h1($error), $wander, end_html;
    exit;
  }
  
  ##
  ## Login screen
  ##
  if(!$auth and param('action') ne 'login' and param('action') ne 'newaccount'){
    print start_html, $wander, end_html;
  } elsif( (param('action') eq 'login' or param('action') eq 'newaccount')
  	 and request_method eq "GET"){
    print start_html('log in');
    print start_form;
    print table(Tr(td('Name'),td(textfield(-name => 'pseudo'))),
  	      Tr(td('Password'),td(password_field(-name => 'password'))),
  	      Tr(td('e-mail (facultatif)'),td(textfield(-name => 'email'))),
  	     );
    print submit, end_form;
  } elsif (param('action') eq 'newaccount'){
    $auth = FALSE;
    $user = param('pseudo');
    my $password = param('password');
    my $sth = $dbh->prepare("SELECT pseudo FROM dvdhk_user WHERE pseudo=?");
    $sth->execute($user);
    if( $sth->rows >0 ){
      print start_html("Error"), h1("login already in use"), $wander, end_html;
      exit;
    } else {
      $sth = $dbh->prepare("INSERT INTO dvdhk_user (pseudo, password, cookie) VALUES (?,?,?)");
      $sth->execute($user, $password, $password);
      my $cookie = [ cookie(-name => 'pseudo',   -value => $user,     @cookie_args),
                     cookie(-name => 'password', -value => $password, @cookie_args) ];
      print header(-cookie => $cookie), start_html("OK"), 
            h1("User `$user' has been created"), end_html;
    }  
  } elsif (param('action') eq 'login'){
    print header, start_html("BUG"), h1("Internal bug"), end_html;
  } elsif (param('action') eq 'view') {
    print header, start_html("Congratulations"), 
          h1("Congratulations for completing the authentification process"), end_html;
  }
  
  =comment
  
  DROP TABLE dvdhk_user;
  CREATE TABLE dvdhk_user (
    pseudo    VARCHAR(255),
    password  VARCHAR(255),
    email     VARCHAR(255),
    cookie    VARCHAR(255)
  );
  INSERT INTO dvdhk_user (pseudo, password, cookie) VALUES ('zzz', 'aaa', 'azerty');
  
  =cut

Débugguage des pseudo-CGI de mod_perl

C'est un peu plus délicat qu'avec des programmes Perl classiques. D'une part, il n'y a pas de sortie d'erreur (il faut regarder dans les fichiers de log pour avoir les messages d'erreur). D'autre part, le programme va rester dans la mémoire du serveur : les problèmes, comme par exemple une fuite de mémoire peuvent s'accumuler.

On peut demander à perl d'envoyer les messages d'erreur au brozeur. (Il vaut mieux ne pas laisser cela lors du passage en production.)

  use CGI::Carp 'fatalsToBrowser';

Lorsque le fichier est modifié, il sera à nouveau lu et compilé par le serveur. Par contre, on ne vérifie pas si les modules qu'il utilise ont été modifiés (voir toutefois Apache::StatINC).

Pour limiter les fuites de mémoire, on peut utiliser la directive MaxRequestsPerChild (dans httpd.conf).

Pour limiter la mémoire utilisée, on peut demander au serveur de charger les modules souvent utilisés.

  PerlModule Apache::DBI
  PerlModule CGI

Toujours, toujours, ajouter les options -w (warnings) et -T (taint check) et le module strict. On peut même ajouter le module diagnostics (qui ralentit un peu les choses : il faudra l'enlever par la suite), qui donne des messages d'erreur plus explicites.

  #!/usr/local/bin/perl -Tw
  use strict;
  use diagnostics;
  use CGI;

En fait, pour le -T, il faut le préciser dans le fichier de configuration d'Apache.

  PerlTaintCheck On

Il est parfois bon de ne pas bufferiser ce que l'on envoie.

  $|++; # unbuffer stdout

On peut aussi demander à Perl de buffuriser mais, périodiquement, de forcer l'envoi des données.

  $r->rflush; # Où est-ce documenté ???

Être explicite dans ses messages d'erreur.

  open FILE, $filename or die "failed to open $filename: $!"

Utiliser cluck et confess au lieu de warn et die (elles affichent la pile des appels de fonctions tout entière).

  use Carp qw/cluck/;
  open FILE, $filename or confess "failed to open $filename: $!"

On peut même demander à perl d'utiliser cluck pour ses propres avertissements (par exemple, use of undefined value) :

  use Carp ();
  local $SIG{__WARN__} = \&Carp::cluck;
  local $SIG{__DIE__} = \&Carp::confess;

Demander à Apache de garder une trace des avertissements de perl.

  PerlWarn On

C'est valable uniquement pour la machine sur laquelle on teste le programme, sur la machine de production, il faut l'enlever.

  PerlWarn Off

Le programme continue même si la connection a été interrompue. On peut vérifier que la connection a été interrrompue de la manière suivante.

  $r->print("\0");
  $r->rflush;
  last if $r->connection->aborted;

Le plupart du temps, le programme ne peut pas être lancé depuis la ligne de commande : il peut utiliser le module Apache::Registry, ou il peut avoir besoin de cookies. Pour voir ce qui se passe quand ça plante vraiment sans que les logs ne donnent d'information utile, on peut utiliser Apache::FakeRequest.

  PerlModule Apache::DBI

Pour tester le programme en situation « réelle », on peut utiliser les différents modules LWP.

  perldoc LWP::Simple
  perodoc LWP

Caveats

Les blocs BEGIN ne sont exécutés qu'une seule fois, à la compilation (même si le programme est lancé plusieures fois). Pour les blocs BEGIN qui sont dans un module, c'est plus compliqué (je n'ai pas compris). Si on a vraiment besoin des blocs BEGIN ou END, on peut regarder du côté des Perl*Handlers.

  perldoc mod_perl.pod

Utiliser le module Apache::DBI plutôt que DBI pour accéder à une base de donnée : la connection sera persistente.

Bien faire la différence entre le variables globales, qui seront toujours là, avec leur bvaleurn lors des exécutions suivantes du script, et les variables locales qui disparaissent à la fin de l'excécution, et ne sont plus là lors de l'invocation suivante.

  use vars qw($foo); # variable globale
  my $bar; # variable locale

Divers

Faire attention aux espaces de nommage.

  Scripts under Apache::Registry are not run in package main, they are
  run in a unique namespace based on the requested uri.

Ne pas mettre de bloc __END__ ou __DATA__.

La fonction exit() est en fait Apache::exit() : c'en est fini pour la page en question, mais le démon httpd et l'interpréteur perl qu'in contient tournent toujours.

La commande print n'envoie pas tout de suite son argument au client : tout est stocké dans un buffer. La commande rflush permet de d'envoyer de contenu de ce buffer au client.

On a parfois besoin d'un symbole non utilisé.

  If you want an unused glob (if you call it A, it may already be
  assigned by another thread in the same process).

  use Symbol;
  my $fh = gensym;
  
  open $fh, "/tmp/foo" or die $!;

Le module Apache::Util définit quelques fonctions intéressantes.

  use Apache::Util qw(:all);

  escape_html
  escape_uri
  unescape_html
  unescape_uri
  parsedate
  ht_time (format a time string)
  validate_password

Divers :

  Apache::File->tmpfile

Modules

On peut aussi écrire, non pas tant des scripts, que des modules. C'est beaucoup plus propre, et on peut les réutiliser dans des pages différentes.

  A FAIRE
  On met ça dans le .htaccess ??? (ou dans le *.conf)
           PerlRequire         /full/path/to/script/Trans.pl
         PerlTransHandler   Trans::handler

  Where can I find examples to get me started?
  Check out the Apache-Perl-contrib tarfile at
  http://perl.apache.org/src/
  use libs './modules';

Modules Apache

Il existe plein de modules Apache. Ils ne sont d'ailleurs pas tous installés par défaut.

Apache::Session permet de suivre un utilisateur.

Apache::Storable permet d'associer des informations à un visiteur, informations qui vont le suivre lors de son voyage dans le site (à l'aide de cookies).

D'autres manières d'utiliser Perl dans les pages Web : Apache::Run

Au lieu d'utiliser Apache::Register, on peut aussi utiliser Apache::PerlRun, qui va compiler à chaque fois le programme (exactement comme pour un CGI), mais qui pourra utiliser des modules déjà présents en mémoire (un peu plus rapidement qu'un CGI, donc). Pour utiliser Apache::Run, il suffit juste de le dire dans httpd.conf. À ÉVITER.

  Alias /cgi-perl/ /perl/apache/scripts/
  PerlModule Apache::PerlRun

  <Location /cgi-perl>
  SetHandler perl-script
  PerlHandler Apache::PerlRun
  Options +ExecCGI
  #optional
  PerlSendHeader On
  ...
  </Location>

Autre manière d'utiliser Perl dans les pages Web : SSI

Les SSI (server-side includes) peuvent contenir du code Perl.

  <!--#perl sub="MySSI::my_function" arg="foo" arg="bar"-->

  <!--#perl arg="foo" arg="bar"
            sub="sub {...}"
  -->

pour que ce soit interpréte, il faut que ce soit dans un fichier *.shtml et qu'Apache soit configuré pour accepter les SSI.

  AddType text/html .shtml
  AddHandler server-parsed .shtml

D'autres manières d'utiliser Perl dans les pages Web : Apache::ASP

Les pseudo-CGI que nous avons vus jusqu'à présent sont des programmes parsemés de code HTML. Certains (les concepteurs de page Web, par opposition aux programmeurs) peuvent préférer écrire du code HTML, avec, de temps à autres (quand on ne peut pas faire autrement), du code Perl. De tels individus peuvent regarder du côté de Apache::ASP. C'est exactement comme php (on mélange du HTML et du code), sauf que c'est du Perl.

D'autres manières d'utiliser Perl dans les pages Web : HTML::Embperl

Pareil qu'Apache::ASP.

D'autres manières d'utiliser Perl dans les pages Web : HTML::Template

Le principe est différent : on sépare vraiment le code HTML du code Perl. On met le code HTML dans des fichiers *.tmpl, dans lequel on utilise des "variables"

  <TMPL_VAR NAME=COLOR>

ou des "boucles"

  <TMPL_LOOP ROWS>
    ...
    <TMPL_VAR NAME=FOO>
    ...
    <TMPL_VAR NAME=BAR>
    ...
  </TMPL_LOOP>

On peut même utiliser ce genre de chose à l'intérieur des balises HTML (donc ce n'est pas du SGML).

  <IMG  WIDTH=320 HEIGHT=256 SRC="<TMPL_VAR NAME=I>" >

On appelle ce fichier depuis un programme Perl.

  #! perl -w
  use strict;
  use HTML::Template
  my $t = HTML::Template->new(filename => 'foo.tmpl');
  $t->param(COLOR => 'blue',
            I     => 'foo.gif',
            ROWS  => [ { foo => 1, bar => 2 }, { foo => 3, bar => 4 } ]);
  print "Content-Type: text/html\n\n";
  print $t->output;

On peut utiliser le module CGI pour manipuler les champs d'un formulaire ou les cookies.

On dispose aussi d'inclusions

  <TMPL_INCLUDE NAME="file.tmpl">

ou de conditionnelles

  <TMPL_IF NAME=COND>
    ...
  <TMPL_ELSE>
    ...
  </TMPL_IF>

Il est possible de demander à mod_perl de garder dans un cache les modèles précompilés.

  my $t = HTML::Template->new(filname => 'foo.tmpl', cache => 1);

On peut aussi lui demander d'utiliser IPC::Shareable pour cela (un seul cache pour tous les serveurs httpd).

  my $t = HTML::Template->new(filname => 'foo.tmpl', shared_cache => 1);

On peut aussi lui demander de « compiler encore plus », à l'aide de HTML::Template::JIT.

D'autres manières d'utiliser Perl dans les pages Web : HTML::Mason

De prime abord, ça ressemble beaucoup à php : on peut mettre du code Perl au milieu du HTML.

  <%perl>
    ... (code Perl)
  </%perl>

  % ... (code Perl)
  % ... (suire du core Perl)
  % ...

Ça ressemble aussi aux templates.

  Il y a <% $sth->rows %> réponses.

On peut même appeler des « fonctions » au milieu du code HTML (ici, bar et baz sont les arguments, qui valent respectivement 1 et 2).

  <& foo, bar => 1, baz => 2 &>

Mais la principale caractéristique est la décomposition du code en composantes. Une composante, c'est juste un fichier qui a l'air d'être du HTML mais qui est en fait une fonction Perl (oui, les arguments sont mis à la fin). Ce fichier s'appelle « foo » (si on rajoute une extension, elle fera partie du nom de la composante quand on l'appellera).

  <address>
  <a href="<% $address %>"><% $name ? $name : $address %>
  </address>
  <%args>
    $name => ""
    $address => "no.one@no.where"
  </%args>

La composante s'appelle comme on l'a vu

  <& foo, bar => 1, baz => 2 &>

ou ainsi :

  <% mc_comp('foo', bar => 1, baz => 2) %>

À la fin d'une composante, on mentionne ses arguments (et leurs valeurs par défaut) dans un bloc <%args>, des action à effectuer avant chaque exécution dans un bloc <%init>, et des actions à n'effectuer que lors du premier appel dans un bloc <%once>.

HTML::Mason est donc très semblable à Zope.

Dans le code Perl, on peut utiliser l'objet requête d'Apache $r, comme d'habitude, et l'objet masson $m. Par exemple, $m->file("foo.txt") va lire le fichier foo.txt et l'inclure.

S'il y a une composante autohandler dans un répertoire, elle est invoquée en premier. Elle appelera explicitement la componsante demandée par $m->call_next.

  <html>
  <head>
    <title>...</title>
  </head>
  <body>
  <% $m->call_next %>
  <& footer, address => 'zoonek@math.jussieu.fr', name => 'Vincent' &>

Quand une composante n'existe pas, on appelle la composante dhandler. On peut l'utiliser pour personnaliser le message d'erreur 404, ou pour aller chercher le fichier ailleurs.

Pour ne pas perdre l'impression qu'on joue avec des pages Web et pas des fonctions Perl, on ne définit pas des routine à l'aide de sub foo { ... }, mais dans un bloc <%method>.

  <%method foo>
    ... (HTML code, with embedded Perl code, as usual)
  </%method>

Le bloc <%filter> permet de modifier un morceau de HTML déjà généré. Par exemple (dans un pied de page, qui contient plein de liens, y compris un lien à la page courrante, que l'on retire).

  <%filter>
    my $uri = $r->uri;
    s {<a href="$uri/?">(.*?)</a>} {<b>$1</b>};
  </%filter>

De nombreux modules tournent autour de HTML::Mason. Ainsi, Mason-CM est un module de « Content Management » : si j'ai bien compris, on utilise le serveur Web comme un serveur CVS, pour permettre aux développeurs de modifier les pages.

PerlHandlers

On peut utiliser mod_perl pour contrôler chaque étape du traitement d'une requête. La plupart des exemples que je cite viennent du livre de Lincoln Stein and Doug MacEachern, Writing Apache Modules with Perl and C, où ils sont détaillés, et dont quelques chapitres sont disponibles sur http://modperl.com:9000/book/chapters/ch7.html

La syntaxe est la suivante :

  PerlAccessHandler Apache::GateKeeper
  PerlSetVar Gate open

On peut mettre ces lignes dans httpd.conf ou (pas toujours) dans les fichiers .htaccess.

  PerlAccessHandler Apache::GateKeeper
  PerlSetVar Gate open

  order deny,allow
  deny from all
  allow from 134.157

Les variables sont nécessairement des chaines de caractères, de préférence entre guillemets, sans retour chariot (sinon, il faut mettre un \ devant le retour chariot, même s'il y a des guillemets). On récupère leur valeur ainsi :

  $r->dir_config("Gate")

Néanmoins, Perl est capable de transformer une chaine de caractères en n'importe quoi.

  @foo = split /\s*,\s*/,      $r->dir_config('Foo');
  %bar = split /\s*(=>|,)\s*/, $r->dir_config('Bar');

Le module Foo::Bar doit contenir une méthode handler qui renvoie OK (on passe à l'étape suivante) ou DECLINED (on essaie les autres modules de la même étape, s'il n'y en a pas, on passe à l'étape suivante). Ces constantes sont définies dans Apache::Constants.

  sub handler {
    my $r = shift;
    ...
    return OK;
  }

Un module peut rajouter des handlers :

  sub handler {
    my $r = shift;
    my $uri = $r->uri;
    ...
    $u->uri(...); # new URI
    $r->handler("perl-script");
    $r->push_handlers( PerlHandler => \&foo_bar );
    return DECLINED;
  }

PerlChildInitHandler

Le serveur vient de se forker, mais n'a encore traité aucune requète. C'est là que l'on mettra diverses initialisations. Voir par exemple Apache::DBI.

PerlPostReadRequestHandler

Le serveur vient de recevoir une requête et il a vérifié que c'était bien du HTTP conforme.

PerlInitHandler

C'est un synonyme de PerlPostReadRequestHandler, mais c'est plus facile à taper.

PerlTransHandler

Traduction de l'URI. La plupart du temps, il s'agit juste de rajouter DocumentRoot devant. C'est à ce niveau qu'interviennent les directives Alias ou Redirect. C'est aussi ici que l'on pourrait convertir les requêtes /CPAN/... en www.cpan.org à l'aide de mod_proxy. On pôurrait aussi mettre un proxy anonyme ou un proxy anti-pub. C'est aussi ici que l'on mettrait un module qui créerait des fichiers *.tar.gz à la demande.

PerlHeaderParserhandler

Apache examine l'en-tête de la requête. C'est ici que l'on peut bloquer les robots polis, ou implémenter une méthode HTTP non supportée ou non standard (autre que GET, HEAD, POST, PUT, DELETE, PATCH).

PerlInitHandler

Généralement, c'est un synonyme de PerlPostReadRequestHandler, mais dans une section <Location>, <Directory> ou <Files>, il intervient juste après la traduction des URI : c'est un synonyme de PerlHeaderParserhandler.

PerlAccessHandler

On regarde si le fichier est accessible (mais on ne sait pas encore qui le demande). C'est à ce niveau-là que l'on peut interdire l'accès de certaines pages à certaines heures (voir les modules Time::*). C'est aussi ici que l'on peut bloquer les robots polis ou malpolis (en utilisant IPC::Shareable pour avoir une variable commune à tous les démons, ou en utilisant une base de données).

Ces modules peuvent renvoyer OK, DECLINED, mais aussi FORBIDDEN.

Généralement, on ne contrôle ce genre de chose que pour la requête principale, pas pour ses sous-requêtes.

  return OK unless $r->is_initial_req;

PerlAuthenHandler

On vérifie que l'utilisateur est bien qui il prétend être, à l'aide d'un login et d'un mot de passe (que l'on peut vérifier à l'aide d'une base de données),

Pour le fichier de configuration, la syntaxe est un peu différente.

  AuthName "Registered Users"
  AuthType Basic
  PerlAuthenHandler Apache::MyAuth
  PerlAuthzHandler  Apache::MyAuthz
  require valid-user

Ici, "Registered Users" est le nom du « domaine » (realm, en anglais), Basic est la méthode d'identification requise (Basic est reconnue par la pupart des brozeurs), Apache::MyAuth et Apache::MyAuthz sont les modules que l'on écrit.

Ce module peut renvoyer OK, DECLINED, AUTH_REQUIRED

Ce module pour fonctionner de pair avec un module d'autorisation.

  $r->push_handlers(PerlAuthzHandler =>\&foobar);

PerlAuthzHandler

On a vérifié que l'utilisateur était bien qui il était, on regarde maintenant s'il a le droit d'accéder à la page en question. On peut faire cela en consultant une base de données contenant les droits de chacun. On peut aussi jouer avec des cookies, ou avec les certificats SSL (avec mod_ssl).

La syntaxe est le même que précédemment.

  AuthName FooBar
  AuthType Basic
  PerlAuthenHandler Apache::MyAuth
  PerlSetVar AuthDB Pg:auth
  PerlAuthzHandler  Apache::MyAuthz
  require $user_name eq 'fred'
  require $level >= 2 && $groups =~ m/authors/;

On récupère les arguments de require, et on les évalue ainsi (ATTENTION, dans cet exemple, les requires sont reliés par des OU, ce n'est probablement pas ce que l'on veut).

  $requires = $r->requires;
  return DECLINED unless $requires;
  ...
  foreach my $entry (@$requires) {
    my $op = $entry->{requirement};
    $op =~ s/\$\{?(\w+\}?/\$DB{'$user'}{$1}/g;
    return OK if eval $op;
    $r->log_errors($@) if $@;
  }

PerlTypeHandler

On détermine le type MIME du fichier. D'habitude, d'est fait en regardant son extension, mais on peut imaginer faire cela en regardant dans une base de données ou en regardant le contenu du fichier (mais ce n'est pas une bonne idée).

PerlFixupHandler

Ce que l'on veut faire juste avant d'envoyer le fichier. Par exemple modifier l'en-tête de la réponse, ou ajouter des cookies pour suivre un utilisateur (dans un site Web dont les pages sont essentiellement statiques).

PerlHandler

Du point de vue du client, c'est l'étape finale : on (calcule et) renvoie le contenu de l'URI.

PerlLogHandler

On a répondu à la requête (mais on n'a pas encore dit au client qu'elle était finie -- il attend toujours, le pauvre), et on écrit quelque part ce que l'on a fait : dans un fichier (c'est ce que l'on fait d'habitude) , dans une base de données, ou même dans un mail.

PerlCleanupHandler

La requête est terminée (et on a dit au client qu'elle était terminée), on fait le ménage. C'est par exemple à ce moment-là que Apache::File->tmpfile efface le fichier temporaire créé. Certains modules (CGI) réinitialisent leurs variables globales.

PerlChildExitHandler

Le serveur (fils) va s'arréter. Par exemple parce que l'on arrête définitivement le serveur, ou parce qu'il a atteint le nombre maximal de requêtes.

Autres utilisations de mod_perl

On peut définir de nouvelles directives, que l'on utilisera dans le fichier httpd.conf ou dans les .htaccess.

On peut mettre des sections en Perl dans les fichiers de configuration. Ainsi

  User www

peut se réécrire

  <Perl>
    $User='www';
  </Perl>

mais on pourrait mettre n'importe quel code Perl pour calculer la valeur de $User.

Si la directive ne prend pas d'argument, on lui assigne la valeur '', si elle prend plusieurs arguments, on la considère comme une liste. S'il y a plusieurs arguments et si la directive est répétée, on la considère comme une liste de références sur des listes, etc.

  <Perl>
     @AddEncoding = ( [qw/x-compress Z/], [qw/x-gzip gz tgz/] );
  </Perl>

Ça devient vite assez compliqué (il y a plein de choses cachées dans le « etc. » précédent), on peut se contenter de modifier la variable $PerlConfig.

  <Perl>
    $Perlconfig  = "User www\n";
    $PerlConfig .= "Group www\n";
  </Perl>

Vincent Zoonekynd
<zoonek@math.jussieu.fr>
latest modification on mer mar 13 09:09:47 CET 2002