/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */


#include <map>
#include <utility>

#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/fast_wildcard_group.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/http/user_agent_matcher.h"
#include "pagespeed/kernel/util/re2.h"

namespace net_instaweb {

const char UserAgentMatcher::kTestUserAgentWebP[] = "test-user-agent-webp";
// Note that this must not contain the substring "webp".
const char UserAgentMatcher::kTestUserAgentNoWebP[] = "test-user-agent-no";

class RequestHeaders;

// These are the user-agents of browsers/mobile devices which support
// image-inlining. The data is from "Latest WURFL Repository"(mobile devices)
// and "Web Patch"(browsers) on http://wurfl.sourceforge.net
// The user-agent string for Opera could be in the form of "Opera 7" or
// "Opera/7", we use the wildcard pattern "Opera?7" for this case.
namespace {

const char kGooglePlusUserAgent[] =
    "*Google (+https://developers.google.com/+/web/snippet/)*";

const char* kImageInliningWhitelist[] = {
  "*Android*",
  "*Chrome/*",
  "*Firefox/*",
  "*iPad*",
  "*iPhone*",
  "*iPod*",
  "*itouch*",
  "*Opera*",
  "*Safari*",
  "*Wget*",
  // Allow in ads policy checks to match usual UA behavior.
  "AdsBot-Google*",
  // Plus IE, see use in the code.
  // The following user agents are used only for internal testing
  "google command line rewriter",
  "webp",
  "webp-la",
};
const char* kImageInliningBlacklist[] = {
  "*Firefox/1.*",
  "*Firefox/2.*",
  "*MSIE 5.*",
  "*MSIE 6.*",
  "*MSIE 7.*",
  "*Opera?5*",
  "*Opera?6*",
  kGooglePlusUserAgent
};

// Exclude BlackBerry OS 5.0 and older. See
// http://supportforums.blackberry.com/t5/Web-and-WebWorks-Development/How-to-detect-the-BlackBerry-Browser/ta-p/559862
// for details on BlackBerry UAs.
// Exclude all Opera Mini: see bug #1070.
// https://github.com/apache/incubator-pagespeed-mod/issues/1070
const char* kLazyloadImagesBlacklist[] = {
  "BlackBerry*CLDC*",
  "*Opera Mini*",
  kGooglePlusUserAgent
};


// For defer js we only allow Firefox4+, IE8+, safari and Chrome
// We'll be updating this as and when required.
// The blacklist is checked first, then if not in there, the whitelist is
// checked.
// Do allow googlebot, since we run defer js for modern browsers.
// Note: None of the following should match a mobile UA.
const char* kDeferJSWhitelist[] = {
  "*Chrome/*",
  "*Firefox/*",
  "*Safari*",
  // Plus IE, see code below.
  "*Wget*",
  "*Googlebot*",
  "*Mediapartners-Google*"
};
const char* kDeferJSBlacklist[] = {
  "*Firefox/1.*",
  "*Firefox/2.*",
  "*Firefox/3.*",
  "*MSIE 5.*",
  "*MSIE 6.*",
  "*MSIE 7.*",
  "*MSIE 8.*",
};
const char* kDeferJSMobileWhitelist[] = {
  "*AppleWebKit/*",
};

// Webp support for most devices should be triggered on Accept:image/webp.
// However we special-case Android 4.0 browsers which are fairly commonly
// used, support webp, and don't send Accept:image/webp.  Very old versions
// of Chrome may support webp without Accept:image/webp, but it is safe to
// ignore them because they are extremely rare.
//
// For legacy webp rewriting, we whitelist Android, but blacklist
// older versions and Firefox, which includes 'Android' in its UA.
// We do this in 2 stages in order to exclude the following category 1 but
// include category 2.
//  1. Firefox on Android does not support WebP, and it has "Android" and
//     "Firefox" in the user agent.
//  2. Recent Opera support WebP, and some Opera have both "Opera" and
//     "Firefox" in the user agent.
const char* kLegacyWebpWhitelist[] = {
  "*Android *",
};

// Based on https://github.com/apache/incubator-pagespeed-mod/issues/978,
// Desktop IE11 will start masquerading as Chrome soon, and according to
// https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!msg/mod-pagespeed-discuss/HYzzdOzJu_k/ftdV8koVgUEJ
// a browser called Midori might (at some point) masquerade as Chrome as well.
const char* kLegacyWebpBlacklist[] = {
  "*Android 0.*",
  "*Android 1.*",
  "*Android 2.*",
  "*Android 3.*",
  "*Firefox/*",
  "*Edge/*",
  "*Trident/*",
  "*Windows Phone*",
  "*Chrome/*",       // Genuine Chrome always sends Accept: webp.
  "*CriOS/*",        // Paranoia: we should not see Android and CriOS together.
};

// To determine lossless webp support and animated webp support, we must
// examine the UA.
const char* kWebpLosslessAlphaWhitelist[] = {
  "*Chrome/??.*",
  "*Chrome/???.*",
  "*CriOS/??.*",
  // User agent used only for internal testing.
  "webp-la",
  "webp-animated",
};

const char* kWebpLosslessAlphaBlacklist[] = {
  "*Chrome/?.*",
  "*Chrome/1?.*",
  "*Chrome/20.*",
  "*Chrome/21.*",
  "*Chrome/22.*",
  "*CriOS/1?.*",
  "*CriOS/20.*",
  "*CriOS/21.*",
  "*CriOS/22.*",
  "*CriOS/23.*",
  "*CriOS/24.*",
  "*CriOS/25.*",
  "*CriOS/26.*",
  "*CriOS/27.*",
  "*CriOS/28.*",
};

// Animated WebP is supported by browsers based on Chromium v32+, including
// Chrome 32+ and Opera 19+. Because since version 15, Opera has been including
// "Chrome/VERSION" in the user agent string [1], the test for Chrome 32+ will
// also cover Opera 19+.
// [1] https://dev.opera.com/blog/opera-user-agent-strings-opera-15-and-beyond/
const char* kWebpAnimatedWhitelist[] = {
  "*Chrome/??.*",
  "*CriOS/??.*",
  "webp-animated",  // User agent for internal testing.
};

const char* kWebpAnimatedBlacklist[] = {
  "*Chrome/?.*",
  "*Chrome/1?.*",
  "*Chrome/2?.*",
  "*Chrome/30.*",
  "*Chrome/31.*",
  "*CriOS/?.*",
  "*CriOS/1?.*",
  "*CriOS/2?.*",
  "*CriOS/30.*",
  "*CriOS/31.*",
};

const char* kInsertDnsPrefetchWhitelist[] = {
  "*Chrome/*",
  "*Firefox/*",
  "*Safari/*",
  // Plus IE, see code below.
  "*Wget*",
};

const char* kInsertDnsPrefetchBlacklist[] = {
  "*Firefox/1.*",
  "*Firefox/2.*",
  "*Firefox/3.*",
  // Safari indicates version with a separate Version/N.N.N token that appears
  // somewhere before the Safari/ token.  This only started with version 3, but
  // versions before 3 are 10+ years old at this point and won't run on any
  // supported OS.
  "*Version/3.*Safari/*",
  "*Version/4.*Safari/*",
  // 5.0.1+ actually did support it, but that's long obsolete, so don't bother
  // contorting the list to include it.
  "*Version/5.*Safari/*",
  "*MSIE 5.*",
  "*MSIE 6.*",
  "*MSIE 7.*",
  "*MSIE 8.*",
};

// Whitelist used for doing the tablet-user-agent check, which also feeds
// into the device type used for storing properties in the property cache.
const char* kTabletUserAgentWhitelist[] = {
  "*Android*",  // Android tablet has "Android" but not "Mobile". Regexp
                // checks for UserAgents should first check the mobile
                // whitelists and blacklists and only then check the tablet
                // whitelist for correct results.
  "*iPad*",
  "*TouchPad*",
  "*Silk-Accelerated*",
  "*Kindle Fire*"
};

// Whitelist used for doing the mobile-user-agent check, which also feeds
// into the device type used for storing properties in the property cache.
const char* kMobileUserAgentWhitelist[] = {
  "*Mozilla*Android*Mobile*",
  "*iPhone*",
  "*BlackBerry*",
  "*Opera Mobi*",
  "*Opera Mini*",
  "*SymbianOS*",
  "*UP.Browser*",
  "*J-PHONE*",
  "*Profile/MIDP*",
  "*profile/MIDP*",
  "*portalmmm*",
  "*DoCoMo*",
  "*Obigo*",
  "AdsBot-Google-Mobile",
};

// Blacklist used for doing the mobile-user-agent check.
const char* kMobileUserAgentBlacklist[] = {
  "*Mozilla*Android*Silk*Mobile*",
  "*Mozilla*Android*Kindle Fire*Mobile*"
};

// Whitelist used for mobilization.
const char* kMobilizationUserAgentWhitelist[] = {
  "*Android*",
  "*Chrome/*",
  "*Firefox/*",
  "*iPad*",
  "*iPhone*",
  "*iPod*",
  "*Opera*",
  "*Safari*",
  "*Wget*",
  "*CriOS/*",                  // Chrome for iOS.
  "*Android *",                // Native Android browser (see blacklist below).
  "*iPhone*",
  "AdsBot-Google*"
};

// Blacklist used for doing the mobilization UA check.
const char* kMobilizationUserAgentBlacklist[] = {
  "*Android 0.*",
  "*Android 1.*",
  "*Android 2.*",
  "*BlackBerry*",
  "*Mozilla*Android*Silk*Mobile*",
  "*Mozilla*Android*Kindle Fire*Mobile*",
  "*Opera Mobi*",
  "*Opera Mini*",
  "*SymbianOS*",
  "*UP.Browser*",
  "*J-PHONE*",
  "*Profile/MIDP*",
  "*profile/MIDP*",
  "*portalmmm*",
  "*DoCoMo*",
  "*Obigo*",
  // TODO(jmaessen): Remove when there's a fix for scroll misbehavior on CriOS.
  "*CriOS/*",                  // Chrome for iOS.
  "*GSA*Safari*",              // Google Search Application for iOS.
  // TODO(jmaessen): Remove when there's a fix for page geometry on the native
  // Android browser (the old WebKit browser).
  "*U; Android 3.*",
  "*U; Android 4.*"
};

// IE 11 and later user agent strings are deliberately difficult.  That would be
// great if random pages never put the browser into backward compatibility mode,
// and all the outstanding caching bugs were fixed, but neither is true and so
// we need to be able to spot IE 11 and treat it as IE even though we're not
// supposed to need to do so ever again.  See
// http://blogs.msdn.com/b/ieinternals/archive/2013/09/21/internet-explorer-11-user-agent-string-ua-string-sniffing-compatibility-with-gecko-webkit.aspx
const char* kIeUserAgents[] = {
  "*MSIE *",                // Should match any IE before 11.
  "*rv:11.?) like Gecko*",  // Other revisions (eg 12.0) are FireFox
  "*IE 1*",                 // Initial numeral avoids Samsung UA
  "*Trident/7*",            // Opera sometimes pretends to be earlier Trident
};
const int kIEBefore11Index = 0;

// Match either 'CriOS' (iOS Chrome) or 'Chrome'. ':?' marks a non-capturing
// group.
const char* kChromeVersionPattern =
    "(?:Chrome|CriOS)/(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)";

// Device strings must not include wildcards.
struct Dimension {
  const char* device_name;
  int width;
  int height;
};

const Dimension kKnownScreenDimensions[] = {
  {"Galaxy Nexus", 720, 1280},
  {"GT-I9300", 720, 1280},
  {"GT-N7100", 720, 1280},
  {"Nexus 4", 768, 1280},
  {"Nexus 10", 1600, 2560},
  {"Nexus S", 480, 800},
  {"Xoom", 800, 1280},
  {"XT907", 540, 960},
};

}  // namespace

UserAgentMatcher::UserAgentMatcher()
    : chrome_version_pattern_(kChromeVersionPattern) {
  // Initialize FastWildcardGroup for image inlining whitelist & blacklist.
  for (int i = 0, n = arraysize(kImageInliningWhitelist); i < n; ++i) {
    supports_image_inlining_.Allow(kImageInliningWhitelist[i]);
  }
  for (int i = 0, n = arraysize(kIeUserAgents); i < n; ++i) {
    supports_image_inlining_.Allow(kIeUserAgents[i]);
  }
  for (int i = 0, n = arraysize(kImageInliningBlacklist); i < n; ++i) {
    supports_image_inlining_.Disallow(kImageInliningBlacklist[i]);
  }
  for (int i = 0, n = arraysize(kLazyloadImagesBlacklist); i < n; ++i) {
    supports_lazyload_images_.Disallow(kLazyloadImagesBlacklist[i]);
  }
  defer_js_whitelist_.Allow(kIeUserAgents[kIEBefore11Index]);
  for (int i = 0, n = arraysize(kDeferJSWhitelist); i < n; ++i) {
    defer_js_whitelist_.Allow(kDeferJSWhitelist[i]);
  }

  // https://github.com/apache/incubator-pagespeed-mod/issues/982
  defer_js_whitelist_.Disallow("* MSIE 9.*");

  for (int i = 0, n = arraysize(kDeferJSBlacklist); i < n; ++i) {
    defer_js_whitelist_.Disallow(kDeferJSBlacklist[i]);
  }

  for (int i = 0, n = arraysize(kDeferJSMobileWhitelist); i < n; ++i) {
    defer_js_mobile_whitelist_.Allow(kDeferJSMobileWhitelist[i]);
  }

  // Do the same for webp support.
  for (int i = 0, n = arraysize(kLegacyWebpWhitelist); i < n; ++i) {
    legacy_webp_.Allow(kLegacyWebpWhitelist[i]);
  }
  for (int i = 0, n = arraysize(kLegacyWebpBlacklist); i < n; ++i) {
    legacy_webp_.Disallow(kLegacyWebpBlacklist[i]);
  }

  for (int i = 0, n = arraysize(kWebpLosslessAlphaWhitelist); i < n; ++i) {
    supports_webp_lossless_alpha_.Allow(kWebpLosslessAlphaWhitelist[i]);
  }
  for (int i = 0, n = arraysize(kWebpLosslessAlphaBlacklist); i < n; ++i) {
    supports_webp_lossless_alpha_.Disallow(kWebpLosslessAlphaBlacklist[i]);
  }
  for (int i = 0, n = arraysize(kWebpAnimatedWhitelist); i < n; ++i) {
    supports_webp_animated_.Allow(kWebpAnimatedWhitelist[i]);
  }
  for (int i = 0, n = arraysize(kWebpAnimatedBlacklist); i < n; ++i) {
    supports_webp_animated_.Disallow(kWebpAnimatedBlacklist[i]);
  }
  for (int i = 0, n = arraysize(kInsertDnsPrefetchWhitelist); i < n; ++i) {
    supports_dns_prefetch_.Allow(kInsertDnsPrefetchWhitelist[i]);
  }
  for (int i = 0, n = arraysize(kIeUserAgents); i < n; ++i) {
    supports_dns_prefetch_.Allow(kIeUserAgents[i]);
  }
  for (int i = 0, n = arraysize(kInsertDnsPrefetchBlacklist); i < n; ++i) {
    supports_dns_prefetch_.Disallow(kInsertDnsPrefetchBlacklist[i]);
  }

  for (int i = 0, n = arraysize(kMobileUserAgentWhitelist); i < n; ++i) {
    mobile_user_agents_.Allow(kMobileUserAgentWhitelist[i]);
  }
  for (int i = 0, n = arraysize(kMobileUserAgentBlacklist); i < n; ++i) {
    mobile_user_agents_.Disallow(kMobileUserAgentBlacklist[i]);
  }
  for (int i = 0, n = arraysize(kTabletUserAgentWhitelist); i < n; ++i) {
    tablet_user_agents_.Allow(kTabletUserAgentWhitelist[i]);
  }
  for (int i = 0, n = arraysize(kMobilizationUserAgentWhitelist); i < n;
       ++i) {
    mobilization_user_agents_.Allow(kMobilizationUserAgentWhitelist[i]);
  }
  for (int i = 0, n = arraysize(kMobilizationUserAgentBlacklist); i < n;
       ++i) {
    mobilization_user_agents_.Disallow(kMobilizationUserAgentBlacklist[i]);
  }
  for (int i = 0, n = arraysize(kIeUserAgents); i < n; ++i) {
    ie_user_agents_.Allow(kIeUserAgents[i]);
  }
  GoogleString known_devices_pattern_string = "(";
  for (int i = 0, n = arraysize(kKnownScreenDimensions); i < n; ++i) {
    const Dimension& dim = kKnownScreenDimensions[i];
    screen_dimensions_map_[dim.device_name] = make_pair(dim.width, dim.height);
    if (i != 0) {
      StrAppend(&known_devices_pattern_string, "|");
    }
    StrAppend(&known_devices_pattern_string, dim.device_name);
  }
  StrAppend(&known_devices_pattern_string, ")");
  known_devices_pattern_.reset(new RE2(known_devices_pattern_string));
}

UserAgentMatcher::~UserAgentMatcher() {
}

bool UserAgentMatcher::IsIe(const StringPiece& user_agent) const {
  return ie_user_agents_.Match(user_agent, false);
}

bool UserAgentMatcher::IsIe9(const StringPiece& user_agent) const {
  return user_agent.find(" MSIE 9.") != GoogleString::npos;
}

bool UserAgentMatcher::SupportsImageInlining(
    const StringPiece& user_agent) const {
  if (user_agent.empty()) {
    return true;
  }
  return supports_image_inlining_.Match(user_agent, false);
}

bool UserAgentMatcher::SupportsLazyloadImages(StringPiece user_agent) const {
  return supports_lazyload_images_.Match(user_agent, true);
}

bool UserAgentMatcher::SupportsDnsPrefetch(
    const StringPiece& user_agent) const {
  return supports_dns_prefetch_.Match(user_agent, false);
}

bool UserAgentMatcher::SupportsJsDefer(const StringPiece& user_agent,
                                       bool allow_mobile) const {
  // TODO(ksimbili): Use IsMobileRequest?
  if (GetDeviceTypeForUA(user_agent) != kDesktop) {
    // TODO(ksimbili): IsMobileUserAgent returns true for tablets too.
    // Fix it when we need to differentiate them.
    return allow_mobile && defer_js_mobile_whitelist_.Match(user_agent, false);
  }
  return user_agent.empty() || defer_js_whitelist_.Match(user_agent, false);
}

bool UserAgentMatcher::LegacyWebp(const StringPiece& user_agent) const {
  return legacy_webp_.Match(user_agent, false);
}

bool UserAgentMatcher::SupportsWebpLosslessAlpha(
    const StringPiece& user_agent) const {
  return supports_webp_lossless_alpha_.Match(user_agent, false);
}

bool UserAgentMatcher::SupportsWebpAnimated(
    const StringPiece& user_agent) const {
  return supports_webp_animated_.Match(user_agent, false);
}

UserAgentMatcher::DeviceType UserAgentMatcher::GetDeviceTypeForUAAndHeaders(
    const StringPiece& user_agent,
    const RequestHeaders* request_headers) const {
  return GetDeviceTypeForUA(user_agent);
}

bool UserAgentMatcher::IsAndroidUserAgent(const StringPiece& user_agent) const {
  return user_agent.find("Android") != GoogleString::npos;
}

bool UserAgentMatcher::IsiOSUserAgent(const StringPiece& user_agent) const {
  return user_agent.find("iPhone") != GoogleString::npos ||
      user_agent.find("iPad") != GoogleString::npos;
}

bool UserAgentMatcher::GetChromeBuildNumber(const StringPiece& user_agent,
                                            int* major, int* minor, int* build,
                                            int* patch) const {
  return RE2::PartialMatch(StringPieceToRe2(user_agent),
                           chrome_version_pattern_, major, minor, build, patch);
}

bool UserAgentMatcher::SupportsDnsPrefetchUsingRelPrefetch(
    const StringPiece& user_agent) const {
  return IsIe9(user_agent);
}

// TODO(bharathbhushan): Make sure GetDeviceTypeForUA is called only once per
// http request.
UserAgentMatcher::DeviceType UserAgentMatcher::GetDeviceTypeForUA(
    const StringPiece& user_agent) const {
  if (mobile_user_agents_.Match(user_agent, false)) {
    return kMobile;
  }
  if (tablet_user_agents_.Match(user_agent, false)) {
    return kTablet;
  }
  return kDesktop;
}

StringPiece UserAgentMatcher::DeviceTypeString(DeviceType device_type) {
  StringPiece device_type_suffix = "";
  switch (device_type) {
    case kMobile:
      device_type_suffix = "mobile";
      break;
    case kTablet:
      device_type_suffix = "tablet";
      break;
    case kDesktop:
    case kEndOfDeviceType:
    default:
      device_type_suffix = "desktop";
      break;
  }
  return device_type_suffix;
}

StringPiece UserAgentMatcher::DeviceTypeSuffix(DeviceType device_type) {
  StringPiece device_type_suffix = "";
  switch (device_type) {
    case kMobile:
      device_type_suffix = "@Mobile";
      break;
    case kTablet:
      device_type_suffix = "@Tablet";
      break;
    case kDesktop:
    case kEndOfDeviceType:
    default:
      device_type_suffix = "@Desktop";
      break;
  }
  return device_type_suffix;
}

bool UserAgentMatcher::UserAgentExceedsChromeiOSBuildAndPatch(
    const StringPiece& user_agent, int required_build,
    int required_patch) const {
  // Verify if this is an iOS user agent.
  if (!IsiOSUserAgent(user_agent)) {
    return false;
  }
  return UserAgentExceedsChromeBuildAndPatch(
      user_agent, required_build, required_patch);
}

bool UserAgentMatcher::UserAgentExceedsChromeAndroidBuildAndPatch(
    const StringPiece& user_agent, int required_build,
    int required_patch) const {
  // Verify if this is an Android user agent.
  if (!IsAndroidUserAgent(user_agent)) {
    return false;
  }
  return UserAgentExceedsChromeBuildAndPatch(
      user_agent, required_build, required_patch);
}

bool UserAgentMatcher::UserAgentExceedsChromeBuildAndPatch(
    const StringPiece& user_agent, int required_build,
    int required_patch) const {
  // By default user agent sniffing is disabled.
  if (required_build == -1 && required_patch == -1) {
    return false;
  }
  int major = -1;
  int minor = -1;
  int parsed_build = -1;
  int parsed_patch = -1;
  if (!GetChromeBuildNumber(user_agent, &major, &minor,
                            &parsed_build, &parsed_patch)) {
    return false;
  }

  if (parsed_build < required_build) {
    return false;
  } else if (parsed_build == required_build && parsed_patch < required_patch) {
    return false;
  }

  return true;
}

bool UserAgentMatcher::SupportsMobilization(
    StringPiece user_agent) const {
  return mobilization_user_agents_.Match(user_agent, false);
}

}  // namespace net_instaweb
