[dw-free] Rearrange, clean up, and comment cgi-bin/LJ/User.pm
[commit: http://hg.dwscoalition.org/dw-free/rev/2dba2ba18e24]
http://bugs.dwscoalition.org/show_bug.cgi?id=390
Reorganize cgi-bin/LJ/User.pm so that things are in logical groups to make
reading and editing easier.
Patch by
denise.
http://bugs.dwscoalition.org/show_bug.cgi?id=390
Reorganize cgi-bin/LJ/User.pm so that things are in logical groups to make
reading and editing easier.
Patch by
--------------------------------------------------------------------------------
diff -r 092af917c214 -r 2dba2ba18e24 cgi-bin/LJ/User.pm
--- a/cgi-bin/LJ/User.pm Fri Feb 27 02:42:00 2009 +0000
+++ b/cgi-bin/LJ/User.pm Fri Feb 27 02:51:19 2009 +0000
@@ -12,6 +12,9 @@
use strict;
no warnings 'uninitialized';
+
+########################################################################
+### Begin LJ::User functions
package LJ::User;
use Carp;
@@ -37,6 +40,70 @@ use Class::Autouse qw(
LJ::S2Theme
);
+########################################################################
+### Please keep these categorized and alphabetized for ease of use.
+### If you need a new category, add it at the end, BEFORE category 99.
+### Categories kinda fuzzy, but better than nothing.
+###
+### Categories:
+### 1. Creating and Deleting Accounts
+### 2. Statusvis and Account Types
+### 3. Working with All Types of Account
+### 4. Login, Session, and Rename Functions
+### 5. Database and Memcache Functions
+### 6. What the App Shows to Users
+### 7. Userprops, Caps, and Displaying Content to Others
+### 8. Formatting Content Shown to Users
+### 9. Logging and Recording Actions
+### 10. Banning-Related Functions
+### 11. Birthdays and Age-Related Functions
+### 12. Comment-Related Functions
+### 13. Community-Related Functions and Authas
+### 14. Content Flagging and Adult Content Functions
+### 15. Email-Related Functions
+### 16. Entry-Related Functions
+### 17. Interest-Related Functions
+### 18. Jabber-Related Functions
+### 19. OpenID and Identity Functions
+### 20. Page Notices Functions
+### 21. Password Functions
+### 22. Priv-Related Functions
+### 23. SMS-Related Functions
+### 24. Styles and S2-Related Functions
+### 25. Subscription, Notifiction, and Messaging Functions
+### 26. Syndication-Related Functions
+### 27. Tag-Related Functions
+### 28. Userpic-Related Functions
+### 99. Miscellaneous Legacy Items
+
+
+
+
+
+########################################################################
+### 1. Creating and Deleting Accounts
+
+
+sub can_expunge {
+ my $u = shift;
+
+ # must be already deleted
+ return 0 unless $u->is_deleted;
+
+ # and deleted 30 days ago
+ my $expunge_days = LJ::conf_test($LJ::DAYS_BEFORE_EXPUNGE) || 30;
+ return 0 unless $u->statusvisdate_unix < time() - 86400*$expunge_days;
+
+ my $hook_rv = 0;
+ if (LJ::are_hooks("can_expunge_user", $u)) {
+ $hook_rv = LJ::run_hook("can_expunge_user", $u);
+ return $hook_rv ? 1 : 0;
+ }
+
+ return 1;
+}
+
+
# class method to create a new account.
sub create {
my ($class, %opts) = @_;
@@ -110,6 +177,28 @@ sub create {
return $u;
}
+
+
+sub create_community {
+ my ($class, %opts) = @_;
+
+ $opts{journaltype} = "C";
+ my $u = LJ::User->create(%opts) or return;
+
+ $u->set_prop("nonmember_posting", $opts{nonmember_posting}+0);
+ $u->set_prop("moderated", $opts{moderated}+0);
+ $u->set_prop("adult_content", $opts{journal_adult_settings}) if LJ::is_enabled("content_flag");
+
+ my $remote = LJ::get_remote();
+ LJ::set_rel($u, $remote, "A"); # maintainer
+ LJ::set_rel($u, $remote, "M") if $opts{moderated}; # moderator if moderated
+ LJ::join_community($remote, $u, 1, 1); # member
+
+ LJ::set_comm_settings($u, $remote, { membership => $opts{membership},
+ postlevel => $opts{postlevel} });
+ return $u;
+}
+
sub create_personal {
my ($class, %opts) = @_;
@@ -182,25 +271,6 @@ sub create_personal {
return $u;
}
-sub create_community {
- my ($class, %opts) = @_;
-
- $opts{journaltype} = "C";
- my $u = LJ::User->create(%opts) or return;
-
- $u->set_prop("nonmember_posting", $opts{nonmember_posting}+0);
- $u->set_prop("moderated", $opts{moderated}+0);
- $u->set_prop("adult_content", $opts{journal_adult_settings}) if LJ::is_enabled("content_flag");
-
- my $remote = LJ::get_remote();
- LJ::set_rel($u, $remote, "A"); # maintainer
- LJ::set_rel($u, $remote, "M") if $opts{moderated}; # moderator if moderated
- LJ::join_community($remote, $u, 1, 1); # member
-
- LJ::set_comm_settings($u, $remote, { membership => $opts{membership},
- postlevel => $opts{postlevel} });
- return $u;
-}
sub create_syndicated {
my ($class, %opts) = @_;
@@ -224,24 +294,31 @@ sub create_syndicated {
return $u;
}
-# retrieve hash of basic syndicated info
-sub get_syndicated {
- my $u = shift;
-
- return unless $u->is_syndicated;
- my $memkey = [$u->{'userid'}, "synd:$u->{'userid'}"];
-
- my $synd = {};
- $synd = LJ::MemCache::get($memkey);
- unless ($synd) {
- my $dbr = LJ::get_db_reader();
- return unless $dbr;
- $synd = $dbr->selectrow_hashref("SELECT * FROM syndicated WHERE userid=$u->{'userid'}");
- LJ::MemCache::set($memkey, $synd) if $synd;
- }
-
- return $synd;
-}
+
+sub delete_and_purge_completely {
+ my $u = shift;
+ # TODO: delete from user tables
+ # TODO: delete from global tables
+ my $dbh = LJ::get_db_writer();
+
+ my @tables = qw(user useridmap reluser priv_map infohistory email password);
+ foreach my $table (@tables) {
+ $dbh->do("DELETE FROM $table WHERE userid=?", undef, $u->id);
+ }
+
+ $dbh->do("DELETE FROM wt_edges WHERE from_userid = ? OR to_userid = ?", undef, $u->id, $u->id);
+ $dbh->do("DELETE FROM reluser WHERE targetid=?", undef, $u->id);
+ $dbh->do("DELETE FROM email_aliases WHERE alias=?", undef, $u->user . "\@$LJ::USER_DOMAIN");
+
+ $dbh->do("DELETE FROM community WHERE userid=?", undef, $u->id)
+ if $u->is_community;
+ $dbh->do("DELETE FROM syndicated WHERE userid=?", undef, $u->id)
+ if $u->is_syndicated;
+ $dbh->do("DELETE FROM content_flag WHERE journalid=? OR reporterid=?", undef, $u->id, $u->id);
+
+ return 1;
+}
+
sub is_protected_username {
my ($class, $username) = @_;
@@ -251,40 +328,325 @@ sub is_protected_username {
return 0;
}
-sub new_from_row {
- my ($class, $row) = @_;
- my $u = bless $row, $class;
-
- # for selfassert method below:
- $u->{_orig_userid} = $u->{userid};
- $u->{_orig_user} = $u->{user};
-
- return $u;
-}
-
-sub new_from_url {
- my ($class, $url) = @_;
-
- # this doesn't seem to like URLs with ?...
- $url =~ s/\?.+$//;
-
- # /users, /community, or /~
- if ($url =~ m!^\Q$LJ::SITEROOT\E/(?:users/|community/|~)([\w-]+)/?!) {
- return LJ::load_user($1);
- }
-
- # user subdomains
- if ($LJ::USER_DOMAIN && $url =~ m!^http://([\w-]+)\.\Q$LJ::USER_DOMAIN\E/?$!) {
- return LJ::load_user($1);
- }
-
- # subdomains that hold a bunch of users (eg, users.siteroot.com/username/)
- if ($url =~ m!^http://\w+\.\Q$LJ::USER_DOMAIN\E/([\w-]+)/?$!) {
- return LJ::load_user($1);
- }
-
- return undef;
-}
+
+sub postreg_completed {
+ my $u = shift;
+
+ return 0 unless $u->bio;
+ return 0 unless $u->interest_count;
+ return 1;
+}
+
+
+sub who_invited {
+ my $u = shift;
+ my $inviterid = LJ::load_rel_user($u, 'I');
+
+ return LJ::load_userid($inviterid);
+}
+
+
+
+########################################################################
+### 2. Statusvis and Account Types
+
+
+sub is_deleted {
+ my $u = shift;
+ return $u->statusvis eq 'D';
+}
+
+
+sub is_expunged {
+ my $u = shift;
+ return $u->statusvis eq 'X' || $u->clusterid == 0;
+}
+
+
+sub is_locked {
+ my $u = shift;
+ return $u->statusvis eq 'L';
+}
+
+
+sub is_memorial {
+ my $u = shift;
+ return $u->statusvis eq 'M';
+}
+
+
+sub is_readonly {
+ my $u = shift;
+ return $u->statusvis eq 'O';
+}
+
+
+sub is_renamed {
+ my $u = shift;
+ return $u->statusvis eq 'R';
+}
+
+
+sub is_suspended {
+ my $u = shift;
+ return $u->statusvis eq 'S';
+}
+
+
+# returns if this user is considered visible
+sub is_visible {
+ my $u = shift;
+ return $u->statusvis eq 'V';
+}
+
+
+sub set_deleted {
+ my $u = shift;
+ my $res = $u->set_statusvis('D');
+
+ # run any account cancellation hooks
+ LJ::run_hooks("account_delete", $u);
+ return $res;
+}
+
+
+sub set_expunged {
+ my $u = shift;
+ return $u->set_statusvis('X');
+}
+
+
+sub set_locked {
+ my $u = shift;
+ return $u->set_statusvis('L');
+}
+
+
+sub set_memorial {
+ my $u = shift;
+ return $u->set_statusvis('M');
+}
+
+
+sub set_readonly {
+ my $u = shift;
+ return $u->set_statusvis('O');
+}
+
+
+sub set_renamed {
+ my $u = shift;
+ return $u->set_statusvis('R');
+}
+
+
+sub set_suspended {
+ my ($u, $who, $reason, $errref) = @_;
+ die "Not enough parameters for LJ::User::set_suspended call" unless $who and $reason;
+
+ my $res = $u->set_statusvis('S');
+ unless ($res) {
+ $$errref = "DB error while setting statusvis to 'S'" if ref $errref;
+ return $res;
+ }
+
+ LJ::statushistory_add($u, $who, "suspend", $reason);
+
+ eval { $u->fb_push };
+ warn "Error running fb_push: $@\n" if $@ && $LJ::IS_DEV_SERVER;
+
+ LJ::run_hooks("account_cancel", $u);
+
+ if (my $err = LJ::run_hook("cdn_purge_userpics", $u)) {
+ $$errref = $err if ref $errref and $err;
+ return 0;
+ }
+
+ return $res; # success
+}
+
+
+# set_statusvis only change statusvis parameter, all accompanied actions are done in set_* methods
+sub set_statusvis {
+ my ($u, $statusvis) = @_;
+
+ croak "Invalid statusvis: $statusvis"
+ unless $statusvis =~ /^(?:
+ V| # visible
+ D| # deleted
+ X| # expunged
+ S| # suspended
+ L| # locked
+ M| # memorial
+ O| # read-only
+ R # renamed
+ )$/x;
+
+ # log the change to userlog
+ $u->log_event('accountstatus', {
+ # remote looked up by log_event
+ old => $u->statusvis,
+ new => $statusvis,
+ });
+
+ # do update
+ return LJ::update_user($u, { statusvis => $statusvis,
+ raw => 'statusvisdate=NOW()' });
+}
+
+
+# sets a user to visible, but also does all of the stuff necessary when a suspended account is unsuspended
+# this can only be run on a suspended account
+sub set_unsuspended {
+ my ($u, $who, $reason, $errref) = @_;
+ die "Not enough parameters for LJ::User::set_unsuspended call" unless $who and $reason;
+
+ unless ($u->is_suspended) {
+ $$errref = "User isn't suspended" if ref $errref;
+ return 0;
+ }
+
+ my $res = $u->set_statusvis('V');
+ unless ($res) {
+ $$errref = "DB error while setting statusvis to 'V'" if ref $errref;
+ return $res;
+ }
+
+ LJ::statushistory_add($u, $who, "unsuspend", $reason);
+
+ eval { $u->fb_push };
+ warn "Error running fb_push: $@\n" if $@ && $LJ::IS_DEV_SERVER;
+
+ return $res; # success
+}
+
+
+sub set_visible {
+ my $u = shift;
+ return $u->set_statusvis('V');
+}
+
+
+sub statusvis {
+ my $u = shift;
+ return $u->{statusvis};
+}
+
+
+sub statusvisdate {
+ my $u = shift;
+ return $u->{statusvisdate};
+}
+
+
+sub statusvisdate_unix {
+ my $u = shift;
+ return LJ::mysqldate_to_time($u->{statusvisdate});
+}
+
+
+
+########################################################################
+### 3. Working with All Types of Account
+
+
+# this will return a hash of information about this user.
+# this is useful for JavaScript endpoints which need to dump
+# JSON data about users.
+sub info_for_js {
+ my $u = shift;
+
+ my %ret = (
+ username => $u->user,
+ display_username => $u->display_username,
+ display_name => $u->display_name,
+ userid => $u->userid,
+ url_journal => $u->journal_base,
+ url_profile => $u->profile_url,
+ url_allpics => $u->allpics_base,
+ ljuser_tag => $u->ljuser_display,
+ is_comm => $u->is_comm,
+ is_person => $u->is_person,
+ is_syndicated => $u->is_syndicated,
+ is_identity => $u->is_identity,
+ is_shared => $u->is_shared,
+ );
+ # Without url_message "Send Message" link should not display
+ $ret{url_message} = $u->message_url unless ($u->opt_usermsg eq 'N');
+
+ LJ::run_hook("extra_info_for_js", $u, \%ret);
+
+ my $up = $u->userpic;
+
+ if ($up) {
+ $ret{url_userpic} = $up->url;
+ $ret{userpic_w} = $up->width;
+ $ret{userpic_h} = $up->height;
+ }
+
+ return %ret;
+}
+
+
+sub is_community {
+ my $u = shift;
+ return $u->{journaltype} eq "C";
+}
+*is_comm = \&is_community;
+
+
+sub is_identity {
+ my $u = shift;
+ return $u->{journaltype} eq "I";
+}
+
+
+sub is_news {
+ my $u = shift;
+ return $u->{journaltype} eq "N";
+}
+
+
+sub is_person {
+ my $u = shift;
+ return $u->{journaltype} eq "P";
+}
+*is_personal = \&is_person;
+
+
+sub is_shared {
+ my $u = shift;
+ return $u->{journaltype} eq "S";
+}
+
+
+sub is_syndicated {
+ my $u = shift;
+ return $u->{journaltype} eq "Y";
+}
+
+
+sub journaltype {
+ my $u = shift;
+ return $u->{journaltype};
+}
+
+
+# return the journal type as a name
+sub journaltype_readable {
+ my $u = shift;
+
+ return {
+ R => 'redirect',
+ I => 'identity',
+ P => 'personal',
+ S => 'shared',
+ Y => 'syndicated',
+ N => 'news',
+ C => 'community',
+ }->{$u->{journaltype}};
+}
+
# returns LJ::User class of a random user, undef if we couldn't get one
# my $random_u = LJ::User->load_random_user(type);
@@ -331,6 +693,13 @@ sub load_random_user {
return undef;
}
+
+sub preload_props {
+ my $u = shift;
+ LJ::load_user_props($u, @_);
+}
+
+
# class method. returns remote (logged in) user object. or undef if
# no session is active.
sub remote {
@@ -338,6 +707,7 @@ sub remote {
return LJ::get_remote($opts);
}
+
# class method. set the remote user ($u or undef) for the duration of this request.
# once set, it'll never be reloaded, unless "unset_remote" is called to forget it.
sub set_remote
@@ -348,6 +718,41 @@ sub set_remote
1;
}
+
+# when was this account created?
+# returns unixtime
+sub timecreate {
+ my $u = shift;
+
+ return $u->{_cache_timecreate} if $u->{_cache_timecreate};
+
+ my $memkey = [$u->id, "tc:" . $u->id];
+ my $timecreate = LJ::MemCache::get($memkey);
+ if ($timecreate) {
+ $u->{_cache_timecreate} = $timecreate;
+ return $timecreate;
+ }
+
+ my $dbr = LJ::get_db_reader() or die "No db";
+ my $when = $dbr->selectrow_array("SELECT timecreate FROM userusage WHERE userid=?", undef, $u->id);
+
+ $timecreate = LJ::mysqldate_to_time($when);
+ $u->{_cache_timecreate} = $timecreate;
+ LJ::MemCache::set($memkey, $timecreate, 60*60*24);
+
+ return $timecreate;
+}
+
+
+# when was last time this account updated?
+# returns unixtime
+sub timeupdate {
+ my $u = shift;
+ my $timeupdate = LJ::get_timeupdate_multi($u->id);
+ return $timeupdate->{$u->id};
+}
+
+
# class method. forgets the cached remote user.
sub unset_remote
{
@@ -357,412 +762,119 @@ sub unset_remote
1;
}
-sub preload_props {
- my $u = shift;
- LJ::load_user_props($u, @_);
-}
-
-sub readonly {
- my $u = shift;
- return LJ::get_cap($u, "readonly");
-}
-
-# returns self (the $u object which can be used for $u->do) if
-# user is writable, else 0
-sub writer {
- my $u = shift;
- return $u if $u->{'_dbcm'} ||= LJ::get_cluster_master($u);
- return 0;
-}
-
-sub userpic {
- my $u = shift;
- return undef unless $u->{defaultpicid};
- return LJ::Userpic->new($u, $u->{defaultpicid});
-}
-
-# returns a true value if the user is underage; or if you give it an argument,
-# will turn on/off that user's underage status. can also take a second argument
-# when you're setting the flag to also update the underage_status userprop
-# which is used to record if a user was ever marked as underage.
-sub underage {
- # has no bearing if this isn't on
- return undef unless LJ::class_bit("underage");
-
- # now get the args and continue
- my $u = shift;
- return LJ::get_cap($u, 'underage') unless @_;
-
- # now set it on or off
- my $on = shift() ? 1 : 0;
- if ($on) {
- $u->add_to_class("underage");
- } else {
- $u->remove_from_class("underage");
- }
-
- # now set their status flag if one was sent
- my $status = shift();
- if ($status || $on) {
- # by default, just records if user was ever underage ("Y")
- $u->underage_status($status || 'Y');
- }
-
- # add to statushistory
- if (my $shwhen = shift()) {
- my $text = $on ? "marked" : "unmarked";
- my $status = $u->underage_status;
- LJ::statushistory_add($u, undef, "coppa", "$text; status=$status; when=$shwhen");
- }
-
- # now fire off any hooks that are available
- LJ::run_hooks('set_underage', {
- u => $u,
- on => $on,
- status => $u->underage_status,
- });
-
- # return true if no failures
- return 1;
-}
-
-# return true if we know user is a minor (< 18)
-sub is_minor {
- my $self = shift;
- my $age = $self->best_guess_age;
- return 0 unless $age;
- return 1 if ($age < 18);
- return 0;
-}
-
-# return true if we know user is a child (< 14)
-sub is_child {
- my $self = shift;
- my $age = $self->best_guess_age;
-
- return 0 unless $age;
- return 1 if ($age < 14);
- return 0;
-}
-
-# get/set the gizmo account of a user
-sub gizmo_account {
- my $u = shift;
-
- # parse out their account information
- my $acct = $u->prop( 'gizmo' );
- my ($validated, $gizmo);
- if ($acct && $acct =~ /^([01]);(.+)$/) {
- ($validated, $gizmo) = ($1, $2);
- }
-
- # setting the account
- # all account sets are initially unvalidated
- if (@_) {
- my $newgizmo = shift;
- $u->set_prop( 'gizmo' => "0;$newgizmo" );
-
- # purge old memcache keys
- LJ::MemCache::delete( "gizmo-ljmap:$gizmo" );
- }
-
- # return the information (either account + validation or just account)
- return wantarray ? ($gizmo, $validated) : $gizmo unless @_;
-}
-
-# get/set the validated status of a user's gizmo account
-sub gizmo_account_validated {
- my $u = shift;
-
- my ($gizmo, $validated) = $u->gizmo_account;
-
- if ( defined $_[0] && $_[0] =~ /[01]/) {
- $u->set_prop( 'gizmo' => "$_[0];$gizmo" );
- return $_[0];
- }
-
- return $validated;
-}
-
-# log a line to our userlog
-sub log_event {
- my $u = shift;
-
- my ($type, $info) = @_;
- return undef unless $type;
- $info ||= {};
-
- # now get variables we need; we use delete to remove them from the hash so when we're
- # done we can just encode what's left
- my $ip = delete($info->{ip}) || LJ::get_remote_ip() || undef;
- my $uniq = delete $info->{uniq};
- unless ($uniq) {
- eval {
- $uniq = BML::get_request()->notes->{uniq};
- };
- }
- my $remote = delete($info->{remote}) || LJ::get_remote() || undef;
- my $targetid = (delete($info->{actiontarget})+0) || undef;
- my $extra = %$info ? join('&', map { LJ::eurl($_) . '=' . LJ::eurl($info->{$_}) } keys %$info) : undef;
-
- # now insert the data we have
- $u->do("INSERT INTO userlog (userid, logtime, action, actiontarget, remoteid, ip, uniq, extra) " .
- "VALUES (?, UNIX_TIMESTAMP(), ?, ?, ?, ?, ?, ?)", undef, $u->{userid}, $type,
- $targetid, $remote ? $remote->{userid} : undef, $ip, $uniq, $extra);
- return undef if $u->err;
- return 1;
-}
-
-# return or set the underage status userprop
-sub underage_status {
- return undef unless LJ::class_bit("underage");
-
- my $u = shift;
-
- # return if they aren't setting it
- unless (@_) {
- return $u->prop("underage_status");
- }
-
- # set and return what it got set to
- $u->set_prop('underage_status', shift());
- return $u->{underage_status};
-}
-
-# returns a true value if user has a reserved 'ext' name.
-sub external {
- my $u = shift;
- return $u->{user} =~ /^ext_/;
-}
-
-# this is for debugging/special uses where you need to instruct
-# a user object on what database handle to use. returns the
-# handle that you gave it.
-sub set_dbcm {
- my $u = shift;
- return $u->{'_dbcm'} = shift;
-}
-
-sub nodb_err {
- my $u = shift;
- return "Database handle unavailable (user: " . $u->user . "; cluster: " . $u->clusterid . ")";
-}
-
-sub is_innodb {
- my $u = shift;
- return $LJ::CACHE_CLUSTER_IS_INNO{$u->{clusterid}}
- if defined $LJ::CACHE_CLUSTER_IS_INNO{$u->{clusterid}};
-
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
- my (undef, $ctable) = $dbcm->selectrow_array("SHOW CREATE TABLE log2");
- die "Failed to auto-discover database type for cluster \#$u->{clusterid}: [$ctable]"
- unless $ctable =~ /^CREATE TABLE/;
-
- my $is_inno = ($ctable =~ /=InnoDB/i ? 1 : 0);
- return $LJ::CACHE_CLUSTER_IS_INNO{$u->{clusterid}} = $is_inno;
-}
-
-sub begin_work {
- my $u = shift;
- return 1 unless $u->is_innodb;
-
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
-
- my $rv = $dbcm->begin_work;
- if ($u->{_dberr} = $dbcm->err) {
- $u->{_dberrstr} = $dbcm->errstr;
- }
- return $rv;
-}
-
-sub commit {
- my $u = shift;
- return 1 unless $u->is_innodb;
-
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
-
- my $rv = $dbcm->commit;
- if ($u->{_dberr} = $dbcm->err) {
- $u->{_dberrstr} = $dbcm->errstr;
- }
- return $rv;
-}
-
-sub rollback {
- my $u = shift;
- return 1 unless $u->is_innodb;
-
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
-
- my $rv = $dbcm->rollback;
- if ($u->{_dberr} = $dbcm->err) {
- $u->{_dberrstr} = $dbcm->errstr;
- }
- return $rv;
-}
-
-# get an $sth from the writer
-sub prepare {
- my $u = shift;
-
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
-
- my $rv = $dbcm->prepare(@_);
- if ($u->{_dberr} = $dbcm->err) {
- $u->{_dberrstr} = $dbcm->errstr;
- }
- return $rv;
-}
-
-# $u->do("UPDATE foo SET key=?", undef, $val);
-sub do {
- my $u = shift;
- my $query = shift;
-
- my $uid = $u->{userid}+0
- or croak "Database update called on null user object";
-
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
-
- $query =~ s!^(\s*\w+\s+)!$1/* uid=$uid */ !;
-
- my $rv = $dbcm->do($query, @_);
- if ($u->{_dberr} = $dbcm->err) {
- $u->{_dberrstr} = $dbcm->errstr;
- }
-
- $u->{_mysql_insertid} = $dbcm->{'mysql_insertid'} if $dbcm->{'mysql_insertid'};
-
- return $rv;
-}
-
-sub selectrow_array {
- my $u = shift;
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
-
- my $set_err = sub {
- if ($u->{_dberr} = $dbcm->err) {
- $u->{_dberrstr} = $dbcm->errstr;
- }
- };
-
- if (wantarray()) {
- my @rv = $dbcm->selectrow_array(@_);
- $set_err->();
- return @rv;
- }
-
- my $rv = $dbcm->selectrow_array(@_);
- $set_err->();
- return $rv;
-}
-
-sub selectcol_arrayref {
- my $u = shift;
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
-
- my $rv = $dbcm->selectcol_arrayref(@_);
-
- if ($u->{_dberr} = $dbcm->err) {
- $u->{_dberrstr} = $dbcm->errstr;
- }
-
- return $rv;
-}
-
-
-sub selectall_hashref {
- my $u = shift;
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
-
- my $rv = $dbcm->selectall_hashref(@_);
-
- if ($u->{_dberr} = $dbcm->err) {
- $u->{_dberrstr} = $dbcm->errstr;
- }
-
- return $rv;
-}
-
-sub selectrow_hashref {
- my $u = shift;
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
-
- my $rv = $dbcm->selectrow_hashref(@_);
-
- if ($u->{_dberr} = $dbcm->err) {
- $u->{_dberrstr} = $dbcm->errstr;
- }
-
- return $rv;
-}
-
-sub err {
- my $u = shift;
- return $u->{_dberr};
-}
-
-sub errstr {
- my $u = shift;
- return $u->{_dberrstr};
-}
-
-sub quote {
- my $u = shift;
- my $text = shift;
-
- my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
- or croak $u->nodb_err;
-
- return $dbcm->quote($text);
-}
-
-sub mysql_insertid {
- my $u = shift;
- if ($u->isa("LJ::User")) {
- return $u->{_mysql_insertid};
- } elsif (LJ::isdb($u)) {
- my $db = $u;
- return $db->{'mysql_insertid'};
- } else {
- die "Unknown object '$u' being passed to LJ::User::mysql_insertid.";
- }
-}
-
-# <LJFUNC>
-# name: LJ::User::dudata_set
-# class: logging
-# des: Record or delete disk usage data for a journal.
-# args: u, area, areaid, bytes
-# des-area: One character: "L" for log, "T" for talk, "B" for bio, "P" for pic.
-# des-areaid: Unique ID within $area, or '0' if area has no ids (like bio)
-# des-bytes: Number of bytes item takes up. Or 0 to delete record.
-# returns: 1.
-# </LJFUNC>
-sub dudata_set {
- my ($u, $area, $areaid, $bytes) = @_;
- $bytes += 0; $areaid += 0;
- if ($bytes) {
- $u->do("REPLACE INTO dudata (userid, area, areaid, bytes) ".
- "VALUES (?, ?, $areaid, $bytes)", undef,
- $u->{userid}, $area);
- } else {
- $u->do("DELETE FROM dudata WHERE userid=? AND ".
- "area=? AND areaid=$areaid", undef,
- $u->{userid}, $area);
- }
- return 1;
-}
+
+########################################################################
+### 4. Login, Session, and Rename Functions
+
+
+# returns a new LJ::Session object, or undef on failure
+sub create_session
+{
+ my ($u, %opts) = @_;
+ return LJ::Session->create($u, %opts);
+}
+
+
+#<LJFUNC>
+# name: LJ::User::get_renamed_user
+# des: Get the actual user of a renamed user
+# args: user
+# returns: user
+# </LJFUNC>
+sub get_renamed_user {
+ my $u = shift;
+ my %opts = @_;
+ my $hops = $opts{hops} || 5;
+
+ # Traverse the renames to the final journal
+ if ($u) {
+ while ($u->{'journaltype'} eq 'R' && $hops-- > 0) {
+ my $rt = $u->prop("renamedto");
+ last unless length $rt;
+ $u = LJ::load_user($rt);
+ }
+ }
+
+ return $u;
+}
+
+
+# name: LJ::User->get_timeactive
+# des: retrieve last active time for user from [dbtable[clustertrack2]] or
+# memcache
+sub get_timeactive {
+ my ($u) = @_;
+ my $memkey = [$u->{userid}, "timeactive:$u->{userid}"];
+ my $active;
+ unless (defined($active = LJ::MemCache::get($memkey))) {
+ # TODO: die if unable to get handle? This was left verbatim from
+ # refactored code.
+ my $dbcr = LJ::get_cluster_def_reader($u) or return 0;
+ $active = $dbcr->selectrow_array("SELECT timeactive FROM clustertrack2 ".
+ "WHERE userid=?", undef, $u->{userid});
+ LJ::MemCache::set($memkey, $active, 86400);
+ }
+ return $active;
+}
+
+
+sub kill_all_sessions {
+ my $u = shift
+ or return 0;
+
+ LJ::Session->destroy_all_sessions($u)
+ or return 0;
+
+ # forget this user, if we knew they were logged in
+ if ($LJ::CACHE_REMOTE && $LJ::CACHE_REMOTE->{userid} == $u->{userid}) {
+ LJ::Session->clear_master_cookie;
+ LJ::User->set_remote(undef);
+ }
+
+ return 1;
+}
+
+
+# $u->kill_session(@sessids)
+sub kill_session {
+ my $u = shift
+ or return 0;
+ my $sess = $u->session
+ or return 0;
+
+ $sess->destroy;
+
+ if ($LJ::CACHE_REMOTE && $LJ::CACHE_REMOTE->{userid} == $u->{userid}) {
+ LJ::Session->clear_master_cookie;
+ LJ::User->set_remote(undef);
+ }
+
+ return 1;
+}
+
+
+sub kill_sessions {
+ my $u = shift;
+ return LJ::Session->destroy_sessions($u, @_);
+}
+
+
+sub logout {
+ my $u = shift;
+ if (my $sess = $u->session) {
+ $sess->destroy;
+ }
+ $u->_logout_common;
+}
+
+
+sub logout_all {
+ my $u = shift;
+ LJ::Session->destroy_all_sessions($u)
+ or die "Failed to logout all";
+ $u->_logout_common;
+}
+
sub make_login_session {
my ($u, $exptype, $ipfixed) = @_;
@@ -823,6 +935,7 @@ sub make_login_session {
return 1;
}
+
# We have about 10 million different forms of activity tracking.
# This one is for tracking types of user activity on a per-hour basis
#
@@ -872,6 +985,233 @@ sub note_activity {
return 1;
}
+
+
+sub rate_check {
+ my ($u, $ratename, $count, $opts) = @_;
+ LJ::rate_check($u, $ratename, $count, $opts);
+}
+
+
+sub rate_log {
+ my ($u, $ratename, $count, $opts) = @_;
+ LJ::rate_log($u, $ratename, $count, $opts);
+}
+
+
+sub record_login {
+ my ($u, $sessid) = @_;
+
+ my $too_old = time() - 86400 * 30;
+ $u->do("DELETE FROM loginlog WHERE userid=? AND logintime < ?",
+ undef, $u->{userid}, $too_old);
+
+ my ($ip, $ua);
+ eval {
+ my $r = BML::get_request();
+ $ip = LJ::get_remote_ip();
+ $ua = $r->header_in('User-Agent');
+ };
+
+ return $u->do("INSERT INTO loginlog SET userid=?, sessid=?, logintime=UNIX_TIMESTAMP(), ".
+ "ip=?, ua=?", undef, $u->{userid}, $sessid, $ip, $ua);
+}
+
+
+# my $sess = $u->session (returns current session)
+# my $sess = $u->session($sessid) (returns given session id for user)
+sub session {
+ my ($u, $sessid) = @_;
+ $sessid = $sessid + 0;
+ return $u->{_session} unless $sessid; # should be undef, or LJ::Session hashref
+ return LJ::Session->instance($u, $sessid);
+}
+
+
+# in list context, returns an array of LJ::Session objects which are active.
+# in scalar context, returns hashref of sessid -> LJ::Session, which are active
+sub sessions {
+ my $u = shift;
+ my @sessions = LJ::Session->active_sessions($u);
+ return @sessions if wantarray;
+ my $ret = {};
+ foreach my $s (@sessions) {
+ $ret->{$s->id} = $s;
+ }
+ return $ret;
+}
+
+
+sub _logout_common {
+ my $u = shift;
+ LJ::Session->clear_master_cookie;
+ LJ::User->set_remote(undef);
+ delete $BML::COOKIE{'BMLschemepref'};
+ eval { BML::set_scheme(undef); };
+}
+
+
+########################################################################
+### 5. Database and Memcache Functions
+
+
+sub begin_work {
+ my $u = shift;
+ return 1 unless $u->is_innodb;
+
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+
+ my $rv = $dbcm->begin_work;
+ if ($u->{_dberr} = $dbcm->err) {
+ $u->{_dberrstr} = $dbcm->errstr;
+ }
+ return $rv;
+}
+
+
+sub cache {
+ my ($u, $key) = @_;
+ my $val = $u->selectrow_array("SELECT value FROM userblobcache WHERE userid=? AND bckey=?",
+ undef, $u->{userid}, $key);
+ return undef unless defined $val;
+ if (my $thaw = eval { Storable::thaw($val); }) {
+ return $thaw;
+ }
+ return $val;
+}
+
+
+# front-end to LJ::cmd_buffer_add, which has terrible interface
+# cmd: scalar
+# args: hashref
+sub cmd_buffer_add {
+ my ($u, $cmd, $args) = @_;
+ $args ||= {};
+ return LJ::cmd_buffer_add($u->{clusterid}, $u->{userid}, $cmd, $args);
+}
+
+
+sub commit {
+ my $u = shift;
+ return 1 unless $u->is_innodb;
+
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+
+ my $rv = $dbcm->commit;
+ if ($u->{_dberr} = $dbcm->err) {
+ $u->{_dberrstr} = $dbcm->errstr;
+ }
+ return $rv;
+}
+
+
+# $u->do("UPDATE foo SET key=?", undef, $val);
+sub do {
+ my $u = shift;
+ my $query = shift;
+
+ my $uid = $u->{userid}+0
+ or croak "Database update called on null user object";
+
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+
+ $query =~ s!^(\s*\w+\s+)!$1/* uid=$uid */ !;
+
+ my $rv = $dbcm->do($query, @_);
+ if ($u->{_dberr} = $dbcm->err) {
+ $u->{_dberrstr} = $dbcm->errstr;
+ }
+
+ $u->{_mysql_insertid} = $dbcm->{'mysql_insertid'} if $dbcm->{'mysql_insertid'};
+
+ return $rv;
+}
+
+
+sub dversion {
+ my $u = shift;
+ return $u->{dversion};
+}
+
+
+sub err {
+ my $u = shift;
+ return $u->{_dberr};
+}
+
+
+sub errstr {
+ my $u = shift;
+ return $u->{_dberrstr};
+}
+
+
+sub is_innodb {
+ my $u = shift;
+ return $LJ::CACHE_CLUSTER_IS_INNO{$u->{clusterid}}
+ if defined $LJ::CACHE_CLUSTER_IS_INNO{$u->{clusterid}};
+
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+ my (undef, $ctable) = $dbcm->selectrow_array("SHOW CREATE TABLE log2");
+ die "Failed to auto-discover database type for cluster \#$u->{clusterid}: [$ctable]"
+ unless $ctable =~ /^CREATE TABLE/;
+
+ my $is_inno = ($ctable =~ /=InnoDB/i ? 1 : 0);
+ return $LJ::CACHE_CLUSTER_IS_INNO{$u->{clusterid}} = $is_inno;
+}
+
+
+sub last_transition {
+ my ($u, $what) = @_;
+ croak "invalid user object" unless LJ::isu($u);
+
+ $u->transition_list($what)->[-1];
+}
+
+
+# log2_do
+# see comments for talk2_do
+sub log2_do {
+ my ($u, $errref, $sql, @args) = @_;
+ return undef unless $u->writer;
+
+ my $dbcm = $u->{_dbcm};
+
+ my $memkey = [$u->{'userid'}, "log2lt:$u->{'userid'}"];
+ my $lockkey = $memkey->[1];
+
+ $dbcm->selectrow_array("SELECT GET_LOCK(?,10)", undef, $lockkey);
+ my $ret = $u->do($sql, undef, @args);
+ $$errref = $u->errstr if ref $errref && $u->err;
+ $dbcm->selectrow_array("SELECT RELEASE_LOCK(?)", undef, $lockkey);
+
+ LJ::MemCache::delete($memkey, 0) if int($ret);
+ return $ret;
+}
+
+
+sub mysql_insertid {
+ my $u = shift;
+ if ($u->isa("LJ::User")) {
+ return $u->{_mysql_insertid};
+ } elsif (LJ::isdb($u)) {
+ my $db = $u;
+ return $db->{'mysql_insertid'};
+ } else {
+ die "Unknown object '$u' being passed to LJ::User::mysql_insertid.";
+ }
+}
+
+
+sub nodb_err {
+ my $u = shift;
+ return "Database handle unavailable (user: " . $u->user . "; cluster: " . $u->clusterid . ")";
+}
+
sub note_transition {
my ($u, $what, $from, $to) = @_;
@@ -906,6 +1246,223 @@ sub note_transition {
return 1;
}
+
+# get an $sth from the writer
+sub prepare {
+ my $u = shift;
+
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+
+ my $rv = $dbcm->prepare(@_);
+ if ($u->{_dberr} = $dbcm->err) {
+ $u->{_dberrstr} = $dbcm->errstr;
+ }
+ return $rv;
+}
+
+
+sub quote {
+ my $u = shift;
+ my $text = shift;
+
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+
+ return $dbcm->quote($text);
+}
+
+
+# memcache key that holds the number of times a user performed one of the rate-limited actions
+sub rate_memkey {
+ my ($u, $rp) = @_;
+
+ return [$u->id, "rate:" . $u->id . ":$rp->{id}"];
+}
+
+
+sub readonly {
+ my $u = shift;
+ return LJ::get_cap($u, "readonly");
+}
+
+
+sub rollback {
+ my $u = shift;
+ return 1 unless $u->is_innodb;
+
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+
+ my $rv = $dbcm->rollback;
+ if ($u->{_dberr} = $dbcm->err) {
+ $u->{_dberrstr} = $dbcm->errstr;
+ }
+ return $rv;
+}
+
+
+sub selectall_hashref {
+ my $u = shift;
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+
+ my $rv = $dbcm->selectall_hashref(@_);
+
+ if ($u->{_dberr} = $dbcm->err) {
+ $u->{_dberrstr} = $dbcm->errstr;
+ }
+
+ return $rv;
+}
+
+
+sub selectcol_arrayref {
+ my $u = shift;
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+
+ my $rv = $dbcm->selectcol_arrayref(@_);
+
+ if ($u->{_dberr} = $dbcm->err) {
+ $u->{_dberrstr} = $dbcm->errstr;
+ }
+
+ return $rv;
+}
+
+
+sub selectrow_array {
+ my $u = shift;
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+
+ my $set_err = sub {
+ if ($u->{_dberr} = $dbcm->err) {
+ $u->{_dberrstr} = $dbcm->errstr;
+ }
+ };
+
+ if (wantarray()) {
+ my @rv = $dbcm->selectrow_array(@_);
+ $set_err->();
+ return @rv;
+ }
+
+ my $rv = $dbcm->selectrow_array(@_);
+ $set_err->();
+ return $rv;
+}
+
+
+sub selectrow_hashref {
+ my $u = shift;
+ my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u)
+ or croak $u->nodb_err;
+
+ my $rv = $dbcm->selectrow_hashref(@_);
+
+ if ($u->{_dberr} = $dbcm->err) {
+ $u->{_dberrstr} = $dbcm->errstr;
+ }
+
+ return $rv;
+}
+
+
+# do some internal consistency checks on self. die if problems,
+# else returns 1.
+sub selfassert {
+ my $u = shift;
+ LJ::assert_is($u->{userid}, $u->{_orig_userid})
+ if $u->{_orig_userid};
+ LJ::assert_is($u->{user}, $u->{_orig_user})
+ if $u->{_orig_user};
+ return 1;
+}
+
+
+sub set_cache {
+ my ($u, $key, $value, $expr) = @_;
+ my $now = time();
+ $expr ||= $now + 86400;
+ $expr += $now if $expr < 315532800; # relative to absolute time
+ $value = Storable::nfreeze($value) if ref $value;
+ $u->do("REPLACE INTO userblobcache (userid, bckey, value, timeexpire) VALUES (?,?,?,?)",
+ undef, $u->{userid}, $key, $value, $expr);
+}
+
+
+# this is for debugging/special uses where you need to instruct
+# a user object on what database handle to use. returns the
+# handle that you gave it.
+sub set_dbcm {
+ my $u = shift;
+ return $u->{'_dbcm'} = shift;
+}
+
+
+# class method, returns { clusterid => [ uid, uid ], ... }
+sub split_by_cluster {
+ my $class = shift;
+
+ my @uids = @_;
+ my $us = LJ::load_userids(@uids);
+
+ my %clusters;
+ foreach my $u (values %$us) {
+ next unless $u;
+ push @{$clusters{$u->clusterid}}, $u->id;
+ }
+
+ return \%clusters;
+}
+
+
+# all reads/writes to talk2 must be done inside a lock, so there's
+# no race conditions between reading from db and putting in memcache.
+# can't do a db write in between those 2 steps. the talk2 -> memcache
+# is elsewhere (talklib.pl), but this $dbh->do wrapper is provided
+# here because non-talklib things modify the talk2 table, and it's
+# nice to centralize the locking rules.
+#
+# return value is return of $dbh->do. $errref scalar ref is optional, and
+# if set, gets value of $dbh->errstr
+#
+# write: (LJ::talk2_do)
+# GET_LOCK
+# update/insert into talk2
+# RELEASE_LOCK
+# delete memcache
+#
+# read: (LJ::Talk::get_talk_data)
+# try memcache
+# GET_LOCk
+# read db
+# update memcache
+# RELEASE_LOCK
+
+sub talk2_do {
+ my ($u, $nodetype, $nodeid, $errref, $sql, @args) = @_;
+ return undef unless $nodetype =~ /^\w$/;
+ return undef unless $nodeid =~ /^\d+$/;
+ return undef unless $u->writer;
+
+ my $dbcm = $u->{_dbcm};
+
+ my $memkey = [$u->{'userid'}, "talk2:$u->{'userid'}:$nodetype:$nodeid"];
+ my $lockkey = $memkey->[1];
+
+ $dbcm->selectrow_array("SELECT GET_LOCK(?,10)", undef, $lockkey);
+ my $ret = $u->do($sql, undef, @args);
+ $$errref = $u->errstr if ref $errref && $u->err;
+ $dbcm->selectrow_array("SELECT RELEASE_LOCK(?)", undef, $lockkey);
+
+ LJ::MemCache::delete($memkey, 0) if int($ret);
+ return $ret;
+}
+
+
sub transition_list {
my ($u, $what) = @_;
croak "invalid user object" unless LJ::isu($u);
@@ -933,12 +1490,116 @@ sub transition_list {
return wantarray() ? @list : \@list;
}
-sub last_transition {
- my ($u, $what) = @_;
- croak "invalid user object" unless LJ::isu($u);
-
- $u->transition_list($what)->[-1];
-}
+
+sub uncache_prop {
+ my ($u, $name) = @_;
+ my $prop = LJ::get_prop("user", $name) or die; # FIXME: use exceptions
+ LJ::MemCache::delete([$u->{userid}, "uprop:$u->{userid}:$prop->{id}"]);
+ delete $u->{$name};
+ return 1;
+}
+
+
+# returns self (the $u object which can be used for $u->do) if
+# user is writable, else 0
+sub writer {
+ my $u = shift;
+ return $u if $u->{'_dbcm'} ||= LJ::get_cluster_master($u);
+ return 0;
+}
+
+
+########################################################################
+### 6. What the App Shows to Users
+
+sub can_use_stylealwaysmine {
+ my $u = shift;
+ my $ret = 0;
+
+ return 0 if $LJ::DISABLED{stylealwaysmine};
+ $ret = LJ::run_hook("can_use_stylealwaysmine", $u);
+ return $ret;
+}
+
+
+# format unixtimestamp according to the user's timezone setting
+sub format_time {
+ my $u = shift;
+ my $time = shift;
+
+ return undef unless $time;
+
+ return eval { DateTime->from_epoch(epoch=>$time, time_zone=>$u->prop("timezone"))->ymd('-') } ||
+ DateTime->from_epoch(epoch => $time)->ymd('-');
+}
+
+
+sub has_enabled_getting_started {
+ my $u = shift;
+
+ return $u->opt_getting_started eq 'Y' ? 1 : 0;
+}
+
+
+sub is_in_beta {
+ my ($u, $key) = @_;
+ return LJ::BetaFeatures->user_in_beta( $u => $key );
+}
+
+
+sub opt_stylealwaysmine {
+ my $u = shift;
+
+ return 0 unless $u->can_use_stylealwaysmine;
+ return $u->raw_prop('opt_stylealwaysmine') eq 'Y' ? 1 : 0;
+}
+
+
+# sometimes when the app throws errors, we want to display "nice"
+# text to end-users, while allowing admins to view the actual error message
+sub show_raw_errors {
+ my $u = shift;
+
+ return 1 if $LJ::IS_DEV_SERVER;
+ return 1 if $LJ::ENABLE_BETA_TOOLS;
+
+ return 1 if LJ::check_priv($u, "supporthelp");
+ return 1 if LJ::check_priv($u, "supportviewscreened");
+ return 1 if LJ::check_priv($u, "siteadmin");
+
+ return 0;
+}
+
+
+# returns a DateTime object corresponding to a user's "now"
+sub time_now {
+ my $u = shift;
+
+ my $now = DateTime->now;
+
+ # if user has timezone, use it!
+ my $tz = $u->prop("timezone");
+ return $now unless $tz;
+
+ $now = eval { DateTime->from_epoch(
+ epoch => time(),
+ time_zone => $tz,
+ );
+ };
+
+ return $now;
+}
+
+
+# return the user's timezone based on the prop if it's defined, otherwise best guess
+sub timezone {
+ my $u = shift;
+
+ my $offset = 0;
+ LJ::get_timezone($u, \$offset);
+ return $offset;
+}
+
sub tosagree_set
{
@@ -976,6 +1637,7 @@ sub tosagree_set
return $rv;
}
+
sub tosagree_verify {
my $u = shift;
return 1 unless $LJ::TOS_CHECK;
@@ -987,370 +1649,297 @@ sub tosagree_verify {
return $rev_cur eq $rev_req;
}
-# my $sess = $u->session (returns current session)
-# my $sess = $u->session($sessid) (returns given session id for user)
-
-sub session {
- my ($u, $sessid) = @_;
- $sessid = $sessid + 0;
- return $u->{_session} unless $sessid; # should be undef, or LJ::Session hashref
- return LJ::Session->instance($u, $sessid);
-}
-
-# in list context, returns an array of LJ::Session objects which are active.
-# in scalar context, returns hashref of sessid -> LJ::Session, which are active
-sub sessions {
- my $u = shift;
- my @sessions = LJ::Session->active_sessions($u);
- return @sessions if wantarray;
- my $ret = {};
- foreach my $s (@sessions) {
- $ret->{$s->id} = $s;
- }
- return $ret;
-}
-
-sub logout {
- my $u = shift;
- if (my $sess = $u->session) {
- $sess->destroy;
- }
- $u->_logout_common;
-}
-
-sub logout_all {
- my $u = shift;
- LJ::Session->destroy_all_sessions($u)
- or die "Failed to logout all";
- $u->_logout_common;
-}
-
-sub _logout_common {
- my $u = shift;
- LJ::Session->clear_master_cookie;
- LJ::User->set_remote(undef);
- delete $BML::COOKIE{'BMLschemepref'};
- eval { BML::set_scheme(undef); };
-}
-
-# returns a new LJ::Session object, or undef on failure
-sub create_session
-{
+
+########################################################################
+### 7. Userprops, Caps, and Displaying Content to Others
+
+
+sub add_to_class {
+ my ($u, $class) = @_;
+ my $bit = LJ::class_bit($class);
+ die "unknown class '$class'" unless defined $bit;
+
+ # call add_to_class hook before we modify the
+ # current $u, so it can make inferences from the
+ # old $u caps vs the new we say we'll be adding
+ if (LJ::are_hooks('add_to_class')) {
+ LJ::run_hooks('add_to_class', $u, $class);
+ }
+
+ return LJ::modify_caps($u, [$bit], []);
+}
+
+
+sub caps {
+ my $u = shift;
+ return $u->{caps};
+}
+
+
+sub can_be_text_messaged_by {
+ my ($u, $sender) = @_;
+
+ return 0 unless $u->get_cap("textmessaging");
+
+ my $security = LJ::TextMessage->tm_security($u);
+
+ return 0 if $security eq "none";
+ return 1 if $security eq "all";
+
+ if ($sender) {
+ return 1 if $security eq "reg";
+ return 1 if $security eq "friends" && $u->trusts( $sender );
+ }
+
+ return 0;
+}
+
+
+sub can_show_location {
+ my $u = shift;
+ croak "invalid user object passed" unless LJ::isu($u);
+ my $remote = LJ::get_remote();
+
+ return 0 if $u->underage;
+ return 0 if $u->opt_showlocation eq 'N';
+ return 0 if $u->opt_showlocation eq 'R' && !$remote;
+ return 0 if $u->opt_showlocation eq 'F' && !$u->trusts( $remote );
+ return 1;
+}
+
+
+sub can_show_onlinestatus {
+ my $u = shift;
+ my $remote = shift;
+ croak "invalid user object passed"
+ unless LJ::isu($u);
+
+ # Nobody can see online status of $u
+ return 0 if $u->opt_showonlinestatus eq 'N';
+
+ # Everybody can see online status of $u
+ return 1 if $u->opt_showonlinestatus eq 'Y';
+
+ # Only mutually trusted people of $u can see online status
+ if ($u->opt_showonlinestatus eq 'F') {
+ return 0 unless $remote;
+ return 1 if $u->mutually_trusts( $remote );
+ return 0;
+ }
+ return 0;
+}
+
+
+# <LJFUNC>
+# name: LJ::User::caps_icon
+# des: get the icon for a user's cap.
+# returns: HTML with site-specific cap icon.
+# </LJFUNC>
+sub caps_icon {
+ my $u = shift;
+ return LJ::user_caps_icon($u->{caps});
+}
+
+
+sub clear_prop {
+ my ($u, $prop) = @_;
+ $u->set_prop($prop, undef);
+ return 1;
+}
+
+
+# returns the country specified by the user
+sub country {
+ my $u = shift;
+ return $u->prop('country');
+}
+
+
+# returns the max capability ($cname) for all the classes
+# the user is a member of
+sub get_cap {
+ my ($u, $cname) = @_;
+ return 1 if $LJ::T_HAS_ALL_CAPS;
+ return LJ::get_cap($u, $cname);
+}
+
+
+# get/set the gizmo account of a user
+sub gizmo_account {
+ my $u = shift;
+
+ # parse out their account information
+ my $acct = $u->prop( 'gizmo' );
+ my ($validated, $gizmo);
+ if ($acct && $acct =~ /^([01]);(.+)$/) {
+ ($validated, $gizmo) = ($1, $2);
+ }
+
+ # setting the account
+ # all account sets are initially unvalidated
+ if (@_) {
+ my $newgizmo = shift;
+ $u->set_prop( 'gizmo' => "0;$newgizmo" );
+
+ # purge old memcache keys
+ LJ::MemCache::delete( "gizmo-ljmap:$gizmo" );
+ }
+
+ # return the information (either account + validation or just account)
+ return wantarray ? ($gizmo, $validated) : $gizmo unless @_;
+}
+
+# get/set the validated status of a user's gizmo account
+sub gizmo_account_validated {
+ my $u = shift;
+
+ my ($gizmo, $validated) = $u->gizmo_account;
+
+ if ( defined $_[0] && $_[0] =~ /[01]/) {
+ $u->set_prop( 'gizmo' => "$_[0];$gizmo" );
+ return $_[0];
+ }
+
+ return $validated;
+}
+
+
+# tests to see if a user is in a specific named class. class
+# names are site-specific.
+sub in_any_class {
+ my ($u, @classes) = @_;
+
+ foreach my $class (@classes) {
+ return 1 if LJ::caps_in_group($u->{caps}, $class);
+ }
+
+ return 0;
+}
+
+
+# tests to see if a user is in a specific named class. class
+# names are site-specific.
+sub in_class {
+ my ($u, $class) = @_;
+ return LJ::caps_in_group($u->{caps}, $class);
+}
+
+
+# must be called whenever birthday, location, journal modtime, journaltype, etc.
+# changes. see LJ/Directory/PackedUserRecord.pm
+sub invalidate_directory_record {
+ my $u = shift;
+
+ # Future: ?
+ # LJ::try_our_best_to("invalidate_directory_record", $u->id);
+ # then elsewhere, map that key to subref. if primary run fails,
+ # put in schwartz, then have one worker (misc-deferred) to
+ # redo...
+
+ my $dbs = defined $LJ::USERSEARCH_DB_WRITER ? LJ::get_dbh($LJ::USERSEARCH_DB_WRITER) : LJ::get_db_writer();
+ $dbs->do("UPDATE usersearch_packdata SET good_until=0 WHERE userid=?",
+ undef, $u->id);
+}
+
+
+# <LJFUNC>
+# name: LJ::User::large_journal_icon
+# des: get the large icon by journal type.
+# returns: HTML to display large journal icon.
+# </LJFUNC>
+sub large_journal_icon {
+ my $u = shift;
+ croak "invalid user object"
+ unless LJ::isu($u);
+
+ my $wrap_img = sub {
+ return "<img src='$LJ::IMGPREFIX/$_[0]' border='0' height='24' " .
+ "width='24' style='padding: 0px 2px 0px 0px' />";
+ };
+
+ # hook will return image to use if it cares about
+ # the $u it's been passed
+ my $hook_img = LJ::run_hook("large_journal_icon", $u);
+ return $wrap_img->($hook_img) if $hook_img;
+
+ if ($u->is_comm) {
+ return $wrap_img->("community24x24.gif");
+ }
+
+ if ($u->is_syndicated) {
+ return $wrap_img->("syndicated24x24.gif");
+ }
+
+ if ($u->is_identity) {
+ return $wrap_img->("openid24x24.gif");
+ }
+
+ # personal, news, or unknown fallthrough
+ return $wrap_img->("userinfo24x24.gif");
+}
+
+
+sub opt_showcontact {
+ my $u = shift;
+
+ if ($u->{'allow_contactshow'} =~ /^(N|Y|R|F)$/) {
+ return $u->{'allow_contactshow'};
+ } else {
+ return 'N' if $u->underage || $u->is_child;
+ return 'F' if $u->is_minor;
+ return 'Y';
+ }
+}
+
+
+sub opt_showlocation {
+ my $u = shift;
+ # option not set = "yes", set to N = "no"
+ $u->_lazy_migrate_infoshow;
+
+ # see comments for opt_showbday
+ if ($LJ::DISABLED{infoshow_migrate} && $u->{allow_infoshow} ne ' ') {
+ return $u->{allow_infoshow} eq 'Y' ? undef : 'N';
+ }
+ if ($u->raw_prop('opt_showlocation') =~ /^(N|Y|R|F)$/) {
+ return $u->raw_prop('opt_showlocation');
+ } else {
+ return 'N' if ($u->underage || $u->is_child);
+ return 'F' if ($u->is_minor);
+ return 'Y';
+ }
+}
+
+
+# opt_showonlinestatus options
+# F = Mutually Trusted
+# Y = Everybody
+# N = Nobody
+sub opt_showonlinestatus {
+ my $u = shift;
+
+ if ($u->raw_prop('opt_showonlinestatus') =~ /^(F|N|Y)$/) {
+ return $u->raw_prop('opt_showonlinestatus');
+ } else {
+ return 'F';
+ }
+}
+
+
+sub profile_url {
my ($u, %opts) = @_;
- return LJ::Session->create($u, %opts);
-}
-
-# $u->kill_session(@sessids)
-sub kill_sessions {
- my $u = shift;
- return LJ::Session->destroy_sessions($u, @_);
-}
-
-sub kill_all_sessions {
- my $u = shift
- or return 0;
-
- LJ::Session->destroy_all_sessions($u)
- or return 0;
-
- # forget this user, if we knew they were logged in
- if ($LJ::CACHE_REMOTE && $LJ::CACHE_REMOTE->{userid} == $u->{userid}) {
- LJ::Session->clear_master_cookie;
- LJ::User->set_remote(undef);
- }
-
- return 1;
-}
-
-sub kill_session {
- my $u = shift
- or return 0;
- my $sess = $u->session
- or return 0;
-
- $sess->destroy;
-
- if ($LJ::CACHE_REMOTE && $LJ::CACHE_REMOTE->{userid} == $u->{userid}) {
- LJ::Session->clear_master_cookie;
- LJ::User->set_remote(undef);
- }
-
- return 1;
-}
-
-# <LJFUNC>
-# name: LJ::User::mogfs_userpic_key
-# class: mogilefs
-# des: Make a mogilefs key for the given pic for the user.
-# args: pic
-# des-pic: Either the userpic hash or the picid of the userpic.
-# returns: 1.
-# </LJFUNC>
-sub mogfs_userpic_key {
- my $self = shift or return undef;
- my $pic = shift or croak "missing required arg: userpic";
-
- my $picid = ref $pic ? $pic->{picid} : $pic+0;
- return "up:$self->{userid}:$picid";
-}
-
-# all reads/writes to talk2 must be done inside a lock, so there's
-# no race conditions between reading from db and putting in memcache.
-# can't do a db write in between those 2 steps. the talk2 -> memcache
-# is elsewhere (talklib.pl), but this $dbh->do wrapper is provided
-# here because non-talklib things modify the talk2 table, and it's
-# nice to centralize the locking rules.
-#
-# return value is return of $dbh->do. $errref scalar ref is optional, and
-# if set, gets value of $dbh->errstr
-#
-# write: (LJ::talk2_do)
-# GET_LOCK
-# update/insert into talk2
-# RELEASE_LOCK
-# delete memcache
-#
-# read: (LJ::Talk::get_talk_data)
-# try memcache
-# GET_LOCk
-# read db
-# update memcache
-# RELEASE_LOCK
-
-sub talk2_do {
- my ($u, $nodetype, $nodeid, $errref, $sql, @args) = @_;
- return undef unless $nodetype =~ /^\w$/;
- return undef unless $nodeid =~ /^\d+$/;
- return undef unless $u->writer;
-
- my $dbcm = $u->{_dbcm};
-
- my $memkey = [$u->{'userid'}, "talk2:$u->{'userid'}:$nodetype:$nodeid"];
- my $lockkey = $memkey->[1];
-
- $dbcm->selectrow_array("SELECT GET_LOCK(?,10)", undef, $lockkey);
- my $ret = $u->do($sql, undef, @args);
- $$errref = $u->errstr if ref $errref && $u->err;
- $dbcm->selectrow_array("SELECT RELEASE_LOCK(?)", undef, $lockkey);
-
- LJ::MemCache::delete($memkey, 0) if int($ret);
- return $ret;
-}
-
-# log2_do
-# see comments for talk2_do
-
-sub log2_do {
- my ($u, $errref, $sql, @args) = @_;
- return undef unless $u->writer;
-
- my $dbcm = $u->{_dbcm};
-
- my $memkey = [$u->{'userid'}, "log2lt:$u->{'userid'}"];
- my $lockkey = $memkey->[1];
-
- $dbcm->selectrow_array("SELECT GET_LOCK(?,10)", undef, $lockkey);
- my $ret = $u->do($sql, undef, @args);
- $$errref = $u->errstr if ref $errref && $u->err;
- $dbcm->selectrow_array("SELECT RELEASE_LOCK(?)", undef, $lockkey);
-
- LJ::MemCache::delete($memkey, 0) if int($ret);
- return $ret;
-}
-
-sub url {
- my $u = shift;
my $url;
-
- if ($u->{'journaltype'} eq "I" && ! $u->{url}) {
- my $id = $u->identity;
- if ($id && $id->typeid eq 'O') {
- $url = $id->value;
- $u->set_prop("url", $url) if $url;
- }
- }
-
- # not openid, what does their 'url' prop say?
- $url ||= $u->prop('url');
- return undef unless $url;
-
- $url = "http://$url" unless $url =~ m!^http://!;
-
+ if ($u->{journaltype} eq "I") {
+ $url = "$LJ::SITEROOT/userinfo.bml?userid=$u->{'userid'}&t=I";
+ $url .= "&mode=full" if $opts{full};
+ } else {
+ $url = $u->journal_base . "/profile";
+ $url .= "?mode=full" if $opts{full};
+ }
return $url;
}
-# returns LJ::Identity object
-sub identity {
- my $u = shift;
- return $u->{_identity} if $u->{_identity};
- return undef unless $u->{'journaltype'} eq "I";
-
- my $memkey = [$u->{userid}, "ident:$u->{userid}"];
- my $ident = LJ::MemCache::get($memkey);
- if ($ident) {
- my $i = LJ::Identity->new(
- typeid => $ident->[0],
- value => $ident->[1],
- );
-
- return $u->{_identity} = $i;
- }
-
- my $dbh = LJ::get_db_writer();
- $ident = $dbh->selectrow_arrayref("SELECT idtype, identity FROM identitymap ".
- "WHERE userid=? LIMIT 1", undef, $u->{userid});
- if ($ident) {
- LJ::MemCache::set($memkey, $ident);
- my $i = LJ::Identity->new(
- typeid => $ident->[0],
- value => $ident->[1],
- );
- return $i;
- }
- return undef;
-}
-
-# returns a URL if account is an OpenID identity. undef otherwise.
-sub openid_identity {
- my $u = shift;
- my $ident = $u->identity;
- return undef unless $ident && $ident->typeid eq 'O';
- return $ident->value;
-}
-
-# returns username or identity display name, not escaped
-sub display_name {
- my $u = shift;
- return $u->{'user'} unless $u->{'journaltype'} eq "I";
-
- my $id = $u->identity;
- return "[ERR:unknown_identity]" unless $id;
-
- my ($url, $name);
- if ($id->typeid eq 'O') {
- require Net::OpenID::Consumer;
- $url = $id->value;
- $name = Net::OpenID::VerifiedIdentity::DisplayOfURL($url, $LJ::IS_DEV_SERVER);
- $name = LJ::run_hook("identity_display_name", $name) || $name;
- }
- return $name;
-}
-
-sub ljuser_display {
- my $u = shift;
- my $opts = shift;
-
- return LJ::ljuser($u, $opts) unless $u->{'journaltype'} eq "I";
-
- my $id = $u->identity;
- return "<b>????</b>" unless $id;
-
- my $andfull = $opts->{'full'} ? "&mode=full" : "";
- my $img = $opts->{'imgroot'} || $LJ::IMGPREFIX;
- my $strike = $opts->{'del'} ? ' text-decoration: line-through;' : '';
- my $profile_url = $opts->{'profile_url'} || '';
- my $journal_url = $opts->{'journal_url'} || '';
-
- my ($url, $name);
-
- if ($id->typeid eq 'O') {
- $url = $journal_url ne '' ? $journal_url : $id->value;
- $name = $u->display_name;
-
- $url ||= "about:blank";
- $name ||= "[no_name]";
-
- $url = LJ::ehtml($url);
- $name = LJ::ehtml($name);
-
- my ($imgurl, $width, $height);
- my $head_size = $opts->{head_size};
- if ($head_size) {
- $imgurl = "$img/openid_${head_size}.gif";
- $width = $head_size;
- $height = $head_size;
- } else {
- $imgurl = "$img/openid-profile.gif";
- $width = 16;
- $height = 16;
- }
-
- if (my $site = LJ::ExternalSite->find_matching_site($url)) {
- $imgurl = $site->icon_url;
- }
-
- my $profile = $profile_url ne '' ? $profile_url : "$LJ::SITEROOT/userinfo.bml?userid=$u->{userid}&t=I$andfull";
-
- return "<span class='ljuser' lj:user='$name' style='white-space: nowrap;$strike'><a href='$profile'><img src='$imgurl' alt='[info]' width='$width' height='$height' style='vertical-align: bottom; border: 0; padding-right: 1px;' /></a><a href='$url' rel='nofollow'><b>$name</b></a></span>";
-
- } else {
- return "<b>????</b>";
- }
-}
-
-# class function - load an identity user, but only if they're already known to us
-sub load_existing_identity_user {
- my ($type, $ident) = @_;
-
- my $dbh = LJ::get_db_reader();
- my $uid = $dbh->selectrow_array("SELECT userid FROM identitymap WHERE idtype=? AND identity=?",
- undef, $type, $ident);
- return $uid ? LJ::load_userid($uid) : undef;
-}
-
-# class function - load an identity user, and if we've never seen them before create a user account for them
-sub load_identity_user {
- my ($type, $ident, $vident) = @_;
-
- my $u = load_existing_identity_user($type, $ident);
-
- # If the user is marked as expunged, move identity mapping aside
- # and continue to create new account.
- # Otherwise return user if it exists.
- if ($u) {
- if ($u->is_expunged) {
- return undef unless ($u->rename_identity);
- } else {
- return $u;
- }
- }
-
- # increment ext_ counter until we successfully create an LJ
- # account. hard cap it at 10 tries. (arbitrary, but we really
- # shouldn't have *any* failures here, let alone 10 in a row)
- my $dbh = LJ::get_db_writer();
- my $uid;
-
- for (1..10) {
- my $extuser = 'ext_' . LJ::alloc_global_counter('E');
-
- my $name = $extuser;
- if ($type eq "O" && ref $vident) {
- $name = $vident->display;
- }
-
- $uid = LJ::create_account({
- caps => undef,
- user => $extuser,
- name => $name,
- journaltype => 'I',
- });
- last if $uid;
- select undef, undef, undef, .10; # lets not thrash over this
- }
- return undef unless $uid &&
- $dbh->do("INSERT INTO identitymap (idtype, identity, userid) VALUES (?,?,?)",
- undef, $type, $ident, $uid);
-
- $u = LJ::load_userid($uid);
-
- # record create information
- my $remote = LJ::get_remote();
- $u->log_event('account_create', { remote => $remote });
-
- return $u;
-}
# instance method: returns userprop for a user. currently from cache with no
# way yet to force master.
@@ -1371,12 +1960,101 @@ sub prop {
return $u->raw_prop($prop);
}
+
sub raw_prop {
my ($u, $prop) = @_;
$u->preload_props($prop) unless exists $u->{$_};
return $u->{$prop};
}
+
+sub remove_from_class {
+ my ($u, $class) = @_;
+ my $bit = LJ::class_bit($class);
+ die "unknown class '$class'" unless defined $bit;
+
+ # call remove_from_class hook before we modify the
+ # current $u, so it can make inferences from the
+ # old $u caps vs what we'll be removing
+ if (LJ::are_hooks('remove_from_class')) {
+ LJ::run_hooks('remove_from_class', $u, $class);
+ }
+
+ return LJ::modify_caps($u, [], [$bit]);
+}
+
+
+# sets prop, and also updates $u's cached version
+sub set_prop {
+ my ($u, $prop, $value) = @_;
+ return 0 unless LJ::set_userprop($u, $prop, $value); # FIXME: use exceptions
+ $u->{$prop} = $value;
+}
+
+
+sub share_contactinfo {
+ my ($u, $remote) = @_;
+
+ return 0 if $u->underage || $u->{journaltype} eq "Y";
+ return 0 if $u->opt_showcontact eq 'N';
+ return 0 if $u->opt_showcontact eq 'R' && !$remote;
+ return 0 if $u->opt_showcontact eq 'F' && !$u->trusts( $remote );
+ return 1;
+}
+
+
+sub should_block_robots {
+ my $u = shift;
+
+ return 1 if $u->prop('opt_blockrobots');
+
+ return 0 unless LJ::is_enabled("content_flag");
+
+ my $adult_content = $u->adult_content_calculated;
+ my $admin_flag = $u->admin_content_flag;
+
+ return 1 if $LJ::CONTENT_FLAGS{$adult_content} && $LJ::CONTENT_FLAGS{$adult_content}->{block_robots};
+ return 1 if $LJ::CONTENT_FLAGS{$admin_flag} && $LJ::CONTENT_FLAGS{$admin_flag}->{block_robots};
+ return 0;
+}
+
+
+sub support_points_count {
+ my $u = shift;
+
+ my $dbr = LJ::get_db_reader();
+ my $userid = $u->id;
+ my $count;
+
+ $count = $u->{_supportpointsum};
+ return $count if defined $count;
+
+ my $memkey = [$userid, "supportpointsum:$userid"];
+ $count = LJ::MemCache::get($memkey);
+ if (defined $count) {
+ $u->{_supportpointsum} = $count;
+ return $count;
+ }
+
+ $count = $dbr->selectrow_array("SELECT totpoints FROM supportpointsum WHERE userid=?", undef, $userid) || 0;
+ $u->{_supportpointsum} = $count;
+ LJ::MemCache::set($memkey, $count, 60*5);
+
+ return $count;
+}
+
+
+sub should_show_schools_to {
+ my ($u, $targetu) = @_;
+
+ return 0 unless LJ::is_enabled("schools");
+ return 1 if $u->{'opt_showschools'} eq '' || $u->{'opt_showschools'} eq 'Y';
+ return 1 if $u->{'opt_showschools'} eq 'F' && $u->trusts( $targetu );
+
+ return 0;
+}
+
+
sub _lazy_migrate_infoshow {
my ($u) = @_;
return 1 if $LJ::DISABLED{infoshow_migrate};
@@ -1403,6 +2081,562 @@ sub _lazy_migrate_infoshow {
return 1;
}
+
+
+########################################################################
+### 8. Formatting Content Shown to Users
+
+sub ajax_auth_token {
+ my $u = shift;
+ return LJ::Auth->ajax_auth_token($u, @_);
+}
+
+
+sub bio {
+ my $u = shift;
+ return LJ::get_bio($u);
+}
+
+
+sub check_ajax_auth_token {
+ my $u = shift;
+ return LJ::Auth->check_ajax_auth_token($u, @_);
+}
+
+
+sub clusterid {
+ return $_[0]->{clusterid};
+}
+
+
+# returns username or identity display name, not escaped
+sub display_name {
+ my $u = shift;
+ return $u->{'user'} unless $u->{'journaltype'} eq "I";
+
+ my $id = $u->identity;
+ return "[ERR:unknown_identity]" unless $id;
+
+ my ($url, $name);
+ if ($id->typeid eq 'O') {
+ require Net::OpenID::Consumer;
+ $url = $id->value;
+ $name = Net::OpenID::VerifiedIdentity::DisplayOfURL($url, $LJ::IS_DEV_SERVER);
+ $name = LJ::run_hook("identity_display_name", $name) || $name;
+ }
+ return $name;
+}
+
+
+# returns username for display
+sub display_username {
+ my $u = shift;
+ return $u->display_name if $u->is_identity;
+ return $u->{user};
+}
+
+
+sub equals {
+ my ($u, $target) = @_;
+
+ return LJ::u_equals($u, $target);
+}
+
+
+# userid
+*userid = \&id;
+sub id {
+ return $_[0]->{userid};
+}
+
+
+sub ljuser_display {
+ my $u = shift;
+ my $opts = shift;
+
+ return LJ::ljuser($u, $opts) unless $u->{'journaltype'} eq "I";
+
+ my $id = $u->identity;
+ return "<b>????</b>" unless $id;
+
+ my $andfull = $opts->{'full'} ? "&mode=full" : "";
+ my $img = $opts->{'imgroot'} || $LJ::IMGPREFIX;
+ my $strike = $opts->{'del'} ? ' text-decoration: line-through;' : '';
+ my $profile_url = $opts->{'profile_url'} || '';
+ my $journal_url = $opts->{'journal_url'} || '';
+
+ my ($url, $name);
+
+ if ($id->typeid eq 'O') {
+ $url = $journal_url ne '' ? $journal_url : $id->value;
+ $name = $u->display_name;
+
+ $url ||= "about:blank";
+ $name ||= "[no_name]";
+
+ $url = LJ::ehtml($url);
+ $name = LJ::ehtml($name);
+
+ my ($imgurl, $width, $height);
+ my $head_size = $opts->{head_size};
+ if ($head_size) {
+ $imgurl = "$img/openid_${head_size}.gif";
+ $width = $head_size;
+ $height = $head_size;
+ } else {
+ $imgurl = "$img/openid-profile.gif";
+ $width = 16;
+ $height = 16;
+ }
+
+ if (my $site = LJ::ExternalSite->find_matching_site($url)) {
+ $imgurl = $site->icon_url;
+ }
+
+ my $profile = $profile_url ne '' ? $profile_url : "$LJ::SITEROOT/userinfo.bml?userid=$u->{userid}&t=I$andfull";
+
+ return "<span class='ljuser' lj:user='$name' style='white-space: nowrap;$strike'><a href='$profile'><img src='$imgurl' alt='[info]' width='$width' height='$height' style='vertical-align: bottom; border: 0; padding-right: 1px;' /></a><a href='$url' rel='nofollow'><b>$name</b></a></span>";
+
+ } else {
+ return "<b>????</b>";
+ }
+}
+
+
+# returns the user-specified name of a journal in valid UTF-8
+# and with HTML escaped
+sub name_html {
+ my $u = shift;
+ return LJ::ehtml($u->name_raw);
+}
+
+
+# returns the user-specified name of a journal exactly as entered
+sub name_orig {
+ my $u = shift;
+ return $u->{name};
+}
+
+
+# returns the user-specified name of a journal in valid UTF-8
+sub name_raw {
+ my $u = shift;
+ LJ::text_out(\$u->{name});
+ return $u->{name};
+}
+
+
+sub new_from_row {
+ my ($class, $row) = @_;
+ my $u = bless $row, $class;
+
+ # for selfassert method below:
+ $u->{_orig_userid} = $u->{userid};
+ $u->{_orig_user} = $u->{user};
+
+ return $u;
+}
+
+
+sub new_from_url {
+ my ($class, $url) = @_;
+
+ # this doesn't seem to like URLs with ?...
+ $url =~ s/\?.+$//;
+
+ # /users, /community, or /~
+ if ($url =~ m!^\Q$LJ::SITEROOT\E/(?:users/|community/|~)([\w-]+)/?!) {
+ return LJ::load_user($1);
+ }
+
+ # user subdomains
+ if ($LJ::USER_DOMAIN && $url =~ m!^http://([\w-]+)\.\Q$LJ::USER_DOMAIN\E/?$!) {
+ return LJ::load_user($1);
+ }
+
+ # subdomains that hold a bunch of users (eg, users.siteroot.com/username/)
+ if ($url =~ m!^http://\w+\.\Q$LJ::USER_DOMAIN\E/([\w-]+)/?$!) {
+ return LJ::load_user($1);
+ }
+
+ return undef;
+}
+
+
+sub url {
+ my $u = shift;
+
+ my $url;
+
+ if ($u->{'journaltype'} eq "I" && ! $u->{url}) {
+ my $id = $u->identity;
+ if ($id && $id->typeid eq 'O') {
+ $url = $id->value;
+ $u->set_prop("url", $url) if $url;
+ }
+ }
+
+ # not openid, what does their 'url' prop say?
+ $url ||= $u->prop('url');
+ return undef unless $url;
+
+ $url = "http://$url" unless $url =~ m!^http://!;
+
+ return $url;
+}
+
+
+# returns username
+*username = \&user;
+sub user {
+ my $u = shift;
+ return $u->{user};
+}
+
+
+sub user_url_arg {
+ my $u = shift;
+ return "I,$u->{userid}" if $u->{journaltype} eq "I";
+ return $u->{user};
+}
+
+
+# if bio_absent is set to "yes", bio won't be updated
+sub set_bio {
+ my ($u, $text, $bio_absent) = @_;
+ $bio_absent = "" unless $bio_absent;
+
+ my $oldbio = $u->bio;
+ my $newbio = $bio_absent eq "yes" ? $oldbio : $text;
+ my $has_bio = ($newbio =~ /\S/) ? "Y" : "N";
+
+ my %update = (
+ 'has_bio' => $has_bio,
+ );
+ LJ::update_user($u, \%update);
+
+ # update their bio text
+ if (($oldbio ne $text) && $bio_absent ne "yes") {
+ if ($has_bio eq "N") {
+ $u->do("DELETE FROM userbio WHERE userid=?", undef, $u->id);
+ $u->dudata_set('B', 0, 0);
+ } else {
+ $u->do("REPLACE INTO userbio (userid, bio) VALUES (?, ?)",
+ undef, $u->id, $text);
+ $u->dudata_set('B', 0, length($text));
+ }
+ LJ::MemCache::set([$u->id, "bio:" . $u->id], $text);
+ }
+}
+
+
+########################################################################
+### 9. Logging and Recording Actions
+
+
+# <LJFUNC>
+# name: LJ::User::dudata_set
+# class: logging
+# des: Record or delete disk usage data for a journal.
+# args: u, area, areaid, bytes
+# des-area: One character: "L" for log, "T" for talk, "B" for bio, "P" for pic.
+# des-areaid: Unique ID within $area, or '0' if area has no ids (like bio)
+# des-bytes: Number of bytes item takes up. Or 0 to delete record.
+# returns: 1.
+# </LJFUNC>
+sub dudata_set {
+ my ($u, $area, $areaid, $bytes) = @_;
+ $bytes += 0; $areaid += 0;
+ if ($bytes) {
+ $u->do("REPLACE INTO dudata (userid, area, areaid, bytes) ".
+ "VALUES (?, ?, $areaid, $bytes)", undef,
+ $u->{userid}, $area);
+ } else {
+ $u->do("DELETE FROM dudata WHERE userid=? AND ".
+ "area=? AND areaid=$areaid", undef,
+ $u->{userid}, $area);
+ }
+ return 1;
+}
+
+
+# log a line to our userlog
+sub log_event {
+ my $u = shift;
+
+ my ($type, $info) = @_;
+ return undef unless $type;
+ $info ||= {};
+
+ # now get variables we need; we use delete to remove them from the hash so when we're
+ # done we can just encode what's left
+ my $ip = delete($info->{ip}) || LJ::get_remote_ip() || undef;
+ my $uniq = delete $info->{uniq};
+ unless ($uniq) {
+ eval {
+ $uniq = BML::get_request()->notes->{uniq};
+ };
+ }
+ my $remote = delete($info->{remote}) || LJ::get_remote() || undef;
+ my $targetid = (delete($info->{actiontarget})+0) || undef;
+ my $extra = %$info ? join('&', map { LJ::eurl($_) . '=' . LJ::eurl($info->{$_}) } keys %$info) : undef;
+
+ # now insert the data we have
+ $u->do("INSERT INTO userlog (userid, logtime, action, actiontarget, remoteid, ip, uniq, extra) " .
+ "VALUES (?, UNIX_TIMESTAMP(), ?, ?, ?, ?, ?, ?)", undef, $u->{userid}, $type,
+ $targetid, $remote ? $remote->{userid} : undef, $ip, $uniq, $extra);
+ return undef if $u->err;
+ return 1;
+}
+
+
+########################################################################
+### 10. Banning-Related Functions
+
+
+sub ban_user {
+ my ($u, $ban_u) = @_;
+
+ my $remote = LJ::get_remote();
+ $u->log_event('ban_set', { actiontarget => $ban_u->id, remote => $remote });
+
+ return LJ::set_rel($u->id, $ban_u->id, 'B');
+}
+
+
+sub ban_user_multi {
+ my ($u, @banlist) = @_;
+
+ LJ::set_rel_multi(map { [$u->id, $_, 'B'] } @banlist);
+
+ my $us = LJ::load_userids(@banlist);
+ foreach my $banuid (@banlist) {
+ $u->log_event('ban_set', { actiontarget => $banuid, remote => LJ::get_remote() });
+ LJ::run_hooks('ban_set', $u, $us->{$banuid}) if $us->{$banuid};
+ }
+
+ return 1;
+}
+
+
+# return if $target is banned from $u's journal
+*has_banned = \&is_banned;
+sub is_banned {
+ my ($u, $target) = @_;
+ return LJ::is_banned($target->userid, $u->userid);
+}
+
+
+sub unban_user_multi {
+ my ($u, @unbanlist) = @_;
+
+ LJ::clear_rel_multi(map { [$u->id, $_, 'B'] } @unbanlist);
+
+ my $us = LJ::load_userids(@unbanlist);
+ foreach my $banuid (@unbanlist) {
+ $u->log_event('ban_unset', { actiontarget => $banuid, remote => LJ::get_remote() });
+ LJ::run_hooks('ban_unset', $u, $us->{$banuid}) if $us->{$banuid};
+ }
+
+ return 1;
+}
+
+
+########################################################################
+### 11. Birthdays and Age-Related Functions
+### FIXME: Some of these may be outdated when we remove under-13 accounts.
+
+
+
+# Users age based off their profile birthdate
+sub age {
+ my $u = shift;
+ croak "Invalid user object" unless LJ::isu($u);
+
+ my $bdate = $u->{bdate};
+ return unless length $bdate;
+
+ my ($year, $mon, $day) = $bdate =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)/;
+ my $age = LJ::calc_age($year, $mon, $day);
+ return $age if $age > 0;
+ return;
+}
+
+
+# This will format the birthdate based on the user prop
+sub bday_string {
+ my $u = shift;
+ croak "invalid user object passed" unless LJ::isu($u);
+ return 0 if $u->underage;
+
+ my $bdate = $u->{'bdate'};
+ my ($year,$mon,$day) = split(/-/, $bdate);
+ my $bday_string = '';
+
+ if ($u->can_show_full_bday && $day > 0 && $mon > 0 && $year > 0) {
+ $bday_string = $bdate;
+ } elsif ($u->can_show_bday && $day > 0 && $mon > 0) {
+ $bday_string = "$mon-$day";
+ } elsif ($u->can_show_bday_year && $year > 0) {
+ $bday_string = $year;
+ }
+ $bday_string =~ s/^0000-//;
+ return $bday_string;
+}
+
+
+# Returns the best guess age of the user, which is init_age if it exists, otherwise age
+sub best_guess_age {
+ my $u = shift;
+ return 0 unless $u->is_person || $u->is_identity;
+ return $u->init_age || $u->age;
+}
+
+
+# returns if this user can join an adult community or not
+# adultref will hold the value of the community's adult content flag
+sub can_join_adult_comm {
+ my ($u, %opts) = @_;
+
+ return 1 unless LJ::is_enabled('content_flag');
+
+ my $adultref = $opts{adultref};
+ my $comm = $opts{comm} or croak "No community passed";
+
+ my $adult_content = $comm->adult_content_calculated;
+ $$adultref = $adult_content;
+
+ if ($adult_content eq "concepts" && ($u->is_child || !$u->best_guess_age)) {
+ return 0;
+ } elsif ($adult_content eq "explicit" && ($u->is_minor || !$u->best_guess_age)) {
+ return 0;
+ }
+
+ return 1;
+}
+
+
+# Birthday logic -- can any of the birthday info be shown
+# This will return true if any birthday info can be shown
+sub can_share_bday {
+ my $u = shift;
+ croak "invalid user object passed" unless LJ::isu($u);
+
+ my %opts = @_;
+ my $with_u = $opts{with} || LJ::get_remote();
+
+ return 0 if $u->opt_sharebday eq 'N';
+ return 0 if $u->opt_sharebday eq 'R' && !$with_u;
+ return 0 if $u->opt_sharebday eq 'F' && !$u->trusts( $with_u );
+ return 1;
+}
+
+
+# Birthday logic -- show appropriate string based on opt_showbday
+# This will return true if the actual birthday can be shown
+sub can_show_bday {
+ my ($u, %opts) = @_;
+ croak "invalid user object passed" unless LJ::isu($u);
+
+ my $to_u = $opts{to} || LJ::get_remote();
+
+ return 0 unless $u->can_share_bday( with => $to_u );
+ return 0 unless $u->opt_showbday eq 'D' || $u->opt_showbday eq 'F';
+ return 1;
+}
+
+
+# This will return true if the actual birth year can be shown
+sub can_show_bday_year {
+ my $u = shift;
+ croak "invalid user object passed" unless LJ::isu($u);
+
+ my %opts = @_;
+ my $to_u = $opts{to} || LJ::get_remote();
+
+ return 0 unless $u->can_share_bday( with => $to_u );
+ return 0 unless $u->opt_showbday eq 'Y' || $u->opt_showbday eq 'F';
+ return 1;
+}
+
+
+# This will return true if month, day, and year can be shown
+sub can_show_full_bday {
+ my $u = shift;
+ croak "invalid user object passed" unless LJ::isu($u);
+
+ my %opts = @_;
+ my $to_u = $opts{to} || LJ::get_remote();
+
+ return 0 unless $u->can_share_bday( with => $to_u );
+ return 0 unless $u->opt_showbday eq 'F';
+ return 1;
+}
+
+
+sub include_in_age_search {
+ my $u = shift;
+
+ # if they don't display the year
+ return 0 if $u->opt_showbday =~ /^[DN]$/;
+
+ # if it's not visible to registered users
+ return 0 if $u->opt_sharebday =~ /^[NF]$/;
+
+ return 1;
+}
+
+
+# This returns the users age based on the init_bdate (users coppa validation birthdate)
+sub init_age {
+ my $u = shift;
+ croak "Invalid user object" unless LJ::isu($u);
+
+ my $init_bdate = $u->prop('init_bdate');
+ return unless $init_bdate;
+
+ my ($year, $mon, $day) = $init_bdate =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)/;
+ my $age = LJ::calc_age($year, $mon, $day);
+ return $age if $age > 0;
+ return;
+}
+
+
+sub next_birthday {
+ my $u = shift;
+ return if $u->is_expunged;
+
+ return $u->selectrow_array("SELECT nextbirthday FROM birthdays " .
+ "WHERE userid = ?", undef, $u->id)+0;
+}
+
+
+# class method, loads next birthdays for a bunch of users
+sub next_birthdays {
+ my $class = shift;
+
+ # load the users we need, so we can get their clusters
+ my $clusters = LJ::User->split_by_cluster(@_);
+
+ my %bdays = ();
+ foreach my $cid (keys %$clusters) {
+ next unless $cid;
+
+ my @users = @{$clusters->{$cid} || []};
+ my $dbcr = LJ::get_cluster_def_reader($cid)
+ or die "Unable to load reader for cluster: $cid";
+
+ my $bind = join(",", map { "?" } @users);
+ my $sth = $dbcr->prepare("SELECT * FROM birthdays WHERE userid IN ($bind)");
+ $sth->execute(@users);
+ while (my $row = $sth->fetchrow_hashref) {
+ $bdays{$row->{userid}} = $row->{nextbirthday};
+ }
+ }
+
+ return \%bdays;
+}
+
# opt_showbday options
# F - Full Display of Birthday
@@ -1429,6 +2663,7 @@ sub opt_showbday {
}
}
+
# opt_sharebday options
# A - All people
# R - Registered Users
@@ -1444,335 +2679,6 @@ sub opt_sharebday {
return 'F' if $u->is_minor;
return 'A';
}
-}
-
-# opt_showljtalk options based on user setting
-# Y = Show the LJ Talk field on profile (default)
-# N = Don't show the LJ Talk field on profile
-sub opt_showljtalk {
- my $u = shift;
-
- # Check for valid value, or just return default of 'Y'.
- if ($u->raw_prop('opt_showljtalk') =~ /^(Y|N)$/) {
- return $u->raw_prop('opt_showljtalk');
- } else {
- return 'Y';
- }
-}
-
-# Show LJ Talk field on profile? opt_showljtalk needs a value of 'Y'.
-sub show_ljtalk {
- my $u = shift;
- croak "Invalid user object passed" unless LJ::isu($u);
-
- # Fail if the user wants to hide the LJ Talk field on their profile,
- # or doesn't even have the ability to show it.
- return 0 if $u->opt_showljtalk eq 'N' || $LJ::DISABLED{'ljtalk'} || !$u->is_person;
-
- # User either decided to show LJ Talk field or has left it at the default.
- return 1 if $u->opt_showljtalk eq 'Y';
-}
-
-# Hide the LJ Talk field on profile? opt_showljtalk needs a value of 'N'.
-sub hide_ljtalk {
- my $u = shift;
- croak "Invalid user object passed" unless LJ::isu($u);
-
- # ... The opposite of showing the field. :)
- return $u->show_ljtalk ? 0 : 1;
-}
-
-sub ljtalk_id {
- my $u = shift;
- croak "Invalid user object passed" unless LJ::isu($u);
-
- return $u->{'user'}.'@'.$LJ::USER_DOMAIN;
-}
-
-sub opt_showlocation {
- my $u = shift;
- # option not set = "yes", set to N = "no"
- $u->_lazy_migrate_infoshow;
-
- # see comments for opt_showbday
- if ($LJ::DISABLED{infoshow_migrate} && $u->{allow_infoshow} ne ' ') {
- return $u->{allow_infoshow} eq 'Y' ? undef : 'N';
- }
- if ($u->raw_prop('opt_showlocation') =~ /^(N|Y|R|F)$/) {
- return $u->raw_prop('opt_showlocation');
- } else {
- return 'N' if ($u->underage || $u->is_child);
- return 'F' if ($u->is_minor);
- return 'Y';
- }
-}
-
-sub opt_showcontact {
- my $u = shift;
-
- if ($u->{'allow_contactshow'} =~ /^(N|Y|R|F)$/) {
- return $u->{'allow_contactshow'};
- } else {
- return 'N' if $u->underage || $u->is_child;
- return 'F' if $u->is_minor;
- return 'Y';
- }
-}
-
-# opt_showonlinestatus options
-# F = Mutually Trusted
-# Y = Everybody
-# N = Nobody
-sub opt_showonlinestatus {
- my $u = shift;
-
- if ($u->raw_prop('opt_showonlinestatus') =~ /^(F|N|Y)$/) {
- return $u->raw_prop('opt_showonlinestatus');
- } else {
- return 'F';
- }
-}
-
-sub can_show_location {
- my $u = shift;
- croak "invalid user object passed" unless LJ::isu($u);
- my $remote = LJ::get_remote();
-
- return 0 if $u->underage;
- return 0 if $u->opt_showlocation eq 'N';
- return 0 if $u->opt_showlocation eq 'R' && !$remote;
- return 0 if $u->opt_showlocation eq 'F' && !$u->trusts( $remote );
- return 1;
-}
-
-sub can_show_onlinestatus {
- my $u = shift;
- my $remote = shift;
- croak "invalid user object passed"
- unless LJ::isu($u);
-
- # Nobody can see online status of $u
- return 0 if $u->opt_showonlinestatus eq 'N';
-
- # Everybody can see online status of $u
- return 1 if $u->opt_showonlinestatus eq 'Y';
-
- # Only mutually trusted people of $u can see online status
- if ($u->opt_showonlinestatus eq 'F') {
- return 0 unless $remote;
- return 1 if $u->mutually_trusts( $remote );
- return 0;
- }
- return 0;
-}
-
-# return the setting indicating how a user can be found by their email address
-# Y - Findable, N - Not findable, H - Findable but identity hidden
-sub opt_findbyemail {
- my $u = shift;
-
- if ($u->raw_prop('opt_findbyemail') =~ /^(N|Y|H)$/) {
- return $u->raw_prop('opt_findbyemail');
- } else {
- return undef;
- }
-}
-
-# return user selected mail encoding or undef
-sub mailencoding {
- my $u = shift;
- my $enc = $u->prop('mailencoding');
-
- return undef unless $enc;
-
- LJ::load_codes({ "encoding" => \%LJ::CACHE_ENCODINGS } )
- unless %LJ::CACHE_ENCODINGS;
- return $LJ::CACHE_ENCODINGS{$enc}
-}
-
-# Birthday logic -- show appropriate string based on opt_showbday
-# This will return true if the actual birthday can be shown
-sub can_show_bday {
- my ($u, %opts) = @_;
- croak "invalid user object passed" unless LJ::isu($u);
-
- my $to_u = $opts{to} || LJ::get_remote();
-
- return 0 unless $u->can_share_bday( with => $to_u );
- return 0 unless $u->opt_showbday eq 'D' || $u->opt_showbday eq 'F';
- return 1;
-}
-
-# Birthday logic -- can any of the birthday info be shown
-# This will return true if any birthday info can be shown
-sub can_share_bday {
- my $u = shift;
- croak "invalid user object passed" unless LJ::isu($u);
-
- my %opts = @_;
- my $with_u = $opts{with} || LJ::get_remote();
-
- return 0 if $u->opt_sharebday eq 'N';
- return 0 if $u->opt_sharebday eq 'R' && !$with_u;
- return 0 if $u->opt_sharebday eq 'F' && !$u->trusts( $with_u );
- return 1;
-}
-
-
-# This will return true if the actual birth year can be shown
-sub can_show_bday_year {
- my $u = shift;
- croak "invalid user object passed" unless LJ::isu($u);
-
- my %opts = @_;
- my $to_u = $opts{to} || LJ::get_remote();
-
- return 0 unless $u->can_share_bday( with => $to_u );
- return 0 unless $u->opt_showbday eq 'Y' || $u->opt_showbday eq 'F';
- return 1;
-}
-
-# This will return true if month, day, and year can be shown
-sub can_show_full_bday {
- my $u = shift;
- croak "invalid user object passed" unless LJ::isu($u);
-
- my %opts = @_;
- my $to_u = $opts{to} || LJ::get_remote();
-
- return 0 unless $u->can_share_bday( with => $to_u );
- return 0 unless $u->opt_showbday eq 'F';
- return 1;
-}
-
-# This will format the birthdate based on the user prop
-sub bday_string {
- my $u = shift;
- croak "invalid user object passed" unless LJ::isu($u);
- return 0 if $u->underage;
-
- my $bdate = $u->{'bdate'};
- my ($year,$mon,$day) = split(/-/, $bdate);
- my $bday_string = '';
-
- if ($u->can_show_full_bday && $day > 0 && $mon > 0 && $year > 0) {
- $bday_string = $bdate;
- } elsif ($u->can_show_bday && $day > 0 && $mon > 0) {
- $bday_string = "$mon-$day";
- } elsif ($u->can_show_bday_year && $year > 0) {
- $bday_string = $year;
- }
- $bday_string =~ s/^0000-//;
- return $bday_string;
-}
-
-# Users age based off their profile birthdate
-sub age {
- my $u = shift;
- croak "Invalid user object" unless LJ::isu($u);
-
- my $bdate = $u->{bdate};
- return unless length $bdate;
-
- my ($year, $mon, $day) = $bdate =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)/;
- my $age = LJ::calc_age($year, $mon, $day);
- return $age if $age > 0;
- return;
-}
-
-sub age_for_adcall {
- my $u = shift;
- croak "Invalid user object" unless LJ::isu($u);
-
- return undef if $u->underage;
- return eval {$u->age || $u->init_age};
-}
-
-# This returns the users age based on the init_bdate (users coppa validation birthdate)
-sub init_age {
- my $u = shift;
- croak "Invalid user object" unless LJ::isu($u);
-
- my $init_bdate = $u->prop('init_bdate');
- return unless $init_bdate;
-
- my ($year, $mon, $day) = $init_bdate =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)/;
- my $age = LJ::calc_age($year, $mon, $day);
- return $age if $age > 0;
- return;
-}
-
-# Returns the best guess age of the user, which is init_age if it exists, otherwise age
-sub best_guess_age {
- my $u = shift;
- return 0 unless $u->is_person || $u->is_identity;
- return $u->init_age || $u->age;
-}
-
-sub gender_for_adcall {
- my $u = shift;
- croak "Invalid user object" unless LJ::isu($u);
-
- my $gender = $u->prop('gender');
- if ($gender && $gender !~ /^[UO]/i) {
- return uc(substr($gender, 0, 1)); # M|F
- }
-
- return "unspecified";
-}
-
-sub should_fire_birthday_notif {
- my $u = shift;
-
- return 0 unless $u->is_person;
- return 0 unless $u->is_visible;
-
- # if the month/day can't be shown
- return 0 if $u->opt_showbday =~ /^[YN]$/;
-
- # if the birthday isn't shown to anyone
- return 0 if $u->opt_sharebday eq "N";
-
- # note: this isn't intended to capture all cases where birthday
- # info is restricted. we want to pare out as much as possible;
- # individual "can user X see this birthday" is handled in
- # LJ::Event::Birthday->matches_filter
-
- return 1;
-}
-
-sub next_birthday {
- my $u = shift;
- return if $u->is_expunged;
-
- return $u->selectrow_array("SELECT nextbirthday FROM birthdays " .
- "WHERE userid = ?", undef, $u->id)+0;
-}
-
-# class method, loads next birthdays for a bunch of users
-sub next_birthdays {
- my $class = shift;
-
- # load the users we need, so we can get their clusters
- my $clusters = LJ::User->split_by_cluster(@_);
-
- my %bdays = ();
- foreach my $cid (keys %$clusters) {
- next unless $cid;
-
- my @users = @{$clusters->{$cid} || []};
- my $dbcr = LJ::get_cluster_def_reader($cid)
- or die "Unable to load reader for cluster: $cid";
-
- my $bind = join(",", map { "?" } @users);
- my $sth = $dbcr->prepare("SELECT * FROM birthdays WHERE userid IN ($bind)");
- $sth->execute(@users);
- while (my $row = $sth->fetchrow_hashref) {
- $bdays{$row->{userid}} = $row->{nextbirthday};
- }
- }
-
- return \%bdays;
}
@@ -1830,14 +2736,22 @@ sub set_next_birthday {
}
-sub include_in_age_search {
- my $u = shift;
-
- # if they don't display the year
- return 0 if $u->opt_showbday =~ /^[DN]$/;
-
- # if it's not visible to registered users
- return 0 if $u->opt_sharebday =~ /^[NF]$/;
+sub should_fire_birthday_notif {
+ my $u = shift;
+
+ return 0 unless $u->is_person;
+ return 0 unless $u->is_visible;
+
+ # if the month/day can't be shown
+ return 0 if $u->opt_showbday =~ /^[YN]$/;
+
+ # if the birthday isn't shown to anyone
+ return 0 if $u->opt_sharebday eq "N";
+
+ # note: this isn't intended to capture all cases where birthday
+ # info is restricted. we want to pare out as much as possible;
+ # individual "can user X see this birthday" is handled in
+ # LJ::Event::Birthday->matches_filter
return 1;
}
@@ -1859,138 +2773,9 @@ sub usersearch_age_with_expire {
return ($age, $expire);
}
-# returns the country specified by the user
-sub country {
- my $u = shift;
- return $u->prop('country');
-}
-
-# sets prop, and also updates $u's cached version
-sub set_prop {
- my ($u, $prop, $value) = @_;
- return 0 unless LJ::set_userprop($u, $prop, $value); # FIXME: use exceptions
- $u->{$prop} = $value;
-}
-
-sub clear_prop {
- my ($u, $prop) = @_;
- $u->set_prop($prop, undef);
- return 1;
-}
-
-sub journal_base {
- my $u = shift;
- return LJ::journal_base($u);
-}
-
-sub allpics_base {
- my $u = shift;
- return "$LJ::SITEROOT/allpics.bml?user=" . $u->user;
-}
-
-sub get_userpic_count {
- my $u = shift or return undef;
- my $count = scalar LJ::Userpic->load_user_userpics($u);
-
- return $count;
-}
-
-sub userpic_quota {
- my $u = shift or return undef;
- my $quota = $u->get_cap('userpics');
-
- return $quota;
-}
-
-sub profile_url {
- my ($u, %opts) = @_;
-
- my $url;
- if ($u->{journaltype} eq "I") {
- $url = "$LJ::SITEROOT/userinfo.bml?userid=$u->{'userid'}&t=I";
- $url .= "&mode=full" if $opts{full};
- } else {
- $url = $u->journal_base . "/profile";
- $url .= "?mode=full" if $opts{full};
- }
- return $url;
-}
-
-# returns the gift shop URL to buy a gift for that user
-sub gift_url {
- my ($u, $opts) = @_;
- croak "invalid user object passed" unless LJ::isu($u);
- my $item = $opts->{item} ? delete $opts->{item} : '';
-
- return "$LJ::SITEROOT/shop/view.bml?item=$item&gift=1&for=$u->{'user'}";
-}
-
-# return the URL to the send message page
-sub message_url {
- my $u = shift;
- croak "invalid user object passed" unless LJ::isu($u);
-
- return undef if $LJ::DISABLED{user_messaging};
- return "$LJ::SITEROOT/inbox/compose.bml?user=$u->{'user'}";
-}
-
-# <LJFUNC>
-# name: LJ::User::large_journal_icon
-# des: get the large icon by journal type.
-# returns: HTML to display large journal icon.
-# </LJFUNC>
-sub large_journal_icon {
- my $u = shift;
- croak "invalid user object"
- unless LJ::isu($u);
-
- my $wrap_img = sub {
- return "<img src='$LJ::IMGPREFIX/$_[0]' border='0' height='24' " .
- "width='24' style='padding: 0px 2px 0px 0px' />";
- };
-
- # hook will return image to use if it cares about
- # the $u it's been passed
- my $hook_img = LJ::run_hook("large_journal_icon", $u);
- return $wrap_img->($hook_img) if $hook_img;
-
- if ($u->is_comm) {
- return $wrap_img->("community24x24.gif");
- }
-
- if ($u->is_syndicated) {
- return $wrap_img->("syndicated24x24.gif");
- }
-
- if ($u->is_identity) {
- return $wrap_img->("openid24x24.gif");
- }
-
- # personal, news, or unknown fallthrough
- return $wrap_img->("userinfo24x24.gif");
-}
-
-# <LJFUNC>
-# name: LJ::User::caps_icon
-# des: get the icon for a user's cap.
-# returns: HTML with site-specific cap icon.
-# </LJFUNC>
-sub caps_icon {
- my $u = shift;
- return LJ::user_caps_icon($u->{caps});
-}
-
-# tests to see if a user is in a specific named class. class
-# names are site-specific.
-sub in_any_class {
- my ($u, @classes) = @_;
-
- foreach my $class (@classes) {
- return 1 if LJ::caps_in_group($u->{caps}, $class);
- }
-
- return 0;
-}
+
+########################################################################
+### 12. Comment-Related Functions
# get recent talkitems posted to this user
@@ -2043,29 +2828,354 @@ sub get_recent_talkitems {
return reverse @recv;
}
-sub record_login {
- my ($u, $sessid) = @_;
-
- my $too_old = time() - 86400 * 30;
- $u->do("DELETE FROM loginlog WHERE userid=? AND logintime < ?",
- undef, $u->{userid}, $too_old);
-
- my ($ip, $ua);
- eval {
- my $r = BML::get_request();
- $ip = LJ::get_remote_ip();
- $ua = $r->header_in('User-Agent');
- };
-
- return $u->do("INSERT INTO loginlog SET userid=?, sessid=?, logintime=UNIX_TIMESTAMP(), ".
- "ip=?, ua=?", undef, $u->{userid}, $sessid, $ip, $ua);
-}
-
-# THIS IS DEPRECATED DO NOT USE
-sub email {
- my ($u, $remote) = @_;
- return $u->emails_visible($remote);
-}
+
+# 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;
+}
+
+
+########################################################################
+### 13. Community-Related Functions and Authas
+
+
+sub can_manage {
+ my ($u, $target) = @_;
+ return LJ::can_manage($u, $target);
+}
+
+
+# can $u post to $targetu?
+sub can_post_to {
+ my ($u, $targetu) = @_;
+
+ return LJ::can_use_journal($u->id, $targetu->user);
+}
+
+
+# returns an array of maintainer userids
+sub maintainer_userids {
+ my $u = shift;
+
+ return () unless $u->is_community;
+ return @{LJ::load_rel_user_cache( $u->id, 'A' )};
+}
+
+
+# returns an array of moderator userids
+sub moderator_userids {
+ my $u = shift;
+
+ return () unless $u->is_community && $u->prop( 'moderated' );
+ return @{LJ::load_rel_user_cache( $u->id, 'M' )};
+}
+
+
+# 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;
+}
+
+
+# Used to promote communities in interest search results
+sub render_promo_of_community {
+ my ($comm, $style) = @_;
+
+ return undef unless $comm;
+
+ $style ||= 'Vertical';
+
+ # get the ljuser link
+ my $commljuser = $comm->ljuser_display;
+
+ # link to journal
+ my $journal_base = $comm->journal_base;
+
+ # get default userpic if any
+ my $userpic = $comm->userpic;
+ my $userpic_html = '';
+ if ($userpic) {
+ my $userpic_url = $userpic->url;
+ $userpic_html = qq { <a href="$journal_base"><img src="$userpic_url" /></a> };
+ }
+
+ my $blurb = $comm->prop('comm_promo_blurb') || '';
+
+ my $join_link = "$LJ::SITEROOT/community/join.bml?comm=$comm->{user}";
+ my $watch_link = "$LJ::SITEROOT/manage/circle/add.bml?user=$comm->{user}&action=subscribe";
+ my $read_link = $comm->journal_base;
+
+ LJ::need_res("stc/lj_base.css");
+
+ # if horizontal, userpic needs to come before everything
+ my $box_class;
+ my $comm_display;
+
+ if (lc $style eq 'horizontal') {
+ $box_class = 'Horizontal';
+ $comm_display = qq {
+ <div class="Userpic">$userpic_html</div>
+ <div class="Title">LJ Community Promo</div>
+ <div class="CommLink">$commljuser</div>
+ };
+ } else {
+ $box_class = 'Vertical';
+ $comm_display = qq {
+ <div class="Title">LJ Community Promo</div>
+ <div class="CommLink">$commljuser</div>
+ <div class="Userpic">$userpic_html</div>
+ };
+ }
+
+
+ my $html = qq {
+ <div class="CommunityPromoBox">
+ <div class="$box_class">
+ $comm_display
+ <div class="Blurb">$blurb</div>
+ <div class="Links"><a href="$join_link">Join</a> | <a href="$watch_link">Watch</a> |
+ <a href="$read_link">Read</a></div>
+
+ <div class='ljclear'> </div>
+ </div>
+ </div>
+ };
+
+ return $html;
+}
+
+
+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. Content Flagging and Adult Content Functions
+### FIXME: Determine which are by-user and which are admin flagging
+### (and remove admin flagging as an option)
+
+
+# defined by the user
+# returns 'none', 'concepts' or 'explicit'
+sub adult_content {
+ my $u = shift;
+
+ my $prop_value = $u->prop('adult_content');
+
+ return $prop_value ? $prop_value : "none";
+}
+
+
+# uses both user- and admin-defined props to figure out the adult content level
+sub adult_content_calculated {
+ my $u = shift;
+
+ return "explicit" if $u->admin_content_flag eq "explicit_adult";
+ return $u->adult_content;
+}
+
+
+# defined by an admin
+sub admin_content_flag {
+ my $u = shift;
+
+ return $u->prop('admin_content_flag');
+}
+
+
+# returns who marked the entry as the 'adult_content_calculated' adult content level
+sub adult_content_marker {
+ my $u = shift;
+
+ return "admin" if $u->admin_content_flag eq "explicit_adult";
+ return "journal";
+}
+
+
+# defuned by the user
+sub adult_content_reason {
+ my $u = shift;
+
+ return $u->prop('adult_content_reason');
+}
+
+
+sub can_admin_content_flagging {
+ my $u = shift;
+
+ return 0 unless LJ::is_enabled("content_flag");
+ return 1 if $LJ::IS_DEV_SERVER;
+ return LJ::check_priv($u, "siteadmin", "contentflag");
+}
+
+
+sub can_flag_content {
+ my $u = shift;
+ my %opts = @_;
+
+ return 0 unless $u->can_see_content_flag_button(%opts);
+ return 0 if LJ::sysban_check("contentflag", $u->user);
+ return 0 unless $u->rate_check("ctflag", 1);
+ return 1;
+}
+
+
+sub can_see_content_flag_button {
+ my $u = shift;
+ my %opts = @_;
+
+ return 0 unless LJ::is_enabled("content_flag");
+
+ my $content = $opts{content};
+
+ # user can't flag any journal they manage nor any entry they posted
+ # user also can't flag non-public entries
+ if (LJ::isu($content)) {
+ return 0 if $u->can_manage($content);
+ } elsif ($content->isa("LJ::Entry")) {
+ return 0 if $u->equals($content->poster);
+ return 0 unless $content->security eq "public";
+ }
+
+ # user can't flag anything if their account isn't at least one month old
+ my $one_month = 60*60*24*30;
+ return 0 unless time() - $u->timecreate >= $one_month;
+
+ return 1;
+}
+
+
+sub hide_adult_content {
+ my $u = shift;
+
+ my $prop_value = $u->prop('hide_adult_content');
+
+ if ($u->is_child || !$u->best_guess_age) {
+ return "concepts";
+ }
+
+ if ($u->is_minor && $prop_value ne "concepts") {
+ return "explicit";
+ }
+
+ return $prop_value ? $prop_value : "none";
+}
+
+
+# returns a number that represents the user's chosen search filtering level
+# 0 = no filtering
+# 1-10 = moderate filtering
+# >10 = strict filtering
+sub safe_search {
+ my $u = shift;
+
+ my $prop_value = $u->prop('safe_search');
+
+ # current user 18+ default is 0
+ # current user <18 default is 10
+ # new user default (prop value is "nu_default") is 10
+ return 0 if $prop_value eq "none";
+ return $prop_value if $prop_value && $prop_value =~ /^\d+$/;
+ return 0 if $prop_value ne "nu_default" && $u->best_guess_age && !$u->is_minor;
+ return 10;
+}
+
+
+# determine if the user in "for_u" should see $u in a search result
+sub should_show_in_search_results {
+ my $u = shift;
+ my %opts = @_;
+
+ return 1 unless LJ::is_enabled("content_flag") && LJ::is_enabled("safe_search");
+
+ my $adult_content = $u->adult_content_calculated;
+ my $admin_flag = $u->admin_content_flag;
+
+ my $for_u = $opts{for};
+ unless (LJ::isu($for_u)) {
+ return $adult_content ne "none" || $admin_flag ? 0 : 1;
+ }
+
+ my $safe_search = $for_u->safe_search;
+ return 1 if $safe_search == 0;
+
+ my $adult_content_flag_level = $LJ::CONTENT_FLAGS{$adult_content} ? $LJ::CONTENT_FLAGS{$adult_content}->{safe_search_level} : 0;
+ my $admin_flag_level = $LJ::CONTENT_FLAGS{$admin_flag} ? $LJ::CONTENT_FLAGS{$admin_flag}->{safe_search_level} : 0;
+
+ return 0 if $adult_content_flag_level && ($safe_search >= $adult_content_flag_level);
+ return 0 if $admin_flag_level && ($safe_search >= $admin_flag_level);
+ return 1;
+}
+
+
+########################################################################
+### 15. Email-Related Functions
+
+
+sub email_for_feeds {
+ my $u = shift;
+
+ # don't display if it's mangled
+ return if $u->prop("opt_mangleemail") eq "Y";
+
+ my $remote = LJ::get_remote();
+ return $u->email_visible($remote);
+}
+
sub email_raw {
my $u = shift;
@@ -2077,19 +3187,12 @@ sub email_raw {
return $u->{_email};
}
-sub validated_mbox_sha1sum {
- my $u = shift;
-
- # must be validated
- return undef unless $u->is_validated;
-
- # must have one on file
- my $email = $u->email_raw;
- return undef unless $email;
-
- # return SHA1, which does not disclose the actual value
- return Digest::SHA1::sha1_hex('mailto:' . $email);
-}
+
+sub email_status {
+ my $u = shift;
+ return $u->{status};
+}
+
# in scalar context, returns user's email address. given a remote user,
# bases decision based on whether $remote user can see it. in list context,
@@ -2099,6 +3202,7 @@ sub email_visible {
return scalar $u->emails_visible($remote);
}
+
sub emails_visible {
my ($u, $remote) = @_;
@@ -2137,25 +3241,44 @@ sub emails_visible {
return wantarray ? @emails : $emails[0];
}
-sub email_for_feeds {
- my $u = shift;
-
- # don't display if it's mangled
- return if $u->prop("opt_mangleemail") eq "Y";
-
- my $remote = LJ::get_remote();
- return $u->email_visible($remote);
-}
-
-sub email_status {
- my $u = shift;
- return $u->{status};
-}
sub is_validated {
my $u = shift;
return $u->email_status eq "A";
}
+
+
+# return user selected mail encoding or undef
+sub mailencoding {
+ my $u = shift;
+ my $enc = $u->prop('mailencoding');
+
+ return undef unless $enc;
+
+ LJ::load_codes({ "encoding" => \%LJ::CACHE_ENCODINGS } )
+ unless %LJ::CACHE_ENCODINGS;
+ return $LJ::CACHE_ENCODINGS{$enc}
+}
+
+
+# return the setting indicating how a user can be found by their email address
+# Y - Findable, N - Not findable, H - Findable but identity hidden
+sub opt_findbyemail {
+ my $u = shift;
+
+ if ($u->raw_prop('opt_findbyemail') =~ /^(N|Y|H)$/) {
+ return $u->raw_prop('opt_findbyemail');
+ } else {
+ return undef;
+ }
+}
+
+
+sub set_email {
+ my ($u, $email) = @_;
+ return LJ::set_email($u->id, $email);
+}
+
sub update_email_alias {
my $u = shift;
@@ -2172,6 +3295,730 @@ sub update_email_alias {
return 0 if $dbh->err;
return 1;
}
+
+
+sub validated_mbox_sha1sum {
+ my $u = shift;
+
+ # must be validated
+ return undef unless $u->is_validated;
+
+ # must have one on file
+ my $email = $u->email_raw;
+ return undef unless $email;
+
+ # return SHA1, which does not disclose the actual value
+ return Digest::SHA1::sha1_hex('mailto:' . $email);
+}
+
+
+########################################################################
+### 16. Entry-Related Functions
+
+# front-end to recent_entries, which forces the remote user to be
+# the owner, so we get everything.
+sub all_recent_entries {
+ my $u = shift;
+ my %opts = @_;
+ $opts{filtered_for} = $u;
+ return $u->recent_entries(%opts);
+}
+
+
+sub draft_text {
+ my ($u) = @_;
+ return $u->prop('entry_draft');
+}
+
+
+# <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
+# TODO: 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
+}
+
+
+sub newpost_minsecurity {
+ my $u = shift;
+
+ return $u->prop('newpost_minsecurity') || 'public';
+}
+
+
+*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 $memkey = [$u->{userid}, "log2ct:$u->{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, $u->{userid});
+ }, $expire);
+}
+
+
+# return the number of posts that the user actually posted themselves
+sub number_of_posted_posts {
+ my $u = shift;
+
+ my $num = $u->number_of_posts;
+ $num-- if LJ::run_hook('user_has_auto_post', $u);
+
+ return $num;
+}
+
+
+# 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 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 ];
+ }
+
+ # TODO: prepending/middle insertion (the former being just the latter), as well
+ # 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;
+}
+
+
+sub third_party_notify_list {
+ my $u = shift;
+
+ my $val = $u->prop('third_party_notify_list');
+ my @services = split(',', $val);
+
+ return @services;
+}
+
+
+# Add a service to a user's notify list
+sub third_party_notify_list_add {
+ my $u = shift;
+ my $svc = shift;
+ return 0 unless $svc;
+
+ # Is it already there?
+ return 1 if $u->third_party_notify_list_contains($svc);
+
+ # Create the new list of services
+ my @cur_services = $u->third_party_notify_list;
+ push @cur_services, $svc;
+ my $svc_list = join(',', @cur_services);
+
+ # Trim a service from the list if it is too long
+ if (length $svc_list > 255) {
+ shift @cur_services;
+ $svc_list = join(',', @cur_services)
+ }
+
+ # Set it
+ $u->set_prop('third_party_notify_list', $svc_list);
+ return 1;
+}
+
+
+# Check if the user's notify list contains a particular service
+sub third_party_notify_list_contains {
+ my $u = shift;
+ my $val = shift;
+
+ return 1 if grep { $_ eq $val } $u->third_party_notify_list;
+
+ return 0;
+}
+
+
+# Remove a service to a user's notify list
+sub third_party_notify_list_remove {
+ my $u = shift;
+ my $svc = shift;
+ return 0 unless $svc;
+
+ # Is it even there?
+ return 1 unless $u->third_party_notify_list_contains($svc);
+
+ # Remove it!
+ $u->set_prop('third_party_notify_list',
+ join(',',
+ grep { $_ ne $svc } $u->third_party_notify_list
+ )
+ );
+ return 1;
+}
+
+
+########################################################################
+### 17. Interest-Related Functions
+
+sub interest_count {
+ my $u = shift;
+
+ # FIXME: fall back to SELECT COUNT(*) if not cached already?
+ return scalar @{LJ::get_interests($u, { justids => 1 })};
+}
+
+
+sub interest_list {
+ my $u = shift;
+
+ return map { $_->[1] } @{ LJ::get_interests($u) };
+}
+
+
+# return hashref with intname => intid
+sub interests {
+ my $u = shift;
+ my $uints = LJ::get_interests($u);
+ my %interests;
+
+ foreach my $int (@$uints) {
+ $interests{$int->[1]} = $int->[0]; # $interests{name} = intid
+ }
+
+ return \%interests;
+}
+
+
+sub lazy_interests_cleanup {
+ my $u = shift;
+
+ my $dbh = LJ::get_db_writer();
+
+ if ($u->is_community) {
+ $dbh->do("INSERT IGNORE INTO comminterests SELECT * FROM userinterests WHERE userid=?", undef, $u->id);
+ $dbh->do("DELETE FROM userinterests WHERE userid=?", undef, $u->id);
+ } else {
+ $dbh->do("INSERT IGNORE INTO userinterests SELECT * FROM comminterests WHERE userid=?", undef, $u->id);
+ $dbh->do("DELETE FROM comminterests WHERE userid=?", undef, $u->id);
+ }
+
+ LJ::memcache_kill($u, "intids");
+ return 1;
+}
+
+
+sub set_interests {
+ my $u = shift;
+ LJ::set_interests($u, @_);
+}
+
+
+########################################################################
+### 18. Jabber-Related Functions
+
+
+# Hide the LJ Talk field on profile? opt_showljtalk needs a value of 'N'.
+sub hide_ljtalk {
+ my $u = shift;
+ croak "Invalid user object passed" unless LJ::isu($u);
+
+ # ... The opposite of showing the field. :)
+ return $u->show_ljtalk ? 0 : 1;
+}
+
+
+# returns whether or not the user is online on jabber
+sub jabber_is_online {
+ my $u = shift;
+
+ return keys %{LJ::Jabber::Presence->get_resources($u)} ? 1 : 0;
+}
+
+
+sub ljtalk_id {
+ my $u = shift;
+ croak "Invalid user object passed" unless LJ::isu($u);
+
+ return $u->{'user'}.'@'.$LJ::USER_DOMAIN;
+}
+
+
+# opt_showljtalk options based on user setting
+# Y = Show the LJ Talk field on profile (default)
+# N = Don't show the LJ Talk field on profile
+sub opt_showljtalk {
+ my $u = shift;
+
+ # Check for valid value, or just return default of 'Y'.
+ if ($u->raw_prop('opt_showljtalk') =~ /^(Y|N)$/) {
+ return $u->raw_prop('opt_showljtalk');
+ } else {
+ return 'Y';
+ }
+}
+
+
+# find what servers a user is logged in to, and send them an IM
+# returns true if sent, false if failure or user not logged on
+# Please do not call from web context
+sub send_im {
+ my ($self, %opts) = @_;
+
+ croak "Can't call in web context" if LJ::is_web_context();
+
+ my $from = delete $opts{from};
+ my $msg = delete $opts{message} or croak "No message specified";
+
+ croak "No from or bot jid defined" unless $from || $LJ::JABBER_BOT_JID;
+
+ my @resources = keys %{LJ::Jabber::Presence->get_resources($self)} or return 0;
+
+ my $res = $resources[0] or return 0; # FIXME: pick correct server based on priority?
+ my $pres = LJ::Jabber::Presence->new($self, $res) or return 0;
+ my $ip = $LJ::JABBER_SERVER_IP || '127.0.0.1';
+
+ my $sock = IO::Socket::INET->new(PeerAddr => "${ip}:5200")
+ or return 0;
+
+ my $vhost = $LJ::DOMAIN;
+
+ my $to_jid = $self->user . '@' . $LJ::DOMAIN;
+ my $from_jid = $from ? $from->user . '@' . $LJ::DOMAIN : $LJ::JABBER_BOT_JID;
+
+ my $emsg = LJ::exml($msg);
+ my $stanza = LJ::eurl(qq{<message to="$to_jid" from="$from_jid"><body>$emsg</body></message>});
+
+ print $sock "send_stanza $vhost $to_jid $stanza\n";
+
+ my $start_time = time();
+
+ while (1) {
+ my $rin = '';
+ vec($rin, fileno($sock), 1) = 1;
+ select(my $rout=$rin, undef, undef, 1);
+ if (vec($rout, fileno($sock), 1)) {
+ my $ln = <$sock>;
+ return 1 if $ln =~ /^OK/;
+ }
+
+ last if time() > $start_time + 5;
+ }
+
+ return 0;
+}
+
+
+# Show LJ Talk field on profile? opt_showljtalk needs a value of 'Y'.
+sub show_ljtalk {
+ my $u = shift;
+ croak "Invalid user object passed" unless LJ::isu($u);
+
+ # Fail if the user wants to hide the LJ Talk field on their profile,
+ # or doesn't even have the ability to show it.
+ return 0 if $u->opt_showljtalk eq 'N' || $LJ::DISABLED{'ljtalk'} || !$u->is_person;
+
+ # User either decided to show LJ Talk field or has left it at the default.
+ return 1 if $u->opt_showljtalk eq 'Y';
+}
+
+
+########################################################################
+### 19. OpenID and Identity Users
+
+# returns a true value if user has a reserved 'ext' name.
+sub external {
+ my $u = shift;
+ return $u->{user} =~ /^ext_/;
+}
+
+
+# returns LJ::Identity object
+sub identity {
+ my $u = shift;
+ return $u->{_identity} if $u->{_identity};
+ return undef unless $u->{'journaltype'} eq "I";
+
+ my $memkey = [$u->{userid}, "ident:$u->{userid}"];
+ my $ident = LJ::MemCache::get($memkey);
+ if ($ident) {
+ my $i = LJ::Identity->new(
+ typeid => $ident->[0],
+ value => $ident->[1],
+ );
+
+ return $u->{_identity} = $i;
+ }
+
+ my $dbh = LJ::get_db_writer();
+ $ident = $dbh->selectrow_arrayref("SELECT idtype, identity FROM identitymap ".
+ "WHERE userid=? LIMIT 1", undef, $u->{userid});
+ if ($ident) {
+ LJ::MemCache::set($memkey, $ident);
+ my $i = LJ::Identity->new(
+ typeid => $ident->[0],
+ value => $ident->[1],
+ );
+ return $i;
+ }
+ return undef;
+}
+
+
+# class function - load an identity user, but only if they're already known to us
+sub load_existing_identity_user {
+ my ($type, $ident) = @_;
+
+ my $dbh = LJ::get_db_reader();
+ my $uid = $dbh->selectrow_array("SELECT userid FROM identitymap WHERE idtype=? AND identity=?",
+ undef, $type, $ident);
+ return $uid ? LJ::load_userid($uid) : undef;
+}
+
+
+# class function - load an identity user, and if we've never seen them before create a user account for them
+sub load_identity_user {
+ my ($type, $ident, $vident) = @_;
+
+ my $u = load_existing_identity_user($type, $ident);
+
+ # If the user is marked as expunged, move identity mapping aside
+ # and continue to create new account.
+ # Otherwise return user if it exists.
+ if ($u) {
+ if ($u->is_expunged) {
+ return undef unless ($u->rename_identity);
+ } else {
+ return $u;
+ }
+ }
+
+ # increment ext_ counter until we successfully create an LJ
+ # account. hard cap it at 10 tries. (arbitrary, but we really
+ # shouldn't have *any* failures here, let alone 10 in a row)
+ my $dbh = LJ::get_db_writer();
+ my $uid;
+
+ for (1..10) {
+ my $extuser = 'ext_' . LJ::alloc_global_counter('E');
+
+ my $name = $extuser;
+ if ($type eq "O" && ref $vident) {
+ $name = $vident->display;
+ }
+
+ $uid = LJ::create_account({
+ caps => undef,
+ user => $extuser,
+ name => $name,
+ journaltype => 'I',
+ });
+ last if $uid;
+ select undef, undef, undef, .10; # lets not thrash over this
+ }
+ return undef unless $uid &&
+ $dbh->do("INSERT INTO identitymap (idtype, identity, userid) VALUES (?,?,?)",
+ undef, $type, $ident, $uid);
+
+ $u = LJ::load_userid($uid);
+
+ # record create information
+ my $remote = LJ::get_remote();
+ $u->log_event('account_create', { remote => $remote });
+
+ return $u;
+}
+
+
+# returns a URL if account is an OpenID identity. undef otherwise.
+sub openid_identity {
+ my $u = shift;
+ my $ident = $u->identity;
+ return undef unless $ident && $ident->typeid eq 'O';
+ return $ident->value;
+}
+
+
+# prepare OpenId part of html-page, if needed
+sub openid_tags {
+ my $u = shift;
+
+ my $head = '';
+
+ # OpenID Server and Yadis
+ if (LJ::OpenID->server_enabled and defined $u) {
+ my $journalbase = $u->journal_base;
+ $head .= qq{<link rel="openid.server" href="$LJ::OPENID_SERVER" />\n};
+ $head .= qq{<meta http-equiv="X-XRDS-Location" content="$journalbase/data/yadis" />\n};
+ }
+
+ return $head;
+}
+
+
+# <LJFUNC>
+# name: LJ::User::rename_identity
+# des: Change an identity user's 'identity', update DB,
+# clear memcache and log change.
+# args: user
+# returns: Success or failure.
+# </LJFUNC>
+sub rename_identity {
+ my $u = shift;
+ return 0 unless ($u && $u->is_identity && $u->is_expunged);
+
+ my $id = $u->identity;
+ return 0 unless $id;
+
+ my $dbh = LJ::get_db_writer();
+
+ # generate a new identity value that looks like ex_oldidvalue555
+ my $tempid = sub {
+ my $ident = shift;
+ my $idtype = shift;
+ my $temp = (length($ident) > 249) ? substr($ident, 0, 249) : $ident;
+ my $exid;
+
+ for (1..10) {
+ $exid = "ex_$temp" . int(rand(999));
+
+ # check to see if this identity already exists
+ unless ($dbh->selectrow_array("SELECT COUNT(*) FROM identitymap WHERE identity=? AND idtype=? LIMIT 1", undef, $exid, $idtype)) {
+ # name doesn't already exist, use this one
+ last;
+ }
+ # name existed, try and get another
+
+ if ($_ >= 10) {
+ return 0;
+ }
+ }
+ return $exid;
+ };
+
+ my $from = $id->value;
+ my $to = $tempid->($id->value, $id->typeid);
+
+ return 0 unless $to;
+
+ $dbh->do("UPDATE identitymap SET identity=? WHERE identity=? AND idtype=?",
+ undef, $to, $from, $id->typeid);
+
+ LJ::memcache_kill($u, "userid");
+
+ LJ::infohistory_add($u, 'identity', $from);
+
+ return 1;
+}
+
+
+########################################################################
+### 20. Page Notices Functions
+
+sub dismissed_page_notices {
+ my $u = shift;
+
+ my $val = $u->prop("dismissed_page_notices");
+ my @notices = split(",", $val);
+
+ return @notices;
+}
+
+
+# add a page notice to a user's dismissed page notices list
+sub dismissed_page_notices_add {
+ my $u = shift;
+ my $notice_string = shift;
+ return 0 unless $notice_string && $LJ::VALID_PAGE_NOTICES{$notice_string};
+
+ # is it already there?
+ return 1 if $u->has_dismissed_page_notice($notice_string);
+
+ # create the new list of dismissed page notices
+ my @cur_notices = $u->dismissed_page_notices;
+ push @cur_notices, $notice_string;
+ my $cur_notices_string = join(",", @cur_notices);
+
+ # remove the oldest notice if the list is too long
+ if (length $cur_notices_string > 255) {
+ shift @cur_notices;
+ $cur_notices_string = join(",", @cur_notices);
+ }
+
+ # set it
+ $u->set_prop("dismissed_page_notices", $cur_notices_string);
+
+ return 1;
+}
+
+
+# remove a page notice from a user's dismissed page notices list
+sub dismissed_page_notices_remove {
+ my $u = shift;
+ my $notice_string = shift;
+ return 0 unless $notice_string && $LJ::VALID_PAGE_NOTICES{$notice_string};
+
+ # is it even there?
+ return 0 unless $u->has_dismissed_page_notice($notice_string);
+
+ # remove it
+ $u->set_prop("dismissed_page_notices", join(",", grep { $_ ne $notice_string } $u->dismissed_page_notices));
+
+ return 1;
+}
+
+
+sub has_dismissed_page_notice {
+ my $u = shift;
+ my $notice_string = shift;
+
+ return 1 if grep { $_ eq $notice_string } $u->dismissed_page_notices;
+ return 0;
+}
+
+
+########################################################################
+### 21. Password Functions
sub can_receive_password {
my ($u, $email) = @_;
@@ -2186,15 +4033,715 @@ sub can_receive_password {
undef, $u->id, $email);
}
-sub share_contactinfo {
- my ($u, $remote) = @_;
-
- return 0 if $u->underage || $u->{journaltype} eq "Y";
- return 0 if $u->opt_showcontact eq 'N';
- return 0 if $u->opt_showcontact eq 'R' && !$remote;
- return 0 if $u->opt_showcontact eq 'F' && !$u->trusts( $remote );
- return 1;
-}
+
+sub password {
+ my $u = shift;
+ $u->{_password} ||= LJ::MemCache::get_or_set([$u->{userid}, "pw:$u->{userid}"], sub {
+ my $dbh = LJ::get_db_writer() or die "Couldn't get db master";
+ return $dbh->selectrow_array("SELECT password FROM password WHERE userid=?",
+ undef, $u->id);
+ });
+ return $u->{_password};
+}
+
+
+sub set_password {
+ my ($u, $password) = @_;
+ return LJ::set_password($u->id, $password);
+}
+
+
+########################################################################
+### 22. Priv-Related Functions
+
+
+sub grant_priv {
+ my ($u, $priv, $arg) = @_;
+ $arg ||= "";
+ my $dbh = LJ::get_db_writer();
+
+ return 1 if LJ::check_priv($u, $priv, $arg);
+
+ my $privid = $dbh->selectrow_array("SELECT prlid FROM priv_list".
+ " WHERE privcode = ?", undef, $priv);
+ return 0 unless $privid;
+
+ $dbh->do("INSERT INTO priv_map (userid, prlid, arg) VALUES (?, ?, ?)",
+ undef, $u->id, $privid, $arg);
+ return 0 if $dbh->err;
+
+ undef $u->{'_privloaded'}; # to force reloading of privs later
+ return 1;
+}
+
+sub revoke_priv {
+ my ($u, $priv, $arg) = @_;
+ $arg ||="";
+ my $dbh = LJ::get_db_writer();
+
+ return 1 unless LJ::check_priv($u, $priv, $arg);
+
+ my $privid = $dbh->selectrow_array("SELECT prlid FROM priv_list".
+ " WHERE privcode = ?", undef, $priv);
+ return 0 unless $privid;
+
+ $dbh->do("DELETE FROM priv_map WHERE userid = ? AND prlid = ? AND arg = ?",
+ undef, $u->id, $privid, $arg);
+ return 0 if $dbh->err;
+
+ undef $u->{'_privloaded'}; # to force reloading of privs later
+ undef $u->{'_priv'};
+ return 1;
+}
+
+sub revoke_priv_all {
+ my ($u, $priv) = @_;
+ my $dbh = LJ::get_db_writer();
+
+ my $privid = $dbh->selectrow_array("SELECT prlid FROM priv_list".
+ " WHERE privcode = ?", undef, $priv);
+ return 0 unless $privid;
+
+ $dbh->do("DELETE FROM priv_map WHERE userid = ? AND prlid = ?",
+ undef, $u->id, $privid);
+ return 0 if $dbh->err;
+
+ undef $u->{'_privloaded'}; # to force reloading of privs later
+ undef $u->{'_priv'};
+ return 1;
+}
+
+
+########################################################################
+### 23. SMS-Related Functions
+### FIXME: Determine which of these are TxtLJ backend (bug 199). All?
+
+
+sub add_sms_quota {
+ my ($u, $qty, $type) = @_;
+
+ return LJ::SMS->add_sms_quota($u, $qty, $type);
+}
+
+
+sub can_use_sms {
+ my $u = shift;
+ return LJ::SMS->can_use_sms($u);
+}
+
+
+sub delete_sms_number {
+ my $u = shift;
+ return LJ::SMS->replace_mapping($u, undef);
+}
+
+
+sub max_sms_bytes {
+ my $u = shift;
+ return LJ::SMS->max_sms_bytes($u);
+}
+
+
+sub max_sms_substr {
+ my ($u, $text, %opts) = @_;
+ return LJ::SMS->max_sms_substr($u, $text, %opts);
+}
+
+
+# opts:
+# no_quota = don't check user quota or deduct from their quota for sending a message
+sub send_sms {
+ my ($u, $msg, %opts) = @_;
+
+ return 0 unless $u;
+
+ croak "invalid user object for object method"
+ unless LJ::isu($u);
+ croak "invalid LJ::SMS::Message object to send"
+ unless $msg && $msg->isa("LJ::SMS::Message");
+
+ my $ret = $msg->send(%opts);
+
+ return $ret;
+}
+
+
+sub send_sms_text {
+ my ($u, $msgtext, %opts) = @_;
+
+ my $msg = LJ::SMS::Message->new(
+ owner => $u,
+ to => $u,
+ type => 'outgoing',
+ body_text => $msgtext,
+ );
+
+ # if user specified a class_key for send, set it on
+ # the msg object
+ if ($opts{class_key}) {
+ $msg->class_key($opts{class_key});
+ }
+
+ $msg->send(%opts);
+}
+
+
+sub set_sms_number {
+ my ($u, $num, %opts) = @_;
+ my $verified = delete $opts{verified};
+
+ # these two are only checked if $num, because it's possible
+ # to just pass ($u, undef, undef) to delete the mapping
+ if ($num) {
+ croak "invalid number" unless $num =~ /^\+\d+$/;
+ croak "invalid verified flag" unless $verified =~ /^[YN]$/;
+ }
+
+ return LJ::SMS->replace_mapping($u, $num, $verified);
+}
+
+
+sub set_sms_number_verified {
+ my ($u, $verified) = @_;
+
+ return LJ::SMS->set_number_verified($u, $verified);
+}
+
+
+sub set_sms_quota {
+ my ($u, $qty, $type) = @_;
+
+ return LJ::SMS->set_sms_quota($u, $qty, $type);
+}
+
+
+sub sms_active {
+ my $u = shift;
+
+ # active if the user has a verified sms number
+ return LJ::SMS->configured_for_user($u);
+}
+
+
+sub sms_active_number {
+ my $u = shift;
+ return LJ::SMS->uid_to_num($u, verified_only => 1);
+}
+
+
+# this method returns any mapped number for the user,
+# regardless of its verification status
+sub sms_mapped_number {
+ my $u = shift;
+ return LJ::SMS->uid_to_num($u, verified_only => 0);
+}
+
+
+sub sms_message_count {
+ my $u = shift;
+ return LJ::SMS->message_count($u, @_);
+}
+
+
+sub sms_num_instime {
+ my $u = shift;
+
+ return LJ::SMS->num_instime($u->sms_mapped_number);
+}
+
+
+sub sms_pending {
+ my $u = shift;
+
+ # pending if user has an unverified number
+ return LJ::SMS->pending_for_user($u);
+}
+
+
+sub sms_pending_number {
+ my $u = shift;
+ my $num = LJ::SMS->uid_to_num($u, verified_only => 0);
+ return undef unless $num;
+ return $num if LJ::SMS->num_is_pending($num);
+ return undef;
+}
+
+
+sub sms_quota_remaining {
+ my ($u, $type) = @_;
+
+ return LJ::SMS->sms_quota_remaining($u, $type);
+}
+
+
+sub sms_register_time_remaining {
+ my $u = shift;
+
+ return LJ::SMS->num_register_time_remaining($u);
+}
+
+
+sub sms_sent_message_count {
+ my $u = shift;
+ return LJ::SMS->sent_message_count($u, @_);
+}
+
+
+sub subtract_sms_quota {
+ my ($u, $qty, $type) = @_;
+
+ return LJ::SMS->subtract_sms_quota($u, $qty, $type);
+}
+
+
+########################################################################
+### 24. Styles and S2-Related Functions
+
+
+# Check to see if the user can use eboxes at all
+sub can_use_ebox {
+ my $u = shift;
+
+ return ref $LJ::DISABLED{ebox} ? !$LJ::DISABLED{ebox}->($u) : !$LJ::DISABLED{ebox};
+}
+
+
+# Allow users to choose eboxes if:
+# 1. The entire ebox feature isn't disabled AND
+# 2. The option to choose eboxes isn't disabled OR
+# 3. The option to choose eboxes is disabled AND
+# 4. The user already has eboxes turned on
+sub can_use_ebox_ui {
+ my $u = shift;
+ my $allow_ebox = 1;
+
+ if ($LJ::DISABLED{ebox_option}) {
+ $allow_ebox = $u->prop('journal_box_entries');
+ }
+
+ return $u->can_use_ebox && $allow_ebox;
+}
+
+
+sub journal_base {
+ my $u = shift;
+ return LJ::journal_base($u);
+}
+
+
+sub opt_ctxpopup {
+ my $u = shift;
+
+ # if unset, default to on
+ my $prop = $u->raw_prop('opt_ctxpopup') || 'Y';
+
+ return $prop eq 'Y';
+}
+
+
+sub opt_embedplaceholders {
+ my $u = shift;
+
+ my $prop = $u->raw_prop('opt_embedplaceholders');
+
+ if (defined $prop) {
+ return $prop;
+ } else {
+ my $imagelinks = $u->prop('opt_imagelinks');
+ return $imagelinks;
+ }
+}
+
+
+# revert S2 style to the default if the user is using a layout/theme layer that they don't have permission to use
+sub revert_style {
+ my $u = shift;
+
+ # FIXME: this solution is suboptimal
+ # - ensure that these packages are loaded via Class::Autouse by calling a method on them
+ LJ::S2->can("dostuff");
+ LJ::S2Theme->can("dostuff");
+ LJ::Customize->can("dostuff");
+
+ my $current_theme = LJ::Customize->get_current_theme($u);
+ return unless $current_theme;
+ my $default_theme_of_current_layout = LJ::S2Theme->load_default_of($current_theme->layoutid, user => $u);
+ return unless $default_theme_of_current_layout;
+
+ my $default_style = LJ::run_hook('get_default_style', $u) || $LJ::DEFAULT_STYLE;
+ my $default_layout_uniq = exists $default_style->{layout} ? $default_style->{layout} : '';
+ my $default_theme_uniq = exists $default_style->{theme} ? $default_style->{theme} : '';
+
+ my %style = LJ::S2::get_style($u, "verify");
+ my $public = LJ::S2::get_public_layers();
+ my $userlay = LJ::S2::get_layers_of_user($u);
+
+ # check to see if the user is using a custom layout or theme
+ # if so, we want to let them keep using it
+ foreach my $layerid (keys %$userlay) {
+ return if $current_theme->layoutid == $layerid;
+ return if $current_theme->themeid == $layerid;
+ }
+
+ # if the user cannot use the layout or the default theme of that layout, switch to the default style (if it's defined)
+ if (($default_layout_uniq || $default_theme_uniq) && (!LJ::S2::can_use_layer($u, $current_theme->layout_uniq) || !$default_theme_of_current_layout->available_to($u))) {
+ my $new_theme;
+ if ($default_theme_uniq) {
+ $new_theme = LJ::S2Theme->load_by_uniq($default_theme_uniq);
+ } else {
+ my $layoutid = $public->{$default_layout_uniq}->{s2lid} if $public->{$default_layout_uniq} && $public->{$default_layout_uniq}->{type} eq "layout";
+ $new_theme = LJ::S2Theme->load_default_of($layoutid, user => $u) if $layoutid;
+ }
+
+ return unless $new_theme;
+
+ # look for a style that uses the default layout/theme, and use it if it exists
+ my $styleid = $new_theme->get_styleid_for_theme($u);
+ my $style_exists = 0;
+ if ($styleid) {
+ $style_exists = 1;
+ $u->set_prop("s2_style", $styleid);
+
+ my $stylelayers = LJ::S2::get_style_layers($u, $u->prop('s2_style'));
+ foreach my $layer (qw(user i18nc i18n core)) {
+ $style{$layer} = exists $stylelayers->{$layer} ? $stylelayers->{$layer} : 0;
+ }
+ }
+
+ # set the layers that are defined by $default_style
+ while (my ($layer, $name) = each %$default_style) {
+ next if $name eq "";
+ next unless $public->{$name};
+ my $id = $public->{$name}->{s2lid};
+ $style{$layer} = $id if $id;
+ }
+
+ # make sure core was set
+ $style{core} = $new_theme->coreid
+ if $style{core} == 0;
+
+ # make sure the other layers were set
+ foreach my $layer (qw(user i18nc i18n)) {
+ $style{$layer} = 0 unless $style{$layer} || $style_exists;
+ }
+
+ # create the style
+ if ($style_exists) {
+ LJ::Customize->implicit_style_create($u, %style);
+ } else {
+ LJ::Customize->implicit_style_create({ 'force' => 1 }, $u, %style);
+ }
+
+ # if the user can use the layout but not the theme, switch to the default theme of that layout
+ # we know they can use this theme at this point because if they couldn't, the above block would have caught it
+ } elsif (LJ::S2::can_use_layer($u, $current_theme->layout_uniq) && !LJ::S2::can_use_layer($u, $current_theme->uniq)) {
+ $style{theme} = $default_theme_of_current_layout->themeid;
+ LJ::Customize->implicit_style_create($u, %style);
+ }
+
+ return;
+}
+
+
+sub show_control_strip {
+ my $u = shift;
+
+ LJ::run_hook('control_strip_propcheck', $u, 'show_control_strip') unless $LJ::DISABLED{control_strip_propcheck};
+
+ my $prop = $u->raw_prop('show_control_strip');
+ return 0 if $prop =~ /^off/;
+
+ return 'dark' if $prop eq 'forced';
+
+ return $prop;
+}
+
+
+sub view_control_strip {
+ my $u = shift;
+
+ LJ::run_hook('control_strip_propcheck', $u, 'view_control_strip') unless $LJ::DISABLED{control_strip_propcheck};
+
+ my $prop = $u->raw_prop('view_control_strip');
+ return 0 if $prop =~ /^off/;
+
+ return 'dark' if $prop eq 'forced';
+
+ return $prop;
+}
+
+
+########################################################################
+### 25. Subscription, Notifiction, and Messaging Functions
+
+
+# this is the count used to check the maximum subscription count
+sub active_inbox_subscription_count {
+ my $u = shift;
+ return scalar ( grep { $_->active && $_->enabled } $u->find_subscriptions(method => 'Inbox') );
+}
+
+
+sub can_add_inbox_subscription {
+ my $u = shift;
+ return $u->active_inbox_subscription_count >= $u->max_subscriptions ? 0 : 1;
+}
+
+
+# can this user use ESN?
+sub can_use_esn {
+ my $u = shift;
+ return 0 if $LJ::DISABLED{esn};
+ my $disable = $LJ::DISABLED{esn_ui};
+ return 1 unless $disable;
+
+ if (ref $disable eq 'CODE') {
+ return $disable->($u) ? 0 : 1;
+ }
+
+ return $disable ? 0 : 1;
+}
+
+
+# 1/0 if someone can send a message to $u
+sub can_receive_message {
+ my ($u, $sender) = @_;
+
+ my $opt_usermsg = $u->opt_usermsg;
+ return 0 if $opt_usermsg eq 'N' || !$sender;
+ return 0 if $u->has_banned($sender);
+ return 0 if $opt_usermsg eq 'M' && !$u->mutually_trusts($sender);
+ return 0 if $opt_usermsg eq 'F' && !$u->trusts($sender);
+
+ return 1;
+}
+
+
+# delete all of a user's subscriptions
+sub delete_all_subscriptions {
+ my $u = shift;
+ return LJ::Subscription->delete_all_subs($u);
+}
+
+
+# delete all of a user's subscriptions
+sub delete_all_inactive_subscriptions {
+ my $u = shift;
+ my $dryrun = shift;
+ return LJ::Subscription->delete_all_inactive_subs($u, $dryrun);
+}
+
+
+# ensure that this user does not have more than the maximum number of subscriptions
+# allowed by their cap, and enable subscriptions up to their current limit
+sub enable_subscriptions {
+ my $u = shift;
+
+ # first thing, disable everything they don't have caps for
+ # and make sure everything is enabled that should be enabled
+ map { $_->available_for_user($u) ? $_->enable : $_->disable } $u->find_subscriptions(method => 'Inbox');
+
+ my $max_subs = $u->get_cap('subscriptions');
+ my @inbox_subs = grep { $_->active && $_->enabled } $u->find_subscriptions(method => 'Inbox');
+
+ if ((scalar @inbox_subs) > $max_subs) {
+ # oh no, too many subs.
+ # disable the oldest subscriptions that are "tracking" subscriptions
+ my @tracking = grep { $_->is_tracking_category } @inbox_subs;
+
+ # oldest subs first
+ @tracking = sort {
+ return $a->createtime <=> $b->createtime;
+ } @tracking;
+
+ my $need_to_deactivate = (scalar @inbox_subs) - $max_subs;
+
+ for (1..$need_to_deactivate) {
+ my $sub_to_deactivate = shift @tracking;
+ $sub_to_deactivate->deactivate if $sub_to_deactivate;
+ }
+ } else {
+ # make sure all subscriptions are activated
+ my $need_to_activate = $max_subs - (scalar @inbox_subs);
+
+ # get deactivated subs
+ @inbox_subs = grep { $_->active && $_->available_for_user } $u->find_subscriptions(method => 'Inbox');
+
+ for (1..$need_to_activate) {
+ my $sub_to_activate = shift @inbox_subs;
+ $sub_to_activate->activate if $sub_to_activate;
+ }
+ }
+}
+
+
+sub esn_inbox_default_expand {
+ my $u = shift;
+
+ my $prop = $u->raw_prop('esn_inbox_default_expand');
+ return $prop ne 'N';
+}
+
+
+# interim solution while legacy/ESN notifications are both happening:
+# checks possible subscriptions to see if user will get an ESN notification
+# THIS IS TEMPORARY. FIXME. Should only be called by talklib.
+# params: journal, arg1 (entry ditemid), arg2 (comment talkid)
+sub gets_notified {
+ my ($u, %params) = @_;
+
+ $params{event} = "LJ::Event::JournalNewComment";
+ $params{method} = "LJ::NotificationMethod::Email";
+
+ my $has_sub;
+
+ # did they subscribe to the parent comment?
+ $has_sub = LJ::Subscription->find($u, %params);
+ return $has_sub if $has_sub;
+
+ # remove the comment-specific parameter, then check for an entry subscription
+ $params{arg2} = 0;
+ $has_sub = LJ::Subscription->find($u, %params);
+ return $has_sub if $has_sub;
+
+ # remove the entry-specific parameter, then check if they're subscribed to the entire journal
+ $params{arg1} = 0;
+ $has_sub = LJ::Subscription->find($u, %params);
+ return $has_sub;
+}
+
+
+# search for a subscription
+*find_subscriptions = \&has_subscription;
+sub has_subscription {
+ my ($u, %params) = @_;
+ croak "No parameters" unless %params;
+
+ return LJ::Subscription->find($u, %params);
+}
+
+
+sub max_subscriptions {
+ my $u = shift;
+ return $u->get_cap('subscriptions');
+}
+
+
+# return the URL to the send message page
+sub message_url {
+ my $u = shift;
+ croak "invalid user object passed" unless LJ::isu($u);
+
+ return undef if $LJ::DISABLED{user_messaging};
+ return "$LJ::SITEROOT/inbox/compose.bml?user=$u->{'user'}";
+}
+
+
+sub new_message_count {
+ my $u = shift;
+ my $inbox = $u->notification_inbox;
+ my $count = $inbox->unread_count;
+
+ return $count || 0;
+}
+
+
+sub notification_archive {
+ my $u = shift;
+ return LJ::NotificationArchive->new($u);
+}
+
+
+# Returns the NotificationInbox for this user
+*inbox = \¬ification_inbox;
+sub notification_inbox {
+ my $u = shift;
+ return LJ::NotificationInbox->new($u);
+}
+
+
+# opt_usermsg options
+# Y - Registered Users
+# F - Trusted Users
+# M - Mutually Trusted Users
+# N - Nobody
+sub opt_usermsg {
+ my $u = shift;
+
+ if ($u->raw_prop('opt_usermsg') =~ /^(Y|F|M|N)$/) {
+ return $u->raw_prop('opt_usermsg');
+ } else {
+ return 'N' if $u->underage || $u->is_child;
+ return 'M' if $u->is_minor;
+ return 'Y';
+ }
+}
+
+
+# subscribe to an event
+sub subscribe {
+ my ($u, %opts) = @_;
+ croak "No subscription options" unless %opts;
+
+ return LJ::Subscription->create($u, %opts);
+}
+
+
+sub subscription_count {
+ my $u = shift;
+ return scalar LJ::Subscription->subscriptions_of_user($u);
+}
+
+
+sub subscriptions {
+ my $u = shift;
+ return LJ::Subscription->subscriptions_of_user($u);
+}
+
+
+########################################################################
+### 26. Syndication-Related Functions
+
+# retrieve hash of basic syndicated info
+sub get_syndicated {
+ my $u = shift;
+
+ return unless $u->is_syndicated;
+ my $memkey = [$u->{'userid'}, "synd:$u->{'userid'}"];
+
+ my $synd = {};
+ $synd = LJ::MemCache::get($memkey);
+ unless ($synd) {
+ my $dbr = LJ::get_db_reader();
+ return unless $dbr;
+ $synd = $dbr->selectrow_hashref("SELECT * FROM syndicated WHERE userid=$u->{'userid'}");
+ LJ::MemCache::set($memkey, $synd) if $synd;
+ }
+
+ return $synd;
+}
+
+
+########################################################################
+### 27. Tag-Related Functions
+
+# 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);
+}
+
+
+sub tags {
+ my $u = shift;
+
+ return LJ::Tags::get_usertags($u);
+}
+
+
+########################################################################
+### 28. Userpic-Related Functions
# <LJFUNC>
# name: LJ::User::activate_userpics
@@ -2330,214 +4877,148 @@ sub activate_userpics {
return 1;
}
-# ensure that this user does not have more than the maximum number of subscriptions
-# allowed by their cap, and enable subscriptions up to their current limit
-sub enable_subscriptions {
- my $u = shift;
-
- # first thing, disable everything they don't have caps for
- # and make sure everything is enabled that should be enabled
- map { $_->available_for_user($u) ? $_->enable : $_->disable } $u->find_subscriptions(method => 'Inbox');
-
- my $max_subs = $u->get_cap('subscriptions');
- my @inbox_subs = grep { $_->active && $_->enabled } $u->find_subscriptions(method => 'Inbox');
-
- if ((scalar @inbox_subs) > $max_subs) {
- # oh no, too many subs.
- # disable the oldest subscriptions that are "tracking" subscriptions
- my @tracking = grep { $_->is_tracking_category } @inbox_subs;
-
- # oldest subs first
- @tracking = sort {
- return $a->createtime <=> $b->createtime;
- } @tracking;
-
- my $need_to_deactivate = (scalar @inbox_subs) - $max_subs;
-
- for (1..$need_to_deactivate) {
- my $sub_to_deactivate = shift @tracking;
- $sub_to_deactivate->deactivate if $sub_to_deactivate;
- }
- } else {
- # make sure all subscriptions are activated
- my $need_to_activate = $max_subs - (scalar @inbox_subs);
-
- # get deactivated subs
- @inbox_subs = grep { $_->active && $_->available_for_user } $u->find_subscriptions(method => 'Inbox');
-
- for (1..$need_to_activate) {
- my $sub_to_activate = shift @inbox_subs;
- $sub_to_activate->activate if $sub_to_activate;
- }
- }
-}
-
-# revert S2 style to the default if the user is using a layout/theme layer that they don't have permission to use
-sub revert_style {
- my $u = shift;
-
- # FIXME: this solution sucks
- # - ensure that these packages are loaded via Class::Autouse by calling a method on them
- LJ::S2->can("dostuff");
- LJ::S2Theme->can("dostuff");
- LJ::Customize->can("dostuff");
-
- my $current_theme = LJ::Customize->get_current_theme($u);
- return unless $current_theme;
- my $default_theme_of_current_layout = LJ::S2Theme->load_default_of($current_theme->layoutid, user => $u);
- return unless $default_theme_of_current_layout;
-
- my $default_style = LJ::run_hook('get_default_style', $u) || $LJ::DEFAULT_STYLE;
- my $default_layout_uniq = exists $default_style->{layout} ? $default_style->{layout} : '';
- my $default_theme_uniq = exists $default_style->{theme} ? $default_style->{theme} : '';
-
- my %style = LJ::S2::get_style($u, "verify");
- my $public = LJ::S2::get_public_layers();
- my $userlay = LJ::S2::get_layers_of_user($u);
-
- # check to see if the user is using a custom layout or theme
- # if so, we want to let them keep using it
- foreach my $layerid (keys %$userlay) {
- return if $current_theme->layoutid == $layerid;
- return if $current_theme->themeid == $layerid;
- }
-
- # if the user cannot use the layout or the default theme of that layout, switch to the default style (if it's defined)
- if (($default_layout_uniq || $default_theme_uniq) && (!LJ::S2::can_use_layer($u, $current_theme->layout_uniq) || !$default_theme_of_current_layout->available_to($u))) {
- my $new_theme;
- if ($default_theme_uniq) {
- $new_theme = LJ::S2Theme->load_by_uniq($default_theme_uniq);
- } else {
- my $layoutid = $public->{$default_layout_uniq}->{s2lid} if $public->{$default_layout_uniq} && $public->{$default_layout_uniq}->{type} eq "layout";
- $new_theme = LJ::S2Theme->load_default_of($layoutid, user => $u) if $layoutid;
- }
-
- return unless $new_theme;
-
- # look for a style that uses the default layout/theme, and use it if it exists
- my $styleid = $new_theme->get_styleid_for_theme($u);
- my $style_exists = 0;
- if ($styleid) {
- $style_exists = 1;
- $u->set_prop("s2_style", $styleid);
-
- my $stylelayers = LJ::S2::get_style_layers($u, $u->prop('s2_style'));
- foreach my $layer (qw(user i18nc i18n core)) {
- $style{$layer} = exists $stylelayers->{$layer} ? $stylelayers->{$layer} : 0;
- }
- }
-
- # set the layers that are defined by $default_style
- while (my ($layer, $name) = each %$default_style) {
- next if $name eq "";
- next unless $public->{$name};
- my $id = $public->{$name}->{s2lid};
- $style{$layer} = $id if $id;
- }
-
- # make sure core was set
- $style{core} = $new_theme->coreid
- if $style{core} == 0;
-
- # make sure the other layers were set
- foreach my $layer (qw(user i18nc i18n)) {
- $style{$layer} = 0 unless $style{$layer} || $style_exists;
- }
-
- # create the style
- if ($style_exists) {
- LJ::Customize->implicit_style_create($u, %style);
- } else {
- LJ::Customize->implicit_style_create({ 'force' => 1 }, $u, %style);
- }
-
- # if the user can use the layout but not the theme, switch to the default theme of that layout
- # we know they can use this theme at this point because if they couldn't, the above block would have caught it
- } elsif (LJ::S2::can_use_layer($u, $current_theme->layout_uniq) && !LJ::S2::can_use_layer($u, $current_theme->uniq)) {
- $style{theme} = $default_theme_of_current_layout->themeid;
- LJ::Customize->implicit_style_create($u, %style);
- }
-
- return;
-}
-
-sub uncache_prop {
- my ($u, $name) = @_;
- my $prop = LJ::get_prop("user", $name) or die; # FIXME: use exceptions
- LJ::MemCache::delete([$u->{userid}, "uprop:$u->{userid}:$prop->{id}"]);
- delete $u->{$name};
- return 1;
-}
-
-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 ];
- }
-
- # TODO: prepending/middle insertion (the former being just the latter), as well
- # 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;
-}
-
-sub draft_text {
- my ($u) = @_;
- return $u->prop('entry_draft');
-}
-
-sub notable_interests {
- my ($u, $n) = @_;
- $n ||= 20;
-
- # arrayref of arrayrefs of format [intid, intname, intcount];
- my $ints = LJ::get_interests($u)
- or return ();
-
- my @ints = map { $_->[1] } @$ints;
-
- # sorta arrayref inline
- LJ::AdTargetedInterests->sort_interests(\@ints);
-
- return @ints[0..$n-1] if @ints > $n;
- return @ints;
-}
+
+sub allpics_base {
+ my $u = shift;
+ return "$LJ::SITEROOT/allpics.bml?user=" . $u->user;
+}
+
+
+sub get_userpic_count {
+ my $u = shift or return undef;
+ my $count = scalar LJ::Userpic->load_user_userpics($u);
+
+ return $count;
+}
+
+
+# <LJFUNC>
+# name: LJ::User::mogfs_userpic_key
+# class: mogilefs
+# des: Make a mogilefs key for the given pic for the user.
+# args: pic
+# des-pic: Either the userpic hash or the picid of the userpic.
+# returns: 1.
+# </LJFUNC>
+sub mogfs_userpic_key {
+ my $self = shift or return undef;
+ my $pic = shift or croak "missing required arg: userpic";
+
+ my $picid = ref $pic ? $pic->{picid} : $pic+0;
+ return "up:$self->{userid}:$picid";
+}
+
+
+sub userpic {
+ my $u = shift;
+ return undef unless $u->{defaultpicid};
+ return LJ::Userpic->new($u, $u->{defaultpicid});
+}
+
+
+sub userpic_quota {
+ my $u = shift or return undef;
+ my $quota = $u->get_cap('userpics');
+
+ return $quota;
+}
+
+
+
+########################################################################
+### 99. Miscellaneous Legacy Items
+### 99A. Underage functions (FIXME: we shouldn't need these)
+
+# returns a true value if the user is underage; or if you give it an argument,
+# will turn on/off that user's underage status. can also take a second argument
+# when you're setting the flag to also update the underage_status userprop
+# which is used to record if a user was ever marked as underage.
+sub underage {
+ # has no bearing if this isn't on
+ return undef unless LJ::class_bit("underage");
+
+ # now get the args and continue
+ my $u = shift;
+ return LJ::get_cap($u, 'underage') unless @_;
+
+ # now set it on or off
+ my $on = shift() ? 1 : 0;
+ if ($on) {
+ $u->add_to_class("underage");
+ } else {
+ $u->remove_from_class("underage");
+ }
+
+ # now set their status flag if one was sent
+ my $status = shift();
+ if ($status || $on) {
+ # by default, just records if user was ever underage ("Y")
+ $u->underage_status($status || 'Y');
+ }
+
+ # add to statushistory
+ if (my $shwhen = shift()) {
+ my $text = $on ? "marked" : "unmarked";
+ my $status = $u->underage_status;
+ LJ::statushistory_add($u, undef, "coppa", "$text; status=$status; when=$shwhen");
+ }
+
+ # now fire off any hooks that are available
+ LJ::run_hooks('set_underage', {
+ u => $u,
+ on => $on,
+ status => $u->underage_status,
+ });
+
+ # return true if no failures
+ return 1;
+}
+
+
+# return true if we know user is a minor (< 18)
+sub is_minor {
+ my $self = shift;
+ my $age = $self->best_guess_age;
+ return 0 unless $age;
+ return 1 if ($age < 18);
+ return 0;
+}
+
+
+# return true if we know user is a child (< 14)
+sub is_child {
+ my $self = shift;
+ my $age = $self->best_guess_age;
+
+ return 0 unless $age;
+ return 1 if ($age < 14);
+ return 0;
+}
+
+
+# return or set the underage status userprop
+sub underage_status {
+ return undef unless LJ::class_bit("underage");
+
+ my $u = shift;
+
+ # return if they aren't setting it
+ unless (@_) {
+ return $u->prop("underage_status");
+ }
+
+ # set and return what it got set to
+ $u->set_prop('underage_status', shift());
+ return $u->{underage_status};
+}
+
+
+########################################################################
+### 99B. Ad functions (FIXME: we shouldn't need these)
+
# returns $n number of communities that $u is a member of, sorted by update time (most recent to least recent)
+# Probably ad-related; at any rate, it's broken.
sub notable_communities {
my ($u, $n) = @_;
$n ||= 3;
@@ -2567,1864 +5048,55 @@ sub notable_communities {
return map { $us->{$_} } @ret_commids;
}
-# returns the max capability ($cname) for all the classes
-# the user is a member of
-sub get_cap {
- my ($u, $cname) = @_;
- return 1 if $LJ::T_HAS_ALL_CAPS;
- return LJ::get_cap($u, $cname);
-}
-
-# tests to see if a user is in a specific named class. class
-# names are site-specific.
-sub in_class {
- my ($u, $class) = @_;
- return LJ::caps_in_group($u->{caps}, $class);
-}
-
-sub add_to_class {
- my ($u, $class) = @_;
- my $bit = LJ::class_bit($class);
- die "unknown class '$class'" unless defined $bit;
-
- # call add_to_class hook before we modify the
- # current $u, so it can make inferences from the
- # old $u caps vs the new we say we'll be adding
- if (LJ::are_hooks('add_to_class')) {
- LJ::run_hooks('add_to_class', $u, $class);
- }
-
- return LJ::modify_caps($u, [$bit], []);
-}
-
-sub remove_from_class {
- my ($u, $class) = @_;
- my $bit = LJ::class_bit($class);
- die "unknown class '$class'" unless defined $bit;
-
- # call remove_from_class hook before we modify the
- # current $u, so it can make inferences from the
- # old $u caps vs what we'll be removing
- if (LJ::are_hooks('remove_from_class')) {
- LJ::run_hooks('remove_from_class', $u, $class);
- }
-
- return LJ::modify_caps($u, [], [$bit]);
-}
-
-sub cache {
- my ($u, $key) = @_;
- my $val = $u->selectrow_array("SELECT value FROM userblobcache WHERE userid=? AND bckey=?",
- undef, $u->{userid}, $key);
- return undef unless defined $val;
- if (my $thaw = eval { Storable::thaw($val); }) {
- return $thaw;
- }
- return $val;
-}
-
-sub set_cache {
- my ($u, $key, $value, $expr) = @_;
- my $now = time();
- $expr ||= $now + 86400;
- $expr += $now if $expr < 315532800; # relative to absolute time
- $value = Storable::nfreeze($value) if ref $value;
- $u->do("REPLACE INTO userblobcache (userid, bckey, value, timeexpire) VALUES (?,?,?,?)",
- undef, $u->{userid}, $key, $value, $expr);
-}
-
-# 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;
-}
-
-# front-end to recent_entries, which forces the remote user to be
-# the owner, so we get everything.
-sub all_recent_entries {
- my $u = shift;
- my %opts = @_;
- $opts{filtered_for} = $u;
- return $u->recent_entries(%opts);
-}
-
-sub sms_active_number {
- my $u = shift;
- return LJ::SMS->uid_to_num($u, verified_only => 1);
-}
-
-sub sms_pending_number {
- my $u = shift;
- my $num = LJ::SMS->uid_to_num($u, verified_only => 0);
- return undef unless $num;
- return $num if LJ::SMS->num_is_pending($num);
- return undef;
-}
-
-# this method returns any mapped number for the user,
-# regardless of its verification status
-sub sms_mapped_number {
- my $u = shift;
- return LJ::SMS->uid_to_num($u, verified_only => 0);
-}
-
-sub sms_active {
- my $u = shift;
-
- # active if the user has a verified sms number
- return LJ::SMS->configured_for_user($u);
-}
-
-sub sms_pending {
- my $u = shift;
-
- # pending if user has an unverified number
- return LJ::SMS->pending_for_user($u);
-}
-
-sub sms_register_time_remaining {
- my $u = shift;
-
- return LJ::SMS->num_register_time_remaining($u);
-}
-
-sub sms_num_instime {
- my $u = shift;
-
- return LJ::SMS->num_instime($u->sms_mapped_number);
-}
-
-sub set_sms_number {
- my ($u, $num, %opts) = @_;
- my $verified = delete $opts{verified};
-
- # these two are only checked if $num, because it's possible
- # to just pass ($u, undef, undef) to delete the mapping
- if ($num) {
- croak "invalid number" unless $num =~ /^\+\d+$/;
- croak "invalid verified flag" unless $verified =~ /^[YN]$/;
- }
-
- return LJ::SMS->replace_mapping($u, $num, $verified);
-}
-
-sub set_sms_number_verified {
- my ($u, $verified) = @_;
-
- return LJ::SMS->set_number_verified($u, $verified);
-}
-
-sub sms_message_count {
- my $u = shift;
- return LJ::SMS->message_count($u, @_);
-}
-
-sub sms_sent_message_count {
- my $u = shift;
- return LJ::SMS->sent_message_count($u, @_);
-}
-
-sub delete_sms_number {
- my $u = shift;
- return LJ::SMS->replace_mapping($u, undef);
-}
-
-# opts:
-# no_quota = don't check user quota or deduct from their quota for sending a message
-sub send_sms {
- my ($u, $msg, %opts) = @_;
-
- return 0 unless $u;
-
- croak "invalid user object for object method"
- unless LJ::isu($u);
- croak "invalid LJ::SMS::Message object to send"
- unless $msg && $msg->isa("LJ::SMS::Message");
-
- my $ret = $msg->send(%opts);
-
- return $ret;
-}
-
-sub send_sms_text {
- my ($u, $msgtext, %opts) = @_;
-
- my $msg = LJ::SMS::Message->new(
- owner => $u,
- to => $u,
- type => 'outgoing',
- body_text => $msgtext,
- );
-
- # if user specified a class_key for send, set it on
- # the msg object
- if ($opts{class_key}) {
- $msg->class_key($opts{class_key});
- }
-
- $msg->send(%opts);
-}
-
-sub sms_quota_remaining {
- my ($u, $type) = @_;
-
- return LJ::SMS->sms_quota_remaining($u, $type);
-}
-
-sub add_sms_quota {
- my ($u, $qty, $type) = @_;
-
- return LJ::SMS->add_sms_quota($u, $qty, $type);
-}
-
-sub set_sms_quota {
- my ($u, $qty, $type) = @_;
-
- return LJ::SMS->set_sms_quota($u, $qty, $type);
-}
-
-sub max_sms_bytes {
- my $u = shift;
- return LJ::SMS->max_sms_bytes($u);
-}
-
-sub max_sms_substr {
- my ($u, $text, %opts) = @_;
- return LJ::SMS->max_sms_substr($u, $text, %opts);
-}
-
-sub subtract_sms_quota {
- my ($u, $qty, $type) = @_;
-
- return LJ::SMS->subtract_sms_quota($u, $qty, $type);
-}
-
-sub is_syndicated {
- my $u = shift;
- return $u->{journaltype} eq "Y";
-}
-
-sub is_community {
- my $u = shift;
- return $u->{journaltype} eq "C";
-}
-*is_comm = \&is_community;
-
-sub is_shared {
- my $u = shift;
- return $u->{journaltype} eq "S";
-}
-
-sub is_news {
- my $u = shift;
- return $u->{journaltype} eq "N";
-}
-
-sub is_person {
- my $u = shift;
- return $u->{journaltype} eq "P";
-}
-*is_personal = \&is_person;
-
-sub is_identity {
- my $u = shift;
- return $u->{journaltype} eq "I";
-}
-
-# return the journal type as a name
-sub journaltype_readable {
- my $u = shift;
-
- return {
- R => 'redirect',
- I => 'identity',
- P => 'personal',
- S => 'shared',
- Y => 'syndicated',
- N => 'news',
- C => 'community',
- }->{$u->{journaltype}};
-}
-
-*has_friend = \&is_friend;
-sub is_friend {
- confess 'LJ::User->is_friend is deprecated';
-}
-
-sub is_mutual_friend { confess 'LJ::User->is_mutual_friend is deprecated';
-}
-
-sub who_invited {
- my $u = shift;
- my $inviterid = LJ::load_rel_user($u, 'I');
-
- return LJ::load_userid($inviterid);
-}
-
-# front-end to LJ::cmd_buffer_add, which has terrible interface
-# cmd: scalar
-# args: hashref
-sub cmd_buffer_add {
- my ($u, $cmd, $args) = @_;
- $args ||= {};
- return LJ::cmd_buffer_add($u->{clusterid}, $u->{userid}, $cmd, $args);
-}
-
-sub subscriptions {
- my $u = shift;
- return LJ::Subscription->subscriptions_of_user($u);
-}
-
-sub subscription_count {
- my $u = shift;
- return scalar LJ::Subscription->subscriptions_of_user($u);
-}
-
-# this is the count used to check the maximum subscription count
-sub active_inbox_subscription_count {
- my $u = shift;
- return scalar ( grep { $_->active && $_->enabled } $u->find_subscriptions(method => 'Inbox') );
-}
-
-sub max_subscriptions {
- my $u = shift;
- return $u->get_cap('subscriptions');
-}
-
-sub can_add_inbox_subscription {
- my $u = shift;
- return $u->active_inbox_subscription_count >= $u->max_subscriptions ? 0 : 1;
-}
-
-# subscribe to an event
-sub subscribe {
- my ($u, %opts) = @_;
- croak "No subscription options" unless %opts;
-
- return LJ::Subscription->create($u, %opts);
-}
-
-sub subscribe_entry_comments_via_sms {
- my ($u, $entry) = @_;
- croak "Invalid LJ::Entry passed"
- unless $entry && $entry->isa("LJ::Entry");
-
- # don't subscribe if user is over subscription limit
- return unless $u->can_add_inbox_subscription;
-
- my %sub_args =
- ( event => "LJ::Event::JournalNewComment",
- journal => $u,
- arg1 => $entry->ditemid, );
-
- $u->subscribe
- ( method => "LJ::NotificationMethod::SMS",
- %sub_args, );
-
- $u->subscribe
- ( method => "LJ::NotificationMethod::Inbox",
- %sub_args, );
-
- return 1;
-}
-
-# search for a subscription
-*find_subscriptions = \&has_subscription;
-sub has_subscription {
- my ($u, %params) = @_;
- croak "No parameters" unless %params;
-
- return LJ::Subscription->find($u, %params);
-}
-
-# interim solution while legacy/ESN notifications are both happening:
-# checks possible subscriptions to see if user will get an ESN notification
-# THIS IS TEMPORARY. should only be called by talklib.
-# params: journal, arg1 (entry ditemid), arg2 (comment talkid)
-sub gets_notified {
- my ($u, %params) = @_;
-
- $params{event} = "LJ::Event::JournalNewComment";
- $params{method} = "LJ::NotificationMethod::Email";
-
- my $has_sub;
-
- # did they subscribe to the parent comment?
- $has_sub = LJ::Subscription->find($u, %params);
- return $has_sub if $has_sub;
-
- # remove the comment-specific parameter, then check for an entry subscription
- $params{arg2} = 0;
- $has_sub = LJ::Subscription->find($u, %params);
- return $has_sub if $has_sub;
-
- # remove the entry-specific parameter, then check if they're subscribed to the entire journal
- $params{arg1} = 0;
- $has_sub = LJ::Subscription->find($u, %params);
- return $has_sub;
-}
-
-# delete all of a user's subscriptions
-sub delete_all_subscriptions {
- my $u = shift;
- return LJ::Subscription->delete_all_subs($u);
-}
-
-# delete all of a user's subscriptions
-sub delete_all_inactive_subscriptions {
- my $u = shift;
- my $dryrun = shift;
- return LJ::Subscription->delete_all_inactive_subs($u, $dryrun);
-}
-
-# 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;
-}
-
-# can $u post to $targetu?
-sub can_post_to {
- my ($u, $targetu) = @_;
-
- return LJ::can_use_journal($u->id, $targetu->user);
-}
-
-sub delete_and_purge_completely {
- my $u = shift;
- # TODO: delete from user tables
- # TODO: delete from global tables
- my $dbh = LJ::get_db_writer();
-
- my @tables = qw(user useridmap reluser priv_map infohistory email password);
- foreach my $table (@tables) {
- $dbh->do("DELETE FROM $table WHERE userid=?", undef, $u->id);
- }
-
- $dbh->do("DELETE FROM wt_edges WHERE from_userid = ? OR to_userid = ?", undef, $u->id, $u->id);
- $dbh->do("DELETE FROM reluser WHERE targetid=?", undef, $u->id);
- $dbh->do("DELETE FROM email_aliases WHERE alias=?", undef, $u->user . "\@$LJ::USER_DOMAIN");
-
- $dbh->do("DELETE FROM community WHERE userid=?", undef, $u->id)
- if $u->is_community;
- $dbh->do("DELETE FROM syndicated WHERE userid=?", undef, $u->id)
- if $u->is_syndicated;
- $dbh->do("DELETE FROM content_flag WHERE journalid=? OR reporterid=?", undef, $u->id, $u->id);
-
- return 1;
-}
-
-# 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
-}
-
-# do some internal consistency checks on self. die if problems,
-# else returns 1.
-sub selfassert {
- my $u = shift;
- LJ::assert_is($u->{userid}, $u->{_orig_userid})
- if $u->{_orig_userid};
- LJ::assert_is($u->{user}, $u->{_orig_user})
- if $u->{_orig_user};
- return 1;
-}
-
-# Returns the NotificationInbox for this user
-*inbox = \¬ification_inbox;
-sub notification_inbox {
- my $u = shift;
- return LJ::NotificationInbox->new($u);
-}
-
-sub new_message_count {
- my $u = shift;
- my $inbox = $u->notification_inbox;
- my $count = $inbox->unread_count;
-
- return $count || 0;
-}
-
-sub notification_archive {
- my $u = shift;
- return LJ::NotificationArchive->new($u);
-}
-
-# 1/0 if someone can send a message to $u
-sub can_receive_message {
- my ($u, $sender) = @_;
-
- my $opt_usermsg = $u->opt_usermsg;
- return 0 if $opt_usermsg eq 'N' || !$sender;
- return 0 if $u->has_banned($sender);
- return 0 if $opt_usermsg eq 'M' && !$u->mutually_trusts($sender);
- return 0 if $opt_usermsg eq 'F' && !$u->trusts($sender);
-
- return 1;
-}
-
-# opt_usermsg options
-# Y - Registered Users
-# F - Trusted Users
-# M - Mutually Trusted Users
-# N - Nobody
-sub opt_usermsg {
- my $u = shift;
-
- if ($u->raw_prop('opt_usermsg') =~ /^(Y|F|M|N)$/) {
- return $u->raw_prop('opt_usermsg');
- } else {
- return 'N' if $u->underage || $u->is_child;
- return 'M' if $u->is_minor;
- return 'Y';
- }
-}
-
-sub add_friend {
- confess 'LJ::User->add_friend deprecated.';
-}
-
-sub friend_and_watch {
- confess 'LJ::User->friend_and_watch deprecated.';
-}
-
-sub remove_friend {
- confess 'LJ::User->remove_friend has been deprecated.';
-}
-
-sub view_control_strip {
- my $u = shift;
-
- LJ::run_hook('control_strip_propcheck', $u, 'view_control_strip') unless $LJ::DISABLED{control_strip_propcheck};
-
- my $prop = $u->raw_prop('view_control_strip');
- return 0 if $prop =~ /^off/;
-
- return 'dark' if $prop eq 'forced';
-
- return $prop;
-}
-
-sub show_control_strip {
- my $u = shift;
-
- LJ::run_hook('control_strip_propcheck', $u, 'show_control_strip') unless $LJ::DISABLED{control_strip_propcheck};
-
- my $prop = $u->raw_prop('show_control_strip');
- return 0 if $prop =~ /^off/;
-
- return 'dark' if $prop eq 'forced';
-
- return $prop;
-}
-
-# when was this account created?
-# returns unixtime
-sub timecreate {
- my $u = shift;
-
- return $u->{_cache_timecreate} if $u->{_cache_timecreate};
-
- my $memkey = [$u->id, "tc:" . $u->id];
- my $timecreate = LJ::MemCache::get($memkey);
- if ($timecreate) {
- $u->{_cache_timecreate} = $timecreate;
- return $timecreate;
- }
-
- my $dbr = LJ::get_db_reader() or die "No db";
- my $when = $dbr->selectrow_array("SELECT timecreate FROM userusage WHERE userid=?", undef, $u->id);
-
- $timecreate = LJ::mysqldate_to_time($when);
- $u->{_cache_timecreate} = $timecreate;
- LJ::MemCache::set($memkey, $timecreate, 60*60*24);
-
- return $timecreate;
-}
-
-# when was last time this account updated?
-# returns unixtime
-sub timeupdate {
- my $u = shift;
- my $timeupdate = LJ::get_timeupdate_multi($u->id);
- return $timeupdate->{$u->id};
-}
-
-# can this user use ESN?
-sub can_use_esn {
- my $u = shift;
- return 0 if $LJ::DISABLED{esn};
- my $disable = $LJ::DISABLED{esn_ui};
- return 1 unless $disable;
-
- if (ref $disable eq 'CODE') {
- return $disable->($u) ? 0 : 1;
- }
-
- return $disable ? 0 : 1;
-}
-
-sub can_use_sms {
- my $u = shift;
- return LJ::SMS->can_use_sms($u);
-}
-
-sub ajax_auth_token {
- my $u = shift;
- return LJ::Auth->ajax_auth_token($u, @_);
-}
-
-sub check_ajax_auth_token {
- my $u = shift;
- return LJ::Auth->check_ajax_auth_token($u, @_);
-}
-
-# returns username
-*username = \&user;
-sub user {
- my $u = shift;
- return $u->{user};
-}
-
-sub user_url_arg {
- my $u = shift;
- return "I,$u->{userid}" if $u->{journaltype} eq "I";
- return $u->{user};
-}
-
-# returns username for display
-sub display_username {
- my $u = shift;
- return $u->display_name if $u->is_identity;
- return $u->{user};
-}
-
-# returns the user-specified name of a journal exactly as entered
-sub name_orig {
- my $u = shift;
- return $u->{name};
-}
-
-# returns the user-specified name of a journal in valid UTF-8
-sub name_raw {
- my $u = shift;
- LJ::text_out(\$u->{name});
- return $u->{name};
-}
-
-# returns the user-specified name of a journal in valid UTF-8
-# and with HTML escaped
-sub name_html {
- my $u = shift;
- return LJ::ehtml($u->name_raw);
-}
-
-# userid
-*userid = \&id;
-sub id {
- return $_[0]->{userid};
-}
-
-sub clusterid {
- return $_[0]->{clusterid};
-}
-
-# class method, returns { clusterid => [ uid, uid ], ... }
-sub split_by_cluster {
- my $class = shift;
-
- my @uids = @_;
- my $us = LJ::load_userids(@uids);
-
- my %clusters;
- foreach my $u (values %$us) {
- next unless $u;
- push @{$clusters{$u->clusterid}}, $u->id;
- }
-
- return \%clusters;
-}
-
-sub bio {
- my $u = shift;
- return LJ::get_bio($u);
-}
-
-# if bio_absent is set to "yes", bio won't be updated
-sub set_bio {
- my ($u, $text, $bio_absent) = @_;
- $bio_absent = "" unless $bio_absent;
-
- my $oldbio = $u->bio;
- my $newbio = $bio_absent eq "yes" ? $oldbio : $text;
- my $has_bio = ($newbio =~ /\S/) ? "Y" : "N";
-
- my %update = (
- 'has_bio' => $has_bio,
- );
- LJ::update_user($u, \%update);
-
- # update their bio text
- if (($oldbio ne $text) && $bio_absent ne "yes") {
- if ($has_bio eq "N") {
- $u->do("DELETE FROM userbio WHERE userid=?", undef, $u->id);
- $u->dudata_set('B', 0, 0);
- } else {
- $u->do("REPLACE INTO userbio (userid, bio) VALUES (?, ?)",
- undef, $u->id, $text);
- $u->dudata_set('B', 0, length($text));
- }
- LJ::MemCache::set([$u->id, "bio:" . $u->id], $text);
- }
-}
-
-sub opt_ctxpopup {
- my $u = shift;
-
- # if unset, default to on
- my $prop = $u->raw_prop('opt_ctxpopup') || 'Y';
-
- return $prop eq 'Y';
-}
-
-sub opt_embedplaceholders {
- my $u = shift;
-
- my $prop = $u->raw_prop('opt_embedplaceholders');
-
- if (defined $prop) {
- return $prop;
- } else {
- my $imagelinks = $u->prop('opt_imagelinks');
- return $imagelinks;
- }
-}
-
-sub opt_showmutualfriends {
- my $u = shift;
- return $u->raw_prop('opt_showmutualfriends') ? 1 : 0;
-}
-
-# only certain journaltypes can show mutual friends
-sub show_mutualfriends {
- my $u = shift;
-
- return 0 unless $u->journaltype =~ /[PSI]/;
- return $u->opt_showmutualfriends ? 1 : 0;
-}
-
-sub opt_getting_started {
- my $u = shift;
-
- # if unset, default to on
- my $prop = $u->raw_prop('opt_getting_started') || 'Y';
-
- return $prop;
-}
-
-sub opt_stylealwaysmine {
- my $u = shift;
-
- return 0 unless $u->can_use_stylealwaysmine;
- return $u->raw_prop('opt_stylealwaysmine') eq 'Y' ? 1 : 0;
-}
-
-sub can_use_stylealwaysmine {
- my $u = shift;
- my $ret = 0;
-
- return 0 if $LJ::DISABLED{stylealwaysmine};
- $ret = LJ::run_hook("can_use_stylealwaysmine", $u);
- return $ret;
-}
-
-sub has_enabled_getting_started {
- my $u = shift;
-
- return $u->opt_getting_started eq 'Y' ? 1 : 0;
-}
-
-# find what servers a user is logged in to, and send them an IM
-# returns true if sent, false if failure or user not logged on
-# Please do not call from web context
-sub send_im {
- my ($self, %opts) = @_;
-
- croak "Can't call in web context" if LJ::is_web_context();
-
- my $from = delete $opts{from};
- my $msg = delete $opts{message} or croak "No message specified";
-
- croak "No from or bot jid defined" unless $from || $LJ::JABBER_BOT_JID;
-
- my @resources = keys %{LJ::Jabber::Presence->get_resources($self)} or return 0;
-
- my $res = $resources[0] or return 0; # FIXME: pick correct server based on priority?
- my $pres = LJ::Jabber::Presence->new($self, $res) or return 0;
- my $ip = $LJ::JABBER_SERVER_IP || '127.0.0.1';
-
- my $sock = IO::Socket::INET->new(PeerAddr => "${ip}:5200")
- or return 0;
-
- my $vhost = $LJ::DOMAIN;
-
- my $to_jid = $self->user . '@' . $LJ::DOMAIN;
- my $from_jid = $from ? $from->user . '@' . $LJ::DOMAIN : $LJ::JABBER_BOT_JID;
-
- my $emsg = LJ::exml($msg);
- my $stanza = LJ::eurl(qq{<message to="$to_jid" from="$from_jid"><body>$emsg</body></message>});
-
- print $sock "send_stanza $vhost $to_jid $stanza\n";
-
- my $start_time = time();
-
- while (1) {
- my $rin = '';
- vec($rin, fileno($sock), 1) = 1;
- select(my $rout=$rin, undef, undef, 1);
- if (vec($rout, fileno($sock), 1)) {
- my $ln = <$sock>;
- return 1 if $ln =~ /^OK/;
- }
-
- last if time() > $start_time + 5;
- }
-
- return 0;
-}
-
-# returns whether or not the user is online on jabber
-sub jabber_is_online {
- my $u = shift;
-
- return keys %{LJ::Jabber::Presence->get_resources($u)} ? 1 : 0;
-}
-
-sub esn_inbox_default_expand {
- my $u = shift;
-
- my $prop = $u->raw_prop('esn_inbox_default_expand');
- return $prop ne 'N';
-}
-
-sub rate_log {
- my ($u, $ratename, $count, $opts) = @_;
- LJ::rate_log($u, $ratename, $count, $opts);
-}
-
-sub rate_check {
- my ($u, $ratename, $count, $opts) = @_;
- LJ::rate_check($u, $ratename, $count, $opts);
-}
-
-sub statusvis {
- my $u = shift;
- return $u->{statusvis};
-}
-
-sub statusvisdate {
- my $u = shift;
- return $u->{statusvisdate};
-}
-
-sub statusvisdate_unix {
- my $u = shift;
- return LJ::mysqldate_to_time($u->{statusvisdate});
-}
-
-# set_statusvis only change statusvis parameter, all accompanied actions are done in set_* methods
-sub set_statusvis {
- my ($u, $statusvis) = @_;
-
- croak "Invalid statusvis: $statusvis"
- unless $statusvis =~ /^(?:
- V| # visible
- D| # deleted
- X| # expunged
- S| # suspended
- L| # locked
- M| # memorial
- O| # read-only
- R # renamed
- )$/x;
-
- # log the change to userlog
- $u->log_event('accountstatus', {
- # remote looked up by log_event
- old => $u->statusvis,
- new => $statusvis,
- });
-
- # do update
- return LJ::update_user($u, { statusvis => $statusvis,
- raw => 'statusvisdate=NOW()' });
-}
-
-sub set_visible {
- my $u = shift;
- return $u->set_statusvis('V');
-}
-
-sub set_deleted {
- my $u = shift;
- my $res = $u->set_statusvis('D');
-
- # run any account cancellation hooks
- LJ::run_hooks("account_delete", $u);
- return $res;
-}
-
-sub set_expunged {
- my $u = shift;
- return $u->set_statusvis('X');
-}
-
-sub set_suspended {
- my ($u, $who, $reason, $errref) = @_;
- die "Not enough parameters for LJ::User::set_suspended call" unless $who and $reason;
-
- my $res = $u->set_statusvis('S');
- unless ($res) {
- $$errref = "DB error while setting statusvis to 'S'" if ref $errref;
- return $res;
- }
-
- LJ::statushistory_add($u, $who, "suspend", $reason);
-
- eval { $u->fb_push };
- warn "Error running fb_push: $@\n" if $@ && $LJ::IS_DEV_SERVER;
-
- LJ::run_hooks("account_cancel", $u);
-
- if (my $err = LJ::run_hook("cdn_purge_userpics", $u)) {
- $$errref = $err if ref $errref and $err;
- return 0;
- }
-
- return $res; # success
-}
-
-# sets a user to visible, but also does all of the stuff necessary when a suspended account is unsuspended
-# this can only be run on a suspended account
-sub set_unsuspended {
- my ($u, $who, $reason, $errref) = @_;
- die "Not enough parameters for LJ::User::set_unsuspended call" unless $who and $reason;
-
- unless ($u->is_suspended) {
- $$errref = "User isn't suspended" if ref $errref;
- return 0;
- }
-
- my $res = $u->set_statusvis('V');
- unless ($res) {
- $$errref = "DB error while setting statusvis to 'V'" if ref $errref;
- return $res;
- }
-
- LJ::statushistory_add($u, $who, "unsuspend", $reason);
-
- eval { $u->fb_push };
- warn "Error running fb_push: $@\n" if $@ && $LJ::IS_DEV_SERVER;
-
- return $res; # success
-}
-
-sub set_locked {
- my $u = shift;
- return $u->set_statusvis('L');
-}
-
-sub set_memorial {
- my $u = shift;
- return $u->set_statusvis('M');
-}
-
-sub set_readonly {
- my $u = shift;
- return $u->set_statusvis('O');
-}
-
-sub set_renamed {
- my $u = shift;
- return $u->set_statusvis('R');
-}
-
-# returns if this user is considered visible
-sub is_visible {
- my $u = shift;
- return $u->statusvis eq 'V';
-}
-
-sub is_deleted {
- my $u = shift;
- return $u->statusvis eq 'D';
-}
-
-sub is_expunged {
- my $u = shift;
- return $u->statusvis eq 'X' || $u->clusterid == 0;
-}
-
-sub is_suspended {
- my $u = shift;
- return $u->statusvis eq 'S';
-}
-
-sub is_locked {
- my $u = shift;
- return $u->statusvis eq 'L';
-}
-
-sub is_memorial {
- my $u = shift;
- return $u->statusvis eq 'M';
-}
-
-sub is_readonly {
- my $u = shift;
- return $u->statusvis eq 'O';
-}
-
-sub is_renamed {
- my $u = shift;
- return $u->statusvis eq 'R';
-}
-
-sub caps {
- my $u = shift;
- return $u->{caps};
-}
-
-*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 $memkey = [$u->{userid}, "log2ct:$u->{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, $u->{userid});
- }, $expire);
-}
-
-# return the number of posts that the user actually posted themselves
-sub number_of_posted_posts {
- my $u = shift;
-
- my $num = $u->number_of_posts;
- $num-- if LJ::run_hook('user_has_auto_post', $u);
-
- return $num;
-}
-
-# <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
-# TODO: 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;
- }
-}
-
-sub password {
- my $u = shift;
- $u->{_password} ||= LJ::MemCache::get_or_set([$u->{userid}, "pw:$u->{userid}"], sub {
- my $dbh = LJ::get_db_writer() or die "Couldn't get db master";
- return $dbh->selectrow_array("SELECT password FROM password WHERE userid=?",
- undef, $u->id);
- });
- return $u->{_password};
-}
-
-sub journaltype {
- my $u = shift;
- return $u->{journaltype};
-}
-
-sub set_password {
- my ($u, $password) = @_;
- return LJ::set_password($u->id, $password);
-}
-
-sub set_email {
- my ($u, $email) = @_;
- return LJ::set_email($u->id, $email);
-}
-
-
-sub fb_push {
- my $u = shift;
- return unless $u && $u->get_cap("fb_account");
- return Apache::LiveJournal::Interface::FotoBilder::push_user_info( $u->id );
-}
-
-sub grant_priv {
- my ($u, $priv, $arg) = @_;
- $arg ||= "";
- my $dbh = LJ::get_db_writer();
-
- return 1 if LJ::check_priv($u, $priv, $arg);
-
- my $privid = $dbh->selectrow_array("SELECT prlid FROM priv_list".
- " WHERE privcode = ?", undef, $priv);
- return 0 unless $privid;
-
- $dbh->do("INSERT INTO priv_map (userid, prlid, arg) VALUES (?, ?, ?)",
- undef, $u->id, $privid, $arg);
- return 0 if $dbh->err;
-
- undef $u->{'_privloaded'}; # to force reloading of privs later
- return 1;
-}
-
-sub revoke_priv {
- my ($u, $priv, $arg) = @_;
- $arg ||="";
- my $dbh = LJ::get_db_writer();
-
- return 1 unless LJ::check_priv($u, $priv, $arg);
-
- my $privid = $dbh->selectrow_array("SELECT prlid FROM priv_list".
- " WHERE privcode = ?", undef, $priv);
- return 0 unless $privid;
-
- $dbh->do("DELETE FROM priv_map WHERE userid = ? AND prlid = ? AND arg = ?",
- undef, $u->id, $privid, $arg);
- return 0 if $dbh->err;
-
- undef $u->{'_privloaded'}; # to force reloading of privs later
- undef $u->{'_priv'};
- return 1;
-}
-
-sub revoke_priv_all {
- my ($u, $priv) = @_;
- my $dbh = LJ::get_db_writer();
-
- my $privid = $dbh->selectrow_array("SELECT prlid FROM priv_list".
- " WHERE privcode = ?", undef, $priv);
- return 0 unless $privid;
-
- $dbh->do("DELETE FROM priv_map WHERE userid = ? AND prlid = ?",
- undef, $u->id, $privid);
- return 0 if $dbh->err;
-
- undef $u->{'_privloaded'}; # to force reloading of privs later
- undef $u->{'_priv'};
- return 1;
-}
-
-# must be called whenever birthday, location, journal modtime, journaltype, etc.
-# changes. see LJ/Directory/PackedUserRecord.pm
-sub invalidate_directory_record {
- my $u = shift;
-
- # Future: ?
- # LJ::try_our_best_to("invalidate_directory_record", $u->id);
- # then elsewhere, map that key to subref. if primary run fails,
- # put in schwartz, then have one worker (misc-deferred) to
- # redo...
-
- my $dbs = defined $LJ::USERSEARCH_DB_WRITER ? LJ::get_dbh($LJ::USERSEARCH_DB_WRITER) : LJ::get_db_writer();
- $dbs->do("UPDATE usersearch_packdata SET good_until=0 WHERE userid=?",
- undef, $u->id);
-}
-
-# Used to promote communities in interest search results
-sub render_promo_of_community {
- my ($comm, $style) = @_;
-
- return undef unless $comm;
-
- $style ||= 'Vertical';
-
- # get the ljuser link
- my $commljuser = $comm->ljuser_display;
-
- # link to journal
- my $journal_base = $comm->journal_base;
-
- # get default userpic if any
- my $userpic = $comm->userpic;
- my $userpic_html = '';
- if ($userpic) {
- my $userpic_url = $userpic->url;
- $userpic_html = qq { <a href="$journal_base"><img src="$userpic_url" /></a> };
- }
-
- my $blurb = $comm->prop('comm_promo_blurb') || '';
-
- my $join_link = "$LJ::SITEROOT/community/join.bml?comm=$comm->{user}";
- my $watch_link = "$LJ::SITEROOT/manage/circle/add.bml?user=$comm->{user}&action=subscribe";
- my $read_link = $comm->journal_base;
-
- LJ::need_res("stc/lj_base.css");
-
- # if horizontal, userpic needs to come before everything
- my $box_class;
- my $comm_display;
-
- if (lc $style eq 'horizontal') {
- $box_class = 'Horizontal';
- $comm_display = qq {
- <div class="Userpic">$userpic_html</div>
- <div class="Title">LJ Community Promo</div>
- <div class="CommLink">$commljuser</div>
- };
- } else {
- $box_class = 'Vertical';
- $comm_display = qq {
- <div class="Title">LJ Community Promo</div>
- <div class="CommLink">$commljuser</div>
- <div class="Userpic">$userpic_html</div>
- };
- }
-
-
- my $html = qq {
- <div class="CommunityPromoBox">
- <div class="$box_class">
- $comm_display
- <div class="Blurb">$blurb</div>
- <div class="Links"><a href="$join_link">Join</a> | <a href="$watch_link">Watch</a> |
- <a href="$read_link">Read</a></div>
-
- <div class='ljclear'> </div>
- </div>
- </div>
- };
-
- return $html;
-}
-
-sub can_expunge {
- my $u = shift;
-
- # must be already deleted
- return 0 unless $u->is_deleted;
-
- # and deleted 30 days ago
- my $expunge_days = LJ::conf_test($LJ::DAYS_BEFORE_EXPUNGE) || 30;
- return 0 unless $u->statusvisdate_unix < time() - 86400*$expunge_days;
-
- my $hook_rv = 0;
- if (LJ::are_hooks("can_expunge_user", $u)) {
- $hook_rv = LJ::run_hook("can_expunge_user", $u);
- return $hook_rv ? 1 : 0;
- }
-
- return 1;
-}
-
-# Check to see if the user can use eboxes at all
-sub can_use_ebox {
- my $u = shift;
-
- return ref $LJ::DISABLED{ebox} ? !$LJ::DISABLED{ebox}->($u) : !$LJ::DISABLED{ebox};
-}
-
-# Allow users to choose eboxes if:
-# 1. The entire ebox feature isn't disabled AND
-# 2. The option to choose eboxes isn't disabled OR
-# 3. The option to choose eboxes is disabled AND
-# 4. The user already has eboxes turned on
-sub can_use_ebox_ui {
- my $u = shift;
- my $allow_ebox = 1;
-
- if ($LJ::DISABLED{ebox_option}) {
- $allow_ebox = $u->prop('journal_box_entries');
- }
-
- return $u->can_use_ebox && $allow_ebox;
-}
-
-# return hashref with intname => intid
-sub interests {
- my $u = shift;
- my $uints = LJ::get_interests($u);
- my %interests;
-
- foreach my $int (@$uints) {
- $interests{$int->[1]} = $int->[0]; # $interests{name} = intid
- }
-
- return \%interests;
-}
-
-sub interest_list {
- my $u = shift;
-
- return map { $_->[1] } @{ LJ::get_interests($u) };
-}
-
-sub interest_count {
- my $u = shift;
-
- # FIXME: fall back to SELECT COUNT(*) if not cached already?
- return scalar @{LJ::get_interests($u, { justids => 1 })};
-}
-
-sub set_interests {
- my $u = shift;
- LJ::set_interests($u, @_);
-}
-
-sub lazy_interests_cleanup {
- my $u = shift;
-
- my $dbh = LJ::get_db_writer();
-
- if ($u->is_community) {
- $dbh->do("INSERT IGNORE INTO comminterests SELECT * FROM userinterests WHERE userid=?", undef, $u->id);
- $dbh->do("DELETE FROM userinterests WHERE userid=?", undef, $u->id);
- } else {
- $dbh->do("INSERT IGNORE INTO userinterests SELECT * FROM comminterests WHERE userid=?", undef, $u->id);
- $dbh->do("DELETE FROM comminterests WHERE userid=?", undef, $u->id);
- }
-
- LJ::memcache_kill($u, "intids");
- return 1;
-}
-
-# this will return a hash of information about this user.
-# this is useful for JavaScript endpoints which need to dump
-# JSON data about users.
-sub info_for_js {
- my $u = shift;
-
- my %ret = (
- username => $u->user,
- display_username => $u->display_username,
- display_name => $u->display_name,
- userid => $u->userid,
- url_journal => $u->journal_base,
- url_profile => $u->profile_url,
- url_allpics => $u->allpics_base,
- ljuser_tag => $u->ljuser_display,
- is_comm => $u->is_comm,
- is_person => $u->is_person,
- is_syndicated => $u->is_syndicated,
- is_identity => $u->is_identity,
- is_shared => $u->is_shared,
- );
- # Without url_message "Send Message" link should not display
- $ret{url_message} = $u->message_url unless ($u->opt_usermsg eq 'N');
-
- LJ::run_hook("extra_info_for_js", $u, \%ret);
-
- my $up = $u->userpic;
-
- if ($up) {
- $ret{url_userpic} = $up->url;
- $ret{userpic_w} = $up->width;
- $ret{userpic_h} = $up->height;
- }
-
- return %ret;
-}
-
-sub postreg_completed {
- my $u = shift;
-
- return 0 unless $u->bio;
- return 0 unless $u->interest_count;
- return 1;
-}
-
-# return if $target is banned from $u's journal
-*has_banned = \&is_banned;
-sub is_banned {
- my ($u, $target) = @_;
- return LJ::is_banned($target->userid, $u->userid);
-}
-
-sub ban_user {
- my ($u, $ban_u) = @_;
-
- my $remote = LJ::get_remote();
- $u->log_event('ban_set', { actiontarget => $ban_u->id, remote => $remote });
-
- return LJ::set_rel($u->id, $ban_u->id, 'B');
-}
-
-sub ban_user_multi {
- my ($u, @banlist) = @_;
-
- LJ::set_rel_multi(map { [$u->id, $_, 'B'] } @banlist);
-
- my $us = LJ::load_userids(@banlist);
- foreach my $banuid (@banlist) {
- $u->log_event('ban_set', { actiontarget => $banuid, remote => LJ::get_remote() });
- LJ::run_hooks('ban_set', $u, $us->{$banuid}) if $us->{$banuid};
- }
-
- return 1;
-}
-
-sub unban_user_multi {
- my ($u, @unbanlist) = @_;
-
- LJ::clear_rel_multi(map { [$u->id, $_, 'B'] } @unbanlist);
-
- my $us = LJ::load_userids(@unbanlist);
- foreach my $banuid (@unbanlist) {
- $u->log_event('ban_unset', { actiontarget => $banuid, remote => LJ::get_remote() });
- LJ::run_hooks('ban_unset', $u, $us->{$banuid}) if $us->{$banuid};
- }
-
- return 1;
-}
-
-# returns if this user's polls are clustered
-sub polls_clustered {
- my $u = shift;
- return $u->dversion >= 8;
-}
-
-sub dversion {
- my $u = shift;
- return $u->{dversion};
-}
-
-# take a user on dversion 7 and upgrade them to dversion 8 (clustered polls)
-sub upgrade_to_dversion_8 {
- my $u = shift;
- my $dbh = shift;
- my $dbhslo = shift;
- my $dbcm = shift;
-
- # If user has been purged, go ahead and update version
- # Otherwise move their polls
- my $ok = $u->is_expunged ? 1 : LJ::Poll->make_polls_clustered($u, $dbh, $dbhslo, $dbcm);
-
- LJ::update_user($u, { 'dversion' => 8 }) if $ok;
-
- return $ok;
-}
-
-# returns if this user can join an adult community or not
-# adultref will hold the value of the community's adult content flag
-sub can_join_adult_comm {
- my ($u, %opts) = @_;
-
- return 1 unless LJ::is_enabled('content_flag');
-
- my $adultref = $opts{adultref};
- my $comm = $opts{comm} or croak "No community passed";
-
- my $adult_content = $comm->adult_content_calculated;
- $$adultref = $adult_content;
-
- if ($adult_content eq "concepts" && ($u->is_child || !$u->best_guess_age)) {
- return 0;
- } elsif ($adult_content eq "explicit" && ($u->is_minor || !$u->best_guess_age)) {
- return 0;
- }
-
- return 1;
-}
-
-sub is_in_beta {
- my ($u, $key) = @_;
- return LJ::BetaFeatures->user_in_beta( $u => $key );
-}
-
-# return the user's timezone based on the prop if it's defined, otherwise best guess
-sub timezone {
- my $u = shift;
-
- my $offset = 0;
- LJ::get_timezone($u, \$offset);
- return $offset;
-}
-
-# returns a DateTime object corresponding to a user's "now"
-sub time_now {
- my $u = shift;
-
- my $now = DateTime->now;
-
- # if user has timezone, use it!
- my $tz = $u->prop("timezone");
- return $now unless $tz;
-
- $now = eval { DateTime->from_epoch(
- epoch => time(),
- time_zone => $tz,
- );
- };
-
- return $now;
-}
-
-sub can_admin_content_flagging {
+
+sub age_for_adcall {
+ my $u = shift;
+ croak "Invalid user object" unless LJ::isu($u);
+
+ return undef if $u->underage;
+ return eval {$u->age || $u->init_age};
+}
+
+
+sub gender_for_adcall {
+ my $u = shift;
+ croak "Invalid user object" unless LJ::isu($u);
+
+ my $gender = $u->prop('gender');
+ if ($gender && $gender !~ /^[UO]/i) {
+ return uc(substr($gender, 0, 1)); # M|F
+ }
+
+ return "unspecified";
+}
+
+
+sub notable_interests {
+ my ($u, $n) = @_;
+ $n ||= 20;
+
+ # arrayref of arrayrefs of format [intid, intname, intcount];
+ my $ints = LJ::get_interests($u)
+ or return ();
+
+ my @ints = map { $_->[1] } @$ints;
+
+ # sorta arrayref inline
+ LJ::AdTargetedInterests->sort_interests(\@ints);
+
+ return @ints[0..$n-1] if @ints > $n;
+ return @ints;
+}
+
+
+sub qct_value_for_ads {
my $u = shift;
return 0 unless LJ::is_enabled("content_flag");
- return 1 if $LJ::IS_DEV_SERVER;
- return LJ::check_priv($u, "siteadmin", "contentflag");
-}
-
-sub can_see_content_flag_button {
- my $u = shift;
- my %opts = @_;
-
- return 0 unless LJ::is_enabled("content_flag");
-
- my $content = $opts{content};
-
- # user can't flag any journal they manage nor any entry they posted
- # user also can't flag non-public entries
- if (LJ::isu($content)) {
- return 0 if $u->can_manage($content);
- } elsif ($content->isa("LJ::Entry")) {
- return 0 if $u->equals($content->poster);
- return 0 unless $content->security eq "public";
- }
-
- # user can't flag anything if their account isn't at least one month old
- my $one_month = 60*60*24*30;
- return 0 unless time() - $u->timecreate >= $one_month;
-
- return 1;
-}
-
-sub can_flag_content {
- my $u = shift;
- my %opts = @_;
-
- return 0 unless $u->can_see_content_flag_button(%opts);
- return 0 if LJ::sysban_check("contentflag", $u->user);
- return 0 unless $u->rate_check("ctflag", 1);
- return 1;
-}
-
-# sometimes when the app throws errors, we want to display "nice"
-# text to end-users, while allowing admins to view the actual error message
-sub show_raw_errors {
- my $u = shift;
-
- return 1 if $LJ::IS_DEV_SERVER;
- return 1 if $LJ::ENABLE_BETA_TOOLS;
-
- return 1 if LJ::check_priv($u, "supporthelp");
- return 1 if LJ::check_priv($u, "supportviewscreened");
- return 1 if LJ::check_priv($u, "siteadmin");
-
- return 0;
-}
-
-# defined by the user
-# returns 'none', 'concepts' or 'explicit'
-sub adult_content {
- my $u = shift;
-
- my $prop_value = $u->prop('adult_content');
-
- return $prop_value ? $prop_value : "none";
-}
-
-# defuned by the user
-sub adult_content_reason {
- my $u = shift;
-
- return $u->prop('adult_content_reason');
-}
-
-# defined by an admin
-sub admin_content_flag {
- my $u = shift;
-
- return $u->prop('admin_content_flag');
-}
-
-# uses both user- and admin-defined props to figure out the adult content level
-sub adult_content_calculated {
- my $u = shift;
-
- return "explicit" if $u->admin_content_flag eq "explicit_adult";
- return $u->adult_content;
-}
-
-# returns who marked the entry as the 'adult_content_calculated' adult content level
-sub adult_content_marker {
- my $u = shift;
-
- return "admin" if $u->admin_content_flag eq "explicit_adult";
- return "journal";
-}
-
-sub can_manage {
- my ($u, $target) = @_;
- return LJ::can_manage($u, $target);
-}
-
-sub hide_adult_content {
- my $u = shift;
-
- my $prop_value = $u->prop('hide_adult_content');
-
- if ($u->is_child || !$u->best_guess_age) {
- return "concepts";
- }
-
- if ($u->is_minor && $prop_value ne "concepts") {
- return "explicit";
- }
-
- return $prop_value ? $prop_value : "none";
-}
-
-# returns a number that represents the user's chosen search filtering level
-# 0 = no filtering
-# 1-10 = moderate filtering
-# >10 = strict filtering
-sub safe_search {
- my $u = shift;
-
- my $prop_value = $u->prop('safe_search');
-
- # current user 18+ default is 0
- # current user <18 default is 10
- # new user default (prop value is "nu_default") is 10
- return 0 if $prop_value eq "none";
- return $prop_value if $prop_value && $prop_value =~ /^\d+$/;
- return 0 if $prop_value ne "nu_default" && $u->best_guess_age && !$u->is_minor;
- return 10;
-}
-
-# determine if the user in "for_u" should see $u in a search result
-sub should_show_in_search_results {
- my $u = shift;
- my %opts = @_;
-
- return 1 unless LJ::is_enabled("content_flag") && LJ::is_enabled("safe_search");
my $adult_content = $u->adult_content_calculated;
my $admin_flag = $u->admin_content_flag;
- my $for_u = $opts{for};
- unless (LJ::isu($for_u)) {
- return $adult_content ne "none" || $admin_flag ? 0 : 1;
- }
-
- my $safe_search = $for_u->safe_search;
- return 1 if $safe_search == 0;
-
- my $adult_content_flag_level = $LJ::CONTENT_FLAGS{$adult_content} ? $LJ::CONTENT_FLAGS{$adult_content}->{safe_search_level} : 0;
- my $admin_flag_level = $LJ::CONTENT_FLAGS{$admin_flag} ? $LJ::CONTENT_FLAGS{$admin_flag}->{safe_search_level} : 0;
-
- return 0 if $adult_content_flag_level && ($safe_search >= $adult_content_flag_level);
- return 0 if $admin_flag_level && ($safe_search >= $admin_flag_level);
- return 1;
-}
-
-sub equals {
- my ($u, $target) = @_;
-
- return LJ::u_equals($u, $target);
-}
-
-sub tags {
- my $u = shift;
-
- return LJ::Tags::get_usertags($u);
-}
-
-sub newpost_minsecurity {
- my $u = shift;
-
- return $u->prop('newpost_minsecurity') || 'public';
-}
-
-sub third_party_notify_list {
- my $u = shift;
-
- my $val = $u->prop('third_party_notify_list');
- my @services = split(',', $val);
-
- return @services;
-}
-
-# Check if the user's notify list contains a particular service
-sub third_party_notify_list_contains {
- my $u = shift;
- my $val = shift;
-
- return 1 if grep { $_ eq $val } $u->third_party_notify_list;
-
- return 0;
-}
-
-# Add a service to a user's notify list
-sub third_party_notify_list_add {
- my $u = shift;
- my $svc = shift;
- return 0 unless $svc;
-
- # Is it already there?
- return 1 if $u->third_party_notify_list_contains($svc);
-
- # Create the new list of services
- my @cur_services = $u->third_party_notify_list;
- push @cur_services, $svc;
- my $svc_list = join(',', @cur_services);
-
- # Trim a service from the list if it is too long
- if (length $svc_list > 255) {
- shift @cur_services;
- $svc_list = join(',', @cur_services)
- }
-
- # Set it
- $u->set_prop('third_party_notify_list', $svc_list);
- return 1;
-}
-
-# Remove a service to a user's notify list
-sub third_party_notify_list_remove {
- my $u = shift;
- my $svc = shift;
- return 0 unless $svc;
-
- # Is it even there?
- return 1 unless $u->third_party_notify_list_contains($svc);
-
- # Remove it!
- $u->set_prop('third_party_notify_list',
- join(',',
- grep { $_ ne $svc } $u->third_party_notify_list
- )
- );
- return 1;
-}
-
-# 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);
-}
-
-sub qct_value_for_ads {
- my $u = shift;
-
- return 0 unless LJ::is_enabled("content_flag");
-
- my $adult_content = $u->adult_content_calculated;
- my $admin_flag = $u->admin_content_flag;
-
if ($LJ::CONTENT_FLAGS{$adult_content} && $LJ::CONTENT_FLAGS{$adult_content}->{qct_value_for_ads}) {
return $LJ::CONTENT_FLAGS{$adult_content}->{qct_value_for_ads};
}
@@ -4435,83 +5107,6 @@ sub qct_value_for_ads {
return 0;
}
-sub should_block_robots {
- my $u = shift;
-
- return 1 if $u->prop('opt_blockrobots');
-
- return 0 unless LJ::is_enabled("content_flag");
-
- my $adult_content = $u->adult_content_calculated;
- my $admin_flag = $u->admin_content_flag;
-
- return 1 if $LJ::CONTENT_FLAGS{$adult_content} && $LJ::CONTENT_FLAGS{$adult_content}->{block_robots};
- return 1 if $LJ::CONTENT_FLAGS{$admin_flag} && $LJ::CONTENT_FLAGS{$admin_flag}->{block_robots};
- return 0;
-}
-
-# memcache key that holds the number of times a user performed one of the rate-limited actions
-sub rate_memkey {
- my ($u, $rp) = @_;
-
- return [$u->id, "rate:" . $u->id . ":$rp->{id}"];
-}
-
-# prepare OpenId part of html-page, if needed
-sub openid_tags {
- my $u = shift;
-
- my $head = '';
-
- # OpenID Server and Yadis
- if (LJ::OpenID->server_enabled and defined $u) {
- my $journalbase = $u->journal_base;
- $head .= qq{<link rel="openid.server" href="$LJ::OPENID_SERVER" />\n};
- $head .= qq{<meta http-equiv="X-XRDS-Location" content="$journalbase/data/yadis" />\n};
- }
-
- return $head;
-}
-
-# 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;
-}
# returns undef if there shouldn't be an option for this user
# B = show ads [B]oth to logged-out traffic on the user's journal and on the user's app pages
@@ -4527,6 +5122,7 @@ sub ad_visibility {
return $prop_val =~ /^[BJA]$/ ? $prop_val : 'B';
}
+
sub wants_ads_on_app {
my $u = shift;
@@ -4534,6 +5130,7 @@ sub wants_ads_on_app {
return $ad_visibility eq "B" || $ad_visibility eq "A" ? 1 : 0;
}
+
sub wants_ads_in_journal {
my $u = shift;
@@ -4541,550 +5138,546 @@ sub wants_ads_in_journal {
return $ad_visibility eq "B" || $ad_visibility eq "J" ? 1 : 0;
}
-# format unixtimestamp according to the user's timezone setting
-sub format_time {
- my $u = shift;
- my $time = shift;
-
- return undef unless $time;
-
- return eval { DateTime->from_epoch(epoch=>$time, time_zone=>$u->prop("timezone"))->ymd('-') } ||
- DateTime->from_epoch(epoch => $time)->ymd('-');
-}
-
-sub support_points_count {
- my $u = shift;
+
+########################################################################
+### 99C. Deprecated (FIXME: we shouldn't need these)
+
+
+# THIS IS DEPRECATED DO NOT USE
+sub email {
+ my ($u, $remote) = @_;
+ return $u->emails_visible($remote);
+}
+
+
+*has_friend = \&is_friend;
+sub is_friend {
+ confess 'LJ::User->is_friend is deprecated';
+}
+
+
+sub is_mutual_friend { confess 'LJ::User->is_mutual_friend is deprecated';
+}
+
+
+sub add_friend {
+ confess 'LJ::User->add_friend deprecated.';
+}
+
+
+sub friend_and_watch {
+ confess 'LJ::User->friend_and_watch deprecated.';
+}
+
+
+sub remove_friend {
+ confess 'LJ::User->remove_friend has been deprecated.';
+}
+
+
+sub fb_push {
+ my $u = shift;
+ return unless $u && $u->get_cap("fb_account");
+ return Apache::LiveJournal::Interface::FotoBilder::push_user_info( $u->id );
+}
+
+
+# take a user on dversion 7 and upgrade them to dversion 8 (clustered polls)
+# DW doesn't support anything earlier than dversion 8, so this can
+# probably go away at some point.
+
+# returns if this user's polls are clustered
+# DW doesn't support anything earlier than dversion 8, so this can
+# probably go away at some point.
+sub polls_clustered {
+ my $u = shift;
+ return $u->dversion >= 8;
+}
+
+
+sub upgrade_to_dversion_8 {
+ my $u = shift;
+ my $dbh = shift;
+ my $dbhslo = shift;
+ my $dbcm = shift;
+
+ # If user has been purged, go ahead and update version
+ # Otherwise move their polls
+ my $ok = $u->is_expunged ? 1 : LJ::Poll->make_polls_clustered($u, $dbh, $dbhslo, $dbcm);
+
+ LJ::update_user($u, { 'dversion' => 8 }) if $ok;
+
+ return $ok;
+}
+
+# FIXME: Needs updating for WTF
+sub opt_showmutualfriends {
+ my $u = shift;
+ return $u->raw_prop('opt_showmutualfriends') ? 1 : 0;
+}
+
+# FIXME: Needs updating for WTF
+# only certain journaltypes can show mutual friends
+sub show_mutualfriends {
+ my $u = shift;
+
+ return 0 unless $u->journaltype =~ /[PSI]/;
+ return $u->opt_showmutualfriends ? 1 : 0;
+}
+
+
+# FIXME: Needs updating for our gift shop
+# after that, it goes in section 7
+# returns the gift shop URL to buy a gift for that user
+sub gift_url {
+ my ($u, $opts) = @_;
+ croak "invalid user object passed" unless LJ::isu($u);
+ my $item = $opts->{item} ? delete $opts->{item} : '';
+
+ return "$LJ::SITEROOT/shop/view.bml?item=$item&gift=1&for=$u->{'user'}";
+}
+
+
+# FIXME: Getting Started has been removed; verify this function can go
+sub opt_getting_started {
+ my $u = shift;
+
+ # if unset, default to on
+ my $prop = $u->raw_prop('opt_getting_started') || 'Y';
+
+ return $prop;
+}
+
+
+# FIXME: We're not using TxtLJ, so this can probably go.
+# Came from section 25.
+sub subscribe_entry_comments_via_sms {
+ my ($u, $entry) = @_;
+ croak "Invalid LJ::Entry passed"
+ unless $entry && $entry->isa("LJ::Entry");
+
+ # don't subscribe if user is over subscription limit
+ return unless $u->can_add_inbox_subscription;
+
+ my %sub_args =
+ ( event => "LJ::Event::JournalNewComment",
+ journal => $u,
+ arg1 => $entry->ditemid, );
+
+ $u->subscribe
+ ( method => "LJ::NotificationMethod::SMS",
+ %sub_args, );
+
+ $u->subscribe
+ ( method => "LJ::NotificationMethod::Inbox",
+ %sub_args, );
+
+ return 1;
+}
+
+
+
+########################################################################
+### End LJ::User functions
+
+########################################################################
+### Begin LJ functions
+
+package LJ;
+
+use Carp;
+
+########################################################################
+### Please keep these categorized and alphabetized for ease of use.
+### If you need a new category, add it at the end, BEFORE category 99.
+### Categories kinda fuzzy, but better than nothing. Weird numbers are
+### to match the sections above -- please check up there if adding.
+###
+### Categories:
+### 1. Creating and Deleting Accounts
+### 3. Working with All Types of Accounts
+### 4. Login, Session, and Rename Functions
+### 5. Database and Memcache Functions
+### 6. What the App Shows to Users
+### 7. Userprops, Caps, and Displaying Content to Others
+### 8. Formatting Content Shown to Users
+### 9. Logging and Recording Actions
+### 12. Comment-Related Functions
+### 13. Community-Related Functions and Authas
+### 15. Email-Related Functions
+### 16. Entry-Related Functions
+### 17. Interest-Related Functions
+### 19. OpenID and Identity Functions
+### 21. Password Functions
+### 22. Priv-Related Functions
+### 24. Styles and S2-Related Functions
+### 28. Userpic-Related Functions
+### 99. Miscellaneous Legacy Items
+
+########################################################################
+### 1. Creating and Deleting Accounts
+
+
+# <LJFUNC>
+# name: LJ::create_account
+# des: Creates a new basic account. <strong>Note:</strong> This function is
+# not really too useful but should be extended to be useful so
+# htdocs/create.bml can use it, rather than doing the work itself.
+# returns: integer of userid created, or 0 on failure.
+# args: dbarg?, opts
+# des-opts: hashref containing keys 'user', 'name', 'password', 'email', 'caps', 'journaltype'.
+# </LJFUNC>
+sub create_account {
+ &nodb;
+ my $opts = shift;
+ my $u = LJ::User->create(%$opts)
+ or return 0;
+
+ return $u->id;
+}
+
+
+# <LJFUNC>
+# name: LJ::new_account_cluster
+# des: Which cluster to put a new account on. $DEFAULT_CLUSTER if it's
+# a scalar, random element from [ljconfig[default_cluster]] if it's arrayref.
+# also verifies that the database seems to be available.
+# returns: clusterid where the new account should be created; 0 on error
+# (such as no clusters available).
+# </LJFUNC>
+sub new_account_cluster
+{
+ # if it's not an arrayref, put it in an array ref so we can use it below
+ my $clusters = ref $LJ::DEFAULT_CLUSTER ? $LJ::DEFAULT_CLUSTER : [ $LJ::DEFAULT_CLUSTER+0 ];
+
+ # select a random cluster from the set we've chosen in $LJ::DEFAULT_CLUSTER
+ return LJ::random_cluster(@$clusters);
+}
+
+
+# returns the clusterid of a random cluster which is up
+# -- accepts @clusters as an arg to enforce a subset, otherwise
+# uses @LJ::CLUSTERS
+sub random_cluster {
+ my @clusters = @_ ? @_ : @LJ::CLUSTERS;
+
+ # iterate through the new clusters from a random point
+ my $size = @clusters;
+ my $start = int(rand() * $size);
+ foreach (1..$size) {
+ my $cid = $clusters[$start++ % $size];
+
+ # verify that this cluster is in @LJ::CLUSTERS
+ my @check = grep { $_ == $cid } @LJ::CLUSTERS;
+ next unless scalar(@check) >= 1 && $check[0] == $cid;
+
+ # try this cluster to see if we can use it, return if so
+ my $dbcm = LJ::get_cluster_master($cid);
+ return $cid if $dbcm;
+ }
+
+ # if we get here, we found no clusters that were up...
+ return 0;
+}
+
+
+########################################################################
+### 2. Working with All Types of Accounts
+
+
+# <LJFUNC>
+# name: LJ::canonical_username
+# des: normalizes username.
+# info:
+# args: user
+# returns: the canonical username given, or blank if the username is not well-formed
+# </LJFUNC>
+sub canonical_username
+{
+ my $user = shift;
+ if ($user =~ /^\s*([A-Za-z0-9_\-]{1,25})\s*$/) {
+ # perl 5.8 bug: $user = lc($1) sometimes causes corruption when $1 points into $user.
+ $user = $1;
+ $user = lc($user);
+ $user =~ s/-/_/g;
+ return $user;
+ }
+ return ""; # not a good username.
+}
+
+
+# <LJFUNC>
+# name: LJ::get_userid
+# des: Returns a userid given a username.
+# info: Results cached in memory. On miss, does DB call. Not advised
+# to use this many times in a row... only once or twice perhaps
+# per request. Tons of serialized db requests, even when small,
+# are slow. Opposite of [func[LJ::get_username]].
+# args: dbarg?, user
+# des-user: Username whose userid to look up.
+# returns: Userid, or 0 if invalid user.
+# </LJFUNC>
+sub get_userid
+{
+ &nodb;
+ my $user = shift;
+
+ $user = LJ::canonical_username($user);
+
+ if ($LJ::CACHE_USERID{$user}) { return $LJ::CACHE_USERID{$user}; }
+
+ my $userid = LJ::MemCache::get("uidof:$user");
+ return $LJ::CACHE_USERID{$user} = $userid if $userid;
my $dbr = LJ::get_db_reader();
- my $userid = $u->id;
- my $count;
-
- $count = $u->{_supportpointsum};
- return $count if defined $count;
-
- my $memkey = [$userid, "supportpointsum:$userid"];
- $count = LJ::MemCache::get($memkey);
- if (defined $count) {
- $u->{_supportpointsum} = $count;
- return $count;
- }
-
- $count = $dbr->selectrow_array("SELECT totpoints FROM supportpointsum WHERE userid=?", undef, $userid) || 0;
- $u->{_supportpointsum} = $count;
- LJ::MemCache::set($memkey, $count, 60*5);
-
- return $count;
-}
-
-sub should_show_schools_to {
- my ($u, $targetu) = @_;
-
- return 0 unless LJ::is_enabled("schools");
- return 1 if $u->{'opt_showschools'} eq '' || $u->{'opt_showschools'} eq 'Y';
- return 1 if $u->{'opt_showschools'} eq 'F' && $u->trusts( $targetu );
-
- return 0;
-}
-
-sub can_be_text_messaged_by {
- my ($u, $sender) = @_;
-
- return 0 unless $u->get_cap("textmessaging");
-
- my $security = LJ::TextMessage->tm_security($u);
-
- return 0 if $security eq "none";
- return 1 if $security eq "all";
-
- if ($sender) {
- return 1 if $security eq "reg";
- return 1 if $security eq "friends" && $u->trusts( $sender );
- }
-
- return 0;
-}
-
-# <LJFUNC>
-# name: LJ::User::rename_identity
-# des: Change an identity user's 'identity', update DB,
-# clear memcache and log change.
-# args: user
-# returns: Success or failure.
-# </LJFUNC>
-sub rename_identity {
- my $u = shift;
- return 0 unless ($u && $u->is_identity && $u->is_expunged);
-
- my $id = $u->identity;
- return 0 unless $id;
-
- my $dbh = LJ::get_db_writer();
-
- # generate a new identity value that looks like ex_oldidvalue555
- my $tempid = sub {
- my $ident = shift;
- my $idtype = shift;
- my $temp = (length($ident) > 249) ? substr($ident, 0, 249) : $ident;
- my $exid;
-
- for (1..10) {
- $exid = "ex_$temp" . int(rand(999));
-
- # check to see if this identity already exists
- unless ($dbh->selectrow_array("SELECT COUNT(*) FROM identitymap WHERE identity=? AND idtype=? LIMIT 1", undef, $exid, $idtype)) {
- # name doesn't already exist, use this one
- last;
- }
- # name existed, try and get another
-
- if ($_ >= 10) {
- return 0;
- }
- }
- return $exid;
- };
-
- my $from = $id->value;
- my $to = $tempid->($id->value, $id->typeid);
-
- return 0 unless $to;
-
- $dbh->do("UPDATE identitymap SET identity=? WHERE identity=? AND idtype=?",
- undef, $to, $from, $id->typeid);
-
- LJ::memcache_kill($u, "userid");
-
- LJ::infohistory_add($u, 'identity', $from);
-
- return 1;
-}
-
-#<LJFUNC>
-# name: LJ::User::get_renamed_user
-# des: Get the actual user of a renamed user
-# args: user
-# returns: user
-# </LJFUNC>
-sub get_renamed_user {
- my $u = shift;
- my %opts = @_;
- my $hops = $opts{hops} || 5;
-
- # Traverse the renames to the final journal
+ $userid = $dbr->selectrow_array("SELECT userid FROM useridmap WHERE user=?", undef, $user);
+
+ # implicitly create an account if we're using an external
+ # auth mechanism
+ if (! $userid && ref $LJ::AUTH_EXISTS eq "CODE")
+ {
+ $userid = LJ::create_account({ 'user' => $user,
+ 'name' => $user,
+ 'password' => '', });
+ }
+
+ if ($userid) {
+ $LJ::CACHE_USERID{$user} = $userid;
+ LJ::MemCache::set("uidof:$user", $userid);
+ }
+
+ return ($userid+0);
+}
+
+
+# <LJFUNC>
+# name: LJ::get_username
+# des: Returns a username given a userid.
+# info: Results cached in memory. On miss, does DB call. Not advised
+# to use this many times in a row... only once or twice perhaps
+# per request. Tons of serialized db requests, even when small,
+# are slow. Opposite of [func[LJ::get_userid]].
+# args: dbarg?, user
+# des-user: Username whose userid to look up.
+# returns: Userid, or 0 if invalid user.
+# </LJFUNC>
+sub get_username
+{
+ &nodb;
+ my $userid = shift;
+ $userid += 0;
+
+ # Checked the cache first.
+ if ($LJ::CACHE_USERNAME{$userid}) { return $LJ::CACHE_USERNAME{$userid}; }
+
+ # if we're using memcache, it's faster to just query memcache for
+ # an entire $u object and just return the username. otherwise, we'll
+ # go ahead and query useridmap
+ if (@LJ::MEMCACHE_SERVERS) {
+ my $u = LJ::load_userid($userid);
+ return undef unless $u;
+
+ $LJ::CACHE_USERNAME{$userid} = $u->{'user'};
+ return $u->{'user'};
+ }
+
+ my $dbr = LJ::get_db_reader();
+ my $user = $dbr->selectrow_array("SELECT user FROM useridmap WHERE userid=?", undef, $userid);
+
+ # Fall back to master if it doesn't exist.
+ unless (defined $user) {
+ my $dbh = LJ::get_db_writer();
+ $user = $dbh->selectrow_array("SELECT user FROM useridmap WHERE userid=?", undef, $userid);
+ }
+
+ return undef unless defined $user;
+
+ $LJ::CACHE_USERNAME{$userid} = $user;
+ return $user;
+}
+
+
+# is a user object (at least a hashref)
+sub isu {
+ return unless ref $_[0];
+ return 1 if UNIVERSAL::isa($_[0], "LJ::User");
+
+ if (ref $_[0] eq "HASH" && $_[0]->{userid}) {
+ carp "User HASH objects are deprecated from use." if $LJ::IS_DEV_SERVER;
+ return 1;
+ }
+}
+
+
+# <LJFUNC>
+# name: LJ::load_user
+# des: Loads a user record, from the [dbtable[user]] table, given a username.
+# args: dbarg?, user, force?
+# des-user: Username of user to load.
+# des-force: if set to true, won't return cached user object and will
+# query a dbh.
+# returns: Hashref, with keys being columns of [dbtable[user]] table.
+# </LJFUNC>
+sub load_user
+{
+ &nodb;
+ my ($user, $force) = @_;
+
+ $user = LJ::canonical_username($user);
+ return undef unless length $user;
+
+ my $get_user = sub {
+ my $use_dbh = shift;
+ my $db = $use_dbh ? LJ::get_db_writer() : LJ::get_db_reader();
+ my $u = _load_user_raw($db, "user", $user)
+ or return undef;
+
+ # set caches since we got a u from the master
+ LJ::memcache_set_u($u) if $use_dbh;
+
+ return _set_u_req_cache($u);
+ };
+
+ # caller is forcing a master, return now
+ return $get_user->("master") if $force || $LJ::_PRAGMA_FORCE_MASTER;
+
+ my $u;
+
+ # return process cache if we have one
+ if ($u = $LJ::REQ_CACHE_USER_NAME{$user}) {
+ $u->selfassert;
+ return $u;
+ }
+
+ # check memcache
+ {
+ my $uid = LJ::MemCache::get("uidof:$user");
+ $u = LJ::memcache_get_u([$uid, "userid:$uid"]) if $uid;
+ return _set_u_req_cache($u) if $u;
+ }
+
+ # try to load from master if using memcache, otherwise from slave
+ $u = $get_user->(scalar @LJ::MEMCACHE_SERVERS);
+ return $u if $u;
+
+ # setup LDAP handler if this is the first time
+ if ($LJ::LDAP_HOST && ! $LJ::AUTH_EXISTS) {
+ require LJ::LDAP;
+ $LJ::AUTH_EXISTS = sub {
+ my $user = shift;
+ my $rec = LJ::LDAP::load_ldap_user($user);
+ return $rec ? $rec : undef;
+ };
+ }
+
+ # if user doesn't exist in the LJ database, it's possible we're using
+ # an external authentication source and we should create the account
+ # implicitly.
+ my $lu;
+ if (ref $LJ::AUTH_EXISTS eq "CODE" && ($lu = $LJ::AUTH_EXISTS->($user)))
+ {
+ my $name = ref $lu eq "HASH" ? ($lu->{'nick'} || $lu->{name} || $user) : $user;
+ if (LJ::create_account({
+ 'user' => $user,
+ 'name' => $name,
+ 'email' => ref $lu eq "HASH" ? $lu->email_raw : "",
+ 'password' => "",
+ }))
+ {
+ # this should pull from the master, since it was _just_ created
+ return $get_user->("master");
+ }
+ }
+
+ return undef;
+}
+
+
+# load either a username, or a "I,<userid>" parameter.
+sub load_user_arg {
+ my ($arg) = @_;
+ my $user = LJ::canonical_username($arg);
+ return LJ::load_user($user) if length $user;
+ if ($arg =~ /^I,(\d+)$/) {
+ my $u = LJ::load_userid($1);
+ return $u if $u->is_identity;
+ }
+ return; # undef/()
+}
+
+
+sub load_user_or_identity {
+ my $arg = shift;
+
+ my $user = LJ::canonical_username($arg);
+ return LJ::load_user($user) if $user;
+
+ # return undef if not dot in arg (can't be a URL)
+ return undef unless $arg =~ /\./;
+
+ my $dbh = LJ::get_db_writer();
+ my $url = lc($arg);
+ $url = "http://$url" unless $url =~ m!^http://!;
+ $url .= "/" unless $url =~ m!/$!;
+ my $uid = $dbh->selectrow_array("SELECT userid FROM identitymap WHERE idtype=? AND identity=?",
+ undef, 'O', $url);
+ return LJ::load_userid($uid) if $uid;
+ return undef;
+}
+
+
+# <LJFUNC>
+# name: LJ::load_userid
+# des: Loads a user record, from the [dbtable[user]] table, given a userid.
+# args: dbarg?, userid, force?
+# des-userid: Userid of user to load.
+# des-force: if set to true, won't return cached user object and will
+# query a dbh
+# returns: Hashref with keys being columns of [dbtable[user]] table.
+# </LJFUNC>
+sub load_userid
+{
+ &nodb;
+ my ($userid, $force) = @_;
+ return undef unless $userid;
+
+ my $get_user = sub {
+ my $use_dbh = shift;
+ my $db = $use_dbh ? LJ::get_db_writer() : LJ::get_db_reader();
+ my $u = _load_user_raw($db, "userid", $userid)
+ or return undef;
+
+ LJ::memcache_set_u($u) if $use_dbh;
+ return _set_u_req_cache($u);
+ };
+
+ # user is forcing master, return now
+ return $get_user->("master") if $force || $LJ::_PRAGMA_FORCE_MASTER;
+
+ my $u;
+
+ # check process cache
+ $u = $LJ::REQ_CACHE_USER_ID{$userid};
if ($u) {
- while ($u->{'journaltype'} eq 'R' && $hops-- > 0) {
- my $rt = $u->prop("renamedto");
- last unless length $rt;
- $u = LJ::load_user($rt);
- }
- }
-
- return $u;
-}
-
-sub dismissed_page_notices {
- my $u = shift;
-
- my $val = $u->prop("dismissed_page_notices");
- my @notices = split(",", $val);
-
- return @notices;
-}
-
-sub has_dismissed_page_notice {
- my $u = shift;
- my $notice_string = shift;
-
- return 1 if grep { $_ eq $notice_string } $u->dismissed_page_notices;
- return 0;
-}
-
-# add a page notice to a user's dismissed page notices list
-sub dismissed_page_notices_add {
- my $u = shift;
- my $notice_string = shift;
- return 0 unless $notice_string && $LJ::VALID_PAGE_NOTICES{$notice_string};
-
- # is it already there?
- return 1 if $u->has_dismissed_page_notice($notice_string);
-
- # create the new list of dismissed page notices
- my @cur_notices = $u->dismissed_page_notices;
- push @cur_notices, $notice_string;
- my $cur_notices_string = join(",", @cur_notices);
-
- # remove the oldest notice if the list is too long
- if (length $cur_notices_string > 255) {
- shift @cur_notices;
- $cur_notices_string = join(",", @cur_notices);
- }
-
- # set it
- $u->set_prop("dismissed_page_notices", $cur_notices_string);
-
- return 1;
-}
-
-# remove a page notice from a user's dismissed page notices list
-sub dismissed_page_notices_remove {
- my $u = shift;
- my $notice_string = shift;
- return 0 unless $notice_string && $LJ::VALID_PAGE_NOTICES{$notice_string};
-
- # is it even there?
- return 0 unless $u->has_dismissed_page_notice($notice_string);
-
- # remove it
- $u->set_prop("dismissed_page_notices", join(",", grep { $_ ne $notice_string } $u->dismissed_page_notices));
-
- return 1;
-}
-
-# name: LJ::User->get_timeactive
-# des: retrieve last active time for user from [dbtable[clustertrack2]] or
-# memcache
-sub get_timeactive {
- my ($u) = @_;
- my $memkey = [$u->{userid}, "timeactive:$u->{userid}"];
- my $active;
- unless (defined($active = LJ::MemCache::get($memkey))) {
- # TODO: die if unable to get handle? This was left verbatim from
- # refactored code.
- my $dbcr = LJ::get_cluster_def_reader($u) or return 0;
- $active = $dbcr->selectrow_array("SELECT timeactive FROM clustertrack2 ".
- "WHERE userid=?", undef, $u->{userid});
- LJ::MemCache::set($memkey, $active, 86400);
- }
- return $active;
-}
-
-# returns an array of maintainer userids
-sub maintainer_userids {
- my $u = shift;
-
- return () unless $u->is_community;
- return @{LJ::load_rel_user_cache( $u->id, 'A' )};
-}
-
-# returns an array of moderator userids
-sub moderator_userids {
- my $u = shift;
-
- return () unless $u->is_community && $u->prop( 'moderated' );
- return @{LJ::load_rel_user_cache( $u->id, 'M' )};
-}
-
-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;
-}
-
-package LJ;
-
-use Carp;
-
-# <LJFUNC>
-# name: LJ::get_authas_list
-# des: Get a list of usernames a given user can authenticate as.
-# returns: an array of usernames.
-# args: u, opts?
-# des-opts: Optional hashref. keys are:
-# - type: 'P' to only return users of journaltype 'P'.
-# - cap: cap to filter users on.
-# </LJFUNC>
-sub get_authas_list {
- my ($u, $opts) = @_;
-
- # used to accept a user type, now accept an opts hash
- $opts = { 'type' => $opts } unless ref $opts;
-
- # Two valid types, Personal or Community
- $opts->{'type'} = undef unless $opts->{'type'} =~ m/^(P|C)$/;
-
- my $ids = LJ::load_rel_target($u, 'A');
- return undef unless $ids;
-
- # load_userids_multiple
- my %users;
- LJ::load_userids_multiple([ map { $_, \$users{$_} } @$ids ], [$u]);
-
- return map { $_->{'user'} }
- grep { ! $opts->{'cap'} || LJ::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 || LJ::u_equals($_, $u) }
-
- # can't work as an expunged account
- grep { !$_->is_expunged && $_->{clusterid} > 0 }
- $u, sort { $a->{'user'} cmp $b->{'user'} } values %users;
-}
-
-# <LJFUNC>
-# name: LJ::get_postto_list
-# des: Get the list of usernames a given user can post to.
-# returns: an array of usernames
-# args: u, opts?
-# des-opts: Optional hashref. keys are:
-# - type: 'P' to only return users of journaltype 'P'.
-# - cap: cap to filter users on.
-# </LJFUNC>
-sub get_postto_list {
- my ($u, $opts) = @_;
-
- # used to accept a user type, now accept an opts hash
- $opts = { 'type' => $opts } unless ref $opts;
-
- # only one valid type right now
- $opts->{'type'} = 'P' if $opts->{'type'};
-
- my $ids = LJ::load_rel_target($u, 'P');
- return undef unless $ids;
-
- # load_userids_multiple
- my %users;
- LJ::load_userids_multiple([ map { $_, \$users{$_} } @$ids ], [$u]);
-
- return $u->{'user'}, sort map { $_->{'user'} }
- grep { ! $opts->{'cap'} || LJ::get_cap($_, $opts->{'cap'}) }
- grep { ! $opts->{'type'} || $opts->{'type'} eq $_->{'journaltype'} }
- grep { $_->clusterid > 0 }
- grep { $_->is_visible }
- values %users;
-}
-
-# <LJFUNC>
-# name: LJ::can_view
-# des: Checks to see if the remote user can view a given journal entry.
-# <b>Note:</b> This is meant for use on single entries at a time,
-# not for calling many times on every entry in a journal.
-# returns: boolean; 1 if remote user can see item
-# args: remote, item
-# des-item: Hashref from the 'log' table.
-# </LJFUNC>
-sub can_view
-{
-
-# TODO: fold this into LJ::Entry->visible_to :(
-
- &nodb;
- my $remote = shift;
- my $item = shift;
-
- # public is okay
- return 1 if $item->{'security'} eq "public";
-
- # must be logged in otherwise
- return 0 unless $remote;
-
- my $userid = int($item->{'ownerid'} || $item->{'journalid'});
- my $remoteid = int($remote->{'userid'});
-
- # owners can always see their own.
- return 1 if $userid == $remoteid;
-
- # other people can't read private
- return 0 if $item->{'security'} eq "private";
-
- # should be 'usemask' security from here out, otherwise
- # assume it's something new and return 0
- return 0 unless $item->{'security'} eq "usemask";
-
- # if it's usemask, we have to refuse non-personal journals,
- # so we have to load the user
- return 0 unless $remote->{'journaltype'} eq 'P' || $remote->{'journaltype'} eq 'I';
-
- # this far down we have to load the user
- my $u = LJ::want_user( $userid ) or return 0;
-
- # check if it's a community and they're a member
- return 1 if $u->is_community &&
- $remote->member_of( $u );
-
- # now load allowmask
- my $allowed = ( $u->trustmask( $remoteid ) & int($item->{'allowmask'}) );
- return $allowed ? 1 : 0; # no need to return matching mask
-}
-
-# <LJFUNC>
-# name: LJ::wipe_major_memcache
-# des: invalidate all major memcache items associated with a given user.
-# args: u
-# returns: nothing
-# </LJFUNC>
-sub wipe_major_memcache
-{
- my $u = shift;
- my $userid = LJ::want_userid($u);
- foreach my $key ("userid","bio","talk2ct","talkleftct","log2ct",
- "log2lt","memkwid","dayct2","s1overr","s1uc","fgrp",
- "wt_edges","wt_edges_rev","tu","upicinf","upiccom",
- "upicurl", "upicdes", "intids", "memct", "lastcomm")
- {
- LJ::memcache_kill($userid, $key);
- }
-}
-
-# <LJFUNC>
-# name: LJ::load_user_props
-# des: Given a user hashref, loads the values of the given named properties
-# into that user hashref.
-# args: dbarg?, u, opts?, propname*
-# des-opts: hashref of opts. set key 'cache' to use memcache.
-# des-propname: the name of a property from the [dbtable[userproplist]] table.
-# </LJFUNC>
-sub load_user_props
-{
- &nodb;
-
- my $u = shift;
- return unless isu($u);
- return if $u->is_expunged;
-
- my $opts = ref $_[0] ? shift : {};
- my (@props) = @_;
-
- my ($sql, $sth);
- LJ::load_props("user");
-
- ## user reference
- my $uid = $u->{'userid'}+0;
- $uid = LJ::get_userid($u->{'user'}) unless $uid;
-
- my $mem = {};
- my $use_master = 0;
- my $used_slave = 0; # set later if we ended up using a slave
-
- if (@LJ::MEMCACHE_SERVERS) {
- my @keys;
- foreach (@props) {
- next if exists $u->{$_};
- my $p = LJ::get_prop("user", $_);
- die "Invalid userprop $_ passed to LJ::load_user_props." unless $p;
- push @keys, [$uid,"uprop:$uid:$p->{'id'}"];
- }
- $mem = LJ::MemCache::get_multi(@keys) || {};
- $use_master = 1;
- }
-
- $use_master = 1 if $opts->{'use_master'};
-
- my @needwrite; # [propid, propname] entries we need to save to memcache later
-
- my %loadfrom;
- my %multihomed; # ( $propid => 0/1 ) # 0 if we haven't loaded it, 1 if we have
- unless (@props) {
- # case 1: load all props for a given user.
- # multihomed props are stored on userprop and userproplite2, but since they
- # should always be in sync, it doesn't matter which gets loaded first, the
- # net results should be the same. see doc/server/lj.int.multihomed_userprops.html
- # for more information.
- $loadfrom{'userprop'} = 1;
- $loadfrom{'userproplite'} = 1;
- $loadfrom{'userproplite2'} = 1;
- $loadfrom{'userpropblob'} = 1;
- } else {
- # case 2: load only certain things
- foreach (@props) {
- next if exists $u->{$_};
- my $p = LJ::get_prop("user", $_);
- die "Invalid userprop $_ passed to LJ::load_user_props." unless $p;
- if (defined $mem->{"uprop:$uid:$p->{'id'}"}) {
- $u->{$_} = $mem->{"uprop:$uid:$p->{'id'}"};
- next;
- }
- push @needwrite, [ $p->{'id'}, $_ ];
- my $source = $p->{'indexed'} ? "userprop" : "userproplite";
- if ($p->{datatype} eq 'blobchar') {
- $source = "userpropblob"; # clustered blob
- }
- elsif ($p->{'cldversion'} && $u->{'dversion'} >= $p->{'cldversion'}) {
- $source = "userproplite2"; # clustered
- }
- elsif ($p->{multihomed}) {
- $multihomed{$p->{id}} = 0;
- $source = "userproplite2";
- }
- push @{$loadfrom{$source}}, $p->{'id'};
- }
- }
-
- foreach my $table (qw{userproplite userproplite2 userpropblob userprop}) {
- next unless exists $loadfrom{$table};
- my $db;
- if ($use_master) {
- $db = ($table =~ m{userprop(lite2|blob)}) ?
- LJ::get_cluster_master($u) :
- LJ::get_db_writer();
- }
- unless ($db) {
- $db = ($table =~ m{userprop(lite2|blob)}) ?
- LJ::get_cluster_reader($u) :
- LJ::get_db_reader();
- $used_slave = 1;
- }
- $sql = "SELECT upropid, value FROM $table WHERE userid=$uid";
- if (ref $loadfrom{$table}) {
- $sql .= " AND upropid IN (" . join(",", @{$loadfrom{$table}}) . ")";
- }
- $sth = $db->prepare($sql);
- $sth->execute;
- while (my ($id, $v) = $sth->fetchrow_array) {
- delete $multihomed{$id} if $table eq 'userproplite2';
- $u->{$LJ::CACHE_PROPID{'user'}->{$id}->{'name'}} = $v;
- }
-
- # push back multihomed if necessary
- if ($table eq 'userproplite2') {
- push @{$loadfrom{userprop}}, $_ foreach keys %multihomed;
- }
- }
-
- # see if we failed to get anything above and need to hit the master.
- # this usually happens the first time a multihomed prop is hit. this
- # code will propagate that prop down to the cluster.
- if (%multihomed) {
-
- # verify that we got the database handle before we try propagating data
- if ($u->writer) {
- my @values;
- foreach my $id (keys %multihomed) {
- my $pname = $LJ::CACHE_PROPID{user}{$id}{name};
- if (defined $u->{$pname} && $u->{$pname}) {
- push @values, "($uid, $id, " . $u->quote($u->{$pname}) . ")";
- } else {
- push @values, "($uid, $id, '')";
- }
- }
- $u->do("REPLACE INTO userproplite2 VALUES " . join ',', @values);
- }
- }
-
- # Add defaults to user object.
-
- # If this was called with no @props, then the function tried
- # to load all metadata. but we don't know what's missing, so
- # try to apply all defaults.
- unless (@props) { @props = keys %LJ::USERPROP_DEF; }
-
- foreach my $prop (@props) {
- next if (defined $u->{$prop});
- $u->{$prop} = $LJ::USERPROP_DEF{$prop};
- }
-
- unless ($used_slave) {
- my $expire = time() + 3600*24;
- foreach my $wr (@needwrite) {
- my ($id, $name) = ($wr->[0], $wr->[1]);
- LJ::MemCache::set([$uid,"uprop:$uid:$id"], $u->{$name} || "", $expire);
- }
- }
-}
+ $u->selfassert;
+ return $u;
+ }
+
+ # check memcache
+ $u = LJ::memcache_get_u([$userid,"userid:$userid"]);
+ return _set_u_req_cache($u) if $u;
+
+ # get from master if using memcache
+ return $get_user->("master") if @LJ::MEMCACHE_SERVERS;
+
+ # check slave
+ $u = $get_user->();
+ return $u if $u;
+
+ # if we didn't get a u from the reader, fall back to master
+ return $get_user->("master");
+}
+
# <LJFUNC>
# name: LJ::load_userids
@@ -5098,6 +5691,7 @@ sub load_userids
LJ::load_userids_multiple([ map { $_ => \$u{$_} } @_ ]);
return \%u;
}
+
# <LJFUNC>
# name: LJ::load_userids_multiple
@@ -5182,187 +5776,38 @@ sub load_userids_multiple
}
}
-# des-db: $dbh/$dbr
-# des-key: either "userid" or "user" (the WHERE part)
-# des-vals: value or arrayref of values for key to match on
-# des-hook: optional code ref to run for each $u
-# returns: last $u found
-sub _load_user_raw
-{
- my ($db, $key, $vals, $hook) = @_;
- $hook ||= sub {};
- $vals = [ $vals ] unless ref $vals eq "ARRAY";
-
- my $use_isam;
- unless ($LJ::CACHE_NO_ISAM{user} || scalar(@$vals) > 10) {
- eval { $db->do("HANDLER user OPEN"); };
- if ($@ || $db->err) {
- $LJ::CACHE_NO_ISAM{user} = 1;
- } else {
- $use_isam = 1;
- }
- }
-
- my $last;
-
- if ($use_isam) {
- $key = "PRIMARY" if $key eq "userid";
- foreach my $v (@$vals) {
- my $sth = $db->prepare("HANDLER user READ `$key` = (?) LIMIT 1");
- $sth->execute($v);
- my $row = $sth->fetchrow_hashref;
- if ($row) {
- my $u = LJ::User->new_from_row($row);
- $hook->($u);
- $last = $u;
- }
- }
- $db->do("HANDLER user close");
- } else {
- my $in = join(", ", map { $db->quote($_) } @$vals);
- my $sth = $db->prepare("SELECT * FROM user WHERE $key IN ($in)");
- $sth->execute;
- while (my $row = $sth->fetchrow_hashref) {
- my $u = LJ::User->new_from_row($row);
- $hook->($u);
- $last = $u;
- }
- }
-
- return $last;
-}
-
-sub _set_u_req_cache {
- my $u = shift or die "no u to set";
-
- # if we have an existing user singleton, upgrade it with
- # the latested data, but keep using its address
- if (my $eu = $LJ::REQ_CACHE_USER_ID{$u->{'userid'}}) {
- LJ::assert_is($eu->{userid}, $u->{userid});
- $eu->selfassert;
- $u->selfassert;
-
- $eu->{$_} = $u->{$_} foreach keys %$u;
- $u = $eu;
- }
- $LJ::REQ_CACHE_USER_NAME{$u->{'user'}} = $u;
- $LJ::REQ_CACHE_USER_ID{$u->{'userid'}} = $u;
- return $u;
-}
-
-sub load_user_or_identity {
- my $arg = shift;
-
- my $user = LJ::canonical_username($arg);
- return LJ::load_user($user) if $user;
-
- # return undef if not dot in arg (can't be a URL)
- return undef unless $arg =~ /\./;
-
- my $dbh = LJ::get_db_writer();
- my $url = lc($arg);
- $url = "http://$url" unless $url =~ m!^http://!;
- $url .= "/" unless $url =~ m!/$!;
- my $uid = $dbh->selectrow_array("SELECT userid FROM identitymap WHERE idtype=? AND identity=?",
- undef, 'O', $url);
- return LJ::load_userid($uid) if $uid;
- return undef;
-}
-
-# load either a username, or a "I,<userid>" parameter.
-sub load_user_arg {
- my ($arg) = @_;
- my $user = LJ::canonical_username($arg);
- return LJ::load_user($user) if length $user;
- if ($arg =~ /^I,(\d+)$/) {
- my $u = LJ::load_userid($1);
- return $u if $u->is_identity;
- }
- return; # undef/()
-}
-
-# <LJFUNC>
-# name: LJ::load_user
-# des: Loads a user record, from the [dbtable[user]] table, given a username.
-# args: dbarg?, user, force?
-# des-user: Username of user to load.
-# des-force: if set to true, won't return cached user object and will
-# query a dbh.
-# returns: Hashref, with keys being columns of [dbtable[user]] table.
-# </LJFUNC>
-sub load_user
-{
- &nodb;
- my ($user, $force) = @_;
-
- $user = LJ::canonical_username($user);
- return undef unless length $user;
-
- my $get_user = sub {
- my $use_dbh = shift;
- my $db = $use_dbh ? LJ::get_db_writer() : LJ::get_db_reader();
- my $u = _load_user_raw($db, "user", $user)
- or return undef;
-
- # set caches since we got a u from the master
- LJ::memcache_set_u($u) if $use_dbh;
-
- return _set_u_req_cache($u);
- };
-
- # caller is forcing a master, return now
- return $get_user->("master") if $force || $LJ::_PRAGMA_FORCE_MASTER;
-
- my $u;
-
- # return process cache if we have one
- if ($u = $LJ::REQ_CACHE_USER_NAME{$user}) {
- $u->selfassert;
- return $u;
- }
-
- # check memcache
- {
- my $uid = LJ::MemCache::get("uidof:$user");
- $u = LJ::memcache_get_u([$uid, "userid:$uid"]) if $uid;
- return _set_u_req_cache($u) if $u;
- }
-
- # try to load from master if using memcache, otherwise from slave
- $u = $get_user->(scalar @LJ::MEMCACHE_SERVERS);
- return $u if $u;
-
- # setup LDAP handler if this is the first time
- if ($LJ::LDAP_HOST && ! $LJ::AUTH_EXISTS) {
- require LJ::LDAP;
- $LJ::AUTH_EXISTS = sub {
- my $user = shift;
- my $rec = LJ::LDAP::load_ldap_user($user);
- return $rec ? $rec : undef;
- };
- }
-
- # if user doesn't exist in the LJ database, it's possible we're using
- # an external authentication source and we should create the account
- # implicitly.
- my $lu;
- if (ref $LJ::AUTH_EXISTS eq "CODE" && ($lu = $LJ::AUTH_EXISTS->($user)))
- {
- my $name = ref $lu eq "HASH" ? ($lu->{'nick'} || $lu->{name} || $user) : $user;
- if (LJ::create_account({
- 'user' => $user,
- 'name' => $name,
- 'email' => ref $lu eq "HASH" ? $lu->email_raw : "",
- 'password' => "",
- }))
- {
- # this should pull from the master, since it was _just_ created
- return $get_user->("master");
- }
- }
-
- return undef;
-}
+
+# <LJFUNC>
+# name: LJ::make_user_active
+# des: Record user activity per cluster, on [dbtable[clustertrack2]], to
+# make per-activity cluster stats easier.
+# args: userid, type
+# des-userid: source userobj ref
+# des-type: currently unused
+# </LJFUNC>
+sub mark_user_active {
+ my ($u, $type) = @_; # not currently using type
+ return 0 unless $u; # do not auto-vivify $u
+ my $uid = $u->{userid};
+ return 0 unless $uid && $u->{clusterid};
+
+ # Update the clustertrack2 table, but not if we've done it for this
+ # user in the last hour. if no memcache servers are configured
+ # we don't do the optimization and just always log the activity info
+ if (@LJ::MEMCACHE_SERVERS == 0 ||
+ LJ::MemCache::add("rate:tracked:$uid", 1, 3600)) {
+
+ return 0 unless $u->writer;
+ my $active = time();
+ $u->do("REPLACE INTO clustertrack2 SET ".
+ "userid=?, timeactive=?, clusterid=?", undef,
+ $uid, $active, $u->{clusterid}) or return 0;
+ my $memkey = [$u->{userid}, "timeactive:$u->{userid}"];
+ LJ::MemCache::set($memkey, $active, 86400);
+ }
+ return 1;
+}
+
# <LJFUNC>
# name: LJ::u_equals
@@ -5377,292 +5822,237 @@ sub u_equals {
return $u1 && $u2 && $u1->{'userid'} == $u2->{'userid'};
}
-# <LJFUNC>
-# name: LJ::load_userid
-# des: Loads a user record, from the [dbtable[user]] table, given a userid.
-# args: dbarg?, userid, force?
-# des-userid: Userid of user to load.
-# des-force: if set to true, won't return cached user object and will
-# query a dbh
-# returns: Hashref with keys being columns of [dbtable[user]] table.
-# </LJFUNC>
-sub load_userid
-{
- &nodb;
- my ($userid, $force) = @_;
- return undef unless $userid;
-
- my $get_user = sub {
- my $use_dbh = shift;
- my $db = $use_dbh ? LJ::get_db_writer() : LJ::get_db_reader();
- my $u = _load_user_raw($db, "userid", $userid)
- or return undef;
-
- LJ::memcache_set_u($u) if $use_dbh;
- return _set_u_req_cache($u);
- };
-
- # user is forcing master, return now
- return $get_user->("master") if $force || $LJ::_PRAGMA_FORCE_MASTER;
-
- my $u;
-
- # check process cache
- $u = $LJ::REQ_CACHE_USER_ID{$userid};
- if ($u) {
- $u->selfassert;
- return $u;
- }
-
- # check memcache
- $u = LJ::memcache_get_u([$userid,"userid:$userid"]);
- return _set_u_req_cache($u) if $u;
-
- # get from master if using memcache
- return $get_user->("master") if @LJ::MEMCACHE_SERVERS;
-
- # check slave
- $u = $get_user->();
- return $u if $u;
-
- # if we didn't get a u from the reader, fall back to master
- return $get_user->("master");
-}
-
-sub memcache_get_u
-{
- my @keys = @_;
- my @ret;
- foreach my $ar (values %{LJ::MemCache::get_multi(@keys) || {}}) {
- my $row = LJ::MemCache::array_to_hash("user", $ar)
- or next;
- my $u = LJ::User->new_from_row($row);
- push @ret, $u;
- }
- return wantarray ? @ret : $ret[0];
-}
-
-sub memcache_set_u
-{
- my $u = shift;
- return unless $u;
- my $expire = time() + 1800;
- my $ar = LJ::MemCache::hash_to_array("user", $u);
- return unless $ar;
- LJ::MemCache::set([$u->{'userid'}, "userid:$u->{'userid'}"], $ar, $expire);
- LJ::MemCache::set("uidof:$u->{user}", $u->{userid});
-}
-
-# <LJFUNC>
-# name: LJ::get_bio
-# des: gets a user bio, from DB or memcache.
-# args: u, force
-# des-force: true to get data from cluster master.
-# returns: string
-# </LJFUNC>
-sub get_bio {
- my ($u, $force) = @_;
- return unless $u && $u->{'has_bio'} eq "Y";
-
- my $bio;
-
- my $memkey = [$u->{'userid'}, "bio:$u->{'userid'}"];
- unless ($force) {
- my $bio = LJ::MemCache::get($memkey);
- return $bio if defined $bio;
- }
-
- # not in memcache, fall back to disk
- my $db = @LJ::MEMCACHE_SERVERS || $force ?
- LJ::get_cluster_def_reader($u) : LJ::get_cluster_reader($u);
- $bio = $db->selectrow_array("SELECT bio FROM userbio WHERE userid=?",
- undef, $u->{'userid'});
-
- # set in memcache
- LJ::MemCache::add($memkey, $bio);
-
- return $bio;
-}
-
-# <LJFUNC>
-# name: LJ::journal_base
-# des: Returns URL of a user's journal.
-# info: The tricky thing is that users with underscores in their usernames
-# can't have some_user.example.com as a hostname, so that's changed into
-# some-user.example.com.
-# args: uuser, vhost?
-# des-uuser: User hashref or username of user whose URL to make.
-# des-vhost: What type of URL. Acceptable options: "users", to make a
-# http://user.example.com/ URL; "tilde" for http://example.com/~user/;
-# "community" for http://example.com/community/user; or the default
-# will be http://example.com/users/user. If unspecified and uuser
-# is a user hashref, then the best/preferred vhost will be chosen.
-# returns: scalar; a URL.
-# </LJFUNC>
-sub journal_base
-{
- my ($user, $vhost) = @_;
-
- if (! isu($user) && LJ::are_hooks("journal_base")) {
- my $u = LJ::load_user($user);
- $user = $u if $u;
- }
-
- if (isu($user)) {
- my $u = $user;
-
- my $hookurl = LJ::run_hook("journal_base", $u, $vhost);
- return $hookurl if $hookurl;
-
- $user = $u->{'user'};
- unless (defined $vhost) {
- if ($LJ::FRONTPAGE_JOURNAL eq $user) {
- $vhost = "front";
- } elsif ($u->{'journaltype'} eq "P") {
- $vhost = "";
- } elsif ($u->{'journaltype'} eq "C") {
- $vhost = "community";
- }
-
- }
- }
-
- if ($vhost eq "users") {
- my $he_user = $user;
- $he_user =~ s/_/-/g;
- return "http://$he_user.$LJ::USER_DOMAIN";
- } elsif ($vhost eq "tilde") {
- return "$LJ::SITEROOT/~$user";
- } elsif ($vhost eq "community") {
- return "$LJ::SITEROOT/community/$user";
- } elsif ($vhost eq "front") {
- return $LJ::SITEROOT;
- } elsif ($vhost =~ /^other:(.+)/) {
- return "http://$1";
- } else {
- return "$LJ::SITEROOT/users/$user";
- }
-}
-
-
-# <LJFUNC>
-# name: LJ::load_user_privs
-# class:
-# des: loads all of the given privs for a given user into a hashref, inside
-# the user record. See also [func[LJ::check_priv]].
-# args: u, priv, arg?
-# des-priv: Priv names to load (see [dbtable[priv_list]]).
-# des-arg: Optional argument. See also [func[LJ::check_priv]].
-# returns: boolean
-# </LJFUNC>
-sub load_user_privs
-{
- &nodb;
+
+# <LJFUNC>
+# name: LJ::want_user
+# des: Returns user object when passed either userid or user object. Useful to functions that
+# want to accept either.
+# args: user
+# des-user: Either a userid or a user hash with the userid in its 'userid' key.
+# returns: The user object represented by said userid or username.
+# </LJFUNC>
+sub want_user
+{
+ my $uuid = shift;
+ return undef unless $uuid;
+ return $uuid if ref $uuid;
+ return LJ::load_userid($uuid) if $uuid =~ /^\d+$/;
+ Carp::croak("Bogus caller of LJ::want_user with non-ref/non-numeric parameter");
+}
+
+
+# <LJFUNC>
+# name: LJ::want_userid
+# des: Returns userid when passed either userid or the user hash. Useful to functions that
+# want to accept either. Forces its return value to be a number (for safety).
+# args: userid
+# des-userid: Either a userid, or a user hash with the userid in its 'userid' key.
+# returns: The userid, guaranteed to be a numeric value.
+# </LJFUNC>
+sub want_userid
+{
+ my $uuserid = shift;
+ return ($uuserid->{'userid'} + 0) if ref $uuserid;
+ return ($uuserid + 0);
+}
+
+
+########################################################################
+### 3. Login, Session, and Rename Functions
+
+
+# returns the country that the remote IP address comes from
+# undef is returned if the country cannot be determined from the IP
+sub country_of_remote_ip {
+ if (eval "use IP::Country::Fast; 1;") {
+ my $ip = LJ::get_remote_ip();
+ return undef unless $ip;
+
+ my $reg = IP::Country::Fast->new();
+ my $country = $reg->inet_atocc($ip);
+
+ # "**" is returned if the IP is private
+ return undef if $country eq "**";
+ return $country;
+ }
+
+ return undef;
+}
+
+1;
+
+
+sub get_active_journal
+{
+ return $LJ::ACTIVE_JOURNAL;
+}
+
+# returns either $remote or the authenticated user that $remote is working with
+sub get_effective_remote {
+ my $authas_arg = shift || "authas";
+
+ return undef unless LJ::is_web_context();
+
+ my $remote = LJ::get_remote();
+ return undef unless $remote;
+
+ my $authas = $BMLCodeBlock::GET{authas} || $BMLCodeBlock::POST{authas} || $remote->user;
+ return $remote if $authas eq $remote->user;
+
+ return LJ::get_authas_user($authas);
+}
+
+
+# <LJFUNC>
+# name: LJ::get_remote
+# des: authenticates the user at the remote end based on their cookies
+# and returns a hashref representing them.
+# args: opts?
+# des-opts: 'criterr': scalar ref to set critical error flag. if set, caller
+# should stop processing whatever it's doing and complain
+# about an invalid login with a link to the logout page.
+# 'ignore_ip': ignore IP address of remote for IP-bound sessions
+# returns: hashref containing 'user' and 'userid' if valid user, else
+# undef.
+# </LJFUNC>
+sub get_remote
+{
+ my $opts = ref $_[0] eq "HASH" ? shift : {};
+ return $LJ::CACHE_REMOTE if $LJ::CACHED_REMOTE && ! $opts->{'ignore_ip'};
+
+ my $no_remote = sub {
+ LJ::User->set_remote(undef);
+ return undef;
+ };
+
+ # can't have a remote user outside of web context
+ my $r = eval { BML::get_request(); };
+ return $no_remote->() unless $r;
+
+ my $criterr = $opts->{criterr} || do { my $d; \$d; };
+ $$criterr = 0;
+
+ $LJ::CACHE_REMOTE_BOUNCE_URL = "";
+
+ # set this flag if any of their ljsession cookies contained the ".FS"
+ # opt to use the fast server. if we later find they're not logged
+ # in and set it, or set it with a free account, then we give them
+ # the invalid cookies error.
+ my $tried_fast = 0;
+ my $sessobj = LJ::Session->session_from_cookies(tried_fast => \$tried_fast,
+ redirect_ref => \$LJ::CACHE_REMOTE_BOUNCE_URL,
+ ignore_ip => $opts->{ignore_ip},
+ );
+
+ my $u = $sessobj ? $sessobj->owner : undef;
+
+ # inform the caller that this user is faking their fast-server cookie
+ # attribute.
+ if ($tried_fast && ! LJ::get_cap($u, "fastserver")) {
+ $$criterr = 1;
+ }
+
+ return $no_remote->() unless $sessobj;
+
+ # renew soon-to-expire sessions
+ $sessobj->try_renew;
+
+ # augment hash with session data;
+ $u->{'_session'} = $sessobj;
+
+ # keep track of activity for the user we just loaded from db/memcache
+ # - if necessary, this code will actually run in Apache's cleanup handler
+ # so latency won't affect the user
+ if (@LJ::MEMCACHE_SERVERS && ! $LJ::DISABLED{active_user_tracking}) {
+ push @LJ::CLEANUP_HANDLERS, sub { $u->note_activity('A') };
+ }
+
+ LJ::User->set_remote($u);
+ $r->notes->{ljuser} = $u->{user};
+ return $u;
+}
+
+
+sub handle_bad_login
+{
+ my ($u, $ip) = @_;
+ return 1 unless $u;
+
+ $ip ||= LJ::get_remote_ip();
+ return 1 unless $ip;
+
+ # an IP address is permitted such a rate of failures
+ # until it's banned for a period of time.
+ my $udbh;
+ if (! LJ::rate_log($u, "failed_login", 1, { 'limit_by_ip' => $ip }) &&
+ ($udbh = LJ::get_cluster_master($u)))
+ {
+ $udbh->do("REPLACE INTO loginstall (userid, ip, time) VALUES ".
+ "(?,INET_ATON(?),UNIX_TIMESTAMP())", undef, $u->{'userid'}, $ip);
+ }
+ return 1;
+}
+
+
+sub login_ip_banned
+{
+ my ($u, $ip) = @_;
+ return 0 unless $u;
+
+ $ip ||= LJ::get_remote_ip();
+ return 0 unless $ip;
+
+ my $udbr;
+ my $rateperiod = LJ::get_cap($u, "rateperiod-failed_login");
+ if ($rateperiod && ($udbr = LJ::get_cluster_reader($u))) {
+ my $bantime = $udbr->selectrow_array("SELECT time FROM loginstall WHERE ".
+ "userid=$u->{'userid'} AND ip=INET_ATON(?)",
+ undef, $ip);
+ if ($bantime && $bantime > time() - $rateperiod) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+
+# returns URL we have to bounce the remote user to in order to
+# get their domain cookie
+sub remote_bounce_url {
+ return $LJ::CACHE_REMOTE_BOUNCE_URL;
+}
+
+
+sub set_active_journal
+{
+ $LJ::ACTIVE_JOURNAL = shift;
+}
+
+
+sub set_remote {
my $remote = shift;
- my @privs = @_;
- return unless $remote and @privs;
-
- # return if we've already loaded these privs for this user.
- @privs = grep { ! $remote->{'_privloaded'}->{$_} } @privs;
- return unless @privs;
-
- my $dbr = LJ::get_db_reader();
- return unless $dbr;
- foreach (@privs) { $remote->{'_privloaded'}->{$_}++; }
- @privs = map { $dbr->quote($_) } @privs;
- my $sth = $dbr->prepare("SELECT pl.privcode, pm.arg ".
- "FROM priv_map pm, priv_list pl ".
- "WHERE pm.prlid=pl.prlid AND ".
- "pl.privcode IN (" . join(',',@privs) . ") ".
- "AND pm.userid=$remote->{'userid'}");
- $sth->execute;
- while (my ($priv, $arg) = $sth->fetchrow_array) {
- unless (defined $arg) { $arg = ""; } # NULL -> ""
- $remote->{'_priv'}->{$priv}->{$arg} = 1;
- }
-}
-
-# <LJFUNC>
-# name: LJ::check_priv
-# des: Check to see if a user has a certain privilege.
-# info: Usually this is used to check the privs of a $remote user.
-# See [func[LJ::get_remote]]. As such, a $u argument of undef
-# is okay to pass: 0 will be returned, as an unknown user can't
-# have any rights.
-# args: dbarg?, u, priv, arg?
-# des-priv: Priv name to check for (see [dbtable[priv_list]])
-# des-arg: Optional argument. If defined, function only returns true
-# when $remote has a priv of type $priv also with arg $arg, not
-# just any priv of type $priv, which is the behavior without
-# an $arg. Arg can be "*", for all args.
-# returns: boolean; true if user has privilege
-# </LJFUNC>
-sub check_priv
-{
- &nodb;
- my ($u, $priv, $arg) = @_;
- return 0 unless $u;
-
- LJ::load_user_privs($u, $priv)
- unless $u->{'_privloaded'}->{$priv};
-
- # no access if they don't have the priv
- return 0 unless defined $u->{'_priv'}->{$priv};
-
- # at this point we know they have the priv
- return 1 unless defined $arg;
-
- # check if they have the right arguments
- return 1 if defined $u->{'_priv'}->{$priv}->{$arg};
- return 1 if defined $u->{'_priv'}->{$priv}->{"*"};
-
- # don't have the right argument
- return 0;
-}
-
-#
-#
-# <LJFUNC>
-# name: LJ::remote_has_priv
-# class:
-# des: Check to see if the given remote user has a certain privilege.
-# info: <strong>Deprecated</strong>. You should
-# use [func[LJ::load_user_privs]] + [func[LJ::check_priv]], instead.
-# args:
-# des-:
-# returns:
-# </LJFUNC>
-sub remote_has_priv
-{
- &nodb;
- my $remote = shift;
- my $privcode = shift; # required. priv code to check for.
- my $ref = shift; # optional, arrayref or hashref to populate
- return 0 unless ($remote);
-
- ### authentication done. time to authorize...
-
- my $dbr = LJ::get_db_reader();
- my $sth = $dbr->prepare("SELECT pm.arg FROM priv_map pm, priv_list pl WHERE pm.prlid=pl.prlid AND pl.privcode=? AND pm.userid=?");
- $sth->execute($privcode, $remote->{'userid'});
-
- my $match = 0;
- if (ref $ref eq "ARRAY") { @$ref = (); }
- if (ref $ref eq "HASH") { %$ref = (); }
- while (my ($arg) = $sth->fetchrow_array) {
- $match++;
- if (ref $ref eq "ARRAY") { push @$ref, $arg; }
- if (ref $ref eq "HASH") { $ref->{$arg} = 1; }
- }
- return $match;
-}
+ LJ::User->set_remote($remote);
+ 1;
+}
+
+
+sub unset_remote
+{
+ LJ::User->unset_remote;
+ 1;
+}
+
+
+########################################################################
+### 5. Database and Memcache Functions
+
# $dom: 'L' == log, 'T' == talk, 'M' == modlog, 'S' == session,
# 'R' == memory (remembrance), 'K' == keyword id,
# 'P' == phone post, 'C' == pending comment
# 'O' == pOrtal box id, 'V' == 'vgift', 'E' == ESN subscription id
# 'Q' == Notification Inbox, 'G' == 'SMS messaGe'
-# 'D' == 'moDule embed contents', 'I' == Import data block,
+# 'D' == 'moDule embed contents', 'I' == Import data block
# 'Z' == import status item
#
# FIXME: both phonepost and vgift are ljcom. need hooks. but then also
@@ -5791,10 +6181,10 @@ sub alloc_user_counter
undef, $uid);
} elsif ($dom eq "I") {
$newmax = $dbh->selectrow_array("SELECT MAX(import_data_id) FROM import_data WHERE userid=?",
- undef, $uid);
+ undef, $uid);
} elsif ($dom eq "Z") {
$newmax = $dbh->selectrow_array("SELECT MAX(import_status_id) FROM import_status WHERE userid=?",
- undef, $uid);
+ undef, $uid);
} else {
die "No user counter initializer defined for area '$dom'.\n";
}
@@ -5807,187 +6197,296 @@ sub alloc_user_counter
return LJ::alloc_user_counter($u, $dom, { recurse => 1 });
}
-# <LJFUNC>
-# name: LJ::make_user_active
-# des: Record user activity per cluster, on [dbtable[clustertrack2]], to
-# make per-activity cluster stats easier.
-# args: userid, type
-# des-userid: source userobj ref
-# des-type: currently unused
-# </LJFUNC>
-sub mark_user_active {
- my ($u, $type) = @_; # not currently using type
- return 0 unless $u; # do not auto-vivify $u
- my $uid = $u->{userid};
- return 0 unless $uid && $u->{clusterid};
-
- # Update the clustertrack2 table, but not if we've done it for this
- # user in the last hour. if no memcache servers are configured
- # we don't do the optimization and just always log the activity info
- if (@LJ::MEMCACHE_SERVERS == 0 ||
- LJ::MemCache::add("rate:tracked:$uid", 1, 3600)) {
-
- return 0 unless $u->writer;
- my $active = time();
- $u->do("REPLACE INTO clustertrack2 SET ".
- "userid=?, timeactive=?, clusterid=?", undef,
- $uid, $active, $u->{clusterid}) or return 0;
- my $memkey = [$u->{userid}, "timeactive:$u->{userid}"];
- LJ::MemCache::set($memkey, $active, 86400);
- }
- return 1;
-}
-
-# <LJFUNC>
-# name: LJ::infohistory_add
-# des: Add a line of text to the [[dbtable[infohistory]] table for an account.
-# args: uuid, what, value, other?
-# des-uuid: User id or user object to insert infohistory for.
-# des-what: What type of history is being inserted (15 chars max).
-# des-value: Value for the item (255 chars max).
-# des-other: Optional. Extra information / notes (30 chars max).
-# returns: 1 on success, 0 on error.
-# </LJFUNC>
-sub infohistory_add {
- my ($uuid, $what, $value, $other) = @_;
- $uuid = LJ::want_userid($uuid);
- return unless $uuid && $what && $value;
-
- # get writer and insert
- my $dbh = LJ::get_db_writer();
- my $gmt_now = LJ::mysql_time(time(), 1);
- $dbh->do("INSERT INTO infohistory (userid, what, timechange, oldvalue, other) VALUES (?, ?, ?, ?, ?)",
- undef, $uuid, $what, $gmt_now, $value, $other);
- return $dbh->err ? 0 : 1;
-}
-
-# <LJFUNC>
-# name: LJ::set_userprop
-# des: Sets/deletes a userprop by name for a user.
-# info: This adds or deletes from the
-# [dbtable[userprop]]/[dbtable[userproplite]] tables. One
-# crappy thing about this interface is that it doesn't allow
-# a batch of userprops to be updated at once, which is the
-# common thing to do.
-# args: dbarg?, uuserid, propname, value, memonly?
-# des-uuserid: The userid of the user or a user hashref.
-# des-propname: The name of the property. Or a hashref of propname keys and corresponding values.
-# des-value: The value to set to the property. If undefined or the
-# empty string, then property is deleted.
-# des-memonly: if true, only writes to memcache, and not to database.
-# </LJFUNC>
-sub set_userprop
-{
- &nodb;
- my ($u, $propname, $value, $memonly) = @_;
- $u = ref $u ? $u : LJ::load_userid($u);
- my $userid = $u->{'userid'}+0;
-
- my $hash = ref $propname eq "HASH" ? $propname : { $propname => $value };
-
- my %action; # $table -> {"replace"|"delete"} -> [ "($userid, $propid, $qvalue)" | propid ]
- my %multihomed; # { $propid => $value }
-
- foreach $propname (keys %$hash) {
- LJ::run_hook("setprop", prop => $propname,
- u => $u, value => $value);
-
- my $p = LJ::get_prop("user", $propname) or
- die "Invalid userprop $propname passed to LJ::set_userprop.";
- if ($p->{multihomed}) {
- # collect into array for later handling
- $multihomed{$p->{id}} = $hash->{$propname};
- next;
- }
- my $table = $p->{'indexed'} ? "userprop" : "userproplite";
- if ($p->{datatype} eq 'blobchar') {
- $table = 'userpropblob';
- }
- elsif ($p->{'cldversion'} && $u->{'dversion'} >= $p->{'cldversion'}) {
- $table = "userproplite2";
- }
- unless ($memonly) {
- my $db = $action{$table}->{'db'} ||= (
- $table !~ m{userprop(lite2|blob)}
- ? LJ::get_db_writer()
- : $u->writer );
- return 0 unless $db;
- }
- $value = $hash->{$propname};
- if (defined $value && $value) {
- push @{$action{$table}->{"replace"}}, [ $p->{'id'}, $value ];
- } else {
- push @{$action{$table}->{"delete"}}, $p->{'id'};
- }
- }
-
- my $expire = time() + 3600*24;
- foreach my $table (keys %action) {
- my $db = $action{$table}->{'db'};
- if (my $list = $action{$table}->{"replace"}) {
- if ($db) {
- my $vals = join(',', map { "($userid,$_->[0]," . $db->quote($_->[1]) . ")" } @$list);
- $db->do("REPLACE INTO $table (userid, upropid, value) VALUES $vals");
- }
- LJ::MemCache::set([$userid,"uprop:$userid:$_->[0]"], $_->[1], $expire) foreach (@$list);
- }
- if (my $list = $action{$table}->{"delete"}) {
- if ($db) {
- my $in = join(',', @$list);
- $db->do("DELETE FROM $table WHERE userid=$userid AND upropid IN ($in)");
- }
- LJ::MemCache::set([$userid,"uprop:$userid:$_"], "", $expire) foreach (@$list);
- }
- }
-
- # if we had any multihomed props, set them here
- if (%multihomed) {
- my $dbh = LJ::get_db_writer();
- return 0 unless $dbh && $u->writer;
- while (my ($propid, $pvalue) = each %multihomed) {
- if (defined $pvalue && $pvalue) {
- # replace data into master
- $dbh->do("REPLACE INTO userprop VALUES (?, ?, ?)",
- undef, $userid, $propid, $pvalue);
- } else {
- # delete data from master, but keep in cluster
- $dbh->do("DELETE FROM userprop WHERE userid = ? AND upropid = ?",
- undef, $userid, $propid);
- }
-
- # fail out?
- return 0 if $dbh->err;
-
- # put data in cluster
- $pvalue ||= '';
- $u->do("REPLACE INTO userproplite2 VALUES (?, ?, ?)",
- undef, $userid, $propid, $pvalue);
- return 0 if $u->err;
-
- # set memcache
- LJ::MemCache::set([$userid,"uprop:$userid:$propid"], $pvalue, $expire);
- }
- }
-
- return 1;
-}
-
-# <LJFUNC>
-# name: LJ::get_shared_journals
-# des: Gets an array of shared journals a user has access to.
-# returns: An array of shared journals.
+
+sub memcache_get_u
+{
+ my @keys = @_;
+ my @ret;
+ foreach my $ar (values %{LJ::MemCache::get_multi(@keys) || {}}) {
+ my $row = LJ::MemCache::array_to_hash("user", $ar)
+ or next;
+ my $u = LJ::User->new_from_row($row);
+ push @ret, $u;
+ }
+ return wantarray ? @ret : $ret[0];
+}
+
+
+# <LJFUNC>
+# name: LJ::memcache_kill
+# des: Kills a memcache entry, given a userid and type.
+# args: uuserid, type
+# des-uuserid: a userid or u object
+# des-type: memcached key type, will be used as "$type:$userid"
+# returns: results of LJ::MemCache::delete
+# </LJFUNC>
+sub memcache_kill {
+ my ($uuid, $type) = @_;
+ my $userid = LJ::want_userid($uuid);
+ return undef unless $userid && $type;
+
+ return LJ::MemCache::delete([$userid, "$type:$userid"]);
+}
+
+
+sub memcache_set_u
+{
+ my $u = shift;
+ return unless $u;
+ my $expire = time() + 1800;
+ my $ar = LJ::MemCache::hash_to_array("user", $u);
+ return unless $ar;
+ LJ::MemCache::set([$u->{'userid'}, "userid:$u->{'userid'}"], $ar, $expire);
+ LJ::MemCache::set("uidof:$u->{user}", $u->{userid});
+}
+
+
+sub update_user
+{
+ my ($arg, $ref) = @_;
+ my @uid;
+
+ if (ref $arg eq "ARRAY") {
+ @uid = @$arg;
+ } else {
+ @uid = want_userid($arg);
+ }
+ @uid = grep { $_ } map { $_ + 0 } @uid;
+ return 0 unless @uid;
+
+ my @sets;
+ my @bindparams;
+ my $used_raw = 0;
+ while (my ($k, $v) = each %$ref) {
+ if ($k eq "raw") {
+ $used_raw = 1;
+ push @sets, $v;
+ } elsif ($k eq 'email') {
+ set_email($_, $v) foreach @uid;
+ } elsif ($k eq 'password') {
+ set_password($_, $v) foreach @uid;
+ } else {
+ push @sets, "$k=?";
+ push @bindparams, $v;
+ }
+ }
+ return 1 unless @sets;
+ my $dbh = LJ::get_db_writer();
+ return 0 unless $dbh;
+ {
+ local $" = ",";
+ my $where = @uid == 1 ? "userid=$uid[0]" : "userid IN (@uid)";
+ $dbh->do("UPDATE user SET @sets WHERE $where", undef,
+ @bindparams);
+ return 0 if $dbh->err;
+ }
+ if (@LJ::MEMCACHE_SERVERS) {
+ LJ::memcache_kill($_, "userid") foreach @uid;
+ }
+
+ if ($used_raw) {
+ # for a load of userids from the master after update
+ # so we pick up the values set via the 'raw' option
+ require_master(sub { LJ::load_userids(@uid) });
+ } else {
+ foreach my $uid (@uid) {
+ while (my ($k, $v) = each %$ref) {
+ my $cache = $LJ::REQ_CACHE_USER_ID{$uid} or next;
+ $cache->{$k} = $v;
+ }
+ }
+ }
+
+ # log this updates
+ LJ::run_hooks("update_user", userid => $_, fields => $ref)
+ for @uid;
+
+ return 1;
+}
+
+
+# <LJFUNC>
+# name: LJ::wipe_major_memcache
+# des: invalidate all major memcache items associated with a given user.
# args: u
-# </LJFUNC>
-sub get_shared_journals
-{
- my $u = shift;
- my $ids = LJ::load_rel_target($u, 'A') || [];
-
- # have to get usernames;
- my %users;
- LJ::load_userids_multiple([ map { $_, \$users{$_} } @$ids ], [$u]);
- return sort map { $_->{'user'} } values %users;
-}
+# returns: nothing
+# </LJFUNC>
+sub wipe_major_memcache
+{
+ my $u = shift;
+ my $userid = LJ::want_userid($u);
+ foreach my $key ("userid","bio","talk2ct","talkleftct","log2ct",
+ "log2lt","memkwid","dayct2","s1overr","s1uc","fgrp",
+ "wt_edges","wt_edges_rev","tu","upicinf","upiccom",
+ "upicurl", "upicdes", "intids", "memct", "lastcomm")
+ {
+ LJ::memcache_kill($userid, $key);
+ }
+}
+
+# <LJFUNC>
+# name: LJ::_load_user_raw
+# des-db: $dbh/$dbr
+# des-key: either "userid" or "user" (the WHERE part)
+# des-vals: value or arrayref of values for key to match on
+# des-hook: optional code ref to run for each $u
+# returns: last $u found
+sub _load_user_raw
+{
+ my ($db, $key, $vals, $hook) = @_;
+ $hook ||= sub {};
+ $vals = [ $vals ] unless ref $vals eq "ARRAY";
+
+ my $use_isam;
+ unless ($LJ::CACHE_NO_ISAM{user} || scalar(@$vals) > 10) {
+ eval { $db->do("HANDLER user OPEN"); };
+ if ($@ || $db->err) {
+ $LJ::CACHE_NO_ISAM{user} = 1;
+ } else {
+ $use_isam = 1;
+ }
+ }
+
+ my $last;
+
+ if ($use_isam) {
+ $key = "PRIMARY" if $key eq "userid";
+ foreach my $v (@$vals) {
+ my $sth = $db->prepare("HANDLER user READ `$key` = (?) LIMIT 1");
+ $sth->execute($v);
+ my $row = $sth->fetchrow_hashref;
+ if ($row) {
+ my $u = LJ::User->new_from_row($row);
+ $hook->($u);
+ $last = $u;
+ }
+ }
+ $db->do("HANDLER user close");
+ } else {
+ my $in = join(", ", map { $db->quote($_) } @$vals);
+ my $sth = $db->prepare("SELECT * FROM user WHERE $key IN ($in)");
+ $sth->execute;
+ while (my $row = $sth->fetchrow_hashref) {
+ my $u = LJ::User->new_from_row($row);
+ $hook->($u);
+ $last = $u;
+ }
+ }
+
+ return $last;
+}
+
+
+sub _set_u_req_cache {
+ my $u = shift or die "no u to set";
+
+ # if we have an existing user singleton, upgrade it with
+ # the latested data, but keep using its address
+ if (my $eu = $LJ::REQ_CACHE_USER_ID{$u->{'userid'}}) {
+ LJ::assert_is($eu->{userid}, $u->{userid});
+ $eu->selfassert;
+ $u->selfassert;
+
+ $eu->{$_} = $u->{$_} foreach keys %$u;
+ $u = $eu;
+ }
+ $LJ::REQ_CACHE_USER_NAME{$u->{'user'}} = $u;
+ $LJ::REQ_CACHE_USER_ID{$u->{'userid'}} = $u;
+ return $u;
+}
+
+
+########################################################################
+### 6. What the App Shows to Users
+
+# <LJFUNC>
+# name: LJ::get_timezone
+# des: Gets the timezone offset for the user.
+# args: u, offsetref, fakedref
+# des-u: user object.
+# des-offsetref: reference to scalar to hold timezone offset;
+# des-fakedref: reference to scalar to hold whether this timezone was
+# faked. 0 if it is the timezone specified by the user.
+# returns: nonzero if successful.
+# </LJFUNC>
+sub get_timezone {
+ my ($u, $offsetref, $fakedref) = @_;
+
+ # See if the user specified their timezone
+ if (my $tz = $u->prop('timezone')) {
+ # If the eval fails, we'll fall through to guessing instead
+ my $dt = eval {
+ DateTime->from_epoch(
+ epoch => time(),
+ time_zone => $tz,
+ );
+ };
+
+ if ($dt) {
+ $$offsetref = $dt->offset() / (60 * 60); # Convert from seconds to hours
+ $$fakedref = 0 if $fakedref;
+
+ return 1;
+ }
+ }
+
+ # Either the user hasn't set a timezone or we failed at
+ # loading it. We guess their current timezone's offset
+ # by comparing the gmtime of their last post with the time
+ # they specified on that post.
+
+ # first, check request cache
+ my $timezone = $u->{_timezone_guess};
+ if ($timezone) {
+ $$offsetref = $timezone;
+ return 1;
+ }
+
+ # next, check memcache
+ my $memkey = [$u->userid, 'timezone_guess:' . $u->userid];
+ my $memcache_data = LJ::MemCache::get($memkey);
+ if ($memcache_data) {
+ # fill the request cache since it was empty
+ $u->{_timezone_guess} = $memcache_data;
+ $$offsetref = $memcache_data;
+ return 1;
+ }
+
+ # nothing in cache; check db
+ my $dbcr = LJ::get_cluster_def_reader($u);
+ return 0 unless $dbcr;
+
+ $$fakedref = 1 if $fakedref;
+
+ # grab the times on the last post that wasn't backdated.
+ # (backdated is rlogtime == $LJ::EndOfTime)
+ if (my $last_row = $dbcr->selectrow_hashref(
+ qq{
+ SELECT rlogtime, eventtime
+ FROM log2
+ WHERE journalid = ? AND rlogtime <> ?
+ ORDER BY rlogtime LIMIT 1
+ }, undef, $u->{userid}, $LJ::EndOfTime)) {
+ my $logtime = $LJ::EndOfTime - $last_row->{'rlogtime'};
+ my $eventtime = LJ::mysqldate_to_time($last_row->{'eventtime'}, 1);
+ my $hourdiff = ($eventtime - $logtime) / 3600;
+
+ # if they're up to a quarter hour behind, round up.
+ $hourdiff = $hourdiff > 0 ? int($hourdiff + 0.25) : int($hourdiff - 0.25);
+
+ # if the offset is more than 24h in either direction, then the last
+ # entry is probably unreliable. don't use any offset at all.
+ $$offsetref = (-24 < $hourdiff && $hourdiff < 24) ? $hourdiff : 0;
+
+ # set the caches
+ $u->{_timezone_guess} = $$offsetref;
+ my $expire = 60*60*24; # 24 hours
+ LJ::MemCache::set($memkey, $$offsetref, $expire);
+ }
+
+ return 1;
+}
+
# <LJFUNC>
# class: component
@@ -6093,508 +6592,198 @@ sub ljuser
}
}
-sub set_email {
- my ($userid, $email) = @_;
-
- my $dbh = LJ::get_db_writer();
- if ($LJ::DEBUG{'write_emails_to_user_table'}) {
- $dbh->do("UPDATE user SET email=? WHERE userid=?", undef,
- $email, $userid);
- }
- $dbh->do("REPLACE INTO email (userid, email) VALUES (?, ?)",
- undef, $userid, $email);
-
- # update caches
- LJ::memcache_kill($userid, "userid");
- LJ::MemCache::delete([$userid, "email:$userid"]);
- my $cache = $LJ::REQ_CACHE_USER_ID{$userid} or return;
- $cache->{'_email'} = $email;
-}
-
-sub set_password {
- my ($userid, $password) = @_;
-
- my $dbh = LJ::get_db_writer();
- if ($LJ::DEBUG{'write_passwords_to_user_table'}) {
- $dbh->do("UPDATE user SET password=? WHERE userid=?", undef,
- $password, $userid);
- }
- $dbh->do("REPLACE INTO password (userid, password) VALUES (?, ?)",
- undef, $userid, $password);
-
- # update caches
- LJ::memcache_kill($userid, "userid");
- LJ::MemCache::delete([$userid, "pw:$userid"]);
- my $cache = $LJ::REQ_CACHE_USER_ID{$userid} or return;
- $cache->{'_password'} = $password;
-}
-
-sub update_user
-{
- my ($arg, $ref) = @_;
- my @uid;
-
- if (ref $arg eq "ARRAY") {
- @uid = @$arg;
- } else {
- @uid = want_userid($arg);
- }
- @uid = grep { $_ } map { $_ + 0 } @uid;
- return 0 unless @uid;
-
- my @sets;
- my @bindparams;
- my $used_raw = 0;
- while (my ($k, $v) = each %$ref) {
- if ($k eq "raw") {
- $used_raw = 1;
- push @sets, $v;
- } elsif ($k eq 'email') {
- set_email($_, $v) foreach @uid;
- } elsif ($k eq 'password') {
- set_password($_, $v) foreach @uid;
- } else {
- push @sets, "$k=?";
- push @bindparams, $v;
- }
- }
- return 1 unless @sets;
- my $dbh = LJ::get_db_writer();
- return 0 unless $dbh;
- {
- local $" = ",";
- my $where = @uid == 1 ? "userid=$uid[0]" : "userid IN (@uid)";
- $dbh->do("UPDATE user SET @sets WHERE $where", undef,
- @bindparams);
- return 0 if $dbh->err;
- }
+
+########################################################################
+### 7. Userprops, Caps, and Displaying Content to Others
+
+# <LJFUNC>
+# name: LJ::get_bio
+# des: gets a user bio, from DB or memcache.
+# args: u, force
+# des-force: true to get data from cluster master.
+# returns: string
+# </LJFUNC>
+sub get_bio {
+ my ($u, $force) = @_;
+ return unless $u && $u->{'has_bio'} eq "Y";
+
+ my $bio;
+
+ my $memkey = [$u->{'userid'}, "bio:$u->{'userid'}"];
+ unless ($force) {
+ my $bio = LJ::MemCache::get($memkey);
+ return $bio if defined $bio;
+ }
+
+ # not in memcache, fall back to disk
+ my $db = @LJ::MEMCACHE_SERVERS || $force ?
+ LJ::get_cluster_def_reader($u) : LJ::get_cluster_reader($u);
+ $bio = $db->selectrow_array("SELECT bio FROM userbio WHERE userid=?",
+ undef, $u->{'userid'});
+
+ # set in memcache
+ LJ::MemCache::add($memkey, $bio);
+
+ return $bio;
+}
+
+
+# <LJFUNC>
+# name: LJ::load_user_props
+# des: Given a user hashref, loads the values of the given named properties
+# into that user hashref.
+# args: dbarg?, u, opts?, propname*
+# des-opts: hashref of opts. set key 'cache' to use memcache.
+# des-propname: the name of a property from the [dbtable[userproplist]] table.
+# </LJFUNC>
+sub load_user_props
+{
+ &nodb;
+
+ my $u = shift;
+ return unless isu($u);
+ return if $u->is_expunged;
+
+ my $opts = ref $_[0] ? shift : {};
+ my (@props) = @_;
+
+ my ($sql, $sth);
+ LJ::load_props("user");
+
+ ## user reference
+ my $uid = $u->{'userid'}+0;
+ $uid = LJ::get_userid($u->{'user'}) unless $uid;
+
+ my $mem = {};
+ my $use_master = 0;
+ my $used_slave = 0; # set later if we ended up using a slave
+
if (@LJ::MEMCACHE_SERVERS) {
- LJ::memcache_kill($_, "userid") foreach @uid;
- }
-
- if ($used_raw) {
- # for a load of userids from the master after update
- # so we pick up the values set via the 'raw' option
- require_master(sub { LJ::load_userids(@uid) });
- } else {
- foreach my $uid (@uid) {
- while (my ($k, $v) = each %$ref) {
- my $cache = $LJ::REQ_CACHE_USER_ID{$uid} or next;
- $cache->{$k} = $v;
- }
- }
- }
-
- # log this updates
- LJ::run_hooks("update_user", userid => $_, fields => $ref)
- for @uid;
-
- return 1;
-}
-
-# <LJFUNC>
-# name: LJ::get_timezone
-# des: Gets the timezone offset for the user.
-# args: u, offsetref, fakedref
-# des-u: user object.
-# des-offsetref: reference to scalar to hold timezone offset;
-# des-fakedref: reference to scalar to hold whether this timezone was
-# faked. 0 if it is the timezone specified by the user.
-# returns: nonzero if successful.
-# </LJFUNC>
-sub get_timezone {
- my ($u, $offsetref, $fakedref) = @_;
-
- # See if the user specified their timezone
- if (my $tz = $u->prop('timezone')) {
- # If the eval fails, we'll fall through to guessing instead
- my $dt = eval {
- DateTime->from_epoch(
- epoch => time(),
- time_zone => $tz,
- );
- };
-
- if ($dt) {
- $$o