From a4625ed67e93896375abb4e4dbb5d6e7fc8c9ed4 Mon Sep 17 00:00:00 2001 From: Victor Wagner Date: Mon, 31 Mar 2008 18:35:57 +0000 Subject: [PATCH] *** empty log message *** --- doc/profile.txt | 13 +++ doc/templates.txt | 34 ++++-- forum/TODO | 30 ++++-- forum/forum | 262 ++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 287 insertions(+), 52 deletions(-) diff --git a/doc/profile.txt b/doc/profile.txt index 967be3b..2d59894 100644 --- a/doc/profile.txt +++ b/doc/profile.txt @@ -87,3 +87,16 @@ restricted_user_info, недоступны для редактирования Для этого следует не использовать в элементах option атрибут value. При отсутствии этого атрибута будет использовано значение текста пункта меню. + +Обязательные поля + +Если в форме регистрации присутсвует скрытое поле с названием required, +и его значение представляет собой перечисленный через запятуз список +полей, то незаполнение этих полей будут проинтерпретированы как ошибка. + +Поля форм регистрации и редактирования профиля, которые не являются +атрибутами пользователя. + +Если в форме присутствует скрытое поле ignore, значение которого +представляет собой список полей формы через запятую, значения этих полей +будут проигнорированы при создании профайла пользователя. diff --git a/doc/templates.txt b/doc/templates.txt index fde12d5..a995244 100644 --- a/doc/templates.txt +++ b/doc/templates.txt @@ -45,9 +45,15 @@ author - ник автора сообщения. innerHtml заменяется mdate - дата публикации сообщения innerHtml заменяется на дату avatar - элемент img атрибут src которого заменияется на аватар автора, или на templates/1x1.gif если у автора нет аватара. -astatus - статус автора на форуме innerHtml заменяется на статус -acomment - комментарий к нику, введенный автором при регистрации. +ap-status - статус автора на форуме innerHtml заменяется на статус +ap-comment - комментарий к нику, введенный автором при регистрации. innerHtml заменяется на комментарий +И прочие классы с префиксом ap-, innerHtml которых заменяеняется +на соответствущие поля из профайла автора. Если поле имеет в имени +подчерк, допустимо вместо подчерка использовать дефис "-" в названии +класа. + + msubject - тема сообщения. Заменяется innerHtml mtext - текст сообщения. innerHtml заменяется на отформатированный текст mreply - ссылка на скрипт ответа. Атрибут href будет заменен на @@ -146,18 +152,29 @@ mreply - ссылка на скрипт ответа. Атрибут href буд Страница списка тем (головная страница форума) -Может иметь элемент с классом header, описывающий форум в целом (его -создатель, вводный текст и т.д. +Может иметь элемент с классом annotation, описывающий форум в целом (его +создатель, вводный текст и т.д. устроенный внутри аналогично элементу +списка форумов (см ниже). Если в шаблоне присутствует элемент meta +name="description", то туда помещается текстовое представление аннотации +форума. + +Если в шаблоне присутствуют элементы с классом top-page, то они будут +сохранены только на головной странице форума, а при создании подфорумов +будут из их оглавлений удаляться. Шаблоном описания конкретной темы является элемент с классом topic, -устроенный аналогично message (показывается текст и автор первой реплики -темы) с той разницей, что элемент с классом subject должен быть ссылкой. +Содержащий элемент с классом title (название темы, должно быть +ссылкой), abstract (аннотация темы) +author (ссылка), date (дата создания темы), tlink (якорь для ссылок на +элемент списка тем ), last-updated и msgcount. Внутри элемента с классом topic должна присутствовать форма с кнопками edit delete move setrights и скрытым полем id. Кнопку setrights следует показывать только пользователю с правами администратора. + + элемент с классом topic должен быть заключен в элемент с классом topiclist. @@ -168,8 +185,9 @@ topiclist. вставляется непосредственно за предыдущим -Кроме этого, cтраница должна иметь ссылку с классом newtopic или форму с именем -newtopic и кнопкой submit с именем newtopic +Кроме этого, cтраница должна иметь ссылку на форумный скрипт с +параметром newtopic=1 или форму с именем +topicinfo и кнопкой submit с именем newtopic Список подфорумов устроен аналогично списку тем. diff --git a/forum/TODO b/forum/TODO index d499084..d107b2f 100644 --- a/forum/TODO +++ b/forum/TODO @@ -1,12 +1,20 @@ Roadmap по server-side части -1. процедура раскрутки -2. страничка юзера -3. список юзеров -4. редактирование user profile и фиксы в регистрации -5. Механизм регистрации с подтверждением -6. delete (message, topic, forum) -7. edit (message,topic,forum) -8. move (message,topic,forum) -9. setrights -10. applytemplates -11. Почтовые оповещения о новых репликах, RSS или recent comments page + ++ 1. Генерация валидного HTML 4.01 ++ 2. страничка юзера ++ 3. список юзеров (отпрофайлить) ++ 4. Показ формы редактирования профиля +5 редактирование user profile +6. Статистика по репликам на странице форума +7 recent comments page +8. Придумать способ прописывать в шаблонах ссылки на страницы +8. delete (message, topic, forum) +9. процедура раскрутки +10. Механизм регистрации с подтверждением +11. edit (message,topic,forum) +12. move (message,topic,forum) +13. setrights +14. applytemplates +15. Почтовые оповещения о новых репликах, RSS или +16. Блок top_page_only в шаблоне форума. + diff --git a/forum/forum b/forum/forum index 9f12ad8..d2ae686 100755 --- a/forum/forum +++ b/forum/forum @@ -73,6 +73,8 @@ if ($cgi->request_method ne "POST") { openid_verify($cgi,$forum); } elsif ($cgi->param("logout")) { logout('logout',$cgi,$forum); + } elsif ($cgi->param("profile")) { + show_profile("profile",$cgi,$forum); } else { for my $param ($cgi->param) { # Среди параметров, указанных в URL ищем тот, который задает @@ -243,7 +245,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) { @@ -254,7 +256,7 @@ 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 "Ошибка конфигурации форума", @@ -272,6 +274,17 @@ sub show_error { # подставляются # 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); +} +sub prepare_template { my ($form,$cgi,$forum) = @_; my $tree = gettemplate($forum,$form,$ENV{'PATH_INFO'}); @@ -314,21 +327,22 @@ sub show_template { ELEMENT: for my $element ($f->find_by_tag_name("textarea","input","select")) { my $name = $element->attr("name"); + #print STDERR "Found element <".$element->tag()." name=\"$name\">\n" ; + #print STDERR "Corresponding \$cgi->param($name)=\"",$cgi->param($name),"\"\n"; $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",""); @@ -343,7 +357,9 @@ sub show_template { $element->push_content($cgi->param($name)); } elsif ($element->tag eq "select") { for my $option ($element->find_by_tag_name("option")) { - if (grep($option->attr("value") eq $_, $cgi->param($name))) { + my $value = $option->attr("value") || + $option->as_text(); + if (grep($value eq $_, $cgi->param($name))) { $option->attr("selected",""); } else { $option->attr("selected",undef); @@ -363,13 +379,8 @@ sub show_template { $f->push_content($element); } } - - - print - $cgi->header(-type=>"text/html",-charset=>"utf-8",($forum->{cookies}?(-cookie=>$forum->{cookies}):())), - $tree->as_HTML("<>&"); - exit; -} + return $tree; +} # # Поправляет ссылки на служебные файлы и скрипты форума # @@ -392,10 +403,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); @@ -408,6 +420,11 @@ sub fix_forum_links { 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}) ; @@ -416,7 +433,7 @@ 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 } @@ -515,7 +532,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; @@ -654,6 +670,90 @@ sub forum_redirect { 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; + print STDERR "Substituting userinfo for $user\n"; + while(my ($field,$value) = each %$userinfo) { + $value = $value->{src} if ($field eq 'avatar' && ref($value)); + $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; + } + } + } + } + # Подставляем аватарку + print STDERR "avatar=",$userinfo->{avatar},"\n"; + 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) = @_; + +} +# # Обработка результатов заполнения формы регистрации. # # @@ -813,8 +913,7 @@ sub show_user_page { } } } - my $page = - $tree->as_HTML("<>&"); + 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}):())), @@ -825,7 +924,7 @@ sub profile_links { foreach my $profile_link ($tree->look_down(_tag=>"a", href=>qr/profile=/)) { if ((defined $rights && $rights eq "admin")|| - (defined $forum->{autheticated}{user} && + (defined $forum->{authenticated}{user} && $forum->{authenticated}{user} eq $user)) { $profile_link->attr("href", @@ -911,7 +1010,7 @@ sub reply { # # Генерируем идентификатор записи. # - my $id = get_uid($forum); + my $id = "m".get_uid($forum); # @@ -984,8 +1083,9 @@ sub reply { show_error($forum,"В форме управления сообщением нет поля author"); } # Подставляем mdate + my $posted = strftime("%d.%m.%Y %H:%M",localtime()); substinfo($newmsg,["class"=>"mdate"], - _content =>strftime("%d.%m.%Y %H:%M",localtime())); + _content =>$posted); # Подставляем mreply substinfo($newmsg,[_tag=>"a","class"=>"mreply"],"href" => $cgi->url(-absolute=>1,-path_info=>1)."?reply=1&id=$id"); @@ -1005,15 +1105,58 @@ sub reply { substinfo($newmsg,[_tag => "a",class=>"mparent"], style=>"display: none;"); } - + my $msgcount=0; + for my $msg ($newmsg->parent->look_down("class"=>"message")) { + $msgcount ++; + } + # # Делаем Уфф и сохраняем то, что получилось # + record_as_recent($forum,$newmsg->clone); savetree($path_translated,$tree,$lockfd); record_statistics($forum,"message"), + update_topic_list($forum,$path_translated,$msgcount,$posted); forum_redirect($cgi,$forum); } +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 $style = $msglist->attr("style"); + if ($style && $style =~ s/display: none;//) { + $msglist->attr("style",$style); + $msglist->look_down(class=>"message")->replace_with($msg); + } else { + my $prev = $msglist->look_down("class"=>"message"); + $prev->preinsert($msg); + } + } + savetree($forum->{forumroot}."/recent.html",$tree,$lockfd); +} # # Обработка операции создания новой темы. # @@ -1087,10 +1230,12 @@ sub new_topic { $newtopic->attr("id",$urlname); my $controlform = $newtopic->look_down(_tag=>"form",class=>"topicinfo"); if ($controlform) { - $controlform->attr("action"=>$url); + $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); @@ -1159,8 +1304,15 @@ sub new_forum { # 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); @@ -1209,10 +1361,24 @@ sub new_forum { $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); } @@ -1252,7 +1418,6 @@ sub getrights { my $user_status = "normal"; LEVEL: while (length($dir)) { - print STDERR "Searcghing for perms in $dir\n"; if (-f "$dir/perms.txt") { open $f,"<","$dir/perms.txt"; my $status = undef; @@ -1297,7 +1462,8 @@ 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); + $tree->parse_file($f); return ($tree,$f); } # @@ -1309,7 +1475,7 @@ sub savetree { my ($filename,$tree,$lockfd) = @_; my $f; open $f,">",$filename . ".new" or return undef; - print $f $tree->as_HTML("<>&"); + print $f output_html($tree); close $f; # FIXME - только для POSIX. unlink $filename; @@ -1317,6 +1483,15 @@ sub savetree { close $lockfd if defined($lockfd); } # +# Cериализовать HTML-документ с DOCTYPE (workaround вокруг баги в +# HTML::TreeBuilder) +# +sub output_html { + my $tree=shift; + return ''. + $tree->as_HTML("<>&"); +} +# # Читает шаблон и подготавливает его к размещению по указанной URL. # Если url не указана, считается что шаблон будет показан как результат # текущего http-запроса. @@ -1329,12 +1504,33 @@ sub gettemplate { show_error($forum,"Нет шаблона $template"); exit; } - my $tree = HTML::TreeBuilder->new_from_file($filename); + 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; +} # # Получает уникальный числовой идентификатор. # @@ -1521,7 +1717,7 @@ sub input2tree { sub str2tree { my ($data)=@_; - my $tree = HTML::TreeBuilder->new(); + my $tree = make_tree(); # Set parser options here $tree->parse("
$data
"); $tree->eof; -- 2.39.2