# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.

package LJ::Session;
use strict;
use Carp qw(croak);
use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
use LJ::Utils qw(rand_chars);

use constant VERSION => 1;

# NOTES
#
# * fields in this object:
#     userid, sessid, exptype, auth, timecreate, timeexpire, ipfixed
#
# * do not store any references in the LJ::Session instances because of serialization
#   and storage in memcache
#
# * a user makes a session(s).  cookies aren't sessions.  cookies are handles into
#   sessions, and there can be lots of cookies to get the same session.
#
# * this file is a mix of instance, class, and util functions/methods
#
# * the 'auth' field of the session object is the prized possession which
#   we might hide from XSS attackers.  they can steal domain cookies but
#   they're not good very long and can't do much.  it's the ljmastersession
#   containing the auth that we care about.
#

############################################################################
#  CREATE/LOAD SESSIONS OBJECTS
############################################################################

sub instance {
    my ( $class, $u, $sessid ) = @_;

    return undef unless $u && !$u->is_expunged;

    # try memory
    my $memkey = _memkey( $u, $sessid );
    my $sess   = LJ::MemCache::get($memkey);
    return $sess if $sess;

    # try master
    $sess = $u->selectrow_hashref(
        "SELECT userid, sessid, exptype, auth, timecreate, timeexpire, ipfixed "
            . "FROM sessions WHERE userid=? AND sessid=?",
        undef, $u->{'userid'}, $sessid
    ) or return undef;

    bless $sess;
    LJ::MemCache::set( $memkey, $sess );
    return $sess;
}

sub active_sessions {
    my ( $class, $u ) = @_;
    return unless $u && !$u->is_expunged;

    my $sth = $u->prepare( "SELECT userid, sessid, exptype, auth, timecreate, timeexpire, ipfixed "
            . "FROM sessions WHERE userid=? AND timeexpire > UNIX_TIMESTAMP()" );
    $sth->execute( $u->{userid} );
    my @ret;
    while ( my $rec = $sth->fetchrow_hashref ) {
        bless $rec;
        push @ret, $rec;
    }
    return @ret;
}

sub create {
    my ( $class, $u, %opts ) = @_;

    # validate options
    my $exptype = delete $opts{'exptype'} || "short";
    my $ipfixed = delete $opts{'ipfixed'};              # undef or scalar ipaddress  FIXME: validate
    my $nolog   = delete $opts{'nolog'} || 0;           # 1 to not log to loginlogs
    croak("Invalid exptype") unless $exptype =~ /^short|long|once$/;

    croak( "Invalid options: " . join( ", ", keys %opts ) ) if %opts;

    my $udbh = LJ::get_cluster_master($u);
    return undef unless $udbh;

    # clean up any old, expired sessions they might have (lazy clean)
    $u->do( "DELETE FROM sessions WHERE userid=? AND timeexpire < UNIX_TIMESTAMP()",
        undef, $u->{userid} );

    # FIXME: but this doesn't remove their memcached keys

    my $expsec     = LJ::Session->session_length($exptype);
    my $timeexpire = time() + $expsec;

    my $sess = {
        auth       => LJ::rand_chars(10),
        exptype    => $exptype,
        ipfixed    => $ipfixed,
        timeexpire => $timeexpire,
    };

    my $id = LJ::alloc_user_counter( $u, 'S' );
    return undef unless $id;

    $u->record_login($id)
        unless $nolog;

    $u->do(
        "REPLACE INTO sessions (userid, sessid, auth, exptype, "
            . "timecreate, timeexpire, ipfixed) VALUES (?,?,?,?,UNIX_TIMESTAMP()," . "?,?)",
        undef, $u->{'userid'}, $id, $sess->{'auth'}, $exptype, $timeexpire, $ipfixed
    );

    return undef if $u->err;
    $sess->{'sessid'} = $id;
    $sess->{'userid'} = $u->{'userid'};

    # clean up old sessions
    my $old =
        $udbh->selectcol_arrayref( "SELECT sessid FROM sessions WHERE "
            . "userid=$u->{'userid'} AND "
            . "timeexpire < UNIX_TIMESTAMP()" );
    $u->kill_sessions(@$old) if $old;

    # mark account as being used
    LJ::mark_user_active( $u, 'login' );

    bless $sess;
    return $u->{'_session'} = $sess;
}

############################################################################
#  INSTANCE METHODS
############################################################################

# not stored in database, call this before calling to update cookie strings
sub set_flags {
    my ( $sess, $flags ) = @_;
    $sess->{flags} = $flags;
    return;
}

sub flags {
    my $sess = shift;
    return $sess->{flags};
}

sub set_ipfixed {
    my ( $sess, $ip ) = @_;
    return $sess->_dbupdate( ipfixed => $ip );
}

sub set_exptype {
    my ( $sess, $exptype ) = @_;
    croak("Invalid exptype") unless $exptype =~ /^short|long|once$/;
    return $sess->_dbupdate(
        exptype    => $exptype,
        timeexpire => time() + LJ::Session->session_length($exptype)
    );
}

sub _dbupdate {
    my ( $sess, %changes ) = @_;
    my $u = $sess->owner;

    my $n_userid = $sess->{userid} + 0;
    my $n_sessid = $sess->{sessid} + 0;

    my @sets;
    my @values;
    foreach my $k ( keys %changes ) {
        push @sets,   "$k=?";
        push @values, $changes{$k};
    }

    my $rv = $u->do(
        "UPDATE sessions SET "
            . join( ", ", @sets )
            . " WHERE userid=$n_userid AND sessid=$n_sessid",
        undef, @values
    );
    if ( !$rv ) {

        # FIXME: eventually use Error::Strict here on return
        return 0;
    }

    # update ourself, once db update succeeded
    foreach my $k ( keys %changes ) {
        $sess->{$k} = $changes{$k};
    }

    LJ::MemCache::delete( $sess->_memkey );
    return 1;

}

# returns unix timestamp of expiration
sub expiration_time {
    my $sess = shift;

    # expiration time if we have it,
    return $sess->{timeexpire} if $sess->{timeexpire};

    $sess->{timeexpire} = time() + LJ::Session->session_length( $sess->{exptype} );
    return $sess->{timeexpire};
}

# return format of the "ljloggedin" cookie.
sub loggedin_cookie_string {
    my ($sess) = @_;
    return "u$sess->{userid}:s$sess->{sessid}";
}

sub master_cookie_string {
    my $sess = shift;

    my $ver    = VERSION;
    my $cookie = "v$ver:" . "u$sess->{userid}:" . "s$sess->{sessid}:" . "a$sess->{auth}";

    if ( $sess->{flags} ) {
        $cookie .= ":f$sess->{flags}";
    }

    $cookie .= "//" . LJ::eurl( $LJ::COOKIE_GEN || "" );
    return $cookie;
}

sub domsess_cookie_string {
    my ( $sess, $domcook ) = @_;
    croak("No domain cookie provided") unless $domcook;

    # compute a signed domain key
    my ( $time, $key ) = LJ::get_secret();
    my $sig = domsess_signature( $time, $sess, $domcook );

    # the cookie
    my $ver = VERSION;
    my $value =
          "v$ver:"
        . "u$sess->{userid}:"
        . "s$sess->{sessid}:"
        . "t$time:"
        . "g$sig//"
        . LJ::eurl( $LJ::COOKIE_GEN || "" );

    return $value;
}

# sets new ljmastersession cookie given the session object
sub update_master_cookie {
    my ($sess) = @_;

    my @expires;
    if ( $sess->{exptype} eq 'long' ) {
        push @expires, expires => $sess->expiration_time;
    }

    my $domain = $LJ::DOMAIN_WEB || $LJ::DOMAIN;

    set_cookie(
        ljmastersession => $sess->master_cookie_string,
        domain          => $domain,
        path            => '/',
        http_only       => 1,
        @expires,
    );

    set_cookie(
        ljloggedin => $sess->loggedin_cookie_string,
        domain     => $LJ::DOMAIN,
        path       => '/',
        http_only  => 1,
        @expires,
    );

    $sess->owner->preload_props('schemepref');

    if ( my $scheme = $sess->owner->prop('schemepref') ) {
        set_cookie(
            BMLschemepref => $scheme,
            domain        => $LJ::DOMAIN,
            path          => '/',
            http_only     => 1,
            @expires,
        );
    }
    else {
        set_cookie(
            BMLschemepref => "",
            domain        => $LJ::DOMAIN,
            path          => '/',
            delete        => 1
        );
    }

    return;
}

sub auth {
    my $sess = shift;
    return $sess->{auth};
}

# NOTE: do not store any references in the LJ::Session instances because of serialization
# and storage in memcache
sub owner {
    my $sess = shift;
    return LJ::load_userid( $sess->{userid} );
}

# instance method:  has this session expired, or is it IP bound and
# bound to the wrong IP?
sub valid {
    my $sess = shift;
    my $now  = time();
    my $err  = sub { 0; };

    return $err->("Invalid auth") if $sess->{'timeexpire'} < $now;

    if ( $sess->{'ipfixed'} && !$LJ::Session::OPT_IGNORE_IP ) {
        my $remote_ip = LJ::get_remote_ip();
        return $err->("Session wrong IP ($remote_ip != $sess->{ipfixed})")
            if $sess->{'ipfixed'} ne $remote_ip;
    }

    return 1;
}

sub id {
    my $sess = shift;
    return $sess->{sessid};
}

sub ipfixed {
    my $sess = shift;
    return $sess->{ipfixed};
}

sub exptype {
    my $sess = shift;
    return $sess->{exptype};
}

# end a session
sub destroy {
    my $sess = shift;
    my $id   = $sess->id;
    my $u    = $sess->owner;

    return LJ::Session->destroy_sessions( $u, $id );
}

# based on our type and current expiration length, update this cookie if we need to
sub try_renew {
    my ( $sess, $cookies ) = @_;

    # only renew long type cookies
    return if $sess->{exptype} ne 'long';

    # how long to live for
    my $u           = $sess->owner;
    my $sess_length = LJ::Session->session_length( $sess->{exptype} );
    my $now         = time();
    my $new_expire  = $now + $sess_length;

    # if there is a new session length to be set and the user's db writer is available,
    # go ahead and set the new session expiration in the database. then only update the
    # cookies if the database operation is successful
    if (   $sess_length
        && $sess->{'timeexpire'} - $now < $sess_length / 2
        && $u->writer
        && $sess->_dbupdate( timeexpire => $new_expire ) )
    {
        $sess->update_master_cookie;
    }
}

############################################################################
#  CLASS METHODS
############################################################################

# NOTE: internal function REQUIRES trusted input
sub helper_url {
    my ( $class, $dest ) = @_;

    return unless $dest;

    my $u = LJ::get_remote();
    unless ($u) {
        LJ::Session->clear_master_cookie;
        return $dest;
    }

    my $domcook = LJ::Session->domain_cookie($dest)
        or return;

    if ( $dest =~ m!^(https?://)([^/]*?)\.\Q$LJ::USER_DOMAIN\E/?([^/]*)! ) {
        my $url = "$1$2.$LJ::USER_DOMAIN/";
        if ( is_journal_subdomain($2) ) {
            $url .= "$3/"
                if $3 && ( $3 ne '/' );    # 'http://community.livejournal.com/name/__setdomsess'
        }

        my $sess   = $u->session;
        my $cookie = $sess->domsess_cookie_string($domcook);
        return
              $url
            . "__setdomsess?dest="
            . LJ::eurl($dest) . "&k="
            . LJ::eurl($domcook) . "&v="
            . LJ::eurl($cookie);
    }

    return;
}

# given a URL (or none, for current url), what domain cookie represents this URL?
# return undef if not URL for a domain cookie, which means either bogus URL
# or the master cookies should be tried.
sub domain_cookie {
    my ( $class,     $url )  = @_;
    my ( $subdomain, $user ) = LJ::Session->domain_journal($url);

    # undef:  not on a user-subdomain
    return undef unless $subdomain;

    # on a user subdomain, or shared subdomain
    if ( $user ne "" ) {
        $user =~ s/-/_/g;    # URLs may be - or _, convert to _ which is what usernames contain
        return "ljdomsess.$subdomain.$user";
    }
    else {
        return "ljdomsess.$subdomain";
    }
}

# given an optional URL (by default, the current URL), what is the username
# of that URL?.  undef if no user.  in list context returns the ($subdomain, $user)
# where $user can be "" if $subdomain isn't, say, "community" or "users".
# in scalar context, userame is always the canonical username (no hypens/capitals)
sub domain_journal {
    my ( $class, $url ) = @_;

    $url ||= LJ::create_url( undef, keep_args => 1 );
    return undef
        unless $url =~ m!^https?://(.+?)(/.*)$!;

    my ( $host, $path ) = ( $1, $2 );
    $host = lc($host);

    # don't return a domain cookie for the master domain
    return undef if $host eq lc($LJ::DOMAIN_WEB) || $host eq lc($LJ::DOMAIN);

    return undef
        unless $host =~ m!^([-\w\.]{1,50})\.\Q$LJ::USER_DOMAIN\E$!;

    my $subdomain = lc($1);
    if ( is_journal_subdomain($subdomain) ) {
        my $user = get_path_user($path);
        return undef unless $user;
        return wantarray ? ( $subdomain, $user ) : $user;
    }

    # where $subdomain is actually a username:
    return wantarray ? ( $subdomain, "" ) : LJ::canonical_username($subdomain);
}

sub url_owner {
    my ( $class, $url ) = @_;
    $url ||= LJ::create_url( undef, keep_args => 1 );
    my ( $subdomain, $user ) = LJ::Session->domain_journal($url);
    $user = $subdomain if $user eq "";
    return LJ::canonical_username($user);
}

# CLASS METHOD
#  -- frontend to session_from_domain_cookie and session_from_master_cookie below
sub session_from_cookies {
    my $class   = shift;
    my %getopts = @_;

    my $r = DW::Request->get;
    return undef unless $r;

    my $sessobj;

    my $domain_cookie = LJ::Session->domain_cookie;
    if ($domain_cookie) {

        # journal domain
        $sessobj =
            LJ::Session->session_from_domain_cookie( \%getopts, $r->cookie_multi($domain_cookie) );
    }
    else {
        # this is the master cookie at "www.livejournal.com" or "livejournal.com";
        my @cookies = $r->cookie_multi('ljmastersession');

        # but support old clients who are just sending an "ljsession" cookie which they got
        # from LJ::Protocol's "generatesession" mode.
        unless (@cookies) {
            @cookies = $r->cookie_multi('ljsession');
            $getopts{old_cookie} = 1;
        }
        $sessobj = LJ::Session->session_from_master_cookie( \%getopts, @cookies );
    }

    return $sessobj;
}

# CLASS METHOD
#   -- but not called directly.  usually called by LJ::Session->session_from_cookies above
sub session_from_domain_cookie {
    my $class = shift;
    my $opts  = ref $_[0] ? shift() : {};

    my $r = DW::Request->get;

    # the logged-in cookie
    my $li_cook = $r->cookie('ljloggedin');
    return undef unless $li_cook;

    my $no_session = sub {
        my $reason = shift;
        warn "No session found for domain cookie: $reason\n" if $LJ::IS_DEV_SERVER;

        my $rr = $opts->{redirect_ref};
        $$rr =
            "$LJ::SITEROOT/misc/get_domain_session?return="
            . LJ::eurl( LJ::create_url( undef, keep_args => 1 ) )
            if $rr;

        return undef;
    };

    my @cookies = grep { $_ } @_;
    return $no_session->("no cookies") unless @cookies;

    my $domcook = LJ::Session->domain_cookie;

    foreach my $cookie (@cookies) {
        my $sess = valid_domain_cookie( $domcook, $cookie->[0], $li_cook );
        return $sess if $sess;
    }

    return $no_session->("no valid cookie");
}

# CLASS METHOD
#   -- but not called directly.  usually called by LJ::Session->session_from_cookies above
# call: ( $opts?, @ljmastersession_cookie(s) )
# return value is LJ::Session object if we found one; else undef
# FIXME: document ops
sub session_from_master_cookie {
    my $class   = shift;
    my $opts    = ref $_[0] ? shift() : {};
    my @cookies = grep { $_ } @_;
    return undef unless @cookies;

    my $r = DW::Request->get;

    my $errs       = delete $opts->{errlist}    || [];
    my $tried_fast = delete $opts->{tried_fast} || do { my $foo; \$foo; };
    my $ignore_ip  = delete $opts->{ignore_ip}  ? 1 : 0;
    my $old_cookie = delete $opts->{old_cookie} ? 1 : 0;

    delete $opts->{redirect_ref};    # we don't use this
    croak("Unknown options") if %$opts;

    my $now = time();

    # our return value
    my $sess;

    my $li_cook = $r->cookie('ljloggedin');

COOKIE:
    foreach my $sessdata (@cookies) {
        my ( $cookie, $gen ) = split( m!//!, $sessdata->[0] );

        my ( $version, $userid, $sessid, $auth, $flags );

        my $dest = {
            v => \$version,
            u => \$userid,
            s => \$sessid,
            a => \$auth,
            f => \$flags,
        };

        my $bogus = 0;
        foreach my $var ( split /:/, $cookie ) {
            if ( $var =~ /^(\w)(.+)$/ && $dest->{$1} ) {
                ${ $dest->{$1} } = $2;
            }
            else {
                $bogus = 1;
            }
        }

        # must do this first so they can't trick us
        $$tried_fast = 1 if $flags && $flags =~ /\.FS\b/;

        next COOKIE if $bogus;

        next COOKIE unless valid_cookie_generation($gen);

        my $err = sub {
            $sess = undef;
            push @$errs, "$sessdata: $_[0]";
        };

        # fail unless version matches current
        unless ( $version == VERSION ) {
            $err->("no ws auth");
            next COOKIE;
        }

        my $u = LJ::load_userid($userid);
        unless ($u) {
            $err->("user doesn't exist");
            next COOKIE;
        }

        # locked accounts can't be logged in
        if ( $u->is_locked ) {
            $err->("User account is locked.");
            next COOKIE;
        }

        $sess = LJ::Session->instance( $u, $sessid );

        unless ($sess) {
            $err->("Couldn't find session");
            next COOKIE;
        }

        unless ( $sess->{auth} eq $auth ) {
            $err->("Invald auth");
            next COOKIE;
        }

        unless ( $sess->valid ) {
            $err->("expired or IP bound problems");
            next COOKIE;
        }

        # make sure their ljloggedin cookie
        unless ( $old_cookie || $sess->loggedin_cookie_string eq $li_cook ) {
            $err->("loggedin cookie bogus");
            next COOKIE;
        }

        last COOKIE;
    }

    return $sess;
}

# class method
sub destroy_all_sessions {
    my ( $class, $u ) = @_;
    return 0 unless $u;

    my $udbh = LJ::get_cluster_master($u)
        or return 0;

    my $sessions = $udbh->selectcol_arrayref( "SELECT sessid FROM sessions WHERE " . "userid=?",
        undef, $u->{'userid'} );

    return LJ::Session->destroy_sessions( $u, @$sessions ) if @$sessions;
    return 1;
}

# class method
sub destroy_sessions {
    my ( $class, $u, @sessids ) = @_;

    my $in = join( ',', map { $_ + 0 } @sessids );
    return 1 unless $in;
    my $userid = $u->{'userid'};
    foreach (qw(sessions sessions_data)) {
        $u->do( "DELETE FROM $_ WHERE userid=? AND " . "sessid IN ($in)", undef, $userid )
            or return 0;    # FIXME: use Error::Strict
    }
    foreach my $id (@sessids) {
        $id += 0;
        LJ::MemCache::delete( _memkey( $u, $id ) );
    }
    return 1;

}

sub clear_master_cookie {
    my ($class) = @_;

    my $domain = $LJ::DOMAIN_WEB || $LJ::DOMAIN;

    set_cookie(
        ljmastersession => "",
        domain          => $domain,
        path            => '/',
        delete          => 1
    );

    set_cookie(
        ljloggedin => "",
        domain     => $LJ::DOMAIN,
        path       => '/',
        delete     => 1
    );
}

# CLASS method for getting the length of a given session type in seconds
sub session_length {
    my ( $class, $exptype ) = @_;
    croak("Invalid exptype") unless $exptype =~ /^short|long|once$/;

    return {
        short => 60 * 60 * 24 * 1.5,    # 1.5 days
        long  => 60 * 60 * 24 * 60,     # 60 days
        once  => 60 * 60 * 2,           # 2 hours
    }->{$exptype};
}

# returns the URL to go to after setting the domain cookie
sub setdomsess_handler {
    my ($class) = @_;

    my $r = DW::Request->get;

    my $get = $r->get_args;

    my $dest    = $get->{'dest'};
    my $domcook = $get->{'k'};
    my $cookie  = $get->{'v'};

    return "$LJ::SITEROOT" unless valid_destination($dest);
    return $dest unless valid_domain_cookie( $domcook, $cookie, $r->cookie('ljloggedin') );

    my $path = get_cookie_path($dest);

    my $expires = $LJ::DOMSESS_EXPIRATION || 0;    # session-cookie only
    set_cookie(
        $domcook  => $cookie,
        path      => $path,
        http_only => 1,
        expires   => $expires
    );

    # add in a trailing slash, if URL doesn't have at least two slashes.
    # otherwise the path on the cookie above (which is like /community/)
    # won't be caught when we bounce them to /community.
    unless ( $dest =~ m!^https?://.+?/.+?/! || $path eq "/" ) {

        # add a slash unless we can slip one in before the query parameters
        $dest .= "/" unless $dest =~ s!\?!/?!;
    }

    return $dest;
}

############################################################################
#  UTIL FUNCTIONS
############################################################################

sub domsess_signature {
    my ( $time, $sess, $domcook ) = @_;

    my $u      = $sess->owner;
    my $secret = LJ::get_secret($time);

    my $data = join( "-", $sess->{auth}, $domcook, $u->{userid}, $sess->{sessid}, $time );
    my $sig  = hmac_sha1_hex( $data, $secret );
    return $sig;
}

# function or instance method.
# FIXME: update the documentation for memkeys
sub _memkey {
    if ( @_ == 2 ) {
        my ( $u, $sessid ) = @_;
        $sessid += 0;
        return [ $u->{'userid'}, "ljms:$u->{'userid'}:$sessid" ];
    }
    else {
        my $sess = shift;
        return [ $sess->{'userid'}, "ljms:$sess->{'userid'}:$sess->{sessid}" ];
    }
}

# FIXME: move this somewhere better
sub set_cookie {
    my ( $key, $value, %opts ) = @_;

    my $r = DW::Request->get;
    return unless $r;

    my $http_only = delete $opts{http_only};
    my $domain    = delete $opts{domain};
    my $path      = delete $opts{path};
    my $expires   = delete $opts{expires};
    my $delete    = delete $opts{delete};
    croak( "Invalid cookie options: " . join( ", ", keys %opts ) ) if %opts;

    # expires can be absolute or relative.  this is gross or clever, your pick.
    $expires += time() if $expires && $expires <= 1135217120;

    # set expires to 5 seconds after 1970.  definitely in the past.
    # so cookie will be deleted.
    $expires = 5 if $delete;

    $r->add_cookie(
        name     => $key,
        value    => $value,
        expires  => $expires ? LJ::time_to_cookie($expires) : undef,
        domain   => $domain || undef,
        path     => $path || undef,
        httponly => $http_only ? 1 : 0,
    );

}

# returns undef or a session, given a $domcook and its $val, as well
# as the current logged-in cookie $li_cook which says the master
# session's uid/sessid
sub valid_domain_cookie {
    my ( $domcook, $val, $li_cook, $opts ) = @_;
    $opts ||= {};

    my ( $cookie, $gen ) = split m!//!, $val;

    my ( $version, $uid, $sessid, $time, $sig, $flags );
    my $dest = {
        v => \$version,
        u => \$uid,
        s => \$sessid,
        t => \$time,
        g => \$sig,
        f => \$flags,
    };

    my $bogus = 0;
    foreach my $var ( split /:/, $cookie ) {
        if ( $var =~ /^(\w)(.+)$/ && $dest->{$1} ) {
            ${ $dest->{$1} } = $2;
        }
        else {
            $bogus = 1;
        }
    }

    my $not_valid = sub {
        my $reason = shift;
        warn "Invalid domain cookie: $reason\n" if $LJ::IS_DEV_SERVER;

        return undef;
    };

    return $not_valid->("bogus params") if $bogus;
    return $not_valid->("wrong gen") unless valid_cookie_generation($gen);
    return $not_valid->("wrong ver") if $version != VERSION;

    # have to be relatively new.  these shouldn't last longer than a day
    # or so anyway.
    unless ( $opts->{ignore_age} ) {
        my $now = time();
        return $not_valid->("old cookie") unless $time > $now - 86400 * 7;
    }

    my $u = LJ::load_userid($uid)
        or return $not_valid->("no user $uid");

    my $sess = $u->session($sessid)
        or return $not_valid->("no session $sessid");

    # the master session can't be expired or ip-bound to wrong IP
    return $not_valid->("not valid") unless $sess->valid;

    # the per-domain cookie has to match the session of the master cookie
    unless ( $opts->{ignore_li_cook} ) {
        my $sess_licook = $sess->loggedin_cookie_string;
        return $not_valid->("li_cook mismatch.  session=$sess_licook, user=$li_cook")
            unless $sess_licook eq $li_cook;
    }

    my $correct_sig = domsess_signature( $time, $sess, $domcook );
    return $not_valid->("signature wrong") unless $correct_sig eq $sig;

    return $sess;
}

sub valid_destination {
    my $dest = shift;
    return $dest =~ qr!^https?://[-\w\.]+\.\Q$LJ::USER_DOMAIN\E/!;
}

sub valid_cookie_generation {
    my $gen  = shift || '';
    my $dgen = LJ::durl($gen);
    foreach my $okay ( $LJ::COOKIE_GEN, @LJ::COOKIE_GEN_OKAY ) {
        $okay = '' unless defined $okay;
        return 1 if $gen eq $okay;
        return 1 if $dgen eq $okay;
    }
    return 0;
}

sub is_journal_subdomain {
    my ($subdomain) = @_;
    return 0 unless defined $subdomain;
    $subdomain = lc $subdomain;

    my $func = $LJ::SUBDOMAIN_FUNCTION{$subdomain};
    return $func && $func eq "journal" ? 1 : 0;
}

sub get_cookie_path {
    my ($dest) = @_;
    my $path = '/';    # By default cookie path is root

    # If it is not the master domain, include the username

    if ( $dest && $dest =~ m!^https?://(.+?)(/.*)$! ) {
        my ( $host, $url_path ) = ( lc($1), $2 );
        my $path_user = get_path_user($url_path);

        if (
            $host =~ m!^([-\w\.]{1,50})\.\Q$LJ::USER_DOMAIN\E$!
            && is_journal_subdomain($1)    # undef: not on a user-subdomain
            && $path_user
            )
        {

            $path = '/' . $path_user . '/';
        }
    }

    return $path;
}

sub get_path_user {
    my ($path) = @_;
    return unless $path =~ m!^/(\w{1,$LJ::USERNAME_MAXLENGTH})\b!;
    return lc $1;
}

1;
