chiark / gitweb /
Only allow the bad.posters, watch.words, and good.posters lists to be
[modbot-ulm.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 "bad.posters.list"
401         && $list_to_edit ne "watch.words.list" );
402
403   return &untaint( $list_to_edit );
404 }
405
406 # sets a configuration list (good posters etc)
407 sub set_config_list {
408   my $list_content = $request{"list"};
409   my $list_to_edit = &required_parameter( "list_to_edit" );
410
411   $list_content .= "\n";
412   $list_content =~ s/\r//g;
413   $list_content =~ s/\n+/\n/g;
414   $list_content =~ s/\n +/\n/g;
415   $list_content =~ s/^\n+//g;
416
417   $list_to_edit = &check_config_list( $list_to_edit );
418
419   my $list_file = &full_config_file_name( $list_to_edit );
420
421   open_file_for_writing( LIST, "$list_file.new" ) 
422     || &error( "Could not open $list_file for writing" );
423   print LIST $list_content;
424   close( LIST );
425
426   report_list_diff("$list_to_edit", sub {
427       my ($diff)= @_;
428       my $child= fork; die unless defined $child;
429       if (!$child) {
430           open STDOUT, '>&DIFF' or die $!;
431           exec 'diff','-u','-L', "$list_to_edit.old",'-L', "$list_to_edit.new",'--', "$list_file","$list_file.new";
432           die $!;
433       }
434       waitpid($child,0) == $child or die "$list_file $!";
435       $?==0 or $?==256 or die "$list_file $?";
436       return !!$?;
437   });
438   rename ("$list_file.new", "$list_file");
439 }
440
441 # deletes a user
442 sub delete_user {
443   my $user = &required_parameter( "user" );
444
445   &user_error( "User \U$user" . " does not exist!" ) 
446     if( ! defined $moderators{"\U$user"} );
447   &user_error( "Cannot delete user admin" )
448     if( "\U$user" eq "ADMIN" );
449
450   delete $moderators{"\U$user"};
451
452   &save_moderators;
453 }
454
455 # validate password change
456 sub validate_change_password {
457   my $user = &required_parameter( "moderator" );
458   my $new_password = &required_parameter( "new_password" );
459
460   &user_error( "Password may only contain letters and digits" )
461     if( ! ($new_password =~ /^[a-zA-Z0-9]+$/ ) );
462   &user_error( "Cannot change password for user admin" )
463     if( "\U$user" eq "ADMIN" );
464
465   $moderators{"\U$user"} = "\U$new_password";
466
467   &save_moderators;
468   &html_welcome_page;
469 }
470
471 # reads rejection reasons
472 sub read_rejection_reasons {
473   my $newsgroup = &required_parameter( 'newsgroup' );
474   my $reasons = &full_config_file_name( "rejection-reasons" );
475   open( REASONS, $reasons ) || &error( "Could not open file $reasons" );
476  
477   while( <REASONS> ) {
478         chop;
479         my ($name, $title) = split( /::/ );
480         $rejection_reasons{$name} = $title;
481         push @short_rejection_reasons, $name;
482   }
483
484   close REASONS;
485 }
486
487 sub find_sendmail {
488
489   my $sendmail = "";
490
491   foreach (@sendmail) {
492     if( -x $_ ) {
493       $sendmail = $_;
494       last;
495     }
496   }
497  
498   &error( "Sendmail not found" ) if( !$sendmail );
499
500   return $sendmail;
501 }
502
503 # email_message message recipient
504 sub email_message {
505   my $recipient = pop( @_ );
506   my $message = pop( @_ );
507   my $sendmail= find_sendmail;
508   my $sendmail_command = "$sendmail $recipient";
509   $sendmail_command =~ /(^.*$)/; 
510   $sendmail_command = $1; # untaint
511   open_pipe_for_writing( SENDMAIL, "$sendmail_command > /dev/null " )
512                          or die $!;
513   print SENDMAIL $message or die $!;
514   close( SENDMAIL ) or die "$? $!";
515                 
516 }
517
518 sub article_file_name {
519   my $file = pop( @_ );
520   return "$queues_dir/$newsgroup/$file";
521 }
522
523 sub untaint {
524   $arg = pop( @_ );
525   $arg =~ /(^.*$)/;
526   return $1;
527 }
528
529 sub rmdir_rf {
530   my $dir = pop( @_ );
531
532   return if &is_demo_mode;
533
534   opendir( DIR, $dir ) || return;
535   while( $_ = readdir(DIR) ) {
536     unlink &untaint( "$dir/$_" );
537   }
538   closedir( DIR );
539   rmdir( $dir );
540 }
541
542 sub approval_decision {
543   $newsgroup = &required_parameter( 'newsgroup' );
544   my $comment = &get_parameter( 'comment' );
545   my $decision = "";
546
547   my $poster_decision = &optional_parameter( "poster_decision" );
548   my $thread_decision = &optional_parameter( "thread_decision" );
549   
550   foreach( keys %request ) {
551     if( /^decision_(dir_[0-9a-z_]+)$/ ) {
552       $decision = $request{$&};
553       my $file= $1; # untainted
554
555       next if $request{'skip_submit'};
556       next if $decision eq 'skip';
557
558       my $waf= &article_file_name($1).'/stump-warning.txt';
559       if ($decision eq 'leave') {
560           my $now= time;  defined $now or die $!;
561           utime $now,$now, $waf or $!==&ENOENT or die "$waf $!";
562           next;
563       }
564
565       if ($decision eq 'consider') {
566           if (!open ADDWARN, '>>', $waf) {
567               $!==&ENOENT or die "$waf $!";
568           } else {
569               print ADDWARN "A moderator has marked this message for further consideration - please consult your comoderators before approving.\n" or die $!;
570               close ADDWARN or die $!;
571           }
572           next;
573       }
574
575       die "$decision ?" unless $decision =~ m/^(approve|reject \w+)$/;
576       $decision= $1;
577
578       my $fullpath = &article_file_name( $file ) . "/stump-prolog.txt";
579
580       $decision = "reject thread" if $thread_decision eq "ban";
581       $decision = "approve" if $thread_decision eq "preapprove";
582
583       #$decision = "reject blocklist" if $poster_decision eq "ban";
584       die if $decision ne "approve" and $poster_decision eq "preapprove";
585
586       if( -r $fullpath && open( MESSAGE, "$fullpath" ) ) {
587         my $RealSubject = "", $From = "", $Subject = "";
588         while( <MESSAGE> ) {
589           if( /^Subject: /i ) {
590             chop;
591             $Subject = $_;
592             $Subject =~ s/Subject: +//i;
593           } elsif( /^Real-Subject: /i ) {
594             chop;
595             $RealSubject = $_;
596             $RealSubject =~ s/Real-Subject: +//i;
597             $RealSubject =~ s/Re: +//i;
598           } elsif( /^From: / ) {
599             chop;
600             $From = $_;
601             $From =~ s/From: //i;
602           }
603           last if /^$/;
604         }
605         close MESSAGE;
606
607         &add_to_config_file( "good.posters.list", $From ) 
608                 if $poster_decision eq "preapprove";
609
610         &add_to_config_file( "good.subjects.list", $RealSubject ) 
611                 if $thread_decision eq "preapprove";
612
613         &add_to_config_file( "watch.posters.list", $From ) 
614                 if $poster_decision eq "suspicious";
615
616         &add_to_config_file( "bad.posters.list", $From ) 
617                 if $poster_decision eq "ban";
618
619         &add_to_config_file( "bad.subjects.list", $RealSubject ) 
620                 if $thread_decision eq "ban";
621
622         &add_to_config_file( "watch.subjects.list", $RealSubject ) 
623                 if $thread_decision eq "watch";
624
625 # Subject, newsgroup, ShortDirectoryName, decision, comment
626         &process_approval_decision( $Subject, $newsgroup, $file, $decision, $comment, "moderator \U$request{'moderator'}" );
627
628       }
629     }
630   }
631
632   &html_moderation_screen;
633 }
634
635 # gets the count of unapproved articles sitting in the queue
636 sub get_article_count {
637   my $newsgroup = pop( @_ );
638    my $count = 0;
639    my $dir = &getQueueDir( $newsgroup );
640    opendir( DIR, $dir );
641    my $file;
642    while( $file = readdir( DIR ) ) {
643      $count++ if( -d "$dir/$file" && $file ne "." && $file ne ".." && -r "$dir/$file/full_message.txt" );
644    }
645
646    return $count;
647 }
648
649 # processes web request
650 sub processWebRequest {
651
652   my $action = $request{'action'};
653   my $newsgroup = $request{'newsgroup'};
654   my $moderator = $request{'moderator'};
655   my $password = $request{'password'};
656
657   $moderator = "\L$moderator";
658
659   if( $action eq "login_screen" ) {
660     &html_login_screen;
661   } elsif( $action eq "moderation_screen" ) {
662     &authenticate( $newsgroup, $moderator, $password );
663     if( $moderator eq "admin" ) {
664       &html_newsgroup_management;
665     } else {
666       &html_moderation_screen;
667     }
668   } elsif( $action eq "moderator_admin" ) {
669     &authenticate( $newsgroup, $moderator, $password );
670     &html_newsgroup_management;
671   } elsif( $action eq "edit_list" ) {
672     &authenticate( $newsgroup, $moderator, $password );
673     &edit_configuration_list;
674   } elsif( $action eq "add_user" ) {
675     &authenticate( $newsgroup, $moderator, $password );
676     if( $moderator ne "admin" ) {
677       &security_alert( "Moderator $moderator tried to add user in $newsgroup" );
678       &user_error( "Only administrator (login ADMIN) can add or delete users" );
679     }
680
681     &add_user;
682     &html_newsgroup_management;
683   } elsif( $action eq "set_config_list" ) {
684     &authenticate( $newsgroup, $moderator, $password );
685     &set_config_list;
686     &html_newsgroup_management;
687   } elsif( $action eq "delete_user" ) {
688     &authenticate( $newsgroup, $moderator, $password );
689     if( $moderator ne "admin" ) {
690       &security_alert( "Moderator $moderator tried to add user in $newsgroup" );
691       &user_error( "Only administrator (login ADMIN) can add or delete users" );
692     }
693     &delete_user;
694     &html_newsgroup_management;
695   } elsif( $action eq "approval_decision" ) {
696     &authenticate( $newsgroup, $moderator, $password );
697     if( $moderator eq "admin" ) {
698       &user_error( "Login ADMIN exists for user management only" );
699     }
700     &approval_decision;
701   } elsif( $action eq "moderate_article" ) {
702     &authenticate( $newsgroup, $moderator, $password );
703     if( $moderator eq "admin" ) {
704       &user_error( "Login ADMIN exists for user management only" );
705     }
706     &html_moderate_article();
707   } elsif( $action eq "change_password" ) {
708     &authenticate( $newsgroup, $moderator, $password );
709     &html_change_password;
710   } elsif( $action eq "search_logs" ) {
711     &authenticate( $newsgroup, $moderator, $password );
712     &html_search_logs;
713   } elsif( $action eq "validate_change_password" ) {
714     &authenticate( $newsgroup, $moderator, $password );
715     &validate_change_password;
716 #  } elsif( $action eq "init_request_newsgroup_creation" ) {
717 #    &init_request_newsgroup_creation;
718 #  } elsif( $action eq "complete_newsgroup_creation_request" ) {
719 #    &complete_newsgroup_creation_request;
720   } elsif( $action eq "webstump_admin_screen" ) {
721     &webstump_admin_screen;
722   } elsif( $action eq "admin_login" ) {
723     &admin_login_screen;
724   } elsif( $action eq "admin_add_newsgroup" ) {
725     &admin_add_newsgroup;
726   } elsif( $action eq "help" ) {
727     &display_help;
728   } else {
729     &error( "Unknown user action: '$action'" );
730   }
731 }
732
733
734 1;