#------------------------------------------------------------
# Copyright (C) 2006 RoyaltyShare, Inc.   All Rights Reserved
# $Id$
#------------------------------------------------------------
package RSApache::Session;

#
# This package controls a user's session with our site.
# It writes and reads cookies to manage this. Currently
# we only use one cookie to monitor access to our sites.
# User's must log in to each client site to gain access,
# the cookie does not work across sites.
#
# The cookie expires after one day and we do not refresh
# the cookie during a user's session.
#
# We allow administrator users to "log in as" other users
# from the user management section.  This requires the
# notion of a "master user" and an "active user", where
# the master use is the administrator and the active user
# is the user they are logged in as.  The active user is
# always used for permission checks and site access.
#
# JPK - Session state is now held in a database table called 'session'.
# The cookie just contains the session_id, and a checksum.
# Note- If you want to reset the session table, use 'TRUNCATE TABLE session'.
# This will drop all the rows, _and_ reset the auto-increment session_id.
#

use strict;
use warnings;
use CGI;
use CGI::Cookie;
use Data::Dumper;

use Digest::MD5 qw(md5_hex);

use lib '/app/tools/common/lib';
use Common::Consts;
use Common::Assert;
use Common::Util qw(secondsToDateTime dateTimeToSeconds);

use lib '/app/tools/data_classes/lib';
use User::User;

use lib '/app/tools/rps/lib';
use RPS::DB::Item::Session;

use constant SESSION_EXPIRE_SECONDS => 86400;
use constant CURRENT_USER_COOKIE    => "RS_USER";

# ---------------------------
# Constructor
# ---------------------------
sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;
    $self->{active_user} = undef;
    $self->{master_user} = undef;

    return $self;
}

# ---------------------------
# Properties
# ---------------------------

# The ActiveUser object
# JPK - The only code that uses this accessors to _set_ these user values
# is this module.
sub ActiveUser {
    my $self = shift;

    if ( !defined $self->{active_user} ) {
        $self->_readSessionStateFromDB();
    }
    return $self->{active_user};
}

# The MasterUser object
sub MasterUser {
    my $self = shift;

    if ( !defined $self->{master_user} ) {
        $self->_readSessionStateFromDB();
    }
    return $self->{master_user};
}

# ---------------------------
# Public Methods
# ---------------------------

# one user is logging in as himself
sub Create {
    my $self = shift;
    my $user = shift;

    return $self->CreateAs( active_user => $user, master_user => $user );
}

# allows the current user to login
# as someone else.  Pass both user objects
# ... and is also called at login in general!
sub CreateAs {
    my $self        = shift;
    my %args        = @_;
    my $active_user = $args{active_user};
    my $master_user = $args{master_user};

    assert($active_user);
    assert($master_user);

    $self->{active_user} = $active_user;
    $self->{master_user} = $master_user;

    # create cookie for "active_user" and set it.
    my $user_cookie;
    my $cookie_val = $self->_genSessionCookie();
    if ( !defined $cookie_val ) {
        $self->errstr("Login::CreateAs - _genSessionCookie failed.");
        return undef;
    }

    $user_cookie = CGI::Cookie->new(
        -name    => CURRENT_USER_COOKIE,
        -value   => $cookie_val,
        -domain  => ".royaltyshare.com",
        -expires => "+12M",
    );

    return $self->SetCookie($user_cookie);
}

# end the session by zeroing the time of the cookie
# this making it "expired" next time it's checked.
# Q: "Why don't you just set the "expires" property of the cookie?"
# A: Because the browser gets rid of old expired cookies and we don't want that.
#
sub End {
    my $self = shift;

    return undef if ( !$self->IsValid() );

    my $user_cookie = $self->GetCookie(CURRENT_USER_COOKIE);
    return undef if ( !defined $user_cookie );

    my $cookie_val = $user_cookie->value();
    return undef if ( !defined $cookie_val );

    my ( $session_id, $checksum ) = split( '-', $cookie_val );

    # Check whether the cookie even matches this session_id.
    #
    my $dbItem = RPS::DB::Item::Session->Lookup( session_id => $session_id );
    $dbItem->delete();

    return 1;
}

# commits a cookie to the apache headers
sub SetCookie {
    my $self   = shift;
    my $cookie = shift;

    my $r = Apache->request();
    $r->err_headers_out->add( "Set-Cookie" => $cookie );
    return 1;
}

# get a cookie by name
sub GetCookie {
    my $self        = shift;
    my $cookie_name = shift;

    return undef if ( !defined $cookie_name || $cookie_name eq "" );

    my %cookies = CGI::Cookie->fetch;
    return $cookies{$cookie_name};
}

# check if the current session is valid.
# look at the current user cookie and verify
# that it matches our hash_check value for
# those user values.
#
# if it is valid then set the current user
# and the "logged in as" user properties for this
# session.
sub IsValid {
    my $self = shift;

    my $user_cookie = $self->GetCookie(CURRENT_USER_COOKIE);
    return undef if ( !defined $user_cookie );

    my $cookie_val = $user_cookie->value();
    return undef if ( !defined $cookie_val );

    # The session cookie contains:
    # - session_id
    # - checksum
    #
    # Currently, the checksum is an md5 hash created from the session_id, the date_created,
    #  client ip_address, and some magic data.
    #
    my ( $session_id, $checksum ) = split( '-', $cookie_val );

    # Get the session data from the database. We'll check the 'valid' flag first,
    # then calculate the checksum and compare it with the cookie's.
    #
    my $dbItem = RPS::DB::Item::Session->Lookup( session_id => $session_id );
    return undef if ( !defined $dbItem || !defined $dbItem->session_id );

    my $dateTimeSeconds = dateTimeToSeconds( $dbItem->date_created() );
    my $hash_check = md5_hex( $session_id, $dateTimeSeconds, _clientIP(), _secret_text() );
    return undef if ( $hash_check ne $checksum );

    # JPK - Checking client_id here, but we will wish to be selective about this test.
    #       RS admins are going to be allowed to change clients without logging out.
    #
    if ( 1 != $self->MasterUser()->AccessLevel() ) {
        return undef if ( $dbItem->client_id ne $ENV{CLIENT_ID} );
    }

    # jpk - merge - There is some code here that I need to evaluate further.
    # Going to yank it out of the merged file, but we may need to re-implement it
    # This is the original comment that sort of explains what it was about:
    ## new feature -- force them to log back in if they're going to login.royaltyshare.com
    ## ensures that they see the client drop-down if needed.

    # The session appears valid, but should we expire it?
    #

    if ( $dateTimeSeconds + SESSION_EXPIRE_SECONDS < time() ) {

        # the cookie is too old
        # print STDERR "cookie has expired\n";
        $dbItem->delete;
        return undef;
    }

    # This is a valid session.
    # I want to update the session record with the current time so we can tell
    # just _how_ active a particular session may be.
    #
    $dbItem->last_access(Common::DB::Item::kDateTimeNow);
    $dbItem->save();
    return 1;

}

sub errstr {
    my $self = shift;

    if (@_) {
        my ($val) = @_;
        $self->{errstr} = $val;
    }

    return $self->{errstr};
}

# --------------------------------------------
# Private methods
# --------------------------------------------

# generate a cookie value for this user object
# JPK - Really, generate a new session in the database, then
# generate a cookie based on that.
#
sub _genSessionCookie {
    my $self      = shift;
    my $client_id = shift;    ## for specifying the client (multi-client login)

    my $now    = time();
    my $ip     = _clientIP();
    my $a_user = $self->ActiveUser;
    my $m_user = $self->MasterUser;

    # Only 1 session is allowed per master_user_id. Sensible enough.
    # I also want to avoid having sessions linger around in the table,
    # so I'm adding this call to get rid of old sessions when we establish
    # new ones.
    #
    RPS::DB::Item::Session->RemoveOldSessions( $m_user->UserID );

    my $clientID = Common::RSApp::GetClientID();

    # Now we can establish the new session in the database.
    #
    my $dbItem = RPS::DB::Item::Session->Create();
    $dbItem->master_user_id( $m_user->UserID );
    $dbItem->active_user_id( $a_user->UserID );
    $dbItem->client_id($clientID);
    $dbItem->ip_address($ip);
    $dbItem->date_created( secondsToDateTime($now) );
    $dbItem->save();

    my $session_id = $dbItem->session_id;
    die "Error - unable to generate a session cookie!" unless defined $session_id;

    my $cookie_hash = md5_hex( $session_id, $now, $ip, _secret_text() );

    return join( '-', $session_id, $cookie_hash );
}

sub _readSessionStateFromDB {
    my ($self) = @_;

    my $user_cookie = $self->GetCookie(CURRENT_USER_COOKIE);
    my $cookie_val = $user_cookie->value() if ( defined $user_cookie );
    return undef if ( !defined $cookie_val );

    my ( $session_id, $checksum ) = split( '-', $cookie_val );

    my ( $masterUserID, $activeUserID, $clientID );
    my $dbItem = RPS::DB::Item::Session->Lookup( session_id => $session_id );
    if ($dbItem) {
        $masterUserID = $dbItem->master_user_id;
        $activeUserID = $dbItem->active_user_id;
        $clientID     = $dbItem->client_id;
    }

    my $masterUser = new User::User( user_id => $masterUserID );

    #    my $masterUser = AppUser::User::User->new(userID => $masterUserID);
    $masterUser->ClientID($clientID);
    $self->{master_user} = $masterUser;

    #    my $activeUser = new User::User(user_id => $activeUserID);
    my $activeUser;
    if ( $masterUserID == $activeUserID ) {
        $activeUser = $masterUser;
    } else {
        $activeUser = new User::User( user_id => $activeUserID );

        #		$activeUser = AppUser::User::User->new(userID => $activeUserID);
    }
    $activeUser->ClientID($clientID);
    $self->{active_user} = $activeUser;

}

# our key to generating the hash_check
sub _secret_text {
    return "Fm9XZUbOdXftbk0EZmhExEsnYbioAiW8t";
}

sub _clientIP {
    my $r = Apache->request();
    return $r->connection->remote_ip();
}

###
1;    # Play nicely.
###
