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