Add ICU message format support
[chromium-blink-merge.git] / ios / web / web_state / js / resources / core.js
blob4ec337496c86b316360cafa42e0f18f2f1638075
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           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.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()});
412   };
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()});
427   };
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;
434     return anchor.href;
435   };
437   // Intercept window.close calls.
438   window.close = function() {
439     invokeOnHost_({'command': 'window.close.self'});
440   };
442   window.addEventListener('hashchange', function(evt) {
443     invokeOnHost_({'command': 'window.hashchange'});
444   });
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)
452       return true;
454     var frames = currentWindow.frames;
455     for (var index = 0; index < frames.length; ++index) {
456       var frame = frames[index];
457       if (frame === undefined)
458         continue;
459       if (hasFrame_(frame, name))
460         return true;
461     }
462     return false;
463   };
465   // Checks if |node| is an anchor to be opened in the current tab.
466   var isInternaLink_ = function(node) {
467     if (!(node instanceof HTMLAnchorElement))
468       return false;
470     // Anchor with href='javascript://.....' will be opened in the current tab
471     // for simplicity.
472     if (node.href.indexOf('javascript:') == 0)
473       return true;
475     // UIWebView will take care of the following cases.
476     //
477     // - If the given browsing context name is the empty string or '_self', then
478     //   the chosen browsing context must be the current one.
479     //
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.
484     //
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.
488     //
489     // Here an undefined target is considered in the same way as an empty
490     // target.
491     if (node.target === undefined || node.target === '' ||
492         node.target === '_self' || node.target === '_parent' ||
493         node.target === '_top') {
494       return true;
495     }
497     // A new browsing context is being requested for an '_blank' target.
498     if (node.target === '_blank')
499       return false;
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
504     // being requested.
505     return hasFrame_(window, node.target);
506   };
508   var getTargetLink_ = function(target) {
509     var node = target;
510     // Find the closest ancester that is a link.
511     while (node) {
512       if (node instanceof HTMLAnchorElement)
513         break;
514       node = node.parentNode;
515     }
516     return node;
517   };
519   var setExternalRequest_ = function(href, target) {
520     if (typeof(target) == 'undefined' || target == '_blank' || target == '') {
521       target = '' + Date.now() + '-' + Math.random();
522     }
523     if (typeof(href) == 'undefined') {
524       // W3C recommended behavior.
525       href = 'about:blank';
526     }
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
531     // CRWWebController.
532     invokeOnHostImmediate_({'command': 'externalRequest',
533                                'href': href,
534                              'target': target,
535                      'referrerPolicy': getReferrerPolicy_()});
536   };
538   var resetExternalRequest_ = function() {
539     invokeOnHost_({'command': 'resetExternalRequest'});
540   };
542   var clickBubbleListener_ = function(evt) {
543     if (evt['defaultPrevented']) {
544       resetExternalRequest_();
545     }
546     // Remove the listener.
547     evt.currentTarget.removeEventListener(
548         'click', clickBubbleListener_, false);
549   };
551   var getComputedWebkitTouchCallout_ = function(element) {
552     return window.getComputedStyle(element, null)['webkitTouchCallout'];
553   };
555   /**
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.
560    */
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())
565       return;
567     document.addEventListener('click', function(evt) {
568       var node = getTargetLink_(evt.target);
570       if (!node)
571         return;
573       if (isInternaLink_(node)) {
574         return;
575       }
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;
586         } else {
587           break;
588         }
589       }
590     }, true);
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);
597       if (!node)
598         return;
600       if (isInternaLink_(node)) {
601         if (evt['defaultPrevented'])
602           return;
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);
608         }
609         return;
610       } else {
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_();
615         else
616           setExternalRequest_(node.href, node.target);
617       }
618     }, false);
620     // Capture form submit actions.
621     document.addEventListener('submit', function(evt) {
622       if (evt['defaultPrevented'])
623         return;
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.
631       if (!action)
632         action = document.location.href;
633       invokeOnHost_({
634                'command': 'document.submit',
635               'formName': __gCrWeb.common.getFormIdentifier(evt.srcElement),
636                   'href': __gCrWeb['getFullyQualifiedURL'](action),
637           'targetsFrame': targetsFrame
638       });
639     }, false);
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();
647     return true;
648   };
650   __gCrWeb.core.documentInject();
651 }());  // End of anonymous object