/**
 * @fileoverview Utils for CSP evaluator.
 * @author lwe@google.com (Lukas Weichselbaum)
 *
 * @license
 * Copyright 2016 Google Inc. All rights reserved.
 * Licensed 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.
 */


import * as csp from './csp';


/**
 * Removes scheme from url.
 * @param url Url.
 * @return url without scheme.
 */
export function getSchemeFreeUrl(url: string): string {
  url = url.replace(/^\w[+\w.-]*:\/\//i, '');
  // Remove URL scheme.
  url = url.replace(/^\/\//, '');
  // Remove protocol agnostic "//"
  return url;
}

/**
 * Get the hostname from the given url string in a way that supports schemeless
 * URLs and wildcards (aka `*`) in hostnames
 */
export function getHostname(url: string): string {
  const hostname =
      new URL(
          'https://' +
      getSchemeFreeUrl(url)
        .replace(':*', '')  // Remove wildcard port
        .replace('*', 'wildcard_placeholder'))
          .hostname.replace('wildcard_placeholder', '*');

  // Some browsers strip the brackets from IPv6 addresses when you access the
  // hostname. If the scheme free url starts with something that vaguely looks
  // like an IPv6 address and our parsed hostname doesn't have the brackets,
  // then we add them back to work around this
  const ipv6Regex = /^\[[\d:]+\]/;
  if (getSchemeFreeUrl(url).match(ipv6Regex) && !hostname.match(ipv6Regex)) {
    return '[' + hostname + ']';
  }
  return hostname;
}

function setScheme(u: string): string {
  if (u.startsWith('//')) {
    return u.replace('//', 'https://');
  }
  return u;
}

/**
 * Searches for allowlisted CSP origin (URL with wildcards) in list of urls.
 * @param cspUrlString The allowlisted CSP origin. Can contain domain and
 *   path wildcards.
 * @param listOfUrlStrings List of urls to search in.
 * @return First match found in url list, null otherwise.
 */
export function matchWildcardUrls(
    cspUrlString: string, listOfUrlStrings: string[]): URL|null {
  // non-Chromium browsers don't support wildcards in domain names. We work
  // around this by replacing the wildcard with `wildcard_placeholder` before
  // parsing the domain and using that as a magic string. This magic string is
  // encapsulated in this function such that callers of this function do not
  // have to worry about this detail.
  const cspUrl =
      new URL(setScheme(cspUrlString
        .replace(':*', '')  // Remove wildcard port
        .replace('*', 'wildcard_placeholder')));
  const listOfUrls = listOfUrlStrings.map(u => new URL(setScheme(u)));
  const host = cspUrl.hostname.toLowerCase();
  const hostHasWildcard = host.startsWith('wildcard_placeholder.');
  const wildcardFreeHost = host.replace(/^\wildcard_placeholder/i, '');
  const path = cspUrl.pathname;
  const hasPath = path !== '/';

  for (const url of listOfUrls) {
    const domain = url.hostname;
    if (!domain.endsWith(wildcardFreeHost)) {
      // Domains don't match.
      continue;
    }

    // If the host has no subdomain wildcard and doesn't match, continue.
    if (!hostHasWildcard && host !== domain) {
      continue;
    }

    // If the allowlisted url has a path, check if one of the url paths
    // match.
    if (hasPath) {
      // https://www.w3.org/TR/CSP2/#source-list-path-patching
      if (path.endsWith('/')) {
        if (!url.pathname.startsWith(path)) {
          continue;
        }
      } else {
        if (url.pathname !== path) {
          // Path doesn't match.
          continue;
        }
      }
    }

    // We found a match.
    return url;
  }

  // No match was found.
  return null;
}


/**
 * Applies a check to all directive values of a csp.
 * @param parsedCsp Parsed CSP.
 * @param check The check function that
 *   should get applied on directive values.
 */
export function applyCheckFunktionToDirectives(
    parsedCsp: csp.Csp,
    check: (directive: string, directiveValues: string[]) => void,
) {
  const directiveNames = Object.keys(parsedCsp.directives);

  for (const directive of directiveNames) {
    const directiveValues = parsedCsp.directives[directive];
    if (directiveValues) {
      check(directive, directiveValues);
    }
  }
}
