Fix breakages in https://codereview.chromium.org/1155713003/
[chromium-blink-merge.git] / ios / web / web_state / js / resources / core.js
blobe2cb58bb19dc70a966ff36198c1e5397baf69f02
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 new 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           invokeOnHost_({
115               'command': 'window.error',
116               'message': 'iframe contentWindow.document is not accessible.'});
117           return currentElement;
118         }
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);
128       }
129       return currentElement;
130     };
132     return elementsFromCoordinates(newCoordinate(x, y));
133   };
135   var spiralCoordinates = function(x, y) {
136     var coordinates = [];
138     var maxAngle = Math.PI * 2.0 * 3.0;
139     var pointCount = 30;
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)});
149     }
151     return coordinates;
152   };
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.
164         continue;
165       }
167       if (getComputedWebkitTouchCallout_(element) === 'none')
168         continue;
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.
171       var level = 0;
172       while (++level < 8 && element && element != document) {
173         var tagName = element.tagName;
174         if (!tagName)
175           continue;
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.
182           return '{}';
183         }
185         if (tagName === 'a' && element.href) {
186           // Found a link.
187           return __gCrWeb.common.JSONStringify(
188               {href: element.href,
189                referrerPolicy: getReferrerPolicy_(element)});
190         }
192         if (tagName === 'img' && element.src) {
193           // Found an image.
194           var result = {src: element.src,
195                         referrerPolicy: getReferrerPolicy_()};
196           // Copy the title, if any.
197           if (element.title) {
198             result.title = element.title;
199           }
200           // Check if the image is also a link.
201           var parent = element.parentNode;
202           while (parent) {
203             if (parent.tagName &&
204                 parent.tagName.toLowerCase() === 'a' &&
205                 parent.href) {
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;
212                 continue;
213               }
214               result.href = parent.href;
215               result.referrerPolicy = getReferrerPolicy_(parent);
216               break;
217             }
218             parent = parent.parentNode;
219           }
220           return __gCrWeb.common.JSONStringify(result);
221         }
222         element = element.parentNode;
223       }
224     }
225     return '{}';
226   };
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);
232   };
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) {
243     if (value === null)
244       return 'null';
245     if (value === undefined)
246       return 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;
253     }
254     return __gCrWeb.common.JSONStringify(value);
255   };
257   /*
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.
261    */
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 || '';
273       var msg = {
274         'command': 'form.activity',
275         'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement.form),
276         'fieldName': fieldName,
277         'type': evt.type,
278         'value': value
279       };
280       if (evt.keyCode)
281         msg.keyCode = evt.keyCode;
282       invokeOnHost_(msg);
283     };
285     // Focus events performed on the 'capture' phase otherwise they are often
286     // not received.
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);
295   };
297   // Returns true if the supplied window or any frames inside contain an input
298   // field of type 'password'.
299   // @private
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.
305     if (!doc) {
306       return false;
307     }
309     if (doc.querySelector('input[type=password]')) {
310       return true;
311     }
313     var frames = win.frames;
314     for (var i = 0; i < frames.length; i++) {
315       if (hasPasswordField_(frames[i])) {
316         return true;
317       }
318     }
320     return false;
321   };
323   function invokeOnHost_(command) {
324     __gCrWeb.message.invokeOnHost(command);
325   };
327   function invokeOnHostImmediate_(command) {
328     __gCrWeb.message.invokeOnHostImmediate(command);
329   };
331   /**
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.
337    * @private
338    */
339   var getReferrerPolicy_ = function(opt_linkElement) {
340     if (opt_linkElement) {
341       var rel = opt_linkElement.getAttribute('rel');
342       if (rel && rel.toLowerCase() == 'noreferrer') {
343         return 'never';
344       }
345     }
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();
351       }
352     }
353     return 'default';
354   };
356   // Provides a way for other injected javascript to access the page's referrer
357   // policy.
358   __gCrWeb['getPageReferrerPolicy'] = function() {
359     return getReferrerPolicy_();
360   };
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);
370     if (stateObject)
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);
378     }, 0);
379   };
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);
387   };
389   // Intercept window.history methods to call back/forward natively.
390   window.history.back = function() {
391     invokeOnHost_({'command': 'window.history.back'});
392   };
393   window.history.forward = function() {
394     invokeOnHost_({'command': 'window.history.forward'});
395   };
396   window.history.go = function(delta) {
397     invokeOnHost_({'command': 'window.history.go', 'value': delta});
398   };
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()});
411   };
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()});
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();
649   // Form prototype loaded with event to supply Autocomplete API
650   // functionality.
651   HTMLFormElement.prototype.requestAutocomplete = function() {
652     invokeOnHost_(
653          {'command': 'form.requestAutocomplete',
654          'formName': __gCrWeb.common.getFormIdentifier(this)});
655   };
656 }  // End of anonymous object