# 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::Verify;

use 5.10.1;
use strict;
use warnings;

use fields qw();

use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;

use constant user_can_create_account => 1;
use constant extern_id_used          => 0;

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

sub can_change_password {
  return $_[0]->can('change_password');
}

sub create_or_update_user {
  my ($self, $params) = @_;
  my $dbh = Bugzilla->dbh;

  my $extern_id = $params->{extern_id};
  my $username  = $params->{bz_username} || $params->{username};
  my $password  = $params->{password} || '*';
  my $real_name = $params->{realname} || '';
  my $user_id   = $params->{user_id};

  # A passed-in user_id always overrides anything else, for determining
  # what account we should return.
  if (!$user_id) {
    my $username_user_id = login_to_id($username || '');
    my $extern_user_id;
    if ($extern_id) {
      trick_taint($extern_id);
      $extern_user_id = $dbh->selectrow_array(
        'SELECT userid
                 FROM profiles WHERE extern_id = ?', undef, $extern_id
      );
    }

    # If we have both a valid extern_id and a valid username, and they are
    # not the same id, then we have a conflict.
    if ( $username_user_id
      && $extern_user_id
      && $username_user_id ne $extern_user_id)
    {
      my $extern_name = Bugzilla::User->new($extern_user_id)->login;
      return {
        failure => AUTH_ERROR,
        error   => "extern_id_conflict",
        details =>
          {extern_id => $extern_id, extern_user => $extern_name, username => $username}
      };
    }

    # If we have a valid username, but no valid id,
    # then we have to create the user. This happens when we're
    # passed only a username, and that username doesn't exist already.
    if ($username && !$username_user_id && !$extern_user_id) {
      validate_email_syntax($username) || return {
        failure => AUTH_ERROR,
        error   => 'auth_invalid_email',
        details => {addr => $username}
      };

      # Usually we'd call validate_password, but external authentication
      # systems might follow different standards than ours. So in this
      # place here, we call trick_taint without checks.
      trick_taint($password);

      # XXX Theoretically this could fail with an error, but the fix for
      # that is too involved to be done right now.
      my $user
        = Bugzilla::User->create({
        login_name => $username, cryptpassword => $password, realname => $real_name
        });
      $username_user_id = $user->id;
    }

    # If we have a valid username id and an extern_id, but no valid
    # extern_user_id, then we have to set the user's extern_id.
    if ($extern_id && $username_user_id && !$extern_user_id) {
      $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
        undef, $extern_id, $username_user_id);
      Bugzilla->memcached->clear({table => 'profiles', id => $username_user_id});
    }

    # Finally, at this point, one of these will give us a valid user id.
    $user_id = $extern_user_id || $username_user_id;
  }

  # If we still don't have a valid user_id, then we weren't passed
  # enough information in $params, and we should die right here.
  ThrowCodeError(
    'bad_arg',
    {
      argument => 'params',
      function => 'Bugzilla::Auth::Verify::create_or_update_user'
    }
  ) unless $user_id;

  my $user = new Bugzilla::User($user_id);

  # Now that we have a valid User, we need to see if any data has to be updated.
  my $changed = 0;

  if ($username && lc($user->login) ne lc($username)) {
    validate_email_syntax($username) || return {
      failure => AUTH_ERROR,
      error   => 'auth_invalid_email',
      details => {addr => $username}
    };
    $user->set_login($username);
    $changed = 1;
  }
  if ($real_name && $user->name ne $real_name) {

    # $real_name is more than likely tainted, but we only use it
    # in a placeholder and we never use it after this.
    trick_taint($real_name);
    $user->set_name($real_name);
    $changed = 1;
  }
  $user->update() if $changed;

  return {user => $user};
}

1;

__END__

=head1 NAME

Bugzilla::Auth::Verify - An object that verifies usernames and passwords.

=head1 DESCRIPTION

Bugzilla::Auth::Verify provides the "Verifier" part of the Bugzilla 
login process. (For details, see the "STRUCTURE" section of 
L<Bugzilla::Auth>.)

It is mostly an abstract class, requiring subclasses to implement
most methods.

Note that callers outside of the C<Bugzilla::Auth> package should never
create this object directly. Just create a C<Bugzilla::Auth> object
and call C<login> on it.

=head1 VERIFICATION METHODS

These are the methods that have to do with the actual verification.

Subclasses MUST implement these methods.

=over 4

=item C<check_credentials($login_data)>

Description: Checks whether or not a username is valid.
Params:      $login_data - A C<$login_data> hashref, as described in
                           L<Bugzilla::Auth>.
                           This C<$login_data> hashref MUST contain
                           C<username>, and SHOULD also contain
                           C<password>.
Returns:     A C<$login_data> hashref with C<bz_username> set. This
             method may also set C<realname>. It must avoid changing
             anything that is already set.

=back

=head1 MODIFICATION METHODS

These are methods that change data in the actual authentication backend.

These methods are optional, they do not have to be implemented by
subclasses.

=over 4

=item C<create_or_update_user($login_data)>

Description: Automatically creates a user account in the database
             if it doesn't already exist, or updates the account
             data if C<$login_data> contains newer information.

Params:      $login_data - A C<$login_data> hashref, as described in
                           L<Bugzilla::Auth>.
                           This C<$login_data> hashref MUST contain
                           either C<user_id>, C<bz_username>, or
                           C<username>. If both C<username> and C<bz_username>
                           are specified, C<bz_username> is used as the
                           login name of the user to create in the database.
                           It MAY also contain C<extern_id>, in which
                           case it still MUST contain C<bz_username> or
                           C<username>.
                           It MAY contain C<password> and C<realname>.

Returns:     A hashref with one element, C<user>, which is a
             L<Bugzilla::User> object. May also return a login error
             as described in L<Bugzilla::Auth>.

Note:        This method is not abstract, it is actually implemented
             and creates accounts in the Bugzilla database. Subclasses
             should probably all call the C<Bugzilla::Auth::Verify>
             version of this function at the end of their own
             C<create_or_update_user>.

=item C<change_password($user, $password)>

Description: Modifies the user's password in the authentication backend.
Params:      $user - A L<Bugzilla::User> object representing the user
                     whose password we want to change.
             $password - The user's new password.
Returns:     Nothing.

=back

=head1 INFO METHODS

These are methods that describe the capabilities of this object.
These are all no-parameter methods that return either C<true> or 
C<false>.

=over 4

=item C<user_can_create_account>

Whether or not users can manually create accounts in this type of
account source. Defaults to C<true>.

=item C<extern_id_used>

Whether or not this verifier method uses the extern_id field. If
used, users with editusers permission will be be allowed to
edit the extern_id for all users.

The default value is C<false>.

=back

=head1 B<Methods in need of POD>

=over

=item can_change_password

=back