#!/usr/bin/perl # # Directory search code. # ############################################################ # # Misc Notes... # # directory handle can only touch: # community # friends # payments # userinterests # userprop # userusage # use strict; package LJ::Dir; use Digest::MD5 qw(md5_hex); my $MAX_RETURN_RESULT = 1000; my %filters = ( 'int' => { 'searcher' => \&search_int, 'validate' => \&validate_int, }, 'fr' => { 'searcher' => \&search_fr, 'validate' => \&validate_fr, }, 'fro' => { 'searcher' => \&search_fro, 'validate' => \&validate_fro, }, 'loc' => { 'validate' => \&validate_loc, 'searcher' => \&search_loc, }, #'gen' => { 'validate' => \&validate_gen, # 'searcher' => \&search_gen, }, 'age' => { 'validate' => \&validate_age, 'searcher' => \&search_age, }, 'ut' => { 'validate' => \&validate_ut, 'searcher' => \&search_ut, }, 'com' => { 'searcher' => \&search_com, 'validate' => \&validate_com, }, ); # validate all filter options # sub validate { my ($req, $errors) = @_; my @filters; foreach my $f (sort keys %filters) { next unless $filters{$f}->{'validate'}; if ($filters{$f}->{'validate'}->($req, $errors)) { push @filters, $f; } } return sort @filters; } # entry point to do a search: give it # a db-read handle # directory master (must be able to write to dirsearchres2) # hashref of the request, # a listref of where to put the user hashrefs returned, # hashref of where to return results of the query sub do_search { my ($dbr, $dbdir, $req, $users, $info) = @_; my $sth; # clear return buffers @{$users} = (); %{$info} = (); my @crits; foreach my $f (sort keys %filters) { next unless $filters{$f}->{'validate'}->($req, []); my @criteria = $filters{$f}->{'searcher'}->($dbr, $req, $info); if (@criteria) { push @crits, @criteria; } else { # filters return nothing to signal an error, and should have set $info->{'errmsg'} $info->{'errmsg'} = "[Filter $f failed] $info->{'errmsg'}"; return 0; } } unless (scalar(@crits)) { $info->{'errmsg'} = "You did not enter any search criteria."; return 0; } ########## time to build us some huge SQL statement. yee haw. my $orderby; my %only_one_copy = qw(community c user u userusage uu); ## keep track of what table aliases we've used my %alias_used; $alias_used{'u'} = "?"; # only used by dbr, not dbdir $alias_used{'c'} = "?"; # might be used later, if opt_format eq "com" $alias_used{'uu'} = "?"; # might be used later, if opt_sort is by time my %conds; # where condition -> 1 my %useridcol; # all keys here equal each other (up.userid == uu.userid == ..) ## foreach each critera, build up the query foreach my $crit (@crits) { ### each search criteria has its own table aliases. make those unique. my %map_alias = (); # keep track of local -> global table alias mapping foreach my $localalias (keys %{$crit->{'tables'}}) { my $table = $crit->{'tables'}->{$localalias}; my $newalias; # some tables might be used multiple times but they're # setup such that opening them multiple times is useless. if ($only_one_copy{$table}) { $newalias = $only_one_copy{$table}; $alias_used{$newalias} = $table; } else { my $ct = 1; $newalias = $localalias; while ($alias_used{$newalias}) { $ct++; $newalias = "$localalias$ct"; } $alias_used{$newalias} = $table; } $map_alias{$localalias} = $newalias; } ## add each condition to the where clause, after fixing up aliases foreach my $cond (@{$crit->{'conds'}}) { $cond =~ s/\{(\w+?)\}/$map_alias{$1}/g; $conds{$cond} = 1; } ## add join to u.userid table my $cond = $crit->{'userid'}; if ($cond) { $cond =~ s/\{(\w+?)\}/$map_alias{$1}/g; $useridcol{$cond} = 1; } } my $pagesize = $req->{'opt_pagesize'}+0 || 100; if ($pagesize > 200) { $pagesize = 200; } if ($pagesize < 5) { $pagesize = 5; } $req->{'opt_format'} ||= "pics"; if ($req->{'opt_format'} eq "com") { $alias_used{'c'} = "community"; $useridcol{"c.userid"} = 1; } $req->{'opt_sort'} ||= "ut"; if ($req->{'opt_sort'} eq "ut") { $alias_used{'uu'} = 'userusage'; $useridcol{"uu.userid"} = 1; $orderby = "ORDER BY uu.timeupdate DESC"; } elsif ($req->{'opt_sort'} eq "user") { $alias_used{'u'} = 'user'; $useridcol{"u.userid"} = 1; $orderby = "ORDER BY u.user"; } elsif ($req->{'opt_sort'} eq "name") { $alias_used{'u'} = 'user'; $useridcol{"u.userid"} = 1; $orderby = "ORDER BY u.name"; } # delete reserved table aliases the didn't end up being used foreach (keys %alias_used) { delete $alias_used{$_} if $alias_used{$_} eq "?"; } # add clauses to make all userid cols equal each other my $useridcol; # any one foreach my $ca (keys %useridcol) { foreach my $cb (keys %useridcol) { next if $ca eq $cb; $conds{"$ca=$cb"} = 1; } $useridcol = $ca; } my $fromwhat = join(", ", map { "$alias_used{$_} $_" } keys %alias_used); my $conds = join(" AND ", keys %conds); my $sql = "SELECT $useridcol FROM $fromwhat WHERE $conds $orderby LIMIT $MAX_RETURN_RESULT"; if ($req->{'sql'}) { $info->{'errmsg'} = "SQL: $sql"; return 0; } my $qdig = $dbr->quote(md5_hex($sql)); my $hit_cache = 0; my $count = 0; my @ids; # delete any stale results $dbdir->do("DELETE FROM dirsearchres2 WHERE qdigest=$qdig AND ". "dateins < DATE_SUB(NOW(), INTERVAL 15 MINUTE)"); # mark query as in progress. $dbdir->do("INSERT INTO dirsearchres2 (qdigest, dateins, userids) ". "VALUES ($qdig, NOW(), '[searching]')"); if ($dbdir->err) { # if there's an error inserting that, we know something's there. # let's see what! my $ids = $dbdir->selectrow_array("SELECT userids FROM dirsearchres2 ". "WHERE qdigest=$qdig"); if (defined $ids) { if ($ids eq "[searching]") { # somebody else (or same user before) is still searching $info->{'searching'} = 1; return 1; } @ids = split(/,/, $ids); $count = scalar(@ids); $hit_cache = 1; } } ## guess we'll have to query it. if (! $hit_cache) { BML::do_later(sub { $sth = $dbdir->prepare($sql); $sth->execute; while (my ($id) = $sth->fetchrow_array) { push @ids, $id; } my $ids = $dbdir->quote(join(",", @ids)); $dbdir->do("REPLACE INTO dirsearchres2 (qdigest, dateins, userids) ". "VALUES ($qdig, NOW(), $ids)"); }); $info->{'searching'} = 1; return 1; } my $page = $req->{'page'} || 1; my $skip = ($page-1)*$pagesize; my $pages = int($count / $pagesize) + (($count % $pagesize) ? 1 : 0); $pages ||= 1; if ($page > $pages) { $page = $pages; } $info->{'pages'} = $pages; $info->{'page'} = $page; $info->{'first'} = ($page-1)*$pagesize+1; $info->{'last'} = $page * $pagesize; $info->{'count'} = $count; if ($count == $MAX_RETURN_RESULT) { $info->{'overflow'} = 1; } if ($page == $pages) { $info->{'last'} = $count; } ## now, get info on the ones we want. @ids = grep{ $_+0 } @ids[($info->{'first'}-1)..($info->{'last'}-1)]; return 1 unless @ids; my %u; LJ::load_userids_multiple([ map { $_ => \$u{$_} } @ids ]); my $tu = LJ::get_timeupdate_multi(@ids); my $now = time(); # need to get community info if ($req->{'opt_format'} eq "com") { my $in = join(',', @ids); my $sth = $dbr->prepare("SELECT userid, membership, postlevel ". "FROM community ". "WHERE userid IN ($in)"); $sth->execute; while (my ($uid, $mem, $postlev) = $sth->fetchrow_array) { next unless $u{$uid}; $u{$uid}->{'membership'} = $mem; $u{$uid}->{'postlevel'} = $postlev; } foreach (@ids) { delete $u{$_} unless $u{$_}->{'membership'}; } } foreach my $id (@ids) { next unless $u{$id} && $u{$id}->{'statusvis'} eq "V"; $u{$id}->{'secondsold'} = $tu->{$id} ? $now - $tu->{$id} : undef; push @$users, $u{$id} if $u{$id}; } return 1; } sub ago_text { my $secondsold = shift; return "Never." unless ($secondsold); my $num; my $unit; if ($secondsold > 60*60*24*7) { $num = int($secondsold / (60*60*24*7)); $unit = "week"; } elsif ($secondsold > 60*60*24) { $num = int($secondsold / (60*60*24)); $unit = "day"; } elsif ($secondsold > 60*60) { $num = int($secondsold / (60*60)); $unit = "hour"; } elsif ($secondsold > 60) { $num = int($secondsold / (60)); $unit = "minute"; } else { $num = $secondsold; $unit = "second"; } return "$num $unit" . ($num==1?"":"s") . " ago"; } ########## INTEREST ############ sub validate_int { my ($req, $errors) = @_; my $int = lc($req->{'int_like'}); $int =~ s/^\s+//; $int =~ s/\s+$//; return 0 unless $int; $req->{'int_like'} = $int; return 1; } sub search_int { my ($dbr, $req, $info) = @_; my $arg = $req->{'int_like'}; push @{$info->{'english'}}, "are interested in \"$arg\""; ## find interest id, if one doth exist. my $qint = $dbr->quote($req->{'int_like'}); my $intid = $dbr->selectrow_array("SELECT intid FROM interests ". "WHERE interest=$qint"); unless ($intid) { $info->{'errmsg'} = "The interest you have entered is not valid."; return; } my $UI_TABLE = $req->{'com_do'} ? "comminterests" : "userinterests"; return { 'tables' => { 'ui' => $UI_TABLE, }, 'conds' => [ "{ui}.intid=$intid" ], 'userid' => "{ui}.userid", }; } ######## HAS FRIEND ############## sub validate_fr { my ($req, $errors) = @_; return 0 unless $req->{'fr_user'} =~ /\S/; return 1; } sub search_fr { my ($dbr, $req, $info) = @_; my $user = lc($req->{'fr_user'}); my $arg = $user; push @{$info->{'english'}}, "consider \"$arg\" a friend"; my $friendid = LJ::get_userid($user); return { 'tables' => { 'f' => 'friends', }, 'conds' => [ "{f}.friendid=$friendid" ], 'userid' => "{f}.userid", }; } ######## FRIEND OF ############## sub validate_fro { my ($req, $errors) = @_; return 0 unless $req->{'fro_user'} =~ /\S/; return 1; } sub search_fro { my ($dbr, $req, $info) = @_; my $user = lc($req->{'fro_user'}); my $arg = $user; push @{$info->{'english'}}, "are considered a friend by \"$arg\""; my $userid = LJ::get_userid($user); return { 'tables' => { 'f' => 'friends', }, 'conds' => [ "{f}.userid=$userid" ], 'userid' => "{f}.friendid", }; } ########### LOCATION ############### sub validate_loc { my ($req, $errors) = @_; return 0 unless $req->{'loc_cn'}; unless ($req->{'loc_cn'} =~ /^[A-Z]{2}$/ || # ISO code $req->{'loc_cn'} =~ /^LJ/) # site-local country/region code { push @$errors, "Invalid country for location search."; return 0; } return 1; } sub search_loc { my ($dbr, $req, $info) = @_; my ($sth); my ($longcountry, $longstate, $longcity); my $qcode = $dbr->quote(uc($req->{'loc_cn'})); $sth = $dbr->prepare("SELECT item FROM codes WHERE type='country' AND code=$qcode"); $sth->execute; ($longcountry) = $sth->fetchrow_array; $longstate = lc($req->{'loc_st'}); $longstate =~ s/(\w+)/\u$1/g; $longcity = lc($req->{'loc_ci'}); $longcity =~ s/(\w+)/\u$1/g; $req->{'loc_st'} = lc($req->{'loc_st'}); $req->{'loc_ci'} = lc($req->{'loc_ci'}); if ($req->{'loc_cn'} eq "US") { my $qstate = $dbr->quote($req->{'loc_st'}); if (length($req->{'loc_st'}) > 2) { ## convert long state name into state code $sth = $dbr->prepare("SELECT code FROM codes WHERE type='state' AND item=$qstate"); $sth->execute; my ($code) = $sth->fetchrow_array; if ($code) { $req->{'loc_st'} = lc($code); } } else { $sth = $dbr->prepare("SELECT item FROM codes WHERE type='state' AND code=$qstate"); $sth->execute; ($longstate) = $sth->fetchrow_array; } } push @{$info->{'english'}}, "live in " . join(", ", grep { $_; } ($longcity, $longstate, $longcountry)); my $p = LJ::get_prop("user", "sidx_loc"); unless ($p) { $info->{'errmsg'} = "Userprop sidx_loc doesn't exist. Run update-db.pl?"; return; } my $prefix = join("-", $req->{'loc_cn'}, $req->{'loc_st'}, $req->{'loc_ci'}); $prefix =~ s/\-+$//; # remove trailing hyphens $prefix =~ s![\_\%\"\']!\\$&!g; #### do the sub requests. return { 'tables' => { 'up' => 'userprop', }, 'conds' => [ "{up}.upropid=$p->{'id'}", "{up}.value LIKE '$prefix%'", ], 'userid' => "{up}.userid", }; } ########### GENDER ################### sub validate_gen { my ($req, $errors) = @_; return 0 unless $req->{'gen_sel'}; unless ($req->{'gen_sel'} eq "M" || $req->{'gen_sel'} eq "F") { push @$errors, "You must select either Male or Female when searching by gender.\n"; return 0; } return 1; } sub search_gen { my ($dbr, $req, $info) = @_; my $args = $req->{'gen_sel'}; push @{$info->{'english'}}, "are " . ($args eq "M" ? "male" : "female"); my $qgen = $dbr->quote($args); my $p = LJ::get_prop("user", "gender"); unless ($p) { $info->{'errmsg'} = "Userprop gender doesn't exist. Run update-db.pl?"; return; } return { 'tables' => { 'up' => 'userprop', }, 'conds' => [ "{up}.upropid=$p->{'id'}", "{up}.value=$qgen", ], 'userid' => "{up}.userid", }; } ########### AGE ################### sub validate_age { my ($req, $errors) = @_; return 0 if $req->{'age_min'} eq "" && $req->{'age_max'} eq ""; for (qw(age_min age_max)) { unless ($req->{$_} =~ /^\d+$/) { push @$errors, "Both min and max age must be specified for an age query."; return 0; } } if ($req->{'age_min'} > $req->{'age_max'}) { push @$errors, "Minimum age must be less than maximum age."; return 0; } if ($req->{'age_min'} < 14) { push @$errors, "You cannot search for users under 14 years of age."; return 0; } return 1; } sub search_age { my ($dbr, $req, $info) = @_; my $qagemin = $dbr->quote($req->{'age_min'}); my $qagemax = $dbr->quote($req->{'age_max'}); my $args = "$req->{'age_min'}-$req->{'age_max'}"; if ($req->{'age_min'} == $req->{'age_max'}) { push @{$info->{'english'}}, "are $req->{'age_min'} years old"; } else { push @{$info->{'english'}}, "are between $req->{'age_min'} and $req->{'age_max'} years old"; } my $p = LJ::get_prop("user", "sidx_bdate"); unless ($p) { $info->{'errmsg'} = "Userprop sidx_bdate doesn't exist. Run update-db.pl?"; return; } return { 'tables' => { 'up' => 'userprop', }, 'conds' => [ "{up}.upropid=$p->{'id'}", "{up}.value BETWEEN DATE_SUB(NOW(), INTERVAL $qagemax YEAR) AND DATE_SUB(NOW(), INTERVAL $qagemin YEAR)", ], 'userid' => "{up}.userid", }; } ########### UPDATE TIME ################### sub validate_ut { my ($req, $errors) = @_; return 0 unless $req->{'ut_days'}; unless ($req->{'ut_days'} =~ /^\d+$/) { push @$errors, "Days since last updated must be a postive, whole number."; return; } return 1; } sub search_ut { my ($dbr, $req, $info) = @_; my $qdays = $req->{'ut_days'}+0; if ($qdays == 1) { push @{$info->{'english'}}, "have updated their journal in the past day"; } else { push @{$info->{'english'}}, "have updated their journal in the past $qdays days"; } return { 'tables' => { 'uu' => 'userusage', }, 'conds' => [ "{uu}.timeupdate > DATE_SUB(NOW(), INTERVAL $qdays DAY)", ], 'userid' => "{uu}.userid", }; } ######### community sub validate_com { my ($req, $errors) = @_; return 0 unless $req->{'com_do'}; return 1; } sub search_com { my ($dbr, $req, $info) = @_; $info->{'allwhat'} = "communities"; return { 'tables' => { 'c' => 'community', }, 'userid' => "{c}.userid", }; } 1;