mourningdove/cgi-bin/LJ/User/Journal.pm
2026-05-24 01:03:05 +00:00

960 lines
28 KiB
Perl

# 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};
}
# <LJFUNC>
# 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, <strong>or</strong> 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?
# </LJFUNC>
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, $s_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, "<a href='$url'>$name</a>";
}
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 );
}
# <LJFUNC>
# 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.
# </LJFUNC>
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;