650 lines
18 KiB
Perl
Executable File
650 lines
18 KiB
Perl
Executable File
#!/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;
|