X-Git-Url: http://www.wagner.pp.ru/gitweb/?a=blobdiff_plain;f=forum%2Fforum;h=8c966e93bec89259db459e407268233edc289762;hb=577989dad1f989210c7b3a7a6917c08ae3b95c8b;hp=989c5bc173ed374e8d975effa24c11eb61d4d49a;hpb=e520a1139a35b2e8f30407dcee80ad363f0e3b76;p=oss%2Fstilllife.git diff --git a/forum/forum b/forum/forum index 989c5bc..8c966e9 100755 --- a/forum/forum +++ b/forum/forum @@ -19,103 +19,65 @@ 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 = ( - reply => \&reply, - edit => \&edit_comment, - delete => \&delete_comment, - move => \&move_comment, - newtopic=> \&new_topic, - newforum=> \&new_forum, - login => \&login, - register=>\®ister, - profile=>\&profile, - setrights=>\&set_rights, - openidlogin=>\&openid_login, - openidvfy =>\&openid_verify +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"} ); # -# Уровень прав, которые необходимо иметь пользователю для совершения -# определенного действия -# иерархия вида undef < banned < normal < author < moderator "login", - edit => "author", - delete => "author", - newtopic => "normal", - move => "moderator", - newforum => "moderator", - profile => "normal", - setrights => "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); -if ($cgi->request_method ne "POST") { -# Запрос к скрипту методом GET. Надо показать форму, если только это не -# редирект от OpenId-сервера - if ($cgi->param('openidvfy')) { - openid_verify($cgi,$forum); - } elsif ($cgi->param("logout")) { - logout('logout',$cgi,$forum); - } else { - for my $param ($cgi->param) { -# Среди параметров, указанных в URL ищем тот, который задает -# действие - if (exists $actions{$param}) { -# Мы, конечно уже проверили, что в названии параметра -# нехороших символов нет, но чтобы perl в taint mode не -# ругался... - if (allow_operation($param,$cgi,$forum)) { - show_template($1,$cgi,$forum) if $param=~/^(\w+)$/; - exit; - } else { - if (!$forum->{"authenticated"}) { - $cgi->param("returnto",$cgi->url(-full=>1)); - show_template("login",$cgi,$forum); - exit; - - } else { - show_error($forum,"У Вас нет прав на выполнение этой - операции") - } - } - } - } - show_error($forum,"Некорректный вызов скрипта. Отсутствует параметр - действия"); - } -} else { - # Запрос методом POST. Вызываем обработчик - for my $param ($cgi->param) { - if (exists $actions{$param}) { - if (allow_operation($param,$cgi,$forum)) { - $actions{$param}->($param,$cgi,$forum); - exit; - } else { - show_error($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,"У Вас нет прав на выполнение этой операции") } } - print STDERR "Получены параметры ",join(" ",$cgi->param),"\n"; - 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=""; @@ -141,14 +103,16 @@ sub dir2url { return $prefix.substr($dir,length($root)); } # -# Поиск файла .forum вверх по дереву от $ENV{PATH_TRANSLATED} +# Поиск файла .forum вверх по дереву от $path_translated # Значение PATH_TRANSLATED считаем безопасным - наш web-сервер нам не # враг. # Возвращает список имя,значение, имя, значение который прививается в # хэш - +# sub get_forum_config { - my @path=split("/",$1) if $ENV{PATH_TRANSLATED}=~/^(\S+)$/; + $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; @@ -163,6 +127,7 @@ sub get_forum_config { # файл .forum $config{"forumtop"} = dir2url($cgi,join("/",@path)); + $config{"forumroot"} = join("/",@path); # Если в конфиге отсутствует переменная templates, но # рядом с конфигом присутствует директория templates, # то шаблоны там. @@ -183,7 +148,6 @@ sub get_forum_config { } $config{"userurl"} = dir2url($cgi,$config{"userdir"}); - # # Если нет ссылки в конфиге на файл паролей или он не # существует, выдаем ошибку. С офоромлением, так как шаблоны @@ -197,9 +161,13 @@ sub get_forum_config { 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; @@ -223,7 +191,7 @@ sub get_forum_config { sub show_error { my ($cfg,$msg) = @_; if ( -r $cfg->{"templates"}."/error.html") { - my $tree = HTML::TreeBuilder->new_from_file($cfg->{"templates"}."/error.html"); + my $tree = treefromfile($cfg->{"templates"}."/error.html"); my $node= $tree->find_by_attribute('class','error'); my $body; if (!$node) { @@ -234,28 +202,17 @@ sub show_error { $node->delete_content; $node->push_content($msg); print $cgi->header(-type=>'text/html',-charset=>'utf-8'); - print $tree->as_HTML("<>&"); + print output_html($tree); } else { print $cgi->header(-type=>'text/html',-charset=>'utf-8'); print "Ошибка конфигурации форума", "

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

", - $cgi->escapeHTML($msg),"

", + escapeHTML($msg),"

", "

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

"; } exit; } -sub gettemplate { - my ($forum, $template,$url) = @_; - my $filename=$forum->{"templates"}."/$template.html"; - if (! -r $filename) { - show_error($forum,"Нет шаблона $template"); - exit; - } - my $tree = HTML::TreeBuilder->new_from_file($filename); - fix_forum_links($forum,$tree,$url); - return $tree; -} # # Вывод шаблона формы. В шаблоне должна присутстовать форма с # именем, совпадающим с именем form. Если в $cgi есть параметры, имена @@ -263,6 +220,18 @@ sub gettemplate { # подставляются # 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'}); @@ -306,20 +275,19 @@ sub show_template { for my $element ($f->find_by_tag_name("textarea","input","select")) { my $name = $element->attr("name"); $substituted{$name} = 1; - #print STDERR "substituting form element name $name tag ",$element->tag, - # "value='",$cgi->param($name),"'\n"; if (defined $cgi->param($name)) { if ($element->tag eq "input") { - next ELEMENT if grep ($element->attr("type") eq - $_,"button","submit","reset"); - if ($element->attr("type") eq "check") { + 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 ($element->attr("type") eq + } elsif ($type eq "radio") { if ($element->attr("value") eq $cgi->param($name)) { $element->attr("checked",""); @@ -329,13 +297,27 @@ sub show_template { } else { $element->attr("value",$cgi->param($name)); } - } elsif ($f->tag eq "textarea") { - $f->delete_content; - $f->push_content($cgi->param("name")); - } elsif ($f->tag eq "select") { - for my $option ($f->find_by_tag_name("option")) { - if (grep($option->attr("value") eq $_, $cgi-param("name"))) { - $option->attr("selected",""); + } 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); } @@ -345,7 +327,7 @@ sub show_template { } } - $f->attr("type","POST"); + $f->attr("method","POST"); for my $required ($form,"returnto") { if (!$substituted{$required}) { my $element = new HTML::Element('input', @@ -354,18 +336,17 @@ sub show_template { $f->push_content($element); } } - - - print - $cgi->header(-type=>"text/html",-charset=>"utf-8",($forum->{cookies}?(-cookie=>$forum->{cookies}):())), - $tree->as_HTML("<>&"); -} + return $tree; +} # # Поправляет ссылки на служебные файлы и скрипты форума # sub fix_forum_links { my ($forum,$tree,$path_info) = @_; - $path_info=$ENV{'PATH_INFO'} if (!defined $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")) { @@ -379,10 +360,11 @@ sub fix_forum_links { } # Обрабатываем наши специальные 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" => $cgi->url(-absolute=>1, - -path_info=>0,-query_string=>0).$forum->{userurl}); + $element->attr("href" => $userlist); next ELEMENT; } elsif ($element->attr("rel") eq "forum-script") { $element->attr("href" => $script_with_path); @@ -391,9 +373,15 @@ sub fix_forum_links { } my $link = $element->attr($attr); # Абсолютная ссылка - оставляем как есть. - next ELEMENT if (! defined $link || $link=~/^\w+:/); + 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}) ; @@ -402,58 +390,80 @@ sub fix_forum_links { $link =~ s/^[^\?]+/forum/; } if (!($link =~ s!^templates/!$forum->{templatesurl}/!) && - !($link =~ s!^users/!$forum->{usersurl}/!) && + !($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) = @_; -my %userinfo = %{$forum->{"authenticated"}}; +my ($tree,$forum,$user) = @_; +my %userinfo; +if (defined $user) { + %userinfo=%$user; +} else { + # Если не сказано, какой юзер, то текущий. + %userinfo = %{$forum->{"authenticated"}} +} # # Специально обрабатываем поля user (должна быть ссылка) и avatar # (должен быть img). -my @userlink = $tree->look_down("_tag"=>"a","class"=>"author"); -if (@userlink) { - my $userpage; - if ($userinfo{"user"}=~/^http:/) { - $userpage = $userinfo{"user"}; + 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 { - $userpage = - $cgi->url(-absolute=>1).$forum->{"userurl"}."/".$cgi->escape($userinfo{"user"}); - } - for my $element (@userlink) { - $element->attr(href=>$userpage); + 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(); - $element->push_content($userinfo{"user"}); - } -} -delete $userinfo{"userpage"}; -delete $userinfo{"user"}; -my $avatar = $tree->look_down("_tag"=>"img","class"=>"avatar"); -if ($avatar) { - $avatar->attr(src=>$userinfo{"avatar"}); -} -delete $userinfo{"avatar"}; - -while (my ($field,$value)=each %userinfo) { - my $element = $tree->look_down("class","a".$field); - if ($element) { - $element->delete_content(); - # - # FixME - allow HTML in author attributes - $element->push_content($value); - } + $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"); + } + } -} } # @@ -462,7 +472,7 @@ while (my ($field,$value)=each %userinfo) { # sub authorize_user { - ($cgi,$forum) = @_; + my ($cgi,$forum) = @_; if (my $session=$cgi->cookie("slsession")) { # Пользователь имеет куку my %sessbase; @@ -480,7 +490,6 @@ sub authorize_user { my %userbase; dbmopen %userbase,datafile($forum,"passwd"),0644; if ( $userbase{$user}) { - print STDERR "getting user info for $user\n"; my $userinfo = thaw($userbase{$user}); delete $userinfo->{"passwd"}; $userinfo->{"user"} = $user; @@ -490,8 +499,7 @@ sub authorize_user { } print STDERR "user $user restored session $session\n"; $forum->{"authenticated"}=$userinfo; - print STDERR "authorize_user: - ",$forum->{authenticated}{user}, + print STDERR "authorize_user: ",$forum->{authenticated}{user}, $forum->{authenticated},"\n"; } dbmclose %userbase; @@ -565,6 +573,11 @@ sub authenticate { 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"); @@ -610,11 +623,131 @@ sub forum_redirect { $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); +} +# # Обработка результатов заполнения формы регистрации. # # @@ -645,6 +778,7 @@ sub register { } } } + $cgi->delete("required"); my %userbase; dbmopen %userbase,datafile($forum,"passwd"),0644 or form_error($formname,$cgi,$forum,"Ошибка открытия файла паролей $!"); @@ -652,60 +786,166 @@ sub register { dbmclose %userbase; form_error($formname,$cgi,$forum,"Имя пользователя '".$cgi->param("user"). "' уже занято"); } - if ($cgi->param("email") && ! Email::Valid->address($cgi->param("email"))) { - form_error($formname,$cgi,$forum,"Некорректный E-Mail адрес"); + 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); } - my $saltstring = 'ABCDEFGHIJKLMNOPQRSTUVWXUZabcdefghijklmnopqrstuvwxuz0123456789./'; - my $salt = substr($saltstring,int(rand(64)),1). - substr($saltstring,int(rand(64)),1); - my $password=crypt($cgi->param("pass1"),$salt); - my $userinfo = {passwd=>$password}; +} +sub make_profile { + my ($formname,$cgi,$forum,$userinfo,$isadmin) =@_; # Удаляем лишние поля - $cgi->delete("required"); - $cgi->delete("register"); - $cgi->delete("user"); - $cgi->delete("pass1"); - $cgi->delete("pass2"); foreach my $field (split(/\s*,\s*/,$cgi->param('ignore'))) { if (!$cgi->param($field)) { $cgi->delete($field); } } - my $returnto = $cgi->param("returnto"); - $cgi->delete("returnto"); + 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 $f = $cgi->upload("avatarfile"); - binmode $f,":bytes"; - my $out; - my $filename = $1 if $cgi->param("avatarfile")=~/([^\/\\]+)$/; - open $out,">",$forum->{"userdir"}."/".$filename; - binmode $out,":bytes"; - my $buffer; - while (my $bytes = read($f,$buffer,4096)) { - print $out $buffer; + 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')); } - close $f; - close $out; - $userinfo->{'avatar'}= $forum->{"userurl"}."/".$filename; - $cgi->delete("avatar"); - $cgi->delete("avatarfile"); } - + my @restrict=(); + @restrict = split /\s*,\s*/, $forum->{restricted_user_info} + unless $isadmin; + foreach my $param ($cgi->param) { - $userinfo->{$param} = $cgi->param($param); - } - $userinfo->{registered} = time; - if (exists $forum->{default_status}) { - $userinfo->{status} = $forum->{default_status}; + 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); + } } - print STDERR "stilllife forum: registering user $user\n"; - $userbase{$user} = freeze($userinfo); - dbmclose %userbase; - newsession(undef,$forum,$user); - forum_redirect($cgi,$forum,$returnto) +} +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, # поскольку мы поддерживаем логин одновременный с отправкой реплики. @@ -718,12 +958,6 @@ sub login { show_template(@_); } } -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")]; -} # # Обработчик формы logout. В отличие от большинства обработчиков форм, # поддерживает обработку методом GET @@ -741,24 +975,18 @@ sub logout { } sub allow_operation { my ($operation,$cgi,$forum) = @_; - return 1 if (!exists($permissions{$operation})); + return 1 if (!exists($operation->{rights})); if (!$forum->{authenticated}) { - return 1 if ($permissions{$operation} eq "login"); + return 1 if ($operation->{rights} eq "login"); return 0; } my $user = $forum->{authenticated}{user} ; my $accesslevel=getrights($cgi,$forum); - # Если permissions{$operation} равны author, нам нужно извлечь - # текст из соответствующего файла и положить его в - # cgi->param("text"); Заодно определим и автора - my ($itemauthor,$itemtext)=get_message_by_id($cgi->param("id")) if - $permissions{$operation} eq "author"; return 1 if ($accesslevel eq "admin"); - return 0 if ($permissions{$operation} eq "admin"); + return 0 if ($operation->{rights} eq "admin"); return 1 if ($accesslevel eq "moderator"); return 0 if $accesslevel eq "banned"; - return 0 if $permissions{$operation} eq "author" && $user ne $itemauthor; return 1; } @@ -770,41 +998,27 @@ sub reply { # # Находим файл дискуссии, в который надо поместить реплику # - my ($tree,$lockfd)=gettree($ENV{'PATH_TRANSLATED'}); - my $messagetpl = $tree->look_down(class=>"message"); - if (!$messagetpl) { + my ($tree,$lockfd)=gettree($path_translated); + my $newmsg = newlistelement($tree,"message","messagelist"); + if (!$newmsg) { show_error($forum,"Шаблон темы не содержит элемента с классом message"); - exit; } # # Генерируем идентификатор записи. # - my $id = get_uid($forum); + my $id = "m".get_uid($forum); # # Сохраняем приаттаченные картинки, если есть. # - my $dir = $ENV{PATH_TRANSLATED}; - $dir=~ s/[^\/]+$// if (-f $dir); - my %attached; - for (my $i=1;defined $cgi->param("image$i"); $i++) { - my $userpath=$cgi->param("image$i"); - my $filename=lc($1) if $userpath =~ /([^\/\\]+)$/; - attached{$filename} = $id."_".$filename; - my $in = $cgi->upload("image$i"); - my $out; - open $out,">$dir/$attached{$filename}"; - binmode $out,":bytes"; - local $_=undef; - my $data = <$in>; - print $out $data; - close $in; - close $out; - } + 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 и чистим его # @@ -813,30 +1027,12 @@ sub reply { # Находим в тексте URL на приаттаченные картинки и меняем на те # имена, под которыми мы их сохранили. # - for my $image ($txtree->find_by_tag_name("img")) { - my $file; - if ( exists $attached{$file=lc($image->attr("src"))}) { - $image->attr("src" => $attached{$file}); - my ($width,$height) = imgsize($dir ."/".$attached{$file}); - $image->attr("width" =>$width); - $image->attr("height" => $height); - } - } - # - # Копируем элемент с классом message - # - my $newmsg = $messagetpl->clone; - my $parent = $messagetpl->parent; - $parent->push_content($newmsg); + fix_image_links($txtree,\%attached,$dir); # # Подставляем данные сообщения # $newmsg->attr("id"=>$id); - if (my $subj=$newmsg->look_down("class"=>"subject") && - $cgi->param("subject")) { - $subj->delete_content; - $subj->push_content($cgi->param("subject")); - } + substinfo($newmsg,[class=>"subject"],_content=>$cgi->param("subject")); my $textnode=$newmsg->look_down("class"=>"mtext"); if (!$textnode) { show_error($forum,"В шаблоне реплики нет места для текста"); @@ -853,70 +1049,901 @@ sub reply { # my $editform=$newmsg->look_down(_tag=>"form","class"=>"msginfo"); if ($editform) { - my $idfield = $editform->look_down(_tag=>"input","name"=>"id"); - if (!$idfield) { - show_error($forum,"В форме управления сообщением нет поля - id"); - } - $idfield->attr("value" => $id); - my $authorfield = $editform->look_down(_tag=>"input","name"=>"author"); - if (!$authorfield) { - show_error($forum,"В форме управления сообщением нет поля - id"); - } - $authorfield->attr("value"=>$forum->{authenticated}{user}); + 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 $date = $newmsg->look_down("class"=>"mdate"); - if ($date) { - $date->delete_content; - $date->push_content(strftime("%d.%m.%Y %H:%M",localtime())); - - } + my $posted = strftime("%d.%m.%Y %H:%M",localtime()); + substinfo($newmsg,["class"=>"mdate"], + _content =>$posted); # Подставляем mreply - my $reply_link = $newmsg->look_down(_tag=>"a","class"=>"mreply"); - $reply_link->attr("href"=> $cgi->url(-absolute=>1,-path_info=>1). - "?reply=1&id=$id") if ($reply_link); + substinfo($newmsg,[_tag=>"a","class"=>"mreply"],"href" => + $cgi->url(-absolute=>1,-path_info=>1)."?reply=1&id=$id"); # Подставляем manchor - my $anchor = $newmsg->look_down(_tag=>"a","class"=>"manchor"); - if (! $anchor) { - show_error($forum,"В шаблоне сообщения отсутствует якорь для - ссылок на него"); - exit; - } - $anchor->attr(href=>undef); - $anchor->attr(name=>"#$id"); + substinfo($newmsg,[_tag=>"a","class"=>"manchor"], + "name"=>"#$id","href"=>undef) or + show_error($forum,"В шаблоне сообщения отсутствует якорь для ссылок на него"); # подставляем mlink - my $link = $newmsg->look_down(_tag=>"a","class"=>"mlink"); - $link->attr(href=>$cgi->path_info."#id"); + substinfo($newmsg,[_tag=>"a","class"=>"mlink"], + href=>$cgi->path_info."#$id"); # подставляем mparent my $parent_id=$cgi->param("id"); - my $parent_link=$newmsg->lookdown(_tag => "a",class=>"mparent"); - if ($parent_link) { - if ($parent_id) { - $parent_link->attr("href"=>$cgi->path_info."#$parent_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 { - # Если parent_id отсутствует, т.е. это начало нового треда - # просто делаем ссылку невидимой. - $parent_link->delete_content(); + 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}; + + } + # Подставляем их куда надо + for $d (@dirs) { + 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 $msglist = $tree->look_down("class"=>"messagelist"); - if ($msglist) { - my $style = $msglist->attr("style"); - $msglist->attr("style",$style) if $style =~ s/display: none;//; + 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; # - # Делаем Уфф и сохраняем то, что получилось + # Сохраняем логотип # - savetree($ENV{PATH_TRANSLATED},$tree,$lockfd); - forum_redirect($cgi,$forum); - + 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}) { + $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($dir ."/".$forum->{indexfile},$tree); + 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, @@ -927,8 +1954,11 @@ sub getrights { if (!$forum->{authenticated}) { return undef; } + return $forum->{authenticated}{rights} if + exists $forum->{authenticated}{rights}; my $user = $forum->{authenticated}{user}; - my $dir = $ENV{'PATH_TRANSLATED'}; + my $dir = $path_translated; + $dir =~s/\/$//; $dir =~s/\/[^\/]+$// if (!-d $dir); my $f; my $user_status = "normal"; @@ -944,10 +1974,10 @@ sub getrights { chomp; if ($user eq $_ && defined $status) { if ($status eq "banned") { - return $status; + return $forum->{authenticated}{rights}=$status; } if ($status eq "admins" ) { - return "admin"; + return $forum->{authenticated}{rights}="admin"; } $user_status = "moderator"; } @@ -955,15 +1985,20 @@ sub getrights { } close $f; last LEVEL if -f "$dir/.forum"; - # Strip last path component. - $dir =~s/\/[^\/]+$// } + # Strip last path component. + $dir =~s/\/[^\/]+$// } - return $user_status; + return $forum->{authenticated}{rights}=$user_status; } + +#------------------------------------------------------------------ +# Работа с файлами и идентификторами +#------------------------------------------------------------------ + # # Залочить файл и получить его распрасенное представление. # Возвращает пару ($tree,$lockfd) @@ -973,42 +2008,100 @@ sub gettree { my $f; open $f,"<",$filename or return undef; flock $f, LOCK_EX; - my $tree = HTML::TreeBuilder->new_from_file($f); + my $tree = treefromfile($f); return ($tree,$f); } # # Сохранить дерево и закрыть lockfd. # # - -sub savetree { - my ($filename,$tree,$lockfd) = shift; - my $f; +sub replacefile { + my $filename = shift; + my $content = shift; + my $f; open $f,">",$filename . ".new" or return undef; - print $f $tree->as_HTML("<>&"); + 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"); + 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; - return sprintf ("%08s",$id); + $id=~/(\d+)/; + return sprintf ("%08s",$1); } # -------------------------------------------------------------------- # OpenID registration @@ -1016,12 +2109,13 @@ sub get_uid { sub create_openid_consumer { my ($cgi,$forum) = @_; return Net::OpenID::Consumer ->new( - ua => LWP::UserAgent->new(), + ua => LWP::UserAgent->new( agent => "Stilllife/1.0"), args => $cgi, consumer_secret=>"X9RWPo0rBE7yLja6VB3d", required_root => $cgi->url(-base=>1)); } + # openidstart - вызывается когда обнаружено что текущее имя # пользователя, пытающегося аутентифицироваться, содержит http:// # @@ -1057,7 +2151,7 @@ sub openidstart { # реплики) # sub openid_verify { - my ($cgi,$forum) = @_; + 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); @@ -1077,11 +2171,25 @@ sub openid_verify { my $username = $user; $username =~ s/^http:\/\///; if (!$userbase{$username}) { - $userbase{$username} = freeze($forum->{authenticated}={"openiduser"=>1}); + # Тащим 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, вызываем обработку реплики @@ -1096,6 +2204,51 @@ sub openid_verify { 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; +} #----------------------------------------------------------------- # Обработка форматированных текстовых полей #----------------------------------------------------------------- @@ -1104,18 +2257,16 @@ 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=~s/\r?\n\r?\n/<\/p>

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

".$text."

"; - } else { - $text="
".$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", @@ -1124,23 +2275,164 @@ sub input2tree { $element->delete() if defined $element; } } - # Проверяем на наличие URL-ок не оформленных ссылками. + # 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 = HTML::TreeBuilder->new(); + my $tree = make_tree(); + $tree->ignore_ignorable_whitespace(1); # Set parser options here - $tree->parse($data); + $tree->parse("
$data
"); $tree->eof; - return $tree; - + 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 $_; +}