# 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::User::Setting;

use 5.10.1;
use strict;
use warnings;

use parent qw(Exporter);


# Module stuff
@Bugzilla::User::Setting::EXPORT = qw(
  get_all_settings
  get_defaults
  add_setting
  clear_settings_cache
);

use Bugzilla::Error;
use Bugzilla::Util qw(trick_taint get_text);

###############################
###  Module Initialization  ###
###############################

sub new {
  my $invocant     = shift;
  my $setting_name = shift;
  my $user_id      = shift;

  my $class = ref($invocant) || $invocant;
  my $subclass = '';

  # Create a ref to an empty hash and bless it
  my $self = {};

  my $dbh = Bugzilla->dbh;

  # Confirm that the $setting_name is properly formed;
  # if not, throw a code error.
  #
  # NOTE: due to the way that setting names are used in templates,
  # they must conform to to the limitations set for HTML NAMEs and IDs.
  #
  if (!($setting_name =~ /^[a-zA-Z][-.:\w]*$/)) {
    ThrowCodeError("setting_name_invalid", {name => $setting_name});
  }

  # If there were only two parameters passed in, then we need
  # to retrieve the information for this setting ourselves.
  if (scalar @_ == 0) {

    my ($default, $is_enabled, $value);
    ($default, $is_enabled, $value, $subclass) = $dbh->selectrow_array(
      q{SELECT default_value, is_enabled, setting_value, subclass
                 FROM setting
            LEFT JOIN profile_setting
                   ON setting.name = profile_setting.setting_name
                WHERE name = ?
                  AND profile_setting.user_id = ?}, undef, $setting_name, $user_id
    );

    # if not defined, then grab the default value
    if (!defined $value) {
      ($default, $is_enabled, $subclass) = $dbh->selectrow_array(
        q{SELECT default_value, is_enabled, subclass
                   FROM setting
                   WHERE name = ?}, undef, $setting_name
      );
    }

    $self->{'is_enabled'}    = $is_enabled;
    $self->{'default_value'} = $default;

    # IF the setting is enabled, AND the user has chosen a setting
    # THEN return that value
    # ELSE return the site default, and note that it is the default.
    if (($is_enabled) && (defined $value)) {
      $self->{'value'} = $value;
    }
    else {
      $self->{'value'}     = $default;
      $self->{'isdefault'} = 1;
    }
  }
  else {
    # If the values were passed in, simply assign them and return.
    $self->{'is_enabled'}    = shift;
    $self->{'default_value'} = shift;
    $self->{'value'}         = shift;
    $self->{'is_default'}    = shift;
    $subclass                = shift;
  }
  if ($subclass) {
    eval('require ' . $class . '::' . $subclass);
    $@ && ThrowCodeError('setting_subclass_invalid', {'subclass' => $subclass});
    $class = $class . '::' . $subclass;
  }
  bless($self, $class);

  $self->{'_setting_name'} = $setting_name;
  $self->{'_user_id'}      = $user_id;

  return $self;
}

###############################
###  Subroutine Definitions ###
###############################

sub add_setting {
  my ($name, $values, $default_value, $subclass, $force_check, $silently) = @_;
  my $dbh = Bugzilla->dbh;

  my $exists = _setting_exists($name);
  return if ($exists && !$force_check);

  ($name && length($default_value // ''))
    || ThrowCodeError("setting_info_invalid");

  if ($exists) {

    # If this setting exists, we delete it and regenerate it.
    $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name);
    $dbh->do('DELETE FROM setting WHERE name = ?',       undef, $name);

    # Remove obsolete user preferences for this setting.
    if (defined $values && scalar(@$values)) {
      my $list = join(', ', map { $dbh->quote($_) } @$values);
      $dbh->do(
        "DELETE FROM profile_setting
                      WHERE setting_name = ? AND setting_value NOT IN ($list)", undef,
        $name
      );
    }
  }
  elsif (!$silently) {
    print get_text('install_setting_new', {name => $name}) . "\n";
  }
  $dbh->do(
    q{INSERT INTO setting (name, default_value, is_enabled, subclass)
                    VALUES (?, ?, 1, ?)}, undef, ($name, $default_value, $subclass)
  );

  my $sth = $dbh->prepare(
    q{INSERT INTO setting_value (name, value, sortindex)
                                    VALUES (?, ?, ?)}
  );

  my $sortindex = 5;
  foreach my $key (@$values) {
    $sth->execute($name, $key, $sortindex);
    $sortindex += 5;
  }
}

sub get_all_settings {
  my ($user_id) = @_;
  my $settings  = {};
  my $dbh       = Bugzilla->dbh;

  my $cache_key = "user_settings.$user_id";
  my $rows = Bugzilla->memcached->get_config({key => $cache_key});
  if (!$rows) {
    $rows = $dbh->selectall_arrayref(
      q{SELECT name, default_value, is_enabled, setting_value, subclass
                FROM setting
           LEFT JOIN profile_setting
                     ON setting.name = profile_setting.setting_name
                     AND profile_setting.user_id = ?}, undef, ($user_id)
    );
    Bugzilla->memcached->set_config({key => $cache_key, data => $rows});
  }

  foreach my $row (@$rows) {
    my ($name, $default_value, $is_enabled, $value, $subclass) = @$row;

    my $is_default;

    if (($is_enabled) && (defined $value)) {
      $is_default = 0;
    }
    else {
      $value      = $default_value;
      $is_default = 1;
    }

    $settings->{$name}
      = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value,
      $value, $is_default, $subclass);
  }

  return $settings;
}

sub clear_settings_cache {
  my ($user_id) = @_;
  Bugzilla->memcached->clear_config({key => "user_settings.$user_id"});
}

sub get_defaults {
  my ($user_id)        = @_;
  my $dbh              = Bugzilla->dbh;
  my $default_settings = {};

  $user_id ||= 0;

  my $rows = $dbh->selectall_arrayref(
    q{SELECT name, default_value, is_enabled, subclass
                                            FROM setting}
  );

  foreach my $row (@$rows) {
    my ($name, $default_value, $is_enabled, $subclass) = @$row;

    $default_settings->{$name}
      = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value,
      $default_value, 1, $subclass);
  }

  return $default_settings;
}

sub set_default {
  my ($setting_name, $default_value, $is_enabled) = @_;
  my $dbh = Bugzilla->dbh;

  my $sth = $dbh->prepare(
    q{UPDATE setting
                                 SET default_value = ?, is_enabled = ?
                               WHERE name = ?}
  );
  $sth->execute($default_value, $is_enabled, $setting_name);
}

sub _setting_exists {
  my ($setting_name) = @_;
  my $dbh = Bugzilla->dbh;
  return $dbh->selectrow_arrayref("SELECT 1 FROM setting WHERE name = ?",
    undef, $setting_name)
    || 0;
}


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

  return $self->{'legal_values'} if defined $self->{'legal_values'};

  my $dbh = Bugzilla->dbh;
  $self->{'legal_values'} = $dbh->selectcol_arrayref(
    q{SELECT value
                  FROM setting_value
                 WHERE name = ?
              ORDER BY sortindex}, undef, $self->{'_setting_name'}
  );

  return $self->{'legal_values'};
}

sub validate_value {
  my $self = shift;

  if (grep(/^$_[0]$/, @{$self->legal_values()})) {
    trick_taint($_[0]);
  }
  else {
    ThrowCodeError('setting_value_invalid',
      {'name' => $self->{'_setting_name'}, 'value' => $_[0]});
  }
}

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

  my $dbh = Bugzilla->dbh;
  my $sth = $dbh->do(
    q{ DELETE
                            FROM profile_setting
                           WHERE setting_name = ?
                             AND user_id = ?}, undef, $self->{'_setting_name'},
    $self->{'_user_id'}
  );
  $self->{'value'}      = $self->{'default_value'};
  $self->{'is_default'} = 1;
}

sub set {
  my ($self, $value) = @_;
  my $dbh = Bugzilla->dbh;
  my $query;

  if ($self->{'is_default'}) {
    $query = q{INSERT INTO profile_setting
                   (setting_value, setting_name, user_id)
                   VALUES (?,?,?)};
  }
  else {
    $query = q{UPDATE profile_setting
                      SET setting_value = ?
                    WHERE setting_name = ?
                      AND user_id = ?};
  }
  $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'});

  $self->{'value'}      = $value;
  $self->{'is_default'} = 0;
}


1;

__END__

=head1 NAME

Bugzilla::User::Setting - Object for a user preference setting

=head1 SYNOPSIS

Setting.pm creates a setting object, which is a hash containing the user
preference information for a single preference for a single user. These 
are usually accessed through the "settings" object of a user, and not 
directly.

=head1 DESCRIPTION

use Bugzilla::User::Setting;
my $settings;

$settings->{$setting_name} = new Bugzilla::User::Setting(
   $setting_name, $user_id);

OR

$settings->{$setting_name} = new Bugzilla::User::Setting(
   $setting_name, $user_id, $is_enabled,
   $default_value, $value, $is_default);

=head1 CLASS FUNCTIONS

=over 4

=item C<add_setting($name, \@values, $default_value, $subclass, $force_check)>

Description: Checks for the existence of a setting, and adds it 
             to the database if it does not yet exist.

Params:      C<$name> - string - the name of the new setting
             C<$values> - arrayref - contains the new choices
               for the new Setting.
             C<$default_value> - string - the site default
             C<$subclass> - string - name of the module returning
               the list of valid values. This means legal values are
               not stored in the DB.
             C<$force_check> - boolean - when true, the existing setting
               and all its values are deleted and replaced by new data.

Returns:     a pointer to a hash of settings


=item C<get_all_settings($user_id)>

Description: Provides the user's choices for each setting in the 
             system; if the user has made no choice, uses the site
             default instead.
Params:      C<$user_id> - integer - the user id.
Returns:     a pointer to a hash of settings

=item C<get_defaults($user_id)>

Description: When a user is not logged in, they must use the site
             defaults for every settings; this subroutine provides them.
Params:      C<$user_id> (optional) - integer - the user id.  Note that
             this optional parameter is mainly for internal use only.
Returns:     A pointer to a hash of settings.  If $user_id was passed, set
             the user_id value for each setting.

=item C<set_default($setting_name, $default_value, $is_enabled)>

Description: Sets the global default for a given setting. Also sets
             whether users are allowed to choose their own value for
             this setting, or if they must use the global default.
Params:      C<$setting_name> - string - the name of the setting
             C<$default_value> - string - the new default value for this setting
             C<$is_enabled> - boolean - if false, all users must use the global default
Returns:     nothing

=item C<clear_settings_cache($user_id)>

Description: Clears cached settings data for the specified user.  Must be
             called after updating any user's setting.
Params:      C<$user_id> - integer - the user id.
Returns:     nothing

=begin private

=item C<_setting_exists>

Description: Determines if a given setting exists in the database.
Params:      C<$setting_name> - string - the setting name
Returns:     boolean - true if the setting already exists in the DB.

=end private

=back

=head1 METHODS

=over 4

=item C<legal_values($setting_name)>

Description: Returns all legal values for this setting
Params:      none
Returns:     A reference to an array containing all legal values

=item C<validate_value>

Description: Determines whether a value is valid for the setting
             by checking against the list of legal values.
             Untaints the parameter if the value is indeed valid,
             and throws a setting_value_invalid code error if not.
Params:      An lvalue containing a candidate for a setting value
Returns:     nothing

=item C<reset_to_default>

Description: If a user chooses to use the global default for a given 
             setting, their saved entry is removed from the database via 
             this subroutine.
Params:      none
Returns:     nothing

=item C<set($value)>

Description: If a user chooses to use their own value rather than the 
             global value for a given setting, OR changes their value for
             a given setting, this subroutine is called to insert or 
             update the database as appropriate.
Params:      C<$value> - string - the new value for this setting for this user.
Returns:     nothing

=back