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