960 lines
28 KiB
Perl
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;
|