# ================================================================== # Gossamer Links - enhanced directory management system # # Website : http://gossamer-threads.com/ # Support : http://gossamer-threads.com/scripts/support/ # CVS Info : 087,071,086,086,085 # Revision : $Id: Search.pm,v 1.48 2006/08/08 23:30:09 brewt Exp $ # # Copyright (c) 2001 Gossamer Threads Inc. All Rights Reserved. # Redistribution in part or in whole strictly prohibited. Please # see LICENSE file for full details. # ================================================================== package Links::User::Search; # ================================================================== use strict; use Links qw/:objects/; use Links::SiteHTML; use Links::Build; my $time_hires; sub handle { #-------------------------------------------------------------------------------- # Determine whether we are displaying the search form, or doing a # search. # my $db = $DB->table('Links'); my $results = {}; my $args = $IN->get_hash; # Remove search fields we aren't allowed to search on. my @bad = (@{$CFG->{search_blocked}}, qw/isValidated ExpiryDate/); for my $col (@bad) { $col =~ s/^\s*|\s*$//g; if ($args->{$col}) { delete $args->{$col}; $IN->delete($col); } for (qw(lt gt opt le ge ne)) { delete $args->{"$col-$_"}; $IN->delete("$col-$_"); } } # If query is set we know we are searching. return search() if defined $args->{query} and $args->{query} =~ /\S/; # Otherwise, if we pass in a field name, we can search on that too. foreach (keys %{$db->cols}) { for my $opt ('', qw/-lt -gt -le -ge -ne/) { return search() if defined $args->{"$_$opt"} and length $args->{"$_$opt"}; } } print $IN->header(); print Links::SiteHTML::display('search', { main_title_loop => Links::Build::build('title', Links::language('LINKS_SEARCH'), "$CFG->{db_cgi_url}/search.cgi") }); } sub search { # ------------------------------------------------------------------ # Do the search and print out the results. # my $results = $PLG->dispatch('search_results', \&query, {}); if (defined $results->{error}) { print $IN->header(); $results->{main_title_loop} = Links::Build::build('title', Links::language('LINKS_SEARCH'), "$CFG->{db_cgi_url}/search.cgi"); print Links::SiteHTML::display('search', $results); } else { print $IN->header(); $results->{main_title_loop} = Links::Build::build('title', Links::language('LINKS_SEARCH_RESULTS'), "$CFG->{db_cgi_url}/search.cgi"); print Links::SiteHTML::display('search_results', $results); } if ($CFG->{debug_level} > 1) { print "
"; } } sub query { # ------------------------------------------------------------------ # Query the database. # # First get our search options. my $args = $IN->get_hash; if ($args->{query}) { $args->{query} =~ s/^\s+//; $args->{query} =~ s/\s+$//; } $args->{bool} = (defined $args->{bool} and $args->{bool} =~ /^(and|or)$/i) ? uc $1 : $CFG->{search_bool}; $args->{nh} = (defined $args->{nh} and $args->{nh} =~ /^(\d+)$/) ? $1 : 1; $args->{mh} = (defined $args->{mh} and $args->{mh} =~ /^\d+$/) ? $args->{mh} : $CFG->{search_maxhits}; $args->{mh} = 200 if $args->{mh} > 200; # Safety limit $args->{substring} = defined $args->{substring} ? $args->{substring} : $CFG->{search_substring}; $args->{so} = (defined $args->{so} and $args->{so} =~ /^(asc|desc)$/i) ? $1 : ''; $args->{sb} and ($args->{sb} =~ /^[\w\s,]+$/ or ($args->{sb} = '')); delete $args->{ma}; # Make sure we only search on validated links. $args->{isValidated} = 'Yes'; $args->{ExpiryDate} = '>=' . time if $CFG->{payment}->{enabled}; my $query = $args->{query} || ''; my $term = $IN->escape($query); my $links = $DB->table('Links'); my $categories = $DB->table('Category'); # We don't do a category search if we only have a filters. my $filter = 0; if (!defined $query or $query eq '') { $filter = 1; } $args->{filter} = $filter; # Note: if you use this or the search_set_link_callback, remember to $PLG->action(STOP) or your callback won't be used $args->{callback} = $PLG->dispatch('search_set_cat_callback', sub { return \&_cat_search_subcat if shift }, $args->{catid}); my $orig_sb = $args->{sb}; my $orig_so = $args->{so}; $args->{sb} = $CFG->{build_sort_order_search_cat}; $args->{so} = ''; $filter and $args->{sb} =~ s/\s*,?\s*score//; my $started; if (length $query and $CFG->{search_logging} and $args->{nh} == 1) { if (!defined $time_hires) { $time_hires = eval { require Time::HiRes } || 0; } $started = $time_hires ? Time::HiRes::time() : time; } my $cat_sth; $cat_sth = $categories->query_sth($args) unless $filter; my $cat_count = $filter ? 0 : $categories->hits(); $args->{callback} = $PLG->dispatch('search_set_link_callback', sub { return \&_search_subcat if shift }, $args->{catid}); $args->{sb} = $orig_sb ? $orig_sb : $CFG->{build_sort_order_search} || ''; $args->{so} = (defined $orig_so and $orig_so =~ /^(asc|desc)$/i) ? $1 : 'ASC'; $filter and $args->{sb} =~ s/\s*,?\s*score//; # Don't force sorting by whether or not a link is paid, as that would make # searching almost useless w.r.t. unpaid links since a 1% paid match would be # higher than a 99% unpaid match. my $link_sth = $links->query_sth($args); my $link_count = $links->hits; # Log the search if it's a new query if (length $query and $CFG->{search_logging} and $args->{nh} == 1) { my $elapsed = ($time_hires ? Time::HiRes::time() : time) - $started; my $results = $link_count || 0; my $sl = $DB->table('SearchLogs'); my $q = lc $query; substr($q, 255) = '' if length $q > 255; if (my $row = $sl->select({ slog_query => $q })->fetchrow_hashref) { my $slog_time = defined $row->{slog_time} ? ($row->{slog_time} * $row->{slog_count} + $elapsed) / ($row->{slog_count} + 1) : $elapsed; $sl->update({ slog_count => $row->{slog_count} + 1, slog_time => sprintf('%.6f', $slog_time), slog_last => time, slog_hits => $results }, { slog_query => $q }); } else { $sl->insert({ slog_query => $q, slog_count => 1, slog_time => sprintf('%.6f', $elapsed), slog_last => time, slog_hits => $results }) or die "$GT::SQL::error"; } } # Return if no results. unless ($link_count or $cat_count) { return { error => Links::language('SEARCH_NOLINKS', $term), term => $term }; } # Now format the category results. my $count = 0; my ($category_results, @category_results_loop); if (!$filter and $cat_count) { while (my $cat = $cat_sth->fetchrow_hashref) { last if ($count++ > $args->{mh}); my $title = Links::Build::build('title_linked', { name => $cat->{Full_Name}, complete => 1, home => 0 }); $category_results .= "", GT::SQL->query_stack_disp , "
$title" . join("", map { Links::SiteHTML::display('link', $_) } @{$link_output{$cat}}); } return $links; }; # Generate a toolbar if requested. my ($toolbar, %paging); if ($link_count > $args->{mh} or $cat_count > $args->{mh}) { my $url = $CFG->{db_cgi_url} . "/" . $IN->url; $url =~ s/([;&?]?)nh=(\d+)/($1 and $1 eq '?') ? '?' : ''/eg; $toolbar = Links::Build::build(search_toolbar => { url => $url, numlinks => $link_count > $cat_count ? $link_count : $cat_count, nh => $args->{nh}, mh => $args->{mh} }); %paging = ( url => $url, num_hits => $link_count > $cat_count ? $link_count : $cat_count, max_hits => $args->{mh}, current_page => $args->{nh} ); } else { $toolbar = ''; } # Print the output. my $results = { link_results => $link_results, link_results_loop => \@link_results_loop, category_results => $category_results, category_results_loop => \@category_results_loop, link_hits => $link_count, cat_hits => $cat_count, next => $toolbar, paging => \%paging, term => $term, highlight => $CFG->{search_highlighting} }; return $results; } sub _search_subcat { # ------------------------------------------------------------------- # First argument is the query/table object, second argument is the current # result set (note: can be quite large). Must return a new result set. # my ($query, $results) = @_; return $results unless (keys %$results); # No matches. my $cat_db = $DB->table('Category'); my $catlink_db = $DB->table('CatLinks', 'Category'); # We need the full name of the category. my @cat_ids = $IN->param('catid') or return $results; my (@children, %seen); foreach my $id (@cat_ids) { next if ($id !~ /^\d+$/); my $child = $cat_db->children($id) or next; push @children, @$child, $id; } @children or return $results; @children = grep !$seen{$_}++, @children; # Now do the joined query. my %filtered = map { $_ => $results->{$_} } $catlink_db->select(LinkID => { CategoryID => \@children, LinkID => [keys %$results] })->fetchall_list; return \%filtered; } sub _search_subcat_and { # ------------------------------------------------------------------- # Search subcategories using AND. # my ($query, $results) = @_; return $results unless (keys %$results); # No matches my $cat_db = $DB->table('Category'); my $catlink_db = $DB->table('CatLinks', 'Category'); # We need the full name of the category. my @cat_ids = $IN->param('catid') or return $results; my %final = %$results; foreach my $id (@cat_ids) { next unless ($id =~ /^\d+$/); my @children; my $childs = $cat_db->children($id); push @children, @$childs, $id; my $cond = GT::SQL::Condition->new( CategoryID => 'IN' => \@children, LinkID => 'IN' => [ keys %final ] ); %final = (); my $sth = $catlink_db->select($cond, ['LinkID']); while (my $link_id = $sth->fetchrow_array) { $final{$link_id} = $results->{$link_id}; } last unless keys %final; } return \%final; } sub _cat_search_subcat { # ------------------------------------------------------------------- # First argument is the query/table object, second argument is the current # result set (note: can be quite large). Must return a new result set. # my ($query, $results) = @_; return $results unless (keys %$results); # No matches. my $cat_db = $DB->table('Category'); my @cat_ids = $IN->param('catid') or return $results; my (@children, %seen); foreach my $id (@cat_ids) { next if ($id !~ /^\d+$/); my $child = $cat_db->children($id) or next; push @children, @$child, $id; } @children or return $results; @children = grep { ! $seen{$_}++ } @children; my %subcats = map { $_ => 1 } @children; my $filtered = {}; while (my ($k, $s) = each %$results) { $filtered->{$k} = $s if (exists $subcats{$k}); } return $filtered; } 1;