Tuesday, July 11, 2006

Perl in Postgres tips

I recently needed to convert the type of a Postgres text field from varchar to date. There would probably have been a pure SQL solution, but since using Perl in Postgres had been so easy last time I tried, it seemed like the obvious and simple solution to use Perl again.

However, while writing the text2date function below, I lost quite some time with a few little traps of plperl. So here is my short list of tips for using PL/Perl in PostgreSQL:
  • All examples suggest using $$ as a delimiter in function definitions, but that only works in recent versions of PostgreSQL. With 7.4 - the version currently in Debian stable (Sarge) - you cannot use $$ and need single quotes instead.
  • When using single quotes, backslashes must be escaped (doubled) in the Perl code. You will get strange results if you forgot that.
  • To see the code as Postgres actually stored it:
    SELECT prosrc FROM pg_proc WHERE proname = 'YourPerlFunction';
  • warn doesn't work. Use elog(level, msg) instead. The documentation for elog says:

elog(level, msg)
Emit a log or error message. Possible levels are DEBUG, LOG, INFO, NOTICE, WARNING, and ERROR.
ERROR raises an error condition; if this is not trapped by the surrounding Perl code, the error propagates out to the calling query, causing the current transaction or subtransaction to be aborted. This is effectively the same as the Perl die command.
Finally, here is the text2date function I used:
CREATE OR REPLACE FUNCTION text2date(text) RETURNS "date" AS '
$_=shift;
my ($day, $month, $year) = /^\\s* (\\d{1,2}) [^\\d]+ (\\d{1,2}) [^\\d]+ (\\d{4}) .* /x;
if ( !($day && $month && $year) ) {
elog(WARNING, "No date found in $_\\n");
return undef;
}
elsif ($day > 31 || $month > 12 || length($year) != 4) {
elog(WARNING, "Wrong date: $year-$month-$day\\n");
return undef;
}
return "$year-$month-$day";
' LANGUAGE 'plperl' IMMUTABLE STRICT;
And this is how it was stored in Postgres:
# SELECT prosrc FROM pg_proc WHERE proname = 'text2date';

$_=shift;
my ($day, $month, $year) = /^\s* (\d{1,2}) [^\d]+ (\d{1,2}) [^\d]+ (\d{4}) .* /x;
if ( !($day && $month && $year) ) {
elog(WARNING, "No date found in $_\n");
return undef;
}
elsif ($day > 31 || $month > 12 || length($year) != 4) {
elog(WARNING, "Wrong date: $year-$month-$day\n");
return undef;
}
return "$year-$month-$day";

Obviously, this is for European style dates.

Labels:

Tuesday, July 04, 2006

Liens email dans les pages web

J'ai eu un peu de mal à retrouver ce lien, alors je suppose que ça vaut la peine de l'inclure dans un petit article de blog. (Finalement, mon attention a été détournée par de nombreux à-côtés; les liens pertinents sont dans les tout derniers paragraphes).

Bien qu'on puisse considérer que la lutte anti-spam s'apparente à un ré-arrangement des transats sur le pont du Titanic, il faut quand même encore mettre des liens email dans des pages web. Là, bien sûr, ils sont immédiatement collectés pour servir à nous inonder de "pourriels", comme disent les français.

Comment avoir des liens "mailto:" cliquables, sans que tous les robots de spam en profitent? Les trucs simples comme "user [at] example [dot] com", ou les plus compréhensibles mais très peu pratiques images montrant le texte de l'adresse ne sont pas cliquables. N'essayez même pas les vieux trucs avec les entités HTML (@ pour "@" etc.); ça fait longtemps que ça ne marche plus. Il n'existe plus guère de robot qui s'y prenne les pieds.

Ce qui marche bien est le javascript, que les robots collecteurs d'adresse ne décodent pas; du moins pas encore, à ma connaissance. Mais la plupart de ces astuces javascript qu'on trouve sur le web utilisent un chiffrage antique (littéralement), et même pas sa version ROT13, mais un décalage de 1. Etant donné qu'une simple expression Perl comme s/(.)/chr(ord($1)-1)/eg; permet de le décoder, je doute que ça résiste longtemps. Tiens pendant que j'y suis, voilà un script Perl complet qui suffit pour extraire d'une page web les emails cryptés de cette façon (quel que soit le décalage utilisé):
use LWP::Simple;
my $url = shift or die "Usage: $0 URL\n";
my $html = get($url);

while ( $html =~ /href=['"]javascript:.*?\(['"](.*?)['"]\)/g ) {
my $crypted = $1;
foreach my $i (-25..25) {
my $try = $crypted;
$try =~ s/(.)/chr(ord($1)+$i)/eg;
if ($try =~ /@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}/) {
print "$try (i=$i)\n";
}
}
}

Bref, je cherchais un peu mieux que ça.

Un certain Jim à qui on essayait de vendre un de ces scripts minables a poussé la solution javascript usuelle (autre exemple ici) à un niveau de cryptage un peu plus actuel. Avec Email Protector, il fournit une page où on peut crypter son adresse email. Il n'y a plus qu'à copier le code fourni aux bons endroits, après avoir séparé du code le lien mailto: lui-même et les fonctions javascript aux noms "obfusqués" et pittoresques. Certains trouveront cette solution exagérée, mais justement, je trouve aussi un certain charme là où d'autres ne verront que du "over-engineered".

Update: Finalement, j'utilise ça régulièrement, et mes suis même adapté le cryptage des adresses en un petit script Perl (disponible ici en .pl et en .bat), pour pouvoir l'utiliser directement à la ligne de commande et obtenir la partie à coller dans le HTML:

#mailcrypt 11 31 user@example.com
<!-- crypted email link -->
<a href="javascript:var N=341,D=43;bid('127 300 95 104 4 95 263 202 318 7 147 95 271 99 133 318',N,D)"
title="Click to send email"
class="mail"<Email</a>

Au préalable, il faut évidemment ajouter
<script type="text/javascript" src="/js/emailProtector.js"></script> dans la page, et sauver l'excellent script de Jim Tucek.

Labels: , , , , , ,