diff options
author | stuartmorgan <stuartmorgan@chromium.org> | 2015-03-24 15:22:28 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-03-24 22:23:15 +0000 |
commit | 1ab833b209a2e2fc1e6bf2001d0bfcc5181e7bf2 (patch) | |
tree | 886435b5ff0cf5b98fea89ba7a59f0be77ce1b3c /ios | |
parent | 187d3948887386d83b37e1b7101562022ad26d1b (diff) | |
download | chromium_src-1ab833b209a2e2fc1e6bf2001d0bfcc5181e7bf2.zip chromium_src-1ab833b209a2e2fc1e6bf2001d0bfcc5181e7bf2.tar.gz chromium_src-1ab833b209a2e2fc1e6bf2001d0bfcc5181e7bf2.tar.bz2 |
Upstream ios/web/ JS files
This upstreams all the injected JS for the web layer (but not any of
the packaging bits, which will come later)
BUG=464810
Review URL: https://codereview.chromium.org/1029983002
Cr-Commit-Position: refs/heads/master@{#322080}
Diffstat (limited to 'ios')
-rw-r--r-- | ios/web/web_state/js/resources/base.js | 2 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/common.js | 113 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/console.js | 44 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/core.js | 646 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/core_dynamic_ui.js | 157 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/core_dynamic_wk.js | 59 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/dialog_overrides.js | 114 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/message.js | 2 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/message_dynamic_ui.js | 2 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/message_dynamic_wk.js | 2 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/plugin_placeholder.js | 199 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/web_bundle_ui.js | 16 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/web_bundle_wk.js | 15 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/window_id.js | 31 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/window_open_ui.js | 245 | ||||
-rw-r--r-- | ios/web/web_state/js/resources/window_open_wk.js | 15 |
16 files changed, 1662 insertions, 0 deletions
diff --git a/ios/web/web_state/js/resources/base.js b/ios/web/web_state/js/resources/base.js index 97b9622..216ddaf 100644 --- a/ios/web/web_state/js/resources/base.js +++ b/ios/web/web_state/js/resources/base.js @@ -8,6 +8,8 @@ // this.style because member identifiers are minified by default. // See http://goo.gl/FwOgy +goog.provide('__crweb.base'); + // This object is checked on the main app to know when to inject (or not). window['__gCrWeb'] = {}; diff --git a/ios/web/web_state/js/resources/common.js b/ios/web/web_state/js/resources/common.js index 1cea022..7e84350 100644 --- a/ios/web/web_state/js/resources/common.js +++ b/ios/web/web_state/js/resources/common.js @@ -4,6 +4,8 @@ // This file provides common methods that can be shared by other JavaScripts. +goog.provide('__crweb.common'); + /** * Namespace for this file. It depends on |__gCrWeb| having already been * injected. String 'common' is used in |__gCrWeb['common']| as it needs to be @@ -576,4 +578,115 @@ new function() { element.dispatchEvent(changeEvent); }, 0); }; + + /** + * Retrieves favicon information. + * + * @return {Object} Object containing favicon data. + */ + __gCrWeb.common.getFavicons = function() { + var favicons = []; + var hasFavicon = false; + favicons.toJSON = null; // Never inherit Array.prototype.toJSON. + var links = document.getElementsByTagName('link'); + var linkCount = links.length; + for (var i = 0; i < linkCount; ++i) { + if (links[i].rel) { + var rel = links[i].rel.toLowerCase(); + if (rel == 'shortcut icon' || + rel == 'icon' || + rel == 'apple-touch-icon' || + rel == 'apple-touch-icon-precomposed') { + var favicon = { + rel: links[i].rel.toLowerCase(), + href: links[i].href + }; + favicons.push(favicon); + if (rel == 'icon' || rel == 'shortcut icon') { + hasFavicon = true; + } + } + } + } + if (!hasFavicon) { + // If an HTTP(S)? webpage does not reference a "favicon" then search + // for a file named "favicon.ico" at the root of the website (legacy). + // http://en.wikipedia.org/wiki/Favicon + var location = document.location; + if (location.protocol == 'http:' || location.protocol == 'https:') { + var favicon = { + rel: 'icon', + href: location.origin + '/favicon.ico' + }; + favicons.push(favicon); + } + } + return favicons; + }; + + /** + * Checks whether an <object> node is plugin content (as <object> can also be + * used to embed images). + * @param {HTMLElement} node The <object> node to check. + * @return {Boolean} Whether the node appears to be a plugin. + * @private + */ + var objectNodeIsPlugin_ = function(node) { + return node.hasAttribute('classid') || + (node.hasAttribute('type') && node.type.indexOf('image/') != 0); + }; + + /** + * Checks whether plugin a node has fallback content. + * @param {HTMLElement} node The node to check. + * @return {Boolean} Whether the node has fallback. + * @private + */ + var pluginHasFallbackContent_ = function(node) { + return node.textContent.trim().length > 0 || + node.getElementsByTagName('img').length > 0; + }; + + /** + * Returns a list of plugin elements in the document that have no fallback + * content. For nested plugins, only the innermost plugin element is returned. + * @return {Array} A list of plugin elements. + * @private + */ + var findPluginNodesWithoutFallback_ = function() { + var pluginNodes = []; + var objects = document.getElementsByTagName('object'); + var objectCount = objects.length; + for (var i = 0; i < objectCount; i++) { + var object = objects[i]; + if (objectNodeIsPlugin_(object) && + !pluginHasFallbackContent_(object)) { + pluginNodes.push(object); + } + } + var applets = document.getElementsByTagName('applet'); + var appletsCount = applets.length; + for (var i = 0; i < appletsCount; i++) { + var applet = applets[i]; + if (!pluginHasFallbackContent_(applet)) { + pluginNodes.push(applet); + } + } + return pluginNodes; + }; + + /** + * Finds and stores any plugins that don't have placeholders. + * Returns true if any plugins without placeholders are found. + */ + __gCrWeb.common.updatePluginPlaceholders = function() { + var plugins = findPluginNodesWithoutFallback_(); + if (plugins.length > 0) { + // Store the list of plugins in a known place for the replacement script + // to use, then trigger it. + __gCrWeb['placeholderTargetPlugins'] = plugins; + return true; + } + return false; + }; } // End of anonymous object diff --git a/ios/web/web_state/js/resources/console.js b/ios/web/web_state/js/resources/console.js new file mode 100644 index 0000000..0e67644 --- /dev/null +++ b/ios/web/web_state/js/resources/console.js @@ -0,0 +1,44 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Scripts to allow page console.log() etc. output to be seen on the console +// of the host application. + +goog.provide('__crweb.console'); + +/** + * Namespace for this module. + */ +__gCrWeb.console = {}; + +/* Beginning of anonymous object. */ +new function() { + function sendConsoleMessage(method, originalArguments) { + message = Array.prototype.slice.call(originalArguments).join(' '); + __gCrWeb.message.invokeOnHost({'command': 'console', + 'method': method, + 'message': message, + 'origin': document.location.origin}); + } + + console.log = function() { + sendConsoleMessage('log', arguments); + }; + + console.debug = function() { + sendConsoleMessage('debug', arguments); + }; + + console.info = function() { + sendConsoleMessage('info', arguments); + }; + + console.warn = function() { + sendConsoleMessage('warn', arguments); + }; + + console.error = function() { + sendConsoleMessage('error', arguments); + }; +} diff --git a/ios/web/web_state/js/resources/core.js b/ios/web/web_state/js/resources/core.js new file mode 100644 index 0000000..5e5d0dd --- /dev/null +++ b/ios/web/web_state/js/resources/core.js @@ -0,0 +1,646 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file adheres to closure-compiler conventions in order to enable +// compilation with ADVANCED_OPTIMIZATIONS. In particular, members that are to +// be accessed externally should be specified in this['style'] as opposed to +// this.style because member identifiers are minified by default. +// See http://goo.gl/FwOgy + +goog.provide('__crweb.core'); + +/** + * The Chrome object is populated in an anonymous object defined at + * initialization to prevent polluting the global namespace. + */ + +/* Beginning of anonymous object. */ +new function() { + // TODO(jimblackler): use this namespace as a wrapper for all externally- + // visible functions, to be consistent with other JS scripts. crbug.com/380390 + __gCrWeb['core'] = {}; + + // JavaScript errors are logged on the main application side. The handler is + // added ASAP to catch any errors in startup. Note this does not appear to + // work in iOS < 5. + window.addEventListener('error', function(event) { + // Sadly, event.filename and event.lineno are always 'undefined' and '0' + // with UIWebView. + invokeOnHost_({'command': 'window.error', + 'message': event.message.toString()}); + }); + + /** + * Margin in points around touchable elements (e.g. links for custom context + * menu). + * @type {number} + */ + var touchMargin_ = 25; + + __gCrWeb['innerSizeAsString'] = function() { + return window.innerWidth + '/' + window.innerHeight; + }; + + // Implementation of document.elementFromPoint that is working for iOS4 and + // iOS5 and that also goes into frames and iframes. + var elementFromPoint_ = function(x, y) { + var elementFromPointIsUsingViewPortCoordinates = function(win) { + if (win.pageYOffset > 0) { // Page scrolled down. + return (win.document.elementFromPoint( + 0, win.pageYOffset + win.innerHeight - 1) === null); + } + if (win.pageXOffset > 0) { // Page scrolled to the right. + return (win.document.elementFromPoint( + win.pageXOffset + win.innerWidth - 1, 0) === null); + } + return false; // No scrolling, don't care. + }; + + var newCoordinate = function(x, y) { + var coordinates = { + x: x, y: y, + viewPortX: x - window.pageXOffset, viewPortY: y - window.pageYOffset, + useViewPortCoordinates: false, + window: window + }; + return coordinates; + }; + + // Returns the coordinates of the upper left corner of |obj| in the + // coordinates of the window that |obj| is in. + var getPositionInWindow = function(obj) { + var coord = { x: 0, y: 0 }; + while (obj.offsetParent) { + coord.x += obj.offsetLeft; + coord.y += obj.offsetTop; + obj = obj.offsetParent; + } + return coord; + }; + + var elementsFromCoordinates = function(coordinates) { + coordinates.useViewPortCoordinates = coordinates.useViewPortCoordinates || + elementFromPointIsUsingViewPortCoordinates(coordinates.window); + + var currentElement = null; + if (coordinates.useViewPortCoordinates) { + currentElement = coordinates.window.document.elementFromPoint( + coordinates.viewPortX, coordinates.viewPortY); + } else { + currentElement = coordinates.window.document.elementFromPoint( + coordinates.x, coordinates.y); + } + // We have to check for tagName, because if a selection is made by the + // UIWebView, the element we will get won't have one. + if (!currentElement || !currentElement.tagName) { + return null; + } + if (currentElement.tagName.toLowerCase() === 'iframe' || + currentElement.tagName.toLowerCase() === 'frame') { + // The following condition is true if the iframe is in a different + // domain; no further information is accessible. + if (typeof(currentElement.contentWindow.document) == 'undefined') { + invokeOnHost_({ + 'command': 'window.error', + 'message': 'iframe contentWindow.document is not accessible.'}); + return currentElement; + } + var framePosition = getPositionInWindow(currentElement); + coordinates.viewPortX -= + framePosition.x - coordinates.window.pageXOffset; + coordinates.viewPortY -= + framePosition.y - coordinates.window.pageYOffset; + coordinates.window = currentElement.contentWindow; + coordinates.x -= framePosition.x + coordinates.window.pageXOffset; + coordinates.y -= framePosition.y + coordinates.window.pageYOffset; + return elementsFromCoordinates(coordinates); + } + return currentElement; + }; + + return elementsFromCoordinates(newCoordinate(x, y)); + }; + + var spiralCoordinates = function(x, y) { + var coordinates = []; + + var maxAngle = Math.PI * 2.0 * 3.0; + var pointCount = 30; + var angleStep = maxAngle / pointCount; + var speed = touchMargin_ / maxAngle; + + for (var index = 0; index < pointCount; index++) { + var angle = angleStep * index; + var radius = angle * speed; + + coordinates.push({x: x + Math.round(Math.cos(angle) * radius), + y: y + Math.round(Math.sin(angle) * radius)}); + } + + return coordinates; + }; + + // Returns the url of the image or link under the selected point. Returns an + // empty string if no links or images are found. + __gCrWeb['getElementFromPoint'] = function(x, y) { + var hitCoordinates = spiralCoordinates(x, y); + for (var index = 0; index < hitCoordinates.length; index++) { + var coordinates = hitCoordinates[index]; + + var element = elementFromPoint_(coordinates.x, coordinates.y); + if (!element || !element.tagName) { + // Nothing under the hit point. Try the next hit point. + continue; + } + + if (getComputedWebkitTouchCallout_(element) === 'none') + continue; + // Also check element's ancestors. A bound on the level is used here to + // avoid large overhead when no links or images are found. + var level = 0; + while (++level < 8 && element && element != document) { + var tagName = element.tagName; + if (!tagName) + continue; + tagName = tagName.toLowerCase(); + + if (tagName === 'input' || tagName === 'textarea' || + tagName === 'select' || tagName === 'option') { + // If the element is a known input element, stop the spiral search and + // return empty results. + return '{}'; + } + + if (tagName === 'a' && element.href) { + // Found a link. + return __gCrWeb.common.JSONStringify( + {href: element.href, + referrerPolicy: getReferrerPolicy_(element)}); + } + + if (tagName === 'img' && element.src) { + // Found an image. + var result = {src: element.src, + referrerPolicy: getReferrerPolicy_()}; + // Copy the title, if any. + if (element.title) { + result.title = element.title; + } + // Check if the image is also a link. + var parent = element.parentNode; + while (parent) { + if (parent.tagName && + parent.tagName.toLowerCase() === 'a' && + parent.href) { + // This regex identifies strings like void(0), + // void(0) ;void(0);, ;;;; + // which result in a NOP when executed as JavaScript. + var regex = RegExp("^javascript:(?:(?:void\\(0\\)|;)\\s*)+$"); + if (parent.href.match(regex)) { + parent = parent.parentNode; + continue; + } + result.href = parent.href; + result.referrerPolicy = getReferrerPolicy_(parent); + break; + } + parent = parent.parentNode; + } + return __gCrWeb.common.JSONStringify(result); + } + element = element.parentNode; + } + } + return '{}'; + }; + + // Returns true if the top window or any frames inside contain an input + // field of type 'password'. + __gCrWeb['hasPasswordField'] = function() { + return hasPasswordField_(window); + }; + + // Returns a string that is formatted according to the JSON syntax rules. + // This is equivalent to the built-in JSON.stringify() function, but is + // less likely to be overridden by the website itself. This public function + // should not be used if spoofing it would create a security vulnerability. + // The |__gCrWeb| object itself does not use it; it uses its private + // counterpart instead. + // Prevents websites from changing stringify's behavior by adding the + // method toJSON() by temporarily removing it. + __gCrWeb['stringify'] = function(value) { + if (value === null) + return 'null'; + if (value === undefined) + return undefined; + if (typeof(value.toJSON) == 'function') { + var originalToJSON = value.toJSON; + value.toJSON = undefined; + var stringifiedValue = __gCrWeb.common.JSONStringify(value); + value.toJSON = originalToJSON; + return stringifiedValue; + } + return __gCrWeb.common.JSONStringify(value); + }; + + /* + * Adds the listeners that are used to handle forms, enabling autofill and + * the replacement method to dismiss the keyboard needed because of the + * Autofill keyboard accessory. + */ + function addFormEventListeners_() { + // Focus and input events for form elements are messaged to the main + // application for broadcast to CRWWebControllerObservers. + // This is done with a single event handler for each type being added to the + // main document element which checks the source element of the event; this + // is much easier to manage than adding handlers to individual elements. + var formActivity = function(evt) { + var srcElement = evt.srcElement; + var fieldName = srcElement.name || ''; + var value = srcElement.value || ''; + + var msg = { + 'command': 'form.activity', + 'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement.form), + 'fieldName': fieldName, + 'type': evt.type, + 'value': value + }; + if (evt.keyCode) + msg.keyCode = evt.keyCode; + invokeOnHost_(msg); + }; + + // Focus events performed on the 'capture' phase otherwise they are often + // not received. + document.addEventListener('focus', formActivity, true); + document.addEventListener('blur', formActivity, true); + document.addEventListener('change', formActivity, true); + + // Text input is watched at the bubbling phase as this seems adequate in + // practice and it is less obtrusive to page scripts than capture phase. + document.addEventListener('input', formActivity, false); + document.addEventListener('keyup', formActivity, false); + }; + + // Returns true if the supplied window or any frames inside contain an input + // field of type 'password'. + // @private + var hasPasswordField_ = function(win) { + var doc = win.document; + + // We may will not be allowed to read the 'document' property from a frame + // that is in a different domain. + if (!doc) { + return false; + } + + if (doc.querySelector('input[type=password]')) { + return true; + } + + var frames = win.frames; + for (var i = 0; i < frames.length; i++) { + if (hasPasswordField_(frames[i])) { + return true; + } + } + + return false; + }; + + function invokeOnHost_(command) { + __gCrWeb.message.invokeOnHost(command); + }; + + function invokeOnHostImmediate_(command) { + __gCrWeb.message.invokeOnHostImmediate(command); + }; + + /** + * Gets the referrer policy to use for navigations away from the current page. + * If a link element is passed, and it includes a rel=noreferrer tag, that + * will override the page setting. + * @param {HTMLElement} linkElement The link triggering the navigation. + * @return {String} The policy string. + * @private + */ + var getReferrerPolicy_ = function(linkElement) { + if (linkElement) { + var rel = linkElement.getAttribute('rel'); + if (rel && rel.toLowerCase() == 'noreferrer') { + return 'never'; + } + } + + var metaTags = document.getElementsByTagName('meta'); + for (var i = 0; i < metaTags.length; ++i) { + if (metaTags[i].name.toLowerCase() == 'referrer') { + return metaTags[i].content.toLowerCase(); + } + } + return 'default'; + }; + + // Provides a way for other injected javascript to access the page's referrer + // policy. + __gCrWeb['getPageReferrerPolicy'] = function() { + return getReferrerPolicy_(); + }; + + // Various aspects of global DOM behavior are overridden here. + + // A popstate event needs to be fired anytime the active history entry + // changes. Either via back, forward, go navigation or by loading the URL, + // clicking on a link, etc. + __gCrWeb['dispatchPopstateEvent'] = function(stateObject) { + var popstateEvent = window.document.createEvent('HTMLEvents'); + popstateEvent.initEvent('popstate', true, false); + if (stateObject) + popstateEvent.state = JSON.parse(stateObject); + + // setTimeout() is used in order to return immediately. Otherwise the + // dispatchEvent call waits for all event handlers to return, which could + // cause a ReentryGuard failure. + window.setTimeout(function() { + window.dispatchEvent(popstateEvent); + }, 0); + }; + + // Keep the original replaceState() method. It's needed to update UIWebView's + // URL and window.history.state property during history navigations that don't + // cause a page load. + var originalWindowHistoryReplaceState = window.history.replaceState; + __gCrWeb['replaceWebViewURL'] = function(url, stateObject) { + originalWindowHistoryReplaceState.call(history, stateObject, null, url); + }; + + // Intercept window.history methods to call back/forward natively. + window.history.back = function() { + invokeOnHost_({'command': 'window.history.back'}); + }; + window.history.forward = function() { + invokeOnHost_({'command': 'window.history.forward'}); + }; + window.history.go = function(delta) { + invokeOnHost_({'command': 'window.history.go', 'value': delta}); + }; + window.history.pushState = function(stateObject, pageTitle, pageUrl) { + __gCrWeb.core_dynamic.historyWillChangeState(); + // Calling stringify() on undefined causes a JSON parse error. + var serializedState = + typeof(stateObject) == 'undefined' ? '' : + __gCrWeb.common.JSONStringify(stateObject); + pageUrl = pageUrl || window.location.href; + originalWindowHistoryReplaceState.call(history, stateObject, null, pageUrl); + invokeOnHost_({'command': 'window.history.didPushState', + 'stateObject': serializedState, + 'baseUrl': document.baseURI, + 'pageUrl': pageUrl.toString()}); + }; + window.history.replaceState = function(stateObject, pageTitle, pageUrl) { + __gCrWeb.core_dynamic.historyWillChangeState(); + // Calling stringify() on undefined causes a JSON parse error. + var serializedState = + typeof(stateObject) == 'undefined' ? '' : + __gCrWeb.common.JSONStringify(stateObject); + pageUrl = pageUrl || window.location.href; + originalWindowHistoryReplaceState.call(history, stateObject, null, pageUrl); + invokeOnHost_({'command': 'window.history.didReplaceState', + 'stateObject': serializedState, + 'baseUrl': document.baseURI, + 'pageUrl': pageUrl.toString()}); + }; + + __gCrWeb['getFullyQualifiedURL'] = function(originalURL) { + // A dummy anchor (never added to the document) is used to obtain the + // fully-qualified URL of |originalURL|. + var anchor = document.createElement('a'); + anchor.href = originalURL; + return anchor.href; + }; + + // Intercept window.close calls. + window.close = function() { + invokeOnHost_({'command': 'window.close.self'}); + }; + + window.addEventListener('hashchange', function(evt) { + invokeOnHost_({'command': 'window.hashchange'}); + }); + + __gCrWeb.core_dynamic.addEventListeners(); + + // Returns if a frame with |name| is found in |currentWindow|. + // Note frame.name is undefined for cross domain frames. + var hasFrame_ = function(currentWindow, name) { + if (currentWindow.name === name) + return true; + + var frames = currentWindow.frames; + for (var index = 0; index < frames.length; ++index) { + var frame = frames[index]; + if (frame === undefined) + continue; + if (hasFrame_(frame, name)) + return true; + } + return false; + }; + + // Checks if |node| is an anchor to be opened in the current tab. + var isInternaLink_ = function(node) { + if (!node instanceof HTMLAnchorElement) + return false; + + // Anchor with href='javascript://.....' will be opened in the current tab + // for simplicity. + if (node.href.indexOf('javascript:') == 0) + return true; + + // UIWebView will take care of the following cases. + // + // - If the given browsing context name is the empty string or '_self', then + // the chosen browsing context must be the current one. + // + // - If the given browsing context name is '_parent', then the chosen + // browsing context must be the parent browsing context of the current + // one, unless there is no one, in which case the chosen browsing context + // must be the current browsing context. + // + // - If the given browsing context name is '_top', then the chosen browsing + // context must be the top-level browsing context of the current one, if + // there is one, or else the current browsing context. + // + // Here an undefined target is considered in the same way as an empty + // target. + if (node.target === undefined || node.target === '' || + node.target === '_self' || node.target === '_parent' || + node.target === '_top') { + return true; + } + + // A new browsing context is being requested for an '_blank' target. + if (node.target === '_blank') + return false; + + // Otherwise UIWebView will take care of the case where there exists a + // browsing context whose name is the same as the given browsing context + // name. If there is no such a browsing context, a new browsing context is + // being requested. + return hasFrame_(window, node.target); + }; + + var getTargetLink_ = function(target) { + var node = target; + // Find the closest ancester that is a link. + while (node) { + if (node instanceof HTMLAnchorElement) + break; + node = node.parentNode; + } + return node; + }; + + var setExternalRequest_ = function(href, target) { + if (typeof(target) == 'undefined' || target == '_blank' || target == '') { + target = '' + Date.now() + '-' + Math.random(); + } + if (typeof(href) == 'undefined') { + // W3C recommended behavior. + href = 'about:blank'; + } + // ExternalRequest messages need to be handled before the expected + // shouldStartLoadWithRequest, as such we cannot wait for the regular + // message queue invoke which delays to avoid illegal recursion into + // UIWebView. This immediate class of messages is handled ASAP by + // CRWWebController. + invokeOnHostImmediate_({'command': 'externalRequest', + 'href': href, + 'target': target, + 'referrerPolicy': getReferrerPolicy_()}); + }; + + var resetExternalRequest_ = function() { + invokeOnHost_({'command': 'resetExternalRequest'}); + }; + + var clickBubbleListener_ = function(evt) { + if (evt['defaultPrevented']) { + resetExternalRequest_(); + } + // Remove the listener. + evt.currentTarget.removeEventListener( + 'click', clickBubbleListener_, false); + }; + + var getComputedWebkitTouchCallout_ = function(element) { + return window.getComputedStyle(element, null)['webkitTouchCallout']; + }; + + /** + * This method applies the various document-level overrides. Sometimes the + * document object gets reset in the early stages of the page lifecycle, so + * this is exposed as a method for the application to invoke later. That way + * the window-level overrides can be applied as soon as possible. + */ + __gCrWeb.core.documentInject = function() { + // Perform web view specific operations requiring document.body presence. + // If necessary returns and waits for document to be present. + if (!__gCrWeb.core_dynamic.documentInject()) + return; + + document.addEventListener('click', function(evt) { + var node = getTargetLink_(evt.target); + + if (!node) + return; + + if (isInternaLink_(node)) { + return; + } + setExternalRequest_(node.href, node.target); + // Add listener to the target and its immediate ancesters. These event + // listeners will be removed if they get called. The listeners for some + // elements might never be removed, but if multiple identical event + // listeners are registered on the same event target with the same + // parameters the duplicate instances are discarded. + for (var level = 0; level < 5; ++level) { + if (node && node != document) { + node.addEventListener('click', clickBubbleListener_, false); + node = node.parentNode; + } else { + break; + } + } + }, true); + + // Intercept clicks on anchors (links) during bubbling phase so that the + // browser can handle target type appropriately. + document.addEventListener('click', function(evt) { + var node = getTargetLink_(evt.target); + + if (!node) + return; + + if (isInternaLink_(node)) { + if (evt['defaultPrevented']) + return; + // Internal link. The web view will handle navigation, but register + // the anchor for UIWebView to start the progress indicator ASAP and + // notify web controller as soon as possible of impending navigation. + if (__gCrWeb.core_dynamic.handleInternalClickEvent) { + __gCrWeb.core_dynamic.handleInternalClickEvent(node); + } + return; + } else { + // Resets the external request if it has been canceled, otherwise + // updates the href in case it has been changed. + if (evt['defaultPrevented']) + resetExternalRequest_(); + else + setExternalRequest_(node.href, node.target); + } + }, false); + + // Capture form submit actions. + document.addEventListener('submit', function(evt) { + if (evt['defaultPrevented']) + return; + + var form = evt.target; + var targetsFrame = form.target && hasFrame_(window, form.target); + // TODO(stuartmorgan): Handle external targets. crbug.com/233543 + + var action = form.getAttribute('action'); + // Default action is to re-submit to same page. + if (!action) + action = document.location.href; + invokeOnHost_({ + 'command': 'document.submit', + 'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement), + 'href': __gCrWeb['getFullyQualifiedURL'](action), + 'targetsFrame': targetsFrame + }); + }, false); + + addFormEventListeners_(); + + // Handle or wait for and handle document load completion, if applicable. + if (__gCrWeb.core_dynamic.handleDocumentLoaded) + __gCrWeb.core_dynamic.handleDocumentLoaded(); + + return true; + }; + + __gCrWeb.core.documentInject(); + + // Form prototype loaded with event to supply Autocomplete API + // functionality. + HTMLFormElement.prototype.requestAutocomplete = function() { + invokeOnHost_( + {'command': 'form.requestAutocomplete', + 'formName': __gCrWeb.common.getFormIdentifier(this)}); + }; +} // End of anonymous object diff --git a/ios/web/web_state/js/resources/core_dynamic_ui.js b/ios/web/web_state/js/resources/core_dynamic_ui.js new file mode 100644 index 0000000..e8988b7 --- /dev/null +++ b/ios/web/web_state/js/resources/core_dynamic_ui.js @@ -0,0 +1,157 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Scripts that are conceptually part of core.js, but have UIWebView-specific +// details/behaviors. + +goog.provide('__crweb.core_dynamic_ui'); + +/** + * Namespace for this module. + */ +__gCrWeb.core_dynamic = {}; + +/* Beginning of anonymous object. */ +new function() { + /** + * Resets common.JSONStringify to a clean copy. This can be called to ensure + * that its copy is not of an override injected earlier by the page. This + * must be called after document.body is present. + */ + var resetJsonStringify_ = function() { + var frame = document.createElement('iframe'); + // crwebnull protocol returns NO immediately to reject the load attempt. + // A new frame is still created with an independent window object. + frame.src = 'crwebnull://'; + document.body.appendChild(frame); + // One some occasions the contentWindow is not available, or the JSON object + // is not available. It is not clear why, but this defense still has value + // when it can be applied. + if (frame.contentWindow && frame.contentWindow.JSON) { + // Refresh original stringify object from new window reference (if + // available) in case the originally retained version was not the native + // version. + __gCrWeb.common.JSONStringify = frame.contentWindow.JSON.stringify; + } + document.body.removeChild(frame); + }; + + /** + * Adds UIWebView specific event listeners. + */ + __gCrWeb.core_dynamic.addEventListeners = function() { + window.addEventListener('unload', function(evt) { + // In the case of a newly-created UIWebView, the URL starts as + // about:blank, the window.unload event fires, and then the URL changes. + // However, at this point the window object is *not* reset. After this, + // when the page changes for any reason the window object *is* reset. + // For this reason, we do not report the window.unload event from the + // default page to the first URL. + // This is sent as an immediate command because if the message arrives + // after the page change has taken effect, false positive security errors + // can occur. + if (!document._defaultPage) + __gCrWeb.message.invokeOnHostImmediate({'command': 'window.unload'}); + }); + }; + + /** + * Applies UIWebView specific document-level overrides. These overrides + * require the document body to be present; therefore the method sets event + * listeners and timers to retry upon document body load if document body is + * not yet present. Returns false if on default page or document is not + * present. + */ + __gCrWeb.core_dynamic.documentInject = function() { + // The default page gets the injections to the window object, but not the + // document object. On the first occasion the page changes (from the default + // about:blank) the window.unload event fires but the window object does not + // actually reset. However by the time the DOMContentLoaded event fires the + // document object will have been reset (to a non-default page). + if (document && document['_defaultPage']) { + window.addEventListener('DOMContentLoaded', __gCrWeb.core.documentInject); + return false; + } + if (!document || !document.body) { + // Either the document or document body is not yet available... retest in + // 1 / 4 of a second. + window.setTimeout(__gCrWeb.core.documentInject, 250); + return false; + } + + // Try to guarantee a clean copy of common.JSONStringify. + resetJsonStringify_(); + + // Flush the message queue and send document.present message, if load has + // not been aborted. + if (!document._cancelled) { + if (__gCrWeb.message) { + __gCrWeb.message.invokeQueues(); + } + __gCrWeb.message.invokeOnHost({'command': 'document.present'}); + } + + // Add event listener for title changes. + var lastSeenTitle = document.title; + document.addEventListener('DOMSubtreeModified', function(evt) { + if (document.title !== lastSeenTitle) { + lastSeenTitle = document.title; + __gCrWeb.message.invokeOnHost({'command': 'document.retitled'}); + } + }); + + return true; + }; + + /** + * Notifies client and handles post-document load tasks when document has + * finished loading. + */ + __gCrWeb.core_dynamic.handleDocumentLoaded = function() { + var invokeOnHost_ = __gCrWeb.message.invokeOnHost; + var loaded_ = function() { + invokeOnHost_({'command': 'document.loaded'}); + // Send the favicons to the browser. + invokeOnHost_({'command': 'document.favicons', + 'favicons': __gCrWeb.common.getFavicons()}); + // Add placeholders for plugin content. + if (__gCrWeb.common.updatePluginPlaceholders()) + __gCrWeb.message.invokeOnHost({'command': 'addPluginPlaceholders'}); + }; + + if (document.readyState === 'loaded' || document.readyState === 'complete') + loaded_(); + else + window.addEventListener('load', loaded_); + } + + + /** + * Sends anchor.click message. + */ + __gCrWeb.core_dynamic.handleInternalClickEvent = function(node) { + __gCrWeb.message.invokeOnHost({'command': 'anchor.click', + 'href': node.href}); + } + + /** + * Called when history.pushState and history.replaceState are invoked. + */ + __gCrWeb.core_dynamic.historyWillChangeState = function () { + // UIWebViewWebController does not need to be notified prior to + // history.pushState or history.replaceState calls. + }; + + /** + * Exits Fullscreen video by calling webkitExitFullScreen on every video + * element. + */ + __gCrWeb['exitFullscreenVideo'] = function() { + var videos = document.getElementsByTagName('video'); + var videosLength = videos.length; + for (var i = 0; i < videosLength; ++i) { + videos[i].webkitExitFullScreen(); + } + }; +} diff --git a/ios/web/web_state/js/resources/core_dynamic_wk.js b/ios/web/web_state/js/resources/core_dynamic_wk.js new file mode 100644 index 0000000..dd91478 --- /dev/null +++ b/ios/web/web_state/js/resources/core_dynamic_wk.js @@ -0,0 +1,59 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Scripts that are conceptually part of core.js, but have WKWebView-specific +// details/behaviors. + +goog.provide('__crweb.core_dynamic_wk'); + +/** + * Namespace for this module. + */ +__gCrWeb.core_dynamic = {}; + +/* Beginning of anonymous object. */ +new function() { + /** + * Adds WKWebView specific event listeners. + */ + __gCrWeb.core_dynamic.addEventListeners = function() { + // So far there are no WKWebView specific event listeners. + }; + + /** + * Applies WKWebView specific document-level overrides. Script injection for + * WKWebView always happens after the document is presented; therefore, there + * is no need to provide for invoking documentInject at a later time. + */ + __gCrWeb.core_dynamic.documentInject = function() { + // Flush the message queue. + if (__gCrWeb.message) { + __gCrWeb.message.invokeQueues(); + } + return true; + }; + + /** + * Handles document load completion tasks. Invoked from + * [WKNavigationDelegate webView:didFinishNavigation:], when document load is + * complete. + */ + __gCrWeb.didFinishNavigation = function() { + // Send the favicons to the browser. + __gCrWeb.message.invokeOnHost({'command': 'document.favicons', + 'favicons': __gCrWeb.common.getFavicons()}); + // Add placeholders for plugin content. + if (__gCrWeb.common.updatePluginPlaceholders()) + __gCrWeb.message.invokeOnHost({'command': 'addPluginPlaceholders'}); + } + + /** + * Sends window.history.willChangeState message. Called when + * history.pushState and history.replaceState are invoked. + */ + __gCrWeb.core_dynamic.historyWillChangeState = function() { + __gCrWeb.message.invokeOnHost( + {'command': 'window.history.willChangeState'}); + }; +} diff --git a/ios/web/web_state/js/resources/dialog_overrides.js b/ios/web/web_state/js/resources/dialog_overrides.js new file mode 100644 index 0000000..d5c582e --- /dev/null +++ b/ios/web/web_state/js/resources/dialog_overrides.js @@ -0,0 +1,114 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +goog.provide('__crweb.dialog_overrides'); + +// Namespace for this module. +__gCrWeb.dialogOverrides = {}; + +// Beginning of anonymous object. +new function() { + /* + * Install a wrapper around functions displaying dialogs in order to catch + * code displaying dialog. + * + * Since the Javascript on the page may cache the value of those functions + * and invoke them later, we must only install the wrapper once and change + * their behaviour when required. + * + * Returns a function that allows changing the value of the two booleans + * |suppressDialogs| and |notifyAboutDialogs| that are tested by the wrappers. + */ + var installDialogOverridesMethods = function() { + var suppressDialogs = false; + var notifyAboutDialogs = false; + + // Returns a wrapper function around |originalDialog|. The wrapper may + // suppress the dialog and notify host about show/suppress. + var makeDialogWrapper = function(originalDialogGetter) { + return function() { + if (!suppressDialogs) { + if (notifyAboutDialogs) { + __gCrWeb.message.invokeOnHost({'command': 'dialog.willShow'}); + } + return originalDialogGetter().apply(null, arguments); + } else if (notifyAboutDialogs) { + __gCrWeb.message.invokeOnHost({'command': 'dialog.suppressed'}); + } + }; + }; + + // Install wrapper around the following properties of |window|. + var wrappedFunctionNames = ['alert', 'confirm', 'prompt', 'open']; + var len = wrappedFunctionNames.length; + for (var i = 0; i < len; i++) { + (function(wrappedFunctionName) { + var wrappedDialogMethod = window[wrappedFunctionName]; + window[wrappedFunctionName] = makeDialogWrapper( + function() { return wrappedDialogMethod; }); + })(wrappedFunctionNames[i]); + } + + // Reading or writing to the property 'geolocation' too early breaks + // the API. Make a copy of navigator and stub in the required methods + // without touching the property. See crbug.com/280818 for more + // details. + var stubNavigator = {}; + + // Copy all properties and functions without touching 'geolocation'. + var oldNavigator = navigator; + for (var keyName in navigator) { + if (keyName !== 'geolocation') { + var value = navigator[keyName]; + if (typeof(value) == 'function') { + // Forward functions calls to real navigator. + stubNavigator[keyName] = function() { + return value.apply(oldNavigator, arguments); + } + } else { + Object['defineProperty'](stubNavigator, keyName, { + value: value, + configurable: false, + writable: false, + enumerable: true + }); + } + } + } + + // Stub in 'geolocation' if necessary, using delayed accessor for the + // 'geolocation' property of the original |navigator|. + if ('geolocation' in navigator) { + var geolocation = {}; + var geoPropNames = ['getCurrentPosition', 'watchPosition', 'clearWatch']; + var len = geoPropNames.length; + for (var i = 0; i < len; i++) { + (function(geoPropName) { + geolocation[geoPropName] = makeDialogWrapper(function() { + return function() { + return oldNavigator.geolocation[geoPropName].apply( + oldNavigator.geolocation, arguments); + }; + }); + })(geoPropNames[i]); + } + stubNavigator.geolocation = geolocation; + } + + // Install |stubNavigator| as |navigator|. + navigator = stubNavigator; + + // Returns the closure allowing to change |suppressDialogs| and + // |notifyAboutDialogs| variables. + return function(setEnabled, setNotify) { + suppressDialogs = setEnabled; + notifyAboutDialogs = setNotify; + }; + }; + + // Override certain methods that produce dialogs. This needs to be installed + // after other window methods overrides. + __gCrWeb['setSuppressDialogs'] = installDialogOverridesMethods(); + +} // End of anonymous object diff --git a/ios/web/web_state/js/resources/message.js b/ios/web/web_state/js/resources/message.js index 40ebda4..30e4a6b 100644 --- a/ios/web/web_state/js/resources/message.js +++ b/ios/web/web_state/js/resources/message.js @@ -4,6 +4,8 @@ // Scripts for the message handler. +goog.provide('__crweb.message'); + /** * Namespace for this module. */ diff --git a/ios/web/web_state/js/resources/message_dynamic_ui.js b/ios/web/web_state/js/resources/message_dynamic_ui.js index 5d17954..9ec5cdc 100644 --- a/ios/web/web_state/js/resources/message_dynamic_ui.js +++ b/ios/web/web_state/js/resources/message_dynamic_ui.js @@ -4,6 +4,8 @@ // Scripts for the message handler for use with UIWebView. +goog.provide('__crweb.message_dynamic_ui'); + /** * Namespace for this module. */ diff --git a/ios/web/web_state/js/resources/message_dynamic_wk.js b/ios/web/web_state/js/resources/message_dynamic_wk.js index 644f2fa..d8b0c24 100644 --- a/ios/web/web_state/js/resources/message_dynamic_wk.js +++ b/ios/web/web_state/js/resources/message_dynamic_wk.js @@ -4,6 +4,8 @@ // Scripts for the message handler for use with WKWebView. +goog.provide('__crweb.message_dynamic_wk'); + /** * Namespace for this module. */ diff --git a/ios/web/web_state/js/resources/plugin_placeholder.js b/ios/web/web_state/js/resources/plugin_placeholder.js new file mode 100644 index 0000000..4a588fd --- /dev/null +++ b/ios/web/web_state/js/resources/plugin_placeholder.js @@ -0,0 +1,199 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file adheres to closure-compiler conventions in order to enable +// compilation with ADVANCED_OPTIMIZATIONS. See http://goo.gl/FwOgy +// +// Installs and runs the plugin placeholder function on the |__gCrWeb| object. + +/** + * Namespace for this file. It depends on |__gCrWeb| having already been + * injected. + */ +__gCrWeb['plugin'] = {}; + +/* Beginning of anonymous object. */ +new function() { + + /* Data-URL version of plugin_blocked_android.png. Served this way rather + * than with an intercepted URL to avoid messing up https pages. + */ + __gCrWeb['plugin'].imageData_ = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAAD' + + 'aklEQVR4Xn2Wz2tcVRTHP/e+O28mMxONJKlF4kIkP4luXFgQuuxCBaG41IWrLupOXLur+A' + + 'e4cmV3LiS6qujSLgq2CIKQUqS2YnWsRkzGSTIz7zyHw+EdchnkcOd+7+OeT84578tMwmet' + + 'O1fkar1RRNAgUJuqbeEn/0RUcdS6UX7w0X54/93qw4V+m0IReBiizhAYpG52kfrO86+F9/' + + 'YXNnukHOTpc5SHgpiOu1cT623FBELeGvgTXfppOAjN3dCKm7GIkWiY4LsBnqBPpGqAgN/z' + + 'CDMMBsCWX+pwibd5hzdZZmLNOsxDm8VAzIkt1hX5NLucqgrZm3RlIC/XscKTNlAQpvncMi' + + 'tAnEM33D4nqgbcosBSPT3DRTJ3+Cx+4UfV3/CQniMQQ5g2WMJkoGKHNodUCBDpsYEQ2KGm' + + 'JBKIFPT4nYckB9ueaPxRscamWczco3qXLcR9wx4ndBsziqFSjaOCAWLm4kj0xhhSMVFli4' + + 'opyYuLlJ7s+/xTE6IgcVBthUuW6goHZDiA5IeCAnFEhkKVxxQh+pnoqSeMCEw4Uvt5kEHP' + + 'c8IyF3iJ5De1NYSAMOYvOtxgwBqv0wcE5rR4gcQGq9Sc5wt7bq2JtfYtI0Ys8mCmLhFg7q' + + 'w6XKRStUHJiMJmpC8vglqypAOU/MwRiw7KYGKqxZSKqE/iTKrQAwGxv5oU4ZbzGHCTf1QN' + + 'OTXbQhJ/gbxKjy85IPECHQSQ3EFUfM0+93iZgluM6LuzDUTJOXpc5jcWeDb3DjQrsMhj9t' + + 'TdPcAq8mtjjunyFEtN8ohfOWaVZR88Qd2WKK15a5zoRY8ZmRaNIZ/yCZ/P1u0zY+9TASjc' + + 'q04YMzBhqAAUBXf5iWcITGdql3aTtpIZVnxGYvSxj1VPXUB0EtHnxBoT6iwgeXEwQfwC69' + + 'xmROAcr5DwESxa3XLGW9G9AgPGVKahzzb/UvEcq81PwCl/MyDMrUgxQeMH7tNniQW6nPKA' + + 'e5TU3KUFjPmTRxyofUsFeFVQqyENBHDAYyodJhR0CFrnfaYECgvAjdogEwZCVySQaJ8Zeq' + + 'AL874rsy+2ofT1ev5fkSdmihwF0jpOra/kskTHkGMckkG9Gg7Xvw9XtifXOy/GEgCr7H/r' + + 'yepFOFy5fu1agI9XH71RbRWRrDmHOhrfLYrx9ndv3Wz98R+P7LgG2uyMvgAAAABJRU5Erk' + + 'Jggg=='; + + /** + * Returns the first <embed> child of the given node, if any. + * @param {HTMLElement} node The node to check. + * @return {HTMLElement} The first <embed> child, or null. + * @private + */ + __gCrWeb['plugin'].getEmbedChild_ = function(node) { + if (node.hasChildNodes()) { + for (var i = 0; i < node.childNodes.length; i++) { + if (node.childNodes[i].nodeName === 'EMBED') { + return node.childNodes[i]; + } + } + } + return null; + }; + + /** + * Returns the size for the given plugin element. For the common + * pattern of an IE-specific <object> wrapping an all-other-browsers <embed>, + * the object doesn't have real style info (most notably size), so this uses + * the embed in that case. + * @param {HTMLElement} plugin The <object> node to check. + * @return {Object} The size (width and height) for the plugin element. + * @private + */ + __gCrWeb['plugin'].getPluginSize_ = function(plugin) { + var style; + // For the common pattern of an IE-specific <object> wrapping an + // all-other-browsers <embed>, the object doesn't have real style info + // (most notably size), so this uses the embed in that case. + var embedChild = __gCrWeb['plugin'].getEmbedChild_(plugin); + if (embedChild) { + style = window.getComputedStyle(embedChild); + } else { + style = window.getComputedStyle(plugin); + } + + var width = parseFloat(style.width); + var height = parseFloat(style.height); + if (plugin.tagName === 'APPLET') { + // Size computation doesn't always work correctly with applets in + // UIWebView, so use the attributes as fallbacks. + if (isNaN(width)) { + width = parseFloat(plugin.width); + } + if (isNaN(height)) { + height = parseFloat(plugin.height); + } + } + + return { + 'width': width, + 'height': height + }; + }; + + /** + * Checks whether an element is "significant". Whether a plugin is + * "significant" is a heuristic that attempts to determine if it's a critical + * visual element for the page (i.e., not invisible, or an incidental ad). + * @param {HTMLElement} plugin The <object> node to check. + * @return {Boolean} Whether the node is significant. + * @private + */ + __gCrWeb['plugin'].isSignificantPlugin_ = function(plugin) { + var windowWidth = window.innerWidth; + var windowHeight = window.innerHeight; + var pluginSize = __gCrWeb['plugin'].getPluginSize_(plugin); + var pluginWidth = parseFloat(pluginSize.width); + var pluginHeight = parseFloat(pluginSize.height); + // A plugin must be at least |significantFraction| of one dimension of the + // page, and a minimum size in the other dimension (to weed out banners and + // tall side ads). + var minSize = Math.min(200, windowWidth / 2, windowHeight / 2); + var significantFraction = 0.5; + return (pluginWidth > windowWidth * significantFraction && + pluginHeight > minSize) || + (pluginHeight > windowHeight * significantFraction && + pluginWidth > minSize); + }; + + /** + * Walks the list of detected plugin elements, adding a placeholder to any + * that are "significant" (see above). + * @param {string} message The message to show in the placeholder. + */ + __gCrWeb['plugin']['addPluginPlaceholders'] = function(message) { + var plugins = __gCrWeb['placeholderTargetPlugins']; + for (i = 0; i < plugins.length; i++) { + var plugin = plugins[i]; + if (!__gCrWeb['plugin'].isSignificantPlugin_(plugin)) { + continue; + } + + var pluginSize = __gCrWeb['plugin'].getPluginSize_(plugin); + var widthStyle = pluginSize.width + 'px'; + var heightStyle = pluginSize.height + 'px'; + + // The outer wrapper is a div with relative positioning, as an anchor for + // an inner absolute-position element, whose height is based on whether or + // not there's an embed. If there is, then it's zero height, to avoid + // affecting the layout of the (presumably-full-size) <embed> fallback. If + // not, it's full-height to ensure the placeholder takes up the right + // amount of space in the page layout. Width is full-width either way, to + // avoid being affected by container alignment. + var placeholder = document.createElement('div'); + placeholder.style.width = widthStyle; + if (__gCrWeb['plugin'].getEmbedChild_(plugin)) { + placeholder.style.height = '0'; + } else { + placeholder.style.height = heightStyle; + } + placeholder.style.position = 'relative'; + + // Inside is a full-plugin-size solid box. + var placeholderBox = document.createElement('div'); + placeholderBox.style.position = 'absolute'; + placeholderBox.style.boxSizing = 'border-box'; + placeholderBox.style.width = widthStyle; + placeholderBox.style.height = heightStyle; + placeholderBox.style.border = '1px solid black'; + placeholderBox.style.backgroundColor = '#808080'; + placeholder.appendChild(placeholderBox); + + // Inside that is the plugin placeholder image, centered. + var pluginImg = document.createElement('img'); + var imageSize = 36; + pluginImg.width = imageSize; + pluginImg.height = imageSize; + pluginImg.style.position = 'absolute'; + // Center vertically and horizontally. + var halfSize = imageSize / 2; + pluginImg.style.top = '50%'; + pluginImg.style.marginTop = '-' + halfSize + 'px'; + pluginImg.style.left = '50%'; + pluginImg.style.marginLeft = '-' + halfSize + 'px'; + pluginImg.src = __gCrWeb['plugin'].imageData_; + placeholderBox.appendChild(pluginImg); + + // And below that, the message. + var label = document.createElement('p'); + label.style.width = widthStyle; + label.style.height = '1.5em'; + label.style.position = 'absolute'; + // Position below the image. + label.style.top = '50%'; + label.style.marginTop = imageSize + 'px'; + // Center horizontally. + label.style.textAlign = 'center'; + label.textContent = message; + placeholderBox.appendChild(label); + + plugin.insertBefore(placeholder, plugin.firstChild); + } + }; +} // End of anonymous object diff --git a/ios/web/web_state/js/resources/web_bundle_ui.js b/ios/web/web_state/js/resources/web_bundle_ui.js new file mode 100644 index 0000000..db29cb7 --- /dev/null +++ b/ios/web/web_state/js/resources/web_bundle_ui.js @@ -0,0 +1,16 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Set of scripts required by web layer backed up by UIWebView. +goog.provide('__crweb.web_bundle_ui'); + +goog.require('__crweb.base'); +goog.require('__crweb.common'); +goog.require('__crweb.core'); +goog.require('__crweb.core_dynamic_ui'); +goog.require('__crweb.console'); +goog.require('__crweb.dialog_overrides'); +goog.require('__crweb.message'); +goog.require('__crweb.message_dynamic_ui'); +goog.require('__crweb.window_open_ui'); diff --git a/ios/web/web_state/js/resources/web_bundle_wk.js b/ios/web/web_state/js/resources/web_bundle_wk.js new file mode 100644 index 0000000..ff26a6c --- /dev/null +++ b/ios/web/web_state/js/resources/web_bundle_wk.js @@ -0,0 +1,15 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Set of scripts required by web layer backed up by WKWebView. +goog.provide('__crweb.web_bundle_wk'); + +goog.require('__crweb.base'); +goog.require('__crweb.common'); +goog.require('__crweb.core'); +goog.require('__crweb.core_dynamic_wk'); +goog.require('__crweb.console'); +goog.require('__crweb.dialog_overrides'); +goog.require('__crweb.message'); +goog.require('__crweb.message_dynamic_wk'); diff --git a/ios/web/web_state/js/resources/window_id.js b/ios/web/web_state/js/resources/window_id.js new file mode 100644 index 0000000..73ef670 --- /dev/null +++ b/ios/web/web_state/js/resources/window_id.js @@ -0,0 +1,31 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file adheres to closure-compiler conventions in order to enable +// compilation with ADVANCED_OPTIMIZATIONS. See http://goo.gl/FwOgy + +// Script to set windowId. + + +// Namespace for module, used as presence beacon for injection checks. +__gCrWeb['windowIdObject'] = {}; + +new function() { + // CRWJSWindowIdManager replaces $(WINDOW_ID) with appropriate string upon + // injection. + __gCrWeb['windowId'] = '$(WINDOW_ID)'; + + // Wrap queues flushing in setTimeout to avoid reentrant calls. + // In some circumstances setTimeout does not work on iOS8 if set from + // injected script. There is an assumption that it's happen when the script + // has been injected too early. Do not place anything important to delayed + // function body, since there is no guarantee that it will ever be executed. + // TODO(eugenebut): Find out why setTimeout does not work (crbug.com/402682). + window.setTimeout(function() { + // Send messages queued since message.js injection. + if (__gCrWeb.message) { + __gCrWeb.message.invokeQueues(); + } + }, 0); +} diff --git a/ios/web/web_state/js/resources/window_open_ui.js b/ios/web/web_state/js/resources/window_open_ui.js new file mode 100644 index 0000000..fb418f1 --- /dev/null +++ b/ios/web/web_state/js/resources/window_open_ui.js @@ -0,0 +1,245 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Scripts that are conceptually part of core.js, but have UIWebView-specific +// details/behaviors. + +goog.provide('__crweb.window_open_ui'); + +// Namespace for this module. +__gCrWeb.windowOpen = {}; + +// Beginning of anonymous object. +new function() { + // Preserve a reference to the original window.open method. + __gCrWeb['originalWindowOpen'] = window.open; + + // Object used to keep track of all windows opened from this window. + var openedWindows = {}; + + /** + * Checks if a child window exists with the given name and if so, sets its + * closed property to true and removes it from |openedWindows|. + * @param {String} windowName The name of the window to mark as closed. + */ + __gCrWeb['windowClosed'] = function(windowName) { + if (openedWindows.hasOwnProperty(windowName)) { + openedWindows[windowName].closed = true; + delete openedWindows[windowName]; + } + }; + + function invokeOnHost_(command) { + __gCrWeb.message.invokeOnHost(command); + }; + + var invokeNotImplementedOnHost_ = function(methodName) { + invokeOnHost_({'command': 'window.error', + 'message': methodName + ' is not implemented'}); + }; + + // Define Object watch/unwatch functions to detect assignments to + // certain object properties. Handles defineProperty case only because + // this code runs in UIWebView (i.e. Safari). + var objectWatch = function(obj, prop, handler) { + var val = obj[prop]; + if (delete obj[prop]) { + Object['defineProperty'](obj, prop, { + 'get': function() { + return val; + }, + 'set': function(newVal) { + return val = handler.call(obj, prop, val, newVal); + } + }); + } + }; + + /** + * Creates and returns a window proxy used to represent the window object and + * intercept calls made on it. + * @param {String} target The name of the window. + * @return {Object} A window proxy object for intercepting window methods. + * @private + */ + var createWindowProxy_ = function(target) { + // Create return window object. + // 'name' is always the original supplied name. + var windowProxy = {name: target}; + + // Define window object methods. + windowProxy.alert = function() { + invokeNotImplementedOnHost_('windowProxy.alert'); + }; + + windowProxy.blur = function() { + invokeNotImplementedOnHost_('windowProxy.blur'); + }; + + windowProxy.clearInterval = function() { + invokeNotImplementedOnHost_('windowProxy.clearInterval'); + }; + + windowProxy.clearTimeout = function() { + invokeNotImplementedOnHost_('windowProxy.clearTimeout'); + }; + + windowProxy.close = function() { + invokeOnHost_({'command': 'window.close', + 'target': target}); + }; + + windowProxy.confirm = function() { + invokeNotImplementedOnHost_('windowProxy.confirm'); + }; + + windowProxy.createPopup = function() { + invokeNotImplementedOnHost_('windowProxy.createPopup'); + }; + + windowProxy.focus = function() { + // Noop as the opened window always gets focus. + }; + + windowProxy.moveBy = function() { + invokeNotImplementedOnHost_('windowProxy.moveBy'); + }; + + windowProxy.moveTo = function() { + invokeNotImplementedOnHost_('windowProxy.moveTo'); + }; + + windowProxy.stop = function() { + invokeOnHost_({'command': 'window.stop', + 'target': target}); + }; + + windowProxy.open = function() { + invokeNotImplementedOnHost_('windowProxy.open'); + }; + + windowProxy.print = function() { + invokeNotImplementedOnHost_('windowProxy.print'); + }; + + windowProxy.prompt = function() { + invokeNotImplementedOnHost_('windowProxy.prompt'); + }; + + windowProxy.resizeBy = function() { + invokeNotImplementedOnHost_('windowProxy.resizeBy'); + }; + + windowProxy.resizeTo = function() { + invokeNotImplementedOnHost_('windowProxy.resizeTo'); + }; + + windowProxy.scroll = function() { + invokeNotImplementedOnHost_('windowProxy.scroll'); + }; + + windowProxy.scrollBy = function() { + invokeNotImplementedOnHost_('windowProxy.scrollBy'); + }; + + windowProxy.scrollTo = function() { + invokeNotImplementedOnHost_('windowProxy.scrollTo'); + }; + + windowProxy.setInterval = function() { + invokeNotImplementedOnHost_('windowProxy.setInterval'); + }; + + windowProxy.setTimeout = function() { + invokeNotImplementedOnHost_('windowProxy.setTimeout'); + }; + + // Define window object properties. + // The current window. + windowProxy.self = windowProxy; + // The topmost browser window. + windowProxy.top = windowProxy; + + // Provide proxy document which supplies one method, document.write(). + windowProxy.document = {}; + windowProxy.document.title = ''; + windowProxy.document.write = function(html) { + invokeOnHost_({'command': 'window.document.write', + 'html': html, + 'target': target}); + }; + + windowProxy.document.open = function() { + // The open() method should open an output stream to collect the output + // from any document.write() or document.writeln() methods. + invokeNotImplementedOnHost_('windowProxy.document.open'); + }; + + windowProxy.document.close = function() { + // The close() method should close the output stream previously opened + // with the document.open() method, and displays the collected data in + // this process. + invokeNotImplementedOnHost_('windowProxy.document.close'); + }; + + windowProxy.location = {}; + windowProxy.location.assign = function(url) { + windowProxy.location = url; + }; + // Watch assignments to window.location and window.location.href. + // Invoke equivalent method in ObjC code. + var onWindowProxyLocationChange = function(prop, oldVal, newVal) { + invokeOnHost_({'command': 'window.location', + 'value': __gCrWeb['getFullyQualifiedURL'](newVal), + 'target': target}); + return newVal; + }; + objectWatch(windowProxy, 'location', onWindowProxyLocationChange); + objectWatch(windowProxy.location, 'href', onWindowProxyLocationChange); + windowProxy.closed = false; + + return windowProxy; + }; + + // Intercept window.open calls. + window.open = function(url, target, features) { + if (target == '_parent' || target == '_self' || target == '_top') { + return __gCrWeb['originalWindowOpen'].call(window, url, target, features); + } + + // Because of the difficulty of returning data from JS->ObjC calls, in the + // event of a blank window name the JS side chooses a pseudo-GUID to + // use as the window name which is passed to ObjC and mapped to the real + // Tab there. + var isTargetBlank = (typeof target == 'undefined' || target == '_blank' || + target == '' || target == null); + if (isTargetBlank) { + target = '' + Date.now() + '-' + Math.random(); + } + + if (typeof(url) == 'undefined') { + // W3C recommended behavior. + url = 'about:blank'; + } + + invokeOnHost_({ + 'command': 'window.open', + 'target': target, + 'url': url, + 'referrerPolicy': __gCrWeb.getPageReferrerPolicy() + }); + + // Create a new |windowProxy| if none already exists with |target| as its + // name. + var windowProxy; + if (openedWindows.hasOwnProperty(target)) { + windowProxy = openedWindows[target]; + } else { + windowProxy = createWindowProxy_(target); + openedWindows[target] = windowProxy; + } + return windowProxy; + }; + +} // End of anonymous object diff --git a/ios/web/web_state/js/resources/window_open_wk.js b/ios/web/web_state/js/resources/window_open_wk.js new file mode 100644 index 0000000..209628c --- /dev/null +++ b/ios/web/web_state/js/resources/window_open_wk.js @@ -0,0 +1,15 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +goog.provide('__crweb.window_open_wk'); + +// WKWebView natively supports window.open so there is no need to install +// JavaScript-based fix. CRWJSWindowOpenManager always injected as multiple +// JS managers depend on it. The script does not have any content except +// presenceBeacon. + + +// Namespace for this module. +__gCrWeb.windowOpen = {}; + |