]> www.wagner.pp.ru Git - oss/stilllife.git/blob - forum/forum
Fixed HTML escaping when _format set to text
[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 Image::Size;
23 use HTML::BBReverse;
24 use POSIX;
25 use LWP::UserAgent;
26 use Net::OpenID::Consumer;
27 #
28 # Набор поддерживаемых действий. Хэш вида 
29 # "имя поля в запросе" =>  "функция обработчик"
30 #
31 my %actions = (
32         reply => \&reply,
33         edit => \&edit_comment,
34         delete => \&delete_item,
35         move => \&move_comment,
36         newtopic=> \&new_topic,
37         newforum=> \&new_forum,
38         login => \&login,
39         register=>\&register,
40         profile=>\&profile,
41         setrights=>\&set_rights,
42         openidlogin=>\&openid_login,
43         openidvfy =>\&openid_verify
44 );      
45 #
46 #  Уровень прав, которые необходимо иметь пользователю для совершения
47 #  определенного действия
48 #  иерархия вида undef < banned < normal < author < moderator <admin
49 #  Если операция не упомянута в данном массив, то значит можно всем, в
50 #  том числе  и анониму.
51 # Слово login означает, что вообще-то это normal, но пользователь может
52 # логиниться непосредственно в процессе выполнения операции.
53 my %permissions = (
54         reply => "login",
55         edit => "author",
56         delete => "author",
57         newtopic => "normal",
58         move => "moderator",
59         newforum => "moderator",
60         profile => "normal",
61         setrights => "admin",
62 );      
63 our $path_translated; # Untainted value of PATH_TRANSLATED env var
64 my $cgi = new CGI;
65 print STDERR "--------------------\n";
66 my $forum=get_forum_config();
67
68 authorize_user($cgi,$forum);
69 if ($cgi->request_method ne "POST") {
70 # Запрос к скрипту методом GET. Надо показать форму, если только это не
71 # редирект от OpenId-сервера 
72         if ($cgi->param('openidvfy')) { 
73                 openid_verify($cgi,$forum);
74         } elsif ($cgi->param("logout")) {
75                 logout('logout',$cgi,$forum);
76         } elsif ($cgi->param("profile")) {
77                 show_profile("profile",$cgi,$forum);
78         } elsif ($cgi->param('delete')) {
79                 delete_item("delete",$cgi,$forum);
80         } else {
81                 for my $param ($cgi->param) {
82 # Среди параметров, указанных в URL ищем тот, который задает
83 # действие 
84                         if (exists $actions{$param}) {
85 # Мы, конечно уже проверили, что в названии параметра
86 # нехороших символов нет, но чтобы perl в taint mode не
87 # ругался... 
88                                 if (allow_operation($param,$cgi,$forum)) {
89                                         print STDERR "Allow_operation completed\n";
90                                         show_template($1,$cgi,$forum) if $param=~/^(\w+)$/;     
91                                         exit;
92                                 } else {
93                                         if (!$forum->{"authenticated"}) { 
94                                                 $cgi->param("returnto",$cgi->url(-full=>1));
95                                                 show_template("login",$cgi,$forum);
96                                                 exit;
97
98                                         } else {
99                                                 show_error($forum,"У Вас нет прав на  выполнение этой
100                                                 операции")
101                                         }
102                                 }       
103                         }
104                 }
105                 if (index($path_translated,$forum->{userdir})==0) {
106                         show_user_page($cgi,$forum);
107                 }       
108                 show_error($forum,"Некорректный вызов скрипта. Отсутствует параметр
109                                 действия");
110         }       
111 } else {
112         # Запрос методом POST. Вызываем обработчик
113         for my $param ($cgi->param) {
114                 if (exists $actions{$param}) {
115                         if (allow_operation($param,$cgi,$forum)) {
116                                 $actions{$param}->($param,$cgi,$forum);
117                                 exit;
118                         } else {
119                                 show_error($forum,"У Вас нет прав на  выполнение этой
120                                 операции")
121                         }
122
123                 }
124         }
125         print STDERR "Получены параметры ",join(" ",$cgi->param),"\n";
126         show_error($forum,"Некорректный вызов скрипта. Отсутствует параметр действия");
127 }       
128
129 #-------------------------------------------------------------- 
130 #-------- Чтение конфигурационного файла и связанные с этим действия
131 #------------------------------------------------------------------ 
132
133 #
134 # Преобразование пути в файловой системе сервера в путь в URL
135
136 sub dir2url {
137         my ($cgi,$dir) = @_;
138         my $prefix="";
139         my $pos=rindex $ENV{'PATH_TRANSLATED'},$ENV{'PATH_INFO'};
140         if ($pos <0 && $ENV{'PATH_INFO'}=~m!(/\~\w+)/!) {
141                 $prefix .=$1;
142                 $pos =
143                 rindex($ENV{'PATH_TRANSLATED'},substr($ENV{'PATH_INFO'},length($1)));
144         }
145         if ($pos <0) {
146                 show_error({},"Ошибка конфигурации форума. Не удается определить
147                 алгоритм преобразования директори в URL\n".
148                 "PATH_INFO=$ENV{PATH_INFO}\n".
149                 "PATH_TRANSLATER=$ENV{'PATH_TRANSLATED'}");
150         }       
151         my $root = substr($ENV{'PATH_TRANSLATED'},0,$pos);
152         if (substr($dir,0,length($root)) ne $root) {
153                 show_error({},"Ошибка конфигурации форума. Не удается преобразовать
154                 имя директории $dir в url\n".
155                 "PATH_INFO=$ENV{PATH_INFO}\n".
156                 "PATH_TRANSLATER=$ENV{'PATH_TRANSLATED'}");
157         }
158         return $prefix.substr($dir,length($root));
159 }
160 #
161 # Поиск файла .forum вверх по дереву от $path_translated  
162 # Значение PATH_TRANSLATED считаем безопасным - наш web-сервер нам не
163 # враг.
164 # Возвращает список имя,значение, имя, значение который прививается в
165 # хэш
166 #
167 sub get_forum_config {
168         $path_translated = $1 if $ENV{PATH_TRANSLATED}=~/^(.+)$/;
169         $path_translated=~s/\/+$//;
170         my @path=split("/",$path_translated);
171         while (@path>1) {
172                 if (-r (my $config=join("/",@path,".forum")) ) {
173                         open F,"<",$config;
174                         my %config;
175                         while (<F>) {
176                                 s/#.*$//; #Drop comments;
177                                 $config{$1}=$2 if /(\w+)\s*=\s*(\S.*)$/;
178                         }       
179                         close F;
180                         #
181                         # Переменная forumtop - это URL того места, где находится
182                         # файл .forum
183                          
184                         $config{"forumtop"} = dir2url($cgi,join("/",@path));
185                         $config{"forumroot"} = join("/",@path);
186                         # Если в конфиге отсутствует переменная templates, но
187                         # рядом с конфигом присутствует директория templates,
188                         # то шаблоны там.
189                         #
190                         if (! exists $config{"templates"} 
191                                 && -d (my $filename = join("/",@path,"templates"))) {
192                                         $config{"templates"} = $filename;
193                         }               
194                         $config{"templatesurl"} = dir2url($cgi,$config{"templates"})
195                                 unless exists $config{"templatesurl"};
196                         # 
197                         # То же самое - параметр userdir и директория users
198                         #
199                         if (! exists $config{"userdir"} 
200                                 && -d (my $filename = join("/",@path,"users"))) {
201                                         $config{"userdir"} = $filename;
202
203
204                         }       
205                         $config{"userurl"} = dir2url($cgi,$config{"userdir"});
206                         #
207                         # Если нет ссылки в конфиге на файл паролей или он не 
208                         # существует, выдаем ошибку. С офоромлением, так как шаблоны
209                         #  у нас уже есть
210                         if (!exists $config{"datadir"}) {
211                                 show_error(\%config,"В конфигурации форума не указана
212                                 директория данных "); 
213                                 exit;
214                         }
215                         if (!-d $config{"datadir"}) {
216                                 show_error(\%config,"В конфигурации форума указана несуществующая директория данных "); 
217                                 exit;
218                         }
219                         #
220                         # Некоторые умолчания
221                         #
222                         $config{"authperiod"}="+1M" if (! exists $config{"authperiod"}); 
223                         $config{"renewtime"} = "86000" if (!exists $config{"renewtime"});
224                         $config{"replies_per_page"} = 50 if (!exists $config{"replies_per_page"});
225                         $config{"indexfile"} = "index.html" if (!exists $config{"indexfile"});
226                         return \%config;
227                 }
228                 pop @path;
229         }
230         #
231         # Выводим ошибку 404 без осмысленного оформления, так как данных форума
232         # мы не нашли
233         print "Status: 404\nContent-Type: text/html; charset=utf-8\n\n",
234         "<HTML><HEAD><TITLE>Форум не обнаружен</TITLE></HEAD><BODY>",
235         "<H!>Форум не найден</H!>",
236         "<p>Хвост URL, указанный при вызове скрипта  показывает не на
237         форум</p>",
238         # To make IE think this page is user friendly
239         "<!--",("X" x 512),"--></body></html>\n"; 
240         exit;
241 }
242 #
243 # Вывод сообщения об ошибке по шаблону форума
244 # Шаблон должен содержать элемент с классом error.
245 #
246 sub show_error {
247         my ($cfg,$msg) = @_;
248         if ( -r $cfg->{"templates"}."/error.html") {
249                 my $tree = treefromfile($cfg->{"templates"}."/error.html");
250                 my $node= $tree->find_by_attribute('class','error');
251                 my $body;
252                 if (!$node) {
253                         $body = $tree->find_by_tagname('body');
254                         $body->push_content($node = new
255                         HTML::Element('div','class'=>'error'));
256                 }
257                 $node->delete_content;
258                 $node->push_content($msg);
259                 print $cgi->header(-type=>'text/html',-charset=>'utf-8');
260                 print output_html($tree);
261         } else {
262                 print $cgi->header(-type=>'text/html',-charset=>'utf-8');
263                 print "<html><head><title>Ошибка конфигурации форума</title></head>",
264                 "<body><h1>Ошибка конфигурации форума</h1><p>",
265                 escapeHTML($msg),"</p>",
266                 "<p>При обработке этой ошибки не обнаружен шаблон сообщения об ошибке</p></body></html>";  
267         }
268         exit;
269 }       
270
271 #
272 # Вывод шаблона формы. В шаблоне должна присутстовать форма с  
273 # именем, совпадающим с именем form. Если в $cgi есть параметры, имена
274 # которых совпадают с именами полей этой формы, их значения
275 # подставляются
276 #
277 sub show_template {
278         my $tree = prepare_template(@_);                        
279         send_to_user($tree,@_);
280         exit;
281 }
282 sub send_to_user {
283         my ($tree,$form,$cgi,$forum) = @_;
284         print
285         $cgi->header(-type=>"text/html",-charset=>"utf-8",($forum->{cookies}?(-cookie=>$forum->{cookies}):())),
286         output_html($tree);
287 }       
288 sub prepare_template { 
289         my ($form,$cgi,$forum) = @_;
290         my $tree = gettemplate($forum,$form,$ENV{'PATH_INFO'});
291
292         # Находим форму с классом $form
293         my $f = $tree->look_down("_tag","form",
294                 "name",$form);
295         if (! defined $f) {
296                 # Если не нашли - ругаемся
297                 show_error($forum,"Шаблон для операции $form не содержит формы с
298                 именем $form");
299                 exit;
300         }
301         $cgi->delete('password');
302         if (!$cgi->param("returnto")) {
303                 $cgi->param("returnto", $cgi->referer||$cgi->url(-absolute=>1,-path_info=>1));
304
305         }       
306         if (!$cgi->param($form)) {
307                 $cgi->param($form,1);
308         }       
309         # 
310         # Если ранее была выставлена ошибка с помощью set_error, подставляем
311         # сообщение в элемент с классом error
312         #
313         if ($forum->{error_message}) {
314                 my $errormsg = $tree->look_down("class"=>"error");
315                 if ($errormsg) {
316                         $errormsg->delete_content();
317                         $errormsg->push_content($forum->{error_message});
318                 }
319         }       
320         if ($forum->{"authenticated"}) {
321                  
322                 # Подставляем информацию о текущем пользователе если в шаблоне
323                 # это предусмотрено 
324                 substitute_user_info($tree,$forum);
325                 $cgi->param("user",$forum->{"authenticated"}{"user"}) if (!defined $cgi->param("user"))
326         }
327         my %substituted;
328         ELEMENT:
329         for my $element ($f->find_by_tag_name("textarea","input","select")) {
330                 my $name = $element->attr("name");
331                 $substituted{$name} = 1;
332                 if (defined  $cgi->param($name)) {
333                         if ($element->tag eq "input") {
334                                 my $type=$element->attr('type') || "text";
335                                 next ELEMENT if grep($type eq $_,
336                                                 "button","submit","reset");  
337                                 if ($type eq "check") {
338                                         if (grep($element->attr("value") eq $_,$cgi->param($name))) {
339                                                 $element->attr("checked","");
340                                         } else {
341                                                 $element->attr("checked",undef);
342                                         }
343                                 
344                                 } elsif ($type eq
345                                 "radio") {
346                                         if ($element->attr("value") eq $cgi->param($name)) {
347                                                 $element->attr("checked","");
348                                         } else {
349                                                 $element->attr("checked",undef);
350                                         }
351                                 } else {        
352                                 $element->attr("value",$cgi->param($name));
353                                 }
354                         } elsif ($element->tag eq "textarea") {
355                                 my $data=$cgi->param($name);
356                                 if ($data=~/^<(div|p)\b/ && !defined($cgi->param($name."_format"))) {
357                                         if ($data=~/^<div class="bbcode">(.*)<\/div>$/) {
358                                                 $cgi->param($name."_format","bbcode");
359                                                 my $parser = HTML::BBReverse->new();
360                                                 $data = $parser->reverse($1);
361                                         # FIXME elsif ($data=~/<p class="text">)        
362                                         } else {
363                                                 $cgi->param($name."_format","html");
364                                         }       
365
366                                 }
367                                 $element->delete_content;
368                                 $element->push_content($cgi->param($name));
369                         } elsif ($element->tag eq "select") {
370                                 for my $option ($element->find_by_tag_name("option")) {
371                                         my $value = $option->attr("value") ||
372                                                 $option->as_text();
373                                         if (grep($value eq $_, $cgi->param($name))) {
374                                                 $option->attr("selected","selected");
375                                         } else {        
376                                                 $option->attr("selected",undef);
377                                         }       
378                                 }
379
380                         }
381                 }
382
383         }
384         $f->attr("method","POST");
385         for my $required ($form,"returnto") {
386                 if (!$substituted{$required}) {
387                         my $element = new HTML::Element('input',
388                                 'type' => 'hidden', 'name' => $required,
389                                 'value'=> $cgi->param($required));
390                         $f->push_content($element);
391                 }
392         }       
393         return $tree;
394 }       
395 #
396 # Поправляет ссылки на служебные файлы и скрипты форума
397 #
398 sub fix_forum_links {
399         my ($forum,$tree,$path_info) = @_;
400         if (!defined $path_info) {
401                 $path_info = $ENV{PATH_INFO};
402                 $path_info =~ s/\/+/\//g;
403         }               
404         my $script_with_path = $ENV{SCRIPT_NAME}.$path_info;
405         ELEMENT:
406         for my $element ($tree->find_by_tag_name("form","img","link","script","a")) {
407                 my $attr;
408                 if ($element->tag eq "form")  {
409                         $attr = "action";
410                 } elsif ($element->tag eq "a"|| $element->tag eq "link") {
411                         $attr = "href";
412                 } else {
413                         $attr ="src";
414                 }
415                 
416                 # Обрабатываем наши специальные link rel=""
417                 my $userlist = $cgi->url(-absolute=>1,
418                                         -path_info=>0,-query_string=>0).$forum->{userurl};
419                 if ($element->tag eq "link") {
420                         if ($element->attr("rel") eq "forum-user-list") {
421                                 $element->attr("href" => $userlist);
422                                 next ELEMENT;   
423                         } elsif ($element->attr("rel") eq "forum-script")  {
424                                 $element->attr("href" => $script_with_path);
425                                 next ELEMENT;
426                         }       
427                 }
428                 my $link = $element->attr($attr);
429                 # Абсолютная ссылка - оставляем как есть. 
430                 next ELEMENT if (! defined $link || $link=~/^\w+:/ || $link
431                 eq"."||$link eq ".."); 
432                 # Ссылка от корня сайта. 
433                 if (substr($link,0,1) eq "/") {
434                         # Если там два слэша, заменяем их на forumtop
435                         if (substr($link,0,2) eq '//') {
436                                 $element->attr($attr, $forum->{forumtop}.substr($link,1));
437                                 next ELEMENT;
438                         }       
439                         # Если она не ведет на наш скрипт, не обрабатываем
440                         next ELEMENT if substr($link,0,length($ENV{SCRIPT_NAME}) ne
441                         $ENV{SCRIPT_NAME}) ;
442                         # Иначе пишем туда слово forum вместо реального имени
443                         # скрипта чтобы потом единообразно обработать
444                         $link =~ s/^[^\?]+/forum/;
445                 }
446                 if (!($link =~ s!^templates/!$forum->{templatesurl}/!) &&
447                     !($link =~ s!^users/!$userlist/!) &&
448                     !($link =~ s!^forum\b!$script_with_path!)) {
449                         $link = $forum->{"forumtop"}."/".$link 
450                 }       
451                 $element->attr($attr,$link);
452         }
453 }               
454 #
455 # Подставляет в заданное поддерево информацию о пользователе
456 #
457
458 sub substitute_user_info {
459
460 my ($tree,$forum,$user) = @_;
461 my %userinfo;
462 if (defined $user) {
463         %userinfo=%$user;
464 } else {
465         # Если не сказано, какой юзер, то текущий.
466         %userinfo = %{$forum->{"authenticated"}}  
467 }
468
469 #
470 # Специально обрабатываем поля user (должна быть ссылка) и avatar  
471 # (должен быть img).
472         my $userpage;
473         if ($userinfo{"openiduser"}) {
474                 $userpage = "http://".$userinfo{"user"};
475         } else {
476                 $userpage =
477                 $cgi->url(-absolute=>1).$forum->{"userurl"}."/".$cgi->escape($userinfo{"user"});
478         }       
479         substinfo($tree,["_tag"=>"a","class"=>"author"],
480          href=>$userpage,_content=>$userinfo{"user"});
481         delete $userinfo{"user"};
482         if (ref $userinfo{"avatar"} eq "HASH") {
483                 substinfo($tree,["_tag"=>"img","class"=>"avatar"],
484                 %{$userinfo{'avatar'}});
485         } elsif ($userinfo{'avatar'})  {        
486                 substinfo($tree,["_tag"=>"img","class"=>"avatar"],
487                 src=>$userinfo{"avatar"});
488         } else {
489                 substinfo($tree,["_tag"=>"img","class"=>"avatar"],
490                         src=>$forum->{templatesurl}."/1x1.gif",
491                         width=>1,height=>1);
492         }               
493
494         for my $element ( $tree->look_down("class",qr/^ap-/)) {
495                 my $field=$1 if $element->attr("class")=~/^ap-(.*)$/;   
496                 $element->delete_content();
497                 $field =~ tr/-/_/;
498                 $userinfo{$field} = 0 if (!exists $userinfo{$field} && grep ($field eq
499                         $_,"forums","messages","topics"));
500                 if (exists $userinfo{$field}) {
501                          
502                         my $data;
503                         if ($field eq "registered" || substr($field,0,5) eq "last_") {
504                         $data = strftime("%d.%m.%Y %H:%M",localtime($userinfo{$field}))
505                         } elsif ($userinfo{$field}=~/^<\w+/) {
506                                 $data = str2tree($userinfo{$field});
507                         } else {
508                                 $data = $userinfo{$field}
509                         }
510                         $element->push_content($data);
511                         $element->attr(href=>"mailto:$userinfo{$field}") 
512                                 if ($element->tag eq "a" && $field eq "email");
513                 }       
514         } 
515
516
517 }
518 #
519 # Авторизует зарегистрированного пользователя.
520 # 1. Проверяет куку если есть
521 #
522
523 sub authorize_user      {
524         ($cgi,$forum) = @_;
525         if (my $session=$cgi->cookie("slsession")) {
526         # Пользователь имеет куку
527                 my %sessbase;   
528                 dbmopen %sessbase,datafile($forum,"session"),0644;
529                         if ($sessbase{$session})  {
530                                 my ($user,$expires,$ip)=split(";", $sessbase{$session});
531                                 my $user_cookie = $cgi->cookie("sluser");
532                                 if ($user_cookie ne $user && $user_cookie ne
533                                 "http://".$user) {
534                                         clear_user_cookies($cgi,$forum);
535                                         show_error($forum,"Некорректная пользовательская сессия");
536                                         exit;
537                                 }       
538                                 if (!defined $ip|| $ip eq $ENV{'REMOTE_ADDR'}) {
539                                         my %userbase;
540                                         dbmopen %userbase,datafile($forum,"passwd"),0644;
541                                         if ( $userbase{$user}) {
542                                                 my $userinfo = thaw($userbase{$user});
543                                                 delete $userinfo->{"passwd"};
544                                                 $userinfo->{"user"} = $user;
545                                                 if ($expires-time()< $forum->{"renewtime" }) {
546                                                         delete $sessbase{$session};
547                                                         newsession(\%sessbase,$forum,$user,$ip);
548                                                 }
549                                                 print STDERR "user $user restored session $session\n";
550                                                 $forum->{"authenticated"}=$userinfo;
551                                                 print STDERR "authorize_user: ",$forum->{authenticated}{user},
552                                                 $forum->{authenticated},"\n";
553                                         }       
554                                         dbmclose %userbase; 
555                                 }       
556                         } else {
557                                 clear_user_cookies($cgi,$forum);
558                                 show_error($forum,"Некорректная пользовательская сессия");
559                                 exit;
560                         }
561                 dbmclose %sessbase;
562         }
563 }
564 #
565 # Возвращает путь к файлу в директории 
566 #
567 sub datafile {
568         my ($forum,$filename) = @_;
569         return $forum->{"datadir"}."/".$filename;
570 }       
571
572 #
573 # Создает новую сессию для пользователя и подготавливает куку которую
574 # сохраняет в хэше конфигурации форума
575
576 sub newsession {
577         my ($base,$forum,$user,$bindip) = @_;
578         if (!defined $base) {
579                 $base = {};
580                 dbmopen %$base,datafile($forum,"session"),0644;
581         }       
582         my $sessname;
583         my $t = time();
584         my ($u,$expires,$ip);
585         do {
586                 $sessname = sprintf("%08x",rand(0xffffffff));
587                 if ($base->{"sessname"}) {
588                         ($u,$expires,$ip) = split ";", $base->{$sessname};
589                         delete $base->{$sessname} if $expires < $t;
590                 }
591         } while ($base->{$sessname});
592         my $cookie = $cgi->cookie(-name=>"slsession",
593                 -expires => $forum->{"authperiod"},-value=> $sessname);
594         my $username = $user;
595         $username =~ s/^http:\/\///; #Remoove http:// from OpenID user names 
596         $base->{$sessname}=$username.";".str2time($cookie->expires()).
597                 ($ip?";$ENV{'REMOTE_ADDR'}":"");
598                 
599         $forum->{'cookies'}=[ $cookie,
600         $cgi->cookie(-name=>"sluser",-value=>$user,-expires =>
601         $forum->{authperiod})];                         
602 }
603 #
604 # Выполняет аутентикацию пользователя по логину и паролю и 
605 # создает для него сессию.
606 #
607 sub authenticate {
608         my ($cgi,$forum) = @_;  
609         if ($cgi->param("openidsite")) {
610                 my $openid_url = sprintf($cgi->param("openidsite"),$cgi->param("user"));
611                 openidstart($cgi,$forum,$openid_url);
612         }       
613         my %userbase;
614         dbmopen %userbase,datafile($forum,"passwd"),0644;
615         my $user = $cgi->param("user");
616         my $password = $cgi->param("password");
617         $cgi->delete("password");
618         if (! $userbase{$user}) {
619           set_error($forum,"Неверное имя пользователя или пароль");
620           return undef;
621         }   
622         my $userinfo = thaw($userbase{$user}) ;
623         dbmclose %userbase;
624         #while (my ($key,$val)=each %$userinfo) { print STDERR "$key => '$val'\n";}
625         if (defined $forum->{denied_status} && $userinfo->{status} eq 
626                 $forum->{denied_status}) {
627                 set_error($forum,"Вход пользователя $user в систему заблокирован");
628                 return undef;
629         }       
630         if (crypt($password,$userinfo->{passwd}) eq $userinfo->{passwd}) {
631                 delete $userinfo->{"passwd"};
632                 $cgi->delete("password");
633                 $userinfo->{"user"} = $user;
634                 newsession(undef,$forum,$user);
635                 $forum->{"authenticated"} = $userinfo;          
636                 print STDERR "User $user authenticated successfully\n";
637                 return 1;
638         } else {
639                 set_error($forum,"Неверное имя пользователя или пароль");
640                 return undef;
641         }       
642 }
643 #
644 # Запоминает сообщение об ошибке для последующего вывода show_template
645 #
646 sub set_error {
647         my  ($forum,$message) = @_;
648         print STDERR "set_error: $message\n";
649         $forum->{error_message} = $message;
650 }       
651 #
652 # Выводит текущий шаблон с сообщением об ошибке
653 #
654 sub form_error {
655         my ($form_name,$cgi,$forum,$msg) = @_;
656         set_error($forum,$msg);
657         show_template($form_name,$cgi,$forum);
658         exit;
659 }       
660 #
661 # Выполняет редирект (возможно, с установкой куков) на страницу,
662 # указанную # третьем параметре функции или в параметре CGI-запроса
663 # returnto
664 # Если и то, и другое не определено, пытается сконструировать URL для
665 # возврата из PATH_INFO.
666 #
667
668 sub forum_redirect {
669         my ($cgi,$forum,$url) = @_;
670         if (!defined $url) {
671                 $url = $cgi->param("returnto");
672                 $url =
673                 $cgi->url(-base=>1).($cgi->path_info()||$forum->{forumtop}) if !$url ;
674         }
675         $url = $cgi->url(-base=>1).$url if substr($url,0,1) eq "/";
676         print $cgi->redirect(-url=>$url,
677                 ($forum->{cookies}?(-cookie=>$forum->{cookies}):()));
678         exit;   
679 }
680 #
681 # Заполнение формы редактирования профиля данными пользователя
682
683 sub show_profile {
684         my ($formname,$cgi,$forum) = @_; 
685         my $rights = getrights($cgi,$forum);
686         my $user = $cgi->param("user");
687         if (!$user && substr($path_translated,length($forum->{userdir}) eq
688         $forum->{userdir})) {
689                 $user = substr($path_translated,length($forum->{userdir})+1);
690         }
691         $user = $forum->{authenticated}{user} unless $user;
692         show_error($forum,"Чей профиль вы хотите редактировать?") 
693                 unless $user; 
694         my %base;
695         dbmopen %base,datafile($forum,"passwd"),0664;
696         show_error($forum,"Нет такого пользователя $user") 
697                 unless $base{$user};
698         my $userinfo = thaw($base{$user});
699         dbmclose(%base); 
700         delete $userinfo->{passwd};
701         $userinfo->{user}=$user;
702         while(my ($field,$value) = each %$userinfo) {   
703                 $value = $value->{src} if ($field eq 'avatar' && ref($value));
704                 $cgi->param($field,$value);
705         }       
706         my $tree = prepare_template(@_);
707         # Запрещаем редактирование полей, входящих в restricted_user_info
708         my $form = $tree->look_down(_tag=>"form",name=>"profile");
709         if ($rights ne "admin" && $forum->{restricted_user_info}) {
710                 for my $field (split /\s*,\s*/,$forum->{restricted_user_info}) {
711                         ELEMENT:
712                         for my $element ($form->look_down(name=>$field)) {
713                                 my $tag= $element->tag;
714                                 if ($tag eq 'input') {
715                                         my $newel=new HTML::Element("span",
716                                         "class"=>"restricted-field");
717                                         
718                                         $newel->push_content($element->attr("value"));
719                                         $element->replace_with($newel)->delete();
720                                 } elsif ($tag eq 'textarea') {
721                                         $element->replace_with_content(new HTML::Element("div",
722                                                 class=>"restricted-field"))->delete();
723                                 } elsif ($tag eq 'select') {
724                                         my $newel = new HTML::Element("span",
725                                                 class=>"restricted-field");
726                                         OPTION:
727                                         for my $option ($element->content_list) {
728                                                 if (ref $option eq "HTML::Element" && 
729                                                   $option->attr("selected")) {
730                                                         $newel->push_content($option->detach_content());
731                                                         last OPTION;
732                                                 }
733                                         }
734                                         if (!$newel->content_list) {
735                                                 $newel->push_content(($element->content_list)[0]);
736                                         }       
737                                         $element->replace_with($newel)->delete;
738                                 }       
739                         }
740                 }       
741         }
742         # Подставляем аватарку
743         substinfo($tree,[_tag=>'img',class=>'avatar'],(ref($userinfo->{avatar})?(%{$userinfo->{avatar}}):(src=>$userinfo->{avatar})));
744         for my $userlink ($tree->look_down(_tag => "a",class=>"author")) {
745                 $userlink->delete_content;
746                 $userlink->push_content($user);
747                 if ($forum->{authenticated}{openiduser}) {
748                         $userlink->attr('href'=>"http://$user");
749                 } else {
750                         $userlink->attr('href'=>undef);
751                         $userlink->tag('span');
752                 }
753         }       
754         send_to_user($tree,@_);
755 }
756 # Обработка результатов редактирования профиля пользвателя
757 #
758 sub profile {
759         my ($formname,$cgi,$forum) = @_; 
760         if (!$cgi->param("user")) {
761                 show_error($forum,"В форме нет имени пользователя");
762         }
763         my $user = $cgi->param('user');
764         my $rights = getrights($cgi,$forum);
765         if ($user ne $forum->{authenticated}{user} &&
766                 $rights ne "admin") {
767                 show_error($forum,"У вас нет прав на изменение профиля этого
768                 пользователя");
769         }
770         my %base;
771         dbmopen %base,datafile($forum,"passwd"),0644;
772         if (!$base{$user}) {
773                 show_error($forum,"Несуществующий пользователь $user");
774         }
775         my $userinfo = thaw $base{$user};
776         $userinfo->{user}=$user;
777         #
778         # If password fields are filled, change password
779         #
780         if ($cgi->param('pass1')) {
781                 if ($cgi->param('pass1') eq $cgi->param('pass2')) {
782                         $userinfo->{passwd}=crypt_password($cgi->param('pass1'));
783                 } else {
784                         form_error($formname,$cgi,$forum,"Ошибка при вводе пароля");
785                 }       
786         }       
787         make_profile($formname,$cgi,$forum,$userinfo,$rights eq "admin");
788         delete $userinfo->{user};
789         $base{$user} = freeze $userinfo;
790         dbmclose %base;
791         show_profile($formname,$cgi,$forum);
792 }
793 #
794 # Обработка результатов заполнения формы регистрации.
795 #
796 #
797 sub register {
798         my ($formname,$cgi,$forum) = @_; 
799         #
800         # Возможные ошибки: 
801         # 1 Такой юзер уже есть
802         #
803         #  не заполнено поле user 
804         if (!$cgi->param("user")) {
805                 form_error($formname,$cgi,$forum, "Не заполнено имя пользователя");
806         }       
807         #  или поле password 
808         if (!$cgi->param("pass1"))  {
809                 form_error($formname,$cgi,$forum,"Не указан пароль");
810         }       
811         #  Копии пароля не совпали
812         if ($cgi->param("pass2") ne $cgi->param("pass1")) {
813                 form_error($formname,$cgi,$forum,"Ошибка при вводе пароля");
814         }               
815         my $user = $cgi->param("user");
816         # Не указаны поля, перечисленные в скрытом поле required 
817         if ($cgi->param("required")) { 
818                 foreach my $field (split(/\s*,\s*/,$cgi->param('required'))) {
819                         if (!$cgi->param($field)) {
820                                 form_error($formname,$cgi,$forum,"Не заполнено обязательное поле $field");
821                         }
822                 }       
823         }
824         $cgi->delete("required");
825         my %userbase;
826         dbmopen %userbase,datafile($forum,"passwd"),0644 
827                 or form_error($formname,$cgi,$forum,"Ошибка открытия файла паролей $!");
828         if ($userbase{$cgi->param("user")}) {
829                 dbmclose %userbase;
830                 form_error($formname,$cgi,$forum,"Имя пользователя '".$cgi->param("user"). "' уже занято");
831         }
832         my $userinfo = {passwd=>crypt_password($cgi->param('pass1'))};
833         make_profile($formname,$cgi,$forum,$userinfo,0);
834         $userinfo->{registered} = time;
835         set_default_user_attrs($forum,$userinfo);
836         print STDERR "stilllife forum: registering user $user\n";
837         $userbase{$user} = freeze($userinfo);
838         dbmclose %userbase;
839         if (!defined $forum->{denied_status} || $userinfo->{status} ne
840                 $forum->{denied_status}) { 
841                 newsession(undef,$forum,$user);
842                 forum_redirect($cgi,$forum,$cgi->param("returnto")); 
843         } else {
844                 # FIXME Email validation
845                 # Email to admin
846                 show_template("newuser",$cgi,$forum);
847         }
848 }       
849 sub make_profile {
850         my ($formname,$cgi,$forum,$userinfo,$isadmin) =@_;
851         # Удаляем лишние поля
852         foreach my $field (split(/\s*,\s*/,$cgi->param('ignore'))) {
853                 if (!$cgi->param($field)) {
854                         $cgi->delete($field);
855                 }
856         }       
857         if ($cgi->param("email") && !  Email::Valid->address($cgi->param("email"))) {
858                 form_error($formname,$cgi,$forum,"Некорректный E-Mail адрес");
859         }
860         my $user = $userinfo->{user};
861         my $userprefix=$user;
862         $userprefix=~tr!\\/:    !_!;
863         # Если есть аватар в файле, то сохраняем этот файл и формируем URL
864         # на него.
865         $cgi->delete($formname);
866         $cgi->delete("user");
867         $cgi->delete("pass1");
868         $cgi->delete("pass2");
869         if ($cgi->param("avatarfile" )) {
870                 my $f = $cgi->upload("avatarfile");
871                 binmode $f,":bytes";
872                 my $out;
873                 my $filename = $1 if $cgi->param("avatarfile")=~/([^\/\\]+)$/;
874                 my $path = $forum->{"userdir"}."/".$userprefix."_".$filename;
875                 open $out,">",$path;
876                 binmode $out,":bytes";
877                 my $buffer;
878                 while (my $bytes = read($f,$buffer,4096)) {
879                         print $out $buffer;
880                 }       
881                 close $f;
882                 close $out;
883                 my ($w,$h) = imgsize($path);
884                 $userinfo->{'avatar'}= {src=>$forum->{"userurl"}."/".$userprefix."_".$filename,
885                         width=>$w,height=>$h};
886         } elsif ($cgi->param('avatar')) {
887                 if (!ref($userinfo->{'avatar'}) || 
888                         $userinfo->{avatar}{'src'} ne $cgi->param('avatar')) {
889                         $userinfo->{avatar}=get_avatar_info($cgi->param('avatar'));
890                 }       
891         }
892         my @restrict=();
893         @restrict = split /\s*,\s*/, $forum->{restricted_user_info}
894                 unless $isadmin;
895
896         foreach my $param       ($cgi->param) {
897                 next if  (grep $_ eq $param,@restrict);
898                 next if $param eq 'avatar';
899                 next if $param eq 'avatarfile';
900                 next if $param eq 'returnto';
901                 next if $param =~ /_format$/;
902                 if (defined $cgi->param("${param}_format")) {
903                         my $tree = input2tree($cgi,$forum,$param);
904                         $userinfo->{$param} = tree2str($tree);
905                         $tree->delete();
906                 } else {
907                         $userinfo->{$param} = $cgi->param($param);
908                 }
909         }
910 }
911 sub crypt_password {
912         my $open_password=shift;
913         my $saltstring = 'ABCDEFGHIJKLMNOPQRSTUVWXUZabcdefghijklmnopqrstuvwxuz0123456789./';
914         my $salt = substr($saltstring,int(rand(64)),1).
915                                 substr($saltstring,int(rand(64)),1);
916         my $password=crypt($open_password,$salt);                       
917         return $password;
918 }       
919
920 sub set_default_user_attrs {
921         my ($forum,$userinfo) = @_;
922         while (my($key,$val) = each %$forum) {
923                 next unless $key =~ /^default_(.*)$/;
924                 $userinfo->{$1} = $val;
925         }       
926 }
927
928 sub show_user_page {
929         my ($cgi,$forum) = @_;
930         my $rights;
931         $rights=getrights($cgi,$forum) if ($forum->{authenticated}); 
932         my %base;
933         my $tree;
934         dbmopen %base,datafile($forum,"passwd"),0664;
935         if ($path_translated eq $forum->{userdir}) {
936           # показать список пользователей
937           $tree = gettemplate($forum,"userlist");
938           my $usertpl = $tree->look_down(class=>"userinfo");
939           my $userlist = $usertpl->parent;
940           $usertpl->detach;
941           for my $user (sort keys %base) {
942                         my $block = $usertpl->clone;
943                         $userlist->push_content($block);
944                         my $userinfo =thaw($base{$user});
945                         $userinfo->{"user"} = $user;
946                         substitute_user_info($block,$forum,$userinfo);
947                         profile_links($block,$user,$rights,$cgi,$forum);
948           }             
949           $usertpl->delete;     
950         } else {
951                 my $user = substr($path_translated,length($forum->{userdir})+1);
952                 if (!$base{$user}) {
953                         print $cgi->header(-status=>"404 NOT FOUND");
954                         exit;
955                 }
956                 my $userinfo = thaw($base{$user});
957                 $userinfo->{"user"} = $user;
958                 $tree = gettemplate($forum,"user");
959                 substinfo($tree,[_tag=>"title"],_content=>"Stilllife user: $user");
960                 substitute_user_info($tree,$forum,$userinfo);
961                 profile_links($tree,$user,$rights,$cgi,$forum);
962                 unless ($userinfo->{openiduser}) {
963                         for my $userlink ($tree->look_down(_tag => "a",class=>"author")) {
964                                 $userlink->attr("href",undef);
965                                 $userlink->tag("span");
966                         }       
967                 }
968         }       
969         my $page = output_html($tree);
970         my $length = do {use bytes; length($page);};
971         print $cgi->header(-type=>"text/html",-content_length=>$length,
972         -charset=>"utf-8",($forum->{cookies}?(-cookie=>$forum->{cookies}):())),
973         $page;
974 }
975 sub profile_links {
976         my ($tree,$user,$rights,$cgi,$forum)=@_;
977         foreach my $profile_link ($tree->look_down(_tag=>"a",
978                         href=>qr/profile=/)) {
979                 if ((defined $rights && $rights eq "admin")|| 
980                         (defined $forum->{authenticated}{user} &&
981                          $forum->{authenticated}{user} eq $user)) {
982
983                                 $profile_link->attr("href",
984                                         $ENV{'SCRIPT_NAME'}.$forum->{userurl}.
985                                         "/".$user."?profile=1");
986                 } else {        
987                         $profile_link->delete();
988                 }       
989         }       
990 }
991 sub clear_user_cookies {
992         my ($cgi,$forum) = @_;
993         $forum->{cookies}=[ $cgi->cookie(-name=>"sluser", -value=>"0",
994         -expires=>"-1m"),$cgi->cookie(-name=>"slsession", -value=>"0",
995                         -expires => "-1m")];
996 }                       
997 #
998 # Обработчик формы логина. Сводится к вызову функции authenticate,
999 # поскольку мы поддерживаем логин одновременный с отправкой реплики. 
1000 #
1001 sub login {
1002         my ($form,$cgi,$forum)=@_;
1003         if (authenticate($cgi,$forum)) {
1004                 forum_redirect($cgi,$forum);
1005         } else {
1006                 show_template(@_);
1007         }       
1008 }       
1009 #
1010 # Обработчик формы logout. В отличие от большинства обработчиков форм,
1011 # поддерживает обработку методом GET
1012 #
1013 sub logout {
1014         my ($form,$cgi,$forum) = @_;
1015         clear_user_cookies($cgi,$forum);
1016         if (defined (my $session_id = $cgi->cookie("slsession"))) {
1017                 my %sessiondb;
1018                 dbmopen %sessiondb,datafile($forum,"session"),0644;
1019                 delete $sessiondb{$session_id};
1020                 dbmclose %sessiondb;
1021         }
1022         forum_redirect($cgi,$forum);
1023 }       
1024 sub allow_operation {
1025         my ($operation,$cgi,$forum) = @_;
1026         return 1 if (!exists($permissions{$operation})); 
1027         if (!$forum->{authenticated}) {
1028                 return 1 if ($permissions{$operation} eq "login");
1029                 return 0;
1030         }       
1031         my $user = $forum->{authenticated}{user} ;
1032         my $accesslevel=getrights($cgi,$forum);
1033         
1034         return 1 if ($accesslevel eq "admin");
1035         return 0 if ($permissions{$operation} eq "admin");      
1036         return 1 if ($accesslevel eq "moderator");
1037         return 0 if $accesslevel eq "banned";   
1038         return 1;
1039 }
1040
1041 sub reply {
1042         my ($form,$cgi,$forum) = @_;
1043         if (! exists $forum->{authenticated} ) {
1044                 form_error($form,$cgi,$forum,"Вы не зарегистрировались") if (!authenticate($cgi,$forum)); 
1045         }
1046         #
1047         # Находим файл дискуссии, в который надо поместить реплику
1048         #
1049         my ($tree,$lockfd)=gettree($path_translated); 
1050         my $newmsg = newlistelement($tree,"message","messagelist");
1051         if (!$newmsg) {
1052                 show_error($forum,"Шаблон темы не содержит элемента с классом
1053                 message");
1054                 exit;
1055         }       
1056         
1057         #       
1058         # Генерируем идентификатор записи.
1059         #
1060         my $id = "m".get_uid($forum);
1061
1062
1063         #
1064         # Сохраняем приаттаченные картинки, если есть.
1065         #
1066         my $dir = $path_translated;
1067
1068         $dir=~ s/[^\/]+$// if (-f $dir);
1069         my %attached;
1070         for (my $i=1;$cgi->param("image$i"); $i++) {
1071                 my $userpath=$cgi->param("image$i");
1072                 my $filename=lc($1) if $userpath =~ /([^\/\\]+)$/;
1073                 $attached{$filename} = $id."_".$filename;
1074                 my $in = $cgi->upload("image$i");
1075                 if (!$in) {
1076                         show_error($forum,"Ошибка при загрузке картинки $filename");
1077                         exit;
1078                 }       
1079                 my $out;
1080                 open $out,">$dir/$attached{$filename}";
1081                 binmode $out,":bytes";
1082                 local $/=undef;
1083                 my $data = <$in>;
1084                 print $out $data;
1085                 close $in;
1086                 close $out;
1087         }
1088         #
1089         # Преобразуем текст записи в html и чистим его
1090         #
1091         my $txtree = input2tree($cgi,$forum,"text");
1092         #
1093         # Находим в тексте URL на приаттаченные картинки и меняем на те
1094         # имена, под которыми мы их сохранили.
1095         #
1096         for my $image ($txtree->find_by_tag_name("img")) {
1097                 my $file=lc($image->attr("src"));
1098                 if ( exists $attached{$file}) {
1099                         $image->attr("src" => $attached{$file});
1100                         my ($width,$height) = imgsize($dir ."/".$attached{$file});              
1101                         $image->attr("width" =>$width);
1102                         $image->attr("height" => $height);
1103                 }       
1104         }       
1105         #
1106         # Подставляем данные сообщения 
1107         #
1108         $newmsg->attr("id"=>$id);
1109         substinfo($newmsg,[class=>"subject"],_content=>$cgi->param("subject"));
1110         my $textnode=$newmsg->look_down("class"=>"mtext");
1111         if (!$textnode) {
1112                 show_error($forum,"В шаблоне реплики нет места для текста"); 
1113         }       
1114         $textnode->delete_content();
1115         $textnode->push_content($txtree);
1116         if ($forum->{authenticated}{signature}) {
1117                 $textnode->push_content(new HTML::Element("br"),"--",
1118                 new HTML::Element("br"),str2tree($forum->{authenticated}{signature}));
1119         }
1120         substitute_user_info($newmsg,$forum);
1121         #
1122         # Подставляем данные в форму msginfo
1123         #
1124         my $editform=$newmsg->look_down(_tag=>"form","class"=>"msginfo");
1125         if ($editform) {
1126                 substinfo($editform,[_tag=>"input",name=>"id"],value=>$id) ||
1127                         show_error($forum,"В форме управления сообщением нет поля id");
1128                 substinfo($editform,[_tag=>"input",name=>"author"],value=>
1129                         $forum->{authenticated}{user}) ||
1130                         show_error($forum,"В форме управления сообщением нет поля author");
1131         }
1132         # Подставляем mdate
1133          my $posted = strftime("%d.%m.%Y %H:%M",localtime());
1134         substinfo($newmsg,["class"=>"mdate"],
1135                 _content =>$posted);
1136         # Подставляем mreply
1137         substinfo($newmsg,[_tag=>"a","class"=>"mreply"],"href" =>
1138          $cgi->url(-absolute=>1,-path_info=>1)."?reply=1&id=$id");
1139         # Подставляем manchor
1140         substinfo($newmsg,[_tag=>"a","class"=>"manchor"],
1141                 "name"=>"#$id","href"=>undef) or
1142                 show_error($forum,"В шаблоне сообщения отсутствует якорь для ссылок на него");
1143         # подставляем mlink
1144         substinfo($newmsg,[_tag=>"a","class"=>"mlink"],
1145                 href=>$cgi->path_info."#$id");
1146         # подставляем mparent
1147         my $parent_id=$cgi->param("id");
1148         if ($parent_id) {
1149                 substinfo($newmsg,[_tag => "a",class=>"mparent"], 
1150                         "href"=>$cgi->path_info."#$parent_id",style=>undef);
1151         } else {
1152                 substinfo($newmsg,[_tag => "a",class=>"mparent"], 
1153                         style=>"display: none;");
1154         }       
1155         my $msgcount=0;
1156         for my $msg ($newmsg->parent->look_down("class"=>"message")) {
1157                 $msgcount ++;
1158         }       
1159          
1160         #
1161         # Делаем Уфф и сохраняем то, что получилось 
1162         #
1163         $newmsg = $newmsg->clone;
1164         savetree($path_translated,$tree,$lockfd);
1165         record_as_recent($forum,$newmsg);
1166         record_statistics($forum,"message"),
1167         update_topic_list($forum,$path_translated,$msgcount,$posted);
1168         forum_redirect($cgi,$forum,$cgi->path_info."#$id");
1169 }       
1170 sub update_topic_list {
1171         my ($forum,$topic,$count,$date) = @_;
1172         my ($tree,$lockfd,$block,$index);
1173         if (!ref ($topic)) {
1174         # Если $topic - имя файла, найдем соответствующий индекс и в нем
1175         # элемент с соответствующим id;
1176                 my ($dir,$id)=($1,$2) if $topic =~/(.+)\/([^\/]+).html/;
1177                 $index = $dir."/".$forum->{indexfile};
1178                 ($tree,$lockfd) = gettree($index);
1179                 $block = $tree->look_down("id"=>$id);
1180                 return unless $block;
1181         } else {
1182         # Иначе нам передали кусок готового распарсенного дерева
1183                 $block = $topic;
1184         }
1185         substinfo($block,[class=>"msgcount"],_content=>$count);
1186         substinfo($block,[class=>"last-updated"],_content=>$date);
1187         # и если мы парсили дерево, то мы его и сохраняем
1188         savetree($index,$tree,$lockfd);
1189 }
1190
1191 sub record_as_recent {
1192         my ($forum,$msg) = @_;
1193         my ($tree,$lockfd) = gettree($forum->{forumroot}."/recent.html");
1194         my $msglist = $tree->look_down("class"=>"messagelist");
1195         if ($msglist) {
1196                 my $style = $msglist->attr("style");
1197                 if ($style && $style =~ s/display: none;//) {
1198                         $msglist->attr("style",$style);
1199                         $msglist->look_down(class=>"message")->replace_with($msg);
1200                 } else {
1201                         my @msgs = $msglist->look_down("class"=>"message");
1202                         if (@msgs >= $forum->{replies_per_page}) {
1203                                 for (my $i=$#msgs;$i>=$forum->{replies_per_page}-1;$i--) {
1204                                         $msgs[$i]->delete;
1205                                 }
1206                         }       
1207                         $msgs[0]->preinsert($msg);      
1208                 }
1209         }
1210         savetree($forum->{forumroot}."/recent.html",$tree,$lockfd);
1211 }       
1212 #
1213 # Обработка операции создания новой темы. 
1214 #
1215
1216 sub new_topic {
1217         my ($form,$cgi,$forum) = @_;
1218         #
1219         # Проверяем корректность urlname и прочих полей
1220         #
1221         my $urlname;
1222         if (!$cgi->param("urlname")) {
1223                 $urlname = get_uid($forum);
1224         } else {        
1225                 $urlname=$1 if $cgi->param("urlname") =~ /^([-\w]+)$/;
1226                 form_error($form,$cgi,$forum,"Некорректные символы в urlname.
1227                 Допустимы только латинские буквы, цифры и минус") unless $urlname; 
1228         }
1229         if (!-d $path_translated) {
1230                 show_error($forum,"Операция $form может быть вызвана только со
1231                 страницы форума");
1232         }       
1233         my $filename = "$path_translated/$urlname.html";
1234         if (-f $filename) {
1235                 form_error($form,$cgi,$forum,"Тема с urlname $urlname уже
1236                 существует");
1237         }       
1238         my $url = $cgi->path_info."/$urlname.html";
1239                 $url =~ s/\/+/\//g;
1240         if (!$cgi->param("title")) {
1241                 form_error($form,$cgi,$forum,"Тема должна иметь непустое название");
1242         }       
1243         #
1244         # Создаем собственно тему
1245         #
1246         my $tree = gettemplate($forum,"topic",$url);
1247     # Заполнить название и аннотацию 
1248         my $abstract = input2tree($cgi,$forum,"abstract");
1249         substinfo($tree,[_tag=>"meta","name"=>"description"],content=>$abstract->as_trimmed_text);
1250         substinfo($tree,[_tag=>"title"],_content=>$cgi->param("title"));
1251         my $subtree = $tree->look_down("class"=>"topic");
1252         my $creation_time=strftime("%d.%m.%Y %H:%M",localtime());
1253         if ($subtree) {
1254                 substinfo($subtree,["class"=>"title"],
1255                 _content=>$cgi->param("title"));
1256                 substinfo($subtree,["class"=>"date"],
1257                         _content=>$creation_time);
1258                 # Вставляем в страницу КОПИЮ аннотации, поскольку аннотация
1259                 # нам еще понадобится в списке тем.
1260                 substinfo($subtree,["class"=>"abstract"],_content=>$abstract->clone);   
1261                 substitute_user_info($subtree,$forum);  
1262         } else {
1263                 substinfo($tree,["class"=>"title"],
1264                 _content=>$cgi->param("title"));
1265         }       
1266         # Скрыть список сообщений.
1267         hide_list($tree,"messagelist");
1268         savetree($filename,$tree,undef);
1269         $tree->destroy;
1270         #
1271         # Добавляем элемент в список тем текущего форума
1272         #
1273
1274         my $lockfd;
1275         ($tree,$lockfd)=gettree($path_translated."/".$forum->{"indexfile"});
1276         my $newtopic = newlistelement($tree,"topic","topiclist");
1277         substinfo($newtopic,[_tag=>"a","class"=>"title"],
1278         _content=>$cgi->param("title"), href=>"$urlname.html");
1279         substinfo($newtopic,["class"=>"date"], _content=>$creation_time);
1280         substinfo($newtopic,["class"=>"abstract"],_content=>$abstract); 
1281         substitute_user_info($newtopic,$forum); 
1282         $newtopic->attr("id",$urlname);
1283         my $controlform = $newtopic->look_down(_tag=>"form",class=>"topicinfo");
1284         if ($controlform) {
1285                 $controlform->attr("action"=>$cgi->url(-absolute=>1,-path_info=>0,
1286                         -query_string=>0).$url);
1287                 substinfo($controlform,[_tag=>"input",name=>"author"],value=>
1288                         $forum->{authenticated}{user});
1289         }               
1290         update_topic_list($forum,$newtopic,0,$creation_time);
1291         savetree($path_translated."/".$forum->{"indexfile"},$tree,$lockfd);
1292         record_statistics($forum,"topic");
1293         forum_redirect($cgi,$forum,$cgi->url(-base=>1).$url);
1294 }
1295
1296 sub new_forum {
1297         my ($form,$cgi,$forum) = @_;
1298         #
1299         # Проверяем корректность urlname и прочих полей
1300         #
1301         my $urlname;
1302          if (!$cgi->param("urlname")) {
1303                 form_error($form,$cgi,$forum,"Форуму необходимо задать непустое urlname");
1304          }     
1305          if ($cgi->param("urlname") eq ".") {
1306                 $urlname = "."
1307          } else {       
1308                 $urlname=$1 if $cgi->param("urlname") =~ /^([-\w]+)$/ ;
1309                 form_error($form,$cgi,$forum,"Некорректные символы в urlname.
1310                         Допустимы только латинские буквы, цифры и минус") unless $urlname; 
1311         }
1312         if (!-d $path_translated) {
1313                 show_error($forum,"Операция $form может быть вызвана только со
1314                 страницы форума");
1315         }       
1316         my $newname = "$path_translated/$urlname";
1317         $newname=$path_translated if ($urlname eq ".");  
1318         my $filename = "$newname/$forum->{indexfile}";
1319         if (-f $filename) {
1320                 form_error($form,$cgi,$forum,"Форум $urlname уже существует");
1321         }       
1322         if (!$cgi->param("title")) {
1323                 form_error($form,$cgi,$forum,"Форум должен иметь непустое название");
1324         }
1325         mkdir $newname unless -d $newname;
1326         #
1327         # Сохраняем логотип
1328         #
1329         my ($logo_name,$logo_width,$logo_height);
1330         if ($cgi->param("logo")) {
1331                 my $userpath = $cgi->param("logo");
1332                 $logo_name="logo.".lc($1) if $userpath=~/\.([^.]+)$/;
1333                 my $in = $cgi->upload("logo");
1334                 if (!$in) {
1335                         show_error($forum,"Ошибка при загрузке картинки $userpath");
1336                         exit;
1337                 }       
1338                 my $out;
1339                 open $out,">$newname/$logo_name";
1340                 binmode $out,":bytes";
1341                 local $/=undef;
1342                 my $data = <$in>;
1343                 print $out $data;
1344                 close $in;
1345                 close $out;
1346                 ($logo_width,$logo_height) = imgsize("$newname/$logo_name");
1347         } else {
1348                 $logo_name = $forum->{"templatesurl"}."/1x1.gif";
1349                 $logo_width = 1;
1350                 $logo_height=1;
1351         }       
1352
1353
1354         #
1355         # Создаем собственно оглавление форума
1356         #
1357         
1358         my $url = $cgi->path_info."/$urlname";
1359         $url= $cgi->path_info if $urlname eq ".";
1360         $url =~ s/\/+/\//g;
1361         my $tree = gettemplate($forum,"forum",$url);
1362         # Удалить элементы, который присутствуют только на главной странице
1363         if ($urlname ne ".") {
1364                 for my $element ($tree->look_down("class"=>"top-page")) {
1365                         $element->delete;
1366                 }       
1367         }
1368     # Заполнить название и аннотацию 
1369         my $abstract = input2tree($cgi,$forum,"abstract");
1370         substinfo($tree,[_tag=>"meta","name"=>"description"],content=>$abstract->as_trimmed_text);
1371         substinfo($tree,[_tag=>"title"],_content=>$cgi->param("title"));
1372         my $subtree = $tree->look_down("class"=>"annotation")
1373                 or show_error($forum,"В шаблоне форума отсутствует класс annotation");
1374         my $creation_time=strftime("%d.%m.%Y %H:%M",localtime());
1375                 substinfo($subtree,["class"=>"title"],
1376                 _content=>$cgi->param("title"));
1377                 substinfo($subtree,["class"=>"date"],
1378                         _content=>$creation_time);
1379                 # Вставляем в страницу КОПИЮ аннотации, поскольку аннотация
1380                 # нам еще понадобится в списке тем.
1381                 substinfo($subtree,["class"=>"abstract"],_content=>$abstract->clone);   
1382                 substitute_user_info($subtree,$forum);  
1383         substinfo($subtree,[_tag=>"img","class"=>"logo"],
1384                 src=> $logo_name, width=>$logo_width, height=>$logo_height);
1385         # Скрыть списки подфорумов и тем .
1386         hide_list($tree,"forumlist");
1387         hide_list($tree,"topiclist");
1388         if ($urlname eq ".") {
1389                 for my $link_up ($tree->look_down(_tag=>"a",href=>"..")) {
1390                         $link_up->delete;
1391                 }
1392         }       
1393         savetree($filename,$tree,undef);
1394         $tree->destroy;
1395         #
1396         # Добавляем элемент в список тем текущего форума
1397         #
1398         if ($urlname ne ".") {
1399         my $lockfd;
1400         ($tree,$lockfd)=gettree($path_translated."/".$forum->{"indexfile"});
1401         my $newforum = newlistelement($tree,"forum","forumlist");
1402         substinfo($newforum,[_tag=>"a","class"=>"title"],
1403         _content=>$cgi->param("title"), href=>"$urlname/");
1404         substinfo($newforum,["class"=>"date"], _content=>$creation_time);
1405         substinfo($newforum,["class"=>"abstract"],_content=>$abstract); 
1406         substinfo($newforum,[_tag=>"img","class"=>"logo"],src=>"$urlname/$logo_name",
1407                 width=>$logo_width,height=>$logo_height);
1408         substitute_user_info($newforum,$forum); 
1409         $newforum->attr("id",$urlname);
1410         my $controlform = $newforum->look_down(_tag=>"form",class=>"foruminfo");
1411         if ($controlform) {
1412                 $controlform->attr("action"=>$cgi->url(-absolute=>1,-path_info=>0).
1413                 $url);
1414                 substinfo($controlform,[_tag=>"input",name=>"author"],value=>
1415                         $forum->{authenticated}{user});
1416         } 
1417         savetree($path_translated."/".$forum->{"indexfile"},$tree,$lockfd);
1418         record_statistics($forum,"forum");
1419         } else {
1420         # Создаем тему для "свежих реплик"
1421          my $recent = gettemplate($forum,"topic",$url."/recent.html");  
1422         # remove reply link from page itself
1423         for my $link ($recent->look_down(_tag =>"a", href=>qr/reply=/)) {
1424                 $link->delete;
1425         }       
1426          substinfo($recent,["_tag"=>"title"],$cgi->param("title").": Свежие сообщения");
1427                 substinfo($recent,["class"=>"title"],
1428                 _content=>$cgi->param("title"). ": Свежие сообщения");
1429                 hide_list($recent,"messagelist");
1430                 savetree($path_translated."/recent.html",$recent,undef);
1431
1432         }
1433
1434         forum_redirect($cgi,$forum,$cgi->url(-base=>1).$url);
1435 }
1436
1437 #
1438 # Обработка операции удаления всего на свете
1439 #
1440 sub delete_item {
1441         my ($formname,$cgi,$forum) = @_;
1442         #
1443         # Сначала разберемся, что мы собственно удаляем 
1444         #
1445         if ($cgi->param("user")) {
1446                 delete_user($cgi,$forum,$cgi->param("user"));
1447         } elsif (-f $path_translated) {
1448                 if ($cgi->param("id")) {
1449                         delete_comment($cgi,$forum,$path_translated,$cgi->param("id"));
1450                 } else {
1451                         delete_topic($cgi,$forum,$path_translated);
1452                 }       
1453         } elsif (-d $path_translated && -f $path_translated ."/".  $forum->{indexfile}) {
1454                 delete_forum($cgi,$forum,$path_translated);
1455         } else {
1456                 show_error($forum,"Невозможно удалить неопознанный объект");
1457         }
1458 }       
1459
1460 # Удаление пользователя
1461 #
1462 sub delete_user {
1463         my ($cgi,$forum,$user) = @_;
1464         if ($forum->{authenticated}{user} ne $user &&
1465                 getrights($cgi,$forum) ne "admin") {
1466                         show_error($forum,"У вас недостаточно прав для удаления
1467                         пользователя $user"); 
1468         }               
1469         my %base;
1470         dbmopen %base,datafile($forum,"passwd"),0644;
1471         delete $base{$user};
1472         dbmclose %base;
1473         forum_redirect($cgi,$forum,$forum->{forumtop});
1474 }       
1475 #
1476 # Удаление реплики
1477 #
1478 sub delete_comment {
1479         my ($cgi,$forum,$topic,$id) = @_;
1480         my ($tree,$lockfd) = gettree($topic);
1481         my ($msg) = $tree->look_down(id => $id);
1482         show_error($forum,"В данной теме нет реплики с id=$id") if (!$msg);
1483         if (getrights($cgi,$forum) ne "moderator" 
1484                 && getrights($cgi,$forum) ne "admin") {
1485                 my $author= $msg->look_down(_tag=>"input",name=>"author");
1486                 if ($author->attr("value") ne $forum->{authenticated}{user}) {
1487                         show_error($forum,"У вас нет прав на удаление этого сообщения");
1488                 }       
1489         }
1490         delete_from_list($tree,"messagelist","message",$msg);
1491         savetree($topic,$tree,$lockfd);
1492         $tree->delete;
1493         ($tree,$lockfd) = gettree($forum->{forumroot}."/recent.html");
1494         ($msg) = $tree->look_down(id => $id);
1495         if ($msg) {
1496                 delete_from_list($tree,"messagelist","message",$msg); 
1497                 savetree($forum->{forumroot}."/recent.html",$tree,$lockfd);
1498         } else {
1499                 discardtree($tree,$lockfd);
1500         }
1501         # Если у реплики были картинки, то их тоже надо грохнуть
1502         for my $pic (<$id."_*.*">) {
1503                 unlink $pic;
1504         }       
1505         forum_redirect($cgi,$forum,$cgi->path_info);
1506 }       
1507 #
1508 # Удаление темы
1509 #
1510 sub delete_topic {
1511         my ($cgi,$forum,$topic) = @_;
1512         # Если модератор, то в тему и не заглядываем
1513         my ($dir,$id) = ($1,$2) if $topic=~/^(.*)\/([^\/]+).html$/;  
1514         show_error($forum,"Неверное имя темы $topic") unless ($dir && $id);
1515         if (getrights($cgi,$forum) ne "moderator" &&
1516                 getrights($cgi,$forum) ne "admin") {
1517                 # Проверяем авторство темы и наличие в ней сообщений
1518                 my ($tree,$lockfd) = gettree($topic);
1519                 my $info = $tree->look_down(class=>"topic");
1520                 my $author = $tree->look_down(class=>"author");
1521                 if ($author->as_text() ne $forum->{authenticated}{user}) {
1522                         show_error($forum,"У вас нет права удалять эту тему");
1523                 }
1524                 $info = $tree->look_down(class=>"messagelist");
1525                 if ($info->attr("style") !~ /display: none;/) {
1526                         show_error($forum,"Невозможно удалить тему, содержащую сообщения");
1527                 }       
1528                 discardtree($tree,$lockfd);
1529         } # FIXME при удалении непустой темы модератором почистить recent.html       
1530         my ($tree,$lockfd) = gettree($dir . "/".$forum->{indexfile});
1531         delete_from_list($tree,"topiclist","topic",$id);
1532         savetree($tree,$dir ."/".$forum->{indexfile});
1533         unlink $topic;
1534         my $redirect_url = $cgi->path_info;
1535         $redirect_url =~ s/\/[^\/]*$//;
1536         forum_redirect($cgi,$forum,$redirect_url);
1537 }
1538 #
1539 # Удаление форума
1540 #
1541 sub delete_forum {
1542         my ($cgi,$forum,$dir) = @_;
1543         if (getrights($cgi,$forum) ne "moderator" && getrights($cgi,$forum)
1544         ne "admin") {
1545                 show_error($forum,"У вас нет права удалять форумы");
1546         }       
1547         opendir DIR,$dir;
1548         my $count = 0;
1549         while ( my $entry=readdir DIR) {
1550                 next if $entry eq $forum->{indexfile};
1551                 next if $entry eq "perms.txt";
1552                 next if $entry =~ /^logo.[^.]+/;
1553                 show_error($forum,"Нельзя удалять корень форума" )
1554                         if $entry eq ".forum";
1555                 $count ++;      
1556         }               
1557         closedir DIR;
1558         show_error("Нельзя удалять непустой форум") if $count;
1559         # Находим родительский форум
1560         my $upper = $dir;
1561         $upper=~s/([\/]+)$/$forum->{indexfile}/;
1562         my $id = $1 if $dir =~ /\/([\/]+)$/;
1563         # Удаляем форум из списка.
1564         my ($tree,$lockfd) = gettree($upper);
1565         delete_from_list($tree,"forumlist","forum",$id);
1566         savetree($upper,$tree,$lockfd);
1567         # Удаляем из файловой системы
1568         opendir DIR,$dir;
1569         for my $entry ( readdir DIR) {
1570                 unlink "$dir/$entry";
1571         }       
1572         rmdir $dir;
1573         my $redirect_url = $cgi->path_info;
1574         $redirect_url =~s/\/$//;
1575         $redirect_url =~ s/\/[^\/]*$//;
1576         forum_redirect($cgi,$forum,$redirect_url);      
1577 }       
1578
1579 #---------------------------------------------------------- 
1580 # База пользователей и права доступа
1581 #----------------------------------------------------------
1582 #
1583 # Записывает в базу данных пользователей, сколько каких объектов 
1584 # создал текущий пользователь
1585 #
1586 sub record_statistics {
1587         my ($forum,$type) = @_;
1588         my $user = $forum->{authenticated}{user};
1589         my %base;
1590         dbmopen %base,datafile($forum,"passwd"),0664;
1591         my $userinfo = thaw($base{$user});
1592         $userinfo->{$type."s"}++;
1593         $userinfo->{"last_$type"}=time;
1594         $base{$user} = freeze($userinfo);
1595         dbmclose %base;
1596 }
1597 #
1598 # читает файлы прав доступа в дереве форума, и возвращает
1599 # статус текущего пользователя (undef - аноним, banned, normal,
1600 # moderator или admin
1601
1602 sub getrights {
1603         my ($cgi,$forum) = @_;
1604         if (!$forum->{authenticated}) {
1605                 return undef;
1606         }       
1607         return $forum->{authenticated}{rights} if 
1608                 exists $forum->{authenticated}{rights};
1609         my $user = $forum->{authenticated}{user};
1610         my $dir = $path_translated;
1611         $dir =~s/\/$//;
1612         $dir =~s/\/[^\/]+$// if (!-d $dir);
1613         my $f;
1614         my $user_status = "normal";
1615         LEVEL:
1616         while (length($dir)) {  
1617                 if (-f "$dir/perms.txt") {
1618                         open $f,"<","$dir/perms.txt";
1619                         my $status = undef;
1620                         while (<$f>) {
1621                                 if (/^\[\s*(admins|moderators|banned)\s*\]/) {
1622                                         $status = $1;
1623                                 } else {
1624                                         chomp;
1625                                         if  ($user eq $_ && defined $status) {
1626                                                 if ($status eq "banned") {
1627                                                         return $forum->{authenticated}{rights}=$status;
1628                                                 } 
1629                                                 if ($status eq "admins" ) {
1630                                                         return $forum->{authenticated}{rights}="admin";
1631                                                 }
1632                                                 $user_status = "moderator";
1633                                         }
1634                                 }       
1635                         }
1636                         close $f;
1637                         last LEVEL if  -f "$dir/.forum";
1638                 }       
1639                 # Strip last path component.
1640                 $dir =~s/\/[^\/]+$// 
1641         }               
1642         return $forum->{authenticated}{rights}=$user_status;
1643
1644 }               
1645
1646
1647
1648 #------------------------------------------------------------------
1649 # Работа с файлами и идентификторами
1650 #------------------------------------------------------------------
1651
1652 #
1653 # Залочить файл и получить его распрасенное представление.
1654 # Возвращает пару ($tree,$lockfd)
1655
1656 sub gettree {
1657         my $filename = shift;
1658         my $f;
1659         open $f,"<",$filename or return undef;
1660         flock $f, LOCK_EX;
1661         my $tree = treefromfile($f);
1662         $tree->parse_file($f);
1663         return ($tree,$f);
1664 }       
1665 #
1666 # Сохранить дерево и закрыть lockfd.
1667 #
1668 #
1669
1670 sub savetree {
1671         my ($filename,$tree,$lockfd) = @_;
1672         my $f;
1673         open $f,">",$filename . ".new" or return undef;
1674         print $f output_html($tree);
1675         close $f;
1676         # FIXME - только для POSIX.
1677         unlink $filename;
1678         rename $filename.".new",$filename;
1679         close $lockfd if defined($lockfd);
1680 }       
1681
1682 sub discardtree {
1683         my ($tree,$lockfd) = @_;
1684         flock $lockfd,LOCK_UN;
1685         close $lockfd;
1686         $tree->delete;
1687 }       
1688 #
1689 # Cериализовать HTML-документ с DOCTYPE (workaround вокруг баги в
1690 # HTML::TreeBuilder)
1691 #
1692 sub output_html {
1693         my $tree=shift;
1694         return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'.
1695         $tree->as_HTML("<>&");
1696 }       
1697 #
1698 # Читает шаблон и подготавливает его к размещению по указанной URL.
1699 # Если url не указана, считается что шаблон будет показан как результат
1700 # текущего http-запроса.
1701
1702 sub gettemplate {
1703         my ($forum, $template,$url) = @_;
1704         $url =~ s/\/+/\//g if defined $url;
1705         my $filename=$forum->{"templates"}."/$template.html";
1706         if (! -r $filename) {
1707                 show_error($forum,"Нет шаблона $template");
1708                 exit;
1709         }
1710         my $tree = treefromfile($filename);
1711         fix_forum_links($forum,$tree,$url);
1712         return $tree;
1713 }       
1714
1715 #
1716 # Создает объект HTML::TreeBuilder и выставляет ряд опций.
1717 #
1718
1719 sub make_tree {
1720         my $tree = HTML::TreeBuilder->new;
1721         # Set some options for treebuilder
1722         # Comments are neccessary to convert HTML back to BBCode 
1723         $tree->store_comments(1);
1724         # Avoid converting html into one long-long string
1725         $tree->ignore_ignorable_whitespace(0);
1726         $tree->no_space_compacting(1);
1727         $tree->p_strict(1);
1728         return $tree;
1729 }       
1730
1731 sub treefromfile {
1732         my ($f) = shift;
1733         my $tree = make_tree();
1734         $tree->parse_file($f);
1735         return $tree;
1736 }       
1737 #
1738 # Получает уникальный числовой идентификатор.
1739
1740 sub get_uid {
1741         my $forum = shift;
1742         my $f;
1743         open $f,"+<",datafile($forum,"sequence") or 
1744         flock $f,LOCK_EX;
1745         my $id=<$f> || "0";
1746         $id++;
1747         seek $f,0,0;
1748         printf $f "%8s\n",$id;
1749         close $f;
1750         $id=~/(\d+)/;
1751         return sprintf ("%08s",$1);
1752 }
1753 # --------------------------------------------------------------------
1754 #  OpenID registration
1755 # -------------------------------------------------------------------
1756 sub create_openid_consumer {
1757         my ($cgi,$forum) = @_;
1758         return Net::OpenID::Consumer ->new(
1759                 ua => LWP::UserAgent->new( agent => "Stilllife/1.0"),
1760                 args => $cgi,
1761                 consumer_secret=>"X9RWPo0rBE7yLja6VB3d",
1762                 required_root => $cgi->url(-base=>1));
1763 }               
1764
1765
1766 # openidstart - вызывается когда обнаружено что текущее имя
1767 # пользователя, пытающегося аутентифицироваться, содержит http://
1768 #  
1769 #
1770
1771 sub openidstart {
1772         my ($cgi,$forum,$openidurl) = @_;
1773         #
1774         # Fix duplicated http:// which can be produced by our sprintf based
1775         # login system
1776         #
1777         $openidurl=~s!^http://http://!http://!;
1778         my $csr = create_openid_consumer($cgi,$forum);
1779         my $claimed_identity=$csr->claimed_identity($openidurl);
1780         if (!defined $claimed_identity) {
1781                 show_error($forum,"Указанная URL $openidurl не является OpenId");            
1782                 exit;
1783         }
1784         $cgi->param("openidvfy",1);
1785         $cgi->delete("user");
1786         $cgi->delete("openidsite");
1787         $cgi->delete("password");
1788         my $check_url = $claimed_identity->check_url(
1789                 return_to=> $cgi->url(-full=>1,-path_info=>1,-query=>1),
1790                 trust_root=> $cgi->url(-base=>1));
1791         print $cgi->redirect(-location=>$check_url);
1792         exit;
1793 }       
1794 #
1795 # Вызывается при редиректе от openid producer-а. Проверяет, что
1796 # удаленный сервер подтвердил openid и вызывает операцию для которой
1797 # (либо возврат на исходную страницу при операции login, либо постинг
1798 # реплики) 
1799 #
1800 sub openid_verify {
1801         my ($cgi,$forum) = @_;
1802         my $csr  = create_openid_consumer($cgi,$forum);
1803         if (my $setup_url = $csr->user_setup_url) {
1804                 print $cgi->redirect(-location=>$setup_url);
1805                 exit;
1806         } elsif ($csr->user_cancel) {
1807                 show_error($forum,"Ваш openid-сервер отказался подтвержать вашу
1808                 идентичность");
1809                 exit;
1810         } elsif (my $vident = $csr->verified_identity) {
1811                 #Успешная аутентификация.         
1812                 #Создаем сессию
1813                 my $user = $vident->url; 
1814                 # Remove trailing slash from URL if any
1815                 $user=~s/\/$//;
1816                 my %userbase;
1817                 dbmopen %userbase,datafile($forum,"passwd"),0664;
1818                 my $username = $user; 
1819                 $username =~ s/^http:\/\///;
1820                 if (!$userbase{$username}) {
1821                         # Тащим foaf, если получится
1822                         my %info=get_foaf($csr->ua,$vident->declared_foaf);
1823                         if (ref($info{'avatar'}) eq "HASH" ) {
1824                                 delete $info{'avatar'}{'type'};
1825                         }       
1826                         $info{"openiduser"}=1;
1827                         $info{"registered"}=time; 
1828                         set_default_user_attrs($forum,\%info);
1829                         $info{'status'} = $forum->{openid_status} if $forum->{openid_status};
1830                         $forum->{authenticated}=\%info;
1831                         $userbase{$username} = freeze(\%info);
1832                 } else {
1833                         $forum->{authenticated} = thaw ($userbase{$username});
1834                 }
1835                 dbmclose %userbase;
1836                 if (defined $forum->{denied_status} && 
1837                         ($forum->{authenticated}{status} eq $forum->{denied_status})) {
1838                         show_error($forum,"Вход пользователя $username в систему заблокирован"); 
1839                 }       
1840                 $forum->{"authenticated"}{"user"} = $username;
1841                 newsession(undef,$forum,$user);
1842                 # Если указан параметр reply, вызываем обработку реплики
1843                 if ($cgi->param("reply")) {     
1844                         reply("reply",$cgi,$forum);
1845                         exit;
1846                 }       
1847                 #Иначе, возвращаемся на исходную страницу
1848                 forum_redirect($cgi,$forum,undef);
1849         }       else {
1850                 show_error($forum,"Ошибка OpenId аутентификации");
1851                 exit;
1852         }       
1853 }
1854
1855 sub get_avatar_info {
1856         my ($url,$ua) = @_; 
1857         $ua = LWP::UserAgent->new( agent => "Stilllife/1.0") unless $ua;
1858         my $response = $ua->get($url);
1859         if ($response->is_success) {
1860                         my $image = $response->content;
1861                         my ($w,$h,$type) = imgsize(\$image);
1862                         return {width=>$w,height=>$h,type=>$type,src=>$url};
1863                 } else {
1864                         print STDERR "Error getting $url: ".$response->status_line,"\n";
1865                         return undef;
1866                 }       
1867 }
1868
1869 sub get_foaf {
1870         my ($ua,$foaf_url) = @_; 
1871         my $response = $ua->get($foaf_url);
1872         unless ($response->is_success) {
1873                 print STDERR "Error geting foaf from $foaf_url\n";
1874                 return ();
1875         }       
1876         my $foaf = $response->content;
1877         my %info = foaf_parse($foaf);
1878         if ($info{avatar}) {
1879                 $info{avatar} = get_avatar_info($info{avatar},$ua);
1880         }       
1881         return %info;
1882 }
1883
1884 sub foaf_parse {
1885         my $foaf = shift;
1886         my ($starttag) = $foaf =~ /<(\w+(:\w+)?[^>]+)>/sg;
1887         my %ns = reverse ($starttag =~ /xmlns:(\w+)="([^"]+)"/sg);
1888         my $foaf_prefix = $ns{"http://xmlns.com/foaf/0.1/"};
1889         my $rdf_prefix = $ns{"http://www.w3.org/1999/02/22-rdf-syntax-ns#"};
1890         my ($userpic) = $foaf=~/<$foaf_prefix:img[^>]* $rdf_prefix:resource="([^"]+)"/s;
1891         my @info;
1892         push @info, avatar =>$userpic if $userpic;
1893         my ($icq) = $foaf =~/<$foaf_prefix:icqChatID>([^<]*)<\/$foaf_prefix:icqChatID>/s;
1894         push @info, icq => $icq if ($icq);
1895         my ($jabber) = $foaf =~/<$foaf_prefix:jabberID>([^<]*)<\/$foaf_prefix:jabberID>/s;
1896         push @info, jabber => $jabber if ($jabber);
1897         return @info;
1898 }
1899 #-----------------------------------------------------------------
1900 # Обработка форматированных текстовых полей
1901 #-----------------------------------------------------------------
1902
1903 sub input2tree {
1904         my ($cgi,$forum,$field_name) = @_;
1905         my $format = $cgi->param($field_name."_format");
1906         my $text = $cgi->param($field_name);
1907         if ($format eq "bbcode") {
1908                 my $parser = HTML::BBReverse->new(); 
1909                 $text="<div class=\"bbcode\">".$parser->parse($text)."</div>";
1910         } elsif ($format eq "text") {
1911                 $text=~s/\r?\n\r?\n/<\/p><p class=\"text\">/;
1912                 $text=~s/\r?\n/<br>/;
1913                 $text = "<div><p class=\"text\">".escapeHTML($text)."</p></div>";
1914         } 
1915         my $txtree = str2tree($text);
1916         for my $badtag
1917         ("script","style","head","html","object","embed","iframe","frameset","frame",
1918         ($forum->{forbid_tags}?split(/\s*,\s*/,$forum->{forbid_tags}):())) {
1919                 for my $element ($txtree->find_by_tag_name($badtag)) {
1920                         $element->delete() if defined $element;
1921                 }       
1922         }       
1923         # FIXME Проверяем на наличие URL-ок не оформленных ссылками.
1924         return $txtree;
1925 }       
1926
1927
1928
1929 sub str2tree {
1930         my ($data)=@_;
1931         my $tree = make_tree();
1932         $tree->ignore_ignorable_whitespace(1);
1933         # Set parser options here
1934         $tree->parse("<html><body><div>$data</div></body></html>");
1935         $tree->eof;
1936         my $element=$tree->find("body");
1937         while (($element =($element->content_list)[0])->tag ne "div") {
1938         }
1939         while ($element->content_list==1&& $element->tag eq "div" &&
1940                 !defined $element->attr("style")) {
1941                 $element = ($element->content_list)[0]
1942         }
1943         $element->detach;
1944         $tree->destroy;
1945         return $element;
1946 }       
1947
1948 sub tree2str {
1949         my ($tree)=@_;
1950         return $tree->as_HTML("<>&");
1951 }
1952
1953 #------------------------------------------------------------------------
1954 # Подстановка в дереве
1955 #------------------------------------------------------------------------
1956 # Находит 
1957 # элемент указанного класса и удаляет display: none из его атрибута
1958 # style. Возвращает 1, если элемент был раскрыт, и 0, если он и до этого 
1959 # был видимым.
1960 sub unhide_list {
1961         my ($tree,$class) = @_;
1962         my $msglist = $tree->look_down("class"=>$class);
1963         if ($msglist) {
1964                 my $style = $msglist->attr("style");
1965                 if ($style && $style =~ s/display: none;//) {
1966                         $msglist->attr("style",$style);
1967                         return 1;
1968                 } else {
1969                         return 0;
1970                 }       
1971         } 
1972 }       
1973 #
1974 # Находит первый элемент указанного класса, и приписывает ему display:
1975 # none в style.
1976 #
1977 sub hide_list {
1978         my ($tree,$class)=@_;
1979         my $msglist = $tree->look_down("class"=>$class);
1980         return undef unless $msglist;
1981         if (!$msglist->attr("style")) {
1982                 $msglist->attr("style","display: none;");
1983         } else {
1984                 my $style = $msglist->attr("style");
1985                 unless ($style=~ s/\bdisplay:\s+\w+\s*;/display: none;/) {
1986                         $style .= "display: none;";
1987                 } 
1988                 $msglist->attr("style",$style);
1989         }       
1990         return 1;
1991 }       
1992 #
1993 # Удаляет из списка элемент, если он не последий. Если последний -
1994 # скрывает весь список. Элемент можно указывать как ссылкой на объект 
1995 # HTML::Element, так и значением атрибута id 
1996 #
1997 sub delete_from_list {
1998         my ($tree,$listclass,$itemclass,$item) = @_;
1999         my $list = $tree->look_down(class =>$listclass);
2000         ($item) = $list->look_down(id =>$item) unless ref($item);
2001         return undef unless $item;
2002         my (@items)=$tree->look_down(class=>$itemclass); 
2003         if (@items == 1) {
2004                 hide_list($tree,$listclass);
2005         } else {
2006                 $item->delete;
2007         }
2008         return 1;
2009 }       
2010 #
2011 # Найти все элементы, удоволетворяющие заданному критерию и подставить в
2012 # них указанные атрибуты
2013
2014 # Параметры 1. Дерево (класса HTML::Element)
2015 # 2. Запрос - ссылка на список вида атрибут=>значение. 
2016 #    Этот список будет непосредственно передан в
2017 #    HTML::Element::look_down
2018 # 3. Далее пары имя-атрибута, значение. Если вместо имени атрибута
2019 #    использовать слово _content, заменено будет содержимое элемента.
2020 #    Значение для _content - ссылка на HTML::Element. Если там строка,
2021 #    она будет вставлена как одиночный текстовый узел.
2022 # 4. Возвращает число выполненных подстановок (0, если искомых элементов   
2023 #   не найдено.
2024 #
2025 sub substinfo {
2026         my ($tree,$query,@attrs) = @_;
2027         my $count;
2028         foreach my $element ($tree->look_down(@$query)) {
2029                 $count ++;
2030                 while (@attrs) {
2031                         my $attr = shift @attrs;
2032                         my $value = shift @attrs;
2033                         if ($attr eq "_content") {
2034                                 $element->delete_content;
2035                                 $element->push_content($value);
2036                         } else {        
2037                                 $element->attr($attr,$value);
2038                         }       
2039                 }       
2040         }
2041         return $count;  
2042 }
2043 #
2044 # newlistelement($tree,$elementclass,$listclass) 
2045 #
2046 # Если список с указанным классом скрыт, раскрывает его и возвращает 
2047 # (единственный) элемент 
2048 sub newlistelement {
2049         my ($tree,$element,$list) =@_;
2050         my $msglist = $tree->look_down("class"=>$list);
2051         if ($msglist) {
2052                 my $style = $msglist->attr("style");
2053                 if ($style && $style =~ s/display: none;//) {
2054                         $msglist->attr("style",$style);
2055                         return $msglist->look_down(class=>$element);
2056                 } else {
2057                         my $template = $msglist->look_down("class"=>$element);
2058                         return undef unless $template;
2059                         my $newitem=$template->clone;
2060                         $template->parent->push_content($newitem);
2061                         return $newitem;
2062                 }
2063         } else {
2064                 return undef;
2065         }
2066 }       
2067
2068 sub escapeHTML {
2069         local $_ = shift;
2070         s/\&/&amp;/g;
2071         s/\</&lt;/g;
2072         s/\>/&gt;/g;
2073         return $_;
2074 }