4th of Aug. 2011 by Michael

Frontend Projects

Pimprenelle

This is a little website I wrote some 20 years ago, for playfully exploring a picture catalog. It has a somewhat interesting architecture; there are only two files to it:

pimprenelle.cgi
pimprenelle.db
All images copyr. 2026 Patricia Gabriac
All images copyr. 2026 Patricia Gabriac

The .db file is an SQLite database.

It contains images (SQLite blobs), localised string resources and page templates, based on a parent-child hierarchy in the tables. So for example, the page template can query all the images and French captions for a particular page, and will know which page to open when the user clicks on an image that “opens” a set, or when a back navigation button is pressed.

The CGI script is written in Perl and looks like the code below. All the rest of the app is actually Perl code stored in SQLite text columns. I was writing a website generator called “Recap” based on this DB centered code, and this website is the only example.

What I also still like about Recap is that it handles sessions using query parameters, not cookies. It also did its own session management and client logging. The latest backup of the site I found when preparing this post still had a full 6 year history of client requests, although the server logs where long gone.

#!/run/current-system/sw/bin/perl

use strict;
use warnings;

use CGI qw(:all);
use CGI::Carp qw(fatalsToBrowser set_message);
use DBI;
use HTML::Entities;
use Digest::MD5 qw(md5_hex);
use GD;

my $db_debug;
my $dbh = open_dbi();

my (%globalCode, %globalScalar);

BEGIN {
    sub handle_errors {
        my $msg = shift;

        print "<h1>Carp</h1>";
        print "$msg";

        print "<h2>globalCode Array</h2>";
        foreach (sort keys %globalCode) {
            print  . ':' . $globalCode{} . "<br>\n";
        }
    }

    set_message(\&handle_errors);
}


#
# Pull in subroutine definitions from the database.
#
my $sth = sql_query_handle(
    qq{
        select string_id, literal
        from strings
        where stringtype_id = 'SUBROUTINE'
        order by string_id
    }
);

while (my @row = $sth->fetchrow_array()) {
    $globalCode{$row[0]} = eval $row[1];

    if () {
        if (exists $globalCode{'aardvark'}
            && ref($globalCode{'aardvark'}) eq 'CODE') {

            $globalCode{'aardvark'}->($row[0], );
        }
        else {
            die "Error loading subroutine '$row[0]': ";
        }
    }
}

$sth->finish;


#
# Run the application's main subroutine.
#
if (exists $globalCode{'main'}
    && ref($globalCode{'main'}) eq 'CODE') {

    $globalCode{'main'}->();
}
else {
    die "Pimprenelle database does not contain a valid 'main' subroutine";
}


$dbh->disconnect
    or die "Cannot disconnect from the database";


#
# Open the Pimprenelle SQLite database.
#
sub open_dbi {
    my $dbh = DBI->connect(
        "dbi:SQLite:dbname=/srv/pimprenelle/pimprenelle.db",
        "",
        "",
        {
            RaiseError => 1,
            AutoCommit => 1,
        }
    );

    return $dbh;
}


#
# Prepare and execute an SQL query.
#
sub sql_query_handle {
    my ($sql, @params) = @_;

    my $sth = $dbh->prepare($sql);

    if (defined $sth) {
        my $n = 1;

        foreach my $param (@params) {
            $sth->bind_param($n++, $param);
        }

        $db_debug .= "\n$sql\n";

        $sth->execute()
            or die "Couldn't execute statement: "
                 . $sth->errstr
                 . $db_debug;
    }

    return $sth;
}