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') {
115 'command': 'window.error',
116 'message': 'iframe contentWindow.document is not accessible.'});
117 return currentElement;
119 var framePosition = getPositionInWindow(currentElement);
120 coordinates.viewPortX -=
121 framePosition.x - coordinates.window.pageXOffset;
122 coordinates.viewPortY -=
123 framePosition.y - coordinates.window.pageYOffset;
124 coordinates.window = currentElement.contentWindow;
125 coordinates.x -= framePosition.x + coordinates.window.pageXOffset;
126 coordinates.y -= framePosition.y + coordinates.window.pageYOffset;
127 return elementsFromCoordinates(coordinates);
129 return currentElement;
132 return elementsFromCoordinates(newCoordinate(x, y));
135 var spiralCoordinates = function(x, y) {
136 var coordinates = [];
138 var maxAngle = Math.PI * 2.0 * 3.0;
140 var angleStep = maxAngle / pointCount;
141 var speed = touchMargin_ / maxAngle;
143 for (var index = 0; index < pointCount; index++) {
144 var angle = angleStep * index;
145 var radius = angle * speed;
147 coordinates.push({x: x + Math.round(Math.cos(angle) * radius),
148 y: y + Math.round(Math.sin(angle) * radius)});
154 // Returns the url of the image or link under the selected point. Returns an
155 // empty string if no links or images are found.
156 __gCrWeb['getElementFromPoint'] = function(x, y) {
157 var hitCoordinates = spiralCoordinates(x, y);
158 for (var index = 0; index < hitCoordinates.length; index++) {
159 var coordinates = hitCoordinates[index];
161 var element = elementFromPoint_(coordinates.x, coordinates.y);
162 if (!element || !element.tagName) {
163 // Nothing under the hit point. Try the next hit point.
167 if (getComputedWebkitTouchCallout_(element) === 'none')
169 // Also check element's ancestors. A bound on the level is used here to
170 // avoid large overhead when no links or images are found.
172 while (++level < 8 && element && element != document) {
173 var tagName = element.tagName;
176 tagName = tagName.toLowerCase();
178 if (tagName === 'input' || tagName === 'textarea' ||
179 tagName === 'select' || tagName === 'option') {
180 // If the element is a known input element, stop the spiral search and
181 // return empty results.
185 if (tagName === 'a' && element.href) {
187 return __gCrWeb.common.JSONStringify(
189 referrerPolicy: getReferrerPolicy_(element)});
192 if (tagName === 'img' && element.src) {
194 var result = {src: element.src,
195 referrerPolicy: getReferrerPolicy_()};
196 // Copy the title, if any.
198 result.title = element.title;
200 // Check if the image is also a link.
201 var parent = element.parentNode;
203 if (parent.tagName &&
204 parent.tagName.toLowerCase() === 'a' &&
206 // This regex identifies strings like void(0),
207 // void(0) ;void(0);, ;;;;
208 // which result in a NOP when executed as JavaScript.
209 var regex = RegExp("^javascript:(?:(?:void\\(0\\)|;)\\s*)+$");
210 if (parent.href.match(regex)) {
211 parent = parent.parentNode;
214 result.href = parent.href;
215 result.referrerPolicy = getReferrerPolicy_(parent);
218 parent = parent.parentNode;
220 return __gCrWeb.common.JSONStringify(result);
222 element = element.parentNode;
228 // Returns true if the top window or any frames inside contain an input
229 // field of type 'password'.
230 __gCrWeb['hasPasswordField'] = function() {
231 return hasPasswordField_(window);
234 // Returns a string that is formatted according to the JSON syntax rules.
235 // This is equivalent to the built-in JSON.stringify() function, but is
236 // less likely to be overridden by the website itself. This public function
237 // should not be used if spoofing it would create a security vulnerability.
238 // The |__gCrWeb| object itself does not use it; it uses its private
239 // counterpart instead.
240 // Prevents websites from changing stringify's behavior by adding the
241 // method toJSON() by temporarily removing it.
242 __gCrWeb['stringify'] = function(value) {
245 if (value === undefined)
247 if (typeof(value.toJSON) == 'function') {
248 var originalToJSON = value.toJSON;
249 value.toJSON = undefined;
250 var stringifiedValue = __gCrWeb.common.JSONStringify(value);
251 value.toJSON = originalToJSON;
252 return stringifiedValue;
254 return __gCrWeb.common.JSONStringify(value);
258 * Adds the listeners that are used to handle forms, enabling autofill and
259 * the replacement method to dismiss the keyboard needed because of the
260 * Autofill keyboard accessory.
262 function addFormEventListeners_() {
263 // Focus and input events for form elements are messaged to the main
264 // application for broadcast to CRWWebControllerObservers.
265 // This is done with a single event handler for each type being added to the
266 // main document element which checks the source element of the event; this
267 // is much easier to manage than adding handlers to individual elements.
268 var formActivity = function(evt) {
269 var srcElement = evt.srcElement;
270 var fieldName = srcElement.name || '';
271 var value = srcElement.value || '';
274 'command': 'form.activity',
275 'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement.form),
276 'fieldName': fieldName,
281 msg.keyCode = evt.keyCode;
285 // Focus events performed on the 'capture' phase otherwise they are often
287 document.addEventListener('focus', formActivity, true);
288 document.addEventListener('blur', formActivity, true);
289 document.addEventListener('change', formActivity, true);
291 // Text input is watched at the bubbling phase as this seems adequate in
292 // practice and it is less obtrusive to page scripts than capture phase.
293 document.addEventListener('input', formActivity, false);
294 document.addEventListener('keyup', formActivity, false);
297 // Returns true if the supplied window or any frames inside contain an input
298 // field of type 'password'.
300 var hasPasswordField_ = function(win) {
301 var doc = win.document;
303 // We may will not be allowed to read the 'document' property from a frame
304 // that is in a different domain.
309 if (doc.querySelector('input[type=password]')) {
313 var frames = win.frames;
314 for (var i = 0; i < frames.length; i++) {
315 if (hasPasswordField_(frames[i])) {
323 function invokeOnHost_(command) {
324 __gCrWeb.message.invokeOnHost(command);
327 function invokeOnHostImmediate_(command) {
328 __gCrWeb.message.invokeOnHostImmediate(command);
332 * Gets the referrer policy to use for navigations away from the current page.
333 * If a link element is passed, and it includes a rel=noreferrer tag, that
334 * will override the page setting.
335 * @param {HTMLElement=} opt_linkElement The link triggering the navigation.
336 * @return {string} The policy string.
339 var getReferrerPolicy_ = function(opt_linkElement) {
340 if (opt_linkElement) {
341 var rel = opt_linkElement.getAttribute('rel');
342 if (rel && rel.toLowerCase() == 'noreferrer') {
347 var metaTags = document.getElementsByTagName('meta');
348 for (var i = 0; i < metaTags.length; ++i) {
349 if (metaTags[i].name.toLowerCase() == 'referrer') {
350 return metaTags[i].content.toLowerCase();
356 // Provides a way for other injected javascript to access the page's referrer
358 __gCrWeb['getPageReferrerPolicy'] = function() {
359 return getReferrerPolicy_();
362 // Various aspects of global DOM behavior are overridden here.
364 // A popstate event needs to be fired anytime the active history entry
365 // changes. Either via back, forward, go navigation or by loading the URL,
366 // clicking on a link, etc.
367 __gCrWeb['dispatchPopstateEvent'] = function(stateObject) {
368 var popstateEvent = window.document.createEvent('HTMLEvents');
369 popstateEvent.initEvent('popstate', true, false);
371 popstateEvent.state = JSON.parse(stateObject);
373 // setTimeout() is used in order to return immediately. Otherwise the
374 // dispatchEvent call waits for all event handlers to return, which could
375 // cause a ReentryGuard failure.
376 window.setTimeout(function() {
377 window.dispatchEvent(popstateEvent);
381 // Keep the original replaceState() method. It's needed to update UIWebView's
382 // URL and window.history.state property during history navigations that don't
383 // cause a page load.
384 var originalWindowHistoryReplaceState = window.history.replaceState;
385 __gCrWeb['replaceWebViewURL'] = function(url, stateObject) {
386 originalWindowHistoryReplaceState.call(history, stateObject, '', url);
389 // Intercept window.history methods to call back/forward natively.
390 window.history.back = function() {
391 invokeOnHost_({'command': 'window.history.back'});
393 window.history.forward = function() {
394 invokeOnHost_({'command': 'window.history.forward'});
396 window.history.go = function(delta) {
397 invokeOnHost_({'command': 'window.history.go', 'value': delta});
399 window.history.pushState = function(stateObject, pageTitle, pageUrl) {
400 __gCrWeb.core_dynamic.historyWillChangeState();
401 // Calling stringify() on undefined causes a JSON parse error.
402 var serializedState =
403 typeof(stateObject) == 'undefined' ? '' :
404 __gCrWeb.common.JSONStringify(stateObject);
405 pageUrl = pageUrl || window.location.href;
406 originalWindowHistoryReplaceState.call(history, stateObject, '', pageUrl);
407 invokeOnHost_({'command': 'window.history.didPushState',
408 'stateObject': serializedState,
409 'baseUrl': document.baseURI,
410 'pageUrl': pageUrl.toString()});
412 window.history.replaceState = function(stateObject, pageTitle, pageUrl) {
413 __gCrWeb.core_dynamic.historyWillChangeState();
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();
649 // Form prototype loaded with event to supply Autocomplete API
651 HTMLFormElement.prototype.requestAutocomplete = function() {
653 {'command': 'form.requestAutocomplete',
654 'formName': __gCrWeb.common.getFormIdentifier(this)});
656 } // End of anonymous object