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
.message
.invokeOnHost(
401 {'command': 'window.history.willChangeState'});
402 // Calling stringify() on undefined causes a JSON parse error.
403 var serializedState
=
404 typeof(stateObject
) == 'undefined' ? '' :
405 __gCrWeb
.common
.JSONStringify(stateObject
);
406 pageUrl
= pageUrl
|| window
.location
.href
;
407 originalWindowHistoryReplaceState
.call(history
, stateObject
, '', pageUrl
);
408 invokeOnHost_({'command': 'window.history.didPushState',
409 'stateObject': serializedState
,
410 'baseUrl': document
.baseURI
,
411 'pageUrl': pageUrl
.toString()});
413 window
.history
.replaceState = function(stateObject
, pageTitle
, pageUrl
) {
414 __gCrWeb
.message
.invokeOnHost(
415 {'command': 'window.history.willChangeState'});
417 // Calling stringify() on undefined causes a JSON parse error.
418 var serializedState
=
419 typeof(stateObject
) == 'undefined' ? '' :
420 __gCrWeb
.common
.JSONStringify(stateObject
);
421 pageUrl
= pageUrl
|| window
.location
.href
;
422 originalWindowHistoryReplaceState
.call(history
, stateObject
, '', pageUrl
);
423 invokeOnHost_({'command': 'window.history.didReplaceState',
424 'stateObject': serializedState
,
425 'baseUrl': document
.baseURI
,
426 'pageUrl': pageUrl
.toString()});
429 __gCrWeb
['getFullyQualifiedURL'] = function(originalURL
) {
430 // A dummy anchor (never added to the document) is used to obtain the
431 // fully-qualified URL of |originalURL|.
432 var anchor
= document
.createElement('a');
433 anchor
.href
= originalURL
;
437 // Intercept window.close calls.
438 window
.close = function() {
439 invokeOnHost_({'command': 'window.close.self'});
442 window
.addEventListener('hashchange', function(evt
) {
443 invokeOnHost_({'command': 'window.hashchange'});
446 __gCrWeb
.core_dynamic
.addEventListeners();
448 // Returns if a frame with |name| is found in |currentWindow|.
449 // Note frame.name is undefined for cross domain frames.
450 var hasFrame_ = function(currentWindow
, name
) {
451 if (currentWindow
.name
=== name
)
454 var frames
= currentWindow
.frames
;
455 for (var index
= 0; index
< frames
.length
; ++index
) {
456 var frame
= frames
[index
];
457 if (frame
=== undefined)
459 if (hasFrame_(frame
, name
))
465 // Checks if |node| is an anchor to be opened in the current tab.
466 var isInternaLink_ = function(node
) {
467 if (!(node
instanceof HTMLAnchorElement
))
470 // Anchor with href='javascript://.....' will be opened in the current tab
472 if (node
.href
.indexOf('javascript:') == 0)
475 // UIWebView will take care of the following cases.
477 // - If the given browsing context name is the empty string or '_self', then
478 // the chosen browsing context must be the current one.
480 // - If the given browsing context name is '_parent', then the chosen
481 // browsing context must be the parent browsing context of the current
482 // one, unless there is no one, in which case the chosen browsing context
483 // must be the current browsing context.
485 // - If the given browsing context name is '_top', then the chosen browsing
486 // context must be the top-level browsing context of the current one, if
487 // there is one, or else the current browsing context.
489 // Here an undefined target is considered in the same way as an empty
491 if (node
.target
=== undefined || node
.target
=== '' ||
492 node
.target
=== '_self' || node
.target
=== '_parent' ||
493 node
.target
=== '_top') {
497 // A new browsing context is being requested for an '_blank' target.
498 if (node
.target
=== '_blank')
501 // Otherwise UIWebView will take care of the case where there exists a
502 // browsing context whose name is the same as the given browsing context
503 // name. If there is no such a browsing context, a new browsing context is
505 return hasFrame_(window
, node
.target
);
508 var getTargetLink_ = function(target
) {
510 // Find the closest ancester that is a link.
512 if (node
instanceof HTMLAnchorElement
)
514 node
= node
.parentNode
;
519 var setExternalRequest_ = function(href
, target
) {
520 if (typeof(target
) == 'undefined' || target
== '_blank' || target
== '') {
521 target
= '' + Date
.now() + '-' + Math
.random();
523 if (typeof(href
) == 'undefined') {
524 // W3C recommended behavior.
525 href
= 'about:blank';
527 // ExternalRequest messages need to be handled before the expected
528 // shouldStartLoadWithRequest, as such we cannot wait for the regular
529 // message queue invoke which delays to avoid illegal recursion into
530 // UIWebView. This immediate class of messages is handled ASAP by
532 invokeOnHostImmediate_({'command': 'externalRequest',
535 'referrerPolicy': getReferrerPolicy_()});
538 var resetExternalRequest_ = function() {
539 invokeOnHost_({'command': 'resetExternalRequest'});
542 var clickBubbleListener_ = function(evt
) {
543 if (evt
['defaultPrevented']) {
544 resetExternalRequest_();
546 // Remove the listener.
547 evt
.currentTarget
.removeEventListener(
548 'click', clickBubbleListener_
, false);
551 var getComputedWebkitTouchCallout_ = function(element
) {
552 return window
.getComputedStyle(element
, null)['webkitTouchCallout'];
556 * This method applies the various document-level overrides. Sometimes the
557 * document object gets reset in the early stages of the page lifecycle, so
558 * this is exposed as a method for the application to invoke later. That way
559 * the window-level overrides can be applied as soon as possible.
561 __gCrWeb
.core
.documentInject = function() {
562 // Perform web view specific operations requiring document.body presence.
563 // If necessary returns and waits for document to be present.
564 if (!__gCrWeb
.core_dynamic
.documentInject())
567 document
.addEventListener('click', function(evt
) {
568 var node
= getTargetLink_(evt
.target
);
573 if (isInternaLink_(node
)) {
576 setExternalRequest_(node
.href
, node
.target
);
577 // Add listener to the target and its immediate ancesters. These event
578 // listeners will be removed if they get called. The listeners for some
579 // elements might never be removed, but if multiple identical event
580 // listeners are registered on the same event target with the same
581 // parameters the duplicate instances are discarded.
582 for (var level
= 0; level
< 5; ++level
) {
583 if (node
&& node
!= document
) {
584 node
.addEventListener('click', clickBubbleListener_
, false);
585 node
= node
.parentNode
;
592 // Intercept clicks on anchors (links) during bubbling phase so that the
593 // browser can handle target type appropriately.
594 document
.addEventListener('click', function(evt
) {
595 var node
= getTargetLink_(evt
.target
);
600 if (isInternaLink_(node
)) {
601 if (evt
['defaultPrevented'])
603 // Internal link. The web view will handle navigation, but register
604 // the anchor for UIWebView to start the progress indicator ASAP and
605 // notify web controller as soon as possible of impending navigation.
606 if (__gCrWeb
.core_dynamic
.handleInternalClickEvent
) {
607 __gCrWeb
.core_dynamic
.handleInternalClickEvent(node
);
611 // Resets the external request if it has been canceled, otherwise
612 // updates the href in case it has been changed.
613 if (evt
['defaultPrevented'])
614 resetExternalRequest_();
616 setExternalRequest_(node
.href
, node
.target
);
620 // Capture form submit actions.
621 document
.addEventListener('submit', function(evt
) {
622 if (evt
['defaultPrevented'])
625 var form
= evt
.target
;
626 var targetsFrame
= form
.target
&& hasFrame_(window
, form
.target
);
627 // TODO(stuartmorgan): Handle external targets. crbug.com/233543
629 var action
= form
.getAttribute('action');
630 // Default action is to re-submit to same page.
632 action
= document
.location
.href
;
634 'command': 'document.submit',
635 'formName': __gCrWeb
.common
.getFormIdentifier(evt
.srcElement
),
636 'href': __gCrWeb
['getFullyQualifiedURL'](action
),
637 'targetsFrame': targetsFrame
641 addFormEventListeners_();
643 // Handle or wait for and handle document load completion, if applicable.
644 if (__gCrWeb
.core_dynamic
.handleDocumentLoaded
)
645 __gCrWeb
.core_dynamic
.handleDocumentLoaded();
650 __gCrWeb
.core
.documentInject();
652 // Form prototype loaded with event to supply Autocomplete API
654 HTMLFormElement
.prototype.requestAutocomplete = function() {
656 {'command': 'form.requestAutocomplete',
657 'formName': __gCrWeb
.common
.getFormIdentifier(this)});
659 }()); // End of anonymous object