# This code was forked from the LiveJournal project owned and operated # by Live Journal, Inc. The code has been modified and expanded by # Dreamwidth Studios, LLC. These files were originally licensed under # the terms of the license supplied by Live Journal, Inc, which can # currently be found at: # # http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt # # In accordance with the original license, this code and all its # modifications are provided under the GNU General Public License. # A copy of that license can be found in the LICENSE file included as # part of this distribution. package LJ::User; use strict; no warnings 'uninitialized'; use Carp; use Storable; use LJ::Global::Constants; use LJ::Keywords; use DW::Formats; ######################################################################## ### 13. Community-Related Functions and Authas =head2 Community-Related Functions and Authas =cut sub can_manage { # true if the first user is an admin for the target user. my ( $u, $target ) = @_; # backward compatibility: allow $target to be a userid $target = LJ::want_user($target) or return undef; # is same user? return 1 if $u->equals($target); # people/syn/rename accounts can only be managed by the one account return 0 if $target->journaltype =~ /^[PYR]$/; # check for admin access return 1 if LJ::check_rel( $target, $u, 'A' ); # failed checks, return false return 0; } sub can_manage_other { # true if the first user is an admin for the target user, # UNLESS the two users are the same. my ( $u, $target ) = @_; # backward compatibility: allow $target to be a userid $target = LJ::want_user($target) or return undef; return 0 if $u->equals($target); return $u->can_manage($target); } sub can_moderate { # true if the first user can moderate the target user. my ( $u, $target ) = @_; # backward compatibility: allow $target to be a userid $target = LJ::want_user($target) or return undef; return 1 if $u->can_manage_other($target); return LJ::check_rel( $target, $u, 'M' ); } # can $u post to $targetu? sub can_post_to { my ( $u, $targetu ) = @_; croak "Invalid users passed to LJ::User->can_post_to." unless LJ::isu($u) && LJ::isu($targetu); # if it's you, and you're a person, you can post to it return 1 if $u->is_person && $u->equals($targetu); # else, you have to be an individual and the target has to be a comm return 0 unless $u->is_individual && $targetu->is_community; # check if user has access explicit posting access return 1 if LJ::check_rel( $targetu, $u, 'P' ); # let's check if this community is allowing post access to non-members if ( $targetu->has_open_posting ) { my ( $ml, $pl ) = $targetu->get_comm_settings; return 1 if $pl eq 'members'; } # is the poster an admin for this community? admins can always post return 1 if $u->can_manage($targetu); return 0; } sub can_purchase_for { my ( $remote, $u ) = @_; croak "Invalid users passed to LJ::User->can_purchase_for." unless LJ::isu($u) && LJ::isu($remote); # if it's a community, allow if admin return 1 if $u->is_community && $remote->can_manage($u); # otherwise, you have to be acting on an individual return 0 unless $u->is_individual; # allow if both accounts are registered to the same address return 1 if $remote->has_same_email_as($u); return 0; } # list of communities that $u manages sub communities_managed_list { my ($u) = @_; croak "Invalid users passed to LJ::User->communities_managed_list" unless LJ::isu($u); my $cids = LJ::load_rel_target( $u, 'A' ); return undef unless $cids; my %users = %{ LJ::load_userids(@$cids) }; return map { $_ } grep { $_ && ( $_->is_visible || $_->is_readonly ) } values %users; } # list of communities that $u moderates sub communities_moderated_list { my ($u) = @_; croak "Invalid users passed to LJ::User->communities_moderated_list" unless LJ::isu($u); my $cids = LJ::load_rel_target( $u, 'M' ); return undef unless $cids; my %users = %{ LJ::load_userids(@$cids) }; return map { $_ } grep { $_ && ( $_->is_visible || $_->is_readonly ) } values %users; } # Get an array of usernames a given user can authenticate as. # Valid keys for opts hashref: # - type: filter by given journaltype (P or C) # - cap: filter by users who have given cap # - showall: override hiding of non-visible/non-read-only journals sub get_authas_list { my ( $u, $opts ) = @_; # Two valid types, Personal or Community $opts->{type} = undef unless $opts->{type} =~ m/^[PC]$/; my $ids = LJ::load_rel_target( $u, 'A' ); return undef unless $ids; my %users = %{ LJ::load_userids(@$ids) }; return map { $_->user } grep { !$opts->{cap} || $_->get_cap( $opts->{cap} ) } grep { !$opts->{type} || $opts->{type} eq $_->journaltype } # unless overridden, hide non-visible/non-read-only journals. # always display the user's acct grep { $opts->{showall} || $_->is_visible || $_->is_readonly || $_->equals($u) } # can't work as an expunged account grep { !$_->is_expunged && $_->clusterid > 0 } # put $u at the top of the list, then sort the rest ( $u, sort { $a->user cmp $b->user } values %users ); } # What journals can this user post to? sub posting_access_list { my $u = shift; my @res; my $ids = LJ::load_rel_target( $u, 'P' ); my $us = LJ::load_userids(@$ids); foreach ( values %$us ) { next unless $_->is_visible; push @res, $_; } return sort { $a->{user} cmp $b->{user} } @res; } # gets the relevant communities that the user is a member of # used to suggest communities to a person who know the user sub relevant_communities { my $u = shift; my %comms; my @ids = $u->member_of_userids; my $memberships = LJ::load_userids(@ids); # get all communities that $u is a member of that aren't closed membership # and that wish to be included in the community promo foreach my $membershipid ( keys %$memberships ) { my $membershipu = $memberships->{$membershipid}; next unless $membershipu->is_community; next if $membershipu->optout_community_promo; next unless $membershipu->is_visible; next if $membershipu->is_closed_membership; $comms{$membershipid}->{u} = $membershipu; $comms{$membershipid}->{istatus} = 'normal'; } # get usage information about comms if ( scalar keys %comms ) { my $comms_times = LJ::get_times_multi( keys %comms ); foreach my $commid ( keys %comms ) { if ( $comms_times->{created} && defined $comms_times->{created}->{$commid} ) { $comms{$commid}->{created} = $comms_times->{created}->{$commid}; } if ( $comms_times->{updated} && defined $comms_times->{updated}->{$commid} ) { $comms{$commid}->{updated} = $comms_times->{updated}->{$commid}; } } } # prune the list of communities # # keep a community in the list if: # * it was created in the past 10 days OR # * $u is a maint or mod of it OR # * it was updated in the past 30 days my $over30 = 0; my $now = time(); COMMUNITY: foreach my $commid ( sort { $comms{$b}->{updated} <=> $comms{$a}->{updated} } keys %comms ) { my $commu = $comms{$commid}->{u}; if ( $now - $comms{$commid}->{created} <= 60 * 60 * 24 * 10 ) { # 10 days $comms{$commid}->{istatus} = 'new'; next COMMUNITY; } my @maintainers = $commu->maintainer_userids; my @moderators = $commu->moderator_userids; foreach my $mid ( @maintainers, @moderators ) { if ( $mid == $u->id ) { $comms{$commid}->{istatus} = 'mm'; next COMMUNITY; } } if ($over30) { delete $comms{$commid}; next COMMUNITY; } else { if ( $now - $comms{$commid}->{updated} > 60 * 60 * 24 * 30 ) { # 30 days delete $comms{$commid}; # since we're going through the communities in timeupdate order, # we know every community in %comms after this one was updated # more than 30 days ago $over30 = 1; } } } # if we still have more than 20 comms, delete any with fewer than five members # as long as it's not new and $u isn't a maint/mod if ( scalar keys %comms > 20 ) { foreach my $commid ( keys %comms ) { my $commu = $comms{$commid}->{u}; next unless $comms{$commid}->{istatus} eq 'normal'; my @ids = $commu->member_userids; if ( scalar @ids < 5 ) { delete $comms{$commid}; } } } return %comms; } sub trusts_or_has_member { my ( $u, $target_u ) = @_; $target_u = LJ::want_user($target_u) or return 0; return $target_u->member_of($u) ? 1 : 0 if $u->is_community; return $u->trusts($target_u) ? 1 : 0; } ######################################################################## ### 14. Comment-Related Functions =head2 Comment-Related Functions =cut # true if u1 wouldn't allow a comment from u2, for ANY reason sub does_not_allow_comments_from { my ( $u1, $u2 ) = @_; return unless LJ::isu($u1); if ( LJ::isu($u2) ) { return $u1->has_banned($u2) || $u1->does_not_allow_comments_from_non_access($u2) || $u1->does_not_allow_comments_from_unconfirmed_openid($u2); } else { # Anonymous (which is never allowed on syndicated items) return $u1->prop('opt_whocanreply') ne 'all' || $u1->is_syndicated; } } # true if u1 restricts commenting to trusted and u2 is not trusted sub does_not_allow_comments_from_non_access { my ( $u1, $u2 ) = @_; return unless LJ::isu($u1) && LJ::isu($u2); return $u1->prop('opt_whocanreply') eq 'friends' && !$u1->trusts_or_has_member($u2); } # true if u1 restricts comments to registered users and u2 is a # non-circled OpenID with an unconfirmed email sub does_not_allow_comments_from_unconfirmed_openid { my ( $u1, $u2 ) = @_; return unless LJ::isu($u1) && LJ::isu($u2); return $u1->{'opt_whocanreply'} eq 'reg' && $u2->is_identity && !( $u2->is_validated || $u1->trusts($u2) ); } # get recent talkitems posted to this user # args: maximum number of comments to retrieve # returns: array of hashrefs with jtalkid, nodetype, nodeid, parenttalkid, posterid, state sub get_recent_talkitems { my ( $u, $maxshow, %opts ) = @_; $maxshow ||= 15; my $max_fetch = int( $LJ::TOOLS_RECENT_COMMENTS_MAX * 1.5 ) || 150; # We fetch more items because some may be screened # or from suspended users, and we weed those out later my $remote = $opts{remote} || LJ::get_remote(); return undef unless LJ::isu($u); ## $raw_talkitems - contains DB rows that are not filtered ## to match remote user's permissions to see my $raw_talkitems; my $memkey = [ $u->userid, 'rcntalk:' . $u->userid ]; $raw_talkitems = LJ::MemCache::get($memkey); if ( !$raw_talkitems ) { my $sth = $u->prepare( "SELECT jtalkid, nodetype, nodeid, parenttalkid, " . " posterid, UNIX_TIMESTAMP(datepost) as 'datepostunix', state " . "FROM talk2 " . "WHERE journalid=? AND state <> 'D' " . "ORDER BY jtalkid DESC " . "LIMIT $max_fetch" ); $sth->execute( $u->userid ); $raw_talkitems = $sth->fetchall_arrayref( {} ); LJ::MemCache::set( $memkey, $raw_talkitems, 60 * 5 ); } ## Check remote's permission to see the comment, and create singletons my @recv; foreach my $r (@$raw_talkitems) { last if @recv >= $maxshow; # construct an LJ::Comment singleton my $comment = LJ::Comment->new( $u, jtalkid => $r->{jtalkid} ); $comment->absorb_row(%$r); next unless $comment->visible_to($remote); push @recv, $r; } # need to put the comments in order, with "oldest first" # they are fetched from DB in "recent first" order return reverse @recv; } # return the number of comments a user has posted sub num_comments_posted { my $u = shift; my %opts = @_; my $dbcr = $opts{dbh} || LJ::get_cluster_reader($u); my $userid = $u->id; my $memkey = [ $userid, "talkleftct:$userid" ]; my $count = LJ::MemCache::get($memkey); unless ($count) { my $expire = time() + 3600 * 24 * 2; # 2 days; $count = $dbcr->selectrow_array( "SELECT COUNT(*) FROM talkleft " . "WHERE userid=?", undef, $userid ); LJ::MemCache::set( $memkey, $count, $expire ) if defined $count; } return $count; } # return the number of comments a user has received sub num_comments_received { my $u = shift; my %opts = @_; my $dbcr = $opts{dbh} || LJ::get_cluster_reader($u); my $userid = $u->id; my $memkey = [ $userid, "talk2ct:$userid" ]; my $count = LJ::MemCache::get($memkey); unless ($count) { my $expire = time() + 3600 * 24 * 2; # 2 days; $count = $dbcr->selectrow_array( "SELECT COUNT(*) FROM talk2 " . "WHERE journalid=?", undef, $userid ); LJ::MemCache::set( $memkey, $count, $expire ) if defined $count; } return $count; } ######################################################################## ### 15. Entry-Related Functions =head2 Entry-Related Functions =cut # front-end to recent_entries, which forces the remote user to be # the owner, so we get everything. sub all_recent_entries { my ( $u, %opts ) = @_; $opts{filtered_for} = $u; return $u->recent_entries(%opts); } sub draft_text { my ($u) = @_; return $u->prop('entry_draft'); } sub entryform_width { my ($u) = @_; if ( $u->raw_prop('entryform_width') =~ /^(F|P)$/ ) { return $u->raw_prop('entryform_width'); } else { return 'F'; } } # don't call this directly, only use the wrapper functions below. sub _editor_props { my ( $u, $propname, $new_editor ) = @_; if ( defined $new_editor ) { $new_editor = DW::Formats::validate($new_editor); $u->set_prop( $propname => $new_editor ); return $new_editor; } my $editor_default = DW::Formats::validate( $u->raw_prop($propname) ); if ( !$editor_default ) { return $DW::Formats::default_format; } if ( !DW::Formats::is_active($editor_default) && DW::Formats::is_active( DW::Formats::upgrade($editor_default) ) ) { # Silently upgrade to new version. $editor_default = DW::Formats::upgrade($editor_default); $u->set_prop( $propname => $editor_default ); } return $editor_default; } # getter/setter sub comment_editor { my ( $u, $new_editor ) = @_; return _editor_props( $u, 'comment_editor', $new_editor ); } # getter/setter sub entry_editor2 { my ( $u, $new_editor ) = @_; return _editor_props( $u, 'entry_editor2', $new_editor ); } # getter/setter sub default_entryform_panels { my (%opts) = @_; my $anonymous = $opts{anonymous} ? 1 : 0; my $force_show = $anonymous; return { order => $anonymous ? [ [ "tags", "displaydate", "slug" ], ["currents"], [ "comments", "age_restriction" ], ] : [ [ "tags", "displaydate", "slug" ], # FIXME: should be [ "status" ... ] %] [ "currents", "comments", "age_restriction" ], # FIXME: should be [ ... "scheduled" ] [ "icons", "crosspost", "sticky" ], ], show => { "tags" => 1, "currents" => 1, "slug" => 1, "displaydate" => $force_show, "comments" => $force_show, "age_restriction" => $force_show, "icons" => 1, "crosspost" => $force_show, #"scheduled" => $force_show, "sticky" => 1, #"status" => 1, }, collapsed => {} }; } sub entryform_panels { my ( $u, $val ) = @_; if ( defined $val ) { $u->set_prop( entryform_panels => Storable::nfreeze($val) ); return $val; } my $prop = $u->prop("entryform_panels"); my $default = LJ::User::default_entryform_panels(); my %obsolete = ( access => 1, journal => 1, flags => 1, ); my %need_panels = map { $_ => 1 } keys %{ $default->{show} }; my $ret; $ret = Storable::thaw($prop) if $prop; if ($ret) { # remove any obsolete panels from "show" and "collapse" foreach my $panel ( keys %obsolete ) { delete $ret->{show}->{$panel}; delete $ret->{collapsed}->{$panel}; } foreach my $column ( @{ $ret->{order} } ) { # remove any obsolete panels from "order" my @col = @{$column}; my @del_indexes = grep { $obsolete{ $col[$_] } } 0 .. $#col; if (@del_indexes) { foreach my $del ( reverse @del_indexes ) { splice @col, $del, 1; } } $column = \@col; # fill in any modules that somehow are not in this list foreach my $panel ( @{$column} ) { delete $need_panels{$panel}; } } my @col = @{ $ret->{order}->[2] }; foreach ( keys %need_panels ) { # add back into last column, but respect user's option to show/not-show push @col, $_; $ret->{show}->{$_} = 0 unless defined $ret->{show}->{$_}; } $ret->{order}->[2] = \@col; } else { $ret = $default; } return $ret; } sub entryform_panels_order { my ( $u, $val ) = @_; my $panels = $u->entryform_panels; if ( defined $val ) { $panels->{order} = $val; $panels = $u->entryform_panels($panels); } return $panels->{order}; } sub entryform_panels_visibility { my ( $u, $val ) = @_; my $panels = $u->entryform_panels; if ( defined $val ) { $panels->{show} = $val; $panels = $u->entryform_panels($panels); } return $panels->{show}; } sub entryform_panels_collapsed { my ( $u, $val ) = @_; my $panels = $u->entryform_panels; if ( defined $val ) { $panels->{collapsed} = $val; $panels = $u->entryform_panels($panels); } return $panels->{collapsed}; } # # name: LJ::get_post_ids # des: Given a user object and some options, return the number of posts or the # posts'' IDs (jitemids) that match. # returns: number of matching posts, or IDs of # matching posts (default). # args: u, opts # des-opts: 'security' - [public|private|usemask] # 'allowmask' - integer for friends-only or custom groups # 'start_date' - UTC date after which to look for match # 'end_date' - UTC date before which to look for match # 'return' - if 'count' just return the count # FIXME: Add caching? # sub get_post_ids { my ( $u, %opts ) = @_; my $query = 'SELECT'; my @vals; # parameters to query if ( $opts{'start_date'} || $opts{'end_date'} ) { croak "start or end date not defined" if ( !$opts{'start_date'} || !$opts{'end_date'} ); if ( !( $opts{'start_date'} >= 0 ) || !( $opts{'end_date'} >= 0 ) || !( $opts{'start_date'} <= $LJ::EndOfTime ) || !( $opts{'end_date'} <= $LJ::EndOfTime ) ) { return undef; } } # return count or jitemids if ( $opts{'return'} eq 'count' ) { $query .= " COUNT(*)"; } else { $query .= " jitemid"; } # from the journal entries table for this user $query .= " FROM log2 WHERE journalid=?"; push( @vals, $u->userid ); # filter by security if ( $opts{'security'} ) { $query .= " AND security=?"; push( @vals, $opts{'security'} ); # If friends-only or custom if ( $opts{'security'} eq 'usemask' && $opts{'allowmask'} ) { $query .= " AND allowmask=?"; push( @vals, $opts{'allowmask'} ); } } # filter by date, use revttime as it is indexed if ( $opts{'start_date'} && $opts{'end_date'} ) { # revttime is reverse event time my $s_date = $LJ::EndOfTime - $opts{'start_date'}; my $e_date = $LJ::EndOfTime - $opts{'end_date'}; $query .= " AND revttime?"; push( @vals, $e_date ); } # return count or jitemids if ( $opts{'return'} eq 'count' ) { return $u->selectrow_array( $query, undef, @vals ); } else { my $jitemids = $u->selectcol_arrayref( $query, undef, @vals ) || []; die $u->errstr if $u->err; return @$jitemids; } } # Returns 'rich' or 'plain' depending on user's # setting of which editor they would like to use # and what they last used sub new_entry_editor { my $u = shift; my $editor = $u->prop('entry_editor'); return 'plain' if $editor eq 'always_plain'; # They said they always want plain return 'rich' if $editor eq 'always_rich'; # They said they always want rich return $editor if $editor =~ /(rich|plain)/; # What did they last use? return $LJ::DEFAULT_EDITOR; # Use config default } # What security level to use for new posts. This magic flag is used to give # user's the ability to specify that "if I try to post public, don't let me". # To override this, you have to go back and edit your post. sub newpost_minsecurity { return $_[0]->prop('newpost_minsecurity') || 'public'; } # This loads the user's specified post-by-email security. If they haven't # set that up, then we fall back to the standard new post minimum security. sub emailpost_security { return $_[0]->prop('emailpost_security') || $_[0]->newpost_minsecurity; } *get_post_count = \&number_of_posts; sub number_of_posts { my ( $u, %opts ) = @_; # to count only a subset of all posts if (%opts) { $opts{return} = 'count'; return $u->get_post_ids(%opts); } my $userid = $u->userid; my $memkey = [ $userid, "log2ct:$userid" ]; my $expire = time() + 3600 * 24 * 2; # 2 days return LJ::MemCache::get_or_set( $memkey, sub { return $u->selectrow_array( "SELECT COUNT(*) FROM log2 WHERE journalid=?", undef, $userid ); }, $expire ); } # returns array of LJ::Entry objects, ignoring security sub recent_entries { my ( $u, %opts ) = @_; my $remote = delete $opts{'filtered_for'} || LJ::get_remote(); my $count = delete $opts{'count'} || 50; my $order = delete $opts{'order'} || ""; die "unknown options" if %opts; my $err; my @recent = $u->recent_items( itemshow => $count, err => \$err, clusterid => $u->clusterid, remote => $remote, order => $order, ); die "Error loading recent items: $err" if $err; my @objs; foreach my $ri (@recent) { my $entry = LJ::Entry->new( $u, jitemid => $ri->{itemid} ); push @objs, $entry; # FIXME: populate the $entry with security/posterid/alldatepart/ownerid/rlogtime } return @objs; } sub security_group_display { my ( $u, $allowmask ) = @_; return '' unless LJ::isu($u); return '' unless defined $allowmask; my $remote = LJ::get_remote() or return ''; my $use_urls = $remote->get_cap("security_filter") || $u->get_cap("security_filter"); # see which group ids are in the security mask my %group_ids = ( map { $_ => 1 } grep { $allowmask & ( 1 << $_ ) } 1 .. 60 ); return '' unless scalar( keys %group_ids ) > 0; my @ret; my @groups = $u->trust_groups; foreach my $group (@groups) { next unless $group_ids{ $group->{groupnum} }; # not in mask my $name = LJ::ehtml( $group->{groupname} ); if ($use_urls) { my $url = LJ::eurl( $u->journal_base . "/security/group:$name" ); push @ret, "$name"; } else { push @ret, $name; } } return join( ', ', @ret ); } sub set_draft_text { my ( $u, $draft ) = @_; my $old = $u->draft_text; $LJ::_T_DRAFT_RACE->() if $LJ::_T_DRAFT_RACE; # try to find a shortcut that makes the SQL shorter my @methods; # list of [ $subref, $cost ] # one method is just setting it all at once. which incurs about # 75 bytes of SQL overhead on top of the length of the draft, # not counting the escaping push @methods, [ "set", sub { $u->set_prop( 'entry_draft', $draft ); 1 }, 75 + length $draft ]; # stupid case, setting the same thing: push @methods, [ "noop", sub { 1 }, 0 ] if $draft eq $old; # simple case: appending if ( length $old && $draft =~ /^\Q$old\E(.+)/s ) { my $new = $1; my $appending = sub { my $prop = LJ::get_prop( "user", "entry_draft" ) or die; # FIXME: use exceptions my $rv = $u->do( "UPDATE userpropblob SET value = CONCAT(value, ?) WHERE userid=? AND upropid=? AND LENGTH(value)=?", undef, $new, $u->userid, $prop->{id}, length $old ); return 0 unless $rv > 0; $u->uncache_prop("entry_draft"); return 1; }; push @methods, [ "append", $appending, 40 + length $new ]; } # FIXME: prepending/middle insertion (the former being just the latter), as # well as appending, wihch we could then get rid of # try the methods in increasing order foreach my $m ( sort { $a->[2] <=> $b->[2] } @methods ) { my $func = $m->[1]; if ( $func->() ) { $LJ::_T_METHOD_USED->( $m->[0] ) if $LJ::_T_METHOD_USED; # for testing return 1; } } return 0; } ######################################################################## ### 27. Tag-Related Functions =head2 Tag-Related Functions =cut # can $u add existing tags to $targetu's entries? sub can_add_tags_to { my ( $u, $targetu ) = @_; return LJ::Tags::can_add_tags( $targetu, $u ); } # can $u control (add, delete, edit) the tags of $targetu? sub can_control_tags { my ( $u, $targetu ) = @_; return LJ::Tags::can_control_tags( $targetu, $u ); } # # name: LJ::User::get_keyword_id # class: # des: Get the id for a keyword. # args: uuid, keyword, autovivify? # des-uuid: User object or userid to use. # des-keyword: A string keyword to get the id of. # returns: Returns a kwid into [dbtable[userkeywords]]. # If the keyword doesn't exist, it is automatically created for you. # des-autovivify: If present and 1, automatically create keyword. # If present and 0, do not automatically create the keyword. # If not present, default behavior is the old # style -- yes, do automatically create the keyword. # sub get_keyword_id { my ( $u, $kw, $autovivify ) = @_; $u = LJ::want_user($u); return undef unless $u; $autovivify = 1 unless defined $autovivify; # setup the keyword for use return 0 unless $kw =~ /\S/; $kw = LJ::text_trim( $kw, LJ::BMAX_KEYWORD, LJ::CMAX_KEYWORD ); # get the keyword and insert it if necessary my $kwid = $u->selectrow_array( 'SELECT kwid FROM userkeywords WHERE userid = ? AND keyword = ?', undef, $u->userid, $kw ) + 0; if ( $autovivify && !$kwid ) { # create a new keyword $kwid = LJ::alloc_user_counter( $u, 'K' ); return undef unless $kwid; # attempt to insert the keyword my $rv = $u->do( "INSERT IGNORE INTO userkeywords (userid, kwid, keyword) VALUES (?, ?, ?)", undef, $u->userid, $kwid, $kw ) + 0; return undef if $u->err; # at this point, if $rv is 0, the keyword is already there so try again unless ($rv) { $kwid = $u->selectrow_array( 'SELECT kwid FROM userkeywords WHERE userid = ? AND keyword = ?', undef, $u->userid, $kw ) + 0; } # nuke cache $u->memc_delete('kws'); } return $kwid; } sub tags { my $u = shift; return LJ::Tags::get_usertags($u); } 1;