1186 lines
40 KiB
Perl
1186 lines
40 KiB
Perl
|
#!/usr/bin/perl
|
||
|
|
||
|
package LJ::Tags;
|
||
|
|
||
|
use strict;
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::get_usertagsmulti
|
||
|
# class: tags
|
||
|
# des: Gets a bunch of tags for the specified list of users.
|
||
|
# args: uobj*
|
||
|
# des-uobj: One or more user ids or objects to load the tags for.
|
||
|
# returns: Hashref; { userid => *tagref*, userid => *tagref*, ... } where *tagref* is the
|
||
|
# return value of LJ::Tags::get_usertags -- undef on failure
|
||
|
# </LJFUNC>
|
||
|
sub get_usertagsmulti {
|
||
|
return {} if $LJ::DISABLED{tags};
|
||
|
|
||
|
# get input users
|
||
|
my @uobjs = grep { defined } map { LJ::want_user($_) } @_;
|
||
|
return {} unless @uobjs;
|
||
|
|
||
|
# now setup variables we'll need
|
||
|
my @memkeys; # memcache keys to fetch
|
||
|
my @resjids; # final list of journal ids
|
||
|
my $res = {}; # { jid => { tagid => {}, ... }, ... }; results return hashref
|
||
|
my %jid2cid; # ( jid => cid ); cross reference journals to clusters
|
||
|
my %need; # ( cid => { jid => 1 } ); what we still need
|
||
|
|
||
|
# prepopulate our structures
|
||
|
foreach my $u (@uobjs) {
|
||
|
$jid2cid{$u->{userid}} = $u->{clusterid};
|
||
|
$need{$u->{clusterid}}->{$u->{userid}} = 1;
|
||
|
push @memkeys, [ $u->{userid}, "tags:$u->{userid}" ];
|
||
|
}
|
||
|
|
||
|
# gather data from memcache if available
|
||
|
my $memc = LJ::MemCache::get_multi(@memkeys) || {};
|
||
|
foreach my $key (keys %$memc) {
|
||
|
if ($key =~ /^tags:(\d+)$/) {
|
||
|
my $jid = $1;
|
||
|
my $cid = $jid2cid{$jid};
|
||
|
|
||
|
# set this up in our return hash
|
||
|
$res->{$jid} = $memc->{$key};
|
||
|
|
||
|
# no longer need this user
|
||
|
delete $need{$cid}->{$jid};
|
||
|
|
||
|
# delete cluster if no more users
|
||
|
delete $need{$cid}
|
||
|
unless %{$need{$cid}};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# now, what we need per cluster...
|
||
|
foreach my $cid (keys %need) {
|
||
|
# get db for this cluster
|
||
|
my $dbcr = LJ::get_cluster_def_reader($cid)
|
||
|
or next;
|
||
|
|
||
|
# useful sql
|
||
|
my $in = join(',', map { $_ + 0 } keys %{$need{$cid}});
|
||
|
|
||
|
# get the tags from the database
|
||
|
my $tagrows = $dbcr->selectall_arrayref(qq{
|
||
|
SELECT journalid, kwid, parentkwid, display
|
||
|
FROM usertags
|
||
|
WHERE journalid IN ($in)
|
||
|
});
|
||
|
next if $dbcr->err || ! $tagrows;
|
||
|
|
||
|
# break down into data structures
|
||
|
my %tags; # ( jid => ( id => display ) )
|
||
|
$tags{$_->[0]}->{$_->[1]} = $_->[3]
|
||
|
foreach @$tagrows;
|
||
|
|
||
|
# if they have no tags...
|
||
|
next unless %tags;
|
||
|
|
||
|
# create SQL for finding the proper ids... (userid = ? AND kwid IN (...)) OR (userid = ? ...) ...
|
||
|
my @stmts;
|
||
|
foreach my $uid (keys %tags) {
|
||
|
push @stmts, "(userid = " . ($uid+0) . " AND kwid IN (" .
|
||
|
join(',', map { $_+0 } keys %{$tags{$uid}}) .
|
||
|
"))";
|
||
|
}
|
||
|
my $where = join(' OR ', @stmts);
|
||
|
|
||
|
# get the keyword ids they have used as tags
|
||
|
my $rows = $dbcr->selectall_arrayref("SELECT userid, kwid, keyword FROM userkeywords WHERE $where");
|
||
|
next if $dbcr->err || ! $rows;
|
||
|
|
||
|
# now turn this into a tentative results hash: { userid => { tagid => { name => tagname, ... }, ... } }
|
||
|
foreach my $row (@$rows) {
|
||
|
$res->{$row->[0]}->{$row->[1]} =
|
||
|
{
|
||
|
name => $row->[2],
|
||
|
security => {
|
||
|
public => 0,
|
||
|
groups => {},
|
||
|
private => 0,
|
||
|
friends => 0
|
||
|
},
|
||
|
uses => 0,
|
||
|
display => $tags{$row->[0]}->{$row->[1]},
|
||
|
};
|
||
|
}
|
||
|
@resjids = keys %$res;
|
||
|
|
||
|
# get security counts
|
||
|
my $ids = join(',', map { $_+0 } @resjids);
|
||
|
|
||
|
# populate security counts
|
||
|
my $counts = $dbcr->selectall_arrayref("SELECT journalid, kwid, security, entryct FROM logkwsum WHERE journalid IN ($ids)");
|
||
|
next if $dbcr->err || ! $counts;
|
||
|
|
||
|
# setup some helper values
|
||
|
my $public_mask = 1 << 31;
|
||
|
my $friends_mask = 1 << 0;
|
||
|
|
||
|
# melt this information down into the hashref
|
||
|
foreach my $row (@$counts) {
|
||
|
my ($jid, $kwid, $sec, $ct) = @$row;
|
||
|
|
||
|
# make sure this journal and keyword are present in the results already
|
||
|
# so we don't auto-vivify something with security that has no keyword with it
|
||
|
next unless $res->{$jid} && $res->{$jid}->{$kwid};
|
||
|
|
||
|
# add these to the total uses
|
||
|
$res->{$jid}->{$kwid}->{uses} += $ct;
|
||
|
|
||
|
if ($sec & $public_mask) {
|
||
|
$res->{$jid}->{$kwid}->{security}->{public} += $ct;
|
||
|
$res->{$jid}->{$kwid}->{security_level} = 'public';
|
||
|
} elsif ($sec & $friends_mask) {
|
||
|
$res->{$jid}->{$kwid}->{security}->{friends} += $ct;
|
||
|
$res->{$jid}->{$kwid}->{security_level} = 'friends'
|
||
|
unless $res->{$jid}->{$kwid}->{security_level} eq 'public';
|
||
|
} elsif ($sec) {
|
||
|
# if $sec is true (>0), and not friends/public, then it's a group. but it's
|
||
|
# still in the form of a number, and we want to know which group it is. so
|
||
|
# we must convert the mask back to a bit number with LJ::bit_breakdown. but
|
||
|
# we will only ever have one mask, so we just accept that.
|
||
|
my $grpid = (LJ::bit_breakdown($sec))[0] + 0;
|
||
|
$res->{$jid}->{$kwid}->{security}->{groups}->{$grpid} += $ct;
|
||
|
$res->{$jid}->{$kwid}->{security_level} ||= 'group';
|
||
|
} else {
|
||
|
# $sec must be 0
|
||
|
$res->{$jid}->{$kwid}->{security}->{private} += $ct;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# default securities to private and store to memcache
|
||
|
foreach my $jid (@resjids) {
|
||
|
$res->{$jid}->{$_}->{security_level} ||= 'private'
|
||
|
foreach keys %{$res->{$jid}};
|
||
|
|
||
|
LJ::MemCache::add([ $jid, "tags:$jid" ], $res->{$jid});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $res;
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::get_usertags
|
||
|
# class: tags
|
||
|
# des: Returns the tags that a user has defined for their account.
|
||
|
# args: uobj, opts?
|
||
|
# des-uobj: User object to get tags for.
|
||
|
# des-opts: Optional hashref; key can be 'remote' to filter tags to only ones that remote can see
|
||
|
# returns: Hashref; key being tag id, value being a large hashref (FIXME: document)
|
||
|
# </LJFUNC>
|
||
|
sub get_usertags {
|
||
|
return {} if $LJ::DISABLED{tags};
|
||
|
|
||
|
my $u = LJ::want_user(shift)
|
||
|
or return undef;
|
||
|
my $opts = shift() || {};
|
||
|
|
||
|
# get tags for this user
|
||
|
my $tags = LJ::Tags::get_usertagsmulti($u);
|
||
|
return undef unless $tags;
|
||
|
|
||
|
# get the tags for this user
|
||
|
my $res = $tags->{$u->{userid}} || {};
|
||
|
return {} unless %$res;
|
||
|
|
||
|
# now if they provided a remote, remove the ones they don't want to see; note that
|
||
|
# remote may be undef so we have to check exists
|
||
|
if (exists $opts->{remote}) {
|
||
|
# never going to cull anything if it's you, so return it
|
||
|
return $res if LJ::u_equals($u, $opts->{remote});
|
||
|
|
||
|
# setup helper variables from u to remote
|
||
|
my ($is_friend, $grpmask) = (0, 0);
|
||
|
if ($opts->{remote}) {
|
||
|
$is_friend = LJ::is_friend($u, $opts->{remote});
|
||
|
$grpmask = LJ::get_groupmask($u, $opts->{remote});
|
||
|
}
|
||
|
|
||
|
# figure out what we need to purge
|
||
|
my @purge;
|
||
|
TAG: foreach my $tagid (keys %$res) {
|
||
|
my $sec = $res->{$tagid}->{security_level};
|
||
|
next TAG if $sec eq 'public';
|
||
|
next TAG if $is_friend && $sec eq 'friends';
|
||
|
if ($grpmask && $sec eq 'group') {
|
||
|
foreach my $grpid (%{$res->{$tagid}->{security}->{groups}}) {
|
||
|
next TAG if $grpmask & (1 << $grpid);
|
||
|
}
|
||
|
}
|
||
|
push @purge, $tagid;
|
||
|
}
|
||
|
delete $res->{$_} foreach @purge;
|
||
|
}
|
||
|
|
||
|
return $res;
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::get_entry_tags
|
||
|
# class: tags
|
||
|
# des: Gets tags that have been used on an entry
|
||
|
# args: uuserid, jitemid
|
||
|
# des-uuserid: User id or object of account with entry
|
||
|
# des-jitemid: Journal itemid of entry; may also be arrayref of jitemids in journal.
|
||
|
# returns: Hashref; { jitemid => { tagid => tagname, tagid => tagname, ... }, ... }
|
||
|
# </LJFUNC>
|
||
|
sub get_logtags {
|
||
|
return {} if $LJ::DISABLED{tags};
|
||
|
|
||
|
my $u = LJ::want_user(shift);
|
||
|
return undef unless $u;
|
||
|
|
||
|
# handle magic jitemid parameter
|
||
|
my $jitemid = shift;
|
||
|
unless (ref $jitemid eq 'ARRAY') {
|
||
|
$jitemid = [ $jitemid+0 ];
|
||
|
return undef unless $jitemid->[0];
|
||
|
}
|
||
|
return undef unless @$jitemid;
|
||
|
|
||
|
# transform to a call to get_logtagsmulti
|
||
|
my $ret = LJ::Tags::get_logtagsmulti({ $u->{clusterid} => [ map { [ $u->{userid}, $_ ] } @$jitemid ] });
|
||
|
return undef unless $ret && ref $ret eq 'HASH';
|
||
|
|
||
|
# now construct result hashref
|
||
|
return { map { $_ => $ret->{"$u->{userid} $_"} } @$jitemid };
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::get_logtagsmulti
|
||
|
# class: tags
|
||
|
# des: Load tags on a given set of entries
|
||
|
# args: idsbyc
|
||
|
# des-idsbyc: { clusterid => [ [ jid, jitemid ], [ jid, jitemid ], ... ] }
|
||
|
# returns: hashref with "jid jitemid" keys, value of each being a hashref of
|
||
|
# { tagid => tagname, ... }
|
||
|
# </LJFUNC>
|
||
|
sub get_logtagsmulti {
|
||
|
return {} if $LJ::DISABLED{tags};
|
||
|
|
||
|
# get parameter (only one!)
|
||
|
my $idsbycluster = shift;
|
||
|
return undef unless $idsbycluster && ref $idsbycluster eq 'HASH';
|
||
|
|
||
|
# the mass of variables to make this mess work!
|
||
|
my @jids; # journalids we've seen
|
||
|
my @memkeys; # memcache keys to load
|
||
|
my %ret; # ( jid => { jitemid => [ tagid, tagid, ... ], ... } ); storage for data pre-final conversion
|
||
|
my %set; # ( jid => ( jitemid => [ tagid, tagid, ... ] ) ); for setting in memcache
|
||
|
my $res = {}; # { "jid jitemid" => { tagid => kw, tagid => kw, ... } }; final results hashref for return
|
||
|
my %need; # ( cid => { jid => { jitemid => 1, jitemid => 1 } } ); what still needs loading
|
||
|
my %jid2cid; # ( jid => cid ); map of journal id to clusterid
|
||
|
|
||
|
# construct memcache keys for loading below
|
||
|
foreach my $cid (keys %$idsbycluster) {
|
||
|
foreach my $row (@{$idsbycluster->{$cid} || []}) {
|
||
|
$need{$cid}->{$row->[0]}->{$row->[1]} = 1;
|
||
|
$jid2cid{$row->[0]} = $cid;
|
||
|
push @memkeys, [ $row->[0], "logtag:$row->[0]:$row->[1]" ];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# now hit up memcache to try to find what we can
|
||
|
my $memc = LJ::MemCache::get_multi(@memkeys) || {};
|
||
|
foreach my $key (keys %$memc) {
|
||
|
if ($key =~ /^logtag:(\d+):(\d+)$/) {
|
||
|
my ($jid, $jitemid) = ($1, $2);
|
||
|
my $cid = $jid2cid{$jid};
|
||
|
|
||
|
# save memcache output hashref to out %ret var
|
||
|
$ret{$jid}->{$jitemid} = $memc->{$key};
|
||
|
|
||
|
# no longer need this jid->jitemid combo
|
||
|
delete $need{$cid}->{$jid}->{$jitemid};
|
||
|
|
||
|
# no longer need this user if no more jitemids for them
|
||
|
delete $need{$cid}->{$jid}
|
||
|
unless %{$need{$cid}->{$jid}};
|
||
|
|
||
|
# delete cluster from need if no more users on it
|
||
|
delete $need{$cid}
|
||
|
unless %{$need{$cid}};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# iterate over clusters and construct SQL to get the data...
|
||
|
foreach my $cid (keys %need) {
|
||
|
my $dbcm = LJ::get_cluster_master($cid)
|
||
|
or return undef;
|
||
|
|
||
|
# list of (jid, jitemid) pairs that we get from %need
|
||
|
my @bind;
|
||
|
foreach my $jid (keys %{$need{$cid} || {}}) {
|
||
|
push @bind, ($jid, $_)
|
||
|
foreach keys %{$need{$cid}->{$jid} || {}};
|
||
|
}
|
||
|
|
||
|
# @bind is always even (from above), so the count of query elements we need is the
|
||
|
# number of items in @bind, divided by 2
|
||
|
my $sql = join(' OR ', map { "(journalid = ? AND jitemid = ?)" } 1..(scalar(@bind)/2));
|
||
|
|
||
|
# prepare the query to run
|
||
|
my $sth = $dbcm->prepare("SELECT journalid, jitemid, kwid FROM logtags WHERE ($sql)");
|
||
|
return undef if $dbcm->err || ! $sth;
|
||
|
|
||
|
# execute, fail on error
|
||
|
$sth->execute(@bind);
|
||
|
return undef if $sth->err;
|
||
|
|
||
|
# get data into %set so we add it to memcache later
|
||
|
while (my ($jid, $jitemid, $kwid) = $sth->fetchrow_array) {
|
||
|
push @{$set{$jid}->{$jitemid} ||= []}, $kwid;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# now add the things to memcache that we loaded from the clusters and also
|
||
|
# transport them into the $ret hashref or returning to the user
|
||
|
foreach my $jid (keys %set) {
|
||
|
foreach my $jitemid (keys %{$set{$jid}}) {
|
||
|
LJ::MemCache::add([ $jid, "logtag:$jid:$jitemid" ], $set{$jid}->{$jitemid});
|
||
|
$ret{$jid}->{$jitemid} = $set{$jid}->{$jitemid};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# quickly load all tags for the users we've found
|
||
|
@jids = keys %ret;
|
||
|
my $utags = LJ::Tags::get_usertagsmulti(@jids);
|
||
|
return undef unless $utags;
|
||
|
|
||
|
# last step: convert keywordids to keywords
|
||
|
foreach my $jid (@jids) {
|
||
|
my $tags = $utags->{$jid};
|
||
|
next unless $tags;
|
||
|
|
||
|
# transpose data from %ret into $res hashref which has (kwid => keyword) pairs
|
||
|
foreach my $jitemid (keys %{$ret{$jid}}) {
|
||
|
$res->{"$jid $jitemid"}->{$_} = $tags->{$_}->{name}
|
||
|
foreach @{$ret{$jid}->{$jitemid} || []};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# finally return the result hashref
|
||
|
return $res;
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::can_add_tags
|
||
|
# class: tags
|
||
|
# des: Determines if one account is allowed to add tags to another's post
|
||
|
# args: u, remote
|
||
|
# des-u: User id or object of account tags are being added to
|
||
|
# des-remote: User id or object of account performing the action
|
||
|
# returns: 1 if allowed, 0 if not, undef on error
|
||
|
# </LJFUNC>
|
||
|
sub can_add_tags {
|
||
|
return undef if $LJ::DISABLED{tags};
|
||
|
|
||
|
my $u = LJ::want_user(shift);
|
||
|
my $remote = LJ::want_user(shift);
|
||
|
return undef unless $u && $remote;
|
||
|
return undef unless $remote->{journaltype} eq 'P';
|
||
|
return undef if LJ::is_banned($remote, $u);
|
||
|
|
||
|
# get permission hashref and check it; note that we fall back to the control
|
||
|
# permission, which will allow people to add even if they can't add by default
|
||
|
my $perms = LJ::Tags::get_permission_levels($u);
|
||
|
return LJ::Tags::_remote_satisfies_permission($u, $remote, $perms->{add}) ||
|
||
|
LJ::Tags::_remote_satisfies_permission($u, $remote, $perms->{control});
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::can_control_tags
|
||
|
# class: tags
|
||
|
# des: Determines if one account is allowed to control (add, edit, delete) the tags of another
|
||
|
# args: u, remote
|
||
|
# des-u: User id or object of account tags are being edited on
|
||
|
# des-remote: User id or object of account performing the action
|
||
|
# returns: 1 if allowed, 0 if not, undef on error
|
||
|
# </LJFUNC>
|
||
|
sub can_control_tags {
|
||
|
return undef if $LJ::DISABLED{tags};
|
||
|
|
||
|
my $u = LJ::want_user(shift);
|
||
|
my $remote = LJ::want_user(shift);
|
||
|
return undef unless $u && $remote;
|
||
|
return undef unless $remote->{journaltype} eq 'P';
|
||
|
return undef if LJ::is_banned($remote, $u);
|
||
|
|
||
|
# get permission hashref and check it
|
||
|
my $perms = LJ::Tags::get_permission_levels($u);
|
||
|
return LJ::Tags::_remote_satisfies_permission($u, $remote, $perms->{control});
|
||
|
}
|
||
|
|
||
|
# helper sub internal used by can_*_tags functions
|
||
|
sub _remote_satisfies_permission {
|
||
|
my ($u, $remote, $perm) = @_;
|
||
|
return undef unless $u && $remote && $perm;
|
||
|
|
||
|
# permission checks
|
||
|
if ($perm eq 'public') {
|
||
|
return 1;
|
||
|
} elsif ($perm eq 'none') {
|
||
|
return 0;
|
||
|
} elsif ($perm eq 'friends') {
|
||
|
return LJ::is_friend($u, $remote);
|
||
|
} elsif ($perm eq 'private') {
|
||
|
return LJ::can_manage($remote, $u);
|
||
|
} elsif ($perm =~ /^group:(\d+)$/) {
|
||
|
my $grpid = $1+0;
|
||
|
return undef unless $grpid >= 1 && $grpid <= 30;
|
||
|
|
||
|
my $mask = LJ::get_groupmask($u, $remote);
|
||
|
return ($mask & (1 << $grpid)) ? 1 : 0;
|
||
|
} else {
|
||
|
# else, problem!
|
||
|
return undef;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::get_permission_levels
|
||
|
# class: tags
|
||
|
# des: Gets the permission levels on an account
|
||
|
# args: uobj
|
||
|
# des-uobj: User id or object of account to get permissions for
|
||
|
# returns: Hashref; keys one of 'add', 'control'; values being 'private' (only the account
|
||
|
# in question), 'friends' (all friends), 'public' (everybody), 'group:N' (one
|
||
|
# friend group with given id), or 'none' (nobody can)
|
||
|
# </LJFUNC>
|
||
|
sub get_permission_levels {
|
||
|
return { add => 'none', control => 'none' }
|
||
|
if $LJ::DISABLED{tags};
|
||
|
|
||
|
my $u = LJ::want_user(shift);
|
||
|
return undef unless $u;
|
||
|
|
||
|
# get the prop
|
||
|
LJ::load_user_props($u, 'opt_tagpermissions');
|
||
|
|
||
|
# return defaults for accounts
|
||
|
unless ($u->{opt_tagpermissions}) {
|
||
|
if ($u->{journaltype} eq 'C') {
|
||
|
# communities are members (friends) add, private (maintainers) control
|
||
|
return { add => 'friends', control => 'private' };
|
||
|
} elsif ($u->{journaltype} eq 'P') {
|
||
|
# people let friends add, self control
|
||
|
return { add => 'private', control => 'private' };
|
||
|
} else {
|
||
|
# other account types can't add tags
|
||
|
return { add => 'none', control => 'none' };
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# now split and return
|
||
|
my ($add, $control) = split(/\s*,\s*/, $u->{opt_tagpermissions});
|
||
|
return { add => $add, control => $control };
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::is_valid_tagstring
|
||
|
# class: tags
|
||
|
# des: Determines if a string contains a valid list of tags.
|
||
|
# args: tagstring, listref?
|
||
|
# des-tagstring: Opaque tag string provided by the user.
|
||
|
# des-listref: If specified, return valid list of canonical tags in arrayref here.
|
||
|
# returns: 1 if list is valid, 0 if not.
|
||
|
# </LJFUNC>
|
||
|
sub is_valid_tagstring {
|
||
|
my ($tagstring, $listref) = @_;
|
||
|
return 0 unless $tagstring;
|
||
|
$listref ||= [];
|
||
|
|
||
|
# setup helper subs
|
||
|
my $valid_tag = sub {
|
||
|
my $tag = shift;
|
||
|
return 0 if $tag =~ /^_/; # reserved for future use (starting with underscore)
|
||
|
return 0 if $tag =~ /[\<\>\r\n\t]/; # no HTML, newlines, tabs, etc
|
||
|
return 0 unless $tag =~ /^(?:.+\s?)+$/; # one or more "words"
|
||
|
return 1;
|
||
|
};
|
||
|
my $canonical_tag = sub {
|
||
|
my $tag = shift;
|
||
|
$tag = LJ::trim($tag);
|
||
|
$tag =~ s/\s+/ /g; # condense multiple spaces to a single space
|
||
|
$tag = LJ::text_trim($tag, LJ::BMAX_KEYWORD, LJ::CMAX_KEYWORD);
|
||
|
$tag = lc $tag
|
||
|
if $tag !~ /[\x7f-\xff]/;
|
||
|
return $tag;
|
||
|
};
|
||
|
|
||
|
# now iterate
|
||
|
my @list = grep { length $_ } # only keep things that are something
|
||
|
map { LJ::trim($_) } # remove leading/trailing spaces
|
||
|
split(/\s*,\s*/, $tagstring); # split on comma with optional spaces
|
||
|
return 0 unless @list;
|
||
|
|
||
|
# now validate each one as we go
|
||
|
foreach my $tag (@list) {
|
||
|
# canonicalize and determine validity
|
||
|
$tag = $canonical_tag->($tag);
|
||
|
return 0 unless $valid_tag->($tag);
|
||
|
|
||
|
# now push on our list
|
||
|
push @$listref, $tag;
|
||
|
}
|
||
|
|
||
|
# well, it must have been okay if we got here
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::get_security_breakdown
|
||
|
# class: tags
|
||
|
# des: Returns a list of security levels that apply to the given security information.
|
||
|
# args: security, allowmask
|
||
|
# des-security: 'private', 'public', or 'usemask'
|
||
|
# des-allowmask: a bitmask in standard allowmask form
|
||
|
# returns: List of broken down security levels to use for logkwsum table.
|
||
|
# </LJFUNC>
|
||
|
sub get_security_breakdown {
|
||
|
my ($sec, $mask) = @_;
|
||
|
|
||
|
my @out;
|
||
|
|
||
|
if ($sec eq 'private') {
|
||
|
@out = (0);
|
||
|
} elsif ($sec eq 'public') {
|
||
|
@out = (1 << 31);
|
||
|
} else {
|
||
|
# have to get each group bit into a mask
|
||
|
foreach my $bit (0..30) { # include 0 for friends only
|
||
|
if ($mask & (1 << $bit)) {
|
||
|
push @out, (1 << $bit);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return @out;
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::update_logtags
|
||
|
# class: tags
|
||
|
# des: Updates the tags on an entry. Tags not in the list you provide are deleted.
|
||
|
# args: uobj, jitemid, uobj, tags,
|
||
|
# des-uobj: User id or object of account with entry
|
||
|
# des-jitemid: Journal itemid of entry to tag
|
||
|
# des-opts: Hashref; keys being the action and values of the key being an arrayref of
|
||
|
# tags to involve in the action. Possible actions are 'add', 'set', and
|
||
|
# 'delete'. With those, the value is a hashref of the tags (textual tags)
|
||
|
# to add, set, or delete. Other actions are 'add_ids', 'set_ids', and
|
||
|
# 'delete_ids'. The value arrayref should then contain the tag ids to
|
||
|
# act with. Can also specify 'add_string', 'set_string', or 'delete_string'
|
||
|
# as a comma separated list of user-supplied tags which are then canonicalized
|
||
|
# and used. 'remote' is the remote user taking the actions (required).
|
||
|
# returns: 1 on success, undef on error
|
||
|
# </LJFUNC>
|
||
|
sub update_logtags {
|
||
|
return undef if $LJ::DISABLED{tags};
|
||
|
|
||
|
my $u = LJ::want_user(shift);
|
||
|
my $jitemid = shift() + 0;
|
||
|
return undef unless $u && $jitemid;
|
||
|
return undef unless $u->writer;
|
||
|
|
||
|
# ensure we have an options hashref
|
||
|
my $opts = shift;
|
||
|
return undef unless $opts && ref $opts eq 'HASH';
|
||
|
|
||
|
# perform set logic?
|
||
|
my $do_set = exists $opts->{set} || exists $opts->{set_ids} || exists $opts->{set_string};
|
||
|
|
||
|
# now get extra options
|
||
|
my $remote = LJ::want_user(delete $opts->{remote});
|
||
|
return undef unless $remote || $opts->{force};
|
||
|
|
||
|
# get access levels
|
||
|
my $can_control = LJ::Tags::can_control_tags($u, $remote);
|
||
|
my $can_add = $can_control || LJ::Tags::can_add_tags($u, $remote);
|
||
|
return undef unless $can_add || $opts->{force};
|
||
|
|
||
|
# load the user's tags
|
||
|
my $utags = LJ::Tags::get_usertags($u);
|
||
|
return undef unless $utags;
|
||
|
|
||
|
# take arrayrefs of tag strings and stringify them for validation
|
||
|
foreach my $verb (qw(add set delete)) {
|
||
|
# if given tags, combine into a string
|
||
|
if ($opts->{$verb}) {
|
||
|
$opts->{"${verb}_string"} = join(',', @{$opts->{$verb}});
|
||
|
$opts->{$verb} = [];
|
||
|
}
|
||
|
|
||
|
# now validate the string, if we have one
|
||
|
if ($opts->{"${verb}_string"}) {
|
||
|
$opts->{$verb} = [];
|
||
|
return undef
|
||
|
unless LJ::Tags::is_valid_tagstring($opts->{"${verb}_string"}, $opts->{$verb});
|
||
|
}
|
||
|
|
||
|
# and turn everything into ids
|
||
|
$opts->{"${verb}_ids"} ||= [];
|
||
|
foreach my $kw (@{$opts->{$verb} || []}) {
|
||
|
my $kwid = LJ::get_keyword_id($u, $kw, $can_control);
|
||
|
if ($can_control) {
|
||
|
# error if we failed to create
|
||
|
return undef unless $kwid;
|
||
|
} else {
|
||
|
# if we're not creating, who cares, just skip; also skip if the keyword
|
||
|
# is not really a tag (don't promote it)
|
||
|
next unless $kwid && $utags->{$kwid};
|
||
|
}
|
||
|
|
||
|
# create it if necessary
|
||
|
LJ::Tags::create_usertag($u, $kw, { display => 1 })
|
||
|
unless $utags->{$kwid};
|
||
|
|
||
|
push @{$opts->{"${verb}_ids"}}, $kwid;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# setup %add/%delete hashes, for easier duplicate removal
|
||
|
my %add = ( map { $_ => 1 } @{$opts->{add_ids} || []} );
|
||
|
my %delete = ( map { $_ => 1 } @{$opts->{delete_ids} || []} );
|
||
|
|
||
|
# used to keep counts in sync
|
||
|
my $tags = LJ::Tags::get_logtags($u, $jitemid);
|
||
|
return undef unless $tags;
|
||
|
|
||
|
# now get tags for this entry; which there might be none, so make it a hashref
|
||
|
$tags = $tags->{$jitemid} || {};
|
||
|
|
||
|
# set is broken down into add/delete as necessary
|
||
|
if ($do_set || ($opts->{set_ids} && @{$opts->{set_ids}})) {
|
||
|
# mark everything to delete, we'll fix it shortly
|
||
|
$delete{$_} = 1 foreach keys %{$tags};
|
||
|
|
||
|
# and now go through the set we want, things that are in the delete
|
||
|
# pile are just nudge so we don't touch them, and everything else we
|
||
|
# throw in the add pile
|
||
|
foreach my $id (@{$opts->{set_ids}}) {
|
||
|
$add{$id} = 1
|
||
|
unless delete $delete{$id};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
# now don't readd things we already have
|
||
|
delete $add{$_} foreach keys %{$tags};
|
||
|
|
||
|
# but delete nothing if we're not a controller
|
||
|
%delete = () unless $can_control || $opts->{force};
|
||
|
|
||
|
# bail out if nothing needs to be done
|
||
|
return 1 unless %add || %delete;
|
||
|
|
||
|
# %add and %delete are accurate, but we need to track necessary
|
||
|
# security updates; this is a hash of keyword ids and a modification
|
||
|
# value (a delta; +/-N) to be applied to that row later
|
||
|
my %security;
|
||
|
|
||
|
# get the security of this post for use in %security; do this now so
|
||
|
# we don't interrupt the transaction below
|
||
|
my $l2row = LJ::get_log2_row($u, $jitemid);
|
||
|
return undef unless $l2row;
|
||
|
|
||
|
# calculate security masks
|
||
|
my @sec = LJ::Tags::get_security_breakdown($l2row->{security}, $l2row->{allowmask});
|
||
|
|
||
|
# setup a rollback bail path so that we can undo everything we've done
|
||
|
# if anything fails in the middle; and if the rollback fails, scream loudly
|
||
|
# and burst into flames!
|
||
|
my $rollback = sub {
|
||
|
die $u->errstr unless $u->rollback;
|
||
|
return undef;
|
||
|
};
|
||
|
|
||
|
# start the big transaction, for great justice!
|
||
|
$u->begin_work;
|
||
|
|
||
|
# process additions first
|
||
|
my @bind;
|
||
|
foreach my $kwid (keys %add) {
|
||
|
$security{$kwid}++;
|
||
|
push @bind, $u->{userid}, $jitemid, $kwid;
|
||
|
}
|
||
|
|
||
|
# now add all to both tables; only do 100 rows (300 bind vars) at a time
|
||
|
while (my @list = splice(@bind, 0, 300)) {
|
||
|
my $sql = join(',', map { "(?,?,?)" } 1..(scalar(@list)/3));
|
||
|
|
||
|
$u->do("REPLACE INTO logtags (journalid, jitemid, kwid) VALUES $sql", undef, @list);
|
||
|
return $rollback->() if $u->err;
|
||
|
|
||
|
$u->do("REPLACE INTO logtagsrecent (journalid, jitemid, kwid) VALUES $sql", undef, @list);
|
||
|
return $rollback->() if $u->err;
|
||
|
}
|
||
|
|
||
|
# now process deletions
|
||
|
@bind = ();
|
||
|
foreach my $kwid (keys %delete) {
|
||
|
$security{$kwid}--;
|
||
|
push @bind, $kwid;
|
||
|
}
|
||
|
|
||
|
# now run the SQL
|
||
|
while (my @list = splice(@bind, 0, 100)) {
|
||
|
my $sql = join(',', map { $_ + 0 } @list);
|
||
|
|
||
|
$u->do("DELETE FROM logtags WHERE journalid = ? AND jitemid = ? AND kwid IN ($sql)",
|
||
|
undef, $u->{userid}, $jitemid);
|
||
|
return $rollback->() if $u->err;
|
||
|
|
||
|
$u->do("DELETE FROM logtagsrecent WHERE journalid = ? AND kwid IN ($sql) AND jitemid = ?",
|
||
|
undef, $u->{userid}, $jitemid);
|
||
|
return $rollback->() if $u->err;
|
||
|
}
|
||
|
|
||
|
# now handle lazy cleaning of this table for these tag ids; note that the
|
||
|
# %security hash contains all of the keywords we've operated on in total
|
||
|
my @kwids = keys %security;
|
||
|
my $sql = join(',', map { $_ + 0 } @kwids);
|
||
|
my $sth = $u->prepare("SELECT kwid, COUNT(*) FROM logtagsrecent WHERE journalid = ? AND kwid IN ($sql) GROUP BY 1");
|
||
|
return $rollback->() if $u->err || ! $sth;
|
||
|
$sth->execute($u->{userid});
|
||
|
return $rollback->() if $sth->err;
|
||
|
|
||
|
# now iterate over counts and find ones that are too high
|
||
|
my %delrecent; # kwid => [ jitemid, jitemid, ... ]
|
||
|
while (my ($kwid, $ct) = $sth->fetchrow_array) {
|
||
|
next unless $ct > 120;
|
||
|
|
||
|
# get the times of the entries, the user time (lastn view uses user time), sort it, and then
|
||
|
# we can chop off jitemids that fall below the threshold -- but only in this keyword and only clean
|
||
|
# up some number at a time (25 at most, starting at our threshold)
|
||
|
my $sth2 = $u->prepare(qq{
|
||
|
SELECT t.jitemid
|
||
|
FROM logtagsrecent t, log2 l
|
||
|
WHERE t.journalid = l.journalid
|
||
|
AND t.jitemid = l.jitemid
|
||
|
AND t.journalid = ?
|
||
|
AND t.kwid = ?
|
||
|
ORDER BY l.eventtime DESC
|
||
|
LIMIT 100,25
|
||
|
});
|
||
|
return $rollback->() if $u->err || ! $sth2;
|
||
|
$sth2->execute($u->{userid}, $kwid);
|
||
|
return $rollback->() if $sth2->err;
|
||
|
|
||
|
# push these onto the hash for deleting below
|
||
|
while (my $jit = $sth2->fetchrow_array) {
|
||
|
push @{$delrecent{$kwid} ||= []}, $jit;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# now delete any recents we need to into this format:
|
||
|
# (kwid = 3 AND jitemid IN (2, 3, 4)) OR (kwid = ...) OR ...
|
||
|
# but only if we have some to delete
|
||
|
if (%delrecent) {
|
||
|
my $del = join(' OR ', map {
|
||
|
"(kwid = " . ($_+0) . " AND jitemid IN (" . join(',', map { $_+0 } @{$delrecent{$_}}) . "))"
|
||
|
} keys %delrecent);
|
||
|
$u->do("DELETE FROM logtagsrecent WHERE journalid = ? AND ($del)", undef, $u->{userid});
|
||
|
return $rollback->() if $u->err;
|
||
|
}
|
||
|
|
||
|
# now we must get the current security values in order to come up with a proper update; note that
|
||
|
# we select for update, which locks it so we have a consistent view of the rows
|
||
|
$sth = $u->prepare("SELECT kwid, security, entryct FROM logkwsum WHERE journalid = ? AND kwid IN ($sql) FOR UPDATE");
|
||
|
return $rollback->() if $u->err || ! $sth;
|
||
|
$sth->execute($u->{userid});
|
||
|
return $rollback->() if $sth->err;
|
||
|
|
||
|
# now iterate and get the security counts
|
||
|
my %counts;
|
||
|
while (my ($kwid, $sec, $ct) = $sth->fetchrow_array) {
|
||
|
$counts{$kwid}->{$sec} = $ct;
|
||
|
}
|
||
|
|
||
|
# now we want to update them, and delete any at 0
|
||
|
my (@replace, @delete);
|
||
|
foreach my $kwid (@kwids) {
|
||
|
foreach my $sec (@sec) {
|
||
|
if (exists $counts{$kwid} && exists $counts{$kwid}->{$sec}) {
|
||
|
# an old one exists
|
||
|
my $new = $counts{$kwid}->{$sec} + $security{$kwid};
|
||
|
if ($new > 0) {
|
||
|
# update it
|
||
|
push @replace, [ $kwid, $sec, $new ];
|
||
|
} else {
|
||
|
# delete this one
|
||
|
push @delete, [ $kwid, $sec ];
|
||
|
}
|
||
|
} else {
|
||
|
# add a new one
|
||
|
push @replace, [ $kwid, $sec, $security{$kwid} ];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# handle deletes in one move; well, 100 at a time
|
||
|
while (my @list = splice(@delete, 0, 100)) {
|
||
|
my $sql = join(' OR ', map { "(kwid = ? AND security = ?)" } 1..scalar(@list));
|
||
|
$u->do("DELETE FROM logkwsum WHERE journalid = ? AND ($sql)",
|
||
|
undef, $u->{userid}, map { @$_ } @list);
|
||
|
return $rollback->() if $u->err;
|
||
|
}
|
||
|
|
||
|
# handle replaces and inserts
|
||
|
while (my @list = splice(@replace, 0, 100)) {
|
||
|
my $sql = join(',', map { "(?,?,?,?)" } 1..scalar(@list));
|
||
|
$u->do("REPLACE INTO logkwsum (journalid, kwid, security, entryct) VALUES $sql",
|
||
|
undef, map { $u->{userid}, @$_ } @list);
|
||
|
return $rollback->() if $u->err;
|
||
|
}
|
||
|
|
||
|
# commit everything and smack caches and we're done!
|
||
|
die $u->errstr unless $u->commit;
|
||
|
LJ::Tags::reset_cache($u);
|
||
|
LJ::Tags::reset_cache($u => $jitemid);
|
||
|
return 1;
|
||
|
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::delete_logtags
|
||
|
# class: tags
|
||
|
# des: Deletes all tags on an entry.
|
||
|
# args: uobj, jitemid
|
||
|
# des-uobj: User id or object of account with entry
|
||
|
# des-jitemid: Journal itemid of entry to delete tags from
|
||
|
# returns: undef on error; 1 on success
|
||
|
# </LJFUNC>
|
||
|
sub delete_logtags {
|
||
|
return undef if $LJ::DISABLED{tags};
|
||
|
|
||
|
my $u = LJ::want_user(shift);
|
||
|
my $jitemid = shift() + 0;
|
||
|
return undef unless $u && $jitemid;
|
||
|
|
||
|
# maybe this is ghetto, but it does all of the logic we would otherwise
|
||
|
# have to duplicate here, so no sense in doing that.
|
||
|
return LJ::Tags::update_logtags($u, $jitemid, { set_string => "", force => 1, });
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::reset_cache
|
||
|
# class: tags
|
||
|
# des: Clears out all cached information for a user's tags.
|
||
|
# args: uobj, jitemid?
|
||
|
# des-uobj: User id or object of account to clear cache for
|
||
|
# des-jitemid: Either a single jitemid or an arrayref of jitemids to clear for the user. If
|
||
|
# not present, the user's tags cache is cleared. If present, the cache for those
|
||
|
# entries only are cleared.
|
||
|
# returns: undef on error; 1 on success
|
||
|
# </LJFUNC>
|
||
|
sub reset_cache {
|
||
|
return undef if $LJ::DISABLED{tags};
|
||
|
|
||
|
while (my ($u, $jitemid) = splice(@_, 0, 2)) {
|
||
|
next unless
|
||
|
$u = LJ::want_user($u);
|
||
|
|
||
|
# standard user tags cleanup
|
||
|
unless ($jitemid) {
|
||
|
LJ::MemCache::delete([ $u->{userid}, "tags:$u->{userid}" ]);
|
||
|
}
|
||
|
|
||
|
# now, cleanup entries if necessary
|
||
|
if ($jitemid) {
|
||
|
$jitemid = [ $jitemid ]
|
||
|
unless ref $jitemid eq 'ARRAY';
|
||
|
LJ::MemCache::delete([ $u->{userid}, "logtag:$u->{userid}:$_" ])
|
||
|
foreach @$jitemid;
|
||
|
}
|
||
|
}
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::create_usertag
|
||
|
# class: tags
|
||
|
# des: Creates tags for a user, returning the keyword ids allocated.
|
||
|
# args: uobj, kw, opts?
|
||
|
# des-uobj: User object to create tag on.
|
||
|
# des-kw: Tag string (comma separated list of tags) to create.
|
||
|
# des-opts: Optional; hashref, possible keys being 'display' and value being whether or
|
||
|
# not this tag should be a display tag and 'parenttagid' being the tagid of a
|
||
|
# parent tag for heirarchy.
|
||
|
# returns: undef on error, else a hashref of { keyword => tagid } for each keyword defined
|
||
|
# </LJFUNC>
|
||
|
sub create_usertag {
|
||
|
return undef if $LJ::DISABLED{tags};
|
||
|
|
||
|
my $u = LJ::want_user(shift);
|
||
|
my $kw = shift;
|
||
|
my $opts = shift || {};
|
||
|
return undef unless $u && $kw;
|
||
|
|
||
|
my $tags = [];
|
||
|
my $isvalid = LJ::Tags::is_valid_tagstring($kw, $tags);
|
||
|
return undef unless $isvalid;
|
||
|
|
||
|
my $display = $opts->{display} ? 1 : 0;
|
||
|
my $parentkwid = $opts->{parenttagid} ? ($opts->{parenttagid}+0) : undef;
|
||
|
|
||
|
my %res;
|
||
|
foreach my $tag (@$tags) {
|
||
|
my $kwid = LJ::get_keyword_id($u, $tag);
|
||
|
return undef unless $kwid;
|
||
|
|
||
|
$res{$tag} = $kwid;
|
||
|
}
|
||
|
|
||
|
my $ct = scalar keys %res;
|
||
|
my $bind = join(',', map { "(?,?,?,?)" } 1..$ct);
|
||
|
$u->do("INSERT IGNORE INTO usertags (journalid, kwid, parentkwid, display) VALUES $bind",
|
||
|
undef, map { $u->{userid}, $_, $parentkwid, $display } values %res);
|
||
|
return undef if $u->err;
|
||
|
|
||
|
LJ::Tags::reset_cache($u);
|
||
|
return \%res;
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::validate_tag
|
||
|
# class: tags
|
||
|
# des: Check the validity of a single tag.
|
||
|
# args: tag
|
||
|
# des-tag: The tag to check.
|
||
|
# returns: If valid, the canonicalized tag, else, undef.
|
||
|
# </LJFUNC>
|
||
|
sub validate_tag {
|
||
|
my $tag = shift;
|
||
|
return undef unless $tag;
|
||
|
|
||
|
my $list = [];
|
||
|
return undef unless
|
||
|
LJ::Tags::is_valid_tagstring($tag, $list);
|
||
|
return undef if scalar(@$list) > 1;
|
||
|
|
||
|
return $list->[0];
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::delete_usertag
|
||
|
# class: tags
|
||
|
# des: Deletes a tag for a user, and all mappings.
|
||
|
# args: uobj, type, tag
|
||
|
# des-uobj: User object to delete tag on.
|
||
|
# des-type: Either 'id' or 'name', indicating the type of the third parameter.
|
||
|
# des-tag: If type is 'id', this is the tag id (kwid). If type is 'name', this is the name of the
|
||
|
# tag that we want to delete from the user.
|
||
|
# returns: undef on error, 1 for success, 0 for tag not found
|
||
|
# </LJFUNC>
|
||
|
sub delete_usertag {
|
||
|
return undef if $LJ::DISABLED{tags};
|
||
|
|
||
|
my $u = LJ::want_user(shift);
|
||
|
return undef unless $u;
|
||
|
|
||
|
my ($type, $val) = @_;
|
||
|
|
||
|
my $kwid;
|
||
|
if ($type eq 'name') {
|
||
|
my $tag = LJ::Tags::validate_tag($val);
|
||
|
return undef unless $tag;
|
||
|
|
||
|
$kwid = LJ::get_keyword_id($u, $tag, 0);
|
||
|
} elsif ($type eq 'id') {
|
||
|
$kwid = $val + 0;
|
||
|
}
|
||
|
return undef unless $kwid;
|
||
|
|
||
|
# escape sub
|
||
|
my $rollback = sub {
|
||
|
die $u->errstr unless $u->rollback;
|
||
|
return undef;
|
||
|
};
|
||
|
|
||
|
# start the big transaction
|
||
|
$u->begin_work;
|
||
|
|
||
|
# get items this keyword is on
|
||
|
my $sth = $u->prepare('SELECT jitemid FROM logtags WHERE journalid = ? AND kwid = ? FOR UPDATE');
|
||
|
return $rollback->() if $u->err || ! $sth;
|
||
|
|
||
|
# now get the items
|
||
|
$sth->execute($u->{userid}, $kwid);
|
||
|
return $rollback->() if $sth->err;
|
||
|
|
||
|
# now get list of jitemids for later cache clearing
|
||
|
my @jitemids;
|
||
|
push @jitemids, $_
|
||
|
while $_ = $sth->fetchrow_array;
|
||
|
|
||
|
# delete this tag's information from the relevant tables
|
||
|
foreach my $table (qw(usertags logtags logtagsrecent logkwsum)) {
|
||
|
# no error checking, we're just deleting data that's already semi-unlinked due
|
||
|
# to us already updating the userprop above
|
||
|
$u->do("DELETE FROM $table WHERE journalid = ? AND kwid = ?",
|
||
|
undef, $u->{userid}, $kwid);
|
||
|
}
|
||
|
|
||
|
# all done with our updates
|
||
|
die $u->errstr unless $u->commit;
|
||
|
|
||
|
# reset caches, have to do both of these, one for the usertags one for logtags
|
||
|
LJ::Tags::reset_cache($u);
|
||
|
LJ::Tags::reset_cache($u => \@jitemids);
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::rename_usertag
|
||
|
# class: tags
|
||
|
# des: Deletes a tag for a user, and all mappings.
|
||
|
# args: uobj, type, tag, newname
|
||
|
# des-uobj: User object to delete tag on.
|
||
|
# des-type: Either 'id' or 'name', indicating the type of the third parameter.
|
||
|
# des-tag: If type is 'id', this is the tag id (kwid). If type is 'name', this is the name of the
|
||
|
# tag that we want to rename for the user.
|
||
|
# des-newname: The new name of this tag.
|
||
|
# returns: undef on error, 1 for success, 0 for tag not found
|
||
|
# </LJFUNC>
|
||
|
sub rename_usertag {
|
||
|
return undef if $LJ::DISABLED{tags};
|
||
|
|
||
|
# FIXME/TODO: make this function do merging?
|
||
|
|
||
|
my $u = LJ::want_user(shift);
|
||
|
return undef unless $u;
|
||
|
|
||
|
my ($type, $val, $newname) = @_;
|
||
|
return undef unless $type && $val && $newname;
|
||
|
|
||
|
# validate new tag
|
||
|
$newname = LJ::Tags::validate_tag($newname);
|
||
|
return undef unless $newname;
|
||
|
|
||
|
# get a list of keyword ids to operate on
|
||
|
my $kwid;
|
||
|
if ($type eq 'name') {
|
||
|
$val = LJ::Tags::validate_tag($val);
|
||
|
return undef unless $val;
|
||
|
|
||
|
$kwid = LJ::get_keyword_id($u, $val, 0);
|
||
|
} elsif ($type eq 'id') {
|
||
|
$kwid = $val + 0;
|
||
|
}
|
||
|
return undef unless $kwid;
|
||
|
|
||
|
# see if this is already a keyword
|
||
|
my $newkwid = LJ::get_keyword_id($u, $newname);
|
||
|
return undef unless $newkwid;
|
||
|
|
||
|
# see if the tag we're renaming TO already exists as a keyword,
|
||
|
# if so, don't allow the rename because we don't do merging (yet)
|
||
|
my $tags = LJ::Tags::get_usertags($u);
|
||
|
return undef if $tags->{$newkwid};
|
||
|
|
||
|
# escape sub
|
||
|
my $rollback = sub {
|
||
|
die $u->errstr unless $u->rollback;
|
||
|
return undef;
|
||
|
};
|
||
|
|
||
|
# start the big transaction
|
||
|
$u->begin_work;
|
||
|
|
||
|
# get items this keyword is on
|
||
|
my $sth = $u->prepare('SELECT jitemid FROM logtags WHERE journalid = ? AND kwid = ? FOR UPDATE');
|
||
|
return $rollback->() if $u->err || ! $sth;
|
||
|
|
||
|
# now get the items
|
||
|
$sth->execute($u->{userid}, $kwid);
|
||
|
return $rollback->() if $sth->err;
|
||
|
|
||
|
# now get list of jitemids for later cache clearing
|
||
|
my @jitemids;
|
||
|
push @jitemids, $_
|
||
|
while $_ = $sth->fetchrow_array;
|
||
|
|
||
|
# do database update to migrate from old to new
|
||
|
foreach my $table (qw(usertags logtags logtagsrecent logkwsum)) {
|
||
|
$u->do("UPDATE $table SET kwid = ? WHERE journalid = ? AND kwid = ?",
|
||
|
undef, $newkwid, $u->{userid}, $kwid);
|
||
|
return $rollback->() if $u->err;
|
||
|
}
|
||
|
|
||
|
# all done with our updates
|
||
|
die $u->errstr unless $u->commit;
|
||
|
|
||
|
# reset caches, have to do both of these, one for the usertags one for logtags
|
||
|
LJ::Tags::reset_cache($u);
|
||
|
LJ::Tags::reset_cache($u => \@jitemids);
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::set_usertag_display
|
||
|
# class: tags
|
||
|
# des: Set the display bool for a tag.
|
||
|
# args: uobj, vartype, var, val
|
||
|
# des-uobj: User id or object of account to edit tag on
|
||
|
# des-vartype: Either 'id' or 'name'; indicating what the next parameter is
|
||
|
# des-var: If vartype is 'id', this is the tag (keyword) id; else, it's the tag/keyword itself
|
||
|
# des-val: 1/0; whether to turn the display flag on or off
|
||
|
# returns: 1 on success, undef on error
|
||
|
# </LJFUNC>
|
||
|
sub set_usertag_display {
|
||
|
return undef if $LJ::DISABLED{tags};
|
||
|
|
||
|
my $u = LJ::want_user(shift);
|
||
|
my ($type, $var, $val) = @_;
|
||
|
return undef unless $u;
|
||
|
|
||
|
my $kwid;
|
||
|
if ($type eq 'id') {
|
||
|
$kwid = $var + 0;
|
||
|
} elsif ($type eq 'name') {
|
||
|
$var = LJ::Tags::validate_tag($var);
|
||
|
return undef unless $var;
|
||
|
|
||
|
# do not auto-vivify but get the keyword id
|
||
|
$kwid = LJ::get_keyword_id($u, $var, 0);
|
||
|
}
|
||
|
return undef unless $kwid;
|
||
|
|
||
|
$u->do("UPDATE usertags SET display = ? WHERE journalid = ? AND kwid = ?",
|
||
|
undef, $val ? 1 : 0, $u->{userid}, $kwid);
|
||
|
return undef if $u->err;
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
# <LJFUNC>
|
||
|
# name: LJ::Tags::deleted_friend_group
|
||
|
# class: tags
|
||
|
# des: Called internally when a friends group is deleted.
|
||
|
# args: uobj, bit
|
||
|
# des-uobj: User id or object of account deleting the group.
|
||
|
# des-bit: The id (1..30) of the friends group being deleted.
|
||
|
# returns: 1 of success undef on failure.
|
||
|
# </LJFUNC>
|
||
|
sub deleted_friend_group {
|
||
|
my $u = LJ::want_user(shift);
|
||
|
my $bit = shift() + 0;
|
||
|
return undef unless $u && $bit >= 1 && $bit <= 30;
|
||
|
|
||
|
# delete from logkwsum and then nuke the user's tags
|
||
|
$u->do("DELETE FROM logkwsum WHERE journalid = ? AND security = ?",
|
||
|
undef, $u->{userid}, 1 << $bit);
|
||
|
return undef if $u->err;
|
||
|
|
||
|
# that was simple
|
||
|
LJ::Tags::reset_cache($u);
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
1;
|