#!/usr/bin/perl -T # # Stil Life forum. Copyright (c) by Victor B. Wagner, 2008 # This program distributed under GNU Affero General Public License v3 or # above # http://www.gnu.org/licenses/agpl.html # # Вкратце: Если вы используете этот скрипт на своем сайте, Вы обязаны # сделать доступным его исходный текст. В частности, если Вы внесли # какие-либо изменения, вы должны эти изменения опубликовать. # # Home site of this program http://vitus.wagner.pp.ru/stilllife # use strict; use warnings; use Fcntl qw(:DEFAULT :flock); use CGI; use HTML::TreeBuilder; use Storable qw(freeze thaw); use Date::Parse; use Email::Valid; use Image::Size; use HTML::BBReverse; use POSIX; use LWP::UserAgent; use Net::OpenID::Consumer; # # Набор поддерживаемых действий. Хэш вида # "имя поля в запросе" => "функция обработчик" # my @actions = ( {name=>"openidvfy",GET=>\&openid_verify,POST=>\&openid_verify}, {name=>"logout",GET=>\&logout,POST=>\&logout}, {name=>"reply", POST => \&reply, GET=>\&show_template,rights=>"login"}, {name=>"edit", POST => \&edit_item, GET=>\&show_editable,rights=>"author"}, {name=>"delete",POST => \&delete_item, GET=>\&delete_item,rights=>"author"}, {name=>"move",POST => \&move_item, GET=>\&show_move_dest,rights=>"moderator"}, {name=>"newtopic",POST=> \&new_topic,GET=>\&show_template,rights=>"normal"}, {name=>"newforum",POST=> \&new_forum,GET=>\&show_template,rights=>"moderator"}, {name=>"login",POST => \&login,GET=>\&show_template}, {name=>"register",POST=>\®ister,GET=>\&show_template}, {name=>"profile",POST=>\&profile,GET=>\&show_profile,rights=>"normal"}, {name=>"setrights",POST=>\&set_rights,GET=>\&show_rights,rights=>"admin"}, {name=>"reapply",POST=>\&apply_templates,GET=>\&apply_templates,rights=>"admin"} ); # our $path_translated; # Untainted value of PATH_TRANSLATED env var my $cgi = new CGI; print STDERR "--------------------\n"; my $forum=get_forum_config(); authorize_user($cgi,$forum); my $method = $cgi->request_method; $method = "GET" if ($method eq "HEAD"); for my $action (@actions) { if ($cgi->param($action->{name})) { if (allow_operation($action,$cgi,$forum)) { $action->{$method}->($action->{name},$cgi,$forum); } elsif (!$forum->{"authenticated"}) { $cgi->param("returnto",$cgi->url(-full=>1)); show_template("login",$cgi,$forum); } else { show_error($forum,"У Вас нет прав на выполнение этой операции") } } } if (index($path_translated,$forum->{userdir})==0) { show_user_page($cgi,$forum); } show_error($forum,"Некорректный вызов скрипта. Отсутствует параметр действия"); #-------------------------------------------------------------- #-------- Чтение конфигурационного файла и связанные с этим действия #------------------------------------------------------------------ # # Преобразование пути в файловой системе сервера в путь в URL # sub dir2url { my ($cgi,$dir) = @_; my $prefix=""; my $pos=rindex $ENV{'PATH_TRANSLATED'},$ENV{'PATH_INFO'}; if ($pos <0 && $ENV{'PATH_INFO'}=~m!(/\~\w+)/!) { $prefix .=$1; $pos = rindex($ENV{'PATH_TRANSLATED'},substr($ENV{'PATH_INFO'},length($1))); } if ($pos <0) { show_error({},"Ошибка конфигурации форума. Не удается определить алгоритм преобразования директори в URL\n". "PATH_INFO=$ENV{PATH_INFO}\n". "PATH_TRANSLATER=$ENV{'PATH_TRANSLATED'}"); } my $root = substr($ENV{'PATH_TRANSLATED'},0,$pos); if (substr($dir,0,length($root)) ne $root) { show_error({},"Ошибка конфигурации форума. Не удается преобразовать имя директории $dir в url\n". "PATH_INFO=$ENV{PATH_INFO}\n". "PATH_TRANSLATER=$ENV{'PATH_TRANSLATED'}"); } return $prefix.substr($dir,length($root)); } # # Поиск файла .forum вверх по дереву от $path_translated # Значение PATH_TRANSLATED считаем безопасным - наш web-сервер нам не # враг. # Возвращает список имя,значение, имя, значение который прививается в # хэш # sub get_forum_config { $path_translated = $1 if $ENV{PATH_TRANSLATED}=~/^(.+)$/; $path_translated=~s/\/+$//; my @path=split("/",$path_translated); while (@path>1) { if (-r (my $config=join("/",@path,".forum")) ) { open F,"<",$config; my %config; while () { s/#.*$//; #Drop comments; $config{$1}=$2 if /(\w+)\s*=\s*(\S.*)$/; } close F; # # Переменная forumtop - это URL того места, где находится # файл .forum $config{"forumtop"} = dir2url($cgi,join("/",@path)); $config{"forumroot"} = join("/",@path); # Если в конфиге отсутствует переменная templates, но # рядом с конфигом присутствует директория templates, # то шаблоны там. # if (! exists $config{"templates"} && -d (my $filename = join("/",@path,"templates"))) { $config{"templates"} = $filename; } $config{"templatesurl"} = dir2url($cgi,$config{"templates"}) unless exists $config{"templatesurl"}; # # То же самое - параметр userdir и директория users # if (! exists $config{"userdir"} && -d (my $filename = join("/",@path,"users"))) { $config{"userdir"} = $filename; } $config{"userurl"} = dir2url($cgi,$config{"userdir"}); # # Если нет ссылки в конфиге на файл паролей или он не # существует, выдаем ошибку. С офоромлением, так как шаблоны # у нас уже есть if (!exists $config{"datadir"}) { show_error(\%config,"В конфигурации форума не указана директория данных "); exit; } if (!-d $config{"datadir"}) { show_error(\%config,"В конфигурации форума указана несуществующая директория данных "); exit; } # # Некоторые умолчания # $config{"authperiod"}="+1M" if (! exists $config{"authperiod"}); $config{"renewtime"} = "86000" if (!exists $config{"renewtime"}); $config{"replies_per_page"} = 50 if (!exists $config{"replies_per_page"}); $config{"indexfile"} = "index.html" if (!exists $config{"indexfile"}); return \%config; } pop @path; } # # Выводим ошибку 404 без осмысленного оформления, так как данных форума # мы не нашли print "Status: 404\nContent-Type: text/html; charset=utf-8\n\n", "Форум не обнаружен", "Форум не найден", "

Хвост URL, указанный при вызове скрипта показывает не на форум

", # To make IE think this page is user friendly "\n"; exit; } # # Вывод сообщения об ошибке по шаблону форума # Шаблон должен содержать элемент с классом error. # sub show_error { my ($cfg,$msg) = @_; if ( -r $cfg->{"templates"}."/error.html") { my $tree = treefromfile($cfg->{"templates"}."/error.html"); my $node= $tree->find_by_attribute('class','error'); my $body; if (!$node) { $body = $tree->find_by_tagname('body'); $body->push_content($node = new HTML::Element('div','class'=>'error')); } $node->delete_content; $node->push_content($msg); print $cgi->header(-type=>'text/html',-charset=>'utf-8'); print output_html($tree); } else { print $cgi->header(-type=>'text/html',-charset=>'utf-8'); print "Ошибка конфигурации форума", "

Ошибка конфигурации форума

", escapeHTML($msg),"

", "

При обработке этой ошибки не обнаружен шаблон сообщения об ошибке

"; } exit; } # # Вывод шаблона формы. В шаблоне должна присутстовать форма с # именем, совпадающим с именем form. Если в $cgi есть параметры, имена # которых совпадают с именами полей этой формы, их значения # подставляются # sub show_template { my $tree = prepare_template(@_); send_to_user($tree,@_); exit; } sub send_to_user { my ($tree,$form,$cgi,$forum) = @_; print $cgi->header(-type=>"text/html",-charset=>"utf-8",($forum->{cookies}?(-cookie=>$forum->{cookies}):())), output_html($tree); exit; } sub prepare_template { my ($form,$cgi,$forum) = @_; my $tree = gettemplate($forum,$form,$ENV{'PATH_INFO'}); # Находим форму с классом $form my $f = $tree->look_down("_tag","form", "name",$form); if (! defined $f) { # Если не нашли - ругаемся show_error($forum,"Шаблон для операции $form не содержит формы с именем $form"); exit; } $cgi->delete('password'); if (!$cgi->param("returnto")) { $cgi->param("returnto", $cgi->referer||$cgi->url(-absolute=>1,-path_info=>1)); } if (!$cgi->param($form)) { $cgi->param($form,1); } # # Если ранее была выставлена ошибка с помощью set_error, подставляем # сообщение в элемент с классом error # if ($forum->{error_message}) { my $errormsg = $tree->look_down("class"=>"error"); if ($errormsg) { $errormsg->delete_content(); $errormsg->push_content($forum->{error_message}); } } if ($forum->{"authenticated"}) { # Подставляем информацию о текущем пользователе если в шаблоне # это предусмотрено substitute_user_info($tree,$forum); $cgi->param("user",$forum->{"authenticated"}{"user"}) if (!defined $cgi->param("user")) } my %substituted; ELEMENT: for my $element ($f->find_by_tag_name("textarea","input","select")) { my $name = $element->attr("name"); $substituted{$name} = 1; if (defined $cgi->param($name)) { if ($element->tag eq "input") { my $type=$element->attr('type') || "text"; next ELEMENT if grep($type eq $_, "button","submit","reset"); if ($type eq "check") { if (grep($element->attr("value") eq $_,$cgi->param($name))) { $element->attr("checked",""); } else { $element->attr("checked",undef); } } elsif ($type eq "radio") { if ($element->attr("value") eq $cgi->param($name)) { $element->attr("checked",""); } else { $element->attr("checked",undef); } } else { $element->attr("value",$cgi->param($name)); } } elsif ($element->tag eq "textarea") { my $data=$cgi->param($name); if ($data=~/^<(div|p)\b/ && !defined($cgi->param($name."_format"))) { if ($data=~/^
(.*)<\/div>$/) { $cgi->param($name."_format","bbcode"); my $parser = HTML::BBReverse->new(); $data = $parser->reverse($1); # FIXME elsif ($data=~/

) } else { $cgi->param($name."_format","html"); } } $element->delete_content; $element->push_content($cgi->param($name)); } elsif ($element->tag eq "select") { for my $option ($element->find_by_tag_name("option")) { my $value = $option->attr("value") || $option->as_text(); if (grep($value eq $_, $cgi->param($name))) { $option->attr("selected","selected"); } else { $option->attr("selected",undef); } } } } } $f->attr("method","POST"); for my $required ($form,"returnto") { if (!$substituted{$required}) { my $element = new HTML::Element('input', 'type' => 'hidden', 'name' => $required, 'value'=> $cgi->param($required)); $f->push_content($element); } } return $tree; } # # Поправляет ссылки на служебные файлы и скрипты форума # sub fix_forum_links { my ($forum,$tree,$path_info) = @_; if (!defined $path_info) { $path_info = $ENV{PATH_INFO}; $path_info =~ s/\/+/\//g; } my $script_with_path = $ENV{SCRIPT_NAME}.$path_info; ELEMENT: for my $element ($tree->find_by_tag_name("form","img","link","script","a")) { my $attr; if ($element->tag eq "form") { $attr = "action"; } elsif ($element->tag eq "a"|| $element->tag eq "link") { $attr = "href"; } else { $attr ="src"; } # Обрабатываем наши специальные link rel="" my $userlist = $cgi->url(-absolute=>1, -path_info=>0,-query_string=>0).$forum->{userurl}; if ($element->tag eq "link") { if ($element->attr("rel") eq "forum-user-list") { $element->attr("href" => $userlist); next ELEMENT; } elsif ($element->attr("rel") eq "forum-script") { $element->attr("href" => $script_with_path); next ELEMENT; } } my $link = $element->attr($attr); # Абсолютная ссылка - оставляем как есть. next ELEMENT if (! defined $link || $link=~/^\w+:/ || $link eq"."||$link eq ".."); # Ссылка от корня сайта. if (substr($link,0,1) eq "/") { # Если там два слэша, заменяем их на forumtop if (substr($link,0,2) eq '//') { $element->attr($attr, $forum->{forumtop}.substr($link,1)); next ELEMENT; } # Если она не ведет на наш скрипт, не обрабатываем next ELEMENT if substr($link,0,length($ENV{SCRIPT_NAME}) ne $ENV{SCRIPT_NAME}) ; # Иначе пишем туда слово forum вместо реального имени # скрипта чтобы потом единообразно обработать $link =~ s/^[^\?]+/forum/; } if (!($link =~ s!^templates/!$forum->{templatesurl}/!) && !($link =~ s!^users/!$userlist/!) && !($link =~ s!^forum\b!$script_with_path!)) { $link = $forum->{"forumtop"}."/".$link } $element->attr($attr,$link); } } sub makeuserlink { my ($forum,$tree,$linkclass,$userinfo) = @_; my $userpage; if ($userinfo->{"openiduser"}) { $userpage = "http://".$userinfo->{"user"}; } else { $userpage = $cgi->url(-absolute=>1).$forum->{"userurl"}."/".$cgi->escape($userinfo->{"user"}); } substinfo($tree,["_tag"=>"a","class"=>$linkclass], href=>$userpage,_content=>$userinfo->{"user"}); } # # Подставляет в заданное поддерево информацию о пользователе # sub substitute_user_info { my ($tree,$forum,$user) = @_; my %userinfo; if (defined $user) { %userinfo=%$user; } else { # Если не сказано, какой юзер, то текущий. %userinfo = %{$forum->{"authenticated"}} } # # Специально обрабатываем поля user (должна быть ссылка) и avatar # (должен быть img). makeuserlink($forum,$tree,"author",\%userinfo); delete $userinfo{"user"}; if (ref $userinfo{"avatar"} eq "HASH") { substinfo($tree,["_tag"=>"img","class"=>"avatar"], %{$userinfo{'avatar'}}); } elsif ($userinfo{'avatar'}) { substinfo($tree,["_tag"=>"img","class"=>"avatar"], src=>$userinfo{"avatar"}); } else { substinfo($tree,["_tag"=>"img","class"=>"avatar"], src=>$forum->{templatesurl}."/1x1.gif", width=>1,height=>1); } for my $element ( $tree->look_down("class",qr/^ap-/)) { my $field=$1 if $element->attr("class")=~/^ap-(.*)$/; $element->delete_content(); $field =~ tr/-/_/; $userinfo{$field} = 0 if (!exists $userinfo{$field} && grep ($field eq $_,"forums","messages","topics")); if (exists $userinfo{$field}) { my $data; if ($field eq "registered" || substr($field,0,5) eq "last_") { $data = strftime("%d.%m.%Y %H:%M",localtime($userinfo{$field})) } elsif ($userinfo{$field}=~/^<\w+/) { $data = str2tree($userinfo{$field}); } else { $data = $userinfo{$field} } $element->push_content($data); $element->attr(href=>"mailto:$userinfo{$field}") if ($element->tag eq "a" && $field eq "email"); } } } # # Авторизует зарегистрированного пользователя. # 1. Проверяет куку если есть # sub authorize_user { my ($cgi,$forum) = @_; if (my $session=$cgi->cookie("slsession")) { # Пользователь имеет куку my %sessbase; dbmopen %sessbase,datafile($forum,"session"),0644; if ($sessbase{$session}) { my ($user,$expires,$ip)=split(";", $sessbase{$session}); my $user_cookie = $cgi->cookie("sluser"); if ($user_cookie ne $user && $user_cookie ne "http://".$user) { clear_user_cookies($cgi,$forum); show_error($forum,"Некорректная пользовательская сессия"); exit; } if (!defined $ip|| $ip eq $ENV{'REMOTE_ADDR'}) { my %userbase; dbmopen %userbase,datafile($forum,"passwd"),0644; if ( $userbase{$user}) { my $userinfo = thaw($userbase{$user}); delete $userinfo->{"passwd"}; $userinfo->{"user"} = $user; if ($expires-time()< $forum->{"renewtime" }) { delete $sessbase{$session}; newsession(\%sessbase,$forum,$user,$ip); } print STDERR "user $user restored session $session\n"; $forum->{"authenticated"}=$userinfo; print STDERR "authorize_user: ",$forum->{authenticated}{user}, $forum->{authenticated},"\n"; } dbmclose %userbase; } } else { clear_user_cookies($cgi,$forum); show_error($forum,"Некорректная пользовательская сессия"); exit; } dbmclose %sessbase; } } # # Возвращает путь к файлу в директории # sub datafile { my ($forum,$filename) = @_; return $forum->{"datadir"}."/".$filename; } # # Создает новую сессию для пользователя и подготавливает куку которую # сохраняет в хэше конфигурации форума # sub newsession { my ($base,$forum,$user,$bindip) = @_; if (!defined $base) { $base = {}; dbmopen %$base,datafile($forum,"session"),0644; } my $sessname; my $t = time(); my ($u,$expires,$ip); do { $sessname = sprintf("%08x",rand(0xffffffff)); if ($base->{"sessname"}) { ($u,$expires,$ip) = split ";", $base->{$sessname}; delete $base->{$sessname} if $expires < $t; } } while ($base->{$sessname}); my $cookie = $cgi->cookie(-name=>"slsession", -expires => $forum->{"authperiod"},-value=> $sessname); my $username = $user; $username =~ s/^http:\/\///; #Remoove http:// from OpenID user names $base->{$sessname}=$username.";".str2time($cookie->expires()). ($ip?";$ENV{'REMOTE_ADDR'}":""); $forum->{'cookies'}=[ $cookie, $cgi->cookie(-name=>"sluser",-value=>$user,-expires => $forum->{authperiod})]; } # # Выполняет аутентикацию пользователя по логину и паролю и # создает для него сессию. # sub authenticate { my ($cgi,$forum) = @_; if ($cgi->param("openidsite")) { my $openid_url = sprintf($cgi->param("openidsite"),$cgi->param("user")); openidstart($cgi,$forum,$openid_url); } my %userbase; dbmopen %userbase,datafile($forum,"passwd"),0644; my $user = $cgi->param("user"); my $password = $cgi->param("password"); $cgi->delete("password"); if (! $userbase{$user}) { set_error($forum,"Неверное имя пользователя или пароль"); return undef; } my $userinfo = thaw($userbase{$user}) ; dbmclose %userbase; #while (my ($key,$val)=each %$userinfo) { print STDERR "$key => '$val'\n";} if (defined $forum->{denied_status} && $userinfo->{status} eq $forum->{denied_status}) { set_error($forum,"Вход пользователя $user в систему заблокирован"); return undef; } if (crypt($password,$userinfo->{passwd}) eq $userinfo->{passwd}) { delete $userinfo->{"passwd"}; $cgi->delete("password"); $userinfo->{"user"} = $user; newsession(undef,$forum,$user); $forum->{"authenticated"} = $userinfo; print STDERR "User $user authenticated successfully\n"; return 1; } else { set_error($forum,"Неверное имя пользователя или пароль"); return undef; } } # # Запоминает сообщение об ошибке для последующего вывода show_template # sub set_error { my ($forum,$message) = @_; print STDERR "set_error: $message\n"; $forum->{error_message} = $message; } # # Выводит текущий шаблон с сообщением об ошибке # sub form_error { my ($form_name,$cgi,$forum,$msg) = @_; set_error($forum,$msg); show_template($form_name,$cgi,$forum); exit; } # # Выполняет редирект (возможно, с установкой куков) на страницу, # указанную # третьем параметре функции или в параметре CGI-запроса # returnto # Если и то, и другое не определено, пытается сконструировать URL для # возврата из PATH_INFO. # sub forum_redirect { my ($cgi,$forum,$url) = @_; if (!defined $url) { $url = $cgi->param("returnto"); $url = $cgi->url(-base=>1).($cgi->path_info()||$forum->{forumtop}) if !$url ; } $url = $cgi->url(-base=>1).$url if substr($url,0,1) eq "/"; print $cgi->redirect(-url=>$url, ($forum->{cookies}?(-cookie=>$forum->{cookies}):())); exit; } # # Заполнение формы редактирования профиля данными пользователя sub show_profile { my ($formname,$cgi,$forum) = @_; my $rights = getrights($cgi,$forum); my $user = $cgi->param("user"); if (!$user && substr($path_translated,length($forum->{userdir}) eq $forum->{userdir})) { $user = substr($path_translated,length($forum->{userdir})+1); } $user = $forum->{authenticated}{user} unless $user; show_error($forum,"Чей профиль вы хотите редактировать?") unless $user; my %base; dbmopen %base,datafile($forum,"passwd"),0664; show_error($forum,"Нет такого пользователя $user") unless $base{$user}; my $userinfo = thaw($base{$user}); dbmclose(%base); delete $userinfo->{passwd}; $userinfo->{user}=$user; while(my ($field,$value) = each %$userinfo) { $value = $value->{src} if ($field eq 'avatar' && ref($value)); if ($value =~/^<(div|p)\b/) { my $tree = str2tree($value); tree2input($cgi,$field,$tree); $tree->delete; } else { $cgi->param($field,$value); } } my $tree = prepare_template(@_); # Запрещаем редактирование полей, входящих в restricted_user_info my $form = $tree->look_down(_tag=>"form",name=>"profile"); if ($rights ne "admin" && $forum->{restricted_user_info}) { for my $field (split /\s*,\s*/,$forum->{restricted_user_info}) { ELEMENT: for my $element ($form->look_down(name=>$field)) { my $tag= $element->tag; if ($tag eq 'input') { my $newel=new HTML::Element("span", "class"=>"restricted-field"); $newel->push_content($element->attr("value")); $element->replace_with($newel)->delete(); } elsif ($tag eq 'textarea') { $element->replace_with_content(new HTML::Element("div", class=>"restricted-field"))->delete(); } elsif ($tag eq 'select') { my $newel = new HTML::Element("span", class=>"restricted-field"); OPTION: for my $option ($element->content_list) { if (ref $option eq "HTML::Element" && $option->attr("selected")) { $newel->push_content($option->detach_content()); last OPTION; } } if (!$newel->content_list) { $newel->push_content(($element->content_list)[0]); } $element->replace_with($newel)->delete; } } } } # Подставляем аватарку substinfo($tree,[_tag=>'img',class=>'avatar'],(ref($userinfo->{avatar})?(%{$userinfo->{avatar}}):(src=>$userinfo->{avatar}))); for my $userlink ($tree->look_down(_tag => "a",class=>"author")) { $userlink->delete_content; $userlink->push_content($user); if ($forum->{authenticated}{openiduser}) { $userlink->attr('href'=>"http://$user"); } else { $userlink->attr('href'=>undef); $userlink->tag('span'); } } send_to_user($tree,@_); } # Обработка результатов редактирования профиля пользвателя # sub profile { my ($formname,$cgi,$forum) = @_; if (!$cgi->param("user")) { show_error($forum,"В форме нет имени пользователя"); } my $user = $cgi->param('user'); my $rights = getrights($cgi,$forum); if ($user ne $forum->{authenticated}{user} && $rights ne "admin") { show_error($forum,"У вас нет прав на изменение профиля этого пользователя"); } my %base; dbmopen %base,datafile($forum,"passwd"),0644; if (!$base{$user}) { show_error($forum,"Несуществующий пользователь $user"); } my $userinfo = thaw $base{$user}; $userinfo->{user}=$user; # # If password fields are filled, change password # if ($cgi->param('pass1')) { if ($cgi->param('pass1') eq $cgi->param('pass2')) { $userinfo->{passwd}=crypt_password($cgi->param('pass1')); } else { form_error($formname,$cgi,$forum,"Ошибка при вводе пароля"); } } make_profile($formname,$cgi,$forum,$userinfo,$rights eq "admin"); delete $userinfo->{user}; $base{$user} = freeze $userinfo; dbmclose %base; show_profile($formname,$cgi,$forum); } # # Обработка результатов заполнения формы регистрации. # # sub register { my ($formname,$cgi,$forum) = @_; # # Возможные ошибки: # 1 Такой юзер уже есть # # не заполнено поле user if (!$cgi->param("user")) { form_error($formname,$cgi,$forum, "Не заполнено имя пользователя"); } # или поле password if (!$cgi->param("pass1")) { form_error($formname,$cgi,$forum,"Не указан пароль"); } # Копии пароля не совпали if ($cgi->param("pass2") ne $cgi->param("pass1")) { form_error($formname,$cgi,$forum,"Ошибка при вводе пароля"); } my $user = $cgi->param("user"); # Не указаны поля, перечисленные в скрытом поле required if ($cgi->param("required")) { foreach my $field (split(/\s*,\s*/,$cgi->param('required'))) { if (!$cgi->param($field)) { form_error($formname,$cgi,$forum,"Не заполнено обязательное поле $field"); } } } $cgi->delete("required"); my %userbase; dbmopen %userbase,datafile($forum,"passwd"),0644 or form_error($formname,$cgi,$forum,"Ошибка открытия файла паролей $!"); if ($userbase{$cgi->param("user")}) { dbmclose %userbase; form_error($formname,$cgi,$forum,"Имя пользователя '".$cgi->param("user"). "' уже занято"); } my $userinfo = {passwd=>crypt_password($cgi->param('pass1'))}; make_profile($formname,$cgi,$forum,$userinfo,0); $userinfo->{registered} = time; set_default_user_attrs($forum,$userinfo); print STDERR "stilllife forum: registering user $user\n"; $userbase{$user} = freeze($userinfo); dbmclose %userbase; if (!defined $forum->{denied_status} || $userinfo->{status} ne $forum->{denied_status}) { newsession(undef,$forum,$user); forum_redirect($cgi,$forum,$cgi->param("returnto")); } else { # FIXME Email validation # Email to admin show_template("newuser",$cgi,$forum); } } sub make_profile { my ($formname,$cgi,$forum,$userinfo,$isadmin) =@_; # Удаляем лишние поля foreach my $field (split(/\s*,\s*/,$cgi->param('ignore'))) { if (!$cgi->param($field)) { $cgi->delete($field); } } if ($cgi->param("email") && ! Email::Valid->address($cgi->param("email"))) { form_error($formname,$cgi,$forum,"Некорректный E-Mail адрес"); } my $user = $userinfo->{user}; my $userprefix=$user; $userprefix=~tr!\\/: !_!; # Если есть аватар в файле, то сохраняем этот файл и формируем URL # на него. $cgi->delete($formname); $cgi->delete("user"); $cgi->delete("pass1"); $cgi->delete("pass2"); if ($cgi->param("avatarfile" )) { my $filename = (save_attached_images($cgi,$forum,$forum->{userdir},"${userprefix}_%f", "avatarfile"))[1]; my $path = $forum->{"userdir"}."/".$filename; my ($w,$h) = imgsize($path); $userinfo->{'avatar'}= {src=>$forum->{"userurl"}."/".$userprefix."_".$filename, width=>$w,height=>$h}; } elsif ($cgi->param('avatar')) { if (!ref($userinfo->{'avatar'}) || $userinfo->{avatar}{'src'} ne $cgi->param('avatar')) { $userinfo->{avatar}=get_avatar_info($cgi->param('avatar')); } } my @restrict=(); @restrict = split /\s*,\s*/, $forum->{restricted_user_info} unless $isadmin; foreach my $param ($cgi->param) { next if (grep $_ eq $param,@restrict); next if $param eq 'avatar'; next if $param eq 'avatarfile'; next if $param eq 'returnto'; next if $param =~ /_format$/; if (defined $cgi->param("${param}_format")) { my $tree = input2tree($cgi,$forum,$param); if ($tree) { $userinfo->{$param} = tree2str($tree); $tree->delete(); } else { delete $userinfo->{$param}; } } else { $userinfo->{$param} = $cgi->param($param); } } } sub crypt_password { my $open_password=shift; my $saltstring = 'ABCDEFGHIJKLMNOPQRSTUVWXUZabcdefghijklmnopqrstuvwxuz0123456789./'; my $salt = substr($saltstring,int(rand(64)),1). substr($saltstring,int(rand(64)),1); my $password=crypt($open_password,$salt); return $password; } sub set_default_user_attrs { my ($forum,$userinfo) = @_; while (my($key,$val) = each %$forum) { next unless $key =~ /^default_(.*)$/; $userinfo->{$1} = $val; } } sub show_user_page { my ($cgi,$forum) = @_; my $rights; $rights=getrights($cgi,$forum) if ($forum->{authenticated}); my %base; my $tree; dbmopen %base,datafile($forum,"passwd"),0664; if ($path_translated eq $forum->{userdir}) { # показать список пользователей $tree = gettemplate($forum,"userlist"); my $usertpl = $tree->look_down(class=>"userinfo"); my $userlist = $usertpl->parent; $usertpl->detach; for my $user (sort keys %base) { my $block = $usertpl->clone; $userlist->push_content($block); my $userinfo =thaw($base{$user}); $userinfo->{"user"} = $user; substitute_user_info($block,$forum,$userinfo); profile_links($block,$user,$rights,$cgi,$forum); } $usertpl->delete; } else { my $user = substr($path_translated,length($forum->{userdir})+1); if (!$base{$user}) { print $cgi->header(-status=>"404 NOT FOUND"); exit; } my $userinfo = thaw($base{$user}); $userinfo->{"user"} = $user; $tree = gettemplate($forum,"user"); substinfo($tree,[_tag=>"title"],_content=>"Stilllife user: $user"); substitute_user_info($tree,$forum,$userinfo); profile_links($tree,$user,$rights,$cgi,$forum); unless ($userinfo->{openiduser}) { for my $userlink ($tree->look_down(_tag => "a",class=>"author")) { $userlink->attr("href",undef); $userlink->tag("span"); } } } my $page = output_html($tree); my $length = do {use bytes; length($page);}; print $cgi->header(-type=>"text/html",-content_length=>$length, -charset=>"utf-8",($forum->{cookies}?(-cookie=>$forum->{cookies}):())), $page; } sub profile_links { my ($tree,$user,$rights,$cgi,$forum)=@_; foreach my $profile_link ($tree->look_down(_tag=>"a", href=>qr/profile=/)) { if ((defined $rights && $rights eq "admin")|| (defined $forum->{authenticated}{user} && $forum->{authenticated}{user} eq $user)) { $profile_link->attr("href", $ENV{'SCRIPT_NAME'}.$forum->{userurl}. "/".$user."?profile=1"); } else { $profile_link->delete(); } } } sub clear_user_cookies { my ($cgi,$forum) = @_; $forum->{cookies}=[ $cgi->cookie(-name=>"sluser", -value=>"0", -expires=>"-1m"),$cgi->cookie(-name=>"slsession", -value=>"0", -expires => "-1m")]; } # # Обработчик формы логина. Сводится к вызову функции authenticate, # поскольку мы поддерживаем логин одновременный с отправкой реплики. # sub login { my ($form,$cgi,$forum)=@_; if (authenticate($cgi,$forum)) { forum_redirect($cgi,$forum); } else { show_template(@_); } } # # Обработчик формы logout. В отличие от большинства обработчиков форм, # поддерживает обработку методом GET # sub logout { my ($form,$cgi,$forum) = @_; clear_user_cookies($cgi,$forum); if (defined (my $session_id = $cgi->cookie("slsession"))) { my %sessiondb; dbmopen %sessiondb,datafile($forum,"session"),0644; delete $sessiondb{$session_id}; dbmclose %sessiondb; } forum_redirect($cgi,$forum); } sub allow_operation { my ($operation,$cgi,$forum) = @_; return 1 if (!exists($operation->{rights})); if (!$forum->{authenticated}) { return 1 if ($operation->{rights} eq "login"); return 0; } my $user = $forum->{authenticated}{user} ; my $accesslevel=getrights($cgi,$forum); return 1 if ($accesslevel eq "admin"); return 0 if ($operation->{rights} eq "admin"); return 1 if ($accesslevel eq "moderator"); return 0 if $accesslevel eq "banned"; return 1; } sub reply { my ($form,$cgi,$forum) = @_; if (! exists $forum->{authenticated} ) { form_error($form,$cgi,$forum,"Вы не зарегистрировались") if (!authenticate($cgi,$forum)); } # # Находим файл дискуссии, в который надо поместить реплику # my ($tree,$lockfd)=gettree($path_translated); my $newmsg = newlistelement($tree,"message","messagelist"); if (!$newmsg) { show_error($forum,"Шаблон темы не содержит элемента с классом message"); } # # Генерируем идентификатор записи. # my $id = "m".get_uid($forum); # # Сохраняем приаттаченные картинки, если есть. # my $dir = $path_translated; $dir=~ s/[^\/]+$// if (-f $dir); my %attached=save_attached_images($cgi,$forum,$dir,"${id}_%f", grep(/^image\d+/,$cgi->param)); # # Преобразуем текст записи в html и чистим его # my $txtree = input2tree($cgi,$forum,"text"); # # Находим в тексте URL на приаттаченные картинки и меняем на те # имена, под которыми мы их сохранили. # fix_image_links($txtree,\%attached,$dir); # # Подставляем данные сообщения # $newmsg->attr("id"=>$id); substinfo($newmsg,[class=>"subject"],_content=>$cgi->param("subject")); my $textnode=$newmsg->look_down("class"=>"mtext"); if (!$textnode) { show_error($forum,"В шаблоне реплики нет места для текста"); } $textnode->delete_content(); $textnode->push_content($txtree); if ($forum->{authenticated}{signature}) { $textnode->push_content(new HTML::Element("br"),"--", new HTML::Element("br"),str2tree($forum->{authenticated}{signature})); } substitute_user_info($newmsg,$forum); # # Подставляем данные в форму msginfo # my $editform=$newmsg->look_down(_tag=>"form","class"=>"msginfo"); if ($editform) { substinfo($editform,[_tag=>"input",name=>"id"],value=>$id) || show_error($forum,"В форме управления сообщением нет поля id"); substinfo($editform,[_tag=>"input",name=>"author"],value=> $forum->{authenticated}{user}) || show_error($forum,"В форме управления сообщением нет поля author"); } # Подставляем mdate my $posted = strftime("%d.%m.%Y %H:%M",localtime()); substinfo($newmsg,["class"=>"mdate"], _content =>$posted); # Подставляем mreply substinfo($newmsg,[_tag=>"a","class"=>"mreply"],"href" => $cgi->url(-absolute=>1,-path_info=>1)."?reply=1&id=$id"); # Подставляем manchor substinfo($newmsg,[_tag=>"a","class"=>"manchor"], "name"=>"#$id","href"=>undef) or show_error($forum,"В шаблоне сообщения отсутствует якорь для ссылок на него"); # подставляем mlink substinfo($newmsg,[_tag=>"a","class"=>"mlink"], href=>$cgi->path_info."#$id"); # подставляем mparent my $parent_id=$cgi->param("id"); if ($parent_id) { substinfo($newmsg,[_tag => "a",class=>"mparent"], "href"=>$cgi->path_info."#$parent_id",style=>undef); } else { substinfo($newmsg,[_tag => "a",class=>"mparent"], style=>"display: none;"); } my $msgcount=0; for my $msg ($newmsg->parent->look_down("class"=>"message")) { $msgcount ++; } # # Делаем Уфф и сохраняем то, что получилось # $newmsg = $newmsg->clone; savetree($path_translated,$tree,$lockfd); record_as_recent($forum,$newmsg); record_statistics($forum,"message"), update_topic_list($forum,$path_translated,$msgcount,$posted); forum_redirect($cgi,$forum,$cgi->path_info."#$id"); } sub fix_image_links { my ($txtree,$attached,$dir) =@_; for my $image ($txtree->find_by_tag_name("img")) { my $file=lc($image->attr("src")); if ( exists $attached->{$file}) { $image->attr("src" => $attached->{$file}); my ($width,$height) = imgsize($dir ."/".$attached->{$file}); $image->attr("width" =>$width); $image->attr("height" => $height); } } } sub save_attached_images { my ($cgi,$forum,$dir,$nametemplate,@params) = @_; my %attached; for my $param (@params) { my $userpath=$cgi->param($param); next unless $userpath; my $filename=lc($1) if $userpath =~ /([^\/\\]+)$/; my $ext = $1 if $filename =~/([^.]+)$/; my %subst=(f=>$filename,e=>$ext); $attached{$filename} = $nametemplate; $attached{$filename}=~s/\%([fe])/$subst{$1}/eg; my $in = $cgi->upload($param); if (!$in) { show_error($forum,"Ошибка при загрузке картинки $filename"); exit; } my $out; open $out,">$dir/$attached{$filename}"; binmode $out,":bytes"; local $/=undef; my $data = <$in>; print $out $data; close $in; close $out; } return %attached; } sub update_topic_list { my ($forum,$topic,$count,$date) = @_; my ($tree,$lockfd,$block,$index); if (!ref ($topic)) { # Если $topic - имя файла, найдем соответствующий индекс и в нем # элемент с соответствующим id; my ($dir,$id)=($1,$2) if $topic =~/(.+)\/([^\/]+).html/; $index = $dir."/".$forum->{indexfile}; ($tree,$lockfd) = gettree($index); $block = $tree->look_down("id"=>$id); return unless $block; } else { # Иначе нам передали кусок готового распарсенного дерева $block = $topic; } substinfo($block,[class=>"msgcount"],_content=>$count); substinfo($block,[class=>"last-updated"],_content=>$date); # и если мы парсили дерево, то мы его и сохраняем savetree($index,$tree,$lockfd); } sub record_as_recent { my ($forum,$msg) = @_; my ($tree,$lockfd) = gettree($forum->{forumroot}."/recent.html"); my $msglist = $tree->look_down("class"=>"messagelist"); if ($msglist) { my $old_copy = $msglist->look_down(id=>$msg->attr("id")); if ($old_copy) { $old_copy->replace_with($msg)->delete; } else { my $style = $msglist->attr("style"); if ($style && $style =~ s/display: none;//) { $msglist->attr("style",$style); $msglist->look_down(class=>"message")->replace_with($msg); } else { my @msgs = $msglist->look_down("class"=>"message"); if (@msgs >= $forum->{replies_per_page}) { for (my $i=$#msgs;$i>=$forum->{replies_per_page}-1;$i--) { $msgs[$i]->delete; } } $msgs[0]->preinsert($msg); } } } savetree($forum->{forumroot}."/recent.html",$tree,$lockfd); } sub getperms { my $dir = shift; local $_; my $key; my %users=(admins=>"",moderators=>"",banned=>""); if (open F,"<",$dir."/perms.txt") { while () { next if /^\s*$/; next if /^\s*#/; if (/^\s*\[\s*(\w+)\s*\]\s*$/) { $key = $1; } else { $users{$key} .= $_ if ($key); } } close F; } return %users; } # # Извлекает из указанного html файла содержимое тэга title # sub gettitle { our $gettitle_flag=0; our $gettitle_text=""; open F,"<",shift or return undef; my $parser = HTML::Parser->new(api_version=>3, text_h=>[sub { $gettitle_text .= shift if $gettitle_flag;}, "dtext"], start_h=>[sub { $gettitle_flag =1 if $_[0] eq 'title'}, "tagname" ], end_h => [sub { $_[1]->eof if $_[0] eq 'title'}, "tagname, self" ]); while (!eof F) { my $data; read F,$data,4096; $parser->parse($data) or last; } return $gettitle_text; } # # Показ формы реактирования прав # sub show_rights { my ($form,$cgi,$forum) = @_; # Получаем текущие права доступа my %users=getperms($path_translated); while (my($key,$val) = each %users) { $cgi->param($key,$val); } my $tree = prepare_template(@_); # Пробегаемся вверх по дереву hide_list($tree,"dirlist"); if ($path_translated ne $forum->{forumroot}) { my $d = $path_translated; my @dirs; while ($d) { # и получаем список файлов perms.txt $d=~s/\/[^\/]+$//; unshift @dirs,$d if -f "$d/perms.txt"; last if $d eq $forum->{forumroot}; } print STDERR "Found permission files in @dirs\n"; # Подставляем их куда надо for $d (@dirs) { print STDERR "substituting dir $d\n"; my %users = getperms($d); my $title=gettitle($d."/".$forum->{indexfile}); my $item = newlistelement($tree,"dir","dirlist"); substinfo($item,[_tag => 'a',class=>'dirlink'], href=>$d,_content=>$title); substinfo($item,[_tag=>'a',class=>'rights'], href=>$cgi->url(-absolute=>1,-path_info=>0).dir2url($cgi,$d)."?setrights=1"); while (my ($key,$val) = each %users) { my @data = map {$_=>new HTML::Element("br")} split /\r?\n/, $val; substinfo($item,[class=>$key],_content=>\@data); } } } my $f = $tree->look_down(_tag=>'form',name=>'setrights'); substinfo($f,[_tag=>'a',class=>'dirlink'], href=>$cgi->path_info, _content=>gettitle($path_translated."/".$forum->{indexfile})); # Отдаем юзеру. send_to_user($tree,@_); } sub set_rights { my ($form,$cgi,$forum) = @_; my $newperm = ""; for my $section ('admins','moderators','banned') { my $data = $cgi->param($section); $data=~s/\r?\n/\n/gs; $data.="\n" unless $data =~/\n$/s; $newperm .= "[ $section ]\n$data"; } if (replacefile($path_translated."/perms.txt",$newperm)) { set_error($forum,"Списки модераторов раздела обновлены"); } show_rights(@_); } # # Обработка операции создания новой темы. # sub new_topic { my ($form,$cgi,$forum) = @_; # # Проверяем корректность urlname и прочих полей # my $urlname; if (!$cgi->param("urlname")) { $urlname = get_uid($forum); } else { $urlname=$1 if $cgi->param("urlname") =~ /^([-\w]+)$/; form_error($form,$cgi,$forum,"Некорректные символы в urlname. Допустимы только латинские буквы, цифры и минус") unless $urlname; } if (!-d $path_translated) { show_error($forum,"Операция $form может быть вызвана только со страницы форума"); } my $filename = "$path_translated/$urlname.html"; if (-f $filename) { form_error($form,$cgi,$forum,"Тема с urlname $urlname уже существует"); } my $url = $cgi->path_info."/$urlname.html"; $url =~ s/\/+/\//g; if (!$cgi->param("title")) { form_error($form,$cgi,$forum,"Тема должна иметь непустое название"); } # # Создаем собственно тему # my $tree = gettemplate($forum,"topic",$url); # Заполнить название и аннотацию my $abstract = input2tree($cgi,$forum,"abstract"); substinfo($tree,[_tag=>"meta","name"=>"description"],content=>$abstract->as_trimmed_text); substinfo($tree,[_tag=>"title"],_content=>$cgi->param("title")); my $subtree = $tree->look_down("class"=>"topic"); my $creation_time=strftime("%d.%m.%Y %H:%M",localtime()); if ($subtree) { substinfo($subtree,["class"=>"title"], _content=>$cgi->param("title")); substinfo($subtree,["class"=>"date"], _content=>$creation_time); # Вставляем в страницу КОПИЮ аннотации, поскольку аннотация # нам еще понадобится в списке тем. substinfo($subtree,["class"=>"abstract"],_content=>$abstract->clone); substitute_user_info($subtree,$forum); } else { substinfo($tree,["class"=>"title"], _content=>$cgi->param("title")); } # Скрыть список сообщений. hide_list($tree,"messagelist"); savetree($filename,$tree,undef); $tree->destroy; # # Добавляем элемент в список тем текущего форума # my $lockfd; ($tree,$lockfd)=gettree($path_translated."/".$forum->{"indexfile"}); my $newtopic = newlistelement($tree,"topic","topiclist"); substinfo($newtopic,[_tag=>"a","class"=>"title"], _content=>$cgi->param("title"), href=>"$urlname.html"); substinfo($newtopic,["class"=>"date"], _content=>$creation_time); substinfo($newtopic,["class"=>"abstract"],_content=>$abstract); substitute_user_info($newtopic,$forum); $newtopic->attr("id",$urlname); my $controlform = $newtopic->look_down(_tag=>"form",class=>"topicinfo"); if ($controlform) { $controlform->attr("action"=>$cgi->url(-absolute=>1,-path_info=>0, -query_string=>0).$url); substinfo($controlform,[_tag=>"input",name=>"author"],value=> $forum->{authenticated}{user}); } update_topic_list($forum,$newtopic,0,$creation_time); savetree($path_translated."/".$forum->{"indexfile"},$tree,$lockfd); record_statistics($forum,"topic"); forum_redirect($cgi,$forum,$cgi->url(-base=>1).$url); } sub new_forum { my ($form,$cgi,$forum) = @_; # # Проверяем корректность urlname и прочих полей # my $urlname; if (!$cgi->param("urlname")) { form_error($form,$cgi,$forum,"Форуму необходимо задать непустое urlname"); } if ($cgi->param("urlname") eq ".") { $urlname = "." } else { $urlname=$1 if $cgi->param("urlname") =~ /^([-\w]+)$/ ; form_error($form,$cgi,$forum,"Некорректные символы в urlname. Допустимы только латинские буквы, цифры и минус") unless $urlname; } if (!-d $path_translated) { show_error($forum,"Операция $form может быть вызвана только со страницы форума"); } my $newname = "$path_translated/$urlname"; $newname=$path_translated if ($urlname eq "."); my $filename = "$newname/$forum->{indexfile}"; if (-f $filename) { form_error($form,$cgi,$forum,"Форум $urlname уже существует"); } if (!$cgi->param("title")) { form_error($form,$cgi,$forum,"Форум должен иметь непустое название"); } mkdir $newname unless -d $newname; # # Сохраняем логотип # my ($logo_name,$logo_width,$logo_height); if ($cgi->param("logo")) { $logo_name=(save_attached_images($cgi,$forum,$newname,"logo.%e","logo"))[1]; ($logo_width,$logo_height) = imgsize("$newname/$logo_name"); } else { $logo_name = $forum->{"templatesurl"}."/1x1.gif"; $logo_width = 1; $logo_height=1; } # # Создаем собственно оглавление форума # my $url = $cgi->path_info."/$urlname"; $url= $cgi->path_info if $urlname eq "."; $url =~ s/\/+/\//g; my $tree = gettemplate($forum,"forum",$url); # Удалить элементы, который присутствуют только на главной странице if ($urlname ne ".") { for my $element ($tree->look_down("class"=>"top-page")) { $element->delete; } } # Заполнить название и аннотацию my $abstract = input2tree($cgi,$forum,"abstract"); substinfo($tree,[_tag=>"meta","name"=>"description"],content=>$abstract->as_trimmed_text); substinfo($tree,[_tag=>"title"],_content=>$cgi->param("title")); my $subtree = $tree->look_down("class"=>"annotation") or show_error($forum,"В шаблоне форума отсутствует класс annotation"); my $creation_time=strftime("%d.%m.%Y %H:%M",localtime()); substinfo($subtree,["class"=>"title"], _content=>$cgi->param("title")); substinfo($subtree,["class"=>"date"], _content=>$creation_time); # Вставляем в страницу КОПИЮ аннотации, поскольку аннотация # нам еще понадобится в списке тем. substinfo($subtree,["class"=>"abstract"],_content=>$abstract->clone); substitute_user_info($subtree,$forum); substinfo($subtree,[_tag=>"img","class"=>"logo"], src=> $logo_name, width=>$logo_width, height=>$logo_height); # Скрыть списки подфорумов и тем . hide_list($tree,"forumlist"); hide_list($tree,"topiclist"); if ($urlname eq ".") { for my $link_up ($tree->look_down(_tag=>"a",href=>"..")) { $link_up->delete; } } savetree($filename,$tree,undef); $tree->destroy; # # Добавляем элемент в список тем текущего форума # if ($urlname ne ".") { my $lockfd; ($tree,$lockfd)=gettree($path_translated."/".$forum->{"indexfile"}); my $newforum = newlistelement($tree,"forum","forumlist"); substinfo($newforum,[_tag=>"a","class"=>"title"], _content=>$cgi->param("title"), href=>"$urlname/"); substinfo($newforum,["class"=>"date"], _content=>$creation_time); substinfo($newforum,["class"=>"abstract"],_content=>$abstract); substinfo($newforum,[_tag=>"img","class"=>"logo"],src=>"$urlname/$logo_name", width=>$logo_width,height=>$logo_height); substitute_user_info($newforum,$forum); $newforum->attr("id",$urlname); my $controlform = $newforum->look_down(_tag=>"form",class=>"foruminfo"); if ($controlform) { $controlform->attr("action"=>$cgi->url(-absolute=>1,-path_info=>0). $url); substinfo($controlform,[_tag=>"input",name=>"author"],value=> $forum->{authenticated}{user}); } savetree($path_translated."/".$forum->{"indexfile"},$tree,$lockfd); record_statistics($forum,"forum"); } else { # Создаем тему для "свежих реплик" my $recent = gettemplate($forum,"topic",$url."/recent.html"); # remove reply link from page itself for my $link ($recent->look_down(_tag =>"a", href=>qr/reply=/)) { $link->delete; } substinfo($recent,["_tag"=>"title"],$cgi->param("title").": Свежие сообщения"); substinfo($recent,["class"=>"title"], _content=>$cgi->param("title"). ": Свежие сообщения"); hide_list($recent,"messagelist"); savetree($path_translated."/recent.html",$recent,undef); } forum_redirect($cgi,$forum,$cgi->url(-base=>1).$url); } # # Обработка операций, которые вызываются одинаково, # но выполняются по-разному для разных типов объектов # # Параметры $cgi,$forum, тип => ссылка на функцию ... # где тип - message, topic или forum. Определяет обрабатываемый объект # и вызывает соответствующую фунцкию. Ожидает что функция завершится #вызовом exit. sub dispatch_objtype { my $cgi=shift; my $forum = shift; my %actions=@_; if (-f $path_translated) { if ($cgi->param("id")) { $actions{"message"}->($cgi,$forum,$path_translated,$cgi->param("id")) if exists($actions{'message'}); } else { $actions{topic}->($cgi,$forum,$path_translated) if exists($actions{'topic'}); } } elsif (-d $path_translated && -f $path_translated ."/". $forum->{indexfile}) { print STDERR "Dispatching forum\n"; $actions{'forum'}->($cgi,$forum,$path_translated) if exists($actions{'forum'}); } return undef; } #Удаление sub delete_item { my ($formname,$cgi,$forum) = @_; # # Сначала разберемся, что мы собственно удаляем # if ($cgi->param("user")) { delete_user($cgi,$forum,$cgi->param("user")); } dispatch_objtype($cgi,$forum,topic=>\&delete_topic, message=>\&delete_comment, forum=>\&delete_forum); show_error($forum,"Невозможно удалить неопознанный объект"); } # Показ формы редактирования sub show_editable { my ($formname,$cgi,$forum) = @_; dispatch_objtype($cgi,$forum,"message"=>\&show_messageedit, topic=>\&show_topicedit, forum=>\&show_forumedit); show_error($forum,"Невозможно редактировать неопознанный объект"); } # Применение результатов редактирования sub edit_item { my ($formname,$cgi,$forum) = @_; dispatch_objtype($cgi,$forum,"message"=>\&messageedit, topic=>\&topicedit, forum=>\&forumedit); show_error($forum,"Невозможно редактировать неопознанный объект"); } sub move_item { my ($formname,$cgi,$forum) = @_; dispatch_objtype($cgi,$forum,"message"=>\&move_message, topic=>\&move_topic, forum=>\&move_forum); show_error($forum,"Невозможно переместить неопознанный объект"); } sub show_move_dest { my ($formname,$cgi,$forum) = @_; dispatch_objtype($cgi,$forum,"message"=>\&show_move_message, topic=>\&show_move_topic, forum=>\&show_move_forum); show_error($forum,"Невозможно переместить неопознанный объект"); } # # Удаление пользователя # sub delete_user { my ($cgi,$forum,$user) = @_; if ($forum->{authenticated}{user} ne $user && getrights($cgi,$forum) ne "admin") { show_error($forum,"У вас недостаточно прав для удаления пользователя $user"); } my %base; dbmopen %base,datafile($forum,"passwd"),0644; delete $base{$user}; dbmclose %base; forum_redirect($cgi,$forum,$forum->{forumtop}); } # Проверка прав на изменение реплики sub moderator_or_author { my ($cgi,$forum,$msg)=@_; return 1 if getrights($cgi,$forum) eq "moderator" || getrights($cgi,$forum) eq "admin"; my $author= $msg->look_down(_tag=>"input",name=>"author"); if ($author) { return $author->attr("value") eq $forum->{authenticated}{user}; } elsif ($author = $msg->look_down(class=>"author",_tag=>"a")) { return $author->as_text eq $forum->{authenticated}{user}; } else { return undef; } } # # Удаление реплики # sub delete_comment { my ($cgi,$forum,$topic,$id) = @_; my ($tree,$lockfd) = gettree($topic); my ($msg) = $tree->look_down(id => $id); show_error($forum,"В данной теме нет реплики с id=$id") if (!$msg); show_error($forum,"У вас нет прав на удаление этого сообщения") unless moderator_or_author($cgi,$forum,$msg); delete_from_list($tree,"messagelist","message",$msg); savetree($topic,$tree,$lockfd); $tree->delete; ($tree,$lockfd) = gettree($forum->{forumroot}."/recent.html"); ($msg) = $tree->look_down(id => $id); if ($msg) { delete_from_list($tree,"messagelist","message",$msg); savetree($forum->{forumroot}."/recent.html",$tree,$lockfd); } else { discardtree($tree,$lockfd); } # Если у реплики были картинки, то их тоже надо грохнуть for my $pic (<$id."_*.*">) { unlink $pic; } forum_redirect($cgi,$forum,$cgi->path_info); } # # Удаление темы # sub delete_topic { my ($cgi,$forum,$topic) = @_; # Если модератор, то в тему и не заглядываем my ($dir,$id) = ($1,$2) if $topic=~/^(.*)\/([^\/]+).html$/; show_error($forum,"Неверное имя темы $topic") unless ($dir && $id); if (getrights($cgi,$forum) ne "moderator" && getrights($cgi,$forum) ne "admin") { # Проверяем авторство темы и наличие в ней сообщений my ($tree,$lockfd) = gettree($topic); my $info = $tree->look_down(class=>"topic"); my $author = $tree->look_down(class=>"author"); if ($author->as_text() ne $forum->{authenticated}{user}) { show_error($forum,"У вас нет права удалять эту тему"); } $info = $tree->look_down(class=>"messagelist"); if ($info->attr("style") !~ /display: none;/) { show_error($forum,"Невозможно удалить тему, содержащую сообщения"); } discardtree($tree,$lockfd); } # FIXME при удалении непустой темы модератором почистить recent.html my ($tree,$lockfd) = gettree($dir . "/".$forum->{indexfile}); delete_from_list($tree,"topiclist","topic",$id); savetree($tree,$dir ."/".$forum->{indexfile}); unlink $topic; my $redirect_url = $cgi->path_info; $redirect_url =~ s/\/[^\/]*$//; forum_redirect($cgi,$forum,$redirect_url); } # # Удаление форума # sub delete_forum { my ($cgi,$forum,$dir) = @_; if (getrights($cgi,$forum) ne "moderator" && getrights($cgi,$forum) ne "admin") { show_error($forum,"У вас нет права удалять форумы"); } opendir DIR,$dir; my $count = 0; while ( my $entry=readdir DIR) { next if $entry eq $forum->{indexfile}; next if $entry eq "perms.txt"; next if $entry =~ /^logo.[^.]+/; show_error($forum,"Нельзя удалять корень форума" ) if $entry eq ".forum"; $count ++; } closedir DIR; show_error($forum,"Нельзя удалять непустой форум") if $count; # Находим родительский форум my $upper = $dir; $upper=~s/([\/]+)$/$forum->{indexfile}/; my $id = $1 if $dir =~ /\/([\/]+)$/; # Удаляем форум из списка. my ($tree,$lockfd) = gettree($upper); delete_from_list($tree,"forumlist","forum",$id); savetree($upper,$tree,$lockfd); # Удаляем из файловой системы opendir DIR,$dir; for my $entry ( readdir DIR) { unlink "$dir/$entry"; } rmdir $dir; my $redirect_url = $cgi->path_info; $redirect_url =~s/\/$//; $redirect_url =~ s/\/[^\/]*$//; forum_redirect($cgi,$forum,$redirect_url); } # Показ сообщения для редактирования sub show_messageedit { my ($cgi,$forum,$path,$id)=@_; my ($tree,$lockfd) = gettree($path); my $dirname = $path; $dirname =~ s/\/[^\/]+$//; my ($msg) = $tree->look_down(id => $id); show_error($forum,"В данной теме нет реплики с id=$id") if (!$msg); show_error($forum,"У вас нет прав на редактированиее этого сообщения") unless moderator_or_author($cgi,$forum,$msg); # Выбираем данные сообщения my ($text) = $msg->look_down(class=>"mtext")->content_list; my ($subject) = $msg->look_down(class=>"subject"); tree2input($cgi,"text",$text); $cgi->param("subject"=> $subject->as_text); discardtree($tree,$lockfd); # Подготавливаем шаблон my $form = prepare_template("edit_message",$cgi,$forum); # Подставляем информацию о картинках. opendir D,$dirname; while ( my $filename=readdir D) { next unless $filename =~/^${id}_/; my $pic = "$dirname/$filename"; my $picitem = newlistelement($form,"picture","picturelist"); my ($w,$h) = imgsize($pic); substinfo($picitem,[_tag=>"img", class=>"msgpicture"], src=>dir2url($cgi,$pic),width=>$w,height=>$h); substinfo($picitem,[_tag=>"input",name=>"delpicture"], value=>$filename); substinfo($picitem,[class=>"filename"],_content =>$filename); } closedir D; send_to_user($form,"edit_message",$cgi,$forum); } # Показ темы для редактирования sub show_topicedit { my ($cgi,$forum,$path)=@_; my ($tree,$lockfd) = gettree($path); my $dirname = $path; $dirname =~ s/\/([^\/]+).html$//; my $urlname=$1; my $topic = $tree->look_down("class"=>"topic"); show_error($forum,"У вас нет прав на редактированиее этой темы") unless moderator_or_author($cgi,$forum,$topic); # Выбираем данные сообщения my ($text) = $topic->look_down(class=>"abstract")->content_list; my ($subject) = $topic->look_down(class=>"title"); tree2input($cgi,"abstract",$text); $cgi->param("title"=> $subject->as_text); discardtree($tree,$lockfd); # Подготавливаем шаблон my $form = prepare_template("edit_topic",$cgi,$forum); # Подставляем информацию об URL. substinfo($form,[class=>"urlname"],_content=>$urlname); send_to_user($form,"edit_topic",$cgi,$form); } # Показ форума для редактирования sub show_forumedit { my ($cgi,$forum,$path) = @_; show_error($forum,"У вас нет прав на редактированиее этого форума") unless (getrights($cgi,$forum) eq 'moderator' || getrights($cgi,$forum) eq 'admin'); my ($tree,$lockfd) = gettree($path."/".$forum->{indexfile}); my $dirname = $path; $dirname =~ s/\/([^\/]+)$//; my $urlname=$1; my $anno = $tree->look_down("class"=>"annotation"); # Выбираем данные сообщения my ($text) = $anno->look_down(class=>"abstract")->content_list; my ($subject) = $anno->look_down(class=>"title"); tree2input($cgi,"abstract",$text); $cgi->param("title"=> $subject->as_text); discardtree($tree,$lockfd); # Подготавливаем шаблон my $form = prepare_template("edit_forum",$cgi,$forum); # Подставляем информацию об URL. substinfo($form,[class=>"urlname"],_content=>$urlname); # Подставляем логотип opendir D,$path; my $logo=0; while (my $file = readdir(D)) { next unless $file =~ /^logo.\w+$/; my ($w,$h) = imgsize($path."/".$file); substinfo ($form,[_tag =>"img",class=>"logo"], src=>$cgi->path_info."/".$file, width=>$w, height=>$h); substinfo($form,[_tag=>"input", name=>"delpicture"],value=>$file); $logo=1; last; } closedir D; unless ($logo) { substinfo($form,[class=>"picture"], style=>"display: none;"); } send_to_user($form,"edit_forum",$cgi,$form); } sub messageedit { my ($cgi,$forum,$topic,$id) = @_; my ($tree,$lockfd) = gettree($path_translated); my ($msg) = $tree->look_down(id=>$id); show_error($forum,"В данной теме нет реплики с id=$id") if (!$msg); show_error($forum,"У вас нет прав на редактированиее этого сообщения") unless moderator_or_author($cgi,$forum,$msg); # Обработка картинок my ($dirname,$topic_id)=($1,$2) if $path_translated=~/(.+)\/([^\/]+).html/; # 1. Удаляем помеченные как таковые for my $file ($cgi->param("delpictire")) { unlink $dirname."/".$file; } # 2. Сохраняем свежеприаттаченные my %attached=save_attached_images($cgi,$forum,$dirname,"${id}_%f",grep(/^image\d+/,$cgi->param)); # Преобразуем текст в html my $txtree = input2tree($cgi,$forum,"text"); fix_image_links($txtree,\%attached,$dirname); my $edited = strftime("%d.%m.%Y %H:%M",localtime()); substinfo($msg,[class=>"subject"],_content=>$cgi->param("subject")); substinfo($msg,[class=>"editdate"],_content=>$edited); makeuserlink($forum,$msg,"editor",$forum->{authenticated}); unhide_list($msg,"edited"); my $textnode = $msg->look_down(class=>"mtext"); my ($old_text)=$textnode->splice_content(0,1,$txtree); $old_text->delete; $msg = $msg->clone; my @msgs = $tree->look_down(class=>"message"); my $msgcount=@msgs; savetree($path_translated,$tree,$lockfd); record_as_recent($forum,$msg); update_topic_list($forum,$path_translated,$msgcount,$edited); forum_redirect($cgi,$forum,$cgi->url(-base=>1).$cgi->path_info()."#$id"); } sub topicedit { my ($cgi,$forum,$topic) = @_; show_error($forum,"Нет такой темы ".$cgi->path_info) if (!-f $path_translated); show_error($forum,"У вас нет прав на редактированиее этой темы") unless moderator_or_author($cgi,$forum,$topic); form_error("edit_topic",$cgi,$forum,"Тема должна иметь непустое название") if (!$cgi->param("title")); my $abstract = input2tree($cgi,$forum,"abstract"); my ($tree,$lockfd) = gettree($path_translated); my $subtree = $tree->look_down("class"=>"topic"); if ($subtree) { substinfo($subtree,["class"=>"title"], _content=>$cgi->param("title")); # Вставляем в страницу КОПИЮ аннотации, поскольку аннотация # нам еще понадобится в списке тем. substinfo($subtree,["class"=>"abstract"],_content=>$abstract->clone); } else { substinfo($tree,["class"=>"title"], _content=>$cgi->param("title")); } savetree($path_translated,$tree,$lockfd); $tree->destroy; my ($dir,$id) = ($1,$2) if $path_translated=~/^(.*)\/([^\/]+).html$/; ($tree,$lockfd) = gettree($dir."/".$forum->{indexfile}); my $item = $tree->look_down(id=>$id); substinfo($item,[_tag=>"a","class"=>"title"], _content=>$cgi->param("title")); substinfo($item,["class"=>"abstract"],_content=>$abstract); savetree($dir."/".$forum->{"indexfile"},$tree,$lockfd); forum_redirect($cgi,$forum); } sub forumedit { my ($cgi,$forum,$dir) = @_; show_error($forum,"У вас нет прав на редактированиее этого форума") unless (getrights($cgi,$forum) eq 'moderator' || getrights($cgi,$forum) eq 'admin'); if (!-d $path_translated) { show_error($forum,"Операция редактирования форумаможет быть вызвана только со страницы форума"); } if (!$cgi->param("title")) { form_error("edit_forum",$cgi,$forum,"Форум должен иметь непустое название"); } my ($logo_name,$logo_width,$logo_height); if ($cgi->param("delpicture")) { # Удаляем старый логотип unlink $path_translated ."/".$cgi->param("delpicture"); $logo_name = $forum->{"templatesurl"}."/1x1.gif"; $logo_width = 1; $logo_height=1; } if ($cgi->param("logo")) { # Удаляем все, что похоже на логотип opendir D, $path_translated; for my $f (readdir(D)) { next unless -f $path_translated."/".$f && $f=~/^logo\.\w+$/; unlink $path_translated."/".$f; } closedir D; $logo_name=(save_attached_images($cgi,$forum,$path_translated,"logo.%e","logo"))[1]; ($logo_width,$logo_height) = imgsize("$path_translated/$logo_name"); } my ($tree,$lockfd) = gettree($path_translated . "/".$forum->{indexfile}); my $abstract = input2tree($cgi,$forum,"abstract"); substinfo($tree,[_tag=>"meta","name"=>"description"],content=>$abstract->as_trimmed_text); substinfo($tree,[_tag=>"title"],_content=>$cgi->param("title")); my $subtree = $tree->look_down("class"=>"annotation"); substinfo($subtree,["class"=>"title"], _content=>$cgi->param("title")); # Вставляем в страницу КОПИЮ аннотации, поскольку аннотация # нам еще понадобится в списке тем. substinfo($subtree,["class"=>"abstract"],_content=>$abstract->clone); substinfo($subtree,[_tag=>"img","class"=>"logo"], src=> $logo_name, width=>$logo_width, height=>$logo_height) if defined($logo_name); savetree($path_translated ."/".$forum->{indexfile},$tree,$lockfd); $tree->destroy; if ($path_translated ne $forum->{forumroot}) { my ($updir,$id) = ($1,$2) if $path_translated=~/^(.*)\/([^\/]+)$/; ($tree,$lockfd) = gettree($updir . "/".$forum->{indexfile}); my $item = $tree->look_down(id=>$id); substinfo($item,[_tag=>"a","class"=>"title"], _content=>$cgi->param("title")); substinfo($item,["class"=>"abstract"],_content=>$abstract); substinfo($item,[_tag=>"img","class"=>"logo"],src=>"$id/$logo_name", width=>$logo_width,height=>$logo_height) if $logo_name; savetree($updir."/".$forum->{"indexfile"},$tree,$lockfd); } forum_redirect($cgi,$forum); } #---------------------------------------------------------- # База пользователей и права доступа #---------------------------------------------------------- # # Записывает в базу данных пользователей, сколько каких объектов # создал текущий пользователь # sub record_statistics { my ($forum,$type) = @_; my $user = $forum->{authenticated}{user}; my %base; dbmopen %base,datafile($forum,"passwd"),0664; my $userinfo = thaw($base{$user}); $userinfo->{$type."s"}++; $userinfo->{"last_$type"}=time; $base{$user} = freeze($userinfo); dbmclose %base; } # # читает файлы прав доступа в дереве форума, и возвращает # статус текущего пользователя (undef - аноним, banned, normal, # moderator или admin sub getrights { my ($cgi,$forum) = @_; if (!$forum->{authenticated}) { return undef; } return $forum->{authenticated}{rights} if exists $forum->{authenticated}{rights}; my $user = $forum->{authenticated}{user}; my $dir = $path_translated; $dir =~s/\/$//; $dir =~s/\/[^\/]+$// if (!-d $dir); my $f; my $user_status = "normal"; LEVEL: while (length($dir)) { if (-f "$dir/perms.txt") { open $f,"<","$dir/perms.txt"; my $status = undef; while (<$f>) { if (/^\[\s*(admins|moderators|banned)\s*\]/) { $status = $1; } else { chomp; if ($user eq $_ && defined $status) { if ($status eq "banned") { return $forum->{authenticated}{rights}=$status; } if ($status eq "admins" ) { return $forum->{authenticated}{rights}="admin"; } $user_status = "moderator"; } } } close $f; last LEVEL if -f "$dir/.forum"; } # Strip last path component. $dir =~s/\/[^\/]+$// } return $forum->{authenticated}{rights}=$user_status; } #------------------------------------------------------------------ # Работа с файлами и идентификторами #------------------------------------------------------------------ # # Залочить файл и получить его распрасенное представление. # Возвращает пару ($tree,$lockfd) sub gettree { my $filename = shift; my $f; open $f,"<",$filename or return undef; flock $f, LOCK_EX; my $tree = treefromfile($f); return ($tree,$f); } # # Сохранить дерево и закрыть lockfd. # # sub replacefile { my $filename = shift; my $content = shift; my $f; open $f,">",$filename . ".new" or return undef; print $f $content; close $f; # FIXME - только для POSIX. unlink $filename; rename $filename.".new",$filename; } sub savetree { my ($filename,$tree,$lockfd) = @_; replacefile($filename,output_html($tree)); close $lockfd if defined($lockfd); } sub discardtree { my ($tree,$lockfd) = @_; flock $lockfd,LOCK_UN; close $lockfd; $tree->delete; } # # Cериализовать HTML-документ с DOCTYPE (workaround вокруг баги в # HTML::TreeBuilder) # sub output_html { my $tree=shift; return ''. $tree->as_HTML("<>&"); } # # Читает шаблон и подготавливает его к размещению по указанной URL. # Если url не указана, считается что шаблон будет показан как результат # текущего http-запроса. sub gettemplate { my ($forum, $template,$url) = @_; $url =~ s/\/+/\//g if defined $url; my $filename=$forum->{"templates"}."/$template.html"; if (! -r $filename) { show_error($forum,"Нет шаблона $template"); exit; } my $tree = treefromfile($filename); fix_forum_links($forum,$tree,$url); return $tree; } # # Создает объект HTML::TreeBuilder и выставляет ряд опций. # sub make_tree { my $tree = HTML::TreeBuilder->new; # Set some options for treebuilder # Comments are neccessary to convert HTML back to BBCode $tree->store_comments(1); # Avoid converting html into one long-long string $tree->ignore_ignorable_whitespace(0); $tree->no_space_compacting(1); $tree->p_strict(1); return $tree; } sub treefromfile { my ($f) = shift; my $tree = make_tree(); $tree->parse_file($f); return $tree; } # # Получает уникальный числовой идентификатор. # sub get_uid { my $forum = shift; my $f; open $f,"+<",datafile($forum,"sequence") or flock $f,LOCK_EX; my $id=<$f> || "0"; $id++; seek $f,0,0; printf $f "%8s\n",$id; close $f; $id=~/(\d+)/; return sprintf ("%08s",$1); } # -------------------------------------------------------------------- # OpenID registration # ------------------------------------------------------------------- sub create_openid_consumer { my ($cgi,$forum) = @_; return Net::OpenID::Consumer ->new( ua => LWP::UserAgent->new( agent => "Stilllife/1.0"), args => $cgi, consumer_secret=>"X9RWPo0rBE7yLja6VB3d", required_root => $cgi->url(-base=>1)); } # openidstart - вызывается когда обнаружено что текущее имя # пользователя, пытающегося аутентифицироваться, содержит http:// # # sub openidstart { my ($cgi,$forum,$openidurl) = @_; # # Fix duplicated http:// which can be produced by our sprintf based # login system # $openidurl=~s!^http://http://!http://!; my $csr = create_openid_consumer($cgi,$forum); my $claimed_identity=$csr->claimed_identity($openidurl); if (!defined $claimed_identity) { show_error($forum,"Указанная URL $openidurl не является OpenId"); exit; } $cgi->param("openidvfy",1); $cgi->delete("user"); $cgi->delete("openidsite"); $cgi->delete("password"); my $check_url = $claimed_identity->check_url( return_to=> $cgi->url(-full=>1,-path_info=>1,-query=>1), trust_root=> $cgi->url(-base=>1)); print $cgi->redirect(-location=>$check_url); exit; } # # Вызывается при редиректе от openid producer-а. Проверяет, что # удаленный сервер подтвердил openid и вызывает операцию для которой # (либо возврат на исходную страницу при операции login, либо постинг # реплики) # sub openid_verify { my ($action,$cgi,$forum) = @_; my $csr = create_openid_consumer($cgi,$forum); if (my $setup_url = $csr->user_setup_url) { print $cgi->redirect(-location=>$setup_url); exit; } elsif ($csr->user_cancel) { show_error($forum,"Ваш openid-сервер отказался подтвержать вашу идентичность"); exit; } elsif (my $vident = $csr->verified_identity) { #Успешная аутентификация. #Создаем сессию my $user = $vident->url; # Remove trailing slash from URL if any $user=~s/\/$//; my %userbase; dbmopen %userbase,datafile($forum,"passwd"),0664; my $username = $user; $username =~ s/^http:\/\///; if (!$userbase{$username}) { # Тащим foaf, если получится my %info=get_foaf($csr->ua,$vident->declared_foaf); if (ref($info{'avatar'}) eq "HASH" ) { delete $info{'avatar'}{'type'}; } $info{"openiduser"}=1; $info{"registered"}=time; set_default_user_attrs($forum,\%info); $info{'status'} = $forum->{openid_status} if $forum->{openid_status}; $forum->{authenticated}=\%info; $userbase{$username} = freeze(\%info); } else { $forum->{authenticated} = thaw ($userbase{$username}); } dbmclose %userbase; if (defined $forum->{denied_status} && ($forum->{authenticated}{status} eq $forum->{denied_status})) { show_error($forum,"Вход пользователя $username в систему заблокирован"); } $forum->{"authenticated"}{"user"} = $username; newsession(undef,$forum,$user); # Если указан параметр reply, вызываем обработку реплики if ($cgi->param("reply")) { reply("reply",$cgi,$forum); exit; } #Иначе, возвращаемся на исходную страницу forum_redirect($cgi,$forum,undef); } else { show_error($forum,"Ошибка OpenId аутентификации"); exit; } } sub get_avatar_info { my ($url,$ua) = @_; $ua = LWP::UserAgent->new( agent => "Stilllife/1.0") unless $ua; my $response = $ua->get($url); if ($response->is_success) { my $image = $response->content; my ($w,$h,$type) = imgsize(\$image); return {width=>$w,height=>$h,type=>$type,src=>$url}; } else { print STDERR "Error getting $url: ".$response->status_line,"\n"; return undef; } } sub get_foaf { my ($ua,$foaf_url) = @_; my $response = $ua->get($foaf_url); unless ($response->is_success) { print STDERR "Error geting foaf from $foaf_url\n"; return (); } my $foaf = $response->content; my %info = foaf_parse($foaf); if ($info{avatar}) { $info{avatar} = get_avatar_info($info{avatar},$ua); } return %info; } sub foaf_parse { my $foaf = shift; my ($starttag) = $foaf =~ /<(\w+(:\w+)?[^>]+)>/sg; my %ns = reverse ($starttag =~ /xmlns:(\w+)="([^"]+)"/sg); my $foaf_prefix = $ns{"http://xmlns.com/foaf/0.1/"}; my $rdf_prefix = $ns{"http://www.w3.org/1999/02/22-rdf-syntax-ns#"}; my ($userpic) = $foaf=~/<$foaf_prefix:img[^>]* $rdf_prefix:resource="([^"]+)"/s; my @info; push @info, avatar =>$userpic if $userpic; my ($icq) = $foaf =~/<$foaf_prefix:icqChatID>([^<]*)<\/$foaf_prefix:icqChatID>/s; push @info, icq => $icq if ($icq); my ($jabber) = $foaf =~/<$foaf_prefix:jabberID>([^<]*)<\/$foaf_prefix:jabberID>/s; push @info, jabber => $jabber if ($jabber); return @info; } #----------------------------------------------------------------- # Обработка форматированных текстовых полей #----------------------------------------------------------------- sub input2tree { my ($cgi,$forum,$field_name) = @_; my $format = $cgi->param($field_name."_format"); my $text = $cgi->param($field_name); return "" if (!$text); if ($format eq "bbcode") { my $parser = HTML::BBReverse->new(); $text="

".$parser->parse($text)."
"; } elsif ($format eq "text") { $text = escapeHTML($text); $text=~s/\r?\n\r?\n/<\/p>

/sg; $text=~s/\r?\n/
/sg; $text = "

$text

"; } my $txtree = str2tree($text); for my $badtag ("script","style","head","html","object","embed","iframe","frameset","frame", ($forum->{forbid_tags}?split(/\s*,\s*/,$forum->{forbid_tags}):())) { for my $element ($txtree->find_by_tag_name($badtag)) { $element->delete() if defined $element; } } # FIXME Проверяем на наличие URL-ок не оформленных ссылками. return $txtree; } sub tree2input { my ($cgi,$field_name,$tree) = @_; $cgi->param($field_name=>$tree->as_HTML('<>&"')); $cgi->param("${field_name}_format"=>"html"); } sub str2tree { my ($data)=@_; my $tree = make_tree(); $tree->ignore_ignorable_whitespace(1); # Set parser options here $tree->parse("
$data
"); $tree->eof; my $element=$tree->find("body"); while (($element =($element->content_list)[0])->tag ne "div") { } while ($element->content_list==1&& $element->tag eq "div" && !defined $element->attr("style")) { $element = ($element->content_list)[0] } $element->detach; $tree->destroy; return $element; } sub tree2str { my ($tree)=@_; return $tree->as_HTML("<>&"); } #------------------------------------------------------------------------ # Подстановка в дереве #------------------------------------------------------------------------ # Находит # элемент указанного класса и удаляет display: none из его атрибута # style. Возвращает 1, если элемент был раскрыт, и 0, если он и до этого # был видимым. sub unhide_list { my ($tree,$class) = @_; my $msglist = $tree->look_down("class"=>$class); if ($msglist) { my $style = $msglist->attr("style"); if ($style && $style =~ s/display: none;//) { $msglist->attr("style",$style); return 1; } else { return 0; } } } # # Находит первый элемент указанного класса, и приписывает ему display: # none в style. # sub hide_list { my ($tree,$class)=@_; my $msglist = $tree->look_down("class"=>$class); return undef unless $msglist; if (!$msglist->attr("style")) { $msglist->attr("style","display: none;"); } else { my $style = $msglist->attr("style"); unless ($style=~ s/\bdisplay:\s+\w+\s*;/display: none;/) { $style .= "display: none;"; } $msglist->attr("style",$style); } return 1; } # # Удаляет из списка элемент, если он не последий. Если последний - # скрывает весь список. Элемент можно указывать как ссылкой на объект # HTML::Element, так и значением атрибута id # sub delete_from_list { my ($tree,$listclass,$itemclass,$item) = @_; my $list = $tree->look_down(class =>$listclass); ($item) = $list->look_down(id =>$item) unless ref($item); return undef unless $item; my (@items)=$tree->look_down(class=>$itemclass); if (@items == 1) { hide_list($tree,$listclass); } else { $item->delete; } return 1; } # # Найти все элементы, удоволетворяющие заданному критерию и подставить в # них указанные атрибуты # # Параметры 1. Дерево (класса HTML::Element) # 2. Запрос - ссылка на список вида атрибут=>значение. # Этот список будет непосредственно передан в # HTML::Element::look_down # 3. Далее пары имя-атрибута, значение. Если вместо имени атрибута # использовать слово _content, заменено будет содержимое элемента. # Значение для _content - ссылка на HTML::Element. Если там строка, # она будет вставлена как одиночный текстовый узел. # 4. Возвращает число выполненных подстановок (0, если искомых элементов # не найдено. # sub substinfo { my ($tree,$query,@attrs) = @_; my $count; foreach my $element ($tree->look_down(@$query)) { $count ++; while (@attrs) { my $attr = shift @attrs; my $value = shift @attrs; if ($attr eq "_content") { $element->delete_content; if (ref($value) eq 'ARRAY') { $element->push_content(@$value) ; } else { $element->push_content($value); } } else { $element->attr($attr,$value); } } } return $count; } # # newlistelement($tree,$elementclass,$listclass) # # Если список с указанным классом скрыт, раскрывает его и возвращает # (единственный) элемент sub newlistelement { my ($tree,$element,$list) =@_; my $msglist = $tree->look_down("class"=>$list); if ($msglist) { my $style = $msglist->attr("style"); if ($style && $style =~ s/display: none;//) { $msglist->attr("style",$style); return $msglist->look_down(class=>$element); } else { my $template = $msglist->look_down("class"=>$element); return undef unless $template; my $newitem=$template->clone; $template->parent->push_content($newitem); return $newitem; } } else { return undef; } } sub escapeHTML { local $_ = shift; s/\&/&/g; s/\/>/g; return $_; }