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).
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>
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
Pour l'instant, PostgreSQL se contente d'écouter sur une socket Unix. Mais on peut vouloir le mettre sur Internet.
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.
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.
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.
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.
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.
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
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);
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;
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.
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;
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
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
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
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
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
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';
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).
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>
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
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.
Pareil qu'Apache::ASP.
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.
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.
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; }
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.
Le serveur vient de recevoir une requête et il a vérifié que c'était bien du HTTP conforme.
C'est un synonyme de PerlPostReadRequestHandler, mais c'est plus facile à taper.
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.
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).
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.
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;
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);
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 $@; }
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).
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).
Du point de vue du client, c'est l'étape finale : on (calcule et) renvoie le contenu de l'URI.
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.
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.
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.
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