mark: A photo of Mark kneeling on top of the Taal Volcano in the Philippines. It was a long hike. (Default)
Mark Smith ([staff profile] mark) wrote in [site community profile] changelog2009-02-27 02:51 am

[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 [staff profile] denise.

--------------------------------------------------------------------------------
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'} ? "&amp;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}&amp;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'} ? "&amp;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}&amp;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'>&nbsp;</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 = \&notification_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 = \&notification_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'>&nbsp;</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