| /* |
| * Copyright (C) 2013 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| function removeURLFragment(url) |
| { |
| var hashIndex = url.indexOf("#"); |
| if (hashIndex >= 0) |
| return url.substring(0, hashIndex); |
| return url; |
| } |
| |
| function relativePath(path, basePath) |
| { |
| console.assert(path.charAt(0) === "/"); |
| console.assert(basePath.charAt(0) === "/"); |
| |
| var pathComponents = path.split("/"); |
| var baseComponents = basePath.replace(/\/$/, "").split("/"); |
| var finalComponents = []; |
| |
| var index = 1; |
| for (; index < pathComponents.length && index < baseComponents.length; ++index) { |
| if (pathComponents[index] !== baseComponents[index]) |
| break; |
| } |
| |
| for (var i = index; i < baseComponents.length; ++i) |
| finalComponents.push(".."); |
| |
| for (var i = index; i < pathComponents.length; ++i) |
| finalComponents.push(pathComponents[i]); |
| |
| return finalComponents.join("/"); |
| } |
| |
| function parseSecurityOrigin(securityOrigin) |
| { |
| securityOrigin = securityOrigin ? securityOrigin.trim() : ""; |
| |
| let match = securityOrigin.match(/^(?<scheme>[^:]+):\/\/(?<host>[^\/:]*)(?::(?<port>[\d]+))?$/i); |
| if (!match) |
| return {scheme: null, host: null, port: null}; |
| |
| let scheme = match.groups.scheme.toLowerCase(); |
| let host = match.groups.host.toLowerCase(); |
| let port = Number(match.groups.port) || null; |
| |
| return {scheme, host, port}; |
| } |
| |
| function parseDataURL(url) |
| { |
| if (!url.startsWith("data:")) |
| return null; |
| |
| // data:[<media type>][;charset=<character set>][;base64],<data> |
| let match = url.match(/^data:(?<mime>[^;,]*)?(?:;charset=(?<charset>[^;,]*?))?(?<base64>;base64)?,(?<data>.*)$/); |
| if (!match) |
| return null; |
| |
| let scheme = "data"; |
| let mimeType = match.groups.mime || "text/plain"; |
| let charset = match.groups.charset || "US-ASCII"; |
| let base64 = !!match.groups.base64; |
| let data = decodeURIComponent(match.groups.data); |
| |
| return {scheme, mimeType, charset, base64, data}; |
| } |
| |
| function parseURL(url) |
| { |
| let result = { |
| scheme: null, |
| userinfo: null, |
| host: null, |
| port: null, |
| origin: null, |
| path: null, |
| queryString: null, |
| fragment: null, |
| lastPathComponent: null, |
| }; |
| |
| // dataURLs should be handled by `parseDataURL`. |
| if (url && url.startsWith("data:")) { |
| result.scheme = "data"; |
| return result; |
| } |
| |
| // Internal sourceURLs will fail in URL constructor anyways. |
| if (isWebKitInternalScript(url)) |
| return result; |
| |
| let parsed = null; |
| try { |
| parsed = new URL(url); |
| } catch { |
| return result; |
| } |
| |
| result.scheme = parsed.protocol.slice(0, -1); // remove trailing ":" |
| |
| if (parsed.username) |
| result.userinfo = parsed.username; |
| if (parsed.password) |
| result.userinfo = (result.userinfo || "") + ":" + parsed.password; |
| |
| if (parsed.hostname) |
| result.host = parsed.hostname; |
| |
| if (parsed.port) |
| result.port = Number(parsed.port); |
| |
| if (parsed.origin) |
| result.origin = parsed.origin; |
| else if (result.scheme && result.host) { |
| result.origin = result.scheme + "://" + result.host; |
| if (result.port) |
| result.origin += ":" + result.port; |
| } |
| |
| if (parsed.pathname) |
| result.path = parsed.pathname; |
| |
| if (parsed.search) |
| result.queryString = parsed.search.substring(1); // remove leading "?" |
| |
| if (parsed.hash) |
| result.fragment = parsed.hash.substring(1); // remove leading "#" |
| |
| // Find last path component. |
| if (result.path && result.path !== "/") { |
| // Skip the trailing slash if there is one. |
| let endOffset = result.path.endsWith("/") ? 1 : 0; |
| let lastSlashIndex = result.path.lastIndexOf("/", result.path.length - 1 - endOffset); |
| if (lastSlashIndex !== -1) |
| result.lastPathComponent = result.path.substring(lastSlashIndex + 1, result.path.length - endOffset); |
| } |
| |
| return result; |
| } |
| |
| function absoluteURL(partialURL, baseURL) |
| { |
| partialURL = partialURL ? partialURL.trim() : ""; |
| |
| // Return data and javascript URLs as-is. |
| if (partialURL.startsWith("data:") || partialURL.startsWith("javascript:") || partialURL.startsWith("mailto:")) |
| return partialURL; |
| |
| // If the URL has a scheme it is already a full URL, so return it. |
| if (parseURL(partialURL).scheme) |
| return partialURL; |
| |
| // If there is no partial URL, just return the base URL. |
| if (!partialURL) |
| return baseURL || null; |
| |
| var baseURLComponents = parseURL(baseURL); |
| |
| // The base URL needs to be an absolute URL. Return null if it isn't. |
| if (!baseURLComponents.scheme) |
| return null; |
| |
| // A URL that starts with "//" is a full URL without the scheme. Use the base URL scheme. |
| if (partialURL[0] === "/" && partialURL[1] === "/") |
| return baseURLComponents.scheme + ":" + partialURL; |
| |
| // The path can be null for URLs that have just a scheme and host (like "http://apple.com"). So make the path be "/". |
| if (!baseURLComponents.path) |
| baseURLComponents.path = "/"; |
| |
| // Generate the base URL prefix that is used in the rest of the cases. |
| var baseURLPrefix = baseURLComponents.scheme + "://" + baseURLComponents.host + (baseURLComponents.port ? (":" + baseURLComponents.port) : ""); |
| |
| // A URL that starts with "?" is just a query string that gets applied to the base URL (replacing the base URL query string and fragment). |
| if (partialURL[0] === "?") |
| return baseURLPrefix + baseURLComponents.path + partialURL; |
| |
| // A URL that starts with "/" is an absolute path that gets applied to the base URL (replacing the base URL path, query string and fragment). |
| if (partialURL[0] === "/") |
| return baseURLPrefix + resolveDotsInPath(partialURL); |
| |
| // A URL that starts with "#" is just a fragment that gets applied to the base URL (replacing the base URL fragment, maintaining the query string). |
| if (partialURL[0] === "#") { |
| let queryStringComponent = baseURLComponents.queryString ? "?" + baseURLComponents.queryString : ""; |
| return baseURLPrefix + baseURLComponents.path + queryStringComponent + partialURL; |
| } |
| |
| // Generate the base path that is used in the final case by removing everything after the last "/" from the base URL's path. |
| var basePath = baseURLComponents.path.substring(0, baseURLComponents.path.lastIndexOf("/")) + "/"; |
| return baseURLPrefix + resolveDotsInPath(basePath + partialURL); |
| } |
| |
| function parseQueryString(queryString, arrayResult) |
| { |
| if (!queryString) |
| return arrayResult ? [] : {}; |
| |
| function decode(string) |
| { |
| try { |
| // Replace "+" with " " then decode percent encoded values. |
| return decodeURIComponent(string.replace(/\+/g, " ")); |
| } catch { |
| return string; |
| } |
| } |
| |
| var parameters = arrayResult ? [] : {}; |
| for (let parameterString of queryString.split("&")) { |
| let index = parameterString.indexOf("="); |
| if (index === -1) |
| index = parameterString.length; |
| |
| let name = decode(parameterString.substring(0, index)); |
| let value = decode(parameterString.substring(index + 1)); |
| |
| if (arrayResult) |
| parameters.push({name, value}); |
| else |
| parameters[name] = value; |
| } |
| |
| return parameters; |
| } |
| |
| WI.displayNameForURL = function(url, urlComponents, options = {}) |
| { |
| if (url.startsWith("data:")) |
| return WI.truncateURL(url); |
| |
| if (!urlComponents) |
| urlComponents = parseURL(url); |
| |
| var displayName; |
| try { |
| displayName = decodeURIComponent(urlComponents.lastPathComponent || ""); |
| } catch { |
| displayName = urlComponents.lastPathComponent; |
| } |
| |
| if (options.allowDirectoryAsName && (urlComponents.path === "/" || (displayName && urlComponents.path.endsWith(displayName + "/")))) |
| displayName = "/"; |
| |
| return displayName || WI.displayNameForHost(urlComponents.host) || url; |
| }; |
| |
| WI.truncateURL = function(url, multiline = false, dataURIMaxSize = 6) |
| { |
| if (!url.startsWith("data:")) |
| return url; |
| |
| const dataIndex = url.indexOf(",") + 1; |
| let header = url.slice(0, dataIndex); |
| if (multiline) |
| header += "\n"; |
| |
| const data = url.slice(dataIndex); |
| if (data.length < dataURIMaxSize) |
| return header + data; |
| |
| const firstChunk = data.slice(0, Math.ceil(dataURIMaxSize / 2)); |
| const ellipsis = "\u2026"; |
| const middleChunk = multiline ? `\n${ellipsis}\n` : ellipsis; |
| const lastChunk = data.slice(-Math.floor(dataURIMaxSize / 2)); |
| return header + firstChunk + middleChunk + lastChunk; |
| }; |
| |
| WI.urlWithoutFragment = function(urlString) |
| { |
| try { |
| let url = new URL(urlString); |
| if (url.hash) { |
| url.hash = ""; |
| return url.toString(); |
| } |
| |
| // URL.toString with an empty hash leaves the hash symbol, so we strip it. |
| let result = url.toString(); |
| if (result.endsWith("#")) |
| return result.substring(0, result.length - 1); |
| |
| return result; |
| } catch { } |
| |
| return urlString; |
| }; |
| |
| WI.displayNameForHost = function(host) |
| { |
| // FIXME <rdar://problem/11237413>: This should decode punycode hostnames. |
| return host; |
| }; |
| |
| // https://tools.ietf.org/html/rfc7540#section-8.1.2.3 |
| WI.h2Authority = function(components) |
| { |
| let {scheme, userinfo, host, port} = components; |
| let result = host || ""; |
| |
| // The authority MUST NOT include the deprecated "userinfo" |
| // subcomponent for "http" or "https" schemed URIs. |
| if (userinfo && (scheme !== "http" && scheme !== "https")) |
| result = userinfo + "@" + result; |
| if (port) |
| result += ":" + port; |
| |
| return result; |
| }; |
| |
| // https://tools.ietf.org/html/rfc7540#section-8.1.2.3 |
| WI.h2Path = function(components) |
| { |
| let {scheme, path, queryString} = components; |
| let result = path || ""; |
| |
| // The ":path" pseudo-header field includes the path and query parts |
| // of the target URI. [...] This pseudo-header field MUST NOT be empty |
| // for "http" or "https" URIs; "http" or "https" URIs that do not contain |
| // a path component MUST include a value of '/'. |
| if (!path && (scheme === "http" || scheme === "https")) |
| result = "/"; |
| if (queryString) |
| result += "?" + queryString; |
| |
| return result; |
| }; |