chiark / gitweb /
8397f8694cc8816fc4c94c81f5a5f68525b34280
[modbot-mtm.git] / webstump / scripts / webstump.lib.pl
1 # this is a collection of library functions for stump.
2
3 use IO::Handle;
4
5 # error message
6 sub error {
7   my $msg = pop( @_ );
8
9   if( defined $html_mode ) {
10     print 
11 "Content-Type: text/html\n\n
12 <TITLE>WebSTUMP Error</TITLE>
13 <BODY BGCOLOR=\"#C5C5FF\" BACKGROUND=$base_address_for_files/images/bg1.jpg>
14 <H1>You have encountered an error in WebSTUMP.</H1>";
15
16   &print_image( "construction.gif", "bug in WebSTUMP" );
17
18   print " <B>$msg </B><HR>
19 Please cut and paste this
20 whole page and send it to <A HREF=mailto:$supporter>$supporter</A>.<P>
21 Query Parameters:<P>\n
22 <UL>";
23
24     foreach (keys %request) {
25       print "<LI> $_: $request{$_}\n";
26     }
27     exit 0;
28   }
29
30   die $msg;
31 }
32
33 # user error message
34 sub user_error {
35   my $msg = pop( @_ );
36   if( defined $html_mode ) {
37     print 
38 "Content-Type: text/html\n\n
39 <TITLE>You have made a mistake.</TITLE>
40 <BODY BGCOLOR=\"#C5C5FF\" BACKGROUND=$base_address_for_files/images/bg1.jpg>
41 <H1>You have made a mistake.</H1>
42   ";
43
44   &print_image( "warning_big.gif", "Warning" );
45
46   print " <B>$msg </B><HR>
47 Please go back to the previous page and correct it. If you get really
48 stuck, cut and paste this whole page and send it to <A
49 HREF=mailto:$supporter>$supporter</A>.
50
51 ";
52
53     exit 0;
54   }
55
56   die $msg;
57 }
58
59 # returns full config file name
60 sub full_config_file_name {
61   my $short_name = pop( @_ );
62   my $newsgroup = &required_parameter( "newsgroup" );
63   $newsgroup =~ m/^\w[.0-9a-z+]+$/ or die;
64   $newsgroup= $&;
65   return  "$webstump_home/config/newsgroups/$newsgroup/$short_name";
66 }
67
68 # checks if the admin password supplied is correct
69 sub verify_admin_password {
70
71   my $password = $request{'password'};
72
73   my $password_file = "$webstump_home/config/admin_password.txt";
74
75   open( PASSWORD, $password_file )
76         || &error( "Password file $password_file does not exist" );
77   my $correct_password = <PASSWORD>;
78   chomp $correct_password;
79   close( PASSWORD );
80
81   &user_error( "invalid admin password" )
82         if( $password ne $correct_password );
83
84 }
85
86 #
87 # appends a string to file.
88 #
89 sub append_to_file {
90   my $msg = pop( @_ );
91   my $file = pop( @_ );
92
93   open_file_for_appending( FILE, "$file" ) 
94         || die "Could not open $file for writing";
95   print FILE $msg;
96   close( FILE );
97 }
98
99 #
100 # add to config file
101 sub add_to_config_file {
102   my $line = pop( @_ );
103   my $file = pop( @_ );
104
105 print STDERR "File = $file, line= $line\n";
106
107   if( !&name_is_in_list( $line, $file ) ) {
108     &report_list_diff($file, sub {
109         print DIFF "Added: $line\n" or die $!;
110     });
111     &append_to_file( &full_config_file_name( $file ), "$line\n" );
112   }
113 }
114
115
116 sub report_list_diff ($$) {
117   my ($list_file, $innards) = @_;
118
119   my $head = &full_config_file_name( "change-notify-header" );
120   if (!open DHEAD, '<', $head) {
121       $!==&ENOENT or die "$head $!";
122       return;
123   }
124   my $diff = "$list_file.diff.$$.tmp";
125   my $ok= eval {
126       open DIFF, '>>', $diff or die "$diff $!";
127       while (<DHEAD>) { print DIFF or die $!; }
128       print DIFF <<END or die $!;
129
130 Moderator: $request{'moderator'}
131 Control file: $list_file
132
133 END
134       DHEAD->error and die $!;
135       DIFF->flush or die $!;
136
137       my $goahead= &$innards($diff);
138
139       if ($goahead) {
140           print DIFF "\n-- \n" or die $!;
141           close DIFF or die $!;
142           my $child= fork; die unless defined $child;
143           if (!$child) {
144               open STDIN, '<', $diff or die "$diff $!";
145               exec find_sendmail(), qw(-odb -oem -oee -oi -t);
146               die $!;
147           }
148           waitpid($child,0) == $child or die "$list_file $!";
149       }
150       $?==0 or die "$list_file $?";
151       unlink $diff or die $!;
152       1;
153   };
154   if (!$ok) {
155       unlink $diff;
156       &error("Could not report change to $list_file: $@");
157   }
158 }
159
160 # from CGI.pm
161 # unescape URL-encoded data
162 sub unescape {
163     my $todecode = shift;
164     $todecode =~ tr/+/ /;       # pluses become spaces
165     $todecode =~ s/%([0-9a-fA-F]{2})/pack("c",hex($1))/ge;
166     return $todecode;
167 }
168  
169 # sets various useful variables, etc
170 sub setup_variables {
171   $newsgroups_list_file = "$webstump_home/config/newsgroups.lst";
172 }
173
174 # initializes webstump, reads newsgroups list
175 sub init_webstump {
176   &setup_variables;
177
178   # read the NG list
179   opendir( NEWSGROUPS, "$webstump_home/config/newsgroups" )
180         || &error( "can't open $webstump_home/config/newsgroups" );
181
182     while( $_ = readdir( NEWSGROUPS ) ) {
183       my $file = "$webstump_home/config/newsgroups/$_/address.txt";
184       my $ng = $_;
185
186       next if ! -r $file;
187
188       open( FILE, $file );
189       $addr = <FILE>;
190       chop $addr;
191       close( FILE );
192
193         &error( "Invalid entry $_ in the newsgroups database." )
194                 if( !$ng || !$addr );
195         push @newsgroups_array,$ng;
196         $newsgroups_index{$ng} = "$addr";
197     }
198   close( NEWSGROUPS );
199
200   open( LOG, ">>$webstump_home/log/webstump.log" );
201   print LOG "Call from $ENV{'REMOTE_ADDR'}, QUERY_STRING=$ENV{'QUERY_STRING'}\n";
202 }
203
204 # gets the directory name for the newsgroup
205 sub getQueueDir {
206   my $newsgroup = pop( @_ );
207   if( $newsgroups_index{$newsgroup} ) {
208     return "$queues_dir/$newsgroup";
209   } 
210   return ""; # undefined ng
211 }
212
213 # reads request, if any
214 sub readWebRequest {
215   my @query;
216   my %result;
217   if( defined $ENV{"QUERY_STRING"} ) {
218
219     @query = split( /&/, $ENV{"QUERY_STRING"} );
220     foreach( @query ) {
221       my ($name, $value) = split( /=/ );
222       $result{&unescape($name)} = &unescape( $value );
223     }
224   }
225
226   while(<STDIN>) {
227     @query = split( /&/, $_ );
228     foreach( @query ) {
229       my ($name, $value) = split( /=/ );
230       $result{&unescape($name)} = &unescape( $value );
231     }
232   }
233
234   foreach( keys %result ) {
235     print LOG "Request: $_ = $result{$_}\n" if( $_ ne "password" );
236   }
237   return %result;
238 }
239
240 # Checks if the program is running in a demo mode
241 sub is_demo_mode {
242   return &optional_parameter( 'newsgroup' ) eq "demo.newsgroup" 
243          && !$ignore_demo_mode;
244 }
245
246 # opens file for writing
247 sub open_file_for_writing { # filehandle, filename
248   my $filename = pop( @_ );
249   my $filehandle = pop( @_ );
250
251   if( &is_demo_mode ) {
252         return( open( $filehandle, ">/dev/null" ) );  
253   } else {
254         return( open( $filehandle, ">$filename" ) );
255   }
256 }
257
258 # opens pipe for writing
259 sub open_pipe_for_writing { # filehandle, filename
260   my $filename = pop( @_ );
261   my $filehandle = pop( @_ );
262
263   if( &is_demo_mode ) {
264         return( open( $filehandle, ">/dev/null" ) );  
265   } else {
266         return( open( $filehandle, "|$filename" ) );
267   }
268 }
269
270 # opens file for appending
271 sub open_file_for_appending { # filehandle, filename
272   my $filename = pop( @_ );
273   my $filehandle = pop( @_ );
274
275   if( &is_demo_mode ) {
276         return( open( $filehandle, ">>/dev/null" ) );  
277   } else {
278         return( open( $filehandle, ">>$filename" ) );
279   }
280 }
281
282 # gets a parameter
283 sub get_parameter {
284   my $arg = pop( @_ );
285   return "" if( ! defined $request{$arg} );
286   return $request{$arg};
287 }
288
289 # barfs if the required parameter is not supplied
290 sub required_parameter {
291   my $arg = pop( @_ );
292   user_error( "Parameter \"$arg\" is not defined or is empty" )
293         if( ! defined $request{$arg} || !$request{$arg} );
294   return $request{$arg};
295 }
296
297 # optional request parameter
298 sub optional_parameter {
299   my $arg = pop( @_ );
300   return $request{$arg};
301 }
302
303 # issues a security alert
304 sub security_alert {
305   my $msg = pop( @_ );
306   print LOG "SECURITY_ALERT: $msg\n";
307 }
308
309 # reads the moderators info
310 sub read_moderators {
311   my $newsgroup = &required_parameter( "newsgroup" );
312
313   my $file = &full_config_file_name( "moderators" );
314
315   open( MODERATORS, "$file" )
316         || error( "Could not open file with moderator passwords: $file" );
317  
318   while( <MODERATORS> ) {
319     my ($name, $pwd) = split;
320     $moderators{"\U$name"} = "\U$pwd";
321   }
322  
323   close( MODERATORS );
324 }
325
326 # saves the moderators info
327 sub save_moderators {
328   my $newsgroup = &required_parameter( "newsgroup" );
329
330   my $file = &full_config_file_name( "moderators" );
331
332   open_file_for_writing( MODERATORS, $file );
333 #        || &error( "Could not open file with moderator passwords: $file" );
334
335   foreach (keys %moderators) {
336       print MODERATORS "$_ $moderators{$_}\n";
337   }
338  
339   close( MODERATORS );
340 }
341
342 # authenticates user
343 sub authenticate {
344   my $password = &required_parameter( "password" );
345   my $moderator = &required_parameter( "moderator" );
346   my $newsgroup = &required_parameter( "newsgroup" );
347   
348   &read_moderators;
349
350   if( !defined $moderators{"\U$moderator"} || 
351       $moderators{"\U$moderator"} ne "\U$password" ) {
352     &security_alert( "Authentication denied." )
353     &user_error( "Authentication denied." );
354   }
355 }
356
357 # cleans request of dangerous characters
358 sub disinfect_request {
359   if( defined $request{'newsgroup'} ) {
360     $newsgroup = $request{'newsgroup'};
361     $newsgroup =~ m/^(\w[.0-9a-z+]+)$/ or die;
362     $newsgroup= $1;
363     $request{'newsgroup'} = $newsgroup;
364   }
365
366   if( defined $request{'file'} ) {
367     my $file = $request{'file'};
368     $file =~ m/^\w[.0-9a-z]+\.list$|^dir_\d+_\d+$/ or die "$file ?";
369     $file = "$&";
370     $request{'file'} = $file;
371   }
372 }
373
374 # adds a user
375 sub add_user {
376   my $user = &required_parameter( "user" );
377   my $new_password = &required_parameter( "new_password" );
378
379   &user_error( "Username may only contain letters and digits" )
380     if( ! ($user =~ /^[a-zA-Z0-9]+$/ ) );
381   &user_error( "Password may only contain letters and digits" )
382     if( ! ($new_password =~ /^[a-zA-Z0-9]+$/ ) );
383   &user_error( "Cannot change password for user admin" )
384     if( "\U$user" eq "ADMIN" );
385
386   $moderators{"\U$user"} = "\U$new_password";
387
388   &save_moderators;
389 }
390
391 # checks that a config list is in enumerated set of values. Returns 
392 # untainted value
393 sub check_config_list {
394   my $list_to_edit = pop( @_ );
395
396  &user_error( "invalid list name $list_to_edit" )
397     if( $list_to_edit ne "good.posters.list"
398         && $list_to_edit ne "watch.posters.list"
399         && $list_to_edit ne "bad.posters.list"
400         && $list_to_edit ne "good.subjects.list"
401         && $list_to_edit ne "watch.subjects.list"
402         && $list_to_edit ne "bad.subjects.list"
403         && $list_to_edit ne "bad.words.list"
404         && $list_to_edit ne "watch.words.list" );
405
406   return &untaint( $list_to_edit );
407 }
408
409 # sets a configuration list (good posters etc)
410 sub set_config_list {
411   my $list_content = $request{"list"};
412   my $list_to_edit = &required_parameter( "list_to_edit" );
413
414   $list_content .= "\n";
415   $list_content =~ s/\r//g;
416   $list_content =~ s/\n+/\n/g;
417   $list_content =~ s/\n +/\n/g;
418   $list_content =~ s/^\n+//g;
419
420   $list_to_edit = &check_config_list( $list_to_edit );
421
422   my $list_file = &full_config_file_name( $list_to_edit );
423
424   open_file_for_writing( LIST, "$list_file.new" ) 
425     || &error( "Could not open $list_file for writing" );
426   print LIST $list_content;
427   close( LIST );
428
429   report_list_diff("$list_to_edit", sub {
430       my ($diff)= @_;
431       my $child= fork; die unless defined $child;
432       if (!$child) {
433           open STDOUT, '>&DIFF' or die $!;
434           exec 'diff','-u','-L', "$list_to_edit.old",'-L', "$list_to_edit.new",'--', "$list_file","$list_file.new";
435           die $!;
436       }
437       waitpid($child,0) == $child or die "$list_file $!";
438       $?==0 or $?==256 or die "$list_file $?";
439       return !!$?;
440   });
441   rename ("$list_file.new", "$list_file");
442 }
443
444 # deletes a user
445 sub delete_user {
446   my $user = &required_parameter( "user" );
447
448   &user_error( "User \U$user" . " does not exist!" ) 
449     if( ! defined $moderators{"\U$user"} );
450   &user_error( "Cannot delete user admin" )
451     if( "\U$user" eq "ADMIN" );
452
453   delete $moderators{"\U$user"};
454
455   &save_moderators;
456 }
457
458 # validate password change
459 sub validate_change_password {
460   my $user = &required_parameter( "moderator" );
461   my $new_password = &required_parameter( "new_password" );
462
463   &user_error( "Password may only contain letters and digits" )
464     if( ! ($new_password =~ /^[a-zA-Z0-9]+$/ ) );
465   &user_error( "Cannot change password for user admin" )
466     if( "\U$user" eq "ADMIN" );
467
468   $moderators{"\U$user"} = "\U$new_password";
469
470   &save_moderators;
471   &html_welcome_page;
472 }
473
474 # reads rejection reasons
475 sub read_rejection_reasons {
476   my $newsgroup = &required_parameter( 'newsgroup' );
477   my $reasons = &full_config_file_name( "rejection-reasons" );
478   open( REASONS, $reasons ) || &error( "Could not open file $reasons" );
479  
480   while( <REASONS> ) {
481         chop;
482         my ($name, $title) = split( /::/ );
483         $rejection_reasons{$name} = $title;
484         push @short_rejection_reasons, $name;
485   }
486
487   close REASONS;
488 }
489
490 sub find_sendmail {
491
492   my $sendmail = "";
493
494   foreach (@sendmail) {
495     if( -x $_ ) {
496       $sendmail = $_;
497       last;
498     }
499   }
500  
501   &error( "Sendmail not found" ) if( !$sendmail );
502
503   return $sendmail;
504 }
505
506 # email_message message recipient
507 sub email_message {
508   my $recipient = pop( @_ );
509   my $message = pop( @_ );
510   my $sendmail= find_sendmail;
511   my $sendmail_command = "$sendmail $recipient";
512   $sendmail_command =~ /(^.*$)/; 
513   $sendmail_command = $1; # untaint
514   open_pipe_for_writing( SENDMAIL, "$sendmail_command > /dev/null " );
515   print SENDMAIL $message;
516   close( SENDMAIL );
517                 
518 }
519
520 sub article_file_name {
521   my $file = pop( @_ );
522   return "$queues_dir/$newsgroup/$file";
523 }
524
525 sub untaint {
526   $arg = pop( @_ );
527   $arg =~ /(^.*$)/;
528   return $1;
529 }
530
531 sub rmdir_rf {
532   my $dir = pop( @_ );
533
534   return if &is_demo_mode;
535
536   opendir( DIR, $dir ) || return;
537   while( $_ = readdir(DIR) ) {
538     unlink &untaint( "$dir/$_" );
539   }
540   closedir( DIR );
541   rmdir( $dir );
542 }
543
544 sub approval_decision {
545   $newsgroup = &required_parameter( 'newsgroup' );
546   my $comment = &get_parameter( 'comment' );
547   my $decision = "";
548
549   my $poster_decision = &optional_parameter( "poster_decision" );
550   my $thread_decision = &optional_parameter( "thread_decision" );
551   
552   foreach( keys %request ) {
553     if( /^decision_(dir_[0-9a-z_]+)$/ ) {
554       $decision = $request{$&};
555       my $file= $1; # untainted
556
557       next if $request{'skip_submit'};
558       next if $decision eq 'skip';
559
560       my $waf= &article_file_name($1).'/stump-warning.txt';
561       if ($decision eq 'leave') {
562           my $now= time;  defined $now or die $!;
563           utime $now,$now, $waf or $!==&ENOENT or die "$waf $!";
564           next;
565       }
566
567       if ($decision eq 'consider') {
568           if (!open ADDWARN, '>>', $waf) {
569               $!==&ENOENT or die "$waf $!";
570           } else {
571               print ADDWARN "A moderator has marked this message for further consideration - please consult your comoderators before approving.\n" or die $!;
572               close ADDWARN or die $!;
573           }
574           next;
575       }
576
577       die "$decision ?" unless $decision =~ m/^(approve|reject \w+)$/;
578       $decision= $1;
579
580       my $fullpath = &article_file_name( $file ) . "/stump-prolog.txt";
581
582       $decision = "reject thread" if $thread_decision eq "ban";
583       $decision = "approve" if $thread_decision eq "preapprove";
584
585       $decision = "reject abuse" if $poster_decision eq "ban";
586       $decision = "approve" if $poster_decision eq "preapprove";
587
588       if( -r $fullpath && open( MESSAGE, "$fullpath" ) ) {
589         my $RealSubject = "", $From = "", $Subject = "";
590         while( <MESSAGE> ) {
591           if( /^Subject: /i ) {
592             chop;
593             $Subject = $_;
594             $Subject =~ s/Subject: +//i;
595           } elsif( /^Real-Subject: /i ) {
596             chop;
597             $RealSubject = $_;
598             $RealSubject =~ s/Real-Subject: +//i;
599             $RealSubject =~ s/Re: +//i;
600           } elsif( /^From: / ) {
601             chop;
602             $From = $_;
603             $From =~ s/From: //i;
604           }
605           last if /^$/;
606         }
607         close MESSAGE;
608
609         &add_to_config_file( "good.posters.list", $From ) 
610                 if $poster_decision eq "preapprove";
611
612         &add_to_config_file( "good.subjects.list", $RealSubject ) 
613                 if $thread_decision eq "preapprove";
614
615         &add_to_config_file( "watch.posters.list", $From ) 
616                 if $poster_decision eq "suspicious";
617
618         &add_to_config_file( "bad.posters.list", $From ) 
619                 if $poster_decision eq "ban";
620
621         &add_to_config_file( "bad.subjects.list", $RealSubject ) 
622                 if $thread_decision eq "ban";
623
624         &add_to_config_file( "watch.subjects.list", $RealSubject ) 
625                 if $thread_decision eq "watch";
626
627 # Subject, newsgroup, ShortDirectoryName, decision, comment
628         &process_approval_decision( $Subject, $newsgroup, $file, $decision, $comment );
629
630       }
631     }
632   }
633
634   &html_moderation_screen;
635 }
636
637 # gets the count of unapproved articles sitting in the queue
638 sub get_article_count {
639   my $newsgroup = pop( @_ );
640    my $count = 0;
641    my $dir = &getQueueDir( $newsgroup );
642    opendir( DIR, $dir );
643    my $file;
644    while( $file = readdir( DIR ) ) {
645      $count++ if( -d "$dir/$file" && $file ne "." && $file ne ".." && -r "$dir/$file/full_message.txt" );
646    }
647
648    return $count;
649 }
650
651 # processes web request
652 sub processWebRequest {
653
654   my $action = $request{'action'};
655   my $newsgroup = $request{'newsgroup'};
656   my $moderator = $request{'moderator'};
657   my $password = $request{'password'};
658
659   $moderator = "\L$moderator";
660
661   if( $action eq "login_screen" ) {
662     &html_login_screen;
663   } elsif( $action eq "moderation_screen" ) {
664     &authenticate( $newsgroup, $moderator, $password );
665     if( $moderator eq "admin" ) {
666       &html_newsgroup_management;
667     } else {
668       &html_moderation_screen;
669     }
670   } elsif( $action eq "moderator_admin" ) {
671     &authenticate( $newsgroup, $moderator, $password );
672     &html_newsgroup_management;
673   } elsif( $action eq "edit_list" ) {
674     &authenticate( $newsgroup, $moderator, $password );
675     &edit_configuration_list;
676   } elsif( $action eq "add_user" ) {
677     &authenticate( $newsgroup, $moderator, $password );
678     if( $moderator ne "admin" ) {
679       &security_alert( "Moderator $moderator tried to add user in $newsgroup" );
680       &user_error( "Only administrator (login ADMIN) can add or delete users" );
681     }
682
683     &add_user;
684     &html_newsgroup_management;
685   } elsif( $action eq "set_config_list" ) {
686     &authenticate( $newsgroup, $moderator, $password );
687     &set_config_list;
688     &html_newsgroup_management;
689   } elsif( $action eq "delete_user" ) {
690     &authenticate( $newsgroup, $moderator, $password );
691     if( $moderator ne "admin" ) {
692       &security_alert( "Moderator $moderator tried to add user in $newsgroup" );
693       &user_error( "Only administrator (login ADMIN) can add or delete users" );
694     }
695     &delete_user;
696     &html_newsgroup_management;
697   } elsif( $action eq "approval_decision" ) {
698     &authenticate( $newsgroup, $moderator, $password );
699     if( $moderator eq "admin" ) {
700       &user_error( "Login ADMIN exists for user management only" );
701     }
702     &approval_decision;
703   } elsif( $action eq "moderate_article" ) {
704     &authenticate( $newsgroup, $moderator, $password );
705     if( $moderator eq "admin" ) {
706       &user_error( "Login ADMIN exists for user management only" );
707     }
708     &html_moderate_article();
709   } elsif( $action eq "change_password" ) {
710     &authenticate( $newsgroup, $moderator, $password );
711     &html_change_password;
712   } elsif( $action eq "validate_change_password" ) {
713     &authenticate( $newsgroup, $moderator, $password );
714     &validate_change_password;
715 #  } elsif( $action eq "init_request_newsgroup_creation" ) {
716 #    &init_request_newsgroup_creation;
717 #  } elsif( $action eq "complete_newsgroup_creation_request" ) {
718 #    &complete_newsgroup_creation_request;
719   } elsif( $action eq "webstump_admin_screen" ) {
720     &webstump_admin_screen;
721   } elsif( $action eq "admin_login" ) {
722     &admin_login_screen;
723   } elsif( $action eq "admin_add_newsgroup" ) {
724     &admin_add_newsgroup;
725   } elsif( $action eq "help" ) {
726     &display_help;
727   } else {
728     &error( "Unknown user action: '$action'" );
729   }
730 }
731
732
733 1;