PageRenderTime 194ms CodeModel.GetById 2ms app.highlight 179ms RepoModel.GetById 1ms app.codeStats 0ms

/cgi-bin/LJ/Tags.pm

https://bitbucket.org/anall/dw-free-work
Perl | 1616 lines | 1478 code | 52 blank | 86 comment | 61 complexity | 9b109e9a960999dc86352afbf34dd732 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1# This code was forked from the LiveJournal project owned and operated
   2# by Live Journal, Inc. The code has been modified and expanded by
   3# Dreamwidth Studios, LLC. These files were originally licensed under
   4# the terms of the license supplied by Live Journal, Inc, which can
   5# currently be found at:
   6#
   7# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
   8#
   9# In accordance with the original license, this code and all its
  10# modifications are provided under the GNU General Public License.
  11# A copy of that license can be found in the LICENSE file included as
  12# part of this distribution.
  13
  14package LJ::Tags;
  15use strict;
  16
  17use LJ::Global::Constants;
  18use LJ::Lang;
  19
  20# <LJFUNC>
  21# name: LJ::Tags::get_usertagsmulti
  22# class: tags
  23# des: Gets a bunch of tags for the specified list of users.
  24# args: opts?, uobj*
  25# des-opts: Optional hashref with options. Keys can be 'no_gearman' to skip gearman
  26#           task dispatching.
  27# des-uobj: One or more user ids or objects to load the tags for.
  28# returns: Hashref; { userid => *tagref*, userid => *tagref*, ... } where *tagref* is the
  29#          return value of LJ::Tags::get_usertags -- undef on failure
  30# </LJFUNC>
  31sub get_usertagsmulti {
  32    return {} unless LJ::is_enabled('tags');
  33
  34    # options if provided
  35    my $opts = {};
  36    $opts = shift if ref $_[0] eq 'HASH';
  37
  38    # get input users
  39    my @uobjs = grep { defined } map { LJ::want_user($_) } @_;
  40    return {} unless @uobjs;
  41
  42    # now setup variables we'll need
  43    my @memkeys;  # memcache keys to fetch
  44    my $res = {}; # { jid => { tagid => {}, ... }, ... }; results return hashref
  45    my %need;     # ( jid => 0/1 ); whether we need tags for this user
  46
  47    # prepopulate our structures
  48    foreach my $u (@uobjs) {
  49        # don't load if we've previously gotten this one
  50        if (my $cached = $LJ::REQ_CACHE_USERTAGS{$u->{userid}}) {
  51            $res->{$u->{userid}} = $cached;
  52            next;
  53        }
  54
  55        # setup that we need this one
  56        $need{$u->{userid}} = $u;
  57        push @memkeys, [ $u->{userid}, "tags:$u->{userid}" ];
  58    }
  59    return $res unless @memkeys;
  60
  61    # gather data from memcache if available
  62    my $memc = LJ::MemCache::get_multi(@memkeys) || {};
  63    foreach my $key (keys %$memc) {
  64        if ($key =~ /^tags:(\d+)$/) {
  65            my $jid = $1;
  66
  67            # set this up in our return hash and mark unneeded
  68            $LJ::REQ_CACHE_USERTAGS{$jid} = $memc->{$key};
  69            $res->{$jid} = $memc->{$key};
  70            delete $need{$jid};
  71        }
  72    }
  73    return $res unless %need;
  74
  75    # if we're not using gearman, or we're not in web context (implies that we're
  76    # in gearman context?) then we need to use the loader to get the data
  77    my $gc = LJ::gearman_client();
  78    return LJ::Tags::_get_usertagsmulti($res, values %need)
  79        unless LJ::conf_test($LJ::LOADTAGS_USING_GEARMAN, values %need) && $gc && ! $opts->{no_gearman};
  80
  81    # spawn gearman jobs to get each of the users
  82    my $ts = $gc->new_task_set();
  83    foreach my $u (values %need) {
  84        $ts->add_task(Gearman::Task->new("load_usertags", \"$u->{userid}",
  85            {
  86                uniq => '-',
  87                on_complete => sub {
  88                    my $resp = shift;
  89                    my $tags = Storable::thaw($$resp);
  90                    return unless $tags;
  91
  92                    $LJ::REQ_CACHE_USERTAGS{$u->{userid}} = $tags;
  93                    $res->{$u->{userid}} = $tags;
  94                    delete $need{$u->{userid}};
  95                },
  96            }));
  97    }
  98
  99    # now wait for gearman to finish, then we're done
 100    $ts->wait(timeout => 15);
 101    return $res;
 102}
 103
 104# internal sub used by get_usertagsmulti
 105sub _get_usertagsmulti {
 106    my ($res, @uobjs) = @_;
 107    return $res unless @uobjs;
 108
 109    # now setup variables we'll need
 110    my @memkeys;  # memcache keys to fetch
 111    my %jid2cid;  # ( jid => cid ); cross reference journals to clusters
 112    my %need;     # ( cid => { jid => 0/1 } ); whether we need tags for this user
 113    my %need_kws; # ( cid => { jid => 0/1 } ); whether we need keywords for this user
 114    my %kws;      # ( jid => { kwid => keyword, ... } ); keywords for a user
 115    my %dbcrs;    # ( cid => dbcr ); stores database handles
 116
 117    # prepopulate our structures
 118    foreach my $u (@uobjs) {
 119        # we will have to load these
 120        $jid2cid{$u->{userid}} = $u->{clusterid};
 121        $need{$u->{clusterid}}->{$u->{userid}} = 1;
 122        $need_kws{$u->{clusterid}}->{$u->{userid}} = 1;
 123        push @memkeys, [ $u->{userid}, "kws:$u->{userid}" ];
 124    }
 125
 126    # gather data from memcache if available
 127    my $memc = LJ::MemCache::get_multi(@memkeys) || {};
 128    foreach my $key (keys %$memc) {
 129        if ($key =~ /^kws:(\d+)$/) {
 130            my $jid = $1;
 131            my $cid = $jid2cid{$jid};
 132
 133            # save for later and mark unneeded
 134            $kws{$jid} = $memc->{$key};
 135            delete $need_kws{$cid}->{$jid};
 136            delete $need_kws{$cid} unless %{$need_kws{$cid}};
 137        }
 138    }
 139
 140    # get keywords first
 141    foreach my $cid (keys %need_kws) {
 142        next unless %{$need_kws{$cid}};
 143
 144        # get db for this cluster
 145        my $dbcr = ($dbcrs{$cid} ||= LJ::get_cluster_def_reader($cid))
 146            or next;
 147
 148        # get the keywords from the database
 149        my $in = join(',', map { $_ + 0 } keys %{$need_kws{$cid}});
 150        my $kwrows = $dbcr->selectall_arrayref("SELECT userid, kwid, keyword FROM userkeywords WHERE userid IN ($in)");
 151        next if $dbcr->err || ! $kwrows;
 152
 153        # break down into data structures
 154        my %keywords; # ( jid => { kwid => keyword } )
 155        $keywords{$_->[0]}->{$_->[1]} = $_->[2]
 156            foreach @$kwrows;
 157        next unless %keywords;
 158
 159        # save and store to memcache
 160        foreach my $jid (keys %keywords) {
 161            $kws{$jid} = $keywords{$jid};
 162            LJ::MemCache::add([ $jid, "kws:$jid" ], $keywords{$jid});
 163        }
 164    }
 165
 166    # now, what we need per cluster...
 167    foreach my $cid (keys %need) {
 168        next unless %{$need{$cid}};
 169
 170        # get db for this cluster
 171        my $dbcr = ($dbcrs{$cid} ||= LJ::get_cluster_def_reader($cid))
 172            or next;
 173
 174        my @all_jids = map { $_ + 0 } keys %{$need{$cid}};
 175
 176        # get the tags from the database
 177        my $in = join(',', @all_jids);
 178        my $tagrows = $dbcr->selectall_arrayref("SELECT journalid, kwid, parentkwid, display FROM usertags WHERE journalid IN ($in)");
 179        next if $dbcr->err;
 180
 181        # break down into data structures
 182        my %tags; # ( jid => { kwid => display } )
 183        $tags{$_->[0]}->{$_->[1]} = $_->[3]
 184            foreach @$tagrows;
 185
 186        # now turn this into a tentative results hash: { userid => { tagid => { name => tagname, ... }, ... } }
 187        # this is done by combining the information we got from the tags lookup along with
 188        # the stuff from the keyword lookup.  we need the relevant rows from both sources
 189        # before they appear in this hash.
 190        foreach my $jid (keys %tags) {
 191            next unless $kws{$jid};
 192            foreach my $kwid (keys %{$tags{$jid}}) {
 193                $res->{$jid}->{$kwid} =
 194                    {
 195                        name => $kws{$jid}->{$kwid},
 196                        security => {
 197                            public => 0,
 198                            groups => {},
 199                            private => 0,
 200                            protected => 0
 201                        },
 202                        uses => 0,
 203                        display => $tags{$jid}->{$kwid},
 204                    };
 205            }
 206        }
 207
 208        # get security counts
 209        my @resjids = keys %$res;
 210        my $ids = join(',', map { $_+0 } @resjids);
 211
 212        my $counts = [];
 213
 214        # populate security counts
 215        if ( @resjids ) {
 216            $counts = $dbcr->selectall_arrayref("SELECT journalid, kwid, security, entryct FROM logkwsum WHERE journalid IN ($ids)");
 217            next if $dbcr->err;
 218        }
 219
 220        # setup some helper values
 221        my $public_mask = 1 << 63;
 222        my $trust_mask = 1 << 0;
 223
 224        # melt this information down into the hashref
 225        foreach my $row (@$counts) {
 226            my ($jid, $kwid, $sec, $ct) = @$row;
 227
 228            # make sure this journal and keyword are present in the results already
 229            # so we don't auto-vivify something with security that has no keyword with it
 230            next unless $res->{$jid} && $res->{$jid}->{$kwid};
 231
 232            # add these to the total uses
 233            $res->{$jid}->{$kwid}->{uses} += $ct;
 234
 235            if ($sec & $public_mask) {
 236                $res->{$jid}->{$kwid}->{security}->{public} += $ct;
 237                $res->{$jid}->{$kwid}->{security_level} = 'public';
 238            } elsif ($sec & $trust_mask) {
 239                $res->{$jid}->{$kwid}->{security}->{protected} += $ct;
 240                $res->{$jid}->{$kwid}->{security_level} = 'protected'
 241                    unless $res->{$jid}->{$kwid}->{security_level} &&
 242                           $res->{$jid}->{$kwid}->{security_level} eq 'public';
 243            } elsif ($sec) {
 244                # if $sec is true (>0), and not trust/public, then it's a group(s).  but it's
 245                # still in the form of a number, and we want to know which group(s) it is.  so
 246                # we must convert the mask back to a bit number with LJ::bit_breakdown.
 247                foreach my $grpid ( LJ::bit_breakdown($sec) ) {
 248                    $res->{$jid}->{$kwid}->{security}->{groups}->{$grpid} += $ct;
 249                }
 250                $res->{$jid}->{$kwid}->{security_level} ||= 'group';
 251            } else {
 252                # $sec must be 0
 253                $res->{$jid}->{$kwid}->{security}->{private} += $ct;
 254            }
 255        }
 256
 257        # default securities to private and store to memcache
 258        foreach my $jid (@all_jids) {
 259            $res->{$jid} ||= {};
 260            $res->{$jid}->{$_}->{security_level} ||= 'private'
 261                foreach keys %{$res->{$jid}};
 262
 263            $LJ::REQ_CACHE_USERTAGS{$jid} = $res->{$jid};
 264            LJ::MemCache::add([ $jid, "tags:$jid" ], $res->{$jid});
 265        }
 266    }
 267
 268    return $res;
 269}
 270
 271# <LJFUNC>
 272# name: LJ::Tags::get_usertags
 273# class: tags
 274# des: Returns the tags that a user has defined for their account.
 275# args: uobj, opts?
 276# des-uobj: User object to get tags for.
 277# des-opts: Optional hashref; key can be 'remote' to filter tags to only ones that remote can see
 278# returns: Hashref; key being tag id, value being a large hashref (FIXME: document)
 279# </LJFUNC>
 280sub get_usertags {
 281    return {} unless LJ::is_enabled('tags');
 282
 283    my $u = LJ::want_user(shift)
 284        or return undef;
 285    my $opts = shift() || {};
 286
 287    # get tags for this user
 288    my $tags = LJ::Tags::get_usertagsmulti($u);
 289    return undef unless $tags;
 290
 291    # get the tags for this user
 292    my $res = $tags->{$u->{userid}} || {};
 293    return {} unless %$res;
 294
 295    # now if they provided a remote, remove the ones they don't want to see; note that
 296    # remote may be undef so we have to check exists
 297    if ( exists $opts->{remote} ) {
 298        # never going to cull anything if you control it, so just return
 299        return $res if LJ::isu( $opts->{remote} ) && $opts->{remote}->can_manage( $u );
 300
 301        # setup helper variables from u to remote
 302        my ($trusted, $grpmask) = (0, 0);
 303        if ($opts->{remote}) {
 304            $trusted = $u->trusts_or_has_member( $opts->{remote} );
 305            $grpmask = $u->trustmask( $opts->{remote} );
 306        }
 307
 308        # figure out what we need to purge
 309        my @purge;
 310TAG:    foreach my $tagid (keys %$res) {
 311            my $sec = $res->{$tagid}->{security_level};
 312            next TAG if $sec eq 'public';
 313            next TAG if $trusted && $sec eq 'protected';
 314            if ($grpmask && $sec eq 'group') {
 315                foreach my $grpid (keys %{$res->{$tagid}->{security}->{groups}}) {
 316                    next TAG if $grpmask & (1 << $grpid);
 317                }
 318            }
 319            push @purge, $tagid;
 320        }
 321        delete $res->{$_} foreach @purge;
 322    }
 323
 324    return $res;
 325}
 326
 327# <LJFUNC>
 328# name: LJ::Tags::get_entry_tags
 329# class: tags
 330# des: Gets tags that have been used on an entry.
 331# args: uuserid, jitemid
 332# des-uuserid: User id or object of account with entry
 333# des-jitemid: Journal itemid of entry; may also be arrayref of jitemids in journal.
 334# returns: Hashref; { jitemid => { tagid => tagname, tagid => tagname, ... }, ... }
 335# </LJFUNC>
 336sub get_logtags {
 337    return {} unless LJ::is_enabled('tags');
 338
 339    my $u = LJ::want_user(shift);
 340    return undef unless $u;
 341
 342    # handle magic jitemid parameter
 343    my $jitemid = shift;
 344    unless (ref $jitemid eq 'ARRAY') {
 345        $jitemid = [ $jitemid+0 ];
 346        return undef unless $jitemid->[0];
 347    }
 348    return undef unless @$jitemid;
 349
 350    # transform to a call to get_logtagsmulti
 351    my $ret = LJ::Tags::get_logtagsmulti({ $u->{clusterid} => [ map { [ $u->{userid}, $_ ] } @$jitemid ] });
 352    return undef unless $ret && ref $ret eq 'HASH';
 353
 354    # now construct result hashref
 355    return { map { $_ => $ret->{"$u->{userid} $_"} } @$jitemid };
 356}
 357
 358# <LJFUNC>
 359# name: LJ::Tags::get_logtagsmulti
 360# class: tags
 361# des: Load tags on a given set of entries
 362# args: idsbycluster
 363# des-idsbycluster: { clusterid => [ [ jid, jitemid ], [ jid, jitemid ], ... ] }
 364# returns: hashref with "jid jitemid" keys, value of each being a hashref of
 365#          { tagid => tagname, ... }
 366# </LJFUNC>
 367sub get_logtagsmulti {
 368    return {} unless LJ::is_enabled('tags');
 369
 370    # get parameter (only one!)
 371    my $idsbycluster = shift;
 372    return undef unless $idsbycluster && ref $idsbycluster eq 'HASH';
 373
 374    # the mass of variables to make this mess work!
 375    my @jids;     # journalids we've seen
 376    my @memkeys;  # memcache keys to load
 377    my %ret;      # ( jid => { jitemid => [ tagid, tagid, ... ], ... } ); storage for data pre-final conversion
 378    my %set;      # ( jid => ( jitemid => [ tagid, tagid, ... ] ) ); for setting in memcache
 379    my $res = {}; # { "jid jitemid" => { tagid => kw, tagid => kw, ... } }; final results hashref for return
 380    my %need;     # ( cid => { jid => { jitemid => 1, jitemid => 1 } } ); what still needs loading
 381    my %jid2cid;  # ( jid => cid ); map of journal id to clusterid
 382
 383    # construct memcache keys for loading below
 384    foreach my $cid (keys %$idsbycluster) {
 385        foreach my $row (@{$idsbycluster->{$cid} || []}) {
 386            $need{$cid}->{$row->[0]}->{$row->[1]} = 1;
 387            $jid2cid{$row->[0]} = $cid;
 388            $set{$row->[0]}->{$row->[1]} = []; # empty initially
 389            push @memkeys, [ $row->[0], "logtag:$row->[0]:$row->[1]" ];
 390        }
 391    }
 392
 393    # now hit up memcache to try to find what we can
 394    my $memc = LJ::MemCache::get_multi(@memkeys) || {};
 395    foreach my $key (keys %$memc) {
 396        if ($key =~ /^logtag:(\d+):(\d+)$/) {
 397            my ($jid, $jitemid) = ($1, $2);
 398            my $cid = $jid2cid{$jid};
 399
 400            # save memcache output hashref to out %ret var
 401            $ret{$jid}->{$jitemid} = $memc->{$key};
 402
 403            # remove the need to prevent loading from the database and storage to memcache
 404            delete $need{$cid}->{$jid}->{$jitemid};
 405            delete $need{$cid}->{$jid} unless %{$need{$cid}->{$jid}};
 406            delete $need{$cid} unless %{$need{$cid}};
 407        }
 408    }
 409
 410    # iterate over clusters and construct SQL to get the data...
 411    foreach my $cid (keys %need) {
 412        my $dbcm = LJ::get_cluster_master($cid)
 413            or return undef;
 414
 415        # list of (jid, jitemid) pairs that we get from %need
 416        my @where;
 417        foreach my $jid (keys %{$need{$cid} || {}}) {
 418            my @jitemids = keys %{$need{$cid}->{$jid} || {}};
 419            next unless @jitemids;
 420
 421            push @where, "(journalid = $jid AND jitemid IN (" . join(",", @jitemids) . "))";
 422        }
 423
 424        # prepare the query to run
 425        my $where = join(' OR ', @where);
 426        my $rows = $dbcm->selectall_arrayref("SELECT journalid, jitemid, kwid FROM logtags WHERE $where");
 427        return undef if $dbcm->err || ! $rows;
 428
 429        # get data into %set so we add it to memcache later
 430        push @{$set{$_->[0]}->{$_->[1]} ||= []}, $_->[2] foreach @$rows;
 431    }
 432
 433    # now add the things to memcache that we loaded from the clusters and also
 434    # transport them into the $ret hashref or returning to the user
 435    foreach my $jid (keys %set) {
 436        foreach my $jitemid (keys %{$set{$jid}}) {
 437            next unless $need{$jid2cid{$jid}}->{$jid}->{$jitemid};
 438            LJ::MemCache::add([ $jid, "logtag:$jid:$jitemid" ], $set{$jid}->{$jitemid});
 439            $ret{$jid}->{$jitemid} = $set{$jid}->{$jitemid};
 440        }
 441    }
 442
 443    # quickly load all tags for the users we've found
 444    @jids = keys %ret;
 445    my $utags = LJ::Tags::get_usertagsmulti(@jids);
 446    return undef unless $utags;
 447
 448    # last step: convert keywordids to keywords
 449    foreach my $jid (@jids) {
 450        my $tags = $utags->{$jid};
 451        next unless $tags;
 452
 453        # transpose data from %ret into $res hashref which has (kwid => keyword) pairs
 454        foreach my $jitemid (keys %{$ret{$jid}}) {
 455            $res->{"$jid $jitemid"}->{$_} = $tags->{$_}->{name}
 456                foreach @{$ret{$jid}->{$jitemid} || []};
 457        }
 458    }
 459
 460    # finally return the result hashref
 461    return $res;
 462}
 463
 464# <LJFUNC>
 465# name: LJ::Tags::can_add_tags
 466# class: tags
 467# des: Determines if one account is allowed to add tags to another's entry.
 468# args: u, remote
 469# des-u: User id or object of account tags are being added to
 470# des-remote: User id or object of account performing the action
 471# returns: 1 if allowed, 0 if not, undef on error
 472# </LJFUNC>
 473sub can_add_tags {
 474    return undef unless LJ::is_enabled('tags');
 475
 476    my $u = LJ::want_user(shift);
 477    my $remote = LJ::want_user(shift);
 478    return undef unless $u && $remote;
 479
 480    # we don't allow identity users to add tags, even when tag permissions would otherwise allow any user on the site
 481    # exception are communities that explicitly allow identity users to post in them
 482    # FIXME: perhaps we should restrict on all users, but allow for more restrictive settings such as members?
 483    return undef unless $remote->is_personal || $remote->is_identity && $u->prop( 'identity_posting' );
 484    return undef if $u->has_banned( $remote );
 485
 486    # get permission hashref and check it; note that we fall back to the control
 487    # permission, which will allow people to add even if they can't add by default
 488    my $perms = LJ::Tags::get_permission_levels($u);
 489    return LJ::Tags::_remote_satisfies_permission($u, $remote, $perms->{add}) ||
 490           LJ::Tags::_remote_satisfies_permission($u, $remote, $perms->{control});
 491}
 492
 493
 494sub can_add_entry_tags {
 495    return undef unless LJ::is_enabled( "tags" );
 496
 497    my ( $remote, $entry ) = @_;
 498    $remote = LJ::want_user( $remote );
 499
 500    return undef unless $remote && $entry;
 501
 502    my $journal = $entry->journal;
 503    return undef unless $remote->is_personal || $remote->is_identity && $journal->prop( 'identity_posting' );
 504    return undef if $journal->has_banned( $remote );
 505
 506    my $perms = LJ::Tags::get_permission_levels( $journal );
 507
 508    # specific case: are we the author of this entry, or otherwise an admin of the journal?
 509    if ( $perms->{add} eq 'author_admin' ) {
 510        # is author
 511        return 1 if $remote->equals( $entry->poster );
 512
 513        # is journal administrator
 514        return $remote->can_manage( $journal );
 515    }
 516
 517    # general case, see if the remote can add tags to the journal, in general
 518    return 1 if $remote->can_add_tags_to( $journal );
 519
 520    # not allowed
 521    return undef;
 522}
 523
 524# <LJFUNC>
 525# name: LJ::Tags::can_control_tags
 526# class: tags
 527# des: Determines if one account is allowed to control (add, edit, delete) the tags of another.
 528# args: u, remote
 529# des-u: User id or object of account tags are being edited on.
 530# des-remote: User id or object of account performing the action.
 531# returns: 1 if allowed, 0 if not, undef on error
 532# </LJFUNC>
 533sub can_control_tags {
 534    return undef unless LJ::is_enabled('tags');
 535
 536    my $u = LJ::want_user(shift);
 537    my $remote = LJ::want_user(shift);
 538    return undef unless $u && $remote;
 539    return undef unless $remote->is_personal || $remote->is_identity && $u->prop( 'identity_posting' );
 540    return undef if $u->has_banned( $remote );
 541
 542    # get permission hashref and check it
 543    my $perms = LJ::Tags::get_permission_levels($u);
 544    return LJ::Tags::_remote_satisfies_permission($u, $remote, $perms->{control});
 545}
 546
 547# helper sub internal used by can_*_tags functions
 548sub _remote_satisfies_permission {
 549    my ($u, $remote, $perm) = @_;
 550    return undef unless $u && $remote && $perm;
 551
 552    # allow if they can manage it (own, or 'A' edge)
 553    return 1 if $remote->can_manage( $u );
 554
 555    # permission checks
 556    if ($perm eq 'public') {
 557        return 1;
 558    } elsif ($perm eq 'none') {
 559        return 0;
 560    } elsif ( $perm eq 'protected' || $perm eq 'friends' ) { # 'friends' for backwards compatibility
 561        return $u->trusts_or_has_member( $remote );
 562    } elsif ($perm eq 'private') {
 563        return 0;  # $remote->can_manage( $u ) already returned 1 above
 564    } elsif ( $perm eq 'author_admin' ) {
 565        # this tests whether the remote can add tags for this journal in general
 566        # when we don't have an entry object available to us (e.g., posting)
 567        # Existing entries, checking per-entry author permissions, should use
 568        # LJ::Tag::can_add_entry_tags
 569        return $remote->can_manage( $u ) || $remote->member_of( $u );
 570    } elsif ($perm =~ /^group:(\d+)$/) {
 571        my $grpid = $1+0;
 572        return undef unless $grpid >= 1 && $grpid <= 60;
 573
 574        my $mask = $u->trustmask( $remote );
 575        return ($mask & (1 << $grpid)) ? 1 : 0;
 576    } else {
 577        # else, problem!
 578        return undef;
 579    }
 580}
 581
 582# <LJFUNC>
 583# name: LJ::Tags::get_permission_levels
 584# class: tags
 585# des: Gets the permission levels on an account.
 586# args: uobj
 587# des-uobj: User id or object of account to get permissions for.
 588# returns: Hashref; keys one of 'add', 'control'; values being 'private' (only the account
 589#          in question), 'protected' (all trusted), 'public' (everybody), 'group:N' (one
 590#          trust group with given id), or 'none' (nobody can).
 591# </LJFUNC>
 592sub get_permission_levels {
 593    return { add => 'none', control => 'none' }
 594        unless LJ::is_enabled('tags');
 595
 596    my $u = LJ::want_user(shift);
 597    return undef unless $u;
 598
 599    # return defaults for accounts
 600    unless ( $u->prop( 'opt_tagpermissions' ) ) {
 601        if ( $u->is_community ) {
 602            # communities are members (trusted) add, private (maintainers) control
 603            return { add => 'protected', control => 'private' };
 604        } elsif ( $u->is_person ) {
 605            # people let trusted add, self control
 606            return { add => 'private', control => 'private' };
 607        } else {
 608            # other account types can't add tags
 609            return { add => 'none', control => 'none' };
 610        }
 611    }
 612
 613    # now split and return
 614    my ($add, $control) = split(/\s*,\s*/, $u->{opt_tagpermissions});
 615    return { add => $add, control => $control };
 616}
 617
 618# <LJFUNC>
 619# name: LJ::Tags::is_valid_tagstring
 620# class: tags
 621# des: Determines if a string contains a valid list of tags.
 622# args: tagstring, listref?, opts?
 623# des-tagstring: Opaque tag string provided by the user.
 624# des-listref: If specified, return valid list of canonical tags in arrayref here.
 625# des-opts: currently only 'omit_underscore_check' is recognized
 626# returns: 1 if list is valid, 0 if not.
 627# </LJFUNC>
 628sub is_valid_tagstring {
 629    my ($tagstring, $listref, $opts) = @_;
 630    return 0 unless $tagstring;
 631    $listref ||= [];
 632    $opts    ||= {};
 633
 634    # setup helper subs
 635    my $valid_tag = sub {
 636        my $tag = shift;
 637
 638        # a tag that starts with an underscore is reserved for future use,
 639        # but we added this after some underscores already existed.
 640        # Allow underscore tags to be viewed/deleted, but not created/modified.
 641        return 0 if ! $opts->{'omit_underscore_check'} && $tag =~ /^_/;
 642
 643        return 0 if $tag =~ /[\<\>\r\n\t]/;     # no HTML, newlines, tabs, etc
 644        return 0 unless $tag =~ /^(?:.+\s?)+$/; # one or more "words"
 645        return 1;
 646    };
 647    my $canonical_tag = sub {
 648        my $tag = shift;
 649        $tag =~ s/\s+/ /g; # condense multiple spaces to a single space
 650        $tag = LJ::text_trim($tag, LJ::BMAX_KEYWORD, LJ::CMAX_KEYWORD);
 651        $tag = LJ::utf8_lc( $tag );
 652        return $tag;
 653    };
 654
 655    # now iterate
 656    my @list = grep { length $_ }            # only keep things that are something
 657               map { LJ::trim($_) }          # remove leading/trailing spaces
 658               split(/\s*,\s*/, $tagstring); # split on comma with optional spaces
 659    return 0 unless @list;
 660
 661    # now validate each one as we go
 662    foreach my $tag (@list) {
 663        # canonicalize and determine validity
 664        $tag = $canonical_tag->($tag);
 665        return 0 unless $valid_tag->($tag);
 666
 667        # now push on our list
 668        push @$listref, $tag;
 669    }
 670
 671    # well, it must have been okay if we got here
 672    return 1;
 673}
 674
 675# <LJFUNC>
 676# name: LJ::Tags::get_security_level
 677# class: tags
 678# des: Returns the security level that applies to the given security information.
 679# args: security, allowmask
 680# des-security: 'private', 'public', or 'usemask'
 681# des-allowmask: a bitmask in standard allowmask form
 682# returns: Bitwise security level to use for [dbtable[logkwsum]] table.
 683# </LJFUNC>
 684sub get_security_level {
 685    my ($sec, $mask) = @_;
 686
 687    return 0 if $sec eq 'private';
 688    return 1 << 63 if $sec eq 'public';
 689    return $mask;
 690}
 691
 692# <LJFUNC>
 693# name: LJ::Tags::update_logtags
 694# class: tags
 695# des: Updates the tags on an entry.  Tags not in the list you provide are deleted.
 696# args: uobj, jitemid, tags, opts
 697# des-uobj: User id or object of account with entry
 698# des-jitemid: Journal itemid of entry to tag
 699# des-tags: List of tags you want applied to entry.
 700# des-opts: Hashref; keys being the action and values of the key being an arrayref of
 701#           tags to involve in the action.  Possible actions are 'add', 'set', and
 702#           'delete'.  With those, the value is a hashref of the tags (textual tags)
 703#           to add, set, or delete.  Other actions are 'add_ids', 'set_ids', and
 704#           'delete_ids'.  The value arrayref should then contain the tag ids to
 705#           act with.  Can also specify 'add_string', 'set_string', or 'delete_string'
 706#           as a comma separated list of user-supplied tags which are then canonicalized
 707#           and used.  'remote' is the remote user taking the actions (required).
 708#           'err_ref' is ref to scalar to return error messages in.  optional, and may
 709#           not be set by all error conditions.  'ignore_max' if specified will ignore
 710#           a user's max tags limit.
 711# returns: 1 on success, undef on error
 712# </LJFUNC>
 713sub update_logtags {
 714    return undef unless LJ::is_enabled('tags');
 715
 716    my $u = LJ::want_user(shift);
 717    my $jitemid = shift() + 0;
 718    return undef unless $u && $jitemid;
 719    return undef unless $u->writer;
 720
 721    # ensure we have an options hashref
 722    my $opts = shift;
 723    return undef unless $opts && ref $opts eq 'HASH';
 724
 725    # setup error stuff
 726    my $err = sub {
 727        my $fake = "";
 728        my $err_ref = $opts->{err_ref} && ref $opts->{err_ref} eq 'SCALAR' ? $opts->{err_ref} : \$fake;
 729        $$err_ref = shift() || "Unspecified error";
 730        return undef;
 731    };
 732
 733    # perform set logic?
 734    my $do_set = exists $opts->{set} || exists $opts->{set_ids} || exists $opts->{set_string};
 735
 736    # now get extra options
 737    my $remote = LJ::want_user(delete $opts->{remote});
 738    return undef unless $remote || $opts->{force};
 739
 740    # get trust levels
 741    my $entry = LJ::Entry->new( $u, jitemid => $jitemid );
 742    my $can_control = LJ::Tags::can_control_tags($u, $remote);
 743    my $can_add = $can_control || LJ::Tags::can_add_entry_tags( $remote, $entry );
 744
 745    # bail out early if we can't do any actions
 746    return $err->( LJ::Lang::ml( 'taglib.error.access' ) )
 747        unless $can_add || $opts->{force};
 748
 749    # load the user's tags
 750    my $utags = LJ::Tags::get_usertags($u);
 751    return undef unless $utags;
 752
 753    # for errors that we want to skip over silently instead of failing, but still report at the end
 754    my @skippable_errors;
 755    my @unauthorized_add;
 756
 757    # take arrayrefs of tag strings and stringify them for validation
 758    my @to_create;
 759    foreach my $verb (qw(add set delete)) {
 760        # if given tags, combine into a string
 761        if ($opts->{$verb}) {
 762            $opts->{"${verb}_string"} = join(', ', @{$opts->{$verb}});
 763            $opts->{$verb} = [];
 764        }
 765
 766        # now validate the string, if we have one
 767        if ($opts->{"${verb}_string"}) {
 768            $opts->{$verb} = [];
 769            return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $opts->{"${verb}_string"} ) } ) )
 770                unless LJ::Tags::is_valid_tagstring($opts->{"${verb}_string"}, $opts->{$verb});
 771        }
 772
 773        # and turn everything into ids
 774        $opts->{"${verb}_ids"} ||= [];
 775        foreach my $kw (@{$opts->{$verb} || []}) {
 776            my $kwid = $u->get_keyword_id( $kw, $can_control );
 777            if ($can_control) {
 778                # error if we failed to create
 779                return undef unless $kwid;
 780            } else {
 781                # if we're not creating, who cares, just skip; also skip if the keyword
 782                # is not really a tag (don't promote it)
 783                unless ( $kwid && $utags->{$kwid} ) {
 784                    push @unauthorized_add, $kw;
 785                    next;
 786                }
 787            }
 788
 789            # add the ids to the list, and save to create later if needed
 790            push @to_create, $kw unless $utags->{$kwid};
 791            push @{$opts->{"${verb}_ids"}}, $kwid;
 792        }
 793    }
 794
 795    # setup %add/%delete hashes, for easier duplicate removal
 796    my %add = ( map { $_ => 1 } @{$opts->{add_ids} || []} );
 797    my %delete = ( map { $_ => 1 } @{$opts->{delete_ids} || []} );
 798
 799    # used to keep counts in sync
 800    my $tags = LJ::Tags::get_logtags($u, $jitemid);
 801    return undef unless $tags;
 802
 803    # now get tags for this entry; which there might be none, so make it a hashref
 804    $tags = $tags->{$jitemid} || {};
 805
 806    # set is broken down into add/delete as necessary
 807    if ($do_set || ($opts->{set_ids} && @{$opts->{set_ids}})) {
 808        # mark everything to delete, we'll fix it shortly
 809        $delete{$_} = 1 foreach keys %{$tags};
 810
 811        # and now go through the set we want, things that are in the delete
 812        # pile are just nudge so we don't touch them, and everything else we
 813        # throw in the add pile
 814        foreach my $id (@{$opts->{set_ids}}) {
 815            $add{$id} = 1
 816                unless delete $delete{$id};
 817        }
 818    }
 819
 820    # now don't readd things we already have
 821    delete $add{$_} foreach keys %{$tags};
 822
 823    # populate the errref, but don't actually return.
 824    push @skippable_errors, LJ::Lang::ml( "taglib.error.add", { tags => join( ", ", @unauthorized_add ) } ) if @unauthorized_add;
 825    push @skippable_errors, LJ::Lang::ml( "taglib.error.delete", { tags => join( ", ", map { $utags->{$_}->{name} } keys %delete ) } ) if %delete && ! $can_control ;
 826    $err->( join " ", @skippable_errors ) if @skippable_errors;
 827
 828    # but delete nothing if we're not a controller
 829    %delete = () unless $can_control || $opts->{force};
 830
 831    # bail out if nothing needs to be done
 832    return 1 unless %add || %delete;
 833
 834    # at this point we have enough information to determine if they're going to break their
 835    # max, so let's do that so we can bail early enough to prevent a rollback operation
 836    my $max = $opts->{ignore_max} ? 0 : $u->count_tags_max;
 837    if (@to_create && $max && $max > 0) {
 838        my $total = scalar(keys %$utags) + scalar(@to_create);
 839        if ( $total > $max ) {
 840            return $err->(LJ::Lang::ml('taglib.error.toomany2', { max => $max,
 841                                                                 tags => LJ::ehtml( join( ", ", @to_create ) ),
 842                                                                 excess => $total - $max }));
 843        }
 844    }
 845
 846    # now we can create the new tags, since we know we're safe
 847    # We still need to propagate ignore_max, as create_usertag does some checks of it's own.
 848    LJ::Tags::create_usertag( $u, $_, { display => 1, ignore_max => $opts->{ignore_max} } ) foreach @to_create;
 849
 850    # %add and %delete are accurate, but we need to track necessary
 851    # security updates; this is a hash of keyword ids and a modification
 852    # value (a delta; +/-N) to be applied to that row later
 853    my %security;
 854
 855    # get the security of this post for use in %security; do this now so
 856    # we don't interrupt the transaction below
 857    my $l2row = LJ::get_log2_row($u, $jitemid);
 858    return undef unless $l2row;
 859
 860    # calculate security masks
 861    my $sec = LJ::Tags::get_security_level($l2row->{security}, $l2row->{allowmask});
 862
 863    # setup a rollback bail path so that we can undo everything we've done
 864    # if anything fails in the middle; and if the rollback fails, scream loudly
 865    # and burst into flames!
 866    my $rollback = sub {
 867        die $u->errstr unless $u->rollback;
 868        return undef;
 869    };
 870
 871    # start the big transaction, for great justice!
 872    $u->begin_work;
 873
 874    # process additions first
 875    my @bind;
 876    foreach my $kwid (keys %add) {
 877        $security{$kwid}++;
 878        push @bind, $u->{userid}, $jitemid, $kwid;
 879    }
 880
 881    my $recentlimit = $LJ::RECENT_TAG_LIMIT || 500;
 882
 883    # now add all to both tables; only do $recentlimit rows ($recentlimit * 3 bind vars) at a time
 884    while (my @list = splice(@bind, 0, 3 * $recentlimit)) {
 885        my $sql = join(',', map { "(?,?,?)" } 1..(scalar(@list)/3));
 886
 887        $u->do("REPLACE INTO logtags (journalid, jitemid, kwid) VALUES $sql", undef, @list);
 888        return $rollback->() if $u->err;
 889
 890        $u->do("REPLACE INTO logtagsrecent (journalid, jitemid, kwid) VALUES $sql", undef, @list);
 891        return $rollback->() if $u->err;
 892    }
 893
 894    # now process deletions
 895    @bind = ();
 896    foreach my $kwid (keys %delete) {
 897        $security{$kwid}--;
 898        push @bind, $kwid;
 899    }
 900
 901    # now run the SQL
 902    while (my @list = splice(@bind, 0, $recentlimit)) {
 903        my $sql = join(',', map { $_ + 0 } @list);
 904
 905        $u->do("DELETE FROM logtags WHERE journalid = ? AND jitemid = ? AND kwid IN ($sql)",
 906               undef, $u->{userid}, $jitemid);
 907        return $rollback->() if $u->err;
 908
 909        $u->do("DELETE FROM logtagsrecent WHERE journalid = ? AND kwid IN ($sql) AND jitemid = ?",
 910               undef, $u->{userid}, $jitemid);
 911        return $rollback->() if $u->err;
 912    }
 913
 914    # now handle lazy cleaning of this table for these tag ids; note that the
 915    # %security hash contains all of the keywords we've operated on in total
 916    my @kwids = keys %security;
 917    my $sql = join(',', map { $_ + 0 } @kwids);
 918    my $sth = $u->prepare("SELECT kwid, COUNT(*) FROM logtagsrecent WHERE journalid = ? AND kwid IN ($sql) GROUP BY 1");
 919    return $rollback->() if $u->err || ! $sth;
 920    $sth->execute($u->{userid});
 921    return $rollback->() if $sth->err;
 922
 923    # now iterate over counts and find ones that are too high
 924    my %delrecent; # kwid => [ jitemid, jitemid, ... ]
 925    while (my ($kwid, $ct) = $sth->fetchrow_array) {
 926        next unless $ct > $recentlimit + 20;
 927
 928        # get the times of the entries, the user time (lastn view uses user time), sort it, and then
 929        # we can chop off jitemids that fall below the threshold -- but only in this keyword and only clean
 930        # up some number at a time (25 at most, starting at our threshold)
 931        my $sth2 = $u->prepare(qq{
 932                SELECT t.jitemid
 933                FROM logtagsrecent t, log2 l
 934                WHERE t.journalid = l.journalid
 935                  AND t.jitemid = l.jitemid
 936                  AND t.journalid = ?
 937                  AND t.kwid = ?
 938                ORDER BY l.eventtime DESC
 939                LIMIT $recentlimit,25
 940            });
 941        return $rollback->() if $u->err || ! $sth2;
 942        $sth2->execute($u->{userid}, $kwid);
 943        return $rollback->() if $sth2->err;
 944
 945        # push these onto the hash for deleting below
 946        while (my $jit = $sth2->fetchrow_array) {
 947            push @{$delrecent{$kwid} ||= []}, $jit;
 948        }
 949    }
 950
 951    # now delete any recents we need to into this format:
 952    #    (kwid = 3 AND jitemid IN (2, 3, 4)) OR (kwid = ...) OR ...
 953    # but only if we have some to delete
 954    if (%delrecent) {
 955        my $del = join(' OR ', map {
 956                                    "(kwid = " . ($_+0) . " AND jitemid IN (" . join(',', map { $_+0 } @{$delrecent{$_}}) . "))"
 957                               } keys %delrecent);
 958        $u->do("DELETE FROM logtagsrecent WHERE journalid = ? AND ($del)", undef, $u->{userid});
 959        return $rollback->() if $u->err;
 960    }
 961
 962    # now we must get the current security values in order to come up with a proper update; note that
 963    # we select for update, which locks it so we have a consistent view of the rows
 964    $sth = $u->prepare("SELECT kwid, security, entryct FROM logkwsum WHERE journalid = ? AND kwid IN ($sql) FOR UPDATE");
 965    return $rollback->() if $u->err || ! $sth;
 966    $sth->execute($u->{userid});
 967    return $rollback->() if $sth->err;
 968
 969    # now iterate and get the security counts
 970    my %counts;
 971    while (my ($kwid, $secu, $ct) = $sth->fetchrow_array) {
 972        $counts{$kwid}->{$secu} = $ct;
 973    }
 974
 975    # now we want to update them, and delete any at 0
 976    my (@replace, @delete);
 977    foreach my $kwid (@kwids) {
 978        if (exists $counts{$kwid} && exists $counts{$kwid}->{$sec}) {
 979            # an old one exists
 980            my $new = $counts{$kwid}->{$sec} + $security{$kwid};
 981            if ($new > 0) {
 982                # update it
 983                push @replace, [ $kwid, $sec, $new ];
 984            } else {
 985                # delete this one
 986                push @delete, [ $kwid, $sec ];
 987            }
 988        } else {
 989            # add a new one
 990            push @replace, [ $kwid, $sec, $security{$kwid} ];
 991        }
 992    }
 993
 994    # handle deletes in one move; well, 100 at a time
 995    while (my @list = splice(@delete, 0, 100)) {
 996        my $sql = join(' OR ', map { "(kwid = ? AND security = ?)" } 1..scalar(@list));
 997        $u->do("DELETE FROM logkwsum WHERE journalid = ? AND ($sql)",
 998               undef, $u->{userid}, map { @$_ } @list);
 999        return $rollback->() if $u->err;
1000    }
1001
1002    # handle replaces and inserts
1003    while (my @list = splice(@replace, 0, 100)) {
1004        my $sql = join(',', map { "(?,?,?,?)" } 1..scalar(@list));
1005        $u->do("REPLACE INTO logkwsum (journalid, kwid, security, entryct) VALUES $sql",
1006               undef, map { $u->{userid}, @$_ } @list);
1007        return $rollback->() if $u->err;
1008    }
1009
1010    # commit everything and smack caches and we're done!
1011    die $u->errstr unless $u->commit;
1012    LJ::Tags::reset_cache($u);
1013    LJ::Tags::reset_cache($u => $jitemid);
1014    return 1;
1015
1016}
1017
1018# <LJFUNC>
1019# name: LJ::Tags::delete_logtags
1020# class: tags
1021# des: Deletes all tags on an entry.
1022# args: uobj, jitemid
1023# des-uobj: User id or object of account with entry.
1024# des-jitemid: Journal itemid of entry to delete tags from.
1025# returns: undef on error; 1 on success
1026# </LJFUNC>
1027sub delete_logtags {
1028    return undef unless LJ::is_enabled('tags');
1029
1030    my $u = LJ::want_user(shift);
1031    my $jitemid = shift() + 0;
1032    return undef unless $u && $jitemid;
1033
1034    # maybe this is ghetto, but it does all of the logic we would otherwise
1035    # have to duplicate here, so no sense in doing that.
1036    return LJ::Tags::update_logtags($u, $jitemid, { set_string => "", force => 1, });
1037}
1038
1039# <LJFUNC>
1040# name: LJ::Tags::reset_cache
1041# class: tags
1042# des: Clears out all cached information for a user's tags.
1043# args: uobj, jitemid?
1044# des-uobj: User id or object of account to clear cache for
1045# des-jitemid: Either a single jitemid or an arrayref of jitemids to clear for the user.  If
1046#              not present, the user's tags cache is cleared.  If present, the cache for those
1047#              entries only are cleared.
1048# returns: undef on error; 1 on success
1049# </LJFUNC>
1050sub reset_cache {
1051    return undef unless LJ::is_enabled('tags');
1052
1053    while (my ($u, $jitemid) = splice(@_, 0, 2)) {
1054        next unless
1055            $u = LJ::want_user($u);
1056
1057        # standard user tags cleanup
1058        unless ($jitemid) {
1059            delete $LJ::REQ_CACHE_USERTAGS{$u->{userid}};
1060            LJ::MemCache::delete([ $u->{userid}, "tags:$u->{userid}" ]);
1061        }
1062
1063        # now, cleanup entries if necessary
1064        if ($jitemid) {
1065            $jitemid = [ $jitemid ]
1066                unless ref $jitemid eq 'ARRAY';
1067            LJ::MemCache::delete([ $u->{userid}, "logtag:$u->{userid}:$_" ])
1068                foreach @$jitemid;
1069        }
1070    }
1071    return 1;
1072}
1073
1074# <LJFUNC>
1075# name: LJ::Tags::create_usertag
1076# class: tags
1077# des: Creates tags for a user, returning the keyword ids allocated.
1078# args: uobj, kw, opts?
1079# des-uobj: User object to create tag on.
1080# des-kw: Tag string (comma separated list of tags) to create.
1081# des-opts: Optional; hashref, possible keys being 'display' and value being whether or
1082#           not this tag should be a display tag and 'parenttagid' being the tagid of a
1083#           parent tag for hierarchy.  'err_ref' optional key should be a ref to a scalar
1084#           where we will store text about errors.  'ignore_max' if set will ignore the
1085#           user's max tags limit when creating this tag.
1086# returns: undef on error, else a hashref of { keyword => tagid } for each keyword defined
1087# </LJFUNC>
1088sub create_usertag {
1089    return undef unless LJ::is_enabled('tags');
1090
1091    my $u = LJ::want_user(shift);
1092    my $kw = shift;
1093    my $opts = shift || {};
1094    return undef unless $u && $kw;
1095
1096    # setup error stuff
1097    my $err = sub {
1098        my $fake = "";
1099        my $err_ref = $opts->{err_ref} && ref $opts->{err_ref} eq 'SCALAR' ? $opts->{err_ref} : \$fake;
1100        $$err_ref = shift() || "Unspecified error";
1101        return undef;
1102    };
1103
1104    my $tags = [];
1105    return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $kw ) } ) )
1106        unless LJ::Tags::is_valid_tagstring($kw, $tags);
1107
1108    # check to ensure we don't exceed the max of tags
1109    my $max = $opts->{ignore_max} ? 0 : $u->count_tags_max;
1110    if ($max && $max > 0) {
1111        my $cur = scalar(keys %{ LJ::Tags::get_usertags($u) || {} });
1112        my $tagtotal = $cur + scalar(@$tags);
1113        if ($tagtotal > $max) {
1114            return $err->(LJ::Lang::ml('taglib.error.toomany2', { max => $max,
1115                                                                 tags => LJ::ehtml( join( ", ", @$tags ) ) ,
1116                                                                 excess => $tagtotal - $max }));
1117        }
1118    }
1119
1120    my $display = $opts->{display} ? 1 : 0;
1121    my $parentkwid = $opts->{parenttagid} ? ($opts->{parenttagid}+0) : undef;
1122
1123    my %res;
1124    foreach my $tag (@$tags) {
1125        my $kwid = $u->get_keyword_id( $tag );
1126        return undef unless $kwid;
1127
1128        $res{$tag} = $kwid;
1129    }
1130
1131    my $ct = scalar keys %res;
1132    my $bind = join(',', map { "(?,?,?,?)" } 1..$ct);
1133    $u->do("INSERT IGNORE INTO usertags (journalid, kwid, parentkwid, display) VALUES $bind",
1134           undef, map { $u->{userid}, $_, $parentkwid, $display } values %res);
1135    return undef if $u->err;
1136
1137    LJ::Tags::reset_cache($u);
1138    return \%res;
1139}
1140
1141# <LJFUNC>
1142# name: LJ::Tags::validate_tag
1143# class: tags
1144# des: Check the validity of a single tag.
1145# args: tag
1146# des-tag: The tag to check.
1147# returns: If valid, the canonicalized tag, else, undef.
1148# </LJFUNC>
1149sub validate_tag {
1150    my $tag = shift;
1151    return undef unless $tag;
1152
1153    my $list = [];
1154    return undef unless
1155        LJ::Tags::is_valid_tagstring($tag, $list);
1156    return undef if scalar(@$list) > 1;
1157
1158    return $list->[0];
1159}
1160
1161# <LJFUNC>
1162# name: LJ::Tags::delete_usertag
1163# class: tags
1164# des: Deletes a tag for a user, and all mappings.
1165# args: uobj, type, tag
1166# des-uobj: User object to delete tag on.
1167# des-type: Either 'id' or 'name', indicating the type of the third parameter.
1168# des-tag: If type is 'id', this is the tag id (kwid).  If type is 'name', this is the name of the
1169#          tag that we want to delete from the user.
1170# returns: undef on error, 1 for success, 0 for tag not found
1171# </LJFUNC>
1172sub delete_usertag {
1173    return undef unless LJ::is_enabled('tags');
1174
1175    my $u = LJ::want_user(shift);
1176    return undef unless $u;
1177
1178    my ($type, $val) = @_;
1179
1180    my $kwid;
1181    if ($type eq 'name') {
1182        my $tag = LJ::Tags::validate_tag($val);
1183        return undef unless $tag;
1184
1185        $kwid = $u->get_keyword_id( $tag, 0 );
1186    } elsif ($type eq 'id') {
1187        $kwid = $val + 0;
1188    }
1189    return undef unless $kwid;
1190
1191    # escape sub
1192    my $rollback = sub {
1193        die $u->errstr unless $u->rollback;
1194        return undef;
1195    };
1196
1197    # start the big transaction
1198    $u->begin_work;
1199
1200    # get items this keyword is on
1201    my $sth = $u->prepare('SELECT jitemid FROM logtags WHERE journalid = ? AND kwid = ? FOR UPDATE');
1202    return $rollback->() if $u->err || ! $sth;
1203
1204    # now get the items
1205    $sth->execute($u->{userid}, $kwid);
1206    return $rollback->() if $sth->err;
1207
1208    # now get list of jitemids for later cache clearing
1209    my @jitemids;
1210    push @jitemids, $_
1211        while $_ = $sth->fetchrow_array;
1212
1213    # delete this tag's information from the relevant tables
1214    foreach my $table (qw(usertags logtags logtagsrecent logkwsum)) {
1215        # no error checking, we're just deleting data that's already semi-unlinked due
1216        # to us already updating the userprop above
1217        $u->do("DELETE FROM $table WHERE journalid = ? AND kwid = ?",
1218               undef, $u->{userid}, $kwid);
1219    }
1220
1221    # all done with our updates
1222    die $u->errstr unless $u->commit;
1223
1224    # reset caches, have to do both of these, one for the usertags one for logtags
1225    LJ::Tags::reset_cache($u);
1226    LJ::Tags::reset_cache($u => \@jitemids);
1227    return 1;
1228}
1229
1230# <LJFUNC>
1231# name: LJ::Tags::rename_usertag
1232# class: tags
1233# des: Renames a tag for a user
1234# args: uobj, type, tag, newname, error_ref (optional)
1235# des-uobj: User object to delete tag on.
1236# des-type: Either 'id' or 'name', indicating the type of the third parameter.
1237# des-tag: If type is 'id', this is the tag id (kwid).  If type is 'name', this is the name of the
1238#          tag that we want to rename for the user.
1239# des-newname: The new name of this tag.
1240# des-error_ref: (optional) ref to scalar to return error messages in.
1241# returns: undef on error, 1 for success, 0 for tag not found
1242# </LJFUNC>
1243sub rename_usertag {
1244    return undef unless LJ::is_enabled('tags');
1245
1246    my $u = LJ::want_user(shift);
1247    return undef unless $u;
1248
1249    my ($type, $oldkw, $newkw, $ref) = @_;
1250    return undef unless $type && $oldkw && $newkw;
1251
1252    # setup error stuff
1253    my $err = sub {
1254        my $fake = "";
1255        my $err_ref = $ref && ref $ref eq 'SCALAR' ? $ref : \$fake;
1256        $$err_ref = shift() || "Unspecified error";
1257        return undef;
1258    };
1259
1260    # validate new tag
1261    my $newname = LJ::Tags::validate_tag($newkw);
1262    return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $newkw ) } ) )
1263        unless $newname;
1264
1265    # get a list of keyword ids to operate on
1266    my $kwid;
1267    if ($type eq 'name') {
1268        my $val = LJ::Tags::validate_tag($oldkw);
1269        return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $oldkw ) } ) )
1270            unless $val;
1271        $kwid = $u->get_keyword_id( $val, 0 );
1272    } elsif ($type eq 'id') {
1273        $kwid = $oldkw + 0;
1274    }
1275    return $err->() unless $kwid;
1276
1277    # see if this is already a keyword
1278    my $newkwid = $u->get_keyword_id( $newname );
1279    return undef unless $newkwid;
1280
1281    # see if the tag we're renaming TO already exists as a keyword,
1282    # if so, error and suggest merging the tags
1283    # FIXME: ask user to merge and then merge
1284    my $tags = LJ::Tags::get_usertags( $u );
1285    return $err->( LJ::Lang::ml( 'taglib.error.exists', { tagname => LJ::ehtml( $newname ) } ) )
1286        if $tags->{$newkwid};
1287
1288    # escape sub
1289    my $rollback = sub {
1290        die $u->errstr unless $u->rollback;
1291        return undef;
1292    };
1293
1294    # start the big transaction
1295    $u->begin_work;
1296
1297    # get items this keyword is on
1298    my $sth = $u->prepare('SELECT jitemid FROM logtags WHERE journalid = ? AND kwid = ? FOR UPDATE');
1299    return $rollback->() if $u->err || ! $sth;
1300
1301    # now get the items
1302    $sth->execute($u->{userid}, $kwid);
1303    return $rollback->() if $sth->err;
1304
1305    # now get list of jitemids for later cache clearing
1306    my @jitemids;
1307    push @jitemids, $_
1308        while $_ = $sth->fetchrow_array;
1309
1310    # do database update to migrate from old to new
1311    foreach my $table (qw(usertags logtags logtagsrecent logkwsum)) {
1312        $u->do("UPDATE $table SET kwid = ? WHERE journalid = ? AND kwid = ?",
1313               undef, $newkwid, $u->{userid}, $kwid);
1314        return $rollback->() if $u->err;
1315    }
1316
1317    # all done with our updates
1318    die $u->errstr unless $u->commit;
1319
1320    # reset caches, have to do both of these, one for the usertags one for logtags
1321    LJ::Tags::reset_cache($u);
1322    LJ::Tags::reset_cache($u => \@jitemids);
1323    return 1;
1324}
1325
1326# <LJFUNC>
1327# name: LJ::Tags::merge_usertags
1328# class: tags
1329# des: Merges usertags
1330# args: uobj, newname, error_ref, oldnames
1331# des-uobj: User object to merge tag on.
1332# des-newname: new name for these tags, might be one that already exists
1333# des-error_ref: ref to scalar to return error messages in.
1334# des-oldnames: array of tags that need to be merged
1335# returns: undef on error, 1 for success
1336# </LJFUNC>
1337sub merge_usertags {
1338    return undef unless LJ::is_enabled( 'tags' );
1339
1340    my $u = LJ::want_user( shift );
1341    return undef unless $u;
1342    my ( $merge_to, $ref, @merge_fr

Large files files are truncated, but you can click here to view the full file