]> www.wagner.pp.ru Git - oss/stilllife.git/blob - forum/forum
Fixed processing of link rel="forum-user-list"
[oss/stilllife.git] / forum / forum
1 #!/usr/bin/perl -T
2 #
3 # Stil Life forum. Copyright (c) by Victor B. Wagner, 2008    
4 # This program distributed under GNU Affero General Public License v3 or
5 # above
6 # http://www.gnu.org/licenses/agpl.html
7 #
8 # Вкратце: Если вы используете этот скрипт на своем сайте, Вы обязаны
9 # сделать доступным его исходный текст. В частности, если Вы внесли
10 # какие-либо изменения, вы должны эти изменения опубликовать. 
11
12 # Home site of this program http://vitus.wagner.pp.ru/stilllife
13
14 use strict;
15 use warnings;
16 use Fcntl qw(:DEFAULT :flock);
17 use CGI;
18 use HTML::TreeBuilder;
19 use Storable qw(freeze thaw);
20 use Date::Parse;
21 use Email::Valid;
22 use LWP::UserAgent;
23 use Net::OpenID::Consumer;
24 #
25 # Набор поддерживаемых действий. Хэш вида 
26 # "имя поля в запросе" =>  "функция обработчик"
27 #
28 my %actions = (
29         reply => \&reply,
30         edit => \&edit_comment,
31         delete => \&delete_comment,
32         move => \&move_comment,
33         newtopic=> \&new_topic,
34         newforum=> \&new_forum,
35         login => \&login,
36         register=>\&register,
37         profile=>\&profile,
38         setrights=>\&set_rights,
39         openidlogin=>\&openid_login,
40         openidvfy =>\&openid_verify
41 );      
42 #
43 #  Уровень прав, которые необходимо иметь пользователю для совершения
44 #  определенного действия
45 #  иерархия вида undef < banned < normal < author < moderator <admin
46 #  Если операция не упомянута в данном массив, то значит можно всем, в
47 #  том числе  и анониму.
48 # Слово login означает, что вообще-то это normal, но пользователь может
49 # логиниться непосредственно в процессе выполнения операции.
50 my %permissions = (
51         reply => "login",
52         edit => "author",
53         delete => "author",
54         newtopic => "normal",
55         move => "moderator",
56         newforum => "moderator",
57         profile => "normal",
58         setrights => "admin",
59 );      
60
61 my $cgi = new CGI;
62 print STDERR "--------------------\n";
63 my $forum=get_forum_config();
64
65
66 authorize_user($cgi,$forum);
67 if ($cgi->request_method ne "POST") {
68 # Запрос к скрипту методом GET. Надо показать форму, если только это не
69 # редирект от OpenId-сервера 
70         if ($cgi->param('openidvfy')) { 
71                 openid_verify($cgi,$forum);
72         } elsif ($cgi->param("logout")) {
73                 logout('logout',$cgi,$forum);
74         } else {
75                 for my $param ($cgi->param) {
76 # Среди параметров, указанных в URL ищем тот, который задает
77 # действие 
78                         if (exists $actions{$param}) {
79 # Мы, конечно уже проверили, что в названии параметра
80 # нехороших символов нет, но чтобы perl в taint mode не
81 # ругался... 
82                                 if (allow_operation($param,$cgi,$forum)) {
83                                         show_template($1,$cgi,$forum) if $param=~/^(\w+)$/;     
84                                         exit;
85                                 } else {
86                                         if (!$forum->{"authenticated"}) { 
87                                                 $cgi->param("returnto",$cgi->url(-full=>1));
88                                                 show_template("login",$cgi,$forum);
89                                                 exit;
90
91                                         } else {
92                                                 show_error($forum,"У Вас нет прав на  выполнение этой
93                                                 операции")
94                                         }
95                                 }       
96                         }
97                 }
98                 show_error($forum,"Некорректный вызов скрипта. Отсутствует параметр
99                                 действия");
100         }       
101 } else {
102         # Запрос методом POST. Вызываем обработчик
103         for my $param ($cgi->param) {
104                 if (exists $actions{$param}) {
105                         if (allow_operation($param,$cgi,$forum)) {
106                                 $actions{$param}->($param,$cgi,$forum);
107                                 exit;
108                         } else {
109                                 show_error($forum,"У Вас нет прав на  выполнение этой
110                                 операции")
111                         }
112
113                 }
114         }
115         print STDERR "Получены параметры ",join(" ",$cgi->param),"\n";
116         show_error($forum,"Некорректный вызов скрипта. Отсутствует параметр действия");
117 }       
118
119 sub dir2url {
120         my ($cgi,$dir) = @_;
121         my $prefix="";
122         my $pos=rindex $ENV{'PATH_TRANSLATED'},$ENV{'PATH_INFO'};
123         if ($pos <0 && $ENV{'PATH_INFO'}=~m!(/\~\w+)/!) {
124                 $prefix .=$1;
125                 $pos =
126                 rindex($ENV{'PATH_TRANSLATED'},substr($ENV{'PATH_INFO'},length($1)));
127         }
128         if ($pos <0) {
129                 show_error({},"Ошибка конфигурации форума. Не удается определить
130                 алгоритм преобразования директори в URL\n".
131                 "PATH_INFO=$ENV{PATH_INFO}\n".
132                 "PATH_TRANSLATER=$ENV{'PATH_TRANSLATED'}");
133         }       
134         my $root = substr($ENV{'PATH_TRANSLATED'},0,$pos);
135         if (substr($dir,0,length($root)) ne $root) {
136                 show_error({},"Ошибка конфигурации форума. Не удается преобразовать
137                 имя директории $dir в url\n".
138                 "PATH_INFO=$ENV{PATH_INFO}\n".
139                 "PATH_TRANSLATER=$ENV{'PATH_TRANSLATED'}");
140         }
141         return $prefix.substr($dir,length($root));
142 }
143 #
144 # Поиск файла .forum вверх по дереву от $ENV{PATH_TRANSLATED}  
145 # Значение PATH_TRANSLATED считаем безопасным - наш web-сервер нам не
146 # враг.
147 # Возвращает список имя,значение, имя, значение который прививается в
148 # хэш
149
150 sub get_forum_config {
151         my @path=split("/",$1) if $ENV{PATH_TRANSLATED}=~/^(\S+)$/;
152         while (@path>1) {
153                 if (-r (my $config=join("/",@path,".forum")) ) {
154                         open F,"<",$config;
155                         my %config;
156                         while (<F>) {
157                                 s/#.*$//; #Drop comments;
158                                 $config{$1}=$2 if /(\w+)\s*=\s*(\S.*)$/;
159                         }       
160                         close F;
161                         #
162                         # Переменная forumtop - это URL того места, где находится
163                         # файл .forum
164                          
165                         $config{"forumtop"} = dir2url($cgi,join("/",@path));
166                         # Если в конфиге отсутствует переменная templates, но
167                         # рядом с конфигом присутствует директория templates,
168                         # то шаблоны там.
169                         #
170                         if (! exists $config{"templates"} 
171                                 && -d (my $filename = join("/",@path,"templates"))) {
172                                         $config{"templates"} = $filename;
173                         }               
174                         $config{"templatesurl"} = dir2url($cgi,$config{"templates"})
175                                 unless exists $config{"templatesurl"};
176                         # 
177                         # То же самое - параметр userdir и директория users
178                         #
179                         if (! exists $config{"userdir"} 
180                                 && -d (my $filename = join("/",@path,"users"))) {
181                                         $config{"userdir"} = $filename;
182
183
184                         }       
185                         $config{"userurl"} = dir2url($cgi,$config{"userdir"});
186
187                         #
188                         # Если нет ссылки в конфиге на файл паролей или он не 
189                         # существует, выдаем ошибку. С офоромлением, так как шаблоны
190                         #  у нас уже есть
191                         if (!exists $config{"datadir"}) {
192                                 show_error(\%config,"В конфигурации форума не указана
193                                 директория данных "); 
194                                 exit;
195                         }
196                         if (!-d $config{"datadir"}) {
197                                 show_error(\%config,"В конфигурации форума указана несуществующая директория данных "); 
198                                 exit;
199                         }
200                         $config{"authperiod"}="+1M" if (! exists $config{"authperiod"}); 
201                         $config{"renewtime"} = "86000" if (!exists $config{"renewtime"});
202                         $config{"replies_per_page"} = 50 if (!exists $config{"replies_per_page"});
203                         return \%config;
204                 }
205                 pop @path;
206         }
207         #
208         # Выводим ошибку 404 без осмысленного оформления, так как данных форума
209         # мы не нашли
210         print "Status: 404\nContent-Type: text/html; charset=utf-8\n\n",
211         "<HTML><HEAD><TITLE>Форум не обнаружен</TITLE></HEAD><BODY>",
212         "<H!>Форум не найден</H!>",
213         "<p>Хвост URL, указанный при вызове скрипта  показывает не на
214         форум</p>",
215         # To make IE think this page is user friendly
216         "<!--",("X" x 512),"--></body></html>\n"; 
217         exit;
218 }
219 #
220 # Вывод сообщения об ошибке по шаблону форума
221 # Шаблон должен содержать элемент с классом error.
222 #
223 sub show_error {
224         my ($cfg,$msg) = @_;
225         if ( -r $cfg->{"templates"}."/error.html") {
226                 my $tree = HTML::TreeBuilder->new_from_file($cfg->{"templates"}."/error.html");
227                 my $node= $tree->find_by_attribute('class','error');
228                 my $body;
229                 if (!$node) {
230                         $body = $tree->find_by_tagname('body');
231                         $body->push_content($node = new
232                         HTML::Element('div','class'=>'error'));
233                 }
234                 $node->delete_content;
235                 $node->push_content($msg);
236                 print $cgi->header(-type=>'text/html',-charset=>'utf-8');
237                 print $tree->as_HTML("<>&");
238         } else {
239                 print $cgi->header(-type=>'text/html',-charset=>'utf-8');
240                 print "<html><head><title>Ошибка конфигурации форума</title></head>",
241                 "<body><h1>Ошибка конфигурации форума</h1><p>",
242                 $cgi->escapeHTML($msg),"</p>",
243                 "<p>При обработке этой ошибки не обнаружен шаблон сообщения об ошибке</p></body></html>";  
244         }
245         exit;
246 }       
247
248 sub gettemplate {
249         my ($forum, $template,$url) = @_;
250         my $filename=$forum->{"templates"}."/$template.html";
251         if (! -r $filename) {
252                 show_error($forum,"Нет шаблона $template");
253                 exit;
254         }
255         my $tree = HTML::TreeBuilder->new_from_file($filename);
256         fix_forum_links($forum,$tree,$url);
257         return $tree;
258 }       
259 #
260 # Вывод шаблона формы. В шаблоне должна присутстовать форма с  
261 # именем, совпадающим с именем form. Если в $cgi есть параметры, имена
262 # которых совпадают с именами полей этой формы, их значения
263 # подставляются
264 #
265 sub show_template {
266         my ($form,$cgi,$forum) = @_;
267         my $tree = gettemplate($forum,$form,$ENV{'PATH_INFO'});
268
269         # Находим форму с классом $form
270         my $f = $tree->look_down("_tag","form",
271                 "name",$form);
272         if (! defined $f) {
273                 # Если не нашли - ругаемся
274                 show_error($forum,"Шаблон для операции $form не содержит формы с
275                 именем $form");
276                 exit;
277         }
278         $cgi->delete('password');
279         if (!$cgi->param("returnto")) {
280                 $cgi->param("returnto", $cgi->referer||$cgi->url(-absolute=>1,-path_info=>1));
281
282         }       
283         if (!$cgi->param($form)) {
284                 $cgi->param($form,1);
285         }       
286         # 
287         # Если ранее была выставлена ошибка с помощью set_error, подставляем
288         # сообщение в элемент с классом error
289         #
290         if ($forum->{error_message}) {
291                 my $errormsg = $tree->look_down("class"=>"error");
292                 if ($errormsg) {
293                         $errormsg->delete_content();
294                         $errormsg->push_content($forum->{error_message});
295                 }
296         }       
297         if ($forum->{"authenticated"}) {
298                  
299                 # Подставляем информацию о текущем пользователе если в шаблоне
300                 # это предусмотрено 
301                 substitute_user_info($tree,$forum);
302                 $cgi->param("user",$forum->{"authenticated"}{"user"}) if (!defined $cgi->param("user"))
303         }
304         my %substituted;
305         ELEMENT:
306         for my $element ($f->find_by_tag_name("textarea","input","select")) {
307                 my $name = $element->attr("name");
308                 $substituted{$name} = 1;
309                 #print STDERR "substituting form element name $name tag ",$element->tag,
310             #           "value='",$cgi->param($name),"'\n";  
311                 if (defined  $cgi->param($name)) {
312                         if ($element->tag eq "input") {
313                                 next ELEMENT if grep ($element->attr("type") eq
314                                 $_,"button","submit","reset");  
315                                 if ($element->attr("type") eq "check") {
316                                         if (grep($element->attr("value") eq $_,$cgi->param($name))) {
317                                                 $element->attr("checked","");
318                                         } else {
319                                                 $element->attr("checked",undef);
320                                         }
321                                 
322                                 } elsif ($element->attr("type") eq
323                                 "radio") {
324                                         if ($element->attr("value") eq $cgi->param($name)) {
325                                                 $element->attr("checked","");
326                                         } else {
327                                                 $element->attr("checked",undef);
328                                         }
329                                 } else {        
330                                 $element->attr("value",$cgi->param($name));
331                                 }
332                         } elsif ($f->tag eq "textarea") {
333                                 $f->delete_content;
334                                 $f->push_content($cgi->param("name"));
335                         } elsif ($f->tag eq "select") {
336                                 for my $option ($f->find_by_tag_name("option")) {
337                                         if (grep($option->attr("value") eq $_, $cgi-param("name"))) {
338                                                 $option->attr("selected","");
339                                         } else {        
340                                                 $option->attr("selected",undef);
341                                         }       
342                                 }
343
344                         }
345                 }
346
347         }
348         $f->attr("type","POST");
349         for my $required ($form,"returnto") {
350                 if (!$substituted{$required}) {
351                         my $element = new HTML::Element('input',
352                                 'type' => 'hidden', 'name' => $required,
353                                 'value'=> $cgi->param($required));
354                         $f->push_content($element);
355                 }
356         }       
357                                 
358                 
359         print
360         $cgi->header(-type=>"text/html",-charset=>"utf-8",($forum->{cookies}?(-cookie=>$forum->{cookies}):())),
361         $tree->as_HTML("<>&");
362 }
363 #
364 # Поправляет ссылки на служебные файлы и скрипты форума
365 #
366 sub fix_forum_links {
367         my ($forum,$tree,$path_info) = @_;
368         $path_info=$ENV{'PATH_INFO'} if (!defined $path_info);
369         my $script_with_path = $ENV{SCRIPT_NAME}.$path_info;
370         ELEMENT:
371         for my $element ($tree->find_by_tag_name("form","img","link","script","a")) {
372                 my $attr;
373                 if ($element->tag eq "form")  {
374                         $attr = "action";
375                 } elsif ($element->tag eq "a"|| $element->tag eq "link") {
376                         $attr = "href";
377                 } else {
378                         $attr ="src";
379                 }
380                 
381                 # Обрабатываем наши специальные link rel=""
382                 if ($element->tag eq "link") {
383                         if ($element->attr("rel") eq "forum-user-list") {
384                                 $element->attr("href" => $cgi->url(-absolute=>1,
385                                         -path_info=>0,-query_string=>0).$forum->{userurl});
386                                 next ELEMENT;   
387                         } elsif ($element->attr("rel") eq "forum-script")  {
388                                 $element->attr("href" => $script_with_path);
389                                 next ELEMENT;
390                         }       
391                 }
392                 my $link = $element->attr($attr);
393                 # Абсолютная ссылка - оставляем как есть. 
394                 next ELEMENT if (! defined $link || $link=~/^\w+:/); 
395                 # Ссылка от корня сайта. 
396                 if (substr($link,0,1) eq "/") {
397                         # Если она не ведет на наш скрипт, не обрабатываем
398                         next ELEMENT if substr($link,0,length($ENV{SCRIPT_NAME}) ne
399                         $ENV{SCRIPT_NAME}) ;
400                         # Иначе пишем туда слово forum вместо реального имени
401                         # скрипта чтобы потом единообразно обработать
402                         $link =~ s/^[^\?]+/forum/;
403                 }
404                 if (!($link =~ s!^templates/!$forum->{templatesurl}/!) &&
405                     !($link =~ s!^users/!$forum->{usersurl}/!) &&
406                     !($link =~ s!^forum\b!$script_with_path!)) {
407                         $link = $forum->{"forumtop"}."/".$link 
408                 }       
409                 $element->attr($attr,$link);
410         }
411 }               
412 #
413 # Подставляет в заданное поддерево информацию о пользователе
414 #
415
416 sub substitute_user_info {
417
418 my ($tree,$forum) = @_;
419 my %userinfo = %{$forum->{"authenticated"}};
420
421 #
422 # Специально обрабатываем поля user (должна быть ссылка) и avatar  
423 # (должен быть img).
424 my @userlink = $tree->look_down("_tag"=>"a","class"=>"author");
425 if (@userlink) {
426         my $userpage;
427         if ($userinfo{"user"}=~/^http:/) {
428                 $userpage = $userinfo{"user"};
429         } else {
430                 $userpage =
431                 $cgi->url(-absolute=>1).$forum->{"userurl"}."/".$cgi->escape($userinfo{"user"});
432         }       
433         for my $element (@userlink) {
434                 $element->attr(href=>$userpage);
435                 $element->delete_content();
436                 $element->push_content($userinfo{"user"});
437         }
438 }       
439 delete $userinfo{"userpage"};
440 delete $userinfo{"user"};
441 my $avatar = $tree->look_down("_tag"=>"img","class"=>"avatar");
442 if ($avatar) {
443         $avatar->attr(src=>$userinfo{"avatar"});
444 }
445 delete $userinfo{"avatar"};
446
447 while (my ($field,$value)=each %userinfo) {
448         my $element = $tree->look_down("class","a".$field);
449         if ($element) {
450                 $element->delete_content();
451                 # 
452                 # FixME - allow HTML in author attributes
453                 $element->push_content($value);
454         }
455
456 }
457
458 }
459 #
460 # Авторизует зарегистрированного пользователя.
461 # 1. Проверяет куку если есть
462 #
463
464 sub authorize_user      {
465         ($cgi,$forum) = @_;
466         if (my $session=$cgi->cookie("slsession")) {
467         # Пользователь имеет куку
468                 my %sessbase;   
469                 dbmopen %sessbase,datafile($forum,"session"),0644;
470                         if ($sessbase{$session})  {
471                                 my ($user,$expires,$ip)=split(";", $sessbase{$session});
472                                 if (!defined $ip|| $ip eq $ENV{'REMOTE_ADDR'}) {
473                                         my %userbase;
474                                         dbmopen %userbase,datafile($forum,"passwd"),0644;
475                                         if ( $userbase{$user}) {
476                                                 my $userinfo = thaw($userbase{$user});
477                                                 delete $userinfo->{"passwd"};
478                                                 $userinfo->{"user"} = $user;
479                                                 if ($expires-time()< $forum->{"renewtime" }) {
480                                                         delete $sessbase{$session};
481                                                         newsession(\%sessbase,$forum,$user,$ip);
482                                                 }
483                                                 print STDERR "user $user restored session $session\n";
484                                                 $forum->{"authenticated"}=$userinfo;
485                                                 print STDERR "authorize_user:
486                                                 ",$forum->{authenticated}{user},
487                                                 $forum->{authenticated},"\n";
488                                         }       
489                                         dbmclose %userbase; 
490                                 }       
491                         }       
492                 dbmclose %sessbase;
493         }
494 }
495 #
496 # Возвращает путь к файлу в директории 
497 #
498 sub datafile {
499         my ($forum,$filename) = @_;
500         return $forum->{"datadir"}."/".$filename;
501 }       
502
503 #
504 # Создает новую сессию для пользователя и подготавливает куку которую
505 # сохраняет в хэше конфигурации форума
506
507 sub newsession {
508         my ($base,$forum,$user,$bindip) = @_;
509         if (!defined $base) {
510                 $base = {};
511                 dbmopen %$base,datafile($forum,"session"),0644;
512         }       
513         my $sessname;
514         my $t = time();
515         my ($u,$expires,$ip);
516         do {
517                 $sessname = sprintf("%08x",rand(0xffffffff));
518                 if ($base->{"sessname"}) {
519                         ($u,$expires,$ip) = split ";", $base->{$sessname};
520                         delete $base->{$sessname} if $expires < $t;
521                 }
522         } while ($base->{$sessname});
523         my $cookie = $cgi->cookie(-name=>"slsession",
524                 -expires => $forum->{"authperiod"},-value=> $sessname);
525         $base->{$sessname}=$user.";".str2time($cookie->expires()).
526                 ($ip?";$ENV{'REMOTE_ADDR'}":"");
527                 
528         $forum->{'cookies'}=[ $cookie,
529         $cgi->cookie(-name=>"sluser",-value=>$user,-expires =>
530         $forum->{authperiod})];                         
531 }
532 #
533 # Выполняет аутентикацию пользователя по логину и паролю и 
534 # создает для него сессию.
535 #
536 sub authenticate {
537         my ($cgi,$forum) = @_;  
538         if ($cgi->param("openidsite")) {
539                 my $openid_url = sprintf($cgi->param("openidsite"),$cgi->param("user"));
540                 openidstart($cgi,$forum,$openid_url);
541         }       
542         my %userbase;
543         dbmopen %userbase,datafile($forum,"passwd"),0644;
544         my $user = $cgi->param("user");
545         my $password = $cgi->param("password");
546         $cgi->delete("password");
547         if (! $userbase{$user}) {
548           set_error($forum,"Неверное имя пользователя или пароль");
549           return undef;
550         }   
551         my $userinfo = thaw($userbase{$user}) ;
552         dbmclose %userbase;
553         #while (my ($key,$val)=each %$userinfo) { print STDERR "$key => '$val'\n";}
554         if (crypt($password,$userinfo->{passwd}) eq $userinfo->{passwd}) {
555                 delete $userinfo->{"passwd"};
556                 $cgi->delete("password");
557                 $userinfo->{"user"} = $user;
558                 newsession(undef,$forum,$user);
559                 $forum->{"authenticated"} = $userinfo;          
560                 print STDERR "User $user authenticated successfully\n";
561                 return 1;
562         } else {
563                 set_error($forum,"Неверное имя пользователя или пароль");
564                 return undef;
565         }       
566 }
567 #
568 # Запоминает сообщение об ошибке для последующего вывода show_template
569 #
570 sub set_error {
571         my  ($forum,$message) = @_;
572         print STDERR "set_error: $message\n";
573         $forum->{error_message} = $message;
574 }       
575 #
576 # Выводит текущий шаблон с сообщением об ошибке
577 #
578 sub form_error {
579         my ($form_name,$cgi,$forum,$msg) = @_;
580         set_error($forum,$msg);
581         show_template($form_name,$cgi,$forum);
582         exit;
583 }       
584 #
585 # Выполняет редирект (возможно, с установкой куков) на страницу,
586 # указанную # третьем параметре функции или в параметре CGI-запроса
587 # returnto
588 # Если и то, и другое не определено, пытается сконструировать URL для
589 # возврата из PATH_INFO.
590 #
591
592 sub forum_redirect {
593         my ($cgi,$forum,$url) = @_;
594         if (!defined $url) {
595                 $url = $cgi->param("returnto");
596                 $url =
597                 $cgi->url(-base=>1).($cgi->path_info()||$forum->{forumtop}) if !$url ;
598         }
599         print $cgi->redirect(-url=>$url,
600                 ($forum->{cookies}?(-cookie=>$forum->{cookies}):()));
601         exit;   
602 }
603 #
604 # Обработка результатов заполнения формы регистрации.
605 #
606 #
607 sub register {
608         my ($formname,$cgi,$forum) = @_; 
609         #
610         # Возможные ошибки: 
611         # 1 Такой юзер уже есть
612         #
613         #  не заполнено поле user 
614         if (!$cgi->param("user")) {
615                 form_error($formname,$cgi,$forum, "Не заполнено имя пользователя");
616         }       
617         #  или поле password 
618         if (!$cgi->param("pass1"))  {
619                 form_error($formname,$cgi,$forum,"Не указан пароль");
620         }       
621         #  Копии пароля не совпали
622         if ($cgi->param("pass2") ne $cgi->param("pass1")) {
623                 form_error($formname,$cgi,$forum,"Ошибка при вводе пароля");
624         }               
625         my $user = $cgi->param("user");
626         # Не указаны поля, перечисленные в скрытом поле required 
627         if ($cgi->param("required")) { 
628                 foreach my $field (split(/\s*,\s*/,$cgi->param('required'))) {
629                         if (!$cgi->param($field)) {
630                                 form_error($formname,$cgi,$forum,"Не заполнено обязательное поле $field");
631                         }
632                 }       
633         }
634         my %userbase;
635         dbmopen %userbase,datafile($forum,"passwd"),0644 
636                 or form_error($formname,$cgi,$forum,"Ошибка открытия файла паролей $!");
637         if ($userbase{$cgi->param("user")}) {
638                 dbmclose %userbase;
639                 form_error($formname,$cgi,$forum,"Имя пользователя '".$cgi->param("user"). "' уже занято");
640         }
641         if ($cgi->param("email") && !  Email::Valid->address($cgi->param("email"))) {
642                 form_error($formname,$cgi,$forum,"Некорректный E-Mail адрес");
643         }
644         my $saltstring = 'ABCDEFGHIJKLMNOPQRSTUVWXUZabcdefghijklmnopqrstuvwxuz0123456789./';
645         my $salt = substr($saltstring,int(rand(64)),1).
646                                 substr($saltstring,int(rand(64)),1);
647         my $password=crypt($cgi->param("pass1"),$salt);                 
648         my $userinfo = {passwd=>$password};
649         # Удаляем лишние поля
650         $cgi->delete("required");
651         $cgi->delete("register");
652         $cgi->delete("user");
653         $cgi->delete("pass1");
654         $cgi->delete("pass2");
655         foreach my $field (split(/\s*,\s*/,$cgi->param('ignore'))) {
656                 if (!$cgi->param($field)) {
657                         $cgi->delete($field);
658                 }
659         }       
660         my $returnto = $cgi->param("returnto");
661         $cgi->delete("returnto");
662         # Если есть аватар в файле, то сохраняем этот файл и формируем URL
663         # на него.
664         if ($cgi->param("avatarfile" )) {
665                 my $f = $cgi->upload("avatarfile");
666                 binmode $f,":bytes";
667                 my $out;
668                 my $filename = $1 if $cgi->param("avatarfile")=~/([^\/\\]+)$/;
669                 open $out,">",$forum->{"userdir"}."/".$filename;
670                 binmode $out,":bytes";
671                 my $buffer;
672                 while (my $bytes = read($f,$buffer,4096)) {
673                         print $out $buffer;
674                 }       
675                 close $f;
676                 close $out;
677                 $userinfo->{'avatar'}= $forum->{"userurl"}."/".$filename;
678                 $cgi->delete("avatar");
679                 $cgi->delete("avatarfile");
680         }
681         
682         foreach my $param       ($cgi->param) {
683                 $userinfo->{$param} = $cgi->param($param);
684         }
685         $userinfo->{registered} = time;
686         if (exists $forum->{default_status}) {
687                 $userinfo->{status} = $forum->{default_status};
688         }
689         print STDERR "stilllife forum: registering user $user\n";
690         $userbase{$user} = freeze($userinfo);
691         dbmclose %userbase;
692         newsession(undef,$forum,$user);
693         forum_redirect($cgi,$forum,$returnto) 
694 }       
695 #
696 # Обработчик формы логина. Сводится к вызову функции authenticate,
697 # поскольку мы поддерживаем логин одновременный с отправкой реплики. 
698 #
699 sub login {
700         my ($form,$cgi,$forum)=@_;
701         if (authenticate($cgi,$forum)) {
702                 forum_redirect($cgi,$forum);
703         } else {
704                 show_template(@_);
705         }       
706 }       
707 #
708 # Обработчик формы logout. В отличие от большинства обработчиков форм,
709 # поддерживает обработку методом GET
710 #
711 sub logout {
712         my ($form,$cgi,$forum) = @_;
713         $forum->{cookies}=[ $cgi->cookie(-name=>"sluser", -value=>"0",
714         -expires=>"-1m"),$cgi->cookie(-name=>"slsession", -value=>"0",
715                         -expires => "-1m")];
716         if (defined (my $session_id = $cgi->cookie("slsession"))) {
717                 my %sessiondb;
718                 dbmopen %sessiondb,datafile($forum,"session"),0644;
719                 delete $sessiondb{$session_id};
720                 dbmclose %sessiondb;
721         }
722         forum_redirect($cgi,$forum);
723 }       
724 sub allow_operation {
725         my ($operation,$cgi,$forum) = @_;
726         return 1 if (!exists($permissions{$operation})); 
727         if (!$forum->{authenticated}) {
728                 return 1 if ($permissions{$operation} eq "login");
729                 return 0;
730         }       
731         my $user = $forum->{authenticated}{user} ;
732         my $accesslevel=getrights($cgi,$forum);
733         # Если permissions{$operation} равны author, нам нужно извлечь
734         # текст из соответствующего файла и положить его в
735         # cgi->param("text"); Заодно определим и автора
736         my ($itemauthor,$itemtext)=get_message_by_id($cgi->param("id")) if
737                 $permissions{$operation} eq "author";
738         
739         return 1 if ($accesslevel eq "admin");
740         return 0 if ($permissions{$operation} eq "admin");      
741         return 1 if ($accesslevel eq "moderator");
742         return 0 if $accesslevel eq "banned";   
743         return 0 if $permissions{$operation} eq "author" && $user ne $itemauthor;
744         return 1;
745 }
746
747 sub reply {
748         my ($form,$cgi,$forum) = @_;
749         if (! exists $forum->{authenticated} ) {
750                 form_error($form,$cgi,$forum,"Вы не зарегистрировались") if (!authenticate($cgi,$forum)); 
751         }
752         #
753         # Находим файл дискуссии, в который надо поместить реплику
754         #
755         
756         #
757         # Сохраняем приаттаченную картинку, если есть.
758         #
759         
760         # Генерируем идентификатор записи.
761         #
762         
763         #
764         # Преобразуем текст записи в html и чистим его
765         #
766         my $txtree = undef;
767         if ($cgi->param("format") eq "bbcode") {
768
769         } elsif ($cgi->param("format") eq "text") {
770                 my $text = $cgi->escapeHTML($cgi->param("text"));
771                 $text=~s/\r?\n\r?\n/<p>/;
772                 $text=~s/\n/<br>/;
773                 $txtree =
774                 HTML::TreeBuilder->new_from_content("<div>$text</div>");
775         } else { # Default - html
776                 $txtree =
777                 HTML::TreeBuilder->new_from_content("<div>".$cgi->param("text")."</div>");      
778                 for my $badtag
779                 ("script","style","head","html","object","embed","iframe","frameset","frame",
780                 ($forum->{forbid_tags}?split(/\s*,\s*/,$forum->{forbid_tags}):())) {
781                         for my $element ($txtree->find_by_tag_name($badtag)) {
782                                 $element->delete() if defined $element;
783                         }       
784                 }       
785         }       
786 }       
787 #
788 # читает файлы прав доступа в дереве форума, и возвращает
789 # статус текущего пользователя (undef - аноним, banned, normal,
790 # moderator или admin
791
792 sub getrights {
793         my ($cgi,$forum) = @_;
794         if (!$forum->{authenticated}) {
795                 return undef;
796         }       
797         my $user = $forum->{authenticated}{user};
798         my $dir = $ENV{'PATH_TRANSLATED'};
799         $dir =~s/\/[^\/]+$// if (!-d $dir);
800         my $f;
801         my $user_status = "normal";
802         LEVEL:
803         while (length($dir)) {  
804                 if (-f "$dir/perms.txt") {
805                         open $f,"<","$dir/perms.txt";
806                         my $status = undef;
807                         while (<$f>) {
808                                 if (/^\[\s*(admins|moderators|banned)\s*\]/) {
809                                         $status = $1;
810                                 } else {
811                                         chomp;
812                                         if  ($user eq $_ && defined $status) {
813                                                 if ($status eq "banned") {
814                                                         return $status;
815                                                 } 
816                                                 if ($status eq "admins" ) {
817                                                         return "admin";
818                                                 }
819                                                 $user_status = "moderator";
820                                         }
821                                 }       
822                         }
823                         close $f;
824                         last LEVEL if  -f "$dir/.forum";
825                         # Strip last path component.
826                         $dir =~s/\/[^\/]+$// 
827                 }       
828         }               
829         return $user_status;
830
831 }               
832
833
834 #
835 # Залочить файл и получить его распрасенное представление.
836 # Возвращает пару ($tree,$lockfd)
837
838 sub gettree {
839         my $filename = shift;
840         my $f;
841         open $f,"<",$filename or return undef;
842         flock $f, LOCK_EX;
843         my $tree = HTML::TreeBuider->new_from_file($f);
844         return ($tree,$f);
845 }       
846 #
847 # Сохранить дерево и закрыть lockfd.
848 #
849 #
850
851 sub savetree {
852         my ($filename,$tree,$lockfd) = shift;
853         my $f;
854         open $f,">",$filename . ".new" or return undef;
855         print $f $tree->as_HTML("<>&");
856         close $f;
857         # FIXME - только для POSIX.
858         unlink $filename;
859         rename $filename.".new",$filename;
860         close $lockfd if defined($lockfd);
861 }       
862
863
864
865 #
866 # Получает уникальный числовой идентификатор.
867
868 sub get_uid {
869         my $forum = shift;
870         my $f;
871         open $f,"<+",datafile($forum,"sequence");
872         flock $f,LOCK_EX;
873         my $id=<$f> || "0";
874         $id++;
875         seek $f,0,0;
876         printf $f "%8s\n",$id;
877         close $f;
878         return sprintf ("%08s",$id);
879 }
880 #
881 # ----------------- OpenID registration -----------------------------
882
883 sub create_openid_consumer {
884         my ($cgi,$forum) = @_;
885         return Net::OpenID::Consumer ->new(
886                 ua => LWP::UserAgent->new(),
887                 args => $cgi,
888                 consumer_secret=>"X9RWPo0rBE7yLja6VB3d",
889                 required_root => $cgi->url(-base=>1));
890 }               
891
892 # openidstart - вызывается когда обнаружено что текущее имя
893 # пользователя, пытающегося аутентифицироваться, содержит http://
894 #  
895 #
896
897 sub openidstart {
898         my ($cgi,$forum,$openidurl) = @_;
899         #
900         # Fix duplicated http:// which can be produced by our sprintf based
901         # login system
902         #
903         $openidurl=~s!^http://http://!http://!;
904         my $csr = create_openid_consumer($cgi,$forum);
905         my $claimed_identity=$csr->claimed_identity($openidurl);
906         if (!defined $claimed_identity) {
907                 show_error($forum,"Указанная URL $openidurl не является OpenId");            
908                 exit;
909         }
910         $cgi->param("openidvfy",1);
911         $cgi->delete("user");
912         $cgi->delete("openidsite");
913         $cgi->delete("password");
914         my $check_url = $claimed_identity->check_url(
915                 return_to=> $cgi->url(-full=>1,-path_info=>1,-query=>1),
916                 trust_root=> $cgi->url(-base=>1));
917         print $cgi->redirect(-location=>$check_url);
918         exit;
919 }       
920 #
921 # Вызывается при редиректе от openid producer-а. Проверяет, что
922 # удаленный сервер подтвердил openid и вызывает операцию для которой
923 # (либо возврат на исходную страницу при операции login, либо постинг
924 # реплики) 
925 #
926 sub openid_verify {
927         my ($cgi,$forum) = @_;
928         my $csr  = create_openid_consumer($cgi,$forum);
929         if (my $setup_url = $csr->user_setup_url) {
930                 print $cgi->redirect(-location=>$setup_url);
931                 exit;
932         } elsif ($csr->user_cancel) {
933                 show_error($forum,"Ваш openid-сервер отказался подтвержать вашу
934                 идентичность");
935                 exit;
936         } elsif (my $vident = $csr->verified_identity) {
937                 #Успешная аутентификация.         
938                 #Создаем сессию
939                 my $user = $vident->url; 
940                 # Remove trailing slash from URL if any
941                 $user=~s/\/$//;
942                 my %userbase;
943                 dbmopen %userbase,datafile($forum,"passwd"),0664;
944                 if (!$userbase{$user}) {
945                         $userbase{$user} = $forum->{authenticated}={};
946                 } else {
947                         $forum->{authenticated} = thaw ($userbase{$user});
948                 }
949                 dbmclose %userbase;
950                 $forum->{"authenticated"}{"user"} = $user;
951                 newsession(undef,$forum,$user);
952                 # Если указан параметр reply, вызываем обработку реплики
953                 if ($cgi->param("reply")) {     
954                         reply("reply",$cgi,$forum);
955                         exit;
956                 }       
957                 #Иначе, возвращаемся на исходную страницу
958                 forum_redirect($cgi,$forum,undef);
959         }       else {
960                 show_error($forum,"Ошибка OpenId аутентификации");
961                 exit;
962         }       
963 }