#!/usr/bin/perl -Tw # # $Id: wwwboard.pl,v 1.60 2005/02/09 11:45:11 gellyfish Exp $ # use strict; use CGI qw(:standard); use Fcntl qw(:DEFAULT :flock); use POSIX qw(locale_h strftime); CGI::NMS::IPFilter->import(); use vars qw( $DEBUGGING $VERSION $done_headers $emulate_matts_code $max_followups $basedir $baseurl $cgi_url $mesgdir $datafile $mesgfile $faqfile $ext $title $style $show_faq $allow_html $quote_text $quote_char $quote_html $subject_line $use_time $date_fmt $time_fmt $show_poster_ip $enable_preview $enforce_max_len %max_len $strict_image @image_suffixes $locale $charset @bannedwords $bannedwords $bannednets @use_rbls $check_sc_uri ); BEGIN { $VERSION = substr q$Revision: 1.60 $, 10, -1; } # PROGRAM INFORMATION # ------------------- # wwwboard.pl $Revision: 1.60 $ # # This program is licensed in the same way as Perl # itself. You are free to choose between the GNU Public # License or # the Artistic License # # # For a list of changes see CHANGELOG # # For help on configuration or installation see README # # USER CONFIGURATION SECTION # -------------------------- # Modify these to your own settings. You might have to # contact your system administrator if you do not run # your own web server. If the purpose of these # parameters seems unclear, please see the README file. # BEGIN { $DEBUGGING = 1; $emulate_matts_code = 1; $max_followups = 10; $basedir = '/var/www/nms-test/wwwboard'; $baseurl = 'http://nms-test/wwwboard'; $cgi_url = 'http://nms-test/cgi-bin/wwwboard.pl'; $mesgdir = 'messages'; $datafile = 'data.txt'; $mesgfile = 'wwwboard.html'; $faqfile = 'faq.html'; $ext = 'html'; $title = "NMS WWWBoard Version $VERSION"; $style = '/css/nms.css'; $show_faq = 1; $allow_html = 1; $quote_text = 1; $quote_char = ':'; $quote_html = 1; $subject_line = 0; $use_time = 1; $date_fmt = '%d/%m/%y'; $time_fmt = '%T'; $show_poster_ip = 1; $enable_preview = 0; $enforce_max_len = 0; %max_len = ( 'name' => 50, 'email' => 70, 'subject' => 80, 'url' => 150, 'url_title' => 80, 'img' => 150, 'body' => 3000, 'origsubject' => 80, 'origname' => 50, 'origemail' => 70, 'origdate' => 50 ); $strict_image = 1; @image_suffixes = qw(png jpe?g gif); $locale = ''; $charset = 'iso-8859-1'; $bannedwords = ''; $bannednets = ''; @use_rbls = qw(); $check_sc_uri = 0; # # USER CONFIGURATION << END >> # ---------------------------- # (no user serviceable parts beyond here) eval { sub SEEK_SET() { 0; } } unless defined(&SEEK_SET); if ($use_time) { $date_fmt = "$time_fmt $date_fmt"; } use vars qw($html_preview_button); $html_preview_button = ( $enable_preview ? ' ' : '' ); } sub html_header { if ( $CGI::VERSION >= 2.57 ) { # This is the correct way to set the charset print header( '-type' => 'text/html', '-charset' => $charset ); } else { # However CGI.pm older than version 2.57 doesn't have the # -charset option so we cheat: print header( '-type' => "text/html; charset=$charset" ); } } # We need finer control over what gets to the browser and the CGI::Carp # set_message() is not available everywhere :( # This is basically the same as what CGI::Carp does inside but simplified # for our purposes here. BEGIN { sub fatalsToBrowser { my ($message) = @_; if ($DEBUGGING) { $message =~ s//>/g; } else { $message = ''; } my ( $pack, $file, $line, $sub ) = caller(0); my ($id) = $file =~ m%([^/]+)$%; return undef if $file =~ /^\(eval/; html_header() unless $done_headers; print < Error

Application Error

An error has occurred in the program

$message

EOERR die @_; } $SIG{__DIE__} = \&fatalsToBrowser; } use vars qw($cs); $cs = CGI::NMS::Charset->new($charset); # %E is a fake hash for escaping HTML metachars as things are # interploted into strings. use vars qw(%E); tie %E, __PACKAGE__; sub TIEHASH { bless {}, shift } sub FETCH { $cs->escape( $_[1] ) } use vars qw($html_style); $html_style = $style ? qq%% : ''; # We don't need file uploads or very large POST requests. # Annoying locution to shut up 'used only once' warning in # older perl. Localize these to avoid stomping on other # scripts that need file uploads under Apache::Registry. local ( $CGI::DISABLE_UPLOADS, $CGI::POST_MAX ); $CGI::DISABLE_UPLOADS = 1; $CGI::POST_MAX = 1000000; # Empty the environment of potentially harmful variables, # and detaint the path. We accept anything in the path # because $ENV{PATH} is trusted for a CGI script, and in # general we have no way to tell what should be there. delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; $ENV{PATH} =~ /(.*)/ and $ENV{PATH} = $1; $done_headers = 0; if ( check_ip($ENV{REMOTE_ADDR}) ) { error('blocked'); } my $Form = parse_form(); my $variables = get_variables($Form); if ( param('preview') ) { preview_post($variables); } else { open LOCK, ">>$basedir/.lock" or die "open >>$basedir/.lock: $!"; flock LOCK, LOCK_EX or die "flock $basedir/.lock: $!"; my $ft = File::Transaction->new; eval { local $SIG{__DIE__}; $variables->{id} = get_number($ft); new_file( $ft, $variables ); main_page( $ft, $variables ); thread_pages( $ft, $variables ); }; if ($@) { $ft->revert; close LOCK; die $@; } else { $ft->commit; close LOCK; return_html($variables); } } sub get_number { my ($ft) = @_; my $num = 0; my $file = "$basedir/$datafile"; if ( open NUMBER, "<$file" ) { $num = || 0; $num =~ /^(\d+)\s*$/ or die "$file bad"; $num = $1; close NUMBER; } $num++; $num = 1 if $num > 999999; open NUMBER, ">$file.tmp" or die "open >$file.tmp: $!"; print NUMBER $num; close NUMBER or die "close $file.tmp: $!"; $ft->addfile( $file, "$file.tmp" ); return $num; } sub parse_form { my %Form; foreach my $param ( keys %max_len, 'followup' ) { my $val = param($param); defined $val or $val = ''; $Form{$param} = &{ $cs->strip_nonprint_coderef }($val); $Form{$param} =~ s/[\r\n\0]/ /g unless $param eq 'body'; } if ($enforce_max_len) { foreach ( keys %max_len ) { if ( length( $Form{$_} ) > $max_len{$_} ) { if ( $enforce_max_len == 2 ) { error( 'field_size', { Form => \%Form } ); } else { $Form{$_} = substr( $Form{$_}, 0, $max_len{$_} ); } } } } return \%Form; } ############### # Get Variables sub get_variables { my ($Form) = @_; my $variables = { Form => $Form }; my @followup_num; if ( exists $Form->{followup} && length( $Form->{followup} ) ) { $variables->{followup} = 1; @followup_num = split( /,/, $Form->{followup} ); my %fcheck; foreach my $fn (@followup_num) { if ( $fcheck{$fn} or $fn !~ /^(\d+)$/ ) { error( 'followup_data', { Form => $Form } ); } else { $fn = $1; $fcheck{$fn} = 1; } } # truncate the list of followups so that a vandal can't followup # to every existing message on the site. if ( !$emulate_matts_code && $max_followups && $max_followups < @followup_num ) { my $start_followups = $#followup_num - $max_followups; @followup_num = @followup_num[ $start_followups .. $#followup_num ]; } $variables->{followups} = \@followup_num; $variables->{num_followups} = scalar @followup_num; $variables->{last_message} = $followup_num[$#followup_num]; $variables->{origdate} = $Form->{origdate}; $variables->{origname} = $Form->{origname}; $variables->{origsubject} = $Form->{origsubject}; } else { $variables->{followup} = $variables->{num_followups} = 0; } length $Form->{name} or error( 'no_name', $variables ); check_banned( $Form->{name} ) or error('invalid'); $variables->{name} = $Form->{name}; if ( $Form->{email} =~ /(.*\@.*\..*)/ ) { $variables->{email} = $1; } else { $variables->{email} = ''; } if ( $Form->{subject} ) { check_banned( $Form->{subject} ) or error('invalid'); check_uri_rbl( $Form->{subject} ) and error ('invalid'); $variables->{subject} = $Form->{subject}; } else { error( 'no_subject', $variables ); } my $url = validate_url( $Form->{'url'} || '' ); check_uri_rbl( $url ) and error ('invalid'); $Form->{'url_title'} =~ s/&#[0-9]{1,3};//g; $Form->{'url_title'} =~ s/[^a-zA-Z0-9_ ?!&;]//g; if ( $url and $Form->{'url_title'} ) { $variables->{message_url} = $url; $variables->{message_url_title} = $Form->{'url_title'}; } my $message_img = validate_url( $Form->{'img'} || '' ); check_uri_rbl( $message_img ) and error ('invalid'); if ( $message_img and $strict_image ) { my $image_suffixes = join '|', @image_suffixes; unless ( $message_img =~ /($image_suffixes)$/i ) { undef $message_img; } } $message_img and $variables->{message_img} = $message_img; if ( my $body = $Form->{'body'} ) { unless ($allow_html) { # strip out what look like tags, then escape all but # wellformed HTML entities. $body =~ s#]*># #g; $body =~ s/(&#?\w{1,20};)|(.[^&]*)/ defined $1 ? $1 : $cs->escape($2) /ges; } $body = "

$body

"; $body =~ s/\cM//g; $body =~ s|\n\n|

|g; $body =~ s%\n%
%g; if ($allow_html) { $body = filter_html($body); } check_banned($body) or error('invalid'); check_uri_rbl( $body ) and error ('invalid'); $variables->{html_body} = $body; } else { error( 'no_body', $variables ); } if ($quote_text) { my $hidden_body = $Form->{'body'}; $hidden_body =~ s#(]*>)+# #ig unless $quote_html; $variables->{hidden_body} = $hidden_body; } eval { setlocale( LC_TIME, $locale ) if $locale; }; $variables->{date} = strftime( $date_fmt, localtime() ); return $variables; } =item check_ip IPNUMBER If the $bannednets configuration is defined and contains network specifications then check IPNUMBER against it, also if @use_rbls is defined will perform a check against these if it is not found in the local list. Returns a true value if the IP is found in either source. =cut sub check_ip { my ( $ip ) = @_; if ( $bannednets and -s $bannednets ) { open BANNED, "<$bannednets" or die "Can't open $bannednets - $!"; while () { chomp; next unless $_; if (ip_in_network($ip, $_)) { return 1; } } } foreach my $rbl (@use_rbls) { if ( !rbl_check($ip, $rbl )) { return 1; } } return 0; } =item check_banned Implement banned words list. If $bannedwords configuration is defined and is a file will take each line as a regular expression to be compared with the content presented as an argument. =cut sub check_banned { my ($temp) = @_; if ( $bannedwords and -s $bannedwords ) { if (!@bannedwords) { open BANNED, "<$bannedwords" or die "Can't open $bannedwords - $!"; chomp(@bannedwords = ); } foreach my $word (@bannedwords) { return 0 if ( lc($temp) =~ /$word/ ); } } return 1; } ##################### # New File Subroutine sub new_file { my ( $ft, $variables ) = @_; my $md = "$basedir/$mesgdir"; my $file = "$md/$variables->{id}.$ext"; -r $file and die "refusing to overwrite [$file]"; -d $md or mkdir $md, 0755 or die "mkdir $md: $!"; open( NEWFILE, ">$file.tmp" ) || die "Open [$file.tmp]: $!"; my $html_faq = $show_faq ? qq( [ FAQ ]) : ''; my $html_print_name = $variables->{email} ? qq({email}"}">$E{$variables->{name}} ) : $E{ $variables->{name} }; my $ip = $show_poster_ip ? "($ENV{REMOTE_ADDR})" : ''; my $html_pr_follow = ''; if ( $variables->{followup} ) { $html_pr_follow = qq(

In Reply to: {last_message}.$ext"}">$E{$variables->{origsubject}} posted by ); if ( $variables->{origemail} ) { $html_pr_follow .= qq($E{$variables->{origname}}); } else { $html_pr_follow .= $E{ $variables->{origname} }; } $html_pr_follow .= '

'; } my $html_img = $variables->{message_img} ? qq(

\n) : ''; my $html_email_input = $variables->{email} ? qq() : ''; my $html_url = $variables->{message_url} ? qq(
) : ''; my $followups = $variables->{id}; if ( defined $variables->{followups} ) { $followups = join( ',', @{ $variables->{followups} }, $followups ); } print NEWFILE < $E{$variables->{subject}} $html_style

$E{$variables->{subject}}


[ Follow Ups ] [ Post Followup ] [ $E{$title} ] $html_faq


Posted by $html_print_name $E{$ip} on $E{$variables->{date}}

$html_pr_follow $html_img $variables->{html_body}
$html_url

Follow Ups:



Post a Followup

$html_email_input END_HTML my $subject = $variables->{subject}; $subject = 'Re: ' . $subject unless $subject =~ /^Re:/i; if ( $subject_line == 1 ) { print NEWFILE qq(\n); print NEWFILE "\n"; } elsif ( $subject_line == 2 ) { print NEWFILE qq(\n); } else { print NEWFILE qq(\n); } print NEWFILE "\n"; print NEWFILE qq(\n"; print NEWFILE <
Name:
E-Mail:
Subject:$E{$subject}
Subject:
Subject:
Comments:
Optional Link URL:
Link Title:
Optional Image URL:
$html_preview_button

[ Follow Ups ] [ Post Followup ] [ $E{$title} ] $html_faq

END_HTML unless ( close NEWFILE ) { my $err = "close $file.tmp: $!"; unlink "$file.tmp"; die $err; } $ft->addfile( $file, "$file.tmp" ); } ############################### # Main WWWBoard Page Subroutine sub main_page { my ( $ft, $variables ) = @_; if ( $variables->{followup} ) { insert_followup( $ft, $variables, "$basedir/$mesgfile", "$mesgdir/" ); } else { $ft->linewise_rewrite( "$basedir/$mesgfile", sub { if (//) { $_ .= html_message_line( $variables, "$mesgdir/" ); } } ); } } sub insert_followup { my ( $ft, $variables, $file, $url_prefix ) = @_; my %is_followup_to = map { $_ => 1 } @{ $variables->{followups} }; $ft->linewise_rewrite( $file, sub { if (/\Q