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