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

#
# 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::Assert;
use Common::Util qw(secondsToDateTime dateTimeToSeconds);
use Common::Log;

use lib '/app/tools/appuser/lib';
use AppUser::User::User;
use AppUser::DB::Item::UserTerms;

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;
    my $clientID = shift;

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

# 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};

    # client_id is optional. Usually it is not set, and the session is therefore created using
    # the current app's client_id.  However, when logging in via 'login.royaltyshare.com', we need
    # to be able to specify it explicitly.
    #
    my $client_id = $args{client_id};

    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($client_id);
    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;
}

# This is a static method that returns the session id from user session cookie.
#
sub GetSessionIDFromCookie {
    my %cookies = CGI::Cookie->fetch;
    my $cookie  = $cookies{ CURRENT_USER_COOKIE() };
    return undef unless $cookie;
    my $cookie_val = $cookie->value();
    my ( $session_id, $checksum ) = split( '-', $cookie_val );
    return $session_id;
}

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

    my $r =
      $ENV{MOD_PERL_API_VERSION} >= 2
      ? Apache2::RequestUtil->request
      : 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 );

    # If we're in strict access mode, then we'll only allow the user to access the site to which user has an access.
    # Otherwise (if strict_access set to 0), we'll allow the user to access any site
    # Authenticated RS Admin still has access to any site.
    my $strict_access = 0;

    # 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 ( $strict_access && $self->MasterUser()->AccessLevel() != 1 ) { # User is NOT RS Admin
        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.
    #
    Common::Log::Debug( "SESSION_ID $session_id : " . $self->MasterUser->UserID . " : " . $self->ActiveUser->UserID );
    $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 $clientID = 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 );

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

    # Check to see if user has agreed to terms
    #
    my $terms = AppUser::DB::Item::UserTerms->GetLatestTermsByUserID( $m_user->UserID );

    # 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->terms_version($terms);
    $dbItem->date_created( secondsToDateTime($now) );
    $dbItem->save();

    my $session_id = $dbItem->session_id;
    Common::Log::Debug( "NEW SESSION_ID $session_id : " . $m_user->UserID . " : " . $a_user->UserID . " : $clientID : $ip " );
    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 $dbItem = RPS::DB::Item::Session->Lookup( session_id => $session_id );
    return unless defined $dbItem;

    my $masterUserID = $dbItem->master_user_id;
    my $activeUserID = $dbItem->active_user_id;
    my $clientID     = $dbItem->client_id;

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

    my $activeUser;
    if ( $masterUserID == $activeUserID ) {
        $activeUser = $masterUser;
    } else {
        $activeUser = AppUser::User::User->new( userID => $activeUserID );
    }

    $self->{active_user} = $activeUser;

}

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

sub _clientIP {
    my $r =
      $ENV{MOD_PERL_API_VERSION} >= 2
      ? Apache2::RequestUtil->request
      : Apache->request();

    # so, okay, with Apache 2.4 and modperl, we're *supposed* to have a method
    # called $r->useragent_ip available to use, but in AWS-land, we don't seem
    # to have that even though $r->connection->client_ip is noticeably gone.
    #
    # The following works with (or without) their load balancer, but not
    # totally ideal as headers can be spoofed. I suppose if somebody wants to
    # "hack" us so they can stay logged in from more than one IP address,
    # more power to 'em. If pay television channels don't seem to care, we
    # sure don't.

    my ($ip) = $r->headers_in->{'X-Forwarded-For'} =~ /([^,\s]+)$/;

    # if that's not there, they must be hitting us directly
    if ( !$ip ) {
        $ip = $ENV{REMOTE_ADDR};
    }

    return $ip;
}

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