Perl CGI Framework

Al met al heb ik ondertussen een beetje een standaard structuur in al mijn CGI scripts, om snel andere scripts op te zetten (zoals voor mijn Sinterklaaslijstjes). Hier wat uitleg over de delen die de basis vormen.

Begin van het CGI script

Hier wordt het script opgezet. Een aantal zaken zijn hier van belang:

  • Het zetten van de -T optie, vanuit het punt van veiligheid (zie mijn vorige CGI pagina, sectie over veiligheid)
  • Ook zet ik waarschuwingen altijd aan, om programmeerfoutjes snel te vinden
  • Dan volgt er een stuk commentaar, dat ook als helptekst gebruikt kan worden (bij uitvoer van het programma met de -help of -? optie, waar dan ook vervolgens op getest wordt). Dit kan normaal alleen via de command line, niet als cgi in een web page.
  • Plus nog misschien wat zaken om de tekenset goed in te stellen, bijvoorbeeld op utf-8, niet hier maar zie verderop.

#!/usr/bin/perl -T
# Framework for new CGI scripts, Kees Moerman (c) 2004-2010

# set some security and programming mistake preventions
# (note also -T option in line 1)

use warnings; # comparable to -w option
use strict; # don't forget to prefix vars with my

my 1 = "v2.0.0019"; # version number tracking
my $help_text = <<HELP; # help text is placed below, up to HELP

# $0 version 1, Kees Moerman (c) 2004-2010
#
# Syntax: run as cgi script on web server, not via command line
#
# Put a more extensive desription here...
#
# Structure of the program is a kind of state machine,
# with the current state (plus machine state) encoded into the forms
# (as the cgi protocol is stateless).
#

HELP
$help_text =~ s/^# ?//gm; # clean up comment characters

#
# Change list:
# ....

#
# To do:
# ....

# call from command line possible, e.g. 'script.pl a=1 b=2'
# run from command line with help request?

if (defined $ARGV[0] && ($ARGV[0] eq "-?" || $ARGV[0] eq "-h" || $ARGV[0] eq "-help"))
{ print $help_text;
  exit 1;
}

Opzetten van de CGI module

Het opzetten van de CGI module, ook weer met wat veiligheidsinstellingen, maar ook dusdanig dat foutmeldingen ook in de browser terecht komen, in plaats van in een of andere duistere server log file.

# set up CGI module and supporting items, esp for error handling
# This way, we do not have to look through server logs to find warnings

use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
use CGI qw/:standard -no_xhtml/; # load standard CGI routines
$CGI::POST_MAX=1024 * 10; # max 10K size posts
$CGI::DISABLE_UPLOADS = 1; # no uploads allowed

warningsToBrowser(1);
BEGIN { warningsToBrowser(1); } # want warning as HTML comments

# if you need only one CGI object, you can use the function form instead
my $cgi = new CGI;

# cgi caller error handler
my $error = $cgi->cgi_error;
if ($error) # any error already while opening script?
{ # then try to make the best of it
  print $cgi->header(-status=>$error),
  $cgi->start_html('Problems'),
  $cgi->h2('Request not processed'),
  $cgi->strong($error);
  exit 1;
}

Foutafhandeling in het programma

Een routine om ook zelf duidelijke foutmeldingen te kunnen genereren (met name de routine die_html). Wel even aanpassen aan je eigen email adres.

my $html_header_sent = 0; # no cgi header send yet? (for die_html)

sub die_html # fatal error: generate HTML error message
{
  my $message = shift || "no message passed";
  my $program = $0; $program =~ s@.*/@@; # remove path
  my CGI framework = "Error in '$program'";

  if(!$html_header_sent)
  {
    $html_header_sent = 1;
    print header(); # CGI header: simple document type
    print start_html(-title => CGI framework, -BGCOLOR=>'#F0FFFF');
  }
  print
    h1(CGI framework), "\n",
    p("Error message: $message"), "\n",
    p("Please contact the web master of these web pages:\n",
    a({href=>("mailto:yourmailaddress\@yourdomain.nl")})
    ), "\n",
    end_html(); # end of HTML page

  if(open LOGFILE, ">>logfile.txt")
  { print LOGFILE "Fatal error detected in '$program' at " . localtime()
    . " line $., message is '$message', \$\@ = '$@', \$! = '$!'\n";
    close LOGFILE;
  }

  exit(0);
}

De dispatcher

Complexe CGI scripts (bijvoorbeeld met veel forms) worden vaak aangeroepen vanuit verschillende fases, bijvoorbeeld het initiële scherm met de vraag om in te loggen, een scherm met een invulformulier, en een scherm met de resultaten van het opsturen van een formulier. Dit kunnen ieder verschillende CGI programma's zijn, maar dan moet je je programma verdelen over verschillende scripts.

In plaats hiervan programmeer ik mijn complexere CGI functies vaak als een soort state machine, bijvoorbeeld met states 'login' (het inlogscherm met login form), 'select' (met de main form), en 'add' (bevestigingsboodschap). De state wordt meegegeven in de cgi aanroep als een GET/POST argument (zie de vorige pagina's), in de vorm van action=nextstate. De dispatcher zorgt er vervolgens voor dat de juiste routines worden aangeroepen. Hier een voorbeeld uit mijn Sinterklaaslijstjes programma.

print $cgi->header(); # CGI header: simple document type
print my_html_header(); # does the title, etc
$html_header_sent = 1;

# State machine core state diagram:
# Next state is passed through the action parameter in the calling form
# ....


my $cgi_action = $cgi->param('action') || 'unknown';
if    ($cgi_action eq 'unknown'){ sint_login(); }
elsif ($cgi_action eq 'login')  { sint_login(); }
elsif ($cgi_action eq 'select') { sint_select(0); }
elsif ($cgi_action eq 'add')    { sint_own(0); }
elsif ($cgi_action eq 'buy')    { sint_buy(0); }
else
         { die_html("Unknown command '$cgi_action'"); }

UTF-8 Unicode

Als je speciale tekens aan wilt kunnen, als smileys, zal je programma rekening moeten houden met Unicode. Dit is een uitgebreide karakterset, met meer verschillende tekens dan in de 256 combinaties in een enkel byte kunnen. Eén manier om dit te coderen is gebruik te maken van de 'UTF-8' encoding, waarbij indien nodig gebruik wordt gemaakt van meer dan een byte. Zo is een Euro-symbool € het utf-8 teken U+20AC, wat neerkomt op de bytes 0xE2 0x82 0xAC. Alle normale (7-bit) ASCII-tekens blijven hetzelfde (een ASCII file is dus automatisch geldig utf-8 gecodeerde Unicode). Speciale tekens worden vervangen door twee tot vier bytes, waarbij het eerste byte een speciale marker is.

Het mooie is dat Perl intern goed overweg kan met Unicode (en utf-8), en dat je er normaal niets van merkt dat Unicode utf-8 tekens meerdere bytes zijn. In perl gelden ze gewoon als een teken (tenzij je expliciet je data als binary benaderd). Wel moet je het duidelijk maken als je utf-8 ook voor je I/O wilt gebruiken, een paar voorbeelden:

use utf-8; # Dit geeft alléén aan dat de Perl source Unicode utf-8 gecodeerd is, zegt NIETS over file I/O!!

use open ':encoding(utf-8)';  # geeft aan dat álle files utf-8 zijn

if(open(MIJNFILE, $filename)) # dit is het alternatief per file
{
  binmode MIJNFILE, ':encoding(UTF-8)';
  print MIJNFILE "Unicode 😀 string ಠ_ಠ";
  close MIJNFILE;
}

# Omzetten van ingevulde data in een cgi-form naar netjes escaped HTML met Unicode:
use HTML::Entities;           # Hier zitten handige HTML - unicode conversies in
$antwoord = escapeHTML(decode_entities($cgi->param('antwoord')));

Ps: met het Linux file commando kan je snel checken of een file ASCII, Unicode of een andere codering (als latin-1, 256-bits ASCII) bevat.

Zenden van email

Ook handig: vanuit CGI files email verzenden. Vroeger gebruikte ik hiervoor de module MIME::Lite, maar deze wordt helaas niet meer ondersteund. Er zijn verschillende alternatieven, maar die staan niet standard bij mijn web-host geïnstalleerd. Dus, dan maar lekker low-level met behulp van sendmail... Hier een voorbeeld, waarbij ik voor de eenvoud wat controles verwijderd heb, zoals het testen op de juiste vorm van een email-adres. Gelijk een voorbeeld van een email in simpel HTML-formaat:

sub do_sendmail               # to, subject, HTML content --> error code
{
  my $send_to = shift;
  my $subject = shift || "Oops, onderwerp mist (systeemfout)...";
  my $content = shift || "<p>Oops, boodschap inhoud mist (systeemfout)...</p>";
  my $from    = "mijn.email.adres@mijnhost.nl";
  $ENV{PATH}  = "";           # suppress Insecure $ENV{PATH} while running with -T
  my $sendmail= "/usr/sbin/sendmail";        # !! Vervang door het juiste pad !!
  $sendmail   = "|$sendmail -t";             # maak er een pipe van

  if(open(SENDMAIL, $sendmail))              # open een pipe naar het sendmail programma
  {
    binmode SENDMAIL, ':encoding(UTF-8)';    # kijk, daar is de utf-8 weer...
    print SENDMAIL "To: $send_to\n";         # eerst de sendmail header maken
    print SENDMAIL "From: $from\n";
    print SENDMAIL "Subject: $subject\n";    # En ook de email wordt utf-8 gecodeerd
    print SENDMAIL "Content-type: text/html; charset=utf-8\n";
    print SENDMAIL "Content-Transfer-Encoding: 8bit\n\n";  # Einde header: 2x newline
    print SENDMAIL "<html>\n";
    print SENDMAIL '<head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head>';
    print SENDMAIL "\n<body>\n";
    print SENDMAIL $content;
    print SENDMAIL "<p>Dit is een automatisch gegenereerde email\n";
    print SENDMAIL "</p></body></html>\n";
    return close(SENDMAIL);         # 1 = OK
  }
  return 0;                         # failed...
}

Zorg wel dat het pad in $sendmail staat naar waar bij jou sendmail te vinden is.

Verdere functies

Daarnaast heb ik nog standaard routines om HTML uit een template te plukken, configuratiefiles te lezen en te schrijven, en dergelijke, om snel nieuwe scripts in elkaar te kunnen draaien. Maar, dat heeft niets meer te maken met de CGI problematiek, is meer algemene Perl vaardigheden, dus die ga ik hier niet verder beschrijven.

Meer informatie