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

use 5.10.1;
use strict;
use warnings;

use base qw(Exporter);
our @EXPORT = qw(detect_platform detect_op_sys);

use Bugzilla::Field;
use List::MoreUtils qw(natatime);

use constant DEFAULT_VALUE => 'Other';

use constant PLATFORMS_MAP => (

  # PowerPC
  qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"],

  # AMD64, Intel x86_64
  qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32",  "x86",    "PC"],
  qr/\(.*amd64.*\)/                      => ["AMD64", "x86_64", "PC"],
  qr/\(.*x(?:86_)?64.*\)/                => ["AMD64", "x86_64", "PC"],
  qr/\(.*Intel Mac OS X.*\)/             => ["x86_64"],

  # Intel IA64
  qr/\(.*IA64.*\)/ => ["IA64", "PC"],

  # Intel x86
  qr/\(.*Intel.*\)/     => ["IA32", "x86", "PC"],
  qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"],

  # Versions of Windows that only run on Intel x86
  qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"],
  qr/\(.*Win(?:dows |)16.*\)/    => ["IA32", "x86", "PC"],

  # Sparc
  qr/\(.*sparc.*\)/ => ["Sparc", "Sun"],
  qr/\(.*sun4.*\)/  => ["Sparc", "Sun"],

  # Alpha
  qr/\(.*AXP.*\)/i      => ["Alpha", "DEC"],
  qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"],
  qr/\(.*[ _]Alpha\)/i  => ["Alpha", "DEC"],

  # MIPS
  qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
  qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"],

  # 68k
  qr/\(.*68K.*\)/      => ["68k", "Macintosh"],
  qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"],

  # HP
  qr/\(.*9000.*\)/ => ["PA-RISC", "HP"],

  # ARM
  qr/\(.*(?:iPad|iPhone).*\)/ => ["ARM"],
  qr/\(.*ARM.*\)/             => ["ARM", "PocketPC"],

  # PocketPC intentionally before PowerPC
  qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"],

  # PowerPC
  qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"],
  qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"],

  # Stereotypical and broken
  qr/\(.*Windows CE.*\)/        => ["ARM",     "PocketPC"],
  qr/\(.*Macintosh.*\)/         => ["68k",     "Macintosh"],
  qr/\(.*Mac OS [89].*\)/       => ["68k",     "Macintosh"],
  qr/\(.*WOW64.*\)/             => ["x86_64"],
  qr/\(.*Win64.*\)/             => ["IA64"],
  qr/\(Win.*\)/                 => ["IA32",    "x86", "PC"],
  qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32",    "x86", "PC"],
  qr/\(.*OSF.*\)/               => ["Alpha",   "DEC"],
  qr/\(.*HP-?UX.*\)/i           => ["PA-RISC", "HP"],
  qr/\(.*IRIX.*\)/i             => ["MIPS",    "SGI"],
  qr/\(.*(SunOS|Solaris).*\)/   => ["Sparc",   "Sun"],

  # Braindead old browsers who didn't follow convention:
  qr/Amiga/     => ["68k",  "Macintosh"],
  qr/WinMosaic/ => ["IA32", "x86", "PC"],
);

use constant OS_MAP => (

  # Sun
  qr/\(.*Solaris.*\)/      => ["Solaris"],
  qr/\(.*SunOS 5.11.*\)/   => [("OpenSolaris", "Opensolaris", "Solaris 11")],
  qr/\(.*SunOS 5.10.*\)/   => ["Solaris 10"],
  qr/\(.*SunOS 5.9.*\)/    => ["Solaris 9"],
  qr/\(.*SunOS 5.8.*\)/    => ["Solaris 8"],
  qr/\(.*SunOS 5.7.*\)/    => ["Solaris 7"],
  qr/\(.*SunOS 5.6.*\)/    => ["Solaris 6"],
  qr/\(.*SunOS 5.5.*\)/    => ["Solaris 5"],
  qr/\(.*SunOS 5.*\)/      => ["Solaris"],
  qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"],
  qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"],
  qr/\(.*SunOS.*\)/        => ["SunOS"],

  # BSD
  qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"],
  qr/\(.*FreeBSD.*\)/         => ["FreeBSD"],
  qr/\(.*OpenBSD.*\)/         => ["OpenBSD"],
  qr/\(.*NetBSD.*\)/          => ["NetBSD"],

  # Misc POSIX
  qr/\(.*IRIX.*\)/    => ["IRIX"],
  qr/\(.*OSF.*\)/     => ["OSF/1"],
  qr/\(.*Linux.*\)/   => ["Linux"],
  qr/\(.*BeOS.*\)/    => ["BeOS"],
  qr/\(.*AIX.*\)/     => ["AIX"],
  qr/\(.*OS\/2.*\)/   => ["OS/2"],
  qr/\(.*QNX.*\)/     => ["Neutrino"],
  qr/\(.*VMS.*\)/     => ["OpenVMS"],
  qr/\(.*HP-?UX.*\)/  => ["HP-UX"],
  qr/\(.*Android.*\)/ => ["Android"],

  # Windows
  qr/\(.*Windows XP.*\)/         => ["Windows XP"],
  qr/\(.*Windows NT 10\.0.*\)/   => ["Windows 10"],
  qr/\(.*Windows NT 6\.4.*\)/    => ["Windows 10"],
  qr/\(.*Windows NT 6\.3.*\)/    => ["Windows 8.1"],
  qr/\(.*Windows NT 6\.2.*\)/    => ["Windows 8"],
  qr/\(.*Windows NT 6\.1.*\)/    => ["Windows 7"],
  qr/\(.*Windows NT 6\.0.*\)/    => ["Windows Vista"],
  qr/\(.*Windows NT 5\.2.*\)/    => ["Windows Server 2003"],
  qr/\(.*Windows NT 5\.1.*\)/    => ["Windows XP"],
  qr/\(.*Windows 2000.*\)/       => ["Windows 2000"],
  qr/\(.*Windows NT 5.*\)/       => ["Windows 2000"],
  qr/\(.*Win.*9[8x].*4\.9.*\)/   => ["Windows ME"],
  qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"],
  qr/\(.*Win(?:dows |)98.*\)/    => ["Windows 98"],
  qr/\(.*Win(?:dows |)95.*\)/    => ["Windows 95"],
  qr/\(.*Win(?:dows |)16.*\)/    => ["Windows 3.1"],
  qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"],
  qr/\(.*Windows.*NT.*\)/        => ["Windows NT"],

  # OS X
  qr/\(.*(?:iPad|iPhone).*OS 7.*\)/        => ["iOS 7"],
  qr/\(.*(?:iPad|iPhone).*OS 6.*\)/        => ["iOS 6"],
  qr/\(.*(?:iPad|iPhone).*OS 5.*\)/        => ["iOS 5"],
  qr/\(.*(?:iPad|iPhone).*OS 4.*\)/        => ["iOS 4"],
  qr/\(.*(?:iPad|iPhone).*OS 3.*\)/        => ["iOS 3"],
  qr/\(.*(?:iPad|iPhone).*\)/              => ["iOS"],
  qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"],
  qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"],
  qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"],
  qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"],
  qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"],
  qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"],

  # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback
  # support because some browsers refused to include the OS Version.
  qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"],

  # OS X 10.3 is the most likely default version of PowerPC Macs
  # OS X 10.0 is more for configurations which didn't setup 10.x versions
  qr/\(.*Mac OS X.*\)/     => [("Mac OS X 10.3",  "Mac OS X 10.0", "macOS")],
  qr/\(.*Mac OS 9.*\)/     => [("Mac System 9.x", "Mac System 9.0")],
  qr/\(.*Mac OS 8\.6.*\)/  => [("Mac System 8.6", "Mac System 8.5")],
  qr/\(.*Mac OS 8\.5.*\)/  => ["Mac System 8.5"],
  qr/\(.*Mac OS 8\.1.*\)/  => [("Mac System 8.1", "Mac System 8.0")],
  qr/\(.*Mac OS 8\.0.*\)/  => ["Mac System 8.0"],
  qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"],
  qr/\(.*Mac OS 8.*\)/     => ["Mac System 8.6"],
  qr/\(.*Darwin.*\)/       => [("Mac OS X 10.0",  "macOS")],

  # Silly
  qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"],
  qr/\(.*Mac.*PPC.*\)/     => ["Mac System 9.x"],
  qr/\(.*Mac.*68k.*\)/     => ["Mac System 8.0"],

  # Evil
  qr/Amiga/i          => ["Other"],
  qr/WinMosaic/       => ["Windows 95"],
  qr/\(.*32bit.*\)/   => ["Windows 95"],
  qr/\(.*16bit.*\)/   => ["Windows 3.1"],
  qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"],
  qr/\(.*PPC.*\)/     => ["Mac System 9.x"],
  qr/\(.*68K.*\)/     => ["Mac System 8.0"],
);

sub detect_platform {
  my $userAgent = shift || Bugzilla->cgi->user_agent || '';
  my @detected;
  my $iterator = natatime(2, PLATFORMS_MAP);
  while (my ($re, $ra) = $iterator->()) {
    if ($userAgent =~ $re) {
      push @detected, @$ra;
    }
  }
  return _pick_valid_field_value('rep_platform', @detected);
}

sub detect_op_sys {
  my $userAgent = shift || Bugzilla->cgi->user_agent || '';
  my @detected;
  my $iterator = natatime(2, OS_MAP);
  while (my ($re, $ra) = $iterator->()) {
    if ($userAgent =~ $re) {
      push @detected, @$ra;
    }
  }
  push(@detected, "Windows") if grep(/^Windows /, @detected);
  push(@detected, "Mac OS")  if grep(/^Mac /,     @detected);
  return _pick_valid_field_value('op_sys', @detected);
}

# Takes the name of a field and a list of possible values for that field.
# Returns the first value in the list that is actually a valid value for that
# field.
# Returns 'Other' if none of the values match.
sub _pick_valid_field_value {
  my ($field, @values) = @_;
  foreach my $value (@values) {
    return $value if check_field($field, $value, undef, 1);
  }
  return DEFAULT_VALUE;
}

1;

__END__

=head1 NAME

Bugzilla::UserAgent - UserAgent utilities for Bugzilla

=head1 SYNOPSIS

  use Bugzilla::UserAgent;
  printf "platform: %s op-sys: %s\n", detect_platform(), detect_op_sys();

=head1 DESCRIPTION

The functions exported by this module all return information derived from the
remote client's user agent.

=head1 FUNCTIONS

=over 4

=item C<detect_platform>

This function attempts to detect the remote client's platform from the
presented user-agent. If a suitable value on the I<platform> field is found,
that field value will be returned.  If no suitable value is detected,
C<detect_platform> returns I<Other>.

=item C<detect_op_sys>

This function attempts to detect the remote client's operating system from the
presented user-agent. If a suitable value on the I<op_sys> field is found, that
field value will be returned.  If no suitable value is detected,
C<detect_op_sys> returns I<Other>.

=back

