# 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::Config::Common; use 5.10.1; use strict; use warnings; use Email::Address; use Socket; use Bugzilla::Util; use Bugzilla::Constants; use Bugzilla::Field; use Bugzilla::Group; use Bugzilla::Status; use parent qw(Exporter); @Bugzilla::Config::Common::EXPORT = qw(check_multi check_numeric check_regexp check_group check_sslbase check_priority check_severity check_platform check_opsys check_shadowdb check_urlbase check_user_verify_class check_ip check_mail_delivery_method check_notification check_bug_status check_smtp_auth check_theschwartz_available check_maxattachmentsize check_email check_smtp_ssl check_comment_taggers_group check_smtp_server check_resolution ); # Checking functions for the various values sub check_multi { my ($value, $param) = (@_); if ($param->{'type'} eq "s") { unless (scalar(grep {$_ eq $value} (@{$param->{'choices'}}))) { return "Invalid choice '$value' for single-select list param '$param->{'name'}'"; } return ""; } elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') { if (ref($value) ne "ARRAY") { $value = [split(',', $value)] } foreach my $chkParam (@$value) { unless (scalar(grep {$_ eq $chkParam} (@{$param->{'choices'}}))) { return "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'"; } } return ""; } else { return "Invalid param type '$param->{'type'}' for check_multi(); " . "contact your Bugzilla administrator"; } } sub check_numeric { my ($value) = (@_); if ($value !~ /^[0-9]+$/) { return "must be a numeric value"; } return ""; } sub check_regexp { my ($value) = (@_); eval { qr/$value/ }; return $@; } sub check_email { my ($value) = @_; if ($value !~ $Email::Address::mailbox) { return "must be a valid email address."; } return ""; } sub check_sslbase { my $url = shift; if ($url ne '') { if ($url !~ m#^https://([^/]+).*/$#) { return "must be a legal URL, that starts with https and ends with a slash"; } my $host = $1; # Fall back to port 443 if for some reason getservbyname() fails. my $port = getservbyname('https', 'tcp') || 443; if ($host =~ /^(.+):(\d+)$/) { $host = $1; $port = $2; } local *SOCK; my $proto = getprotobyname('tcp'); socket(SOCK, PF_INET, SOCK_STREAM, $proto); my $iaddr = inet_aton($host) || return "The host $host cannot be resolved"; my $sin = sockaddr_in($port, $iaddr); if (!connect(SOCK, $sin)) { return "Failed to connect to $host:$port ($!); unable to enable SSL"; } close(SOCK); } return ""; } sub check_ip { my $inbound_proxies = shift; my @proxies = split(/[\s,]+/, $inbound_proxies); foreach my $proxy (@proxies) { validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address"; } return ""; } sub check_priority { my ($value) = (@_); my $legal_priorities = get_legal_field_values('priority'); if (!grep($_ eq $value, @$legal_priorities)) { return "Must be a legal priority value: one of " . join(", ", @$legal_priorities); } return ""; } sub check_severity { my ($value) = (@_); my $legal_severities = get_legal_field_values('bug_severity'); if (!grep($_ eq $value, @$legal_severities)) { return "Must be a legal severity value: one of " . join(", ", @$legal_severities); } return ""; } sub check_platform { my ($value) = (@_); my $legal_platforms = get_legal_field_values('rep_platform'); if (!grep($_ eq $value, '', @$legal_platforms)) { return "Must be empty or a legal platform value: one of " . join(", ", @$legal_platforms); } return ""; } sub check_opsys { my ($value) = (@_); my $legal_OS = get_legal_field_values('op_sys'); if (!grep($_ eq $value, '', @$legal_OS)) { return "Must be empty or a legal operating system value: one of " . join(", ", @$legal_OS); } return ""; } sub check_bug_status { my $bug_status = shift; my @closed_bug_statuses = map {$_->name} closed_bug_statuses(); if (!grep($_ eq $bug_status, @closed_bug_statuses)) { return "Must be a valid closed status: one of " . join(', ', @closed_bug_statuses); } return ""; } sub check_resolution { my $resolution = shift; my $resolution_field = Bugzilla::Field->new({ name => 'resolution', cache => 1 }); # The empty resolution is included - it represents "no value" my @resolutions = map {$_->name} @{ $resolution_field->legal_values }; if (!grep($_ eq $resolution, @resolutions)) { return "Must be blank or a valid resolution: one of " . join(', ', @resolutions); } return ""; } sub check_group { my $group_name = shift; return "" unless $group_name; my $group = new Bugzilla::Group({'name' => $group_name}); unless (defined $group) { return "Must be an existing group name"; } return ""; } sub check_shadowdb { my ($value) = (@_); $value = trim($value); if ($value eq "") { return ""; } if (!Bugzilla->params->{'shadowdbhost'}) { return "You need to specify a host when using a shadow database"; } # Can't test existence of this because ConnectToDatabase uses the param, # but we can't set this before testing.... # This can really only be fixed after we can use the DBI more openly return ""; } sub check_urlbase { my ($url) = (@_); if ($url && $url !~ m:^http.*/$:) { return "must be a legal URL, that starts with http and ends with a slash."; } return ""; } sub check_user_verify_class { # doeditparams traverses the list of params, and for each one it checks, # then updates. This means that if one param checker wants to look at # other params, it must be below that other one. So you can't have two # params mutually dependent on each other. # This means that if someone clears the LDAP config params after setting # the login method as LDAP, we won't notice, but all logins will fail. # So don't do that. my $params = Bugzilla->params; my ($list, $entry) = @_; $list || return 'You need to specify at least one authentication mechanism'; for my $class (split /,\s*/, $list) { my $res = check_multi($class, $entry); return $res if $res; if ($class eq 'RADIUS') { if (!Bugzilla->feature('auth_radius')) { return "RADIUS support is not available. Run checksetup.pl" . " for more details"; } return "RADIUS servername (RADIUS_server) is missing" if !$params->{"RADIUS_server"}; return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"}; } elsif ($class eq 'LDAP') { if (!Bugzilla->feature('auth_ldap')) { return "LDAP support is not available. Run checksetup.pl" . " for more details"; } return "LDAP servername (LDAPserver) is missing" if !$params->{"LDAPserver"}; return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"}; } } return ""; } sub check_mail_delivery_method { my $check = check_multi(@_); return $check if $check; my $mailer = shift; if ($mailer eq 'Sendmail' and ON_WINDOWS) { # look for sendmail.exe return "Failed to locate " . SENDMAIL_EXE unless -e SENDMAIL_EXE; } return ""; } sub check_maxattachmentsize { my $check = check_numeric(@_); return $check if $check; my $size = shift; my $dbh = Bugzilla->dbh; if ($dbh->isa('Bugzilla::DB::Mysql')) { my (undef, $max_packet) = $dbh->selectrow_array( q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); my $byte_size = $size * 1024; if ($max_packet < $byte_size) { return "You asked for a maxattachmentsize of $byte_size bytes," . " but the max_allowed_packet setting in MySQL currently" . " only allows packets up to $max_packet bytes"; } } return ""; } sub check_notification { my $option = shift; my @current_version = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); if ($current_version[1] % 2 && $option eq 'stable_branch_release') { return "You are currently running a development snapshot, and so your " . "installation is not based on a branch. If you want to be notified " . "about the next stable release, you should select " . "'latest_stable_release' instead"; } if ($option ne 'disabled' && !Bugzilla->feature('updates')) { return "Some Perl modules are missing to get notifications about " . "new releases. See the output of checksetup.pl for more information"; } return ""; } sub check_smtp_server { my $host = shift; my $port; return '' unless $host; if ($host =~ /:/) { ($host, $port) = split(/:/, $host, 2); unless ($port && detaint_natural($port)) { return "Invalid port. It must be an integer (typically 25, 465 or 587)"; } } trick_taint($host); # Let's first try to connect using SSL. If this fails, we fall back to # an unencrypted connection. foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) { my ($class, $default_port) = @$method; next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl'); eval "require $class"; my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5); if ($smtp) { # The connection works! $smtp->quit; return ''; } } return "Cannot connect to $host" . ($port ? " using port $port" : ""); } sub check_smtp_auth { my $username = shift; if ($username and !Bugzilla->feature('smtp_auth')) { return "SMTP Authentication is not available. Run checksetup.pl for" . " more details"; } return ""; } sub check_smtp_ssl { my $use_ssl = shift; if ($use_ssl && !Bugzilla->feature('smtp_ssl')) { return "SSL support is not available. Run checksetup.pl for more details"; } return ""; } sub check_theschwartz_available { my $use_queue = shift; if ($use_queue && !Bugzilla->feature('jobqueue')) { return "Using the job queue requires that you have certain Perl" . " modules installed. See the output of checksetup.pl" . " for more information"; } return ""; } sub check_comment_taggers_group { my $group_name = shift; if ($group_name && !Bugzilla->feature('jsonrpc')) { return "Comment tagging requires installation of the JSONRPC feature"; } return check_group($group_name); } # OK, here are the parameter definitions themselves. # # Each definition is a hash with keys: # # name - name of the param # desc - description of the param (for editparams.cgi) # type - see below # choices - (optional) see below # default - default value for the param # checker - (optional) checking function for validating parameter entry # It is called with the value of the param as the first arg and a # reference to the param's hash as the second argument # # The type value can be one of the following: # # t -- A short text entry field (suitable for a single line) # p -- A short text entry field (as with type = 't'), but the string is # replaced by asterisks (appropriate for passwords) # l -- A long text field (suitable for many lines) # b -- A boolean value (either 1 or 0) # m -- A list of values, with many selectable (shows up as a select box) # To specify the list of values, make the 'choices' key be an array # reference of the valid choices. The 'default' key should be a string # with a list of selected values (as a comma-separated list), i.e.: # { # name => 'multiselect', # desc => 'A list of options, choose many', # type => 'm', # choices => [ 'a', 'b', 'c', 'd' ], # default => [ 'a', 'd' ], # checker => \&check_multi # } # # Here, 'a' and 'd' are the default options, and the user may pick any # combination of a, b, c, and d as valid options. # # &check_multi should always be used as the param verification function # for list (single and multiple) parameter types. # # o -- A list of values, orderable, and with many selectable (shows up as a # JavaScript-enhanced select box if JavaScript is enabled, and a text # entry field if not) # Set up in the same way as type m. # # s -- A list of values, with one selectable (shows up as a select box) # To specify the list of values, make the 'choices' key be an array # reference of the valid choices. The 'default' key should be one of # those values, i.e.: # { # name => 'singleselect', # desc => 'A list of options, choose one', # type => 's', # choices => [ 'a', 'b', 'c' ], # default => 'b', # checker => \&check_multi # } # # Here, 'b' is the default option, and 'a' and 'c' are other possible # options, but only one at a time! # # &check_multi should always be used as the param verification function # for list (single and multiple) parameter types. sub get_param_list { return; } 1; __END__ =head1 NAME Bugzilla::Config::Common - Parameter checking functions =head1 DESCRIPTION All parameter checking functions are called with two parameters: the value to check, and a hash with the details of the param (type, default etc.) as defined in the relevant F package. =head2 Functions =over =item C Checks that a multi-valued parameter (ie types C, C or C) satisfies its contraints. =item C Checks that the value is a valid number =item C Checks that the value is a valid regexp =item C Checks that the required modules for comment tagging are installed, and that a valid group is provided. =back =head1 B =over =item check_notification =item check_priority =item check_ip =item check_user_verify_class =item check_bug_status =item check_resolution =item check_shadowdb =item check_smtp_server =item check_smtp_auth =item check_urlbase =item check_email =item get_param_list =item check_maxattachmentsize =item check_group =item check_opsys =item check_platform =item check_severity =item check_sslbase =item check_mail_delivery_method =item check_theschwartz_available =item check_smtp_ssl =back