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