# Selima Website Content Management System # List.pm: The base list. # Copyright (c) 2004-2018 imacat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Author: imacat # First written: 2004-09-09 package Selima::List; use 5.008; use utf8; use strict; use warnings; BEGIN { # Prototype declaration sub urlcheck(\@); } use CGI qw(); use Encode qw(encode decode FB_CROAK); use HTML::Strip qw(); use IPC::Open3 qw(open3); use POSIX qw(floor); use Selima::AddGet; use Selima::Array; use Selima::CommText; use Selima::ChkFunc; use Selima::DataVars qw($SESSION :db :env :l10n :libdir :lninfo :requri); use Selima::DBI; use Selima::ErrMsg; use Selima::FormFunc; use Selima::GetLang; use Selima::ListPref; use Selima::LogIn; use Selima::HTTP; use Selima::MarkAbbr; use Selima::Query; use Selima::ShortCut; use Selima::Unicode; use Selima::UserPref; # Load these classes use Selima::List::Users; use Selima::List::Groups; use Selima::List::UserMem; use Selima::List::GroupMem; use Selima::List::UserPref; use Selima::List::ScptPriv; use Selima::List::Guestbook; use Selima::List::Guestbook::Public; use Selima::List::Pages; use Selima::List::LinkCat; use Selima::List::Links; use Selima::List::LinkCatz; use Selima::List::Category; use Selima::List::Categorz; use Selima::List::ActLog; use Selima::List::Accounting::Subjects; use Selima::List::Accounting::Subjects::LastLv; use Selima::List::Accounting::Transacts; use Selima::List::Accounting::Records; use Selima::List::Accounting::Reports; use Selima::List::Accounting::Reports::Cash::Summary; use Selima::List::Accounting::Reports::Cash; use Selima::List::Accounting::Reports::Ledger; use Selima::List::Accounting::Reports::Ledger::Summary; use Selima::List::Accounting::Reports::Journal; use Selima::List::Accounting::Reports::TriBlnc; use Selima::List::Accounting::Reports::IncmStat; use Selima::List::Accounting::Reports::BlncShet; use Selima::List::Accounting::Reports::Search; use vars qw(@URLCHECK); @URLCHECK = (COMMON_LIBDIR . "/Selima/urlcheck"); # new: Initialize the handler sub new : method { local ($_, %_); my ($self, $class, $FORM, $table); ($class, $FORM, $table) = @_; $class = ref $class if ref $class ne ""; $self = bless {}, $class; $FORM = curform if !defined $FORM; $self->{"FORM"} = $FORM; $self->{"table"} = $table; $self->{"pageno"} = $FORM->param("pageno"); $self->{"sortby"} = $FORM->param("sortby"); $self->{"query"} = $FORM->param("query"); $self->{"useview"} = $DBH->support(DBI_FEATHER_VIEW) if defined $DBH; $self->{"reverse"} = 0; $self->{"static"} = 0; $self->{"static_filepat"} = "%04d.html"; $self->{"static_lastfile"} = "last.html"; $self->{"limit"} = undef; # The default column labels # Parameters for the called forms $self->{"caller"} = $FORM->param("caller"); $self->{"cformid"} = $FORM->param("cformid"); $self->{"is_called_form"} = defined $self->{"caller"} && defined $self->{"cformid"}; if ($self->{"is_called_form"}) { $self->{"seltext"} = C_("Select"); $self->{"selurl_tmpl"} = $self->{"caller"} . "?formid=" . $self->{"cformid"} . "&selsn=%d"; $self->{"title"} = C_("Select A Data Record"); } else { $self->{"seltext"} = C_("Edit"); $self->{"selurl_tmpl"} = $REQUEST_FILE . "?form=cur&sn=%d"; $self->{"title"} = C_("Manage Data"); } $self->{"fetched"} = 0; $self->{"error"} = {}; # The default number of rows per page -- when user preference system is not available $self->{"DEFAULT_LIST_SIZE"} = 10; $self->{"DEFAULT_LIST_COLS"} = [qw(sn)]; # The default sort order -- for non-VIEW lists only $self->{"DEFAULT_SORTBY"} = undef; # The list brief size $self->{"DEFAULT_BRIEF_LEN"} = 30; # The query abstract span from the found phrase $self->{"QABS_SPAN"} = 30; # The maximum query abstract length $self->{"QABS_MAXLEN"} = 200; $self->{"total"} = undef; $self->{"lastpage"} = undef; $self->{"massdel"} = 0; $self->{"noselect"} = 0; $self->{"col_labels"} = {}; # Default settings # Known columns that should not be displayed (has a special purpose) $self->{"COLS_NO_DISPLAY"} = [qw(_viewurl _sel _selurl _statord)]; # Known columns that should never be searched against $self->{"COLS_NO_SEARCH"} = [qw(pic _urlcheck _viewurl _sel _selurl _statord)]; # Known columns that should not be sorted with $self->{"COLS_NO_SORT_BY"} = [qw(_urlcheck _viewurl _sel _selurl _statord)]; # Columns that should display its brief instead $self->{"COLS_BRIEF"} = []; # Intermediate parsing results when fetching a list $self->{"coldefs"} = []; $self->{"sortkeys"} = []; $self->{"query_phrases"} = []; $self->{"listcols"} = []; # The default column labels $self->col_labels( # Common labels shared by all list handlers "sn" => C_("S/N"), "created" => C_("Created"), "createdby" => C_("Created by"), "updated" => C_("Updated"), "updatedby" => C_("Updated by"), # Other commonly-seen column labels "body" => C_("Content"), "cat" => C_("Category"), "date" => C_("Date"), "disabled" => C_("Disabled?"), "dsc" => C_("Description"), "email" => C_("E-mail"), "hid" => C_("Hidden?"), "html" => C_("HTML?"), "id" => C_("ID."), "kw" => C_("Keywords"), "name" => C_("Name"), "ord" => C_("Order"), "path" => C_("Page path"), "pic" => C_("Picture"), "picratio" => C_("Pic. ratio"), "piccap" => C_("Pic. caption"), "picpos" => C_("Pic. position"), "title" => C_("Title"), "title_en" => C_("English title"), "url" => C_("URL."), "_urlcheck" => C_("Status (slow)"), ); return $self; } # fetch: Fetch the current list sub fetch : method { local ($_, %_); my ($self, $cols, $table, $where, $orderby, $limit, $sth, $sql, $error); $self = $_[0]; # Fetched before return $self->{"error"} if $self->{"fetched"}; $self->{"fetched"} = 1; # Initialize the error status $self->{"error"} = undef; $self->{"total"} = undef; # See if we need to use views or not. # Views make things much faster and easier, but some DBMS has no views. # *MySQL has no views* if ($self->{"useview"}) { ($cols, $table, $where, $orderby, $limit) = $self->select_with_view; } else { ($cols, $table, $where, $orderby, $limit) = $self->select_without_view; } # The number of rows per page $_ = userpref("listsize", ref $self); $self->{"pagesize"} = defined $_? $_: $self->{"DEFAULT_LIST_SIZE"}; # Paging not in use if (!defined $self->{"pagesize"}) { $self->{"select"} = sprintf "SELECT %s FROM %s%s%s%s;\n", $cols, $table, $where, $orderby, $limit; $sql = $self->{"select"}; $sth = $DBH->prepare($sql); $sth->execute; # Fetch everything $self->{"current"} = []; push @{$self->{"current"}}, $_ while defined($_ = $sth->fetchrow_hashref); undef $sth; $self->{"total"} = scalar(@{$self->{"current"}}); $self->{"lastpage"} = 1; $self->{"startno"} = 0; $self->{"endno"} = $self->{"total"} - 1; # If endno is -1 (when total is 0), set to 0 $self->{"endno"} = 0 if $self->{"endno"} < 0; # Done return $self->{"error"}; } # Obtain the total number $self->{"select_total"} = sprintf "SELECT count(*) FROM %s%s%s;\n", $table, $where, $limit; $sql = $self->{"select_total"}; $sth = $DBH->prepare($sql); $sth->execute; $self->{"total"} = ($sth->fetchrow_array)[0]; undef $sth; $self->{"lastpage"} = floor(($self->{"total"} - 1) / $self->{"pagesize"}) + 1; # If last page is 0 (when total is 0), set to page 1 $self->{"lastpage"} = 1 if $self->{"lastpage"} < 1; # Check the page number $error = $self->check_pageno; $self->{"error"} = $error if defined $error && !defined $self->{"error"}; # Calculate the start and end record number $self->{"startno"} = ($self->{"pageno"} - 1) * $self->{"pagesize"}; $self->{"endno"} = $self->{"pageno"} * $self->{"pagesize"} - 1; # If there is not enough remaining records, set to the last one $self->{"endno"} = $self->{"total"} - 1 if $self->{"endno"} > $self->{"total"} - 1; # If the last record is -1 (when total is 0), set to 0 $self->{"endno"} = 0 if $self->{"endno"} < 0; # Obtain everything in this page $self->{"current"} = []; if (!$self->{"reverse"}) { $self->{"select"} = sprintf "SELECT %s FROM %s%s%s LIMIT %d OFFSET %d;\n", $cols, $table, $where, $orderby, $self->{"endno"} - $self->{"startno"} + 1, $self->{"startno"}; } else { $self->{"select"} = sprintf "SELECT %s FROM %s%s%s LIMIT %d OFFSET %d;\n", $cols, $table, $where, $orderby, $self->{"endno"} - $self->{"startno"} + 1, $self->{"total"} - $self->{"endno"} - 1; } # If not empty if ($self->{"total"} > 0) { $sql = $self->{"select"}; $sth = $DBH->prepare($sql); $sth->execute; push @{$self->{"current"}}, $_ while defined($_ = $sth->fetchrow_hashref); undef $sth; } # Set the columns to be displayed $self->check_listcols; # Done return $self->{"error"}; } # page_param: Obtain page parameters sub page_param : method { local ($_, %_); my ($self, $baseurl); $self = $_[0]; # Fetch the current list if not fetched yet $self->fetch if !$self->{"fetched"}; $_ = {}; # The onload event handler $$_{"onload"} = $self->{"onload"} if exists $self->{"onload"}; # No paging return if !defined $self->{"DEFAULT_LIST_SIZE"}; # Do not show the list return if !defined $self->{"total"}; # No record to be listed return if $self->{"total"} == 0; # Our base URL $baseurl = rem_get_arg $REQUEST_FILEQS, "pageno"; # The first page -- only meaningful when there is more than one page if ($self->{"lastpage"} > 1) { if ($self->{"static"}) { $$_{"first"} = sprintf $self->{"static_filepat"}, 1; } elsif ($self->{"reverse"}) { $$_{"first"} = add_get_arg $baseurl, "pageno", 1, DUP_OK; } else { $$_{"first"} = $baseurl; } } # The previous page if ($self->{"pageno"} > 1) { if ($self->{"static"}) { $$_{"prev"} = sprintf $self->{"static_filepat"}, $self->{"pageno"} - 1; } elsif ($self->{"reverse"} || $self->{"pageno"} -1 != 1) { $$_{"prev"} = add_get_arg $baseurl, "pageno", $self->{"pageno"} - 1, DUP_OK; } else { $$_{"prev"} = $baseurl; } } # The next page if ($self->{"pageno"} < $self->{"lastpage"}) { if ($self->{"static"}) { if ( defined $self->{"static_lastfile"} && $self->{"pageno"} + 1 == $self->{"lastpage"}) { $$_{"next"} = $self->{"static_lastfile"}; } else { $$_{"next"} = sprintf $self->{"static_filepat"}, $self->{"pageno"} + 1; } } elsif (!$self->{"reverse"} || $self->{"pageno"} + 1 != $self->{"lastpage"}) { $$_{"next"} = add_get_arg $baseurl, "pageno", $self->{"pageno"} + 1, DUP_OK; } else { $$_{"next"} = $baseurl; } } # The last page -- only meaningful when there is more than one page if ($self->{"lastpage"} > 1) { if ($self->{"static"}) { if (defined $self->{"static_lastfile"}) { $$_{"last"} = $self->{"static_lastfile"}; } else { $$_{"last"} = sprintf $self->{"static_filepat"}, $self->{"lastpage"}; } } elsif (!$self->{"reverse"}) { $$_{"last"} = add_get_arg $baseurl, "pageno", $self->{"lastpage"}, DUP_OK; } else { $$_{"last"} = $baseurl; } } return $_; } # html: Output the list sub html : method { local ($_, %_); my $self; $self = $_[0]; # Fetch the current list if not fetched yet $self->fetch if !$self->{"fetched"}; # Display the title $self->html_title; # Display the error message $self->html_errmsg; # Display a link to add a new item $self->html_newlink; # Display the switch for different lists $self->html_lists_switch; # Display the search box $self->html_search; # Display the data download link $self->html_data_download; # Display the list status message $self->html_liststat; # Display the page bar at the beginning $self->html_pagebar; # List the items $self->html_list; # Display the page bar at the END $self->html_pagebar; # Display a form to change the list preference $self->html_listprefform; return; } # set_listpref: Set the list preference sub set_listpref : method { local ($_, %_); my $self; $self = $_[0]; $_ = new Selima::ListPref($self->{"FORM"}); $_->main; } # # Methods belows are private. Do not call them directly. # Override them when needed. # # liststat_message: Return the current list statistics message sub liststat_message : method { local ($_, %_); my $self; $self = $_[0]; # No record to list if ($self->{"total"} == 0) { # Empty comes from a query if (defined $self->{"query"}) { return C_("Nothing found. Please try another query."); # Empty database } else { return C_("The database is empty."); } # Fit in one page } elsif ($self->{"total"} <= $self->{"pagesize"}) { # Result comes from a query if (defined $self->{"query"}) { return C_("Your query found [*,_1,record].", $self->{"total"}); # List result } else { return C_("[*,_1,record].", $self->{"total"}); } # More than one page } else { # Result comes from a query if (defined $self->{"query"}) { return C_("Your query found [*,_1,record], listing [#,_2] to [#,_3].", $self->{"total"}, $self->{"startno"}+1, $self->{"endno"}+1); # List result } else { return C_("[*,_1,record], listing [#,_2] to [#,_3].", $self->{"total"}, $self->{"startno"}+1, $self->{"endno"}+1); } } } # pagebar_tags: The tags for the page bar # No arrows anymore. No more ASCII-art, as per WCAG 1.0 checkpoint 1.1 sub pagebar_tags : method { local ($_, %_); return ( first => h_abbr(C_("First")), prev => h_abbr(C_("Previous")), next => h_abbr(C_("Next")), last => h_abbr(C_("Last")), ); } # colval: Output a list column value sub colval : method { local ($_, %_); my ($self, $col, %row, $brflen, $alt); ($self, $col, %row) = @_; # Null/no value return h(t_notset()) if !defined $row{$col}; # A brief should be displayed instead $brflen = $self->{"DEFAULT_BRIEF_LEN"}; if (in_array($col, @{$self->{"COLS_BRIEF"}}) && length $row{$col} > $brflen) { # Strip the HTML tags $_ = $row{$col}; if (exists $row{"html"} && $row{"html"}) { # HTML::Strip does not work with decode()d UTF-8 text $_ = encode("UTF-8", $_); $_ = (new HTML::Strip)->parse($_); $_ = decode("UTF-8", $_); } return h(substr $_, 0, $brflen) . "…"; } # Always display "pic" column as a picture # To be done if ($col eq "pic") { #$alt = C_("Picture preview"); #$picid = readpic($row{$col}, # array("max" => $self->{"PIC_THUMBNAIL_SIZE"}), # $row["sn"], $self->{"table"}); #$PICS =& pic_deposit; #$pic = $PICS[$picid]; #return echopic_thumbnail($pic, $alt); return ""; } # Always display "_imgsrc" column as image reference if ($col eq "_imgsrc") { return "\"""; } # Always display "_urlcheck" unencoded, since it is a result text return h($row{$col}) if $col eq "_urlcheck"; # Ordinary columns return h($row{$col}); } # check_pageno: Check the page number sub check_pageno : method { local ($_, %_); my $self; $self = $_[0]; # Save it elsewhere and replace with default value temporarily $_ = $self->{"pageno"}; $self->{"pageno"} = !$self->{"reverse"}? 1: $self->{"lastpage"}; # Page number not specified return if !defined $_; # It is too long, or contains any non-digit character return {"msg"=>N_("Page number ([_1]) invalid. Please specify a valid page number."), "margs"=>[$_]} if length $_ > 9 || /\D/ || /^0+$/; $_ += 0; # Out of range return {"msg"=>N_("Page number ([#,_1]) out of range. Please specify between 1 and [#,_2]."), "margs"=>[$_, $self->{"lastpage"}]} if defined $self->{"lastpage"} && $_ > $self->{"lastpage"}; # Redirect to the page without page number, if it can be omitted if ($self->{"pageno"} == $_) { my ($method, $url); $method = $IS_MODPERL? ($IS_MP2? Apache2::RequestUtil->request->method: Apache->request->method): $ENV{"REQUEST_METHOD"}; $url = rem_get_arg $REQUEST_FILEQS, "pageno"; if ($method eq "POST") { http_303 $url; } else { http_307 $url; } } # OK $self->{"pageno"} = $_; return; } # # Methods belows are private. Do not call them directly. # Do not override them, either. # # select_with_view: Obtain the SQL statement with views # This makes life easier *^_^* sub select_with_view : method { local ($_, %_); my ($self, $cols, $table, $where, $orderby, $limit); $self = $_[0]; # Obtain the view name if (!exists $self->{"view"}) { $self->{"view"} = $self->{"table"} . "_list"; $self->{"view"} .= "_" . getlang LN_DATABASE if @ALL_LINGUAS > 1; } # Obtain the available columns $self->{"cols"} = [$DBH->cols($self->{"view"})]; $cols = "*"; $table = $DBH->quote_identifier($self->{"view"}); # Obtain the SQL WHERE phase $where = $self->sql_filter(); # Obtain the SQL ORDER BY phase $orderby = $self->sql_orderby(); # Obtain the LIMIT phase $limit = defined $self->{"limit"}? " LIMIT " . $self->{"limit"}: ""; return ($cols, $table, $where, $orderby, $limit); } # select_without_view: Obtain the SQL statement manually without views # *BAD BAD BAD~~ Stupid MySQL* Keep it here for compatibility # Should be remove as long as MySQL is vanished sub select_without_view : method { local ($_, %_); my $self; $self = $_[0]; # To be done return $self->select_with_view; } # sql_filter: Get the SQL WHERE phase sub sql_filter : method { local ($_, %_); my ($self, @cols, @conds); $self = $_[0]; # No query, return empty string if (!defined $self->{"query"}) { return " WHERE " . $_ if $self->can("pre_filter") && defined($_ = $self->pre_filter()); return ""; } # Regularize it $self->{"query"} =~ s/^\s*(.*?)\s*$/$1/; # Check if it is filled if ($self->{"query"} eq "") { $self->{"error"} = {"msg"=>N_("Please fill in your query.")}; return " WHERE " . $_ if $self->can("pre_filter") && defined($_ = $self->pre_filter()); return ""; } $self->{"query_phrases"} = [parse_query $self->{"query"}]; # Bounce if nothing to query if (@{$self->{"query_phrases"}} == 0) { return " WHERE " . $_ if $self->can("pre_filter") && defined($_ = $self->pre_filter()); return ""; } # Obtain the columns to query if ($self->{"useview"}) { %_ = map { $_ => 1 } @{$self->{"COLS_NO_SEARCH"}}; @cols = grep !exists $_{$_}, @{$self->{"cols"}}; # Use the column definition kept so far } else { @cols = map ${$_}{"def"}, grep !in_array(${$_}{"alias"}, @{$self->{"COLS_NO_SEARCH"}}), @{$self->{"coldefs"}} } # Compose the query condition @conds = qw(); # Obtain each phase foreach my $phrase (@{$self->{"query_phrases"}}) { my (@subconds, $lphrase); $lphrase = "'%" . $DBH->esclike($phrase) . "%'"; @subconds = qw(); foreach my $col (@cols) { push @subconds, "cast($col AS text) ILIKE $lphrase"; } push @conds, join " OR ", @subconds; } # Append the the pre-defined filter push @conds, $_ if $self->can("pre_filter") && defined($_ = $self->pre_filter()); # Compose the WHERE statement return " WHERE " . (@conds == 1? $conds[0]: join " AND ", map "($_)", @conds); } # sql_orderby: Get the SQL ORDER BY phase sub sql_orderby : method { local ($_, %_); my ($self, @phrases, $error); $self = $_[0]; # Parse the "sortby" argument $self->parse_sortby($self->{"sortby"}); # Check the sort keys, and empty them if invalid $error = $self->check_sortkeys; if (defined $error) { $self->{"error"} = $error if !defined $self->{"error"}; $self->{"sortkeys"} = []; } # Apply DEFAULT_SORTBY if not in a view if ( @{$self->{"sortkeys"}} == 0 && !$self->{"useview"} && defined $self->{"DEFAULT_SORTBY"}) { # Parse the DEFAULT_SORTBY argument $self->parse_sortby($self->{"DEFAULT_SORTBY"}); # Check the sort keys, and empty them if invalid $error = $self->check_sortkeys; if (defined $error) { $self->{"error"} = $error if !defined $self->{"error"}; $self->{"sortkeys"} = []; } } # Set the "sortby" attribute $self->compose_sortby; # Bounce if there is no sorting key return "" if @{$self->{"sortkeys"}} == 0; # Obtain the corresponding SQL phrase @_ = qw(); @_ = map ${$_}{"sql"}, @{$self->{"sortkeys"}}; $_ = " ORDER BY " . join ", ", @_; return $_; } # parse_sortby: Parse the "sortby" argument # $sortby argument should be specified as "key1,-key2,...", # where initial minus (-) before the key means decreasing. sub parse_sortby : method { local ($_, %_); my ($self, $sortby); ($self, $sortby) = @_; $self->{"sortkeys"} = []; # Bounce for nothing return if !defined $sortby; $sortby =~ s/^\s*(.*?)\s*$/$1/; # Bounce if $sortby is empty return if $sortby eq ""; # Split by comma foreach my $phrase (split /,/, $sortby) { # Compose the sort key $_{"key"} = $phrase; $_{"key"} =~ s/^\s*(.*?)\s*$/$1/; $_{"desc"} = 0; $_ = $_{"key"}; $_{"sql"} = $DBH->quote_identifier(encode("UTF-8", $_, FB_CROAK)); # Check the decreasing flag with the initial "-" sign if ($_{"key"} =~ s/^-//) { $_{"desc"} = 1; $_ = $_{"key"}; $_{"sql"} = $DBH->quote_identifier(encode("UTF-8", $_, FB_CROAK)) . " DESC"; } # Add this sort key push @{$self->{"sortkeys"}}, {%_}; } return; } # check_sortkeys: Check if the sorting keys are valid sub check_sortkeys : method { local ($_, %_); my $self; $self = $_[0]; # Skip if nothing to check return if @{$self->{"sortkeys"}} == 0;; # Obtain the valid sorting keys if ($self->{"useview"}) { %_ = map { $_ => 1 } @{$self->{"COLS_NO_SORT_BY"}}; %_ = map { $_ => 1 } grep !exists $_{$_}, @{$self->{"cols"}}; # Check each candidate foreach (@{$self->{"sortkeys"}}) { return {"msg"=>N_("You cannot sort by \"[_1]\"."), "margs"=>[${$_}{"key"}]} if !exists $_{${$_}{"key"}}; } # Use the column definition kept so far } else { # Turn the column definition into an associative array %_ = map { ${$_}{"alias"} => ${$_}{"def"} } @{$self->{"coldefs"}}; # Check each candidate foreach (@{$self->{"sortkeys"}}) { return {"msg"=>N_("You cannot sort by \"[_1]\"."), "margs"=>[${$_}{"key"}]} if !exists $_{${$_}{"key"}}; # Reset the SQL according to the column definition ${$_}{"sql"} = $_{${$_}{"key"}}; ${$_}{"sql"} .= " DESC" if ${$_}{"desc"}; } } # OK return; } # compose_sortby: Compose the "sortby" argument from sorting keys sub compose_sortby : method { local ($_, %_); my $self; $self = $_[0]; # Bounce if there is no sorting keys if (@{$self->{"sortkeys"}} == 0) { $self->{"sortby"} = ""; return; } @_ = qw(); foreach (@{$self->{"sortkeys"}}) { if (${$_}{"desc"}) { push @_, "-" . ${$_}{"key"}; } else { push @_, ${$_}{"key"}; } } $self->{"sortby"} = join ",", @_; return; } # col_labels: Set the column labels sub col_labels : method { local ($_, %_); my ($self, %labels); ($self, %labels) = @_; %{$self->{"col_labels"}} = (%{$self->{"col_labels"}}, %labels); return; } # check_listcols: Set the columns to be displayed sub check_listcols : method { local ($_, %_); my ($self, @listcols, @validcols); $self = $_[0]; # The columns to be displayed $_ = userpref("listcols", ref($self)); if (defined $_) { @listcols = split / /, encode("US-ASCII", $_, FB_CROAK); } else { @listcols = @{$self->{"DEFAULT_LIST_COLS"}}; } # Obtain the columns to list # Remove columns not to be displayed %_ = map { $_ => 1 } @{$self->{"COLS_NO_DISPLAY"}}; @_ = grep !exists $_{$_}, @{$self->{"cols"}}; # Only take valid columns in the preferenced columns %_ = map { $_ => 1 } @_; $self->{"listcols"} = [grep exists $_{$_}, @listcols]; return; } # query_abstract: Get the abstract regarding to the query phrase # It always work with the "body" column sub query_abstract : method { local ($_, %_); my ($self, $current, $body, @queries, $query, $start, $end, @union); my ($len_andsoon, $len, $reached_maximum, $union, $needlen); ($self, $current) = @_; # The column $body = $$current{"body"}; # Strip the HTML tags if (exists $$current{"html"} && $$current{"html"}) { # HTML::Strip does not work with decode()d UTF-8 text $body = encode("UTF-8", $body); $body = (new HTML::Strip)->parse($body); $body = decode("UTF-8", $body); $body = dh($body); } # Trim excess spaces $body =~ s/\s+/ /g; # Sort the query phrases by their lengths if (exists $self->{"query_zh_phrases"}) { @queries = sort { length $b <=> length $a } @{$self->{"query_zh_phrases"}}; } else { @queries = sort { length $b <=> length $a } @{$self->{"query_phrases"}}; } # Gather the abstract of each query phrase @_ = qw(); foreach my $query (@queries) { my $base; $base = 0; # Gather each match while (substr($body, $base) =~ /^(.*?)\Q$query\E/i) { $_ = $base + length $1; $start = $_ - $self->{"QABS_SPAN"}; $start = 0 if $start < 0; $end = $_ + length($query) + $self->{"QABS_SPAN"}; $end = length $body if $end > length $body; push @_, { "start" => $start, "end" => $end, }; $base = $_ + length $query; } } # Sanity check return undef if @_ == 0; # Sort the ranges @_ = sort { $$a{"start"}<=>$$b{"start"} || $$a{"end"}<=>$$b{"end"} } @_; # Get the union of the ranges @union = qw(); $_ = 0; $start = ${$_[0]}{"start"}; $end = ${$_[0]}{"end"}; while (1) { # Find the next segment that exceeds the current segment for ( ; $_ < @_ && ${$_[$_]}{"end"} <= $end; $_++) {}; # Meet the last entry if ($_ == @_) { # Save the last segment push @union, { "start" => $start, "end" => $end, "len" => $end - $start, "text" => substr($body, $start, $end - $start), }; last; } # A new segment seperated from the current segment if (${$_[$_]}{"start"} > $end) { # Save the last segment push @union, { "start" => $start, "end" => $end, "len" => $end - $start, "text" => substr($body, $start, $end - $start), }; # Start a new segment $start = ${$_[$_]}{"start"}; $end = ${$_[$_]}{"end"}; next; } # Expend the current segment $end = ${$_[$_]}{"end"}; next; } # Trim the union $len_andsoon = 1; $len = 0; $len += $len_andsoon if ${$union[0]}{"start"} != 0; $reached_maximum = 0; for ($_ = 0; $_ < @union; $_++) { $union = $union[$_]; $needlen = $$union{"len"} + $len_andsoon; # Not even enough for an abstract section if ($len + $len_andsoon > $self->{"QABS_MAXLEN"}) { $reached_maximum = 1; # Discard the rest sections pop @union while @union > $_; last; # Reached the maximum } elsif ($len + $needlen > $self->{"QABS_MAXLEN"}) { $reached_maximum = 1; $$union{"len"} = $self->{"QABS_MAXLEN"} - $len - $len_andsoon; $$union{"end"} = $$union{"start"} + $$union{"len"}; $$union{"text"} = substr $body, $$union{"start"}, $$union{"len"}; # Discard the rest sections pop @union while @union > $_ + 1; last; } # Not reached the maximum yet $len += $$union{"len"} + $len_andsoon; } # Not reached the maximum yet - check the last section if (!$reached_maximum) { $union = $union[$#union]; $needlen = $$union{"len"}; $needlen += $len_andsoon if $$union{"end"} != length $body; # Not even enough for an abstract section if ($len + $len_andsoon > $self->{"QABS_MAXLEN"}) { # Forget it. We can do nothing now. pop @union; # Reached the maximum } elsif ($len + $needlen > $self->{"QABS_MAXLEN"}) { $$union{"len"} = $self->{"QABS_MAXLEN"} - $len - $len_andsoon; $$union{"end"} = $$union{"start"} + $$union{"len"}; $$union{"text"} = substr $body, $$union{"start"}, $$union{"len"}; } # Not reached the maximum yet } # Mark the query phrases foreach $union (@union) { my @pieces; @pieces = ( { "text" => $$union{"text"}, "is_match" => 0, } ); # Mark each query phrase foreach my $query (@queries) { for ($_ = 0; $_ < @pieces; $_++) { my ($pos, $piece); $piece = $pieces[$_]; # Skip matches of other query phrases next if $$piece{"is_match"}; # Skip if not matched next unless $$piece{"text"} =~ /^(.*?)(\Q$query\E)(.*)$/i; @pieces = ( @pieces[0 ... $_ - 1], { "text" => $1, "is_match" => 0, }, { "text" => $2, "is_match" => 1, }, { "text" => $3, "is_match" => 0, }, @pieces[$_ + 1 ... $#pieces], ); $_++; } } $$union{"marked"} = join "", map(($$_{"is_match"}? "" . h($$_{"text"}) . "": h($$_{"text"})), @pieces); } # Join these segments $_ = join "…", map $$_{"marked"}, @union; $_ = "…" . $_ if ${$union[0]}{"start"} != 0; $_ .= "…" if ${$union[$#union]}{"end"} != length $body; return $_; } # html_title: Display the title # Make it a null function sub html_title : method {} # html_errmsg: Display the error message sub html_errmsg : method { local ($_, %_); my ($self, $message); $self = $_[0]; return if !defined $self->{"error"}; $message = h(err2msg $self->{"error"}); print << "EOT";

$message

EOT return; } # html_newlink: Display a link to add a new item sub html_newlink : method { local ($_, %_); my ($self, $prompt, $url); ($self, $prompt) = @_; # No new item creation if it is a called form return if $self->{"is_called_form"}; return if !defined $prompt; # Start from the default language return if $DBH->is_ml_table($self->{"table"}) && getlang ne $DEFAULT_LANG; $url = $REQUEST_FILEQS; # Remove list parameters $url = rem_get_arg $url, "query", "sortby", "pageno", "form", "formid", "statid"; $url = add_get_arg $url, "form", "new", DUP_OK; $url = h($url); $prompt = h($prompt); print << "EOT";

$prompt

EOT return; } # html_lists_switch: Display the switch for different lists sub html_lists_switch : method { local ($_, %_); my $self; $self = $_[0]; # Bounce for nothing return unless exists $self->{"lists_switch"} && @{$self->{"lists_switch"}} > 0; print "
\n"; print join " |\n", map " " . h($$_{"title"}) . "", @{$self->{"lists_switch"}}; print "
\n\n"; return; } # html_search: Display the search box sub html_search : method { local ($_, %_); my ($self, $prompt, $label, $query, $request_file); ($self, $prompt) = @_; # No search box is displayed if no records yet if ( $self->{"fetched"} && defined $self->{"total"} && $self->{"total"} == 0 && !defined $self->{"query"}) { return; } $request_file = h($REQUEST_FILE); $query = defined $self->{"query"}? h($self->{"query"}): ""; $label = h(C_("Search")); print << "EOT";
EOT return; } # html_data_download: Display the data download link sub html_data_download : method { local ($_, %_); # Normally empty return; } # html_liststat: Display the list statistics sub html_liststat : method { local ($_, %_); my ($self, $message); $self = $_[0]; # Do not show the list return if !defined $self->{"total"}; return if !defined($message = $self->liststat_message()); $message = h($message); print << "EOT";

$message

EOT return; } # html_pagebar: Display a page navigation bar # We display a page bar for scrolling between previous and next 2 pages sub html_pagebar : method { local ($_, %_); my ($self, $html, $startpage, $endpage, $baseurl, $url, $cell, %tags); $self = $_[0]; # Do not show the list return if !defined $self->{"total"}; # Fit in one page - paging is not needed return if $self->{"lastpage"} <= 1; # Cached before if (exists $self->{"html_pagebar"}) { print $self->{"html_pagebar"}; return; } # Fewer than 5 pages if ($self->{"lastpage"} <= 5) { $startpage = 1; $endpage = $self->{"lastpage"}; # Near the beginning } elsif ($self->{"pageno"} < 3) { $startpage = 1; $endpage = 5; # Near the end } elsif ($self->{"pageno"} > $self->{"lastpage"} - 2) { $startpage = $self->{"lastpage"} - 4; $endpage = $self->{"lastpage"}; # Normal, at the middle } else { $startpage = $self->{"pageno"} - 2; $endpage = $self->{"pageno"} + 2; } # Start output $html = ""; %tags = $self->pagebar_tags; # Static page -- Display the index if ($self->{"static"}) { $html .= "
\n" . " " . h(C_("Index")) . " |\n"; # Dynamic page -- Display a paging form } else { # Base url without the "pageno" argument $baseurl = rem_get_arg $REQUEST_FILEQS, "pageno", "statid"; $_ = h($REQUEST_FILE); $html .= << "EOT";
EOT if ($self->{"is_called_form"}) { my ($caller, $cformid); $caller = h($self->{"caller"}); $cformid = h($self->{"cformid"}); print << "EOT"; EOT } if (defined $self->{"query"}) { $_ = h($self->{"query"}); print << "EOT"; EOT } if (defined $self->{"sortby"} && $self->{"sortby"} ne "") { $_ = h($self->{"sortby"}); print << "EOT"; EOT } } # The first page $cell = $tags{"first"}; if ($self->{"pageno"} != 1) { if ($self->{"static"}) { $url = sprintf $self->{"static_filepat"}, 1; } elsif ($self->{"reverse"}) { $url = add_get_arg $baseurl, "pageno", 1, DUP_OK; } else { $url = $baseurl; } $cell = "" . $cell . ""; } $html .= " $cell |\n"; # The previous page $cell = $tags{"prev"}; if ($self->{"pageno"} != 1) { if ($self->{"static"}) { $url = sprintf $self->{"static_filepat"}, $self->{"pageno"} - 1; } elsif ($self->{"reverse"} || $self->{"pageno"} - 1 != 1) { $url = add_get_arg $baseurl, "pageno", $self->{"pageno"} - 1, DUP_OK; } else { $url = $baseurl; } $cell = "" . $cell . ""; } $html .= " $cell |\n"; # Pages before for ($_ = $startpage; $_ < $self->{"pageno"}; $_++) { $cell = h($_); if ($self->{"static"}) { $url = sprintf $self->{"static_filepat"}, $_; } elsif ($self->{"reverse"} || $_ != 1) { $url = add_get_arg $baseurl, "pageno", $_, DUP_OK; } else { $url = $baseurl; } $cell = "" . $cell . ""; $html .= " $cell |\n"; } # Current page $cell = h($self->{"pageno"}); $html .= " $cell |\n"; # Pages after for ($_ = $self->{"pageno"} + 1; $_ <= $endpage; $_++) { $cell = h($_); if ($self->{"static"}) { if ( defined $self->{"static_lastfile"} && $_ == $self->{"lastpage"}) { $url = $self->{"static_lastfile"}; } else { $url = sprintf $self->{"static_filepat"}, $_; } } elsif (!$self->{"reverse"} || $_ != $self->{"lastpage"}) { $url = add_get_arg $baseurl, "pageno", $_, DUP_OK; } else { $url = $baseurl; } $cell = "" . $cell . ""; $html .= " $cell |\n"; } # The next page $cell = $tags{"next"}; if ($self->{"pageno"} != $self->{"lastpage"}) { if ($self->{"static"}) { if ( defined $self->{"static_lastfile"} && $self->{"pageno"} + 1 == $self->{"lastpage"}) { $url = $self->{"static_lastfile"}; } else { $url = sprintf $self->{"static_filepat"}, $self->{"pageno"} + 1; } } elsif (!$self->{"reverse"} || $self->{"pageno"} + 1 != $self->{"lastpage"}) { $url = add_get_arg $baseurl, "pageno", $self->{"pageno"} + 1, DUP_OK; } else { $url = $baseurl; } $cell = "" . $cell . ""; } $html .= " $cell |\n"; # The last page $cell = $tags{"last"}; if ($self->{"pageno"} != $self->{"lastpage"}) { if ($self->{"static"}) { if (defined $self->{"static_lastfile"}) { $url = $self->{"static_lastfile"}; } else { $url = sprintf $self->{"static_filepat"}, $self->{"lastpage"}; } } elsif (!$self->{"reverse"}) { $url = add_get_arg $baseurl, "pageno", $self->{"lastpage"}, DUP_OK; } else { $url = $baseurl; } $cell = "" . $cell . ""; } $html .= " $cell"; if ($self->{"static"}) { $html .= << "EOT";
EOT } else { my ($label, $input, $pageno); $label = h(C_("Page:")); $input = h(C_("View")); $pageno = h($self->{"pageno"}); $html .= << "EOT"; |
EOT } print $html; # Cache it $self->{"html_pagebar"} = $html; return; } # html_list: List the items sub html_list : method { local ($_, %_); my ($self, $baseurl, $url, $need_view); $self = $_[0]; # Do not show the list return if !defined $self->{"total"}; # No record to be listed return if $self->{"total"} == 0; # Remove "sortby" from the URL first $baseurl = $REQUEST_FILEQS; $baseurl = rem_get_arg $baseurl, "formid", "statid", "sortby", "pageno"; # Do we need a view link? $need_view = in_array("_viewurl", @{$self->{"cols"}}) && !$self->{"is_called_form"}; # Check the URL first urlcheck @{$self->{"current"}} if in_array("_urlcheck", @{$self->{"listcols"}}); # Mass deletion -- surround it with a form if ($self->{"massdel"} && !$self->{"is_called_form"}) { my $label; $label = h(C_("Delete the selected items.")); $url = h($REQUEST_FILE); print << "EOT";
EOT } print << "EOT"; EOT print ""; print "" if $self->{"massdel"} && !$self->{"is_called_form"}; print "" if $need_view; print "" if !$self->{"noselect"}; for ($_ = 0; $_ < @{$self->{"listcols"}}; $_++) { print ""; } print "\n"; print << "EOT"; EOT print " \n"; print " \n" if $self->{"massdel"} && !$self->{"is_called_form"}; print " \n" if $need_view; print " \n" if !$self->{"noselect"}; foreach my $col (@{$self->{"listcols"}}) { my ($label, $key); $label = h_abbr(exists ${$self->{"col_labels"}}{$col}? ${$self->{"col_labels"}}{$col}: $col); if (in_array($col, @{$self->{"COLS_NO_SORT_BY"}})) { print << "EOT"; EOT } else { $key = $col; $key = "-$key" if defined $self->{"sortby"} && $self->{"sortby"} eq $key; $url = h(add_get_arg($baseurl, "sortby", $key, DUP_OK)); print << "EOT"; EOT } } print << "EOT"; EOT # Print each record my ($no, $inc, $rowclass); if (!$self->{"reverse"}) { $no = $self->{"startno"} + 1; $inc = 1; } else { $no = $self->{"endno"} + 1; $inc = -1; } $rowclass = ""; foreach my $current (@{$self->{"current"}}) { my ($issel, $selurl); $rowclass = h(($rowclass eq "oddrow")? "evenrow": "oddrow"); # The URL. to handle the selection $issel = ((!in_array("_sel", @{$self->{"cols"}}) || $$current{"_sel"}) && !$self->{"noselect"}); if ($issel) { $selurl = exists $$current{"_selurl"}? $$current{"_selurl"}: sprintf $self->{"selurl_tmpl"}, h($$current{"sn"}); } print << "EOT"; EOT print " \n" if $self->{"massdel"} && !$self->{"is_called_form"}; if ($need_view) { if (!defined $$current{"_viewurl"}) { print " \n" } else { print " \n"; } } if (!$self->{"noselect"}) { print " \n"; } foreach my $col (@{$self->{"listcols"}}) { print " \n"; } print << "EOT"; EOT $no += $inc; } print << "EOT";
" . h_abbr(C_("No.")) . "" . h_abbr(C_("Delete")) . "" . h_abbr(C_("View")) . "" . h_abbr($self->{"seltext"}) . "$label$label
$no" . h(C_("View")) . ""; print "" . h($self->{"seltext"}) . "" if $issel; print "" . $self->colval($col, %$current) . "
EOT # Mass deletion -- surround it with a form if ($self->{"massdel"} && !$self->{"is_called_form"}) { my $label; $label = h(C_("Delete the selected items.")); print << "EOT";
EOT } return; } # html_listprefform: Display a form to change the list preference sub html_listprefform : method { local ($_, %_); my ($self, $submit, $referer, @validcols, $request_file_h, $domain); my ($label, $pagesize); $self = $_[0]; # Do not show the list return if !defined $self->{"total"}; # No record to be listed return if $self->{"total"} == 0; # Return if users preferences are not available return if !use_users || !defined $SESSION; $submit = C_("Set"); # The referer $referer = h(rem_get_arg $REQUEST_FULLURI, "statid"); # Obtain the columns to list %_ = map { $_ => 1 } @{$self->{"COLS_NO_DISPLAY"}}; @validcols = grep !exists $_{$_}, @{$self->{"cols"}}; $request_file_h = h($REQUEST_FILE); # The domain -- my class name $domain = h(ref($self)); print << "EOT";
EOT # The number of rows per page $label = h_abbr(C_("Rows per page:")); $pagesize = h($self->{"pagesize"}); print << "EOT"; EOT # The display columns print h_abbr(C_("Display columns:")) . "\n"; %_ = map { $_ => 1 } @{$self->{"listcols"}}; foreach my $col (@validcols) { my ($name, $label, $check); $label = h_abbr(exists ${$self->{"col_labels"}}{$col}? ${$self->{"col_labels"}}{$col}: $col); $name = h("listcols_" . $col); $check = exists $_{$col}? " checked=\"checked\"": ""; print << "EOT"; EOT } print << "EOT";
EOT return; } ####################### # Below are functions ####################### # urlcheck: Proform URL checks sub urlcheck(\@) { local ($_, %_); my ($current, @tocheck); my ($pid, $out, $err, $exitno, $CHILD_IN, $CHILD_OUT, $CHILD_ERR); $current = $_[0]; @tocheck = qw(); foreach (@$current) { if (!is_url_wellformed $$_{"_urlcheck"}) { $$_{"_urlcheck"} = C_("Malformed"); } else { push @tocheck, $_; } } # Nothing to check further return if @tocheck == 0; # Duplicate the file handles and fork $pid = open3($CHILD_IN, $CHILD_OUT, $CHILD_ERR, @URLCHECK); # Get the result of the child if (defined $CHILD_IN) { print $CHILD_IN join("", map $$_{"_urlcheck"} . "\n", @tocheck) or http_500 join(" ", @URLCHECK) . ": $!"; close $CHILD_IN or http_500 join(" ", @URLCHECK) . ": $!"; } if (defined $CHILD_OUT) { $out = join "", <$CHILD_OUT>; close $CHILD_OUT or http_500 join(" ", @URLCHECK) . ": $!"; } if (defined $CHILD_ERR) { $err = join "", <$CHILD_ERR>; close $CHILD_ERR or http_500 join(" ", @URLCHECK) . ": $!"; } waitpid $pid, 0; $exitno = $? >> 8; #$coredump = $? & 128; http_500 join(" ", @URLCHECK) . ": $exitno $err" if $exitno != 0; # Save the result @_ = split /\n/, $out; for ($_ = 0; $_ < @tocheck; $_++) { ${$tocheck[$_]}{"_urlcheck"} = $_[$_]? C_("OK"): C_("Unreachable"); } return; } no utf8; return 1;