Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ios / web / web_state / js / resources / core.js
blob7c00525b7df913ee777dd642d1bbca7bf571a9fa
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');
17 /**
18  * The Chrome object is populated in an anonymous object defined at
19  * initialization to prevent polluting the global namespace.
20  */
22 /* Beginning of anonymous object. */
23 (function() {
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
30   // work in iOS < 5.
31   window.addEventListener('error', function(event) {
32     // Sadly, event.filename and event.lineno are always 'undefined' and '0'
33     // with UIWebView.
34     invokeOnHost_({'command': 'window.error',
35                    'message': event.message.toString()});
36   });
38   /**
39    * Margin in points around touchable elements (e.g. links for custom context
40    * menu).
41    * @type {number}
42    */
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);
53   };
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);
62       }
63       if (win.pageXOffset > 0) {  // Page scrolled to the right.
64         return (win.document.elementFromPoint(
65             win.pageXOffset + win.innerWidth - 1, 0) === null);
66       }
67       return false;  // No scrolling, don't care.
68     };
70     var newCoordinate = function(x, y) {
71       var coordinates = {
72           x: x, y: y,
73           viewPortX: x - window.pageXOffset, viewPortY: y - window.pageYOffset,
74           useViewPortCoordinates: false,
75           window: window
76       };
77       return coordinates;
78     };
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;
88       }
89       return coord;
90     };
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);
100       } else {
101         currentElement = coordinates.window.document.elementFromPoint(
102             coordinates.x, coordinates.y);
103       }
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) {
107         return null;
108       }
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;
115         }
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);
125       }
126       return currentElement;
127     };
129     return elementsFromCoordinates(newCoordinate(x, y));
130   };
132   var spiralCoordinates = function(x, y) {
133     var coordinates = [];
135     var maxAngle = Math.PI * 2.0 * 3.0;
136     var pointCount = 30;
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)});
146     }
148     return coordinates;
149   };
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.
161         continue;
162       }
164       if (getComputedWebkitTouchCallout_(element) === 'none')
165         continue;
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.
168       var level = 0;
169       while (++level < 8 && element && element != document) {
170         var tagName = element.tagName;
171         if (!tagName)
172           continue;
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.
179           return '{}';
180         }
182         if (tagName === 'a' && element.href) {
183           // Found a link.
184           return __gCrWeb.common.JSONStringify(
185               {href: element.href,
186                referrerPolicy: getReferrerPolicy_(element)});
187         }
189         if (tagName === 'img' && element.src) {
190           // Found an image.
191           var result = {src: element.src,
192                         referrerPolicy: getReferrerPolicy_()};
193           // Copy the title, if any.
194           if (element.title) {
195             result.title = element.title;
196           }
197           // Check if the image is also a link.
198           var parent = element.parentNode;
199           while (parent) {
200             if (parent.tagName &&
201                 parent.tagName.toLowerCase() === 'a' &&
202                 parent.href) {
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;
209                 continue;
210               }
211               result.href = parent.href;
212               result.referrerPolicy = getReferrerPolicy_(parent);
213               break;
214             }
215             parent = parent.parentNode;
216           }
217           return __gCrWeb.common.JSONStringify(result);
218         }
219         element = element.parentNode;
220       }
221     }
222     return '{}';
223   };
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);
229   };
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) {
240     if (value === null)
241       return 'null';
242     if (value === undefined)
243       return 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;
250     }
251     return __gCrWeb.common.JSONStringify(value);
252   };
254   /*
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.
258    */
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 || '';
270       var msg = {
271         'command': 'form.activity',
272         'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement.form),
273         'fieldName': fieldName,
274         'type': evt.type,
275         'value': value
276       };
277       if (evt.keyCode)
278         msg.keyCode = evt.keyCode;
279       invokeOnHost_(msg);
280     };
282     // Focus events performed on the 'capture' phase otherwise they are often
283     // not received.
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);
292   };
294   // Returns true if the supplied window or any frames inside contain an input
295   // field of type 'password'.
296   // @private
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.
302     if (!doc) {
303       return false;
304     }
306     if (doc.querySelector('input[type=password]')) {
307       return true;
308     }
310     var frames = win.frames;
311     for (var i = 0; i < frames.length; i++) {
312       if (hasPasswordField_(frames[i])) {
313         return true;
314       }
315     }
317     return false;
318   };
320   function invokeOnHost_(command) {
321     __gCrWeb.message.invokeOnHost(command);
322   };
324   function invokeOnHostImmediate_(command) {
325     __gCrWeb.message.invokeOnHostImmediate(command);
326   };
328   /**
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.
334    * @private
335    */
336   var getReferrerPolicy_ = function(opt_linkElement) {
337     if (opt_linkElement) {
338       var rel = opt_linkElement.getAttribute('rel');
339       if (rel && rel.toLowerCase() == 'noreferrer') {
340         return 'never';
341       }
342     }
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();
348       }
349     }
350     return 'default';
351   };
353   // Provides a way for other injected javascript to access the page's referrer
354   // policy.
355   __gCrWeb['getPageReferrerPolicy'] = function() {
356     return getReferrerPolicy_();
357   };
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);
367     if (stateObject)
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);
375     }, 0);
376   };
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);
384   };
386   // Intercept window.history methods to call back/forward natively.
387   window.history.back = function() {
388     invokeOnHost_({'command': 'window.history.back'});
389   };
390   window.history.forward = function() {
391     invokeOnHost_({'command': 'window.history.forward'});
392   };
393   window.history.go = function(delta) {
394     invokeOnHost_({'command': 'window.history.go', 'value': delta});
395   };
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()});
409   };
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()});
424   };
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;
431     return anchor.href;
432   };
434   // Intercept window.close calls.
435   window.close = function() {
436     invokeOnHost_({'command': 'window.close.self'});
437   };
439   window.addEventListener('hashchange', function(evt) {
440     invokeOnHost_({'command': 'window.hashchange'});
441   });
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)
449       return true;
451     var frames = currentWindow.frames;
452     for (var index = 0; index < frames.length; ++index) {
453       var frame = frames[index];
454       if (frame === undefined)
455         continue;
456       if (hasFrame_(frame, name))
457         return true;
458     }
459     return false;
460   };
462   // Checks if |node| is an anchor to be opened in the current tab.
463   var isInternaLink_ = function(node) {
464     if (!(node instanceof HTMLAnchorElement))
465       return false;
467     // Anchor with href='javascript://.....' will be opened in the current tab
468     // for simplicity.
469     if (node.href.indexOf('javascript:') == 0)
470       return true;
472     // UIWebView will take care of the following cases.
473     //
474     // - If the given browsing context name is the empty string or '_self', then
475     //   the chosen browsing context must be the current one.
476     //
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.
481     //
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.
485     //
486     // Here an undefined target is considered in the same way as an empty
487     // target.
488     if (node.target === undefined || node.target === '' ||
489         node.target === '_self' || node.target === '_parent' ||
490         node.target === '_top') {
491       return true;
492     }
494     // A new browsing context is being requested for an '_blank' target.
495     if (node.target === '_blank')
496       return false;
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
501     // being requested.
502     return hasFrame_(window, node.target);
503   };
505   var getTargetLink_ = function(target) {
506     var node = target;
507     // Find the closest ancester that is a link.
508     while (node) {
509       if (node instanceof HTMLAnchorElement)
510         break;
511       node = node.parentNode;
512     }
513     return node;
514   };
516   var setExternalRequest_ = function(href, target) {
517     if (typeof(target) == 'undefined' || target == '_blank' || target == '') {
518       target = '' + Date.now() + '-' + Math.random();
519     }
520     if (typeof(href) == 'undefined') {
521       // W3C recommended behavior.
522       href = 'about:blank';
523     }
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
528     // CRWWebController.
529     invokeOnHostImmediate_({'command': 'externalRequest',
530                                'href': href,
531                              'target': target,
532                      'referrerPolicy': getReferrerPolicy_()});
533   };
535   var resetExternalRequest_ = function() {
536     invokeOnHost_({'command': 'resetExternalRequest'});
537   };
539   var clickBubbleListener_ = function(evt) {
540     if (evt['defaultPrevented']) {
541       resetExternalRequest_();
542     }
543     // Remove the listener.
544     evt.currentTarget.removeEventListener(
545         'click', clickBubbleListener_, false);
546   };
548   var getComputedWebkitTouchCallout_ = function(element) {
549     return window.getComputedStyle(element, null)['webkitTouchCallout'];
550   };
552   /**
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.
557    */
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())
562       return;
564     document.addEventListener('click', function(evt) {
565       var node = getTargetLink_(evt.target);
567       if (!node)
568         return;
570       if (isInternaLink_(node)) {
571         return;
572       }
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;
583         } else {
584           break;
585         }
586       }
587     }, true);
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);
594       if (!node)
595         return;
597       if (isInternaLink_(node)) {
598         if (evt['defaultPrevented'])
599           return;
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);
605         }
606         return;
607       } else {
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_();
612         else
613           setExternalRequest_(node.href, node.target);
614       }
615     }, false);
617     // Capture form submit actions.
618     document.addEventListener('submit', function(evt) {
619       if (evt['defaultPrevented'])
620         return;
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.
628       if (!action)
629         action = document.location.href;
630       invokeOnHost_({
631                'command': 'document.submit',
632               'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement),
633                   'href': __gCrWeb['getFullyQualifiedURL'](action),
634           'targetsFrame': targetsFrame
635       });
636     }, false);
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();
644     return true;
645   };
647   __gCrWeb.core.documentInject();
648 }());  // End of anonymous object