X-Git-Url: http://www.wagner.pp.ru/gitweb/?a=blobdiff_plain;f=forum%2Fforum;h=7cb882b48c9c9234da43ab5d6d0ec89cd5dadeb5;hb=394bfadb608997e0366304e916f97e5ceb898161;hp=e73f98af81a9aef22cafb80c16275fd66918ca7f;hpb=8aba8ac06a5627acf0d529584df96bc8bf48a088;p=oss%2Fstilllife.git diff --git a/forum/forum b/forum/forum index e73f98a..7cb882b 100755 --- a/forum/forum +++ b/forum/forum @@ -31,7 +31,7 @@ use Net::OpenID::Consumer; my %actions = ( reply => \&reply, edit => \&edit_comment, - delete => \&delete_comment, + delete => \&delete_item, move => \&move_comment, newtopic=> \&new_topic, newforum=> \&new_forum, @@ -75,6 +75,8 @@ if ($cgi->request_method ne "POST") { logout('logout',$cgi,$forum); } elsif ($cgi->param("profile")) { show_profile("profile",$cgi,$forum); + } elsif ($cgi->param('delete')) { + delete_item("delete",$cgi,$forum); } else { for my $param ($cgi->param) { # Среди параметров, указанных в URL ищем тот, который задает @@ -167,7 +169,6 @@ sub get_forum_config { $path_translated=~s/\/+$//; my @path=split("/",$path_translated); while (@path>1) { - print STDERR "Searching for config in ",join("/",@path),"\n"; if (-r (my $config=join("/",@path,".forum")) ) { open F,"<",$config; my %config; @@ -327,8 +328,6 @@ sub prepare_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; if (defined $cgi->param($name)) { if ($element->tag eq "input") { @@ -353,6 +352,18 @@ sub prepare_template { $element->attr("value",$cgi->param($name)); } } elsif ($element->tag eq "textarea") { + my $data=$cgi->param($name); + if ($data=~/^<(div|p)\b/ && !defined($cgi->param($name."_format"))) { + if ($data=~/^
(.*)<\/div>$/) { + $cgi->param($name."_format","bbcode"); + my $parser = HTML::BBReverse->new(); + $data = $parser->reverse($1); + # FIXME elsif ($data=~/

) + } else { + $cgi->param($name."_format","html"); + } + + } $element->delete_content; $element->push_content($cgi->param($name)); } elsif ($element->tag eq "select") { @@ -360,7 +371,7 @@ sub prepare_template { my $value = $option->attr("value") || $option->as_text(); if (grep($value eq $_, $cgi->param($name))) { - $option->attr("selected",""); + $option->attr("selected","selected"); } else { $option->attr("selected",undef); } @@ -479,7 +490,6 @@ if (defined $user) { src=>$forum->{templatesurl}."/1x1.gif", width=>1,height=>1); } - delete $userinfo{"avatar"}; for my $element ( $tree->look_down("class",qr/^ap-/)) { my $field=$1 if $element->attr("class")=~/^ap-(.*)$/; @@ -492,15 +502,12 @@ if (defined $user) { my $data; if ($field eq "registered" || substr($field,0,5) eq "last_") { $data = strftime("%d.%m.%Y %H:%M",localtime($userinfo{$field})) - } elsif ($field=~/^<\w+/) { + } elsif ($userinfo{$field}=~/^<\w+/) { $data = str2tree($userinfo{$field}); } else { $data = $userinfo{$field} } $element->push_content($data); - if (ref($data)) { - $data->delete; - } $element->attr(href=>"mailto:$userinfo{$field}") if ($element->tag eq "a" && $field eq "email"); } @@ -665,6 +672,7 @@ 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; @@ -691,7 +699,6 @@ sub show_profile { 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); @@ -733,7 +740,6 @@ sub show_profile { } } # Подставляем аватарку - 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; @@ -751,7 +757,38 @@ sub show_profile { # 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); } # # Обработка результатов заполнения формы регистрации. @@ -784,6 +821,7 @@ sub register { } } } + $cgi->delete("required"); my %userbase; dbmopen %userbase,datafile($forum,"passwd"),0644 or form_error($formname,$cgi,$forum,"Ошибка открытия файла паролей $!"); @@ -791,35 +829,50 @@ 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; + my $path = $forum->{"userdir"}."/".$userprefix."_".$filename; + open $out,">",$path; binmode $out,":bytes"; my $buffer; while (my $bytes = read($f,$buffer,4096)) { @@ -827,16 +880,24 @@ sub register { } close $f; close $out; - my ($w,$h) = imgsize($forum->{"userdir"}."/".$filename); - $userinfo->{'avatar'}= {src=>$forum->{"userurl"}."/".$filename, + my ($w,$h) = imgsize($path); + $userinfo->{'avatar'}= {src=>$forum->{"userurl"}."/".$userprefix."_".$filename, width=>$w,height=>$h}; - $cgi->delete("avatar"); - $cgi->delete("avatarfile"); + } elsif ($cgi->param('avatar')) { + if (!ref($userinfo->{'avatar'}) || + $userinfo->{avatar}{'src'} ne $cgi->param('avatar')) { + $userinfo->{avatar}=get_avatar_info($cgi->param('avatar')); + } } - my @restrict = split /\s*,\s*/, $forum->{restricted_user_info}; - set_default_user_attrs($forum,$userinfo); + my @restrict=(); + @restrict = split /\s*,\s*/, $forum->{restricted_user_info} + unless $isadmin; + foreach my $param ($cgi->param) { next if (grep $_ eq $param,@restrict); + next if $param eq 'avatar'; + next if $param eq 'avatarfile'; + next if $param eq 'returnto'; next if $param =~ /_format$/; if (defined $cgi->param("${param}_format")) { my $tree = input2tree($cgi,$forum,$param); @@ -846,22 +907,14 @@ sub register { $userinfo->{$param} = $cgi->param($param); } } - $userinfo->{registered} = time; - if (exists $forum->{default_status}) { - $userinfo->{status} = $forum->{default_status}; - } - 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,$returnto); - } else { - # FIXME Email validation - # Email to admin - show_template("newuser",$cgi,$forum); - } +} +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 { @@ -891,7 +944,7 @@ sub show_user_page { my $userinfo =thaw($base{$user}); $userinfo->{"user"} = $user; substitute_user_info($block,$forum,$userinfo); - profile_links($block,$user,$rights,$forum); + profile_links($block,$user,$rights,$cgi,$forum); } $usertpl->delete; } else { @@ -905,7 +958,7 @@ sub show_user_page { $tree = gettemplate($forum,"user"); substinfo($tree,[_tag=>"title"],_content=>"Stilllife user: $user"); substitute_user_info($tree,$forum,$userinfo); - profile_links($tree,$user,$rights,$forum); + profile_links($tree,$user,$rights,$cgi,$forum); unless ($userinfo->{openiduser}) { for my $userlink ($tree->look_down(_tag => "a",class=>"author")) { $userlink->attr("href",undef); @@ -920,7 +973,7 @@ sub show_user_page { $page; } sub profile_links { - my ($tree,$user,$rights,$forum)=@_; + my ($tree,$user,$rights,$cgi,$forum)=@_; foreach my $profile_link ($tree->look_down(_tag=>"a", href=>qr/profile=/)) { if ((defined $rights && $rights eq "admin")|| @@ -928,7 +981,7 @@ sub profile_links { $forum->{authenticated}{user} eq $user)) { $profile_link->attr("href", - $cgi->url(-absolute=>1,-path_info=>0).$forum->{userurl}. + $ENV{'SCRIPT_NAME'}.$forum->{userurl}. "/".$user."?profile=1"); } else { $profile_link->delete(); @@ -977,17 +1030,11 @@ sub allow_operation { } 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 1 if ($accesslevel eq "moderator"); return 0 if $accesslevel eq "banned"; - return 0 if $permissions{$operation} eq "author" && $user ne $itemauthor; return 1; } @@ -1113,12 +1160,12 @@ sub reply { # # Делаем Уфф и сохраняем то, что получилось # - record_as_recent($forum,$newmsg->clone); + $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); - + forum_redirect($cgi,$forum,$cgi->path_info."#$id"); } sub update_topic_list { my ($forum,$topic,$count,$date) = @_; @@ -1152,8 +1199,8 @@ sub record_as_recent { $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};$i--) { + if (@msgs >= $forum->{replies_per_page}) { + for (my $i=$#msgs;$i>=$forum->{replies_per_page}-1;$i--) { $msgs[$i]->delete; } } @@ -1386,7 +1433,149 @@ sub new_forum { forum_redirect($cgi,$forum,$cgi->url(-base=>1).$url); } - + +# +# Обработка операции удаления всего на свете +# +sub delete_item { + my ($formname,$cgi,$forum) = @_; + # + # Сначала разберемся, что мы собственно удаляем + # + if ($cgi->param("user")) { + delete_user($cgi,$forum,$cgi->param("user")); + } elsif (-f $path_translated) { + if ($cgi->param("id")) { + delete_comment($cgi,$forum,$path_translated,$cgi->param("id")); + } else { + delete_topic($cgi,$forum,$path_translated); + } + } elsif (-d $path_translated && -f $path_translated ."/". $forum->{indexfile}) { + delete_forum($cgi,$forum,$path_translated); + } else { + 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 delete_comment { + my ($cgi,$forum,$topic,$id) = @_; + my ($tree,$lockfd) = gettree($topic); + my ($msg) = $tree->look_down(id => $id); + show_error("В данной теме нет реплики с id=$id") if (!$msg); + if (getrights($cgi,$forum) ne "moderator" + && getrights($cgi,$forum) ne "admin") { + my $author= $msg->look_down(_tag=>"input",name=>"author"); + if ($author->attr("value") ne $forum->{authenticated}{user}) { + show_error($forum,"У вас нет прав на удаление этого сообщения"); + } + } + delete_from_list($tree,"messagelist","message",$msg); + savetree($topic,$tree,$lockfd); + $tree->delete; + ($tree,$lockfd) = gettree($forum->{forumroot}."/recent.html"); + ($msg) = $tree->look_down(id => $id); + if ($msg) { + delete_from_list($tree,"messagelist","message",$msg); + savetree($forum->{forumroot}."/recent.html",$tree,$lockfd); + } else { + discardtree($tree,$lockfd); + } + # Если у реплики были картинки, то их тоже надо грохнуть + for my $pic (<$id."_*.*">) { + unlink $pic; + } + forum_redirect($cgi,$forum,$cgi->path_info); +} +# +# Удаление темы +# +sub delete_topic { + my ($cgi,$forum,$topic) = @_; + # Если модератор, то в тему и не заглядываем + my ($dir,$id) = ($1,$2) if $topic=~/^(.*)\/([^\/]+).html$/; + show_error($forum,"Неверное имя темы $topic") unless ($dir && $id); + if (getrights($cgi,$forum) ne "moderator" && + getrights($cgi,$forum) ne "admin") { + # Проверяем авторство темы и наличие в ней сообщений + my ($tree,$lockfd) = gettree($topic); + my $info = $tree->look_down(class=>"topic"); + my $author = $tree->look_down(class=>"author"); + if ($author->as_text() ne $forum->{authenticated}{user}) { + show_error($forum,"У вас нет права удалять эту тему"); + } + $info = $tree->look_down(class=>"messagelist"); + if ($info->attr("style") !~ /display: none;/) { + show_error($forum,"Невозможно удалить тему, содержащую сообщения"); + } + discardtree($tree,$lockfd); + } # FIXME при удалении непустой темы модератором почистить recent.html + my ($tree,$lockfd) = gettree($dir . "/".$forum->{indexfile}); + delete_from_list($tree,"topiclist","topic",$id); + savetree($tree,$dir ."/".$forum->{indexfile}); + unlink $topic; + my $redirect_url = $cgi->path_info; + $redirect_url =~ s/\/[^\/]*$//; + forum_redirect($cgi,$forum,$redirect_url); +} +# +# Удаление форума +# +sub delete_forum { + my ($cgi,$forum,$dir) = @_; + if (getrights($cgi,$forum) ne "moderator" && getrights($cgi,$forum) + ne "admin") { + show_error($forum,"У вас нет права удалять форумы"); + } + opendir DIR,$dir; + my $count = 0; + while ( my $entry=readdir DIR) { + next if $entry eq $forum->{indexfile}; + next if $entry eq "perms.txt"; + next if $entry =~ /^logo.[^.]+/; + show_error($forum,"Нельзя удалять корень форума" ) + if $entry eq ".forum"; + $count ++; + } + closedir DIR; + show_error("Нельзя удалять непустой форум") 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); +} + #---------------------------------------------------------- # База пользователей и права доступа #---------------------------------------------------------- @@ -1415,6 +1604,8 @@ sub getrights { if (!$forum->{authenticated}) { return undef; } + return $forum->{authenticated}{rights} if + exists $forum->{authenticated}{rights}; my $user = $forum->{authenticated}{user}; my $dir = $path_translated; $dir =~s/\/$//; @@ -1433,10 +1624,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"; } @@ -1448,7 +1639,7 @@ sub getrights { # Strip last path component. $dir =~s/\/[^\/]+$// } - return $user_status; + return $forum->{authenticated}{rights}=$user_status; } @@ -1487,6 +1678,13 @@ sub savetree { rename $filename.".new",$filename; close $lockfd if defined($lockfd); } + +sub discardtree { + my ($tree,$lockfd) = @_; + flock $lockfd,LOCK_UN; + close $lockfd; + $tree->delete; +} # # Cериализовать HTML-документ с DOCTYPE (workaround вокруг баги в # HTML::TreeBuilder) @@ -1564,6 +1762,7 @@ sub create_openid_consumer { required_root => $cgi->url(-base=>1)); } + # openidstart - вызывается когда обнаружено что текущее имя # пользователя, пытающегося аутентифицироваться, содержит http:// # @@ -1626,7 +1825,6 @@ sub openid_verify { } $info{"openiduser"}=1; $info{"registered"}=time; - print STDERR "forum $forum info ".\%info."\n"; set_default_user_attrs($forum,\%info); $info{'status'} = $forum->{openid_status} if $forum->{openid_status}; $forum->{authenticated}=\%info; @@ -1654,6 +1852,20 @@ sub openid_verify { } } +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); @@ -1664,17 +1876,11 @@ sub get_foaf { my $foaf = $response->content; my %info = foaf_parse($foaf); if ($info{avatar}) { - $response = $ua->get($info{avatar}); - if ($response->is_success) { - my $image = $response->content; - my ($w,$h,$type) = imgsize(\$image); - $info{avatar}={width=>$w,height=>$h,type=>$type,src=>$info{avatar}}; - } else { - print STDERR "Error getting $info{avatar}: ".$response->status_line,"\n"; - } + $info{avatar} = get_avatar_info($info{avatar},$ua); } return %info; } + sub foaf_parse { my $foaf = shift; my ($starttag) = $foaf =~ /<(\w+(:\w+)?[^>]+)>/sg; @@ -1723,12 +1929,17 @@ sub input2tree { sub str2tree { my ($data)=@_; my $tree = make_tree(); + $tree->ignore_ignorable_whitespace(1); # Set parser options here $tree->parse("

$data
"); $tree->eof; my $element=$tree->find("body"); while (($element =($element->content_list)[0])->tag ne "div") { } + while ($element->content_list==1&& $element->tag eq "div" && + !defined $element->attr("style")) { + $element = ($element->content_list)[0] + } $element->detach; $tree->destroy; return $element; @@ -1779,6 +1990,24 @@ sub hide_list { 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; +} +# # Найти все элементы, удоволетворяющие заданному критерию и подставить в # них указанные атрибуты # @@ -1835,3 +2064,4 @@ sub newlistelement { return undef; } } +