1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 // This file adheres to closure-compiler conventions in order to enable
6 // compilation with ADVANCED_OPTIMIZATIONS. In particular, members that are to
7 // be accessed externally should be specified in this['style'] as opposed to
8 // this.style because member identifiers are minified by default.
9 // See http://goo.gl/FwOgy
11 goog.provide('__crWeb.core');
13 goog.require('__crWeb.common');
14 goog.require('__crWeb.coreDynamic');
15 goog.require('__crWeb.message');
18 * The Chrome object is populated in an anonymous object defined at
19 * initialization to prevent polluting the global namespace.
22 /* Beginning of anonymous object. */
24 // TODO(jimblackler): use this namespace as a wrapper for all externally-
25 // visible functions, to be consistent with other JS scripts. crbug.com/380390
26 __gCrWeb['core'] = {};
28 // JavaScript errors are logged on the main application side. The handler is
29 // added ASAP to catch any errors in startup. Note this does not appear to
31 window.addEventListener('error', function(event) {
32 // Sadly, event.filename and event.lineno are always 'undefined' and '0'
34 invokeOnHost_({'command': 'window.error',
35 'message': event.message.toString()});
39 * Margin in points around touchable elements (e.g. links for custom context
43 var touchMargin_ = 25;
45 __gCrWeb['getPageWidth'] = function() {
46 var documentElement = document.documentElement;
47 var documentBody = document.body;
48 return Math.max(documentElement.clientWidth,
49 documentElement.scrollWidth,
50 documentElement.offsetWidth,
51 documentBody.scrollWidth,
52 documentBody.offsetWidth);
55 // Implementation of document.elementFromPoint that is working for iOS4 and
56 // iOS5 and that also goes into frames and iframes.
57 var elementFromPoint_ = function(x, y) {
58 var elementFromPointIsUsingViewPortCoordinates = function(win) {
59 if (win.pageYOffset > 0) { // Page scrolled down.
60 return (win.document.elementFromPoint(
61 0, win.pageYOffset + win.innerHeight - 1) === null);
63 if (win.pageXOffset > 0) { // Page scrolled to the right.
64 return (win.document.elementFromPoint(
65 win.pageXOffset + win.innerWidth - 1, 0) === null);
67 return false; // No scrolling, don't care.
70 var newCoordinate = function(x, y) {
73 viewPortX: x - window.pageXOffset, viewPortY: y - window.pageYOffset,
74 useViewPortCoordinates: false,
80 // Returns the coordinates of the upper left corner of |obj| in the
81 // coordinates of the window that |obj| is in.
82 var getPositionInWindow = function(obj) {
83 var coord = { x: 0, y: 0 };
84 while (obj.offsetParent) {
85 coord.x += obj.offsetLeft;
86 coord.y += obj.offsetTop;
87 obj = obj.offsetParent;
92 var elementsFromCoordinates = function(coordinates) {
93 coordinates.useViewPortCoordinates = coordinates.useViewPortCoordinates ||
94 elementFromPointIsUsingViewPortCoordinates(coordinates.window);
96 var currentElement = null;
97 if (coordinates.useViewPortCoordinates) {
98 currentElement = coordinates.window.document.elementFromPoint(
99 coordinates.viewPortX, coordinates.viewPortY);
101 currentElement = coordinates.window.document.elementFromPoint(
102 coordinates.x, coordinates.y);
104 // We have to check for tagName, because if a selection is made by the
105 // UIWebView, the element we will get won't have one.
106 if (!currentElement || !currentElement.tagName) {
109 if (currentElement.tagName.toLowerCase() === 'iframe' ||
110 currentElement.tagName.toLowerCase() === 'frame') {
111 // The following condition is true if the iframe is in a different
112 // domain; no further information is accessible.
113 if (typeof(currentElement.contentWindow.document) == 'undefined') {
114 return currentElement;
116 var framePosition = getPositionInWindow(currentElement);
117 coordinates.viewPortX -=
118 framePosition.x - coordinates.window.pageXOffset;
119 coordinates.viewPortY -=
120 framePosition.y - coordinates.window.pageYOffset;
121 coordinates.window = currentElement.contentWindow;
122 coordinates.x -= framePosition.x + coordinates.window.pageXOffset;
123 coordinates.y -= framePosition.y + coordinates.window.pageYOffset;
124 return elementsFromCoordinates(coordinates);
126 return currentElement;
129 return elementsFromCoordinates(newCoordinate(x, y));
132 var spiralCoordinates = function(x, y) {
133 var coordinates = [];
135 var maxAngle = Math.PI * 2.0 * 3.0;
137 var angleStep = maxAngle / pointCount;
138 var speed = touchMargin_ / maxAngle;
140 for (var index = 0; index < pointCount; index++) {
141 var angle = angleStep * index;
142 var radius = angle * speed;
144 coordinates.push({x: x + Math.round(Math.cos(angle) * radius),
145 y: y + Math.round(Math.sin(angle) * radius)});
151 // Returns the url of the image or link under the selected point. Returns an
152 // empty string if no links or images are found.
153 __gCrWeb['getElementFromPoint'] = function(x, y) {
154 var hitCoordinates = spiralCoordinates(x, y);
155 for (var index = 0; index < hitCoordinates.length; index++) {
156 var coordinates = hitCoordinates[index];
158 var element = elementFromPoint_(coordinates.x, coordinates.y);
159 if (!element || !element.tagName) {
160 // Nothing under the hit point. Try the next hit point.
164 if (getComputedWebkitTouchCallout_(element) === 'none')
166 // Also check element's ancestors. A bound on the level is used here to
167 // avoid large overhead when no links or images are found.
169 while (++level < 8 && element && element != document) {
170 var tagName = element.tagName;
173 tagName = tagName.toLowerCase();
175 if (tagName === 'input' || tagName === 'textarea' ||
176 tagName === 'select' || tagName === 'option') {
177 // If the element is a known input element, stop the spiral search and
178 // return empty results.
182 if (tagName === 'a' && element.href) {
184 return __gCrWeb.common.JSONStringify(
186 referrerPolicy: getReferrerPolicy_(element)});
189 if (tagName === 'img' && element.src) {
191 var result = {src: element.src,
192 referrerPolicy: getReferrerPolicy_()};
193 // Copy the title, if any.
195 result.title = element.title;
197 // Check if the image is also a link.
198 var parent = element.parentNode;
200 if (parent.tagName &&
201 parent.tagName.toLowerCase() === 'a' &&
203 // This regex identifies strings like void(0),
204 // void(0) ;void(0);, ;;;;
205 // which result in a NOP when executed as JavaScript.
206 var regex = RegExp("^javascript:(?:(?:void\\(0\\)|;)\\s*)+$");
207 if (parent.href.match(regex)) {
208 parent = parent.parentNode;
211 result.href = parent.href;
212 result.referrerPolicy = getReferrerPolicy_(parent);
215 parent = parent.parentNode;
217 return __gCrWeb.common.JSONStringify(result);
219 element = element.parentNode;
225 // Returns true if the top window or any frames inside contain an input
226 // field of type 'password'.
227 __gCrWeb['hasPasswordField'] = function() {
228 return hasPasswordField_(window);
231 // Returns a string that is formatted according to the JSON syntax rules.
232 // This is equivalent to the built-in JSON.stringify() function, but is
233 // less likely to be overridden by the website itself. This public function
234 // should not be used if spoofing it would create a security vulnerability.
235 // The |__gCrWeb| object itself does not use it; it uses its private
236 // counterpart instead.
237 // Prevents websites from changing stringify's behavior by adding the
238 // method toJSON() by temporarily removing it.
239 __gCrWeb['stringify'] = function(value) {
242 if (value === undefined)
244 if (typeof(value.toJSON) == 'function') {
245 var originalToJSON = value.toJSON;
246 value.toJSON = undefined;
247 var stringifiedValue = __gCrWeb.common.JSONStringify(value);
248 value.toJSON = originalToJSON;
249 return stringifiedValue;
251 return __gCrWeb.common.JSONStringify(value);
255 * Adds the listeners that are used to handle forms, enabling autofill and
256 * the replacement method to dismiss the keyboard needed because of the
257 * Autofill keyboard accessory.
259 function addFormEventListeners_() {
260 // Focus and input events for form elements are messaged to the main
261 // application for broadcast to CRWWebControllerObservers.
262 // This is done with a single event handler for each type being added to the
263 // main document element which checks the source element of the event; this
264 // is much easier to manage than adding handlers to individual elements.
265 var formActivity = function(evt) {
266 var srcElement = evt.srcElement;
267 var fieldName = srcElement.name || '';
268 var value = srcElement.value || '';
271 'command': 'form.activity',
272 'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement.form),
273 'fieldName': fieldName,
278 msg.keyCode = evt.keyCode;
282 // Focus events performed on the 'capture' phase otherwise they are often
284 document.addEventListener('focus', formActivity, true);
285 document.addEventListener('blur', formActivity, true);
286 document.addEventListener('change', formActivity, true);
288 // Text input is watched at the bubbling phase as this seems adequate in
289 // practice and it is less obtrusive to page scripts than capture phase.
290 document.addEventListener('input', formActivity, false);
291 document.addEventListener('keyup', formActivity, false);
294 // Returns true if the supplied window or any frames inside contain an input
295 // field of type 'password'.
297 var hasPasswordField_ = function(win) {
298 var doc = win.document;
300 // We may will not be allowed to read the 'document' property from a frame
301 // that is in a different domain.
306 if (doc.querySelector('input[type=password]')) {
310 var frames = win.frames;
311 for (var i = 0; i < frames.length; i++) {
312 if (hasPasswordField_(frames[i])) {
320 function invokeOnHost_(command) {
321 __gCrWeb.message.invokeOnHost(command);
324 function invokeOnHostImmediate_(command) {
325 __gCrWeb.message.invokeOnHostImmediate(command);
329 * Gets the referrer policy to use for navigations away from the current page.
330 * If a link element is passed, and it includes a rel=noreferrer tag, that
331 * will override the page setting.
332 * @param {HTMLElement=} opt_linkElement The link triggering the navigation.
333 * @return {string} The policy string.
336 var getReferrerPolicy_ = function(opt_linkElement) {
337 if (opt_linkElement) {
338 var rel = opt_linkElement.getAttribute('rel');
339 if (rel && rel.toLowerCase() == 'noreferrer') {
344 var metaTags = document.getElementsByTagName('meta');
345 for (var i = 0; i < metaTags.length; ++i) {
346 if (metaTags[i].name.toLowerCase() == 'referrer') {
347 return metaTags[i].content.toLowerCase();
353 // Provides a way for other injected javascript to access the page's referrer
355 __gCrWeb['getPageReferrerPolicy'] = function() {
356 return getReferrerPolicy_();
359 // Various aspects of global DOM behavior are overridden here.
361 // A popstate event needs to be fired anytime the active history entry
362 // changes. Either via back, forward, go navigation or by loading the URL,
363 // clicking on a link, etc.
364 __gCrWeb['dispatchPopstateEvent'] = function(stateObject) {
365 var popstateEvent = window.document.createEvent('HTMLEvents');
366 popstateEvent.initEvent('popstate', true, false);
368 popstateEvent.state = JSON.parse(stateObject);
370 // setTimeout() is used in order to return immediately. Otherwise the
371 // dispatchEvent call waits for all event handlers to return, which could
372 // cause a ReentryGuard failure.
373 window.setTimeout(function() {
374 window.dispatchEvent(popstateEvent);
378 // Keep the original replaceState() method. It's needed to update UIWebView's
379 // URL and window.history.state property during history navigations that don't
380 // cause a page load.
381 var originalWindowHistoryReplaceState = window.history.replaceState;
382 __gCrWeb['replaceWebViewURL'] = function(url, stateObject) {
383 originalWindowHistoryReplaceState.call(history, stateObject, '', url);
386 // Intercept window.history methods to call back/forward natively.
387 window.history.back = function() {
388 invokeOnHost_({'command': 'window.history.back'});
390 window.history.forward = function() {
391 invokeOnHost_({'command': 'window.history.forward'});
393 window.history.go = function(delta) {
394 invokeOnHost_({'command': 'window.history.go', 'value': delta});
396 window.history.pushState = function(stateObject, pageTitle, pageUrl) {
397 __gCrWeb.message.invokeOnHost(
398 {'command': 'window.history.willChangeState'});
399 // Calling stringify() on undefined causes a JSON parse error.
400 var serializedState =
401 typeof(stateObject) == 'undefined' ? '' :
402 __gCrWeb.common.JSONStringify(stateObject);
403 pageUrl = pageUrl || window.location.href;
404 originalWindowHistoryReplaceState.call(history, stateObject, '', pageUrl);
405 invokeOnHost_({'command': 'window.history.didPushState',
406 'stateObject': serializedState,
407 'baseUrl': document.baseURI,
408 'pageUrl': pageUrl.toString()});
410 window.history.replaceState = function(stateObject, pageTitle, pageUrl) {
411 __gCrWeb.message.invokeOnHost(
412 {'command': 'window.history.willChangeState'});
414 // Calling stringify() on undefined causes a JSON parse error.
415 var serializedState =
416 typeof(stateObject) == 'undefined' ? '' :
417 __gCrWeb.common.JSONStringify(stateObject);
418 pageUrl = pageUrl || window.location.href;
419 originalWindowHistoryReplaceState.call(history, stateObject, '', pageUrl);
420 invokeOnHost_({'command': 'window.history.didReplaceState',
421 'stateObject': serializedState,
422 'baseUrl': document.baseURI,
423 'pageUrl': pageUrl.toString()});
426 __gCrWeb['getFullyQualifiedURL'] = function(originalURL) {
427 // A dummy anchor (never added to the document) is used to obtain the
428 // fully-qualified URL of |originalURL|.
429 var anchor = document.createElement('a');
430 anchor.href = originalURL;
434 // Intercept window.close calls.
435 window.close = function() {
436 invokeOnHost_({'command': 'window.close.self'});
439 window.addEventListener('hashchange', function(evt) {
440 invokeOnHost_({'command': 'window.hashchange'});
443 __gCrWeb.core_dynamic.addEventListeners();
445 // Returns if a frame with |name| is found in |currentWindow|.
446 // Note frame.name is undefined for cross domain frames.
447 var hasFrame_ = function(currentWindow, name) {
448 if (currentWindow.name === name)
451 var frames = currentWindow.frames;
452 for (var index = 0; index < frames.length; ++index) {
453 var frame = frames[index];
454 if (frame === undefined)
456 if (hasFrame_(frame, name))
462 // Checks if |node| is an anchor to be opened in the current tab.
463 var isInternaLink_ = function(node) {
464 if (!(node instanceof HTMLAnchorElement))
467 // Anchor with href='javascript://.....' will be opened in the current tab
469 if (node.href.indexOf('javascript:') == 0)
472 // UIWebView will take care of the following cases.
474 // - If the given browsing context name is the empty string or '_self', then
475 // the chosen browsing context must be the current one.
477 // - If the given browsing context name is '_parent', then the chosen
478 // browsing context must be the parent browsing context of the current
479 // one, unless there is no one, in which case the chosen browsing context
480 // must be the current browsing context.
482 // - If the given browsing context name is '_top', then the chosen browsing
483 // context must be the top-level browsing context of the current one, if
484 // there is one, or else the current browsing context.
486 // Here an undefined target is considered in the same way as an empty
488 if (node.target === undefined || node.target === '' ||
489 node.target === '_self' || node.target === '_parent' ||
490 node.target === '_top') {
494 // A new browsing context is being requested for an '_blank' target.
495 if (node.target === '_blank')
498 // Otherwise UIWebView will take care of the case where there exists a
499 // browsing context whose name is the same as the given browsing context
500 // name. If there is no such a browsing context, a new browsing context is
502 return hasFrame_(window, node.target);
505 var getTargetLink_ = function(target) {
507 // Find the closest ancester that is a link.
509 if (node instanceof HTMLAnchorElement)
511 node = node.parentNode;
516 var setExternalRequest_ = function(href, target) {
517 if (typeof(target) == 'undefined' || target == '_blank' || target == '') {
518 target = '' + Date.now() + '-' + Math.random();
520 if (typeof(href) == 'undefined') {
521 // W3C recommended behavior.
522 href = 'about:blank';
524 // ExternalRequest messages need to be handled before the expected
525 // shouldStartLoadWithRequest, as such we cannot wait for the regular
526 // message queue invoke which delays to avoid illegal recursion into
527 // UIWebView. This immediate class of messages is handled ASAP by
529 invokeOnHostImmediate_({'command': 'externalRequest',
532 'referrerPolicy': getReferrerPolicy_()});
535 var resetExternalRequest_ = function() {
536 invokeOnHost_({'command': 'resetExternalRequest'});
539 var clickBubbleListener_ = function(evt) {
540 if (evt['defaultPrevented']) {
541 resetExternalRequest_();
543 // Remove the listener.
544 evt.currentTarget.removeEventListener(
545 'click', clickBubbleListener_, false);
548 var getComputedWebkitTouchCallout_ = function(element) {
549 return window.getComputedStyle(element, null)['webkitTouchCallout'];
553 * This method applies the various document-level overrides. Sometimes the
554 * document object gets reset in the early stages of the page lifecycle, so
555 * this is exposed as a method for the application to invoke later. That way
556 * the window-level overrides can be applied as soon as possible.
558 __gCrWeb.core.documentInject = function() {
559 // Perform web view specific operations requiring document.body presence.
560 // If necessary returns and waits for document to be present.
561 if (!__gCrWeb.core_dynamic.documentInject())
564 document.addEventListener('click', function(evt) {
565 var node = getTargetLink_(evt.target);
570 if (isInternaLink_(node)) {
573 setExternalRequest_(node.href, node.target);
574 // Add listener to the target and its immediate ancesters. These event
575 // listeners will be removed if they get called. The listeners for some
576 // elements might never be removed, but if multiple identical event
577 // listeners are registered on the same event target with the same
578 // parameters the duplicate instances are discarded.
579 for (var level = 0; level < 5; ++level) {
580 if (node && node != document) {
581 node.addEventListener('click', clickBubbleListener_, false);
582 node = node.parentNode;
589 // Intercept clicks on anchors (links) during bubbling phase so that the
590 // browser can handle target type appropriately.
591 document.addEventListener('click', function(evt) {
592 var node = getTargetLink_(evt.target);
597 if (isInternaLink_(node)) {
598 if (evt['defaultPrevented'])
600 // Internal link. The web view will handle navigation, but register
601 // the anchor for UIWebView to start the progress indicator ASAP and
602 // notify web controller as soon as possible of impending navigation.
603 if (__gCrWeb.core_dynamic.handleInternalClickEvent) {
604 __gCrWeb.core_dynamic.handleInternalClickEvent(node);
608 // Resets the external request if it has been canceled, otherwise
609 // updates the href in case it has been changed.
610 if (evt['defaultPrevented'])
611 resetExternalRequest_();
613 setExternalRequest_(node.href, node.target);
617 // Capture form submit actions.
618 document.addEventListener('submit', function(evt) {
619 if (evt['defaultPrevented'])
622 var form = evt.target;
623 var targetsFrame = form.target && hasFrame_(window, form.target);
624 // TODO(stuartmorgan): Handle external targets. crbug.com/233543
626 var action = form.getAttribute('action');
627 // Default action is to re-submit to same page.
629 action = document.location.href;
631 'command': 'document.submit',
632 'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement),
633 'href': __gCrWeb['getFullyQualifiedURL'](action),
634 'targetsFrame': targetsFrame
638 addFormEventListeners_();
640 // Handle or wait for and handle document load completion, if applicable.
641 if (__gCrWeb.core_dynamic.handleDocumentLoaded)
642 __gCrWeb.core_dynamic.handleDocumentLoaded();
647 __gCrWeb.core.documentInject();
648 }()); // End of anonymous object