# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

package Bugzilla::Auth::Persist::Cookie;

use 5.14.0;
use strict;
use warnings;

use fields qw();

use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Token;

use List::Util qw(first);

sub new {
  my ($class) = @_;
  my $self = fields::new($class);
  return $self;
}

sub persist_login {
  my ($self, $user) = @_;
  my $dbh          = Bugzilla->dbh;
  my $cgi          = Bugzilla->cgi;
  my $input_params = Bugzilla->input_params;

  my $ip_addr;
  if ($input_params->{'Bugzilla_restrictlogin'}) {
    $ip_addr = remote_ip();

    # The IP address is valid, at least for comparing with itself in a
    # subsequent login
    trick_taint($ip_addr);
  }

  $dbh->bz_start_transaction();

  my $login_cookie
    = Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie');

  $dbh->do(
    "INSERT INTO logincookies (cookie, userid, ipaddr, lastused)
              VALUES (?, ?, ?, NOW())", undef, $login_cookie, $user->id, $ip_addr
  );

  # Issuing a new cookie is a good time to clean up the old
  # cookies.
  $dbh->do("DELETE FROM logincookies WHERE lastused < "
      . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', MAX_LOGINCOOKIE_AGE, 'DAY'));

  $dbh->bz_commit_transaction();

  # We do not want WebServices to generate login cookies.
  # All we need is the login token for User.login.
  return $login_cookie if i_am_webservice();

  # Prevent JavaScript from accessing login cookies.
  my %cookieargs = ('-httponly' => 1);

  # Remember cookie only if admin has told so
  # or admin didn't forbid it and user told to remember.
  if (
    Bugzilla->params->{'rememberlogin'} eq 'on'
    || ( Bugzilla->params->{'rememberlogin'} ne 'off'
      && $input_params->{'Bugzilla_remember'}
      && $input_params->{'Bugzilla_remember'} eq 'on')
    )
  {
    # Not a session cookie, so set an infinite expiry
    $cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT';
  }
  if (Bugzilla->params->{'ssl_redirect'}) {

    # Make these cookies only be sent to us by the browser during
    # HTTPS sessions, if we're using SSL.
    $cookieargs{'-secure'} = 1;
  }

  $cgi->send_cookie(-name => 'Bugzilla_login', -value => $user->id, %cookieargs);
  $cgi->send_cookie(
    -name  => 'Bugzilla_logincookie',
    -value => $login_cookie,
    %cookieargs
  );
}

sub logout {
  my ($self, $param) = @_;

  my $dbh   = Bugzilla->dbh;
  my $cgi   = Bugzilla->cgi;
  my $input = Bugzilla->input_params;
  $param = {} unless $param;
  my $user = $param->{user} || Bugzilla->sudoer || Bugzilla->user;
  my $type = $param->{type} || LOGOUT_ALL;

  if ($type == LOGOUT_ALL) {
    $dbh->do("DELETE FROM logincookies WHERE userid = ?", undef, $user->id);
    $dbh->do("DELETE FROM tokens WHERE userid = ? AND tokentype = 'sudo'",
      undef, $user->id);
    return;
  }

  # The LOGOUT_*_CURRENT options require the current login cookie.
  # If a new cookie has been issued during this run, that's the current one.
  # If not, it's the one we've received.
  my @login_cookies;
  my $cookie = first { $_->name eq 'Bugzilla_logincookie' }
  @{$cgi->{'Bugzilla_cookie_list'}};
  if ($cookie) {
    push(@login_cookies, $cookie->value);
  }
  elsif ($cookie = $cgi->cookie('Bugzilla_logincookie')) {
    push(@login_cookies, $cookie);
  }

  # If we are a webservice using a token instead of cookie
  # then add that as well to the login cookies to delete
  if (my $login_token = $user->authorizer->login_token) {
    push(@login_cookies, $login_token->{'login_token'});
  }

  # Make sure that @login_cookies is not empty to not break SQL statements.
  push(@login_cookies, '') unless @login_cookies;

  # These queries use both the cookie ID and the user ID as keys. Even
  # though we know the userid must match, we still check it in the SQL
  # as a sanity check, since there is no locking here, and if the user
  # logged out from two machines simultaneously, while someone else
  # logged in and got the same cookie, we could be logging the other
  # user out here. Yes, this is very very very unlikely, but why take
  # chances? - bbaetz
  map { trick_taint($_) } @login_cookies;
  @login_cookies = map { $dbh->quote($_) } @login_cookies;
  if ($type == LOGOUT_KEEP_CURRENT) {
    $dbh->do(
      "DELETE FROM logincookies WHERE "
        . $dbh->sql_in('cookie', \@login_cookies, 1)
        . " AND userid = ?",
      undef, $user->id
    );
  }
  elsif ($type == LOGOUT_CURRENT) {
    $dbh->do(
      "DELETE FROM logincookies WHERE "
        . $dbh->sql_in('cookie', \@login_cookies)
        . " AND userid = ?",
      undef, $user->id
    );
    my $token = $cgi->cookie('sudo');
    delete_token($token);
  }
  else {
    die("Invalid type $type supplied to logout()");
  }

  if ($type != LOGOUT_KEEP_CURRENT) {
    clear_browser_cookies();
  }

}

sub clear_browser_cookies {
  my $cgi = Bugzilla->cgi;
  $cgi->remove_cookie('Bugzilla_login');
  $cgi->remove_cookie('Bugzilla_logincookie');
  $cgi->remove_cookie('sudo');
}

1;
