chiark / gitweb /
Support querying and downloading of decision reasons by moderators
[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 ) or die $!;
189       $addr = <FILE>;
190       defined $addr or die $!;
191       chop $addr;
192       close( FILE );
193
194         &error( "Invalid entry $_ in the newsgroups database." )
195                 if( !$ng || !$addr );
196         push @newsgroups_array,$ng;
197         $newsgroups_index{$ng} = "$addr";
198     }
199   close( NEWSGROUPS );
200
201   open( LOG, ">>$webstump_home/log/webstump.log" ) or die $!;
202   LOG->autoflush(1);
203   print LOG "Call from $ENV{'REMOTE_ADDR'}, QUERY_STRING=$ENV{'QUERY_STRING'}\n" or die $!;
204 }
205
206 # gets the directory name for the newsgroup
207 sub getQueueDir {
208   my $newsgroup = pop( @_ );
209   if( $newsgroups_index{$newsgroup} ) {
210     return "$queues_dir/$newsgroup";
211   } 
212   return ""; # undefined ng
213 }
214
215 # reads request, if any
216 sub readWebRequest {
217   my @query;
218   my %result;
219   if( defined $ENV{"QUERY_STRING"} ) {
220
221     @query = split( /&/, $ENV{"QUERY_STRING"} );
222     foreach( @query ) {
223       my ($name, $value) = split( /=/ );
224       $result{&unescape($name)} = &unescape( $value );
225     }
226   }
227
228   while(<STDIN>) {
229     @query = split( /&/, $_ );
230     foreach( @query ) {
231       my ($name, $value) = split( /=/ );
232       $result{&unescape($name)} = &unescape( $value );
233     }
234   }
235
236   foreach( keys %result ) {
237     print LOG "Request: $_ = $result{$_}\n" if( $_ ne "password" );
238   }
239   return %result;
240 }
241
242 # Checks if the program is running in a demo mode
243 sub is_demo_mode {
244   return &optional_parameter( 'newsgroup' ) eq "demo.newsgroup" 
245          && !$ignore_demo_mode;
246 }
247
248 # opens file for writing
249 sub open_file_for_writing { # filehandle, filename
250   my $filename = pop( @_ );
251   my $filehandle = pop( @_ );
252
253   if( &is_demo_mode ) {
254         return( open( $filehandle, ">/dev/null" ) );  
255   } else {
256         return( open( $filehandle, ">$filename" ) );
257   }
258 }
259
260 # opens pipe for writing
261 sub open_pipe_for_writing { # filehandle, filename
262   my $filename = pop( @_ );
263   my $filehandle = pop( @_ );
264
265   if( &is_demo_mode ) {
266         return( open( $filehandle, ">/dev/null" ) );  
267   } else {
268         return( open( $filehandle, "|$filename" ) );
269   }
270 }
271
272 # opens file for appending
273 sub open_file_for_appending { # filehandle, filename
274   my $filename = pop( @_ );
275   my $filehandle = pop( @_ );
276
277   if( &is_demo_mode ) {
278         return( open( $filehandle, ">>/dev/null" ) );  
279   } else {
280         return( open( $filehandle, ">>$filename" ) );
281   }
282 }
283
284 # gets a parameter
285 sub get_parameter {
286   my $arg = pop( @_ );
287   return "" if( ! defined $request{$arg} );
288   return $request{$arg};
289 }
290
291 # barfs if the required parameter is not supplied
292 sub required_parameter {
293   my $arg = pop( @_ );
294   user_error( "Parameter \"$arg\" is not defined or is empty" )
295         if( ! defined $request{$arg} || !$request{$arg} );
296   return $request{$arg};
297 }
298
299 # optional request parameter
300 sub optional_parameter {
301   my $arg = pop( @_ );
302   return $request{$arg};
303 }
304
305 # issues a security alert
306 sub security_alert {
307   my $msg = pop( @_ );
308   print LOG "SECURITY_ALERT: $msg\n";
309 }
310
311 # reads the moderators info
312 sub read_moderators {
313   my $newsgroup = &required_parameter( "newsgroup" );
314
315   my $file = &full_config_file_name( "moderators" );
316
317   open( MODERATORS, "$file" )
318         || error( "Could not open file with moderator passwords: $file" );
319  
320   while( <MODERATORS> ) {
321     my ($name, $pwd) = split;
322     $moderators{"\U$name"} = "\U$pwd";
323   }
324  
325   close( MODERATORS );
326 }
327
328 # saves the moderators info
329 sub save_moderators {
330   my $newsgroup = &required_parameter( "newsgroup" );
331
332   my $file = &full_config_file_name( "moderators" );
333
334   open_file_for_writing( MODERATORS, $file );
335 #        || &error( "Could not open file with moderator passwords: $file" );
336
337   foreach (keys %moderators) {
338       print MODERATORS "$_ $moderators{$_}\n";
339   }
340  
341   close( MODERATORS );
342 }
343
344 # authenticates user
345 sub authenticate {
346   my $password = &required_parameter( "password" );
347   my $moderator = &required_parameter( "moderator" );
348   my $newsgroup = &required_parameter( "newsgroup" );
349   
350   &read_moderators;
351
352   if( !defined $moderators{"\U$moderator"} || 
353       $moderators{"\U$moderator"} ne "\U$password" ) {
354     &security_alert( "Authentication denied." )
355     &user_error( "Authentication denied." );
356   }
357 }
358
359 # cleans request of dangerous characters
360 sub disinfect_request {
361   if( defined $request{'newsgroup'} ) {
362     $newsgroup = $request{'newsgroup'};
363     $newsgroup =~ m/^(\w[.0-9a-z+]+)$/ or die;
364     $newsgroup= $1;
365     $request{'newsgroup'} = $newsgroup;
366   }
367
368   if( defined $request{'file'} ) {
369     my $file = $request{'file'};
370     $file =~ m/^\w[.0-9a-z]+\.list$|^dir_\d+_\d+$/ or die "$file ?";
371     $file = "$&";
372     $request{'file'} = $file;
373   }
374 }
375
376 # adds a user
377 sub add_user {
378   my $user = &required_parameter( "user" );
379   my $new_password = &required_parameter( "new_password" );
380
381   &user_error( "Username may only contain letters and digits" )
382     if( ! ($user =~ /^[a-zA-Z0-9]+$/ ) );
383   &user_error( "Password may only contain letters and digits" )
384     if( ! ($new_password =~ /^[a-zA-Z0-9]+$/ ) );
385   &user_error( "Cannot change password for user admin" )
386     if( "\U$user" eq "ADMIN" );
387
388   $moderators{"\U$user"} = "\U$new_password";
389
390   &save_moderators;
391 }
392
393 # checks that a config list is in enumerated set of values. Returns 
394 # untainted value
395 sub check_config_list {
396   my $list_to_edit = pop( @_ );
397
398  &user_error( "invalid list name $list_to_edit" )
399     if( $list_to_edit ne "good.posters.list"
400         && $list_to_edit ne "watch.posters.list"
401         && $list_to_edit ne "bad.posters.list"
402         && $list_to_edit ne "good.subjects.list"
403         && $list_to_edit ne "watch.subjects.list"
404         && $list_to_edit ne "bad.subjects.list"
405         && $list_to_edit ne "bad.words.list"
406         && $list_to_edit ne "watch.words.list" );
407
408   return &untaint( $list_to_edit );
409 }
410
411 # sets a configuration list (good posters etc)
412 sub set_config_list {
413   my $list_content = $request{"list"};
414   my $list_to_edit = &required_parameter( "list_to_edit" );
415
416   $list_content .= "\n";
417   $list_content =~ s/\r//g;
418   $list_content =~ s/\n+/\n/g;
419   $list_content =~ s/\n +/\n/g;
420   $list_content =~ s/^\n+//g;
421
422   $list_to_edit = &check_config_list( $list_to_edit );
423
424   my $list_file = &full_config_file_name( $list_to_edit );
425
426   open_file_for_writing( LIST, "$list_file.new" ) 
427     || &error( "Could not open $list_file for writing" );
428   print LIST $list_content;
429   close( LIST );
430
431   report_list_diff("$list_to_edit", sub {
432       my ($diff)= @_;
433       my $child= fork; die unless defined $child;
434       if (!$child) {
435           open STDOUT, '>&DIFF' or die $!;
436           exec 'diff','-u','-L', "$list_to_edit.old",'-L', "$list_to_edit.new",'--', "$list_file","$list_file.new";
437           die $!;
438       }
439       waitpid($child,0) == $child or die "$list_file $!";
440       $?==0 or $?==256 or die "$list_file $?";
441       return !!$?;
442   });
443   rename ("$list_file.new", "$list_file");
444 }
445
446 # deletes a user
447 sub delete_user {
448   my $user = &required_parameter( "user" );
449
450   &user_error( "User \U$user" . " does not exist!" ) 
451     if( ! defined $moderators{"\U$user"} );
452   &user_error( "Cannot delete user admin" )
453     if( "\U$user" eq "ADMIN" );
454
455   delete $moderators{"\U$user"};
456
457   &save_moderators;
458 }
459
460 # validate password change
461 sub validate_change_password {
462   my $user = &required_parameter( "moderator" );
463   my $new_password = &required_parameter( "new_password" );
464
465   &user_error( "Password may only contain letters and digits" )
466     if( ! ($new_password =~ /^[a-zA-Z0-9]+$/ ) );
467   &user_error( "Cannot change password for user admin" )
468     if( "\U$user" eq "ADMIN" );
469
470   $moderators{"\U$user"} = "\U$new_password";
471
472   &save_moderators;
473   &html_welcome_page;
474 }
475
476 # reads rejection reasons
477 sub read_rejection_reasons {
478   my $newsgroup = &required_parameter( 'newsgroup' );
479   my $reasons = &full_config_file_name( "rejection-reasons" );
480   open( REASONS, $reasons ) || &error( "Could not open file $reasons" );
481  
482   while( <REASONS> ) {
483         chop;
484         my ($name, $title) = split( /::/ );
485         $rejection_reasons{$name} = $title;
486         push @short_rejection_reasons, $name;
487   }
488
489   close REASONS;
490 }
491
492 sub find_sendmail {
493
494   my $sendmail = "";
495
496   foreach (@sendmail) {
497     if( -x $_ ) {
498       $sendmail = $_;
499       last;
500     }
501   }
502  
503   &error( "Sendmail not found" ) if( !$sendmail );
504
505   return $sendmail;
506 }
507
508 # email_message message recipient
509 sub email_message {
510   my $recipient = pop( @_ );
511   my $message = pop( @_ );
512   my $sendmail= find_sendmail;
513   my $sendmail_command = "$sendmail $recipient";
514   $sendmail_command =~ /(^.*$)/; 
515   $sendmail_command = $1; # untaint
516   open_pipe_for_writing( SENDMAIL, "$sendmail_command > /dev/null " )
517                          or die $!;
518   print SENDMAIL $message or die $!;
519   close( SENDMAIL ) or die "$? $!";
520                 
521 }
522
523 sub article_file_name {
524   my $file = pop( @_ );
525   return "$queues_dir/$newsgroup/$file";
526 }
527
528 sub untaint {
529   $arg = pop( @_ );
530   $arg =~ /(^.*$)/;
531   return $1;
532 }
533
534 sub rmdir_rf {
535   my $dir = pop( @_ );
536
537   return if &is_demo_mode;
538
539   opendir( DIR, $dir ) || return;
540   while( $_ = readdir(DIR) ) {
541     unlink &untaint( "$dir/$_" );
542   }
543   closedir( DIR );
544   rmdir( $dir );
545 }
546
547 sub approval_decision {
548   $newsgroup = &required_parameter( 'newsgroup' );
549   my $comment = &get_parameter( 'comment' );
550   my $decision = "";
551
552   my $poster_decision = &optional_parameter( "poster_decision" );
553   my $thread_decision = &optional_parameter( "thread_decision" );
554   
555   foreach( keys %request ) {
556     if( /^decision_(dir_[0-9a-z_]+)$/ ) {
557       $decision = $request{$&};
558       my $file= $1; # untainted
559
560       next if $request{'skip_submit'};
561       next if $decision eq 'skip';
562
563       my $waf= &article_file_name($1).'/stump-warning.txt';
564       if ($decision eq 'leave') {
565           my $now= time;  defined $now or die $!;
566           utime $now,$now, $waf or $!==&ENOENT or die "$waf $!";
567           next;
568       }
569
570       if ($decision eq 'consider') {
571           if (!open ADDWARN, '>>', $waf) {
572               $!==&ENOENT or die "$waf $!";
573           } else {
574               print ADDWARN "A moderator has marked this message for further consideration - please consult your comoderators before approving.\n" or die $!;
575               close ADDWARN or die $!;
576           }
577           next;
578       }
579
580       die "$decision ?" unless $decision =~ m/^(approve|reject \w+)$/;
581       $decision= $1;
582
583       my $fullpath = &article_file_name( $file ) . "/stump-prolog.txt";
584
585       $decision = "reject thread" if $thread_decision eq "ban";
586       $decision = "approve" if $thread_decision eq "preapprove";
587
588       $decision = "reject abuse" if $poster_decision eq "ban";
589       $decision = "approve" if $poster_decision eq "preapprove";
590
591       if( -r $fullpath && open( MESSAGE, "$fullpath" ) ) {
592         my $RealSubject = "", $From = "", $Subject = "";
593         while( <MESSAGE> ) {
594           if( /^Subject: /i ) {
595             chop;
596             $Subject = $_;
597             $Subject =~ s/Subject: +//i;
598           } elsif( /^Real-Subject: /i ) {
599             chop;
600             $RealSubject = $_;
601             $RealSubject =~ s/Real-Subject: +//i;
602             $RealSubject =~ s/Re: +//i;
603           } elsif( /^From: / ) {
604             chop;
605             $From = $_;
606             $From =~ s/From: //i;
607           }
608           last if /^$/;
609         }
610         close MESSAGE;
611
612         &add_to_config_file( "good.posters.list", $From ) 
613                 if $poster_decision eq "preapprove";
614
615         &add_to_config_file( "good.subjects.list", $RealSubject ) 
616                 if $thread_decision eq "preapprove";
617
618         &add_to_config_file( "watch.posters.list", $From ) 
619                 if $poster_decision eq "suspicious";
620
621         &add_to_config_file( "bad.posters.list", $From ) 
622                 if $poster_decision eq "ban";
623
624         &add_to_config_file( "bad.subjects.list", $RealSubject ) 
625                 if $thread_decision eq "ban";
626
627         &add_to_config_file( "watch.subjects.list", $RealSubject ) 
628                 if $thread_decision eq "watch";
629
630 # Subject, newsgroup, ShortDirectoryName, decision, comment
631         &process_approval_decision( $Subject, $newsgroup, $file, $decision, $comment, "moderator \U$request{'moderator'}" );
632
633       }
634     }
635   }
636
637   &html_moderation_screen;
638 }
639
640 # gets the count of unapproved articles sitting in the queue
641 sub get_article_count {
642   my $newsgroup = pop( @_ );
643    my $count = 0;
644    my $dir = &getQueueDir( $newsgroup );
645    opendir( DIR, $dir );
646    my $file;
647    while( $file = readdir( DIR ) ) {
648      $count++ if( -d "$dir/$file" && $file ne "." && $file ne ".." && -r "$dir/$file/full_message.txt" );
649    }
650
651    return $count;
652 }
653
654 # processes web request
655 sub processWebRequest {
656
657   my $action = $request{'action'};
658   my $newsgroup = $request{'newsgroup'};
659   my $moderator = $request{'moderator'};
660   my $password = $request{'password'};
661
662   $moderator = "\L$moderator";
663
664   if( $action eq "login_screen" ) {
665     &html_login_screen;
666   } elsif( $action eq "moderation_screen" ) {
667     &authenticate( $newsgroup, $moderator, $password );
668     if( $moderator eq "admin" ) {
669       &html_newsgroup_management;
670     } else {
671       &html_moderation_screen;
672     }
673   } elsif( $action eq "moderator_admin" ) {
674     &authenticate( $newsgroup, $moderator, $password );
675     &html_newsgroup_management;
676   } elsif( $action eq "edit_list" ) {
677     &authenticate( $newsgroup, $moderator, $password );
678     &edit_configuration_list;
679   } elsif( $action eq "add_user" ) {
680     &authenticate( $newsgroup, $moderator, $password );
681     if( $moderator ne "admin" ) {
682       &security_alert( "Moderator $moderator tried to add user in $newsgroup" );
683       &user_error( "Only administrator (login ADMIN) can add or delete users" );
684     }
685
686     &add_user;
687     &html_newsgroup_management;
688   } elsif( $action eq "set_config_list" ) {
689     &authenticate( $newsgroup, $moderator, $password );
690     &set_config_list;
691     &html_newsgroup_management;
692   } elsif( $action eq "delete_user" ) {
693     &authenticate( $newsgroup, $moderator, $password );
694     if( $moderator ne "admin" ) {
695       &security_alert( "Moderator $moderator tried to add user in $newsgroup" );
696       &user_error( "Only administrator (login ADMIN) can add or delete users" );
697     }
698     &delete_user;
699     &html_newsgroup_management;
700   } elsif( $action eq "approval_decision" ) {
701     &authenticate( $newsgroup, $moderator, $password );
702     if( $moderator eq "admin" ) {
703       &user_error( "Login ADMIN exists for user management only" );
704     }
705     &approval_decision;
706   } elsif( $action eq "moderate_article" ) {
707     &authenticate( $newsgroup, $moderator, $password );
708     if( $moderator eq "admin" ) {
709       &user_error( "Login ADMIN exists for user management only" );
710     }
711     &html_moderate_article();
712   } elsif( $action eq "change_password" ) {
713     &authenticate( $newsgroup, $moderator, $password );
714     &html_change_password;
715   } elsif( $action eq "search_logs" ) {
716     &authenticate( $newsgroup, $moderator, $password );
717     &html_search_logs;
718   } elsif( $action eq "validate_change_password" ) {
719     &authenticate( $newsgroup, $moderator, $password );
720     &validate_change_password;
721 #  } elsif( $action eq "init_request_newsgroup_creation" ) {
722 #    &init_request_newsgroup_creation;
723 #  } elsif( $action eq "complete_newsgroup_creation_request" ) {
724 #    &complete_newsgroup_creation_request;
725   } elsif( $action eq "webstump_admin_screen" ) {
726     &webstump_admin_screen;
727   } elsif( $action eq "admin_login" ) {
728     &admin_login_screen;
729   } elsif( $action eq "admin_add_newsgroup" ) {
730     &admin_add_newsgroup;
731   } elsif( $action eq "help" ) {
732     &display_help;
733   } else {
734     &error( "Unknown user action: '$action'" );
735   }
736 }
737
738
739 1;