]> www.wagner.pp.ru Git - oss/stilllife.git/blob - forum/forum
*** empty log message ***
[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 $prev = $msglist->look_down("class"=>"message");
1155                         $prev->preinsert($msg); 
1156                 }
1157         }
1158         savetree($forum->{forumroot}."/recent.html",$tree,$lockfd);
1159 }       
1160 #
1161 # Обработка операции создания новой темы. 
1162 #
1163
1164 sub new_topic {
1165         my ($form,$cgi,$forum) = @_;
1166         #
1167         # Проверяем корректность urlname и прочих полей
1168         #
1169         my $urlname;
1170         if (!$cgi->param("urlname")) {
1171                 $urlname = get_uid($forum);
1172         } else {        
1173                 $urlname=$1 if $cgi->param("urlname") =~ /^([-\w]+)$/;
1174                 form_error($form,$cgi,$forum,"Некорректные символы в urlname.
1175                 Допустимы только латинские буквы, цифры и минус") unless $urlname; 
1176         }
1177         if (!-d $path_translated) {
1178                 show_error($forum,"Операция $form может быть вызвана только со
1179                 страницы форума");
1180         }       
1181         my $filename = "$path_translated/$urlname.html";
1182         if (-f $filename) {
1183                 form_error($form,$cgi,$forum,"Тема с urlname $urlname уже
1184                 существует");
1185         }       
1186         my $url = $cgi->path_info."/$urlname.html";
1187                 $url =~ s/\/+/\//g;
1188         if (!$cgi->param("title")) {
1189                 form_error($form,$cgi,$forum,"Тема должна иметь непустое название");
1190         }       
1191         #
1192         # Создаем собственно тему
1193         #
1194         my $tree = gettemplate($forum,"topic",$url);
1195     # Заполнить название и аннотацию 
1196         my $abstract = input2tree($cgi,$forum,"abstract");
1197         substinfo($tree,[_tag=>"meta","name"=>"description"],content=>$abstract->as_trimmed_text);
1198         substinfo($tree,[_tag=>"title"],_content=>$cgi->param("title"));
1199         my $subtree = $tree->look_down("class"=>"topic");
1200         my $creation_time=strftime("%d.%m.%Y %H:%M",localtime());
1201         if ($subtree) {
1202                 substinfo($subtree,["class"=>"title"],
1203                 _content=>$cgi->param("title"));
1204                 substinfo($subtree,["class"=>"date"],
1205                         _content=>$creation_time);
1206                 # Вставляем в страницу КОПИЮ аннотации, поскольку аннотация
1207                 # нам еще понадобится в списке тем.
1208                 substinfo($subtree,["class"=>"abstract"],_content=>$abstract->clone);   
1209                 substitute_user_info($subtree,$forum);  
1210         } else {
1211                 substinfo($tree,["class"=>"title"],
1212                 _content=>$cgi->param("title"));
1213         }       
1214         # Скрыть список сообщений.
1215         hide_list($tree,"messagelist");
1216         savetree($filename,$tree,undef);
1217         $tree->destroy;
1218         #
1219         # Добавляем элемент в список тем текущего форума
1220         #
1221
1222         my $lockfd;
1223         ($tree,$lockfd)=gettree($path_translated."/".$forum->{"indexfile"});
1224         my $newtopic = newlistelement($tree,"topic","topiclist");
1225         substinfo($newtopic,[_tag=>"a","class"=>"title"],
1226         _content=>$cgi->param("title"), href=>"$urlname.html");
1227         substinfo($newtopic,["class"=>"date"], _content=>$creation_time);
1228         substinfo($newtopic,["class"=>"abstract"],_content=>$abstract); 
1229         substitute_user_info($newtopic,$forum); 
1230         $newtopic->attr("id",$urlname);
1231         my $controlform = $newtopic->look_down(_tag=>"form",class=>"topicinfo");
1232         if ($controlform) {
1233                 $controlform->attr("action"=>$cgi->url(-absolute=>1,-path_info=>0,
1234                         -query_string=>0).$url);
1235                 substinfo($controlform,[_tag=>"input",name=>"author"],value=>
1236                         $forum->{authenticated}{user});
1237         }               
1238         update_topic_list($forum,$newtopic,0,$creation_time);
1239         savetree($path_translated."/".$forum->{"indexfile"},$tree,$lockfd);
1240         record_statistics($forum,"topic");
1241         forum_redirect($cgi,$forum,$cgi->url(-base=>1).$url);
1242 }
1243
1244 sub new_forum {
1245         my ($form,$cgi,$forum) = @_;
1246         #
1247         # Проверяем корректность urlname и прочих полей
1248         #
1249         my $urlname;
1250          if (!$cgi->param("urlname")) {
1251                 form_error($form,$cgi,$forum,"Форуму необходимо задать непустое urlname");
1252          }     
1253          if ($cgi->param("urlname") eq ".") {
1254                 $urlname = "."
1255          } else {       
1256                 $urlname=$1 if $cgi->param("urlname") =~ /^([-\w]+)$/ ;
1257                 form_error($form,$cgi,$forum,"Некорректные символы в urlname.
1258                         Допустимы только латинские буквы, цифры и минус") unless $urlname; 
1259         }
1260         if (!-d $path_translated) {
1261                 show_error($forum,"Операция $form может быть вызвана только со
1262                 страницы форума");
1263         }       
1264         my $newname = "$path_translated/$urlname";
1265         $newname=$path_translated if ($urlname eq ".");  
1266         my $filename = "$newname/$forum->{indexfile}";
1267         if (-f $filename) {
1268                 form_error($form,$cgi,$forum,"Форум $urlname уже существует");
1269         }       
1270         if (!$cgi->param("title")) {
1271                 form_error($form,$cgi,$forum,"Форум должен иметь непустое название");
1272         }
1273         mkdir $newname unless -d $newname;
1274         #
1275         # Сохраняем логотип
1276         #
1277         my ($logo_name,$logo_width,$logo_height);
1278         if ($cgi->param("logo")) {
1279                 my $userpath = $cgi->param("logo");
1280                 $logo_name="logo.".lc($1) if $userpath=~/\.([^.]+)$/;
1281                 my $in = $cgi->upload("logo");
1282                 if (!$in) {
1283                         show_error($forum,"Ошибка при загрузке картинки $userpath");
1284                         exit;
1285                 }       
1286                 my $out;
1287                 open $out,">$newname/$logo_name";
1288                 binmode $out,":bytes";
1289                 local $/=undef;
1290                 my $data = <$in>;
1291                 print $out $data;
1292                 close $in;
1293                 close $out;
1294                 ($logo_width,$logo_height) = imgsize("$newname/$logo_name");
1295         } else {
1296                 $logo_name = $forum->{"templatesurl"}."/1x1.gif";
1297                 $logo_width = 1;
1298                 $logo_height=1;
1299         }       
1300
1301
1302         #
1303         # Создаем собственно оглавление форума
1304         #
1305         
1306         my $url = $cgi->path_info."/$urlname";
1307         $url= $cgi->path_info if $urlname eq ".";
1308         $url =~ s/\/+/\//g;
1309         my $tree = gettemplate($forum,"forum",$url);
1310         # Удалить элементы, который присутствуют только на главной странице
1311         if ($urlname ne ".") {
1312                 for my $element ($tree->look_down("class"=>"top-page")) {
1313                         $element->delete;
1314                 }       
1315         }
1316     # Заполнить название и аннотацию 
1317         my $abstract = input2tree($cgi,$forum,"abstract");
1318         substinfo($tree,[_tag=>"meta","name"=>"description"],content=>$abstract->as_trimmed_text);
1319         substinfo($tree,[_tag=>"title"],_content=>$cgi->param("title"));
1320         my $subtree = $tree->look_down("class"=>"annotation")
1321                 or show_error($forum,"В шаблоне форума отсутствует класс annotation");
1322         my $creation_time=strftime("%d.%m.%Y %H:%M",localtime());
1323                 substinfo($subtree,["class"=>"title"],
1324                 _content=>$cgi->param("title"));
1325                 substinfo($subtree,["class"=>"date"],
1326                         _content=>$creation_time);
1327                 # Вставляем в страницу КОПИЮ аннотации, поскольку аннотация
1328                 # нам еще понадобится в списке тем.
1329                 substinfo($subtree,["class"=>"abstract"],_content=>$abstract->clone);   
1330                 substitute_user_info($subtree,$forum);  
1331         substinfo($subtree,[_tag=>"img","class"=>"logo"],
1332                 src=> $logo_name, width=>$logo_width, height=>$logo_height);
1333         # Скрыть списки подфорумов и тем .
1334         hide_list($tree,"forumlist");
1335         hide_list($tree,"topiclist");
1336         if ($urlname eq ".") {
1337                 for my $link_up ($tree->look_down(_tag=>"a",href=>"..")) {
1338                         $link_up->delete;
1339                 }
1340         }       
1341         savetree($filename,$tree,undef);
1342         $tree->destroy;
1343         #
1344         # Добавляем элемент в список тем текущего форума
1345         #
1346         if ($urlname ne ".") {
1347         my $lockfd;
1348         ($tree,$lockfd)=gettree($path_translated."/".$forum->{"indexfile"});
1349         my $newforum = newlistelement($tree,"forum","forumlist");
1350         substinfo($newforum,[_tag=>"a","class"=>"title"],
1351         _content=>$cgi->param("title"), href=>"$urlname/");
1352         substinfo($newforum,["class"=>"date"], _content=>$creation_time);
1353         substinfo($newforum,["class"=>"abstract"],_content=>$abstract); 
1354         substinfo($newforum,[_tag=>"img","class"=>"logo"],src=>"$urlname/$logo_name",
1355                 width=>$logo_width,height=>$logo_height);
1356         substitute_user_info($newforum,$forum); 
1357         $newforum->attr("id",$urlname);
1358         my $controlform = $newforum->look_down(_tag=>"form",class=>"foruminfo");
1359         if ($controlform) {
1360                 $controlform->attr("action"=>$cgi->url(-absolute=>1,-path_info=>0).
1361                 $url);
1362                 substinfo($controlform,[_tag=>"input",name=>"author"],value=>
1363                         $forum->{authenticated}{user});
1364         } 
1365         savetree($path_translated."/".$forum->{"indexfile"},$tree,$lockfd);
1366         record_statistics($forum,"forum");
1367         } else {
1368         # Создаем тему для "свежих реплик"
1369          my $recent = gettemplate($forum,"topic",$url."/recent.html");  
1370         # remove reply link from page itself
1371         for my $link ($recent->look_down(_tag =>"a", href=>qr/reply=/)) {
1372                 $link->delete;
1373         }       
1374          substinfo($recent,["_tag"=>"title"],$cgi->param("title").": Свежие сообщения");
1375                 substinfo($recent,["class"=>"title"],
1376                 _content=>$cgi->param("title"). ": Свежие сообщения");
1377                 hide_list($recent,"messagelist");
1378                 savetree($path_translated."/recent.html",$recent,undef);
1379
1380         }
1381
1382         forum_redirect($cgi,$forum,$cgi->url(-base=>1).$url);
1383 }
1384         
1385 #---------------------------------------------------------- 
1386 # База пользователей и права доступа
1387 #----------------------------------------------------------
1388 #
1389 # Записывает в базу данных пользователей, сколько каких объектов 
1390 # создал текущий пользователь
1391 #
1392 sub record_statistics {
1393         my ($forum,$type) = @_;
1394         my $user = $forum->{authenticated}{user};
1395         my %base;
1396         dbmopen %base,datafile($forum,"passwd"),0664;
1397         my $userinfo = thaw($base{$user});
1398         $userinfo->{$type."s"}++;
1399         $userinfo->{"last_$type"}=time;
1400         $base{$user} = freeze($userinfo);
1401         dbmclose %base;
1402 }
1403 #
1404 # читает файлы прав доступа в дереве форума, и возвращает
1405 # статус текущего пользователя (undef - аноним, banned, normal,
1406 # moderator или admin
1407
1408 sub getrights {
1409         my ($cgi,$forum) = @_;
1410         if (!$forum->{authenticated}) {
1411                 return undef;
1412         }       
1413         my $user = $forum->{authenticated}{user};
1414         my $dir = $path_translated;
1415         $dir =~s/\/$//;
1416         $dir =~s/\/[^\/]+$// if (!-d $dir);
1417         my $f;
1418         my $user_status = "normal";
1419         LEVEL:
1420         while (length($dir)) {  
1421                 if (-f "$dir/perms.txt") {
1422                         open $f,"<","$dir/perms.txt";
1423                         my $status = undef;
1424                         while (<$f>) {
1425                                 if (/^\[\s*(admins|moderators|banned)\s*\]/) {
1426                                         $status = $1;
1427                                 } else {
1428                                         chomp;
1429                                         if  ($user eq $_ && defined $status) {
1430                                                 if ($status eq "banned") {
1431                                                         return $status;
1432                                                 } 
1433                                                 if ($status eq "admins" ) {
1434                                                         return "admin";
1435                                                 }
1436                                                 $user_status = "moderator";
1437                                         }
1438                                 }       
1439                         }
1440                         close $f;
1441                         last LEVEL if  -f "$dir/.forum";
1442                 }       
1443                 # Strip last path component.
1444                 $dir =~s/\/[^\/]+$// 
1445         }               
1446         return $user_status;
1447
1448 }               
1449
1450
1451
1452 #------------------------------------------------------------------
1453 # Работа с файлами и идентификторами
1454 #------------------------------------------------------------------
1455
1456 #
1457 # Залочить файл и получить его распрасенное представление.
1458 # Возвращает пару ($tree,$lockfd)
1459
1460 sub gettree {
1461         my $filename = shift;
1462         my $f;
1463         open $f,"<",$filename or return undef;
1464         flock $f, LOCK_EX;
1465         my $tree = treefromfile($f);
1466         $tree->parse_file($f);
1467         return ($tree,$f);
1468 }       
1469 #
1470 # Сохранить дерево и закрыть lockfd.
1471 #
1472 #
1473
1474 sub savetree {
1475         my ($filename,$tree,$lockfd) = @_;
1476         my $f;
1477         open $f,">",$filename . ".new" or return undef;
1478         print $f output_html($tree);
1479         close $f;
1480         # FIXME - только для POSIX.
1481         unlink $filename;
1482         rename $filename.".new",$filename;
1483         close $lockfd if defined($lockfd);
1484 }       
1485 #
1486 # Cериализовать HTML-документ с DOCTYPE (workaround вокруг баги в
1487 # HTML::TreeBuilder)
1488 #
1489 sub output_html {
1490         my $tree=shift;
1491         return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'.
1492         $tree->as_HTML("<>&");
1493 }       
1494 #
1495 # Читает шаблон и подготавливает его к размещению по указанной URL.
1496 # Если url не указана, считается что шаблон будет показан как результат
1497 # текущего http-запроса.
1498
1499 sub gettemplate {
1500         my ($forum, $template,$url) = @_;
1501         $url =~ s/\/+/\//g if defined $url;
1502         my $filename=$forum->{"templates"}."/$template.html";
1503         if (! -r $filename) {
1504                 show_error($forum,"Нет шаблона $template");
1505                 exit;
1506         }
1507         my $tree = treefromfile($filename);
1508         fix_forum_links($forum,$tree,$url);
1509         return $tree;
1510 }       
1511
1512 #
1513 # Создает объект HTML::TreeBuilder и выставляет ряд опций.
1514 #
1515
1516 sub make_tree {
1517         my $tree = HTML::TreeBuilder->new;
1518         # Set some options for treebuilder
1519         # Comments are neccessary to convert HTML back to BBCode 
1520         $tree->store_comments(1);
1521         # Avoid converting html into one long-long string
1522         $tree->ignore_ignorable_whitespace(0);
1523         $tree->no_space_compacting(1);
1524         $tree->p_strict(1);
1525         return $tree;
1526 }       
1527
1528 sub treefromfile {
1529         my ($f) = shift;
1530         my $tree = make_tree();
1531         $tree->parse_file($f);
1532         return $tree;
1533 }       
1534 #
1535 # Получает уникальный числовой идентификатор.
1536
1537 sub get_uid {
1538         my $forum = shift;
1539         my $f;
1540         open $f,"+<",datafile($forum,"sequence") or 
1541         flock $f,LOCK_EX;
1542         my $id=<$f> || "0";
1543         $id++;
1544         seek $f,0,0;
1545         printf $f "%8s\n",$id;
1546         close $f;
1547         $id=~/(\d+)/;
1548         return sprintf ("%08s",$1);
1549 }
1550 # --------------------------------------------------------------------
1551 #  OpenID registration
1552 # -------------------------------------------------------------------
1553 sub create_openid_consumer {
1554         my ($cgi,$forum) = @_;
1555         return Net::OpenID::Consumer ->new(
1556                 ua => LWP::UserAgent->new( agent => "Stilllife/1.0"),
1557                 args => $cgi,
1558                 consumer_secret=>"X9RWPo0rBE7yLja6VB3d",
1559                 required_root => $cgi->url(-base=>1));
1560 }               
1561
1562 # openidstart - вызывается когда обнаружено что текущее имя
1563 # пользователя, пытающегося аутентифицироваться, содержит http://
1564 #  
1565 #
1566
1567 sub openidstart {
1568         my ($cgi,$forum,$openidurl) = @_;
1569         #
1570         # Fix duplicated http:// which can be produced by our sprintf based
1571         # login system
1572         #
1573         $openidurl=~s!^http://http://!http://!;
1574         my $csr = create_openid_consumer($cgi,$forum);
1575         my $claimed_identity=$csr->claimed_identity($openidurl);
1576         if (!defined $claimed_identity) {
1577                 show_error($forum,"Указанная URL $openidurl не является OpenId");            
1578                 exit;
1579         }
1580         $cgi->param("openidvfy",1);
1581         $cgi->delete("user");
1582         $cgi->delete("openidsite");
1583         $cgi->delete("password");
1584         my $check_url = $claimed_identity->check_url(
1585                 return_to=> $cgi->url(-full=>1,-path_info=>1,-query=>1),
1586                 trust_root=> $cgi->url(-base=>1));
1587         print $cgi->redirect(-location=>$check_url);
1588         exit;
1589 }       
1590 #
1591 # Вызывается при редиректе от openid producer-а. Проверяет, что
1592 # удаленный сервер подтвердил openid и вызывает операцию для которой
1593 # (либо возврат на исходную страницу при операции login, либо постинг
1594 # реплики) 
1595 #
1596 sub openid_verify {
1597         my ($cgi,$forum) = @_;
1598         my $csr  = create_openid_consumer($cgi,$forum);
1599         if (my $setup_url = $csr->user_setup_url) {
1600                 print $cgi->redirect(-location=>$setup_url);
1601                 exit;
1602         } elsif ($csr->user_cancel) {
1603                 show_error($forum,"Ваш openid-сервер отказался подтвержать вашу
1604                 идентичность");
1605                 exit;
1606         } elsif (my $vident = $csr->verified_identity) {
1607                 #Успешная аутентификация.         
1608                 #Создаем сессию
1609                 my $user = $vident->url; 
1610                 # Remove trailing slash from URL if any
1611                 $user=~s/\/$//;
1612                 my %userbase;
1613                 dbmopen %userbase,datafile($forum,"passwd"),0664;
1614                 my $username = $user; 
1615                 $username =~ s/^http:\/\///;
1616                 if (!$userbase{$username}) {
1617                         # Тащим foaf, если получится
1618                         my %info=get_foaf($csr->ua,$vident->declared_foaf);
1619                         if (ref($info{'avatar'}) eq "HASH" ) {
1620                                 delete $info{'avatar'}{'type'};
1621                         }       
1622                         $info{"openiduser"}=1;
1623                         $info{"registered"}=time; 
1624                         print STDERR "forum $forum info ".\%info."\n";
1625                         set_default_user_attrs($forum,\%info);
1626                         $info{'status'} = $forum->{openid_status} if $forum->{openid_status};
1627                         $forum->{authenticated}=\%info;
1628                         $userbase{$username} = freeze(\%info);
1629                 } else {
1630                         $forum->{authenticated} = thaw ($userbase{$username});
1631                 }
1632                 dbmclose %userbase;
1633                 if (defined $forum->{denied_status} && 
1634                         ($forum->{authenticated}{status} eq $forum->{denied_status})) {
1635                         show_error($forum,"Вход пользователя $username в систему заблокирован"); 
1636                 }       
1637                 $forum->{"authenticated"}{"user"} = $username;
1638                 newsession(undef,$forum,$user);
1639                 # Если указан параметр reply, вызываем обработку реплики
1640                 if ($cgi->param("reply")) {     
1641                         reply("reply",$cgi,$forum);
1642                         exit;
1643                 }       
1644                 #Иначе, возвращаемся на исходную страницу
1645                 forum_redirect($cgi,$forum,undef);
1646         }       else {
1647                 show_error($forum,"Ошибка OpenId аутентификации");
1648                 exit;
1649         }       
1650 }
1651
1652 sub get_foaf {
1653         my ($ua,$foaf_url) = @_; 
1654         my $response = $ua->get($foaf_url);
1655         unless ($response->is_success) {
1656                 print STDERR "Error geting foaf from $foaf_url\n";
1657                 return ();
1658         }       
1659         my $foaf = $response->content;
1660         my %info = foaf_parse($foaf);
1661         if ($info{avatar}) {
1662                 $response = $ua->get($info{avatar});
1663                 if ($response->is_success) {
1664                         my $image = $response->content;
1665                         my ($w,$h,$type) = imgsize(\$image);
1666                         $info{avatar}={width=>$w,height=>$h,type=>$type,src=>$info{avatar}};
1667                 } else {
1668                         print STDERR "Error getting $info{avatar}: ".$response->status_line,"\n";
1669                 }       
1670         }       
1671         return %info;
1672 }
1673 sub foaf_parse {
1674         my $foaf = shift;
1675         my ($starttag) = $foaf =~ /<(\w+(:\w+)?[^>]+)>/sg;
1676         my %ns = reverse ($starttag =~ /xmlns:(\w+)="([^"]+)"/sg);
1677         my $foaf_prefix = $ns{"http://xmlns.com/foaf/0.1/"};
1678         my $rdf_prefix = $ns{"http://www.w3.org/1999/02/22-rdf-syntax-ns#"};
1679         my ($userpic) = $foaf=~/<$foaf_prefix:img[^>]* $rdf_prefix:resource="([^"]+)"/s;
1680         my @info;
1681         push @info, avatar =>$userpic if $userpic;
1682         my ($icq) = $foaf =~/<$foaf_prefix:icqChatID>([^<]*)<\/$foaf_prefix:icqChatID>/s;
1683         push @info, icq => $icq if ($icq);
1684         my ($jabber) = $foaf =~/<$foaf_prefix:jabberID>([^<]*)<\/$foaf_prefix:jabberID>/s;
1685         push @info, jabber => $jabber if ($jabber);
1686         return @info;
1687 }
1688 #-----------------------------------------------------------------
1689 # Обработка форматированных текстовых полей
1690 #-----------------------------------------------------------------
1691
1692 sub input2tree {
1693         my ($cgi,$forum,$field_name) = @_;
1694         my $format = $cgi->param($field_name."_format");
1695         my $text = $cgi->param($field_name);
1696         if ($format eq "bbcode") {
1697                 my $parser = HTML::BBReverse->new(); 
1698                 $text="<div class=\"bbcode\">".$parser->parse($text)."</div>";
1699         } elsif ($format eq "text") {
1700                 $text=~s/\r?\n\r?\n/<\/p><p class=\"text\">/;
1701                 $text=~s/\r?\n/<br>/;
1702                 $text = "<div><p class=\"text\">".$text."</p></div>";
1703         } 
1704         my $txtree = str2tree($text);
1705         for my $badtag
1706         ("script","style","head","html","object","embed","iframe","frameset","frame",
1707         ($forum->{forbid_tags}?split(/\s*,\s*/,$forum->{forbid_tags}):())) {
1708                 for my $element ($txtree->find_by_tag_name($badtag)) {
1709                         $element->delete() if defined $element;
1710                 }       
1711         }       
1712         # Проверяем на наличие URL-ок не оформленных ссылками.
1713         return $txtree;
1714 }       
1715
1716
1717
1718 sub str2tree {
1719         my ($data)=@_;
1720         my $tree = make_tree();
1721         # Set parser options here
1722         $tree->parse("<html><body><div>$data</div></body></html>");
1723         $tree->eof;
1724         my $element=$tree->find("body");
1725         while (($element =($element->content_list)[0])->tag ne "div") {
1726         }
1727         $element->detach;
1728         $tree->destroy;
1729         return $element;
1730 }       
1731
1732 sub tree2str {
1733         my ($tree)=@_;
1734         return $tree->as_HTML("<>&");
1735 }
1736
1737 #------------------------------------------------------------------------
1738 # Подстановка в дереве
1739 #------------------------------------------------------------------------
1740 # Находит 
1741 # элемент указанного класса и удаляет display: none из его атрибута
1742 # style. Возвращает 1, если элемент был раскрыт, и 0, если он и до этого 
1743 # был видимым.
1744 sub unhide_list {
1745         my ($tree,$class) = @_;
1746         my $msglist = $tree->look_down("class"=>$class);
1747         if ($msglist) {
1748                 my $style = $msglist->attr("style");
1749                 if ($style && $style =~ s/display: none;//) {
1750                         $msglist->attr("style",$style);
1751                         return 1;
1752                 } else {
1753                         return 0;
1754                 }       
1755         } 
1756 }       
1757 #
1758 # Находит первый элемент указанного класса, и приписывает ему display:
1759 # none в style.
1760 #
1761 sub hide_list {
1762         my ($tree,$class)=@_;
1763         my $msglist = $tree->look_down("class"=>$class);
1764         return undef unless $msglist;
1765         if (!$msglist->attr("style")) {
1766                 $msglist->attr("style","display: none;");
1767         } else {
1768                 my $style = $msglist->attr("style");
1769                 unless ($style=~ s/\bdisplay:\s+\w+\s*;/display: none;/) {
1770                         $style .= "display: none;";
1771                 } 
1772                 $msglist->attr("style",$style);
1773         }       
1774         return 1;
1775 }       
1776 #
1777 # Найти все элементы, удоволетворяющие заданному критерию и подставить в
1778 # них указанные атрибуты
1779
1780 # Параметры 1. Дерево (класса HTML::Element)
1781 # 2. Запрос - ссылка на список вида атрибут=>значение. 
1782 #    Этот список будет непосредственно передан в
1783 #    HTML::Element::look_down
1784 # 3. Далее пары имя-атрибута, значение. Если вместо имени атрибута
1785 #    использовать слово _content, заменено будет содержимое элемента.
1786 #    Значение для _content - ссылка на HTML::Element. Если там строка,
1787 #    она будет вставлена как одиночный текстовый узел.
1788 # 4. Возвращает число выполненных подстановок (0, если искомых элементов   
1789 #   не найдено.
1790 #
1791 sub substinfo {
1792         my ($tree,$query,@attrs) = @_;
1793         my $count;
1794         foreach my $element ($tree->look_down(@$query)) {
1795                 $count ++;
1796                 while (@attrs) {
1797                         my $attr = shift @attrs;
1798                         my $value = shift @attrs;
1799                         if ($attr eq "_content") {
1800                                 $element->delete_content;
1801                                 $element->push_content($value);
1802                         } else {        
1803                                 $element->attr($attr,$value);
1804                         }       
1805                 }       
1806         }
1807         return $count;  
1808 }
1809 #
1810 # newlistelement($tree,$elementclass,$listclass) 
1811 #
1812 # Если список с указанным классом скрыт, раскрывает его и возвращает 
1813 # (единственный) элемент 
1814 sub newlistelement {
1815         my ($tree,$element,$list) =@_;
1816         my $msglist = $tree->look_down("class"=>$list);
1817         if ($msglist) {
1818                 my $style = $msglist->attr("style");
1819                 if ($style && $style =~ s/display: none;//) {
1820                         $msglist->attr("style",$style);
1821                         return $msglist->look_down(class=>$element);
1822                 } else {
1823                         my $template = $msglist->look_down("class"=>$element);
1824                         return undef unless $template;
1825                         my $newitem=$template->clone;
1826                         $template->parent->push_content($newitem);
1827                         return $newitem;
1828                 }
1829         } else {
1830                 return undef;
1831         }
1832 }