[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
![[staff profile]](https://www.dreamwidth.org/img/silk/identity/user_staff.png)
-------------------------------------------------------------------------------- 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