]> www.wagner.pp.ru Git - oss/stilllife.git/blob - forum/forum
*** empty log message ***
[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                                 my $user_cookie = $cgi->cookie("sluser");
473                                 if ($user_cookie ne $user && $user_cookie ne
474                                 "http://".$user) {
475                                         clear_user_cookies($cgi,$forum);
476                                         show_error($forum,"Некорректная пользовательская сессия");
477                                         exit;
478                                 }       
479                                 if (!defined $ip|| $ip eq $ENV{'REMOTE_ADDR'}) {
480                                         my %userbase;
481                                         dbmopen %userbase,datafile($forum,"passwd"),0644;
482                                         if ( $userbase{$user}) {
483                                                 my $userinfo = thaw($userbase{$user});
484                                                 delete $userinfo->{"passwd"};
485                                                 $userinfo->{"user"} = $user;
486                                                 if ($expires-time()< $forum->{"renewtime" }) {
487                                                         delete $sessbase{$session};
488                                                         newsession(\%sessbase,$forum,$user,$ip);
489                                                 }
490                                                 print STDERR "user $user restored session $session\n";
491                                                 $forum->{"authenticated"}=$userinfo;
492                                                 print STDERR "authorize_user:
493                                                 ",$forum->{authenticated}{user},
494                                                 $forum->{authenticated},"\n";
495                                         }       
496                                         dbmclose %userbase; 
497                                 }       
498                         } else {
499                                 clear_user_cookies($cgi,$forum);
500                                 show_error($forum,"Некорректная пользовательская сессия");
501                                 exit;
502                         }
503                 dbmclose %sessbase;
504         }
505 }
506 #
507 # Возвращает путь к файлу в директории 
508 #
509 sub datafile {
510         my ($forum,$filename) = @_;
511         return $forum->{"datadir"}."/".$filename;
512 }       
513
514 #
515 # Создает новую сессию для пользователя и подготавливает куку которую
516 # сохраняет в хэше конфигурации форума
517
518 sub newsession {
519         my ($base,$forum,$user,$bindip) = @_;
520         if (!defined $base) {
521                 $base = {};
522                 dbmopen %$base,datafile($forum,"session"),0644;
523         }       
524         my $sessname;
525         my $t = time();
526         my ($u,$expires,$ip);
527         do {
528                 $sessname = sprintf("%08x",rand(0xffffffff));
529                 if ($base->{"sessname"}) {
530                         ($u,$expires,$ip) = split ";", $base->{$sessname};
531                         delete $base->{$sessname} if $expires < $t;
532                 }
533         } while ($base->{$sessname});
534         my $cookie = $cgi->cookie(-name=>"slsession",
535                 -expires => $forum->{"authperiod"},-value=> $sessname);
536         my $username = $user;
537         $username =~ s/^http:\/\///; #Remoove http:// from OpenID user names 
538         $base->{$sessname}=$username.";".str2time($cookie->expires()).
539                 ($ip?";$ENV{'REMOTE_ADDR'}":"");
540                 
541         $forum->{'cookies'}=[ $cookie,
542         $cgi->cookie(-name=>"sluser",-value=>$user,-expires =>
543         $forum->{authperiod})];                         
544 }
545 #
546 # Выполняет аутентикацию пользователя по логину и паролю и 
547 # создает для него сессию.
548 #
549 sub authenticate {
550         my ($cgi,$forum) = @_;  
551         if ($cgi->param("openidsite")) {
552                 my $openid_url = sprintf($cgi->param("openidsite"),$cgi->param("user"));
553                 openidstart($cgi,$forum,$openid_url);
554         }       
555         my %userbase;
556         dbmopen %userbase,datafile($forum,"passwd"),0644;
557         my $user = $cgi->param("user");
558         my $password = $cgi->param("password");
559         $cgi->delete("password");
560         if (! $userbase{$user}) {
561           set_error($forum,"Неверное имя пользователя или пароль");
562           return undef;
563         }   
564         my $userinfo = thaw($userbase{$user}) ;
565         dbmclose %userbase;
566         #while (my ($key,$val)=each %$userinfo) { print STDERR "$key => '$val'\n";}
567         if (crypt($password,$userinfo->{passwd}) eq $userinfo->{passwd}) {
568                 delete $userinfo->{"passwd"};
569                 $cgi->delete("password");
570                 $userinfo->{"user"} = $user;
571                 newsession(undef,$forum,$user);
572                 $forum->{"authenticated"} = $userinfo;          
573                 print STDERR "User $user authenticated successfully\n";
574                 return 1;
575         } else {
576                 set_error($forum,"Неверное имя пользователя или пароль");
577                 return undef;
578         }       
579 }
580 #
581 # Запоминает сообщение об ошибке для последующего вывода show_template
582 #
583 sub set_error {
584         my  ($forum,$message) = @_;
585         print STDERR "set_error: $message\n";
586         $forum->{error_message} = $message;
587 }       
588 #
589 # Выводит текущий шаблон с сообщением об ошибке
590 #
591 sub form_error {
592         my ($form_name,$cgi,$forum,$msg) = @_;
593         set_error($forum,$msg);
594         show_template($form_name,$cgi,$forum);
595         exit;
596 }       
597 #
598 # Выполняет редирект (возможно, с установкой куков) на страницу,
599 # указанную # третьем параметре функции или в параметре CGI-запроса
600 # returnto
601 # Если и то, и другое не определено, пытается сконструировать URL для
602 # возврата из PATH_INFO.
603 #
604
605 sub forum_redirect {
606         my ($cgi,$forum,$url) = @_;
607         if (!defined $url) {
608                 $url = $cgi->param("returnto");
609                 $url =
610                 $cgi->url(-base=>1).($cgi->path_info()||$forum->{forumtop}) if !$url ;
611         }
612         print $cgi->redirect(-url=>$url,
613                 ($forum->{cookies}?(-cookie=>$forum->{cookies}):()));
614         exit;   
615 }
616 #
617 # Обработка результатов заполнения формы регистрации.
618 #
619 #
620 sub register {
621         my ($formname,$cgi,$forum) = @_; 
622         #
623         # Возможные ошибки: 
624         # 1 Такой юзер уже есть
625         #
626         #  не заполнено поле user 
627         if (!$cgi->param("user")) {
628                 form_error($formname,$cgi,$forum, "Не заполнено имя пользователя");
629         }       
630         #  или поле password 
631         if (!$cgi->param("pass1"))  {
632                 form_error($formname,$cgi,$forum,"Не указан пароль");
633         }       
634         #  Копии пароля не совпали
635         if ($cgi->param("pass2") ne $cgi->param("pass1")) {
636                 form_error($formname,$cgi,$forum,"Ошибка при вводе пароля");
637         }               
638         my $user = $cgi->param("user");
639         # Не указаны поля, перечисленные в скрытом поле required 
640         if ($cgi->param("required")) { 
641                 foreach my $field (split(/\s*,\s*/,$cgi->param('required'))) {
642                         if (!$cgi->param($field)) {
643                                 form_error($formname,$cgi,$forum,"Не заполнено обязательное поле $field");
644                         }
645                 }       
646         }
647         my %userbase;
648         dbmopen %userbase,datafile($forum,"passwd"),0644 
649                 or form_error($formname,$cgi,$forum,"Ошибка открытия файла паролей $!");
650         if ($userbase{$cgi->param("user")}) {
651                 dbmclose %userbase;
652                 form_error($formname,$cgi,$forum,"Имя пользователя '".$cgi->param("user"). "' уже занято");
653         }
654         if ($cgi->param("email") && !  Email::Valid->address($cgi->param("email"))) {
655                 form_error($formname,$cgi,$forum,"Некорректный E-Mail адрес");
656         }
657         my $saltstring = 'ABCDEFGHIJKLMNOPQRSTUVWXUZabcdefghijklmnopqrstuvwxuz0123456789./';
658         my $salt = substr($saltstring,int(rand(64)),1).
659                                 substr($saltstring,int(rand(64)),1);
660         my $password=crypt($cgi->param("pass1"),$salt);                 
661         my $userinfo = {passwd=>$password};
662         # Удаляем лишние поля
663         $cgi->delete("required");
664         $cgi->delete("register");
665         $cgi->delete("user");
666         $cgi->delete("pass1");
667         $cgi->delete("pass2");
668         foreach my $field (split(/\s*,\s*/,$cgi->param('ignore'))) {
669                 if (!$cgi->param($field)) {
670                         $cgi->delete($field);
671                 }
672         }       
673         my $returnto = $cgi->param("returnto");
674         $cgi->delete("returnto");
675         # Если есть аватар в файле, то сохраняем этот файл и формируем URL
676         # на него.
677         if ($cgi->param("avatarfile" )) {
678                 my $f = $cgi->upload("avatarfile");
679                 binmode $f,":bytes";
680                 my $out;
681                 my $filename = $1 if $cgi->param("avatarfile")=~/([^\/\\]+)$/;
682                 open $out,">",$forum->{"userdir"}."/".$filename;
683                 binmode $out,":bytes";
684                 my $buffer;
685                 while (my $bytes = read($f,$buffer,4096)) {
686                         print $out $buffer;
687                 }       
688                 close $f;
689                 close $out;
690                 $userinfo->{'avatar'}= $forum->{"userurl"}."/".$filename;
691                 $cgi->delete("avatar");
692                 $cgi->delete("avatarfile");
693         }
694         
695         foreach my $param       ($cgi->param) {
696                 $userinfo->{$param} = $cgi->param($param);
697         }
698         $userinfo->{registered} = time;
699         if (exists $forum->{default_status}) {
700                 $userinfo->{status} = $forum->{default_status};
701         }
702         print STDERR "stilllife forum: registering user $user\n";
703         $userbase{$user} = freeze($userinfo);
704         dbmclose %userbase;
705         newsession(undef,$forum,$user);
706         forum_redirect($cgi,$forum,$returnto) 
707 }       
708 #
709 # Обработчик формы логина. Сводится к вызову функции authenticate,
710 # поскольку мы поддерживаем логин одновременный с отправкой реплики. 
711 #
712 sub login {
713         my ($form,$cgi,$forum)=@_;
714         if (authenticate($cgi,$forum)) {
715                 forum_redirect($cgi,$forum);
716         } else {
717                 show_template(@_);
718         }       
719 }       
720 sub clear_user_cookies {
721         my ($cgi,$forum) = @_;
722         $forum->{cookies}=[ $cgi->cookie(-name=>"sluser", -value=>"0",
723         -expires=>"-1m"),$cgi->cookie(-name=>"slsession", -value=>"0",
724                         -expires => "-1m")];
725 }                       
726 #
727 # Обработчик формы logout. В отличие от большинства обработчиков форм,
728 # поддерживает обработку методом GET
729 #
730 sub logout {
731         my ($form,$cgi,$forum) = @_;
732         clear_user_cookies($cgi,$forum);
733         if (defined (my $session_id = $cgi->cookie("slsession"))) {
734                 my %sessiondb;
735                 dbmopen %sessiondb,datafile($forum,"session"),0644;
736                 delete $sessiondb{$session_id};
737                 dbmclose %sessiondb;
738         }
739         forum_redirect($cgi,$forum);
740 }       
741 sub allow_operation {
742         my ($operation,$cgi,$forum) = @_;
743         return 1 if (!exists($permissions{$operation})); 
744         if (!$forum->{authenticated}) {
745                 return 1 if ($permissions{$operation} eq "login");
746                 return 0;
747         }       
748         my $user = $forum->{authenticated}{user} ;
749         my $accesslevel=getrights($cgi,$forum);
750         # Если permissions{$operation} равны author, нам нужно извлечь
751         # текст из соответствующего файла и положить его в
752         # cgi->param("text"); Заодно определим и автора
753         my ($itemauthor,$itemtext)=get_message_by_id($cgi->param("id")) if
754                 $permissions{$operation} eq "author";
755         
756         return 1 if ($accesslevel eq "admin");
757         return 0 if ($permissions{$operation} eq "admin");      
758         return 1 if ($accesslevel eq "moderator");
759         return 0 if $accesslevel eq "banned";   
760         return 0 if $permissions{$operation} eq "author" && $user ne $itemauthor;
761         return 1;
762 }
763
764 sub reply {
765         my ($form,$cgi,$forum) = @_;
766         if (! exists $forum->{authenticated} ) {
767                 form_error($form,$cgi,$forum,"Вы не зарегистрировались") if (!authenticate($cgi,$forum)); 
768         }
769         #
770         # Находим файл дискуссии, в который надо поместить реплику
771         #
772         
773         #
774         # Сохраняем приаттаченную картинку, если есть.
775         #
776         
777         # Генерируем идентификатор записи.
778         #
779         
780         #
781         # Преобразуем текст записи в html и чистим его
782         #
783         my $txtree = undef;
784         if ($cgi->param("format") eq "bbcode") {
785
786         } elsif ($cgi->param("format") eq "text") {
787                 my $text = $cgi->escapeHTML($cgi->param("text"));
788                 $text=~s/\r?\n\r?\n/<p>/;
789                 $text=~s/\n/<br>/;
790                 $txtree =
791                 HTML::TreeBuilder->new_from_content("<div>$text</div>");
792         } else { # Default - html
793                 $txtree =
794                 HTML::TreeBuilder->new_from_content("<div>".$cgi->param("text")."</div>");      
795                 for my $badtag
796                 ("script","style","head","html","object","embed","iframe","frameset","frame",
797                 ($forum->{forbid_tags}?split(/\s*,\s*/,$forum->{forbid_tags}):())) {
798                         for my $element ($txtree->find_by_tag_name($badtag)) {
799                                 $element->delete() if defined $element;
800                         }       
801                 }       
802         }       
803 }       
804 #
805 # читает файлы прав доступа в дереве форума, и возвращает
806 # статус текущего пользователя (undef - аноним, banned, normal,
807 # moderator или admin
808
809 sub getrights {
810         my ($cgi,$forum) = @_;
811         if (!$forum->{authenticated}) {
812                 return undef;
813         }       
814         my $user = $forum->{authenticated}{user};
815         my $dir = $ENV{'PATH_TRANSLATED'};
816         $dir =~s/\/[^\/]+$// if (!-d $dir);
817         my $f;
818         my $user_status = "normal";
819         LEVEL:
820         while (length($dir)) {  
821                 if (-f "$dir/perms.txt") {
822                         open $f,"<","$dir/perms.txt";
823                         my $status = undef;
824                         while (<$f>) {
825                                 if (/^\[\s*(admins|moderators|banned)\s*\]/) {
826                                         $status = $1;
827                                 } else {
828                                         chomp;
829                                         if  ($user eq $_ && defined $status) {
830                                                 if ($status eq "banned") {
831                                                         return $status;
832                                                 } 
833                                                 if ($status eq "admins" ) {
834                                                         return "admin";
835                                                 }
836                                                 $user_status = "moderator";
837                                         }
838                                 }       
839                         }
840                         close $f;
841                         last LEVEL if  -f "$dir/.forum";
842                         # Strip last path component.
843                         $dir =~s/\/[^\/]+$// 
844                 }       
845         }               
846         return $user_status;
847
848 }               
849
850
851 #
852 # Залочить файл и получить его распрасенное представление.
853 # Возвращает пару ($tree,$lockfd)
854
855 sub gettree {
856         my $filename = shift;
857         my $f;
858         open $f,"<",$filename or return undef;
859         flock $f, LOCK_EX;
860         my $tree = HTML::TreeBuider->new_from_file($f);
861         return ($tree,$f);
862 }       
863 #
864 # Сохранить дерево и закрыть lockfd.
865 #
866 #
867
868 sub savetree {
869         my ($filename,$tree,$lockfd) = shift;
870         my $f;
871         open $f,">",$filename . ".new" or return undef;
872         print $f $tree->as_HTML("<>&");
873         close $f;
874         # FIXME - только для POSIX.
875         unlink $filename;
876         rename $filename.".new",$filename;
877         close $lockfd if defined($lockfd);
878 }       
879
880
881
882 #
883 # Получает уникальный числовой идентификатор.
884
885 sub get_uid {
886         my $forum = shift;
887         my $f;
888         open $f,"<+",datafile($forum,"sequence");
889         flock $f,LOCK_EX;
890         my $id=<$f> || "0";
891         $id++;
892         seek $f,0,0;
893         printf $f "%8s\n",$id;
894         close $f;
895         return sprintf ("%08s",$id);
896 }
897 #
898 # ----------------- OpenID registration -----------------------------
899
900 sub create_openid_consumer {
901         my ($cgi,$forum) = @_;
902         return Net::OpenID::Consumer ->new(
903                 ua => LWP::UserAgent->new(),
904                 args => $cgi,
905                 consumer_secret=>"X9RWPo0rBE7yLja6VB3d",
906                 required_root => $cgi->url(-base=>1));
907 }               
908
909 # openidstart - вызывается когда обнаружено что текущее имя
910 # пользователя, пытающегося аутентифицироваться, содержит http://
911 #  
912 #
913
914 sub openidstart {
915         my ($cgi,$forum,$openidurl) = @_;
916         #
917         # Fix duplicated http:// which can be produced by our sprintf based
918         # login system
919         #
920         $openidurl=~s!^http://http://!http://!;
921         my $csr = create_openid_consumer($cgi,$forum);
922         my $claimed_identity=$csr->claimed_identity($openidurl);
923         if (!defined $claimed_identity) {
924                 show_error($forum,"Указанная URL $openidurl не является OpenId");            
925                 exit;
926         }
927         $cgi->param("openidvfy",1);
928         $cgi->delete("user");
929         $cgi->delete("openidsite");
930         $cgi->delete("password");
931         my $check_url = $claimed_identity->check_url(
932                 return_to=> $cgi->url(-full=>1,-path_info=>1,-query=>1),
933                 trust_root=> $cgi->url(-base=>1));
934         print $cgi->redirect(-location=>$check_url);
935         exit;
936 }       
937 #
938 # Вызывается при редиректе от openid producer-а. Проверяет, что
939 # удаленный сервер подтвердил openid и вызывает операцию для которой
940 # (либо возврат на исходную страницу при операции login, либо постинг
941 # реплики) 
942 #
943 sub openid_verify {
944         my ($cgi,$forum) = @_;
945         my $csr  = create_openid_consumer($cgi,$forum);
946         if (my $setup_url = $csr->user_setup_url) {
947                 print $cgi->redirect(-location=>$setup_url);
948                 exit;
949         } elsif ($csr->user_cancel) {
950                 show_error($forum,"Ваш openid-сервер отказался подтвержать вашу
951                 идентичность");
952                 exit;
953         } elsif (my $vident = $csr->verified_identity) {
954                 #Успешная аутентификация.         
955                 #Создаем сессию
956                 my $user = $vident->url; 
957                 # Remove trailing slash from URL if any
958                 $user=~s/\/$//;
959                 my %userbase;
960                 dbmopen %userbase,datafile($forum,"passwd"),0664;
961                 my $username = $user; 
962                 $username =~ s/^http:\/\///;
963                 if (!$userbase{$username}) {
964                         $userbase{$username} = $forum->{authenticated}={"openiduser"=>1};
965                 } else {
966                         $forum->{authenticated} = thaw ($userbase{$username});
967                 }
968                 dbmclose %userbase;
969                 $forum->{"authenticated"}{"user"} = $username;
970                 newsession(undef,$forum,$user);
971                 # Если указан параметр reply, вызываем обработку реплики
972                 if ($cgi->param("reply")) {     
973                         reply("reply",$cgi,$forum);
974                         exit;
975                 }       
976                 #Иначе, возвращаемся на исходную страницу
977                 forum_redirect($cgi,$forum,undef);
978         }       else {
979                 show_error($forum,"Ошибка OpenId аутентификации");
980                 exit;
981         }       
982 }