#============================================================================== # # CVS Log # # $Log: AdManager.pm,v $ # Revision 1.14 2001/10/29 10:52:47 wrigley # v0.007 - minimal test added # # Revision 1.13 2001/10/26 14:40:52 wrigley # v0.006 # # Revision 1.12 2001/10/10 17:14:18 wrigley # various bug fixes: new image is based on max ad no., not nads; alt text is included in new advert; ads with no ct URL are rendered as non-clickable # # Revision 1.11 2001/08/29 14:56:37 wrigley # v0.004 # # Revision 1.10 2001/08/24 17:39:39 wrigley # v0.004 # # Revision 1.9 2001/08/24 13:09:55 wrigley # v0.003 # # Revision 1.8 2001/08/23 16:55:17 wrigley # v0.002 # # Revision 1.7 2001/08/23 13:19:59 wrigley # v0.001 # # Revision 1.6 2001/08/23 11:50:04 wrigley # *** empty log message *** # # #============================================================================== =head1 NAME WWW::AdManager - a perl module to administer and serve online advertising =head1 SYNOPSIS =head2 CGI =head3 advert interface WWW::AdManager->new( INTERFACE => 'ADVERT', ADMANAGER_URL => "/admanager", )->output(); =head3 admin interface WWW::AdManager->new( INTERFACE => 'ADMIN', ADMANAGER_URL => "/admanager", ADMANAGER_ADMIN_URL => "/internal/admanager", )->output(); =head2 mod_perl SetHandler perl-script PerlHandler Apache::WWW::AdManager PerlSetVar INTERFACE ADMIN PerlSetVar ADMANAGER_URL /admanager PerlSetVar ADMANAGER_ADMIN_URL /internal/admanager SetHandler perl-script PerlHandler Apache::WWW::AdManager PerlSetVar INTERFACE ADVERT PerlSetVar ADMANAGER_URL /admanager =head1 DESCRIPTION WWW::AdManager is a module which implements a web advert management system. This is based around linked images, that are organized into "campaigns". Within each campaign, the admanager randomizes the display of images, and tracks both "page impressions" - i.e. the number of times the image is displayed - and "clickthroughs" - i.e. the number of times the image is clicked on. The module also provides an administration web interface, which can be served through a access restricted URL, for creating and updating campaigns and adverts, and displaying usage stats. The interface support implementation both through CGI and mod_perl. mod_perl is highly recommended, especially where there are more than one adver to display per page. The insertion of adverts into pages is done using SSI (server side includes - see L, for example). The module does both logging of each relevant transaction (clickthrough / page impression) and live compilation of stats. Campaigns can be sub-divided into sub-campaigns for more convenient organization of adverts. =cut #============================================================================== # # Package declaration # #============================================================================== package WWW::AdManager; #============================================================================== # # Standard pragmas # #============================================================================== use strict; use warnings; #============================================================================== # # Modules # #============================================================================== use CGI_Lite; use File::Path; use TempDir; use Data::Dumper; use IO::File; use IO::Dir; use Fcntl qw( :flock ); use HTML::Entities; use URI; use Image::Size; use LWP::Simple; use Apache::Constants qw( :response :common ); #============================================================================== # # Global variables # #============================================================================== use vars qw( $VERSION %OPTIONS $WINDOW_PADDING $MAX_MARGIN ); $WINDOW_PADDING = 20; $MAX_MARGIN = 20; $VERSION = '0.007'; #============================================================================== # # Private methods # #============================================================================== #------------------------------------------------------------------------------ # # _mkpath( $dir ) - utility method to untaint a pathname and create it, or die. # #------------------------------------------------------------------------------ sub _mkpath { my $self = shift; my $dir = shift; return if -d $dir; $dir = _untaint( $dir ); mkpath( $dir ) or die "Can't create $dir\n"; } #------------------------------------------------------------------------------ # # _untaint( @paths ) - untaint path strings by checking against a regex that # allows alphanumerics, underscores and periods. # #------------------------------------------------------------------------------ sub _untaint { for ( @_ ) { $_ = $1 and next if m{([a-zA-Z0-9_./]*)}; die "untaint $_ failed\n"; } return wantarray ? @_ : $_[0]; } #------------------------------------------------------------------------------ # # _untaint_and_open( $file, $mode ) - untaint the filename %file, and then # open in mode $mode, returning an IO::File object. # #------------------------------------------------------------------------------ sub _untaint_and_open { my $file = shift; my $mode = shift; my $untaintend_file = _untaint( $file ); die "'$untaintend_file' ne '$file'\n" if $untaintend_file ne $file; return IO::File->new( $untaintend_file, $mode ); } #------------------------------------------------------------------------------ # # _freeze( $file, $data ) - "freeze" data in reference $data in filename $file, # using Data::Dumper # #------------------------------------------------------------------------------ sub _freeze { my $self = shift; my $file = shift; my $data = shift; my $fh = _untaint_and_open( $file, 'w' ) or die "can't write to $file: $!\n" ; flock( $fh, LOCK_EX ); print $fh Dumper $data; $fh->close(); } #------------------------------------------------------------------------------ # # _thaw( $file ) - "thaw" data in Data::Dumper file $file, and return as a # reference. # #------------------------------------------------------------------------------ sub _thaw { my $self = shift; my $file = shift; return {} unless -e $file; die "can't read $file\n" unless -r $file; my $fh = _untaint_and_open( $file, 'r' ) or die "can't open $file: $!\n"; flock( $fh, LOCK_SH ); $file = _untaint( $file ); my $data = do $file; $fh->close(); return $data; } #------------------------------------------------------------------------------ # # _read_data - read advert data from the Data::Dumper datafile # #------------------------------------------------------------------------------ sub _read_data { my $self = shift; $self->{ads} = $self->_thaw( $self->{datafile} ); } #------------------------------------------------------------------------------ # # _write_data - write advert data to the Data::Dumper datafile # #------------------------------------------------------------------------------ sub _write_data { my $self = shift; warn "write data for $self->{campaign_path} to $self->{datafile}\n"; eval { $self->_freeze( $self->{datafile}, $self->{ads} ); }; warn "error writing data $self->{datafile}: $@\n" if $@; } #------------------------------------------------------------------------------ # # _status - method called to return appropriate status. If called in mod_perl # mode, returns $self->{APACHE_STATUS}; otherwise, returns $@. In both cases # also logs errors, if they havce occurred. # #------------------------------------------------------------------------------ sub _status { my $self = shift; if ( $ENV{MOD_PERL} ) { die "No APACHE_REQUEST\n" unless $self->{APACHE_REQUEST}; if ( $@ ) { die "$self->{APACHE_REQUEST} can't log_error\n" unless $self->{APACHE_REQUEST}->can( 'log_error' ) ; $self->{APACHE_REQUEST}->log_error( $@ ); $self->{APACHE_STATUS} = SERVER_ERROR; } return $self->{APACHE_STATUS}; } else { warn "$@\n" if $@; return $@; } } #------------------------------------------------------------------------------ # # _redirect( $url ) - redirect to URL, using whichever method is appropriate to # whether in mod_perl mode or not. The caller may then exit. # #------------------------------------------------------------------------------ sub _redirect { my $self = shift; my $url = shift; warn "Redirect to $url\n"; if ( $ENV{MOD_PERL} ) { die "No APACHE_REQUEST\n" unless $self->{APACHE_REQUEST}; die "$self->{APACHE_REQUEST} can't print\n" unless $self->{APACHE_REQUEST}->can( 'header_out' ) ; $self->{APACHE_REQUEST}->header_out( Location => $url ); $self->{APACHE_STATUS} = REDIRECT; } else { print "Cache-Control: max-age=0\nLocation: $url\n\n"; } } #------------------------------------------------------------------------------ # # _print( @stuff ) - print stuff to the browser, using print method appropriate # to whether in mod_perl mode or not. # #------------------------------------------------------------------------------ sub _print { my $self = shift; if ( $ENV{MOD_PERL} ) { die "No APACHE_REQUEST\n" unless $self->{APACHE_REQUEST}; $self->{APACHE_REQUEST}->print( @_ ); } else { print @_; } } #------------------------------------------------------------------------------ # # _http_header( $content_type ) - set the content type in the HTTP header to # $content_type, and add a cache control header to prevent caching. Also, print # out header if not in mod_perl mode. # #------------------------------------------------------------------------------ sub _http_header { my $self = shift; my $type = shift; if ( $ENV{MOD_PERL} ) { warn "set content-type to $type\n"; die "No APACHE_REQUEST\n" unless $self->{APACHE_REQUEST}; $self->{APACHE_REQUEST}->content_type( $type ); $self->{APACHE_REQUEST}->header_out( 'Cache-Control' => 'max-age=0' ); $self->{APACHE_REQUEST}->send_http_header; } else { print "Content-Type: $type\nCache-Control: max-age=0\n\n"; } } #------------------------------------------------------------------------------ # # _subcampaigns( $campaign ) - returns all the sub-campagins of $campaign. # #------------------------------------------------------------------------------ sub _subcampaigns { my $self = shift; my $campaign_path = shift; my $dir = "$self->{DATA_DIR}/$campaign_path"; my %dir; tie %dir, 'IO::Dir', $dir; my @c = grep !/^\./, grep { -e "$dir/$_/admanager.pl" } %dir; return @c; } #------------------------------------------------------------------------------ # # _subcampaigns - returns all the current campaigns. # #------------------------------------------------------------------------------ sub _campaigns { my $self = shift; my %dir; my $dir = $self->{DATA_DIR}; tie %dir, 'IO::Dir', $dir; my @c = grep !/^\./, grep { -e "$dir/$_/admanager.pl" } %dir; return @c; } #------------------------------------------------------------------------------ # # _log_entry( $type ) - log an entry of type $type - either ct (clickthough) or # img (page impression) - for advert number $n. Also increment appropriate # entry in the stats Data::Dumper file. # #------------------------------------------------------------------------------ sub _log_entry { my $self = shift; my $type = shift; my $n = shift; return unless $self->{ads}{log_usage}; my $fh = _untaint_and_open( $self->{logfile}, 'a' ) or die "can't write to $self->{logfile}\n" ; my $url = $self->{ads}{$n}{$type}; my $datestr = scalar( localtime ); my $document_name = $ENV{DOCUMENT_NAME} || '-'; my $remote_host = $ENV{REMOTE_HOST} || $ENV{REMOTE_ADDR} || '-'; print $fh "$datestr:$n:$type:$url:$document_name:$remote_host\n"; warn "Log $n $type in $self->{statsfile}\n"; my $stats = $self->_thaw( $self->{statsfile} ); $stats->{$n}{$type}++; $self->_freeze( $self->{statsfile}, $stats ); } #------------------------------------------------------------------------------ # # _get_ad_keys - get a list of the current ad numbers. # #------------------------------------------------------------------------------ sub _get_ad_keys { my $self = shift; return grep /^\d+$/, keys %{$self->{ads}}; } #------------------------------------------------------------------------------ # # _get_rand_element( @array ) - return a randomly selected element of the array # @array. # #------------------------------------------------------------------------------ sub _get_rand_element { my $self = shift; return $_[rand(@_)]; } #------------------------------------------------------------------------------ # # _get_ads - return a list of all the current ad hashrefs. # #------------------------------------------------------------------------------ sub _get_ads { my $self = shift; return map { $self->{ads}{$_} } $self->_get_ad_keys(); } #------------------------------------------------------------------------------ # # _get_width - get the total width for the current campaign. # #------------------------------------------------------------------------------ sub _get_width { my $self = shift; my $width = 0; for my $ad ( $self->_get_ads() ) { my $w = $ad->{size}[0]; $width = $w > $width ? $w : $width; } $width *= $self->{ads}{nads} if $self->{ads}{nads}; if ( $self->{ads}{margin} ) { my $nmargins = $self->{ads}{nads} - 1; $width += $nmargins * $self->{ads}{margin}; } return $width; } #------------------------------------------------------------------------------ # # _get_height - get the total height for the current campaign # #------------------------------------------------------------------------------ sub _get_height { my $self = shift; my $height = 0; for my $ad ( $self->_get_ads() ) { my $h = $ad->{size}[1]; $height = $h > $height ? $h : $height; } return $height; } #------------------------------------------------------------------------------ # # _get_max_adno - get the max. ad no. # #------------------------------------------------------------------------------ sub _get_max_adno { my $self = shift; my $max = -1; for my $ad ( $self->_get_ads() ) { $max = $ad->{n} if $ad->{n} > $max; } return $max; } #------------------------------------------------------------------------------ # # _get_nads - get the current number of ads. # #------------------------------------------------------------------------------ sub _get_nads { my $self = shift; return scalar $self->_get_ads(); } #------------------------------------------------------------------------------ # # _html_header - returns an HTML header for the admin interface pages. # #------------------------------------------------------------------------------ sub _html_header { my $self = shift; return "Admanager" . ( $self->{campaign_path} ? " ($self->{campaign_path})" : '' ) . "" ; } #------------------------------------------------------------------------------ # # _campaigns_as_html - returns HTML formated list of current campaigns, as well # as a form to submit a new campaign creation request. # #------------------------------------------------------------------------------ sub _campaigns_as_html { my $self = shift; my $other = $self->{campaign_path} ? "Other " : ''; my $html = <${other}Campaigns EOF my @campaigns = $self->_campaigns; $html .= join " | ", map { $_ eq $self->{campaign} ? $_ : <$_ EOF @campaigns ; $html .= < New campaign: EOF return $html; } #------------------------------------------------------------------------------ # # _subcampaigns_as_html - returns HTML formated list of the sub-campaigns of the # current campaign, as well # as a form to submit a new sub-campaign creation # request. # #------------------------------------------------------------------------------ sub _subcampaigns_as_html { my $self = shift; my $campaign_path = $self->{campaign_path}; return '' unless $campaign_path; my $other = ''; if ( $self->{subcampaign} ) # this is a sub-campaign ... { $other = "Other "; } my @subcampaigns = $self->_subcampaigns( $self->{campaign} ); my $html = <${other}Sub-Campaigns of $self->{campaign} EOF $html .= join " | ", map { $_ eq $campaign_path ? $_ : <$_ EOF map { "$self->{campaign}/$_" } @subcampaigns ; $html .= < New sub-campaign: $self->{campaign}/ EOF return $html; } #------------------------------------------------------------------------------ # # _current_campaign_actions_as_html - returns an HTML formatted list of actions # for the current campaign: ad a new advert, view the usage log, test the # campaign, and delete the campaign. # #------------------------------------------------------------------------------ sub _current_campaign_actions_as_html { my $self = shift; return '' unless $self->{campaign_path}; my $add_ad = $self->_get_max_adno() + 1; my $html = <Current Campaign ($self->{campaign_path})

Add a new advert EOF my $nads = $self->_get_nads(); if ( $nads ) { my $h = $self->_get_height( $self->{campaign_path} ); my $w = $self->_get_width( $self->{campaign_path} ); my $width = $w + $WINDOW_PADDING; my $height = $h + $WINDOW_PADDING; $html .= <View the usage log | Test the campaign EOF } my $url = "$self->{ADMANAGER_ADMIN_URL}?" . 'action=delete_campaign&' . "c=$self->{campaign_path}" ; $html .= <Delete the campaign

EOF return $html; } #------------------------------------------------------------------------------ # # _ad_info_as_html( $ad ) - returns an HTML formatted table with the info # for the $ad'th advert in the currrent campaign. # #------------------------------------------------------------------------------ sub _ad_info_as_html { my $self = shift; my $ad = shift; my $n = $ad->{n}; my $nw = $ad->{nw} ? 'Yes' : 'No'; my $alt = $ad->{alt} || ''; my $size = join( 'x', @{$ad->{size}}[0,1] ); my $ad_as_html = $self->_ad_as_html( $n, 1 ); return < $ad_as_html Image:$ad->{img} Image Size:$size Clickthough:$ad->{ct} Alt. text:$alt Open in new window:$nw EOF } #------------------------------------------------------------------------------ # # _ad_stats_as_html( $ad ) - returns an HTML formatted table with the usage # stats for the $ad'th advert in the currrent campaign. # #------------------------------------------------------------------------------ sub _ad_stats_as_html { my $self = shift; my $ad = shift; my $n = $ad->{n}; my $stats = $self->_thaw( $self->{statsfile} ); $stats->{$n}{ct} ||= 0; $stats->{$n}{img} ||= 0; return < No. Impressions:$stats->{$n}{img} No. Clickthroughs:$stats->{$n}{ct} EOF } #------------------------------------------------------------------------------ # # _all_ads_info_as_html - returns an HTML formatted form with the advert # attributes relevant to all adverts in the current campaign: the number of # adverts to display, and the image margins, as well as forms to change these # values. # #------------------------------------------------------------------------------ sub _all_ads_info_as_html { my $self = shift; my $log_usage = $self->{ads}{log_usage}; my $html = sprintf( <
Log usage: EOF $html .= < No. of ads to display:
Image margin:
Total campaign dimensions: $w x $h EOF return $html; } #------------------------------------------------------------------------------ # # _ads_info_as_html - returns an HTML formatted table with all the advert info # for the current campaign, including usage stats, and links to actions for # each ad (edit / delete). # #------------------------------------------------------------------------------ sub _ads_info_as_html { my $self = shift; my $nads = $self->_get_nads(); my $colspan = 2 + $self->{ads}{log_usage}; my $html = <Adverts EOF return $html . "
All Adverts
EOF $html .= $self->_all_ads_info_as_html() . <
" unless $nads; $html .= "Advert"; $html .= "Usage Stats" if $self->{ads}{log_usage}; $html .= " Action"; for my $ad ( sort { $a->{n} <=> $b->{n} } $self->_get_ads() ) { $html .= < EOF $html .= $self->_ad_info_as_html( $ad ); $html .= < EOF $html .= qq{} . $self->_ad_stats_as_html( $ad ) . qq{} if $self->{ads}{log_usage}; $html .= < Edit | Delete EOF } $html .= ""; return $html; } #------------------------------------------------------------------------------ # # _ssi_code_as_html - returns HTML formatted code for inserting the current # campaign as an SSI comment. # #------------------------------------------------------------------------------ sub _ssi_code_as_html { my $self = shift; return '' unless $self->{campaign_path}; my $html = <SSI code
EOF
    $html .= encode_entities( <
EOF
    $html .= <
EOF
return( $html );
}

#------------------------------------------------------------------------------
#
# _main_admin_interface - Returns an HTML table containing all current adverts,
# plus the camapigns and sub-campaings list.
#
#------------------------------------------------------------------------------

sub _main_admin_interface
{
    my $self = shift;

    return 
        $self->_html_header() .
        $self->_campaigns_as_html() .
        $self->_subcampaigns_as_html() .
        $self->_current_campaign_actions_as_html() .
        $self->_ads_info_as_html() .
        $self->_ssi_code_as_html()
    ;
}

#------------------------------------------------------------------------------
#
# _change_log_usage - change the "log" attribute for the current campaign to
# the value stored in the formdata hash. The log attribute determined whether
# usage is logged for this campaign.
#
#------------------------------------------------------------------------------

sub _change_log_usage
{
    my $self = shift;
    my $log_usage = $self->{formdata}{log_usage};
    return unless defined $log_usage;
    warn "Changing log_usage to $log_usage\n";
    $self->{ads}{log_usage} = $log_usage;
    $self->_write_data;
}

#------------------------------------------------------------------------------
#
# _change_margin - change the "margin" attribute for the current campaign to
# the value stored in the formdata hash (margin is the width of the margin
# around each image).
#
#------------------------------------------------------------------------------

sub _change_margin
{
    my $self = shift;
    my $margin = $self->{formdata}{margin};
    return unless defined $margin and int( $margin ) eq $margin;
    warn "Changing margin to $margin\n";
    $self->{ads}{margin} = $margin;
    $self->_write_data;
}

#------------------------------------------------------------------------------
#
# _change_nads - change the "nads" attribute for the current campaign to
# the value stored in the formdata hash (nads is the number of ads to display
# simultaneously for the current campaign).
#
#------------------------------------------------------------------------------

sub _change_nads
{
    my $self = shift;
    my $nads = $self->{formdata}{nads};
    return unless defined $nads;
    warn "Changing no. adverts displayed to $nads\n";
    $self->{ads}{nads} = $nads;
    $self->_write_data;
}

#------------------------------------------------------------------------------
#
# _get_imagesize( $url ) - get the image size for a image URL, using
# LWP::Simple to get the image, and Image::Size to calculate its size.
#
#------------------------------------------------------------------------------

sub _get_imagesize( $ )
{
    my $self = shift;
    my $url = shift;
    my $uri = URI->new_abs( $url, $self->{abs_admin_url} );
    my $buf = get( $uri );
    return imgsize( \$buf );
}

#------------------------------------------------------------------------------
#
# _add_ad - add a new advert, using the img, ct, and ad values stored in the
# formdata hash. Also, calculate the image size for the img image. Check that
# ad is equal to the current no. of ads (this helps to prevent two concurrent
# sessions trying to grab the same new ad no.).
#
#------------------------------------------------------------------------------

sub _add_ad
{
    my $self = shift;
    my $img = $self->{formdata}{img};
    my $ct = $self->{formdata}{ct};
    my $ad = $self->{formdata}{ad};
    my $alt = $self->{formdata}{alt};
    $img or die "No img param\n";
    defined( $ct ) or die "no ct param\n";
    defined( $ad ) or die "no ad param\n";

    my @size = $self->_get_imagesize( $img );
    my $ad_add = $self->_get_max_adno() + 1;
    if ( $img and $ct and defined( $ad ) and $ad == $ad_add )
    {
        warn "Adding ad (no $ad): $img $ct (@size)\n";
        $self->{ads}{$ad} =
            { alt => $alt, ct => $ct, img => $img, size => \@size, n => $ad }
        ;
        $self->_write_data;
    }
    else
    {
        warn "Error: $ad != $ad_add\n";
        for my $ad ( $self->_get_ads )
        {
            warn "\t$ad->{n}\n";
        }
    }
}

#------------------------------------------------------------------------------
#
# _edit_ad - edit the ad specified by the ad value in the formdata hash,
# updating the img, ct, nads, and nw fields, and recalculating the image size,
# if necessary.
#
#------------------------------------------------------------------------------

sub _edit_ad
{
    my $self = shift;
    my $img = $self->{formdata}{img};
    my $ct = $self->{formdata}{ct};
    my $ad = $self->{formdata}{ad};
    my $nads = $self->{formdata}{nads};
    my $nw = $self->{formdata}{nw};
    my $alt = $self->{formdata}{alt};
    $img or die "No img param\n";
    defined( $ct ) or die "no ct param\n";
    defined( $ad ) or die "no ad param\n";

    $self->{ads}{$ad}{ct} = $ct;
    $self->{ads}{$ad}{n} = $ad;
    $self->{ads}{$ad}{nads} = $nads;
    $self->{ads}{$ad}{nw} = $nw;
    $self->{ads}{$ad}{alt} = $alt;
    if ( $self->{ads}{$ad}{img} ne $img )
    {
        $self->{ads}{$ad}{img} = $img;
        my @size = $self->_get_imagesize( $img );
        $self->{ads}{$ad}{size} = \@size;
    }
    warn "Edit $ad ad: $img (", @{$self->{ads}{$ad}{size}}, ") -> $ct\n";
    $self->_write_data;
}

#------------------------------------------------------------------------------
#
# _delete_ad - delete the ad specified by the ad value in the formdata hash.
#
#------------------------------------------------------------------------------

sub _delete_ad
{
    my $self = shift;
    my $ad = $self->{formdata}{ad};
    delete( $self->{ads}{$ad} );
    $self->_write_data;
}

#------------------------------------------------------------------------------
#
# _create_campaign - create a new campaign named after the nc value in the
# formdata hash.
#
#------------------------------------------------------------------------------

sub _create_campaign
{
    my $self = shift;
    my $nc = $self->{formdata}{nc};
    unless ( $nc )
    {
        my $nsc = $self->{formdata}{nsc} or
            warn "no nc param\n" and return;
        ;
        $nc = "$self->{campaign_path}/$nsc";
    }
    warn "Create $nc campaign\n";
    my $datadir = _untaint( "$self->{DATA_DIR}/$nc" );
    unless ( -e $datadir )
    {
        mkpath( $datadir ) or die "Can't create $datadir\n";
    }
    $self->{datafile} = "$datadir/admanager.pl";
    $self->{ads} = { log_usage => 1 };
    $self->_write_data;
}

#------------------------------------------------------------------------------
#
# _delete_campaign - delete the campaign corresponding to the c value in the
# formdata hash.
#
#------------------------------------------------------------------------------

sub _delete_campaign
{
    my $self = shift;
    my $c = $self->{formdata}{c};
    die "No campaign specified\n" unless $c;
    warn "Delete $c campaign\n";
    $c = $self->_munge_filename_from_path( $c );
    my $statfile = "$self->{STATS_DIR}/$c.stats";
    warn "statsfile: $statfile\n";
    if ( -e $statfile )
    {
        warn "Unlink $statfile\n";
        unlink( $statfile ) or die "Can't remove $statfile\n";
    }
    my $logfile = "$self->{LOG_DIR}/$c.log";
    warn "logfile: $statfile\n";
    if ( -e $logfile )
    {
        warn "Unlink $logfile\n";
        unlink( $logfile ) or die "Can't remove $logfile\n";
    }
    my $datadir = _untaint( "$self->{DATA_DIR}/$self->{formdata}{c}" );
    warn "datadir: $datadir\n";
    if ( -e $datadir )
    {
        warn "rmtree $datadir\n";
        rmtree( $datadir ) or die "Can't remove $datadir\n";
    }
    $self->_redirect( $self->{ADMANAGER_ADMIN_URL} );
}


#------------------------------------------------------------------------------
#
# _ad_form( $type ) - return an HTML edit/add ad form based on the $type
# argument, and the equivilent value in the formdata hash (which should contain
# the ad number for the ad to be created / edited).
#
#------------------------------------------------------------------------------

sub _ad_form
{
    my $self = shift;
    my $type = shift;

    my $val = $self->{formdata}{$type};
    my $ad = $self->{ads}{$val}; 
    my $alt = $ad->{alt} || '';
    my $ychecked = $ad->{nw} ? 'checked' : '';
    my $nchecked = $ad->{nw} ? '' : 'checked';
    my $form = <

Image URL:
Alt. Text:
Clickthrough URL:
Open in new window Yes No
EOF return $form; } #------------------------------------------------------------------------------ # # _logfile - return the contents of the logfile for the current campaign. # #------------------------------------------------------------------------------ sub _logfile { my $self = shift; my $fh = _untaint_and_open( $self->{logfile}, 'r' ); return unless $fh; return join '', <$fh>; } #------------------------------------------------------------------------------ # # _page_impression - log a page impression for th advert number specified by # the img value in the formdata hash, and redirect to the corresponding image. # #------------------------------------------------------------------------------ sub _page_impression { my $self = shift; my $n = $self->{formdata}{img}; die "No img parameter set\n" unless defined $n; my $url = $self->{ads}{$n}{img} or die "No image $n im campaign $self->{campaign_path}\n" ; $self->_log_entry( 'img', $n ); warn "page impression $url\n"; $self->_redirect( $url ); } #------------------------------------------------------------------------------ # # _ad_as_html( $n ) - return advert no. $n as HTML. # #------------------------------------------------------------------------------ sub _ad_as_html { my $self = shift; my $n = shift; my $first = shift; my $campaign_path = $self->{campaign_path}; my $ad = $self->{ads}{$n}; my $img = $ad->{img}; my $size = $ad->{size}; my $size_str = $size ? "width=\"$size->[0]\" height=\"$size->[1]\"" : ''; my $url = $self->{ADMANAGER_URL}; $url =~ s/$self->{path_info}$// if $self->{path_info}; $url .= "/$campaign_path"; my $alt = $ad->{alt} || "$campaign_path advert no. $n"; my $rand = $$ . time . rand(1000); my $img_url = $self->{REDIRECT_PAGE_IMPRESSIONS} ? "$url?img=$n&rand=$rand" : "$img?$rand" ; my $margin = $self->{ads}{margin} || 0; my $user_agent = $ENV{HTTP_USER_AGENT}; if ( $user_agent !~ /compatible/ and $user_agent =~ m!Mozilla/4! ) { $margin += $size->[0]; } my $style = $first ? '' : "style=\"margin-left:${margin}px;\""; if ( $ad->{ct} ) { my $target = $ad->{nw} ? 'target="_blank"' : ''; my $ct_url = "$url?ct=$n&rand=$rand"; return < $alt EOF } else { return join( '', qq{}, qq{$alt} ); } } #------------------------------------------------------------------------------ # # _random_ad_as_html - return a random ad as HTML, logging a 'img' entry for # that ad. # #------------------------------------------------------------------------------ sub _random_ad_as_html { my $self = shift; my @html; my $nads = $self->{ads}{nads} || 1; my @ad_keys = $self->_get_ad_keys(); warn "Displaying $nads random ads\n"; for ( 1 .. $nads ) { my $n = $self->_get_rand_element( @ad_keys ); warn "$n chosen from @ad_keys\n"; @ad_keys = grep { $_ ne $n } @ad_keys; $self->_log_entry( 'img', $n ) unless $self->{REDIRECT_PAGE_IMPRESSIONS} ; push( @html, $self->_ad_as_html( $n, $_ == 1 ) ); warn "Display ad $n as HTML\n"; } my $margin = $self->{ads}{margin} || 0; my $spacer = ''; return join( $spacer, @html ); } #------------------------------------------------------------------------------ # # _clickthrough - log a 'ct' entry for the ad corresponding to the ct value in # the formdata hash, and redirect to the corresponding URL. # #------------------------------------------------------------------------------ sub _clickthrough { my $self = shift; my $n = $self->{formdata}{ct}; my $url = $self->{ads}{$n}{ct}; warn "Click though to $url\n"; $self->_log_entry( 'ct', $n ); $self->_redirect( $url ); return $url; } #------------------------------------------------------------------------------ # # _setup_dirs - sets up and creates the directories required. # #------------------------------------------------------------------------------ sub _setup_dirs { my $self = shift; unless ( $self->{INSTALL_DIR} ) { my $root = $ENV{HOME} || TempDir->new || die "Can't work out a root\n"; $self->{INSTALL_DIR} = "$root/.admanager"; } $self->_mkpath( $self->{INSTALL_DIR} ); $self->{STATS_DIR} ||= "$self->{INSTALL_DIR}/stats"; $self->_mkpath( $self->{STATS_DIR} ); $self->{LOG_DIR} ||= "$self->{INSTALL_DIR}/log"; $self->_mkpath( $self->{LOG_DIR} ); $self->{DATA_DIR} ||= "$self->{INSTALL_DIR}/data"; $self->_mkpath( $self->{DATA_DIR} ); $self->{ERR_DIR} ||= "$self->{INSTALL_DIR}/err"; $self->_mkpath( $self->{ERR_DIR} ); } sub _log_errors { my $self = shift; my $errfile = "$self->{ERR_DIR}/$self->{INTERFACE}." . ( $ENV{MOD_PERL} ? 'mod_perl' : 'cgi' ) . ".err" ; $errfile = _untaint( $errfile ); open( STDERR, ">>$errfile" ) or die "Can't write to $errfile: $!\n"; warn "$0: ", scalar( localtime ), "\n"; warn "PATH_INFO: $ENV{PATH_INFO}\n" if $ENV{PATH_INFO}; warn "FORM DATA:\n", map { "\t$_ = $self->{formdata}{$_}\n" } keys %{$self->{formdata}} ; warn "Running under mod_perl\n" if exists $ENV{MOD_PERL}; } #------------------------------------------------------------------------------ # # _munge_filename_from_path( $path ) - create a munged filename from a path, by # replacing '/' characters with '_'s. NOTE: the replacement character needs to # not clash with the allowable filename characters from _untaint. # #------------------------------------------------------------------------------ sub _munge_filename_from_path { my $self = shift; my $path = shift; $path =~ s{/}{_}g; return $path; } #------------------------------------------------------------------------------ # # _setup_files - sets up files required for current campaign # #------------------------------------------------------------------------------ sub _setup_files { my $self = shift; my $campaign_path = shift; return unless $campaign_path; my $cn = $self->_munge_filename_from_path( $campaign_path ); $self->{statsfile} = "$self->{STATS_DIR}/$cn.pl"; $self->{logfile} = "$self->{LOG_DIR}/$cn.log"; my $datadir = "$self->{DATA_DIR}/$campaign_path"; $self->_mkpath( $datadir ); $self->{datafile} = "$datadir/admanager.pl"; } #------------------------------------------------------------------------------ # # _setup_admin_urls - create admin url values # #------------------------------------------------------------------------------ sub _setup_admin_urls { my $self = shift; my ( $proto ) = $ENV{SERVER_PROTOCOL} =~ /^(\w+)/; $self->{abs_admin_url} = lc( $proto ) . '://' . $ENV{SERVER_NAME} . # ( $ENV{SERVER_PORT} != 80 ? ":$ENV{SERVER_PORT}" : '' ) . $ENV{SCRIPT_NAME} . ( $ENV{PATH_INFO} ? $ENV{PATH_INFO} : '' ) . ( $ENV{QUERY_STRING} ? "?$ENV{QUERY_STRING}" : '' ) ; $self->{ADMANAGER_ADMIN_URL} ||= $ENV{SCRIPT_NAME}; $self->{admin_url} = "$self->{ADMANAGER_ADMIN_URL}/$self->{campaign_path}"; } #------------------------------------------------------------------------------ # # _setup_campaign_path - setup campaign_path. This is maintained using the # $PATH_INFO. # #------------------------------------------------------------------------------ sub _setup_campaign_path { my $self = shift; $self->{path_info} = $ENV{PATH_INFO}; my $campaign_path = $self->{path_info} || ''; $campaign_path =~ s{/}{}; $campaign_path ||= ''; ( $self->{campaign}, $self->{subcampaign} ) = split( '/', $campaign_path ); return $self->{campaign_path} = $campaign_path; } #------------------------------------------------------------------------------ # # _check_options - check the options passed to the constructor against the # %OPTIONS hash. Values of this hash are either 'undef' (optional) or contain a # quoted regex to test the option value against. # #------------------------------------------------------------------------------ sub _check_options { my $self = shift; for ( keys %$self ) { die "Unknown option $_\n" unless exists $OPTIONS{$_}; } for my $opt ( grep { defined $OPTIONS{$_} } keys %OPTIONS ) { my $whatami = lc( ref( $OPTIONS{$opt} ) ); die "No $opt option specified\n" unless exists $self->{$opt}; if ( $whatami eq 'regexp' ) { die "$opt option should be $OPTIONS{$opt}\n" unless $self->{$opt} =~ $OPTIONS{$opt} ; } elsif ( $whatami eq 'code' ) { my $ret = $OPTIONS{$opt}->( $self, $opt ); die $ret if defined $ret; } } } #============================================================================== =head1 CONSTRUCTOR The constructor for the module takes a number of options (see L) as a hash of arguments. =cut #============================================================================== sub new { my $class = shift; my %args = @_; my $self = bless \%args, $class; $self->{APACHE_REQUEST} ||= undef; $self->_check_options(); $self->{formdata} = CGI_Lite->new->parse_form_data; $self->_setup_dirs(); $self->_log_errors(); my $campaign_path = $self->_setup_campaign_path(); $self->_setup_admin_urls(); $self->_setup_files( $campaign_path ); $self->_read_data() if $campaign_path; return $self; } #============================================================================== =head1 Apache::Registry HANDLER WWW::Admanger offers a "handler" method that can be used in a mod_perl ennabled Apache web server (see L). Various options can be specified using PerlSetVar directives (see L). These options correspond to the L options, and are listed in the L section. The handler method simple creates a new WWW::AdManager object using these options, and calls the L method on this object. =cut #============================================================================== sub handler { my $r = shift; unless ( $ENV{MOD_PERL} ) { die "handler called in non-mod_perl environment\n"; } return WWW::AdManager->new( APACHE_REQUEST => $r, map { $_ => $r->dir_config->{$_} } grep { $_ ne 'APACHE_REQUEST' } keys %OPTIONS )->output; } #============================================================================== =head1 OPTIONS =cut #============================================================================== =head2 INTERFACE This option deltermined which interface is displayed by the L method. This option is REQUIRED, and its value should be one of ADMIN or ADVERT. Basically, the ADMIN interface presents a user interface to configure the advertising campaigns. This should probably be offered through an access restricted URL. The ADVERT interface presents HTML code to include in a page which is hosing the advert. =head2 ADMANAGER_URL This is the URL that corresponds to the CGI / mod_perl interface through which the advert is served. This is used by WWW::Admanger to generate links (e.g. test links in the ADMIN interface, and clickthrough / page impression links in the ADVERT interface. This option is REQUIRED. =head2 ADMANAGER_ADMIN_URL This is the URL that corresponds to the CGI / mod_perl interface through which the administration interface is presented. This is used by WWW::Admanger to generate links to other views in the administration interface. It is a REQUIRED option if the L option is set to ADMIN. =head2 INSTALL_DIR This option specified the default root directory for installing the various files (logging, stats, advert data) used by / produced by the application. It is an OPTIONAL option. The default is either $HOME/.admanager, if the $HOME environment is set, or $TMP/.admanager, where $TMP is the system temporary directory, as determined by the L module. =head2 ERR_DIR The directory where application error logs are written. This is an OPTIONAL option, and defaults to L/err. =head2 LOG_DIR The directory where application user logs are written. There is a seperate log file per camapign. These logs are of the following format: $datestr:$n:$type:$url:$document_name:$remote_host where: =over 4 =item $datestr Date string returned by localtime. =item $n No. of the advert in the campaign. =item $type Either 'img' if this is a page impression, or 'ct' if this is a clickthrough. =item $url The URL corresponding to the image served / clickthrough redirected to. =item $document_name The document from which the advert was served (through SSI). =item $remote_host The remote host requesting the advert. =back This is an OPTIONAL option, and defaults to L/log. =head2 STATS_DIR The directory where usage statistics files are written. This is an OPTIONAL option, and defaults to L/stats. =head2 DATA_DIR The directory where advert data files are saved (using L). This is an OPTIONAL option, and defaults to L/data. =head2 REDIRECT_PAGE_IMPRESSIONS If this option is set, page impression URLs (i.e. the SRC attribute of the IMG tag) are redirected through the admanager system. This helps to ensure that page impressions are properly logged. It is NOT recommended if the application is being served through CGI, as it is likely to seriously affext performance. =cut %OPTIONS = ( INTERFACE => sub { my $self = shift; my $opt = shift; return "$opt should be (ADMIN|ADVERT)" unless $self->{$opt} =~ /^(ADMIN|ADVERT)$/i ; return "ADMANAGER_ADMIN_URL option not set\n" if $self->{$opt} eq 'ADMIN' and not $self->{ADMANAGER_ADMIN_URL} ; return undef; } , ADMANAGER_URL => sub { undef }, ADMANAGER_ADMIN_URL => undef, INSTALL_DIR => undef, ERR_DIR => undef, LOG_DIR => undef, STATS_DIR => undef, DATA_DIR => undef, REDIRECT_PAGE_IMPRESSIONS => undef, APACHE_REQUEST => sub { my $self = shift; my $opt = shift; return undef unless $ENV{MOD_PERL}; return "new called with MOD_PERL but no $opt\n" unless $self->{$opt} ; return "$opt is not an Apache object ($self->{$opt})\n" unless ref( $self->{$opt} ) =~ /^Apache/ ; $self->{APACHE_STATUS} = OK; return undef; } , ); #============================================================================== =head1 METHODS =cut #============================================================================== =head2 output This moethod is called to generate the appropriate output for the L created. It does the "right thing" depending on whether the application is called through CGI or mod_perl. =cut sub output { my $self = shift; if ( $self->{INTERFACE} eq 'ADVERT' ) { if ( defined $self->{formdata}{ct} ) { my $url = $self->_clickthrough; } elsif ( defined $self->{formdata}{img} ) { my $url = $self->_page_impression; } else { $self->_http_header( 'text/html' ); my $html = $self->_random_ad_as_html; $self->_print( $html ); } } elsif ( $self->{INTERFACE} eq 'ADMIN' ) { if ( defined $self->{formdata}{action} ) { if ( $self->{formdata}{action} eq 'delete_campaign' ) { $self->_delete_campaign(); return $self->_status(); # redirect to the root } elsif ( $self->{formdata}{action} eq 'create' ) { $self->_create_campaign(); } elsif ( $self->{formdata}{action} eq 'edit' ) { $self->_edit_ad(); } elsif ( $self->{formdata}{action} eq 'change log usage' ) { $self->_change_log_usage(); } elsif ( $self->{formdata}{action} eq 'change margin' ) { $self->_change_margin(); } elsif ( $self->{formdata}{action} eq 'change no. ads' ) { $self->_change_nads(); } elsif ( $self->{formdata}{action} eq 'add' ) { $self->_add_ad(); } elsif ( $self->{formdata}{action} eq 'delete_ad' ) { $self->_delete_ad(); } } if ( defined $self->{formdata}{'display_log'} ) { $self->_http_header( 'text/plain' ); my $logfile = $self->_logfile(); $self->_print( $logfile ); return $self->_status; } elsif ( defined $self->{formdata}{edit} ) { $self->_http_header( 'text/html' ); my $form = $self->_ad_form( 'edit' ); $self->_print( $form ); return $self->_status; } elsif ( defined $self->{formdata}{add} ) { $self->_http_header( 'text/html', ); my $form = $self->_ad_form( 'add' ); $self->_print( $form ); return $self->_status; } else { $self->_http_header( 'text/html' ); $self->_print( $self->_main_admin_interface() ); } } return $self->_status; } =head1 ADMINISTRATION WEB INTERFACE =head2 Campaign This section shows a list of links to all the current campaigns, plus a form input to create a new campaign. On creation and selection of a campaign, you should see ... =head2 Sub-Campaigns of $campaign This section shows a list of links all the sub-campaigns of the currently selected campaign (if any) plus a form input to create a new sub-campaign. =head2 Current Campaign ($campaign_path) This section shows details of the current campaign / sub-campaign. This includes: =over 4 =item a link to "Add a new advert" This links to a form which can be used to enter the details of a new advert for the current campaign (Image URL, Alt. Text, Clickthrough URL, and whether the advert is to be opened in a new window). =item a link to "View the usage log" This link display the text of the usage log for the current campaign. See the L for details of the log format. =item a link to "Test the campaign" This link launches a new window with can be used to preview the current campaign. Reloading the window should demonstrate the advert rotation. =item a link to "Delete the campaign" This link is used to delete all information about the current campaign. It throws us a javascript confirmation box, just in case! =back =head2 Adverts This section contains a table with details of all the adverts in the current campaign. It includes links to edit / delete individual adverts, and to change advert attributes for the whole campaign (the no. of ads to display simultaneously, the image margin in pixels). It also includes reports on the usage stats (page impressions / clickthroughs) for each advert in the campaign. The "Log usage" attribute determines whether ad usage (page impressions / clickthroughs are logged, and stats accumulated). The default for this is "Yes" for a new campaign. The "No. of ads to display" attribute allows campaigns with multiple images to be displayed simultaneously (side-by-side). The number of ads displayed >= the number of ads in the campaign, and the displayed images are randomised over the available "slots". The "Image margin" attribute determines the margin around adverts. This is implemented using a margin stylesheet attribute on the img tag. =head2 SSI code This section displays the SSI comment that needs to be inserted in a page to display the current campaign. =head1 AUTHOR Ave Wrigley =head1 COPYRIGHT Copyright (c) 2001 Ave Wrigley. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut #============================================================================== # # True ... # #============================================================================== 1;